Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support unplugin context (again) #1741

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
Comment on lines +39 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

改进错误和警告的处理机制

当前的 warnerror 方法实现过于简单,建议:

  1. 集成到项目的日志系统中
  2. 添加错误级别
  3. 支持结构化日志
#[napi]
impl PluginContext {
    #[napi]
    pub fn warn(&self, msg: String) {
-        println!("WARN: {}", msg)
+        log::warn!("{}", msg)
    }
    #[napi]
    pub fn error(&self, msg: String) {
-        println!("ERROR: {}", msg)
+        log::error!("{}", msg)
    }
}

Committable suggestion skipped: line range outside the PR's diff.

#[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');
}
Comment on lines +3 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

未使用 path.endsWith 的结果

load 方法中,调用了 path.endsWith('.hoo'),但没有使用其返回值。建议使用返回值以实现预期的逻辑,例如:

if (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;
},
Comment on lines +3 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修复 loadInclude 方法中的逻辑错误

loadInclude 方法中的 path.endsWith('.hoo') 表达式的结果未被使用,这会导致该方法总是返回 true

建议修改为:

async loadInclude(path) {
  // this.warn('loadInclude: ' + path);
- path.endsWith('.hoo');
- return true;
+ return path.endsWith('.hoo');
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async loadInclude(path) {
// this.warn('loadInclude: ' + path);
path.endsWith('.hoo');
return true;
},
async loadInclude(path) {
// this.warn('loadInclude: ' + path);
return path.endsWith('.hoo');
},

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
Loading