Skip to content

Commit

Permalink
feat: support unplugin context (#1728)
Browse files Browse the repository at this point in the history
* feat: support plugin context

* fix: napi context

* chore: revert changes

* chore: improve

* feat: add error

* feat: warn and error support object

* feat: support emit_file

* ci: fix test

* chore: improve

* chore: update test

* chore: format

* chore: don't support add watch file

* feat: load and transform adapter, and add unplugin-replace example

* chore: test unplugin-icons

* chore: update pnpm-lock.yaml

* docs: improve

---------

Co-authored-by: xusd320 <[email protected]>
  • Loading branch information
sorrycc and xusd320 authored Dec 24, 2024
1 parent 99012b2 commit 3dd6d9d
Show file tree
Hide file tree
Showing 23 changed files with 621 additions and 83 deletions.
27 changes: 15 additions & 12 deletions crates/binding/src/js_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use napi::NapiRaw;
use napi_derive::napi;
use serde_json::Value;

use crate::js_plugin::PluginContext;
use crate::threadsafe_function::ThreadsafeFunction;

#[napi(object)]
Expand Down Expand Up @@ -81,19 +82,21 @@ pub struct JsHooks {
pub transform_include: Option<JsFunction>,
}

type ResolveIdFuncParams = (PluginContext, String, String, ResolveIdParams);

pub struct TsFnHooks {
pub build_start: Option<ThreadsafeFunction<(), ()>>,
pub build_end: Option<ThreadsafeFunction<(), ()>>,
pub write_bundle: Option<ThreadsafeFunction<(), ()>>,
pub generate_end: Option<ThreadsafeFunction<Value, ()>>,
pub load: Option<ThreadsafeFunction<String, Option<LoadResult>>>,
pub load_include: Option<ThreadsafeFunction<String, Option<bool>>>,
pub watch_changes: Option<ThreadsafeFunction<(String, WatchChangesParams), ()>>,
pub resolve_id:
Option<ThreadsafeFunction<(String, String, ResolveIdParams), Option<ResolveIdResult>>>,
pub _on_generate_file: Option<ThreadsafeFunction<WriteFile, ()>>,
pub transform: Option<ThreadsafeFunction<(String, String), Option<TransformResult>>>,
pub transform_include: Option<ThreadsafeFunction<String, Option<bool>>>,
pub build_start: Option<ThreadsafeFunction<PluginContext, ()>>,
pub build_end: Option<ThreadsafeFunction<PluginContext, ()>>,
pub write_bundle: Option<ThreadsafeFunction<PluginContext, ()>>,
pub generate_end: Option<ThreadsafeFunction<(PluginContext, Value), ()>>,
pub load: Option<ThreadsafeFunction<(PluginContext, String), Option<LoadResult>>>,
pub load_include: Option<ThreadsafeFunction<(PluginContext, String), Option<bool>>>,
pub watch_changes: Option<ThreadsafeFunction<(PluginContext, String, WatchChangesParams), ()>>,
pub resolve_id: Option<ThreadsafeFunction<ResolveIdFuncParams, Option<ResolveIdResult>>>,
pub _on_generate_file: Option<ThreadsafeFunction<(PluginContext, WriteFile), ()>>,
pub transform:
Option<ThreadsafeFunction<(PluginContext, String, String), Option<TransformResult>>>,
pub transform_include: Option<ThreadsafeFunction<(PluginContext, String), Option<bool>>>,
}

impl TsFnHooks {
Expand Down
119 changes: 93 additions & 26 deletions crates/binding/src/js_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 napi_derive::napi;

use crate::js_hook::{
LoadResult, ResolveIdParams, ResolveIdResult, TransformResult, TsFnHooks, WatchChangesParams,
Expand All @@ -27,6 +28,29 @@ fn content_from_result(result: TransformResult) -> Result<Content> {
}
}

#[napi]
pub struct PluginContext {
context: Arc<Context>,
}

#[napi]
impl PluginContext {
#[napi]
pub fn warn(&self, msg: String) {
println!("WARN: {}", msg)
}
#[napi]
pub fn error(&self, msg: String) {
println!("ERROR: {}", msg)
}
#[napi]
pub fn emit_file(&self, origin_path: String, output_path: String) {
let mut assets_info = self.context.assets_info.lock().unwrap();
assets_info.insert(origin_path, output_path);
drop(assets_info);
}
}

pub struct JsPlugin {
pub hooks: TsFnHooks,
pub name: Option<String>,
Expand All @@ -42,27 +66,33 @@ impl Plugin for JsPlugin {
self.enforce.as_deref()
}

fn build_start(&self, _context: &Arc<Context>) -> Result<()> {
fn build_start(&self, context: &Arc<Context>) -> Result<()> {
if let Some(hook) = &self.hooks.build_start {
hook.call(())?
hook.call(PluginContext {
context: context.clone(),
})?
}
Ok(())
}

fn load(&self, param: &PluginLoadParam, _context: &Arc<Context>) -> Result<Option<Content>> {
fn load(&self, param: &PluginLoadParam, context: &Arc<Context>) -> Result<Option<Content>> {
if let Some(hook) = &self.hooks.load {
if self.hooks.load_include.is_some()
&& self
.hooks
.load_include
.as_ref()
.unwrap()
.call(param.file.path.to_string_lossy().to_string())?
== Some(false)
&& self.hooks.load_include.as_ref().unwrap().call((
PluginContext {
context: context.clone(),
},
param.file.path.to_string_lossy().to_string(),
))? == Some(false)
{
return Ok(None);
}
let x: Option<LoadResult> = hook.call(param.file.path.to_string_lossy().to_string())?;
let x: Option<LoadResult> = hook.call((
PluginContext {
context: context.clone(),
},
param.file.path.to_string_lossy().to_string(),
))?;
if let Some(x) = x {
return content_from_result(TransformResult {
content: x.content,
Expand All @@ -79,10 +109,13 @@ impl Plugin for JsPlugin {
source: &str,
importer: &str,
params: &PluginResolveIdParams,
_context: &Arc<Context>,
context: &Arc<Context>,
) -> Result<Option<ResolverResource>> {
if let Some(hook) = &self.hooks.resolve_id {
let x: Option<ResolveIdResult> = hook.call((
PluginContext {
context: context.clone(),
},
source.to_string(),
importer.to_string(),
ResolveIdParams {
Expand Down Expand Up @@ -110,21 +143,31 @@ impl Plugin for JsPlugin {
Ok(None)
}

fn generate_end(&self, param: &PluginGenerateEndParams, _context: &Arc<Context>) -> Result<()> {
fn generate_end(&self, param: &PluginGenerateEndParams, context: &Arc<Context>) -> Result<()> {
// keep generate_end for compatibility
// since build_end does not have none error params in unplugin's api spec
if let Some(hook) = &self.hooks.generate_end {
hook.call(serde_json::to_value(param)?)?
hook.call((
PluginContext {
context: context.clone(),
},
serde_json::to_value(param)?,
))?
}
if let Some(hook) = &self.hooks.build_end {
hook.call(())?
hook.call(PluginContext {
context: context.clone(),
})?
}
Ok(())
}

fn watch_changes(&self, id: &str, event: &str, _context: &Arc<Context>) -> Result<()> {
fn watch_changes(&self, id: &str, event: &str, context: &Arc<Context>) -> Result<()> {
if let Some(hook) = &self.hooks.watch_changes {
hook.call((
PluginContext {
context: context.clone(),
},
id.to_string(),
WatchChangesParams {
event: event.to_string(),
Expand All @@ -134,19 +177,31 @@ impl Plugin for JsPlugin {
Ok(())
}

fn write_bundle(&self, _context: &Arc<Context>) -> Result<()> {
fn write_bundle(&self, context: &Arc<Context>) -> Result<()> {
if let Some(hook) = &self.hooks.write_bundle {
hook.call(())?
hook.call(PluginContext {
context: context.clone(),
})?
}
Ok(())
}

fn before_write_fs(&self, path: &std::path::Path, content: &[u8]) -> Result<()> {
fn before_write_fs(
&self,
path: &std::path::Path,
content: &[u8],
context: &Arc<Context>,
) -> Result<()> {
if let Some(hook) = &self.hooks._on_generate_file {
hook.call(WriteFile {
path: path.to_string_lossy().to_string(),
content: content.to_vec(),
})?;
hook.call((
PluginContext {
context: context.clone(),
},
WriteFile {
path: path.to_string_lossy().to_string(),
content: content.to_vec(),
},
))?;
}
Ok(())
}
Expand All @@ -155,10 +210,16 @@ impl Plugin for JsPlugin {
&self,
content: &mut Content,
path: &str,
_context: &Arc<Context>,
context: &Arc<Context>,
) -> Result<Option<Content>> {
if let Some(hook) = &self.hooks.transform_include {
if hook.call(path.to_string())? == Some(false) {
if hook.call((
PluginContext {
context: context.clone(),
},
path.to_string(),
))? == Some(false)
{
return Ok(None);
}
}
Expand All @@ -170,7 +231,13 @@ impl Plugin for JsPlugin {
_ => return Ok(None),
};

let result: Option<TransformResult> = hook.call((content_str, path.to_string()))?;
let result: Option<TransformResult> = hook.call((
PluginContext {
context: context.clone(),
},
content_str,
path.to_string(),
))?;

if let Some(result) = result {
return content_from_result(result).map(Some);
Expand Down
10 changes: 8 additions & 2 deletions crates/mako/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ pub trait Plugin: Any + Send + Sync {
Ok(())
}

fn before_write_fs(&self, _path: &Path, _content: &[u8]) -> Result<()> {
fn before_write_fs(
&self,
_path: &Path,
_content: &[u8],
_context: &Arc<Context>,
) -> Result<()> {
Ok(())
}

Expand Down Expand Up @@ -422,9 +427,10 @@ impl PluginDriver {
&self,
path: P,
content: C,
context: &Arc<Context>,
) -> Result<()> {
for p in &self.plugins {
p.before_write_fs(path.as_ref(), content.as_ref())?;
p.before_write_fs(path.as_ref(), content.as_ref(), context)?;
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/mako/src/plugins/bundless_compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl BundlessCompiler {

self.context
.plugin_driver
.before_write_fs(&to, content.as_ref())
.before_write_fs(&to, content.as_ref(), &self.context)
.unwrap();

if !self.context.config.output.skip_write {
Expand Down
14 changes: 8 additions & 6 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,6 @@ Notice: When using `"node"`, you also need to set `dynamicImportToRequire` to `t
Specify the plugins to use.

```ts
// JSHooks
{
name?: string;
enforce?: "pre" | "post";
Expand All @@ -599,12 +598,15 @@ Specify the plugins to use.
}
```

JSHooks is a set of hook functions used to extend the compilation process of Mako.
And you can also use this methods in hook functions.

- `name`, plugin name
- `buildStart`, called before Build starts
- `load`, used to load files, return file content and type, type supports `css`, `js`, `jsx`, `ts`, `tsx`
- `generateEnd`, called after Generate completes, `isFirstCompile` can be used to determine if it is the first compilation, `time` is the compilation time, and `stats` is the compilation statistics information
- `this.emitFile({ type: 'asset', fileName: string, source: string | Uint8Array })`, emit a file
- `this.warn(message: string)`, emit a warning
- `this.error(message: string)`, emit a error
- `this.parse(code: string)`, parse the code (CURRENTLY NOT SUPPORTED)
- `this.addWatchFile(filePath: string)`, add a watch file (CURRENTLY NOT SUPPORTED)

Plugins is compatible with [unplugin](https://unplugin.unjs.io/), so you can use plugins from unplugin like [unplugin-icons](https://github.com/unplugin/unplugin-icons), [unplugin-replace](https://github.com/unplugin/unplugin-replace) and so on.

### progress

Expand Down
14 changes: 8 additions & 6 deletions docs/config.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,6 @@ import(/* webpackIgnore: true */ "./foo");
指定使用的插件。

```ts
// JSHooks
{
name?: string;
enforce?: "pre" | "post";
Expand All @@ -597,12 +596,15 @@ import(/* webpackIgnore: true */ "./foo");
}
```

JSHooks 是一组用来扩展 Mako 编译过程的钩子函数
你还可以在 hook 函数里用以下方法

- `name`,插件名称
- `buildStart`,构建开始前调用
- `load`,用于加载文件,返回文件内容和类型,类型支持 `css``js``jsx``ts``tsx`
- `generateEnd`,生成完成后调用,`isFirstCompile` 可用于判断是否为首次编译,`time` 为编译时间,`stats` 是编译统计信息
- `this.emitFile({ type: 'asset', fileName: string, source: string | Uint8Array })`, 添加文件到输出目录
- `this.warn(message: string)`, 添加一个警告
- `this.error(message: string)`, 添加一个错误
- `this.parse(code: string)`, 解析代码 (CURRENTLY NOT SUPPORTED)
- `this.addWatchFile(filePath: string)`, 添加一个监听文件 (CURRENTLY NOT SUPPORTED)

Plugins 兼容 [unplugin](https://unplugin.unjs.io/),所以你可以使用 unplugin 的插件,比如 [unplugin-icons](https://github.com/unplugin/unplugin-icons), [unplugin-replace](https://github.com/unplugin/unplugin-replace) 等。

### progress

Expand Down
7 changes: 7 additions & 0 deletions e2e/fixtures/plugins.context/expect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const assert = require("assert");

const { parseBuildResult, trim, moduleReg } = require("../../../scripts/test-utils");
const { files } = parseBuildResult(__dirname);

const content = files["index.js"];
assert.strictEqual(files['test.txt'], 'test');
6 changes: 6 additions & 0 deletions e2e/fixtures/plugins.context/mako.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins": [
"./plugin"
],
"minify": false
}
6 changes: 6 additions & 0 deletions e2e/fixtures/plugins.context/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

module.exports = {
async load(path) {
path.endsWith('.hoo');
}
};
27 changes: 27 additions & 0 deletions e2e/fixtures/plugins.context/plugins.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = [
{
async loadInclude(path) {
// this.warn('loadInclude: ' + path);
path.endsWith('.hoo');
return true;
},
async load(path) {
if (path.endsWith('.hoo')) {
this.warn('load: ' + path);
this.warn({
message: 'test warn with object',
});
this.error('error: ' + path);
this.emitFile({
type: 'asset',
fileName: 'test.txt',
source: 'test',
});
return {
content: `export default () => <Foooo>.hoo</Foooo>;`,
type: 'jsx',
};
}
}
},
];
1 change: 1 addition & 0 deletions e2e/fixtures/plugins.context/src/foo.hoo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// should be handled with load hook
1 change: 1 addition & 0 deletions e2e/fixtures/plugins.context/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(require('./foo.hoo'));
Loading

0 comments on commit 3dd6d9d

Please sign in to comment.