diff --git a/crates/binding/src/js_hook.rs b/crates/binding/src/js_hook.rs index fcf0615ea..f167655b0 100644 --- a/crates/binding/src/js_hook.rs +++ b/crates/binding/src/js_hook.rs @@ -64,6 +64,10 @@ pub struct JsHooks { ts_type = "(source: string, importer: string, { isEntry: bool }) => Promise<{ id: string }>;" )] pub resolve_id: Option, + #[napi( + ts_type = "(content: { content: string, type: 'css' | 'js' }, path: string) => Promise<{ content: string, type: 'css' | 'js' } | void> | void;" + )] + pub transform: Option, } pub struct TsFnHooks { @@ -74,6 +78,7 @@ pub struct TsFnHooks { pub resolve_id: Option>>, pub _on_generate_file: Option>, + pub transform: Option>>, } impl TsFnHooks { @@ -97,6 +102,9 @@ impl TsFnHooks { _on_generate_file: hooks._on_generate_file.as_ref().map(|hook| unsafe { ThreadsafeFunction::from_napi_value(env.raw(), hook.raw()).unwrap() }), + transform: hooks.transform.as_ref().map(|hook| unsafe { + ThreadsafeFunction::from_napi_value(env.raw(), hook.raw()).unwrap() + }), } } } @@ -125,3 +133,10 @@ pub struct ResolveIdResult { pub struct ResolveIdParams { pub is_entry: bool, } + +#[napi(object, use_nullable = true)] +pub struct TransformResult { + pub content: String, + #[napi(js_name = "type")] + pub content_type: String, +} diff --git a/crates/binding/src/js_plugin.rs b/crates/binding/src/js_plugin.rs index e42b76762..3dbe1ffe6 100644 --- a/crates/binding/src/js_plugin.rs +++ b/crates/binding/src/js_plugin.rs @@ -1,17 +1,20 @@ use std::path::PathBuf; use std::sync::Arc; -use crate::js_hook::{LoadResult, ResolveIdParams, ResolveIdResult, TsFnHooks, WriteFile}; - -pub struct JsPlugin { - pub hooks: TsFnHooks, -} use anyhow::{anyhow, Result}; use mako::ast::file::{Content, JsContent}; use mako::compiler::Context; use mako::plugin::{Plugin, PluginGenerateEndParams, PluginLoadParam, PluginResolveIdParams}; use mako::resolve::{ExternalResource, Resolution, ResolvedResource, ResolverResource}; +use crate::js_hook::{ + LoadResult, TransformResult, ResolveIdParams, ResolveIdResult, TsFnHooks, WriteFile, +}; + +pub struct JsPlugin { + pub hooks: TsFnHooks, +} + impl Plugin for JsPlugin { fn name(&self) -> &str { "js_plugin" @@ -112,4 +115,41 @@ impl Plugin for JsPlugin { } Ok(()) } + + fn load_transform( + &self, + content: &mut Content, + path: &str, + _context: &Arc, + ) -> Result> { + if let Some(hook) = &self.hooks.transform { + let content_str = match content { + Content::Js(js_content) => js_content.content.clone(), + Content::Css(css_content) => css_content.clone(), + _ => return Ok(None), + }; + + let result: Option = hook.call((content_str, path.to_string()))?; + + if let Some(result) = result { + match result.content_type.as_str() { + "js" | "ts" => { + return Ok(Some(Content::Js(JsContent { + content: result.content, + is_jsx: false, + }))) + } + "jsx" | "tsx" => { + return Ok(Some(Content::Js(JsContent { + content: result.content, + is_jsx: true, + }))) + } + "css" => return Ok(Some(Content::Css(result.content))), + _ => return Err(anyhow!("Unsupported content type: {}", result.content_type)), + } + } + } + Ok(None) + } } diff --git a/crates/mako/src/build.rs b/crates/mako/src/build.rs index a520b81b5..b1af48f53 100644 --- a/crates/mako/src/build.rs +++ b/crates/mako/src/build.rs @@ -271,7 +271,12 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( ) -> Result { // 1. load let mut file = file.clone(); - let content = load::Load::load(&file, context.clone())?; + let mut content = load::Load::load(&file, context.clone())?; + let content = context.plugin_driver.load_transform( + &mut content, + &file.path.to_string_lossy(), + &context, + )?; file.set_content(content); // 2. parse diff --git a/crates/mako/src/plugin.rs b/crates/mako/src/plugin.rs index 5d1d54225..42cbfb0aa 100644 --- a/crates/mako/src/plugin.rs +++ b/crates/mako/src/plugin.rs @@ -58,6 +58,15 @@ pub trait Plugin: Any + Send + Sync { Ok(None) } + fn load_transform( + &self, + _content: &mut Content, + _path: &str, + _context: &Arc, + ) -> Result> { + Ok(None) + } + fn resolve_id( &self, _source: &str, @@ -377,4 +386,19 @@ impl PluginDriver { Ok(()) } + + pub fn load_transform( + &self, + content: &mut Content, + path: &str, + context: &Arc, + ) -> Result { + let mut content = content.clone(); + for plugin in &self.plugins { + if let Some(transformed) = plugin.load_transform(&mut content, path, context)? { + content = transformed; + } + } + Ok(content) + } } diff --git a/e2e/fixtures/plugins/expect.js b/e2e/fixtures/plugins/expect.js index 433942d89..f62a275c6 100644 --- a/e2e/fixtures/plugins/expect.js +++ b/e2e/fixtures/plugins/expect.js @@ -12,3 +12,6 @@ assert(content.includes(`children: ".hoo"`), `relative plugin works`); // resolve_id hook assert(content.includes(`resolve_id mocked`), `resolve_id hook works`); assert(content.includes(`module.exports = resolve_id_external;`), `resolve_id hook with external works`); + +// transform hook +assert(content.includes(`console.log('transform_2_1');`), `transform hook works`); diff --git a/e2e/fixtures/plugins/plugins.config.js b/e2e/fixtures/plugins/plugins.config.js index 2dbeafc23..e4d9fc99d 100644 --- a/e2e/fixtures/plugins/plugins.config.js +++ b/e2e/fixtures/plugins/plugins.config.js @@ -1,36 +1,60 @@ - module.exports = [ { async load(path) { - if (path.endsWith('foo.bar')) { + if (path.endsWith("foo.bar")) { return { content: `export default () => foo.bar;`, - type: 'jsx', + type: "jsx", }; } - } + }, }, { async loadInclude(path) { - return path.endsWith('.bar'); + return path.endsWith(".bar"); }, async load() { return { content: `export default () => .bar;`, - type: 'jsx', + type: "jsx", }; - } + }, }, { async resolveId(source, importer, options) { - console.log('resolveId', source, importer, options); - if (source === 'resolve_id') { - return { id: require('path').join(__dirname, 'resolve_id_mock.js'), external: false }; + console.log("resolveId", source, importer, options); + if (source === "resolve_id") { + return { + id: require("path").join(__dirname, "resolve_id_mock.js"), + external: false, + }; } - if (source === 'resolve_id_external') { - return { id: 'resolve_id_external', external: true }; + if (source === "resolve_id_external") { + return { id: "resolve_id_external", external: true }; } return null; - } + }, + }, + { + async transform(code, id) { + if (id.endsWith("transform.ts")) { + console.log("transform", code, id); + return { + content: code.replace("transform", "transform_1"), + type: "ts", + }; + } + }, + }, + { + async transform(code, id) { + if (id.endsWith("transform.ts")) { + console.log("transform", code, id); + return { + content: code.replace("transform", "transform_2"), + type: "ts", + }; + } + }, }, ]; diff --git a/e2e/fixtures/plugins/src/index.tsx b/e2e/fixtures/plugins/src/index.tsx index b53b90ae7..ffa331e06 100644 --- a/e2e/fixtures/plugins/src/index.tsx +++ b/e2e/fixtures/plugins/src/index.tsx @@ -4,3 +4,6 @@ console.log(require('./foo.haha')); console.log(require('./foo.hoo')); console.log(require('resolve_id')); console.log(require('resolve_id_external')); + +// transform +console.log(require('./transform')); diff --git a/e2e/fixtures/plugins/src/transform.ts b/e2e/fixtures/plugins/src/transform.ts new file mode 100644 index 000000000..5fe06ed7d --- /dev/null +++ b/e2e/fixtures/plugins/src/transform.ts @@ -0,0 +1,2 @@ + +console.log('transform'); diff --git a/packages/mako/binding.d.ts b/packages/mako/binding.d.ts index b99643f94..5bd858867 100644 --- a/packages/mako/binding.d.ts +++ b/packages/mako/binding.d.ts @@ -57,6 +57,10 @@ export interface JsHooks { importer: string, { isEntry: bool }, ) => Promise<{ id: string }>; + transform?: ( + content: { content: string; type: 'css' | 'js' }, + path: string, + ) => Promise<{ content: string; type: 'css' | 'js' } | void> | void; } export interface WriteFile { path: string; @@ -73,6 +77,10 @@ export interface ResolveIdResult { export interface ResolveIdParams { isEntry: boolean; } +export interface LoadTransformResult { + content: string; + type: string; +} export interface BuildParams { root: string; config: {