Skip to content

Commit

Permalink
feat: support js process module hook (#1997)
Browse files Browse the repository at this point in the history
* feat: support js process module

* chore: minor version bump

---------

Co-authored-by: brightwu <1521488775@qq.com>
  • Loading branch information
shulandmimi and wre232114 authored Dec 17, 2024
1 parent f967c47 commit 6b84912
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-radios-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@farmfe/core": minor
---

js plugin supports process module hook
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ pub mod transform_html;
pub mod update_finished;
pub mod update_modules;
pub mod write_plugin_cache;
pub mod process_module;
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
use std::sync::Arc;

use farmfe_core::{
config::config_regex::ConfigRegex,
context::CompilationContext,
error::{CompilationError, Result},
module::{ModuleId, ModuleMetaData, ModuleType},
plugin::PluginProcessModuleHookParam,
serde::{Deserialize, Serialize},
swc_common::SourceMap,
swc_ecma_ast::EsVersion,
swc_ecma_parser::{EsSyntax, Syntax},
};
use farmfe_toolkit::{
css::{codegen_css_stylesheet, parse_css_stylesheet, ParseCssModuleResult},
html::{codegen_html_document, parse_html_document},
script::{codegen_module, parse_module, ParseScriptModuleResult},
};
use napi::{bindgen_prelude::FromNapiValue, NapiRaw};

use crate::{
new_js_plugin_hook,
plugin_adapters::js_plugin_adapter::thread_safe_js_plugin_hook::ThreadSafeJsPluginHook,
};

#[napi(object)]
pub struct JsPluginProcessModuleHookFilters {
pub module_types: Vec<String>,
pub resolved_paths: Vec<String>,
}

pub struct PluginProcessModuleHookFilters {
pub module_types: Vec<ModuleType>,
pub resolved_paths: Vec<ConfigRegex>,
}

impl From<JsPluginProcessModuleHookFilters> for PluginProcessModuleHookFilters {
fn from(value: JsPluginProcessModuleHookFilters) -> Self {
Self {
module_types: value.module_types.into_iter().map(|ty| ty.into()).collect(),
resolved_paths: value
.resolved_paths
.into_iter()
.map(|p| ConfigRegex::new(&p))
.collect(),
}
}
}

pub struct JsPluginProcessModuleHook {
tsfn: ThreadSafeJsPluginHook,
filters: PluginProcessModuleHookFilters,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "farmfe_core::serde", rename_all = "camelCase")]
pub struct PluginProcessModuleHookResult {
content: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(crate = "farmfe_core::serde", rename_all = "camelCase")]
struct CompatiblePluginProcessModuleHookParams {
module_id: ModuleId,
module_type: ModuleType,
content: String,
}

fn format_module_metadata_to_code(metadata: &mut ModuleMetaData) -> Result<Option<String>> {
Ok(match metadata {
ModuleMetaData::Script(script_module_meta_data) => {
let cm = Arc::new(SourceMap::default());
let code = codegen_module(
&script_module_meta_data.ast,
EsVersion::latest(),
cm,
None,
false,
None,
)
.map_err(|err| CompilationError::GenericError(err.to_string()))?;

Some(String::from_utf8_lossy(&code).to_string())
}
ModuleMetaData::Css(css_module_meta_data) => {
let (code, _) = codegen_css_stylesheet(&css_module_meta_data.ast, None, false);

Some(code)
}
ModuleMetaData::Html(html_module_meta_data) => {
Some(codegen_html_document(&html_module_meta_data.ast, false))
}
ModuleMetaData::Custom(_) => None,
})
}

fn convert_code_to_metadata(params: &mut PluginProcessModuleHookParam, code: String) -> Result<()> {
let filename = params.module_id.to_string();
match params.meta {
ModuleMetaData::Script(script_module_meta_data) => {
let ParseScriptModuleResult { ast, comments } = parse_module(
&filename,
&code,
// TODO: config should from config or process_module custom config
match params.module_type {
ModuleType::Js | ModuleType::Ts => Syntax::Es(Default::default()),
ModuleType::Jsx | ModuleType::Tsx => Syntax::Es(EsSyntax {
jsx: true,
..Default::default()
}),
_ => Syntax::Es(Default::default()),
},
Default::default(),
)?;

script_module_meta_data.ast = ast;
script_module_meta_data.comments = comments.into()
}
ModuleMetaData::Css(css_module_meta_data) => {
let ParseCssModuleResult { ast, comments } = parse_css_stylesheet(&filename, Arc::new(code))?;

css_module_meta_data.ast = ast;
css_module_meta_data.comments = comments.into();
}
ModuleMetaData::Html(html_module_meta_data) => {
let v = parse_html_document(&filename, Arc::new(code))?;

html_module_meta_data.ast = v;
}
ModuleMetaData::Custom(_) => {
return Ok(());
}
}

Ok(())
}

