feat(plugin): Add PluginContext (#3568)

This commit is contained in:
OJ Kwon 2022-02-15 00:03:17 -08:00 committed by GitHub
parent a5b3dd815b
commit a96217feaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 127 additions and 34 deletions

View File

@ -60,7 +60,11 @@ use swc_ecma_transforms_optimization::{inline_globals2, GlobalExprMap};
use swc_ecma_visit::{Fold, VisitMutWith};
use self::util::BoolOrObject;
use crate::{builder::PassBuilder, plugin::PluginConfig, SwcComments, SwcImportResolver};
use crate::{
builder::PassBuilder,
plugin::{PluginConfig, PluginContext},
SwcComments, SwcImportResolver,
};
#[cfg(test)]
mod tests;
@ -425,6 +429,15 @@ impl Options {
NodeModulesResolver::new(TargetEnv::Node, Default::default(), true),
);
let transform_filename = match base {
FileName::Real(path) => path.as_os_str().to_str().map(String::from),
_ => None,
};
let plugin_context = PluginContext {
filename: transform_filename,
};
let pass = chain!(
lint_to_fold(swc_ecma_lints::rules::all(LintParams {
program: &program,
@ -457,7 +470,7 @@ impl Options {
),
syntax.typescript()
),
crate::plugin::plugins(plugin_resolver, experimental),
crate::plugin::plugins(plugin_resolver, experimental, plugin_context),
custom_before_pass(&program),
// handle jsx
Optional::new(

View File

@ -11,13 +11,37 @@ use swc_ecma_loader::resolvers::{lru::CachingResolver, node::NodeModulesResolver
use swc_ecma_transforms::pass::noop;
use swc_ecma_visit::{noop_fold_type, Fold};
/// A tuple represents a plugin.
/// First element is a resolvable name to the plugin, second is a JSON object
/// that represents configuration option for those plugin.
/// Type of plugin's configuration is up to each plugin - swc/core does not have
/// strong type and it'll be serialized into plain string when it's passed to
/// plugin's entrypoint function.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct PluginConfig(String, serde_json::Value);
/// Struct represents arbitary `context` or `state` to be passed to plugin's
/// entrypoint.
/// While internally this is strongly typed, it is not exposed as public
/// interface to plugin's entrypoint but instead will be passed as JSON string.
/// First, not all of plugins will need to deserialize this - plugin may opt in
/// to access when it's needed. Secondly, we do not have way to avoid breaking
/// changes when adding a new property. We may change this to typed
/// deserialization in the future if we have add-only schema support with
/// serialization / deserialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct PluginContext {
/// The path of the file being processed. This includes all of the path as
/// much as possible.
pub filename: Option<String>,
}
pub fn plugins(
resolver: CachingResolver<NodeModulesResolver>,
config: crate::config::JscExperimental,
plugin_context: PluginContext,
) -> impl Fold {
#[cfg(feature = "plugin")]
{
@ -28,6 +52,7 @@ pub fn plugins(
resolver,
plugins: config.plugins,
plugin_cache: cache_root,
plugin_context,
}
}
@ -44,6 +69,7 @@ struct RustPlugins {
/// swc, as well as cache.
#[cfg(feature = "plugin")]
plugin_cache: Option<swc_plugin_runner::resolve::PluginCache>,
plugin_context: PluginContext,
}
impl RustPlugins {
@ -66,8 +92,12 @@ impl RustPlugins {
if let Some(plugins) = &self.plugins {
for p in plugins {
let config_json = serde_json::to_string(&p.1)
.context("failed to serialize plugin config as json")?;
let config_json = Serialized::serialize(&config_json)?;
.context("Failed to serialize plugin config as json")
.and_then(|value| Serialized::serialize(&value))?;
let context_json = serde_json::to_string(&self.plugin_context)
.context("Failed to serialize plugin context as json")
.and_then(|value| Serialized::serialize(&value))?;
let resolved_path = self
.resolver
@ -83,8 +113,9 @@ impl RustPlugins {
&p.0,
&path,
&mut self.plugin_cache,
config_json,
serialized,
config_json,
context_json,
)?;
}
}

View File

@ -222,26 +222,27 @@ impl VisitMut for TransformVisitor {
// Implement necessary visit_mut_* methods for actual custom transform.
}
/// An entrypoint to the SWC's transform plugin.
/// `plugin_transform` macro handles necessary interop to communicate with the host,
/// and entrypoint function name (`process_transform`) can be anything else.
///
/// If plugin need to handle low-level ptr directly,
/// it is possible to opt out from macro by writing transform fn manually via raw interface
/// An example plugin function with macro support.
/// `plugin_transform` macro interop pointers into deserialized structs, as well
/// as returning ptr back to host.
///
/// It is possible to opt out from macro by writing transform fn manually via
/// `__plugin_process_impl(
/// ast_ptr: *const u8,
/// ast_ptr_len: i32,
/// config_str_ptr: *const u8,
/// config_str_ptr_len: i32) ->
/// config_str_ptr_len: i32,
/// context_str_ptr: *const u8,
/// context_str_ptr_len: i32) ->
/// i32 /* 0 for success, fail otherwise.
/// Note this is only for internal pointer interop result,
/// not actual transform result */
///
/// However, this means plugin author need to handle all of serialization/deserialization
/// steps with communicating with host. Refer `swc_plugin_macro` for more details.
/// if plugin need to handle low-level ptr directly. However, there are
/// important steps manually need to be performed like sending transformed
/// results back to host. Refer swc_plugin_macro how does it work internally.
#[plugin_transform]
pub fn process_transform(program: Program, _plugin_config: String) -> Program {
pub fn process_transform(program: Program, _plugin_config: String, _context: String) -> Program {
program.fold_with(&mut as_folder(TransformVisitor))
}
"#

View File

@ -61,9 +61,10 @@ fn handle_func(func: ItemFn) -> TokenStream {
// There are some cases error won't be wrapped up however - for example, we expect
// serialization of PluginError itself should succeed.
#[no_mangle]
pub fn #process_impl_ident(ast_ptr: *const u8, ast_ptr_len: i32, config_str_ptr: *const u8, config_str_ptr_len: i32) -> i32 {
pub fn #process_impl_ident(ast_ptr: *const u8, ast_ptr_len: i32, config_str_ptr: *const u8, config_str_ptr_len: i32, context_str_ptr: *const u8, context_str_ptr_len: i32) -> i32 {
let ast_ptr_len_usize: Result<usize, std::num::TryFromIntError> = std::convert::TryInto::try_into(ast_ptr_len);
let config_str_ptr_len_usize: Result<usize, std::num::TryFromIntError> = std::convert::TryInto::try_into(config_str_ptr_len);
let context_str_ptr_len_usize: Result<usize, std::num::TryFromIntError> = std::convert::TryInto::try_into(context_str_ptr_len);
if ast_ptr_len_usize.is_err() {
let err = swc_plugin::PluginError::SizeInteropFailure("Failed to convert size of AST pointer".to_string());
@ -75,21 +76,29 @@ fn handle_func(func: ItemFn) -> TokenStream {
return construct_error_ptr(err);
}
if context_str_ptr_len_usize.is_err() {
let err = swc_plugin::PluginError::SizeInteropFailure("Failed to convert size of context string pointer".to_string());
return construct_error_ptr(err);
}
// Read raw serialized bytes from wasm's memory space. Host (SWC) should
// allocate memory, copy bytes and pass ptr to plugin.
let raw_ast_serialized_bytes =
unsafe { std::slice::from_raw_parts(ast_ptr, ast_ptr_len_usize.unwrap()) };
let raw_config_serialized_bytes =
unsafe { std::slice::from_raw_parts(config_str_ptr, config_str_ptr_len_usize.unwrap()) };
let raw_context_serialized_bytes =
unsafe { std::slice::from_raw_parts(context_str_ptr, context_str_ptr_len_usize.unwrap()) };
// Reconstruct SerializedProgram from raw bytes
let serialized_program = swc_plugin::Serialized::new_for_plugin(raw_ast_serialized_bytes, ast_ptr_len);
let serialized_config = swc_plugin::Serialized::new_for_plugin(raw_config_serialized_bytes, config_str_ptr_len);
let serialized_context = swc_plugin::Serialized::new_for_plugin(raw_context_serialized_bytes, context_str_ptr_len);
// Reconstruct `Program` & config string from serialized program
let program = swc_plugin::Serialized::deserialize(&serialized_program);
let config = swc_plugin::Serialized::deserialize(&serialized_config);
let context = swc_plugin::Serialized::deserialize(&serialized_context);
if program.is_err() {
let err = swc_plugin::PluginError::Deserialize(
@ -109,6 +118,14 @@ fn handle_func(func: ItemFn) -> TokenStream {
}
let config: String = config.expect("Should be a string");
if context.is_err() {
let err = swc_plugin::PluginError::Deserialize(
("Failed to deserialize context string received from host".to_string(),
raw_context_serialized_bytes.to_vec())
);
return construct_error_ptr(err);
}
let context: String = context.expect("Should be a string");
// Create a handler wired with plugin's diagnostic emitter, set it for global context.
let handler = swc_plugin::errors::Handler::with_emitter(
@ -126,7 +143,7 @@ fn handle_func(func: ItemFn) -> TokenStream {
}
// Take original plugin fn ident, then call it with interop'ed args
let transformed_program = #ident(program, config);
let transformed_program = #ident(program, config, context);
// Serialize transformed result, return back to the host.
let serialized_result = swc_plugin::Serialized::serialize(&transformed_program);

View File

@ -417,7 +417,7 @@ fn load_plugin(
/// teardown
struct PluginTransformTracker {
// Main transform interface plugin exports
exported_plugin_transform: wasmer::NativeFunc<(i32, i32, i32, i32), i32>,
exported_plugin_transform: wasmer::NativeFunc<(i32, i32, i32, i32, i32, i32), i32>,
// `__free` function automatically exported via swc_plugin sdk to allow deallocation in guest
// memory space
exported_plugin_free: wasmer::NativeFunc<(i32, i32), i32>,
@ -437,7 +437,9 @@ impl PluginTransformTracker {
let tracker = PluginTransformTracker {
exported_plugin_transform: instance
.exports
.get_native_function::<(i32, i32, i32, i32), i32>("__plugin_process_impl")?,
.get_native_function::<(i32, i32, i32, i32, i32, i32), i32>(
"__plugin_process_impl",
)?,
exported_plugin_free: instance
.exports
.get_native_function::<(i32, i32), i32>("__free")?,
@ -500,15 +502,19 @@ impl PluginTransformTracker {
&mut self,
program: &Serialized,
config: &Serialized,
context: &Serialized,
) -> Result<Serialized, Error> {
let guest_program_ptr = self.write_bytes_into_guest(program)?;
let config_str_ptr = self.write_bytes_into_guest(config)?;
let context_str_ptr = self.write_bytes_into_guest(context)?;
let result = self.exported_plugin_transform.call(
guest_program_ptr.0,
guest_program_ptr.1,
config_str_ptr.0,
config_str_ptr.1,
context_str_ptr.0,
context_str_ptr.1,
)?;
self.read_bytes_from_guest(result)
@ -529,13 +535,14 @@ pub fn apply_js_plugin(
plugin_name: &str,
path: &Path,
cache: &mut Option<PluginCache>,
config_json: Serialized,
program: Serialized,
config_json: Serialized,
context_json: Serialized,
) -> Result<Serialized, Error> {
(|| -> Result<_, Error> {
let mut transform_tracker = PluginTransformTracker::new(path, cache)?;
transform_tracker.transform(&program, &config_json)
transform_tracker.transform(&program, &config_json, &context_json)
})()
.with_context(|| {
format!(

View File

@ -82,10 +82,18 @@ fn internal() -> Result<(), Error> {
let program = Serialized::serialize(&program).expect("Should serializable");
let config = Serialized::serialize(&"{}".to_string()).expect("Should serializable");
let context = Serialized::serialize(&"{sourceFileName: 'single_plugin_test'}".to_string())
.expect("Should serializable");
let program_bytes =
swc_plugin_runner::apply_js_plugin("internal-test", &path, &mut None, config, program)
.expect("Plugin should apply transform");
let program_bytes = swc_plugin_runner::apply_js_plugin(
"internal-test",
&path,
&mut None,
program,
config,
context,
)
.expect("Plugin should apply transform");
let program: Program =
Serialized::deserialize(&program_bytes).expect("Should able to deserialize");
@ -119,10 +127,20 @@ fn internal() -> Result<(), Error> {
let program = Serialized::serialize(&program).expect("Should serializable");
let config = Serialized::serialize(&"{}".to_string()).expect("Should serializable");
let context =
Serialized::serialize(&"{sourceFileName: 'single_plugin_handler_test'}".to_string())
.expect("Should serializable");
let _res = HANDLER.set(&handler, || {
swc_plugin_runner::apply_js_plugin("internal-test", &path, &mut None, config, program)
.expect("Plugin should apply transform")
swc_plugin_runner::apply_js_plugin(
"internal-test",
&path,
&mut None,
program,
config,
context,
)
.expect("Plugin should apply transform")
});
Ok(())
@ -151,8 +169,10 @@ fn internal() -> Result<(), Error> {
"internal-test",
&path,
&mut None,
Serialized::serialize(&"{}".to_string()).expect("Should serializable"),
serialized_program,
Serialized::serialize(&"{}".to_string()).expect("Should serializable"),
Serialized::serialize(&"{sourceFileName: 'multiple_plugin_test'}".to_string())
.expect("Should serializable"),
)
.expect("Plugin should apply transform");
@ -161,8 +181,10 @@ fn internal() -> Result<(), Error> {
"internal-test",
&path,
&mut None,
Serialized::serialize(&"{}".to_string()).expect("Should serializable"),
serialized_program,
Serialized::serialize(&"{}".to_string()).expect("Should serializable"),
Serialized::serialize(&"{sourceFileName: 'multiple_plugin_test2'}".to_string())
.expect("Should serializable"),
)
.expect("Plugin should apply transform");

View File

@ -712,7 +712,7 @@ dependencies = [
[[package]]
name = "swc_ecma_ast"
version = "0.65.3"
version = "0.65.4"
dependencies = [
"is-macro",
"num-bigint",
@ -766,7 +766,7 @@ dependencies = [
[[package]]
name = "swc_plugin"
version = "0.25.0"
version = "0.26.1"
dependencies = [
"once_cell",
"swc_atoms",
@ -778,7 +778,7 @@ dependencies = [
[[package]]
name = "swc_plugin_macro"
version = "0.1.0"
version = "0.2.1"
dependencies = [
"proc-macro2",
"quote",

View File

@ -32,7 +32,9 @@ impl VisitMut for ConsoleOutputReplacer {
/// ast_ptr: *const u8,
/// ast_ptr_len: i32,
/// config_str_ptr: *const u8,
/// config_str_ptr_len: i32) ->
/// config_str_ptr_len: i32,
/// context_str_ptr: *const u8,
/// context_str_ptr_len: i32) ->
/// i32 /* 0 for success, fail otherwise.
/// Note this is only for internal pointer interop result,
/// not actual transform result */
@ -41,7 +43,7 @@ impl VisitMut for ConsoleOutputReplacer {
/// important steps manually need to be performed like sending transformed
/// results back to host. Refer swc_plugin_macro how does it work internally.
#[plugin_transform]
pub fn process(program: Program, _plugin_config: String) -> Program {
pub fn process(program: Program, _plugin_config: String, _context: String) -> Program {
HANDLER.with(|handler| {
handler
.struct_span_err(DUMMY_SP, "Test diagnostics from plugin")