impl JsPluginProcessModuleHook {
new_js_plugin_hook!(
PluginProcessModuleHookFilters,
JsPluginProcessModuleHookFilters,
CompatiblePluginProcessModuleHookParams,
PluginProcessModuleHookResult
);

pub fn call(
&self,
param: &mut PluginProcessModuleHookParam,
ctx: Arc<CompilationContext>,
) -> Result<Option<()>> {
if self.filters.module_types.contains(param.module_type)
|| self
.filters
.resolved_paths
.iter()
.any(|m| m.is_match(param.module_id.to_string().as_str()))
{
let Some(result) = format_module_metadata_to_code(param.meta)? else {
return Ok(None);
};

let Some(result) = self
.tsfn
.call::<CompatiblePluginProcessModuleHookParams, PluginProcessModuleHookResult>(
CompatiblePluginProcessModuleHookParams {
module_id: param.module_id.clone(),
module_type: param.module_type.clone(),
content: result,
},
ctx,
None,
)?
else {
return Ok(None);
};

convert_code_to_metadata(param, result.content)?;

return Ok(None);
}

Ok(None)
}
}
27 changes: 21 additions & 6 deletions crates/node/src/plugin_adapters/js_plugin_adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use self::hooks::{
finish::JsPluginFinishHook,
load::JsPluginLoadHook,
plugin_cache_loaded::JsPluginPluginCacheLoadedHook,
process_module::JsPluginProcessModuleHook,
render_resource_pot::JsPluginRenderResourcePotHook,
render_start::JsPluginRenderStartHook,
resolve::JsPluginResolveHook,
Expand Down Expand Up @@ -58,6 +59,7 @@ pub struct JsPluginAdapter {
js_finalize_resources_hook: Option<JsPluginFinalizeResourcesHook>,
js_transform_html_hook: Option<JsPluginTransformHtmlHook>,
js_update_finished_hook: Option<JsPluginUpdateFinishedHook>,
js_process_module_hook: Option<JsPluginProcessModuleHook>,
}

impl JsPluginAdapter {
Expand Down Expand Up @@ -93,6 +95,8 @@ impl JsPluginAdapter {
get_named_property::<JsObject>(env, &js_plugin_object, "transformHtml").ok();
let update_finished_obj =
get_named_property::<JsObject>(env, &js_plugin_object, "updateFinished").ok();
let process_module_obj =
get_named_property::<JsObject>(env, &js_plugin_object, "processModule").ok();

Ok(Self {
name,
Expand Down Expand Up @@ -120,6 +124,8 @@ impl JsPluginAdapter {
.map(|obj| JsPluginTransformHtmlHook::new(env, obj)),
js_update_finished_hook: update_finished_obj
.map(|obj| JsPluginUpdateFinishedHook::new(env, obj)),
js_process_module_hook: process_module_obj
.map(|obj| JsPluginProcessModuleHook::new(env, obj)),
})
}

Expand Down Expand Up @@ -243,6 +249,18 @@ impl Plugin for JsPluginAdapter {
}
}

fn process_module(
&self,
param: &mut farmfe_core::plugin::PluginProcessModuleHookParam,
context: &Arc<CompilationContext>,
) -> Result<Option<()>> {
if let Some(ref js_process_module_hook) = self.js_process_module_hook {
return js_process_module_hook.call(param, context.clone());
}

Ok(None)
}

fn build_end(&self, context: &Arc<CompilationContext>) -> Result<Option<()>> {
if let Some(js_build_end_hook) = &self.js_build_end_hook {
js_build_end_hook.call(EmptyPluginHookParam {}, context.clone())?;
Expand Down Expand Up @@ -397,20 +415,17 @@ impl Plugin for JsPluginAdapter {
}

pub fn get_named_property<T: FromNapiValue>(env: &Env, obj: &JsObject, field: &str) -> Result<T> {
// TODO: maybe can prompt for the name of the plugin
if obj.has_named_property(field).map_err(|e| {
CompilationError::NAPIError(format!(
"Get field {field} of config object failed. {e:?}"
))
CompilationError::NAPIError(format!("Get field {field} of config object failed. {e:?}"))
})? {
unsafe {
T::from_napi_value(
env.raw(),
obj
.get_named_property::<JsUnknown>(field)
.map_err(|e| {
CompilationError::NAPIError(format!(
"Get field {field} of config object failed. {e:?}"
))
CompilationError::NAPIError(format!("Get field {field} of config object failed. {e:?}"))
})?
.raw(),
)
Expand Down
3 changes: 2 additions & 1 deletion examples/node-lazy-compile/e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from 'vitest';
import { test } from 'vitest';
import { watchProjectAndTest } from '../../e2e/vitestSetup.js';
import { basename, dirname } from 'path';
import { fileURLToPath } from 'url';
Expand All @@ -23,6 +23,7 @@ test(`e2e tests - ${name}`, async () => {
},
command
);

// preview build
await runTest('preview');
await runTest('watch');
Expand Down
8 changes: 7 additions & 1 deletion examples/node-lazy-compile/farm.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineConfig } from '@farmfe/core';

const lazyPort = 20000 + (Math.random() * 10000 >> 0);

export default defineConfig({
compilation: {
input: {
Expand All @@ -13,6 +15,10 @@ export default defineConfig({
targetEnv: 'node',
entryFilename: '[entryName].mjs',
filename: '[name].mjs'
}
},

},
server: {
port: lazyPort,
}
});
4 changes: 4 additions & 0 deletions packages/core/binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const enum JsPluginTransformHtmlHookOrder {
Normal = 1,
Post = 2
}
export interface JsPluginProcessModuleHookFilters {
moduleTypes: Array<string>
resolvedPaths: Array<string>
}
export interface WatchDiffResult {
add: Array<string>
remove: Array<string>
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/plugin/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ export function convertPlugin(plugin: JsPlugin): void {
}
}

if (plugin.processModule) {
plugin.processModule.filters ??= {};
plugin.processModule.filters.moduleTypes ??= [];
plugin.processModule.filters.resolvedPaths ??= [];
}

if (plugin.renderResourcePot) {
plugin.renderResourcePot.filters ??= {};

Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/plugin/type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Compiler, ResolvedUserConfig, Server, UserConfig } from '../index.js';
import {
Config,
ModuleType,
PluginLoadHookParam,
PluginLoadHookResult,
PluginResolveHookParam,
Expand All @@ -11,7 +12,7 @@ import {

// https://stackoverflow.com/questions/61047551/typescript-union-of-string-and-string-literals
// eslint-disable-next-line @typescript-eslint/ban-types
type LiteralUnion<T extends string> = T | (string & {});
export type LiteralUnion<T extends string> = T | (string & {});

type ResourcePotType = LiteralUnion<
'runtime' | 'js' | 'css' | 'html' | 'asset'
Expand Down Expand Up @@ -128,6 +129,21 @@ type Callback<P, R> = (
) => Promise<R | null | undefined> | R | null | undefined;
type JsPluginHook<F, P, R> = { filters: F; executor: Callback<P, R> };

export interface PluginProcessModuleParams {
moduleId: string;
moduleType: ModuleType;
content: string;
}

export interface PluginProcessModuleResult {
content: string;
}

type NormalizeFilterParams = {
moduleTypes?: ModuleType[];
resolvedPaths?: string[];
};

export interface JsPlugin {
name: string;
priority?: number;
Expand Down Expand Up @@ -171,6 +187,12 @@ export interface JsPlugin {
PluginTransformHookResult
>;

processModule?: JsPluginHook<
NormalizeFilterParams,
PluginProcessModuleParams,
PluginProcessModuleResult
>;

buildEnd?: { executor: Callback<Record<string, never>, void> };

renderStart?: {
Expand Down
Loading

0 comments on commit 6b84912

Please sign in to comment.