From 73db784f932ed0a2898b55eaed5c9c4e5e2bde94 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Fri, 15 Sep 2023 12:09:42 +0800 Subject: [PATCH] feat: add swc plugin for react server component --- Cargo.lock | 89 ++-- Cargo.toml | 3 +- packages/keep-export/Cargo.toml | 6 +- packages/keep-platform/Cargo.toml | 6 +- packages/node-transform/Cargo.toml | 6 +- packages/react-server-component/.cargo/config | 4 + packages/react-server-component/Cargo.toml | 25 ++ packages/react-server-component/package.json | 13 + packages/react-server-component/src/lib.rs | 389 ++++++++++++++++++ .../react-server-component/tests/fixture.rs | 25 ++ .../fixture/server/server-component/input.js | 3 + .../fixture/server/server-component/output.js | 3 + .../tests/fixture/server/use-client/input.js | 5 + .../tests/fixture/server/use-client/output.js | 2 + packages/remove-export/Cargo.toml | 6 +- 15 files changed, 528 insertions(+), 57 deletions(-) create mode 100644 packages/react-server-component/.cargo/config create mode 100644 packages/react-server-component/Cargo.toml create mode 100644 packages/react-server-component/package.json create mode 100644 packages/react-server-component/src/lib.rs create mode 100644 packages/react-server-component/tests/fixture.rs create mode 100644 packages/react-server-component/tests/fixture/server/server-component/input.js create mode 100644 packages/react-server-component/tests/fixture/server/server-component/output.js create mode 100644 packages/react-server-component/tests/fixture/server/use-client/input.js create mode 100644 packages/react-server-component/tests/fixture/server/use-client/output.js diff --git a/Cargo.lock b/Cargo.lock index eec2c05..ec81666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,18 +38,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "0.7.20" @@ -442,7 +430,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] @@ -568,9 +556,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "miette" @@ -1302,9 +1290,9 @@ dependencies = [ [[package]] name = "swc_atoms" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8066e17abb484602da673e2d35138ab32ce53f26368d9c92113510e1659220b" +checksum = "9f54563d7dcba626d4acfe14ed12def7ecc28e004debe3ecd2c3ee07cc47e449" dependencies = [ "bytecheck", "once_cell", @@ -1318,11 +1306,10 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.31.19" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155f4889fce45ba3758831f72eaad2a4f36be2038b76eea3f44252d85dff272c" +checksum = "9c84742fc22df1c293da5354c1cc8a5b45a045e9dc941005c1fd9cb4e9bdabc1" dependencies = [ - "ahash 0.8.3", "anyhow", "ast_node", "atty", @@ -1352,9 +1339,9 @@ dependencies = [ [[package]] name = "swc_core" -version = "0.79.60" +version = "0.83.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03eb8f62fc39483dee9460b0b223ac937e6b18ad2aadb8c57af4773dba88645c" +checksum = "f90400624ca4fd41952b40f76ea2d7445f67bda66ba09cc87ddd155ae224366a" dependencies = [ "once_cell", "swc_atoms", @@ -1373,9 +1360,9 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.107.5" +version = "0.109.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a6fdad1abebc27eae5b7853c4b95918d7d06e05104835e5757c819164d7f2bd" +checksum = "e063a1614daed3ea8be56e5dd8edb17003409088d2fc9ce4aca3378879812607" dependencies = [ "bitflags 2.4.0", "bytecheck", @@ -1391,9 +1378,9 @@ dependencies = [ [[package]] name = "swc_ecma_codegen" -version = "0.142.13" +version = "0.145.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62228359378d4c8ca7e6053e4fd5a5abe1e51617c9823c35aaf5a132e3582b3a" +checksum = "815ffa630baae41acb9cae372684b772e65e7395954f20162ba4e9ea238f4d16" dependencies = [ "memchr", "num-bigint", @@ -1423,9 +1410,9 @@ dependencies = [ [[package]] name = "swc_ecma_parser" -version = "0.137.12" +version = "0.140.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a6d5d1fca6f2e91c3ed1a8cca5a10ee1ac2d6f4100885fc4ab3b03b79924c7" +checksum = "3c968599841fcecfdc2e490188ad93251897a1bb912882547e6889e14a368399" dependencies = [ "either", "num-bigint", @@ -1443,9 +1430,9 @@ dependencies = [ [[package]] name = "swc_ecma_testing" -version = "0.20.16" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6238a3bc53ab314f271863e299106a21e5d9054f034c8a28d798908fc658a341" +checksum = "0b776795afd44c8df3977391e239a8dedbe2139c5eeb1ea053c1e29314b6d8a7" dependencies = [ "anyhow", "hex", @@ -1456,9 +1443,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_base" -version = "0.130.20" +version = "0.133.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5749b208c0f6fe28c5822956d0770fb26f88bc051866c743beebee7771fadfd0" +checksum = "bd570349c3be938eff1214bba74f2d4d8dafc2e433eedb69e8ae220d425569d7" dependencies = [ "better_scoped_tls", "bitflags 2.4.0", @@ -1479,9 +1466,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_testing" -version = "0.133.20" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cc89ee8b173b742a62d0386ba406bd4896586f6893417d8452fcecc04d98fd" +checksum = "03348a53dcfa46d3dae52691affc1538eaddb19036dd0b448fe08f69a25db18d" dependencies = [ "ansi_term", "anyhow", @@ -1505,9 +1492,9 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.120.16" +version = "0.123.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb902ccfabaa539eef3e989fa92057a9220d47a226a08e770d4b99fc5d9475d" +checksum = "5b6d6b59ebd31b25fe2692ff705c806961e7856de8b7e91fd0942328886cd315" dependencies = [ "indexmap", "num_cpus", @@ -1523,9 +1510,9 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.93.5" +version = "0.95.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b8aef66f4175681cfefc255654fe062c44b0540a30070bce4bbb7a09252e82" +checksum = "2774848b306e17fa280c598ecb192cc2c72a1163942b02d48606514336e9e7c5" dependencies = [ "num-bigint", "swc_atoms", @@ -1549,9 +1536,9 @@ dependencies = [ [[package]] name = "swc_error_reporters" -version = "0.15.19" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d981f79a7bd42305956d82b32808006c55323cc69a58fb3c05dab632df4308e5" +checksum = "c76b479ad1a69bec65b261354b8e2dec8ed0f9ed43c7b54ab053dc4923e1c90e" dependencies = [ "anyhow", "miette", @@ -1637,9 +1624,9 @@ dependencies = [ [[package]] name = "swc_plugin_proxy" -version = "0.36.5" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e26f32d6c186c12b73d1f9c0264adc9a93637de26437db7d5db8c726c94a64" +checksum = "a76ccadcc63a459e096f332730b2d4e09548fc10e0be63df9f3bacecdf5332fe" dependencies = [ "better_scoped_tls", "rkyv", @@ -1649,6 +1636,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "swc_plugin_react_server_component" +version = "0.1.0" +dependencies = [ + "easy-error", + "fxhash", + "serde", + "serde_json", + "swc_common", + "swc_core", + "testing", + "tracing", +] + [[package]] name = "swc_plugin_remove_export" version = "0.1.1" @@ -1760,9 +1761,9 @@ dependencies = [ [[package]] name = "testing" -version = "0.33.22" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930c64423c9f6c4be9f0af8db7f0059e6e18ee6bb98c0cd389e34748b02f558b" +checksum = "dc31f7f4a7baef94495386462c2a55caa0f0885b61b28c120f783132d14938ed" dependencies = [ "ansi_term", "cargo_metadata", diff --git a/Cargo.toml b/Cargo.toml index 0a19700..9642d54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "packages/remove-export", "packages/keep-platform", "packages/keep-export", - "packages/node-transform" + "packages/node-transform", + "packages/react-server-component" ] [profile.release] diff --git a/packages/keep-export/Cargo.toml b/packages/keep-export/Cargo.toml index 18046af..cf0d929 100644 --- a/packages/keep-export/Cargo.toml +++ b/packages/keep-export/Cargo.toml @@ -11,15 +11,15 @@ serde = "1" fxhash= "0.2.1" easy-error = "1.0.0" tracing = { version="0.1.34", features = ["release_max_level_info"] } -swc_core = {version = "0.79.56", features = [ +swc_core = {version = "0.83.10", features = [ "ecma_plugin_transform", "ecma_utils", "ecma_visit", "ecma_ast", "common", ]} -swc_common = { version = "0.31.18", features = ["concurrent"] } +swc_common = { version = "0.32.1", features = ["concurrent"] } serde_json = {version = "1", features = ["unbounded_depth"]} [dev-dependencies] -testing = "0.33.21" +testing = "0.34.1" diff --git a/packages/keep-platform/Cargo.toml b/packages/keep-platform/Cargo.toml index fc320f8..0dab288 100644 --- a/packages/keep-platform/Cargo.toml +++ b/packages/keep-platform/Cargo.toml @@ -12,15 +12,15 @@ fxhash= "0.2.1" lazy_static = "1.4.0" easy-error = "1.0.0" tracing = { version="0.1.34", features = ["release_max_level_info"] } -swc_core = { version = "0.79.56", features = [ +swc_core = { version = "0.83.10", features = [ "ecma_plugin_transform", "ecma_utils", "ecma_visit", "ecma_ast", "common", ]} -swc_common = { version = "0.31.18", features = ["concurrent"] } +swc_common = { version = "0.32.1", features = ["concurrent"] } serde_json = {version = "1", features = ["unbounded_depth"]} [dev-dependencies] -testing = "0.33.21" +testing = "0.34.1" diff --git a/packages/node-transform/Cargo.toml b/packages/node-transform/Cargo.toml index 00277e2..55f81d8 100644 --- a/packages/node-transform/Cargo.toml +++ b/packages/node-transform/Cargo.toml @@ -11,15 +11,15 @@ serde = "1" fxhash= "0.2.1" easy-error = "1.0.0" tracing = { version="0.1.34", features = ["release_max_level_info"] } -swc_core = { version = "0.79.56", features = [ +swc_core = { version = "0.83.10", features = [ "ecma_plugin_transform", "ecma_utils", "ecma_visit", "ecma_ast", "common", ]} -swc_common = { version = "0.31.18", features = ["concurrent"] } +swc_common = { version = "0.32.1", features = ["concurrent"] } serde_json = {version = "1", features = ["unbounded_depth"]} [dev-dependencies] -testing = "0.33.21" +testing = "0.34.1" diff --git a/packages/react-server-component/.cargo/config b/packages/react-server-component/.cargo/config new file mode 100644 index 0000000..9f4c9cf --- /dev/null +++ b/packages/react-server-component/.cargo/config @@ -0,0 +1,4 @@ +# These command aliases are not final, may change +[alias] +# Alias to build actual plugin binary for the specified target. +prepublish = "build --target wasm32-wasi" \ No newline at end of file diff --git a/packages/react-server-component/Cargo.toml b/packages/react-server-component/Cargo.toml new file mode 100644 index 0000000..1dc4563 --- /dev/null +++ b/packages/react-server-component/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "swc_plugin_react_server_component" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +serde = "1" +fxhash= "0.2.1" +easy-error = "1.0.0" +tracing = { version="0.1.34", features = ["release_max_level_info"] } +swc_core = { version = "0.83.10", features = [ + "ecma_plugin_transform", + "ecma_utils", + "ecma_visit", + "ecma_ast", + "common" +]} +swc_common = { version = "0.32.1", features = ["concurrent"] } +serde_json = {version = "1", features = ["unbounded_depth"]} + +[dev-dependencies] +testing = "0.34.1" diff --git a/packages/react-server-component/package.json b/packages/react-server-component/package.json new file mode 100644 index 0000000..36043c1 --- /dev/null +++ b/packages/react-server-component/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ice/swc-plugin-react-server-component", + "version": "0.1.0", + "license": "MIT", + "keywords": ["swc-plugin"], + "main": "swc_plugin_react_server_component.wasm", + "scripts": { + "prepublishOnly": "cargo prepublish --release && cp ../../target/wasm32-wasi/release/swc_plugin_react_server_component.wasm ." + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/react-server-component/src/lib.rs b/packages/react-server-component/src/lib.rs new file mode 100644 index 0000000..4b3575b --- /dev/null +++ b/packages/react-server-component/src/lib.rs @@ -0,0 +1,389 @@ +use serde::Deserialize; +use swc_core::{ + common::{ + FileName, + Span,DUMMY_SP, + errors::HANDLER + }, + ecma::{ + ast::*, + atoms::{JsWord, js_word}, + visit::{Fold, FoldWith, VisitMut, as_folder, noop_visit_mut_type, VisitMutWith}, + utils::{prepend_stmts, quote_ident, quote_str, ExprFactory} + }, + plugin::{plugin_transform, proxies::TransformPluginProgramMetadata, metadata::TransformPluginMetadataContextKind}, +}; + +struct ModuleImports { + source: (JsWord, Span), + specifiers: Vec<(JsWord, Span)>, +} + +pub fn react_server_component(file_name: FileName, is_server: bool) -> impl Fold + VisitMut { + as_folder(ReactServerComponent { + filepath: file_name.to_string(), + is_server, + export_names: vec![], + invalid_server_imports: vec![ + JsWord::from("client-only"), + ], + invalid_client_imports: vec![ + JsWord::from("server-only"), + ], + invalid_server_react_apis: vec![ + JsWord::from("Component"), + JsWord::from("createContext"), + JsWord::from("createFactory"), + JsWord::from("PureComponent"), + JsWord::from("useDeferredValue"), + JsWord::from("useEffect"), + JsWord::from("useImperativeHandle"), + JsWord::from("useInsertionEffect"), + JsWord::from("useLayoutEffect"), + JsWord::from("useReducer"), + JsWord::from("useRef"), + JsWord::from("useState"), + JsWord::from("useSyncExternalStore"), + JsWord::from("useTransition"), + ], + // TODO: add more apis to this list which are not supported in server components. + invalid_server_ice_imports: vec![ + JsWord::from("useData"), + ], + }) +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Config { + pub is_server: bool +} + +struct ReactServerComponent { + filepath: String, + is_server: bool, + export_names: Vec, + invalid_server_imports: Vec, + invalid_client_imports: Vec, + invalid_server_react_apis: Vec, + invalid_server_ice_imports: Vec, +} + +impl ReactServerComponent { + fn create_module_proxy(&self, module: &mut Module) { + // Clear all statements and module decalarations. + module.body.clear(); + let file_path = quote_str!(&*self.filepath); + + prepend_stmts( + &mut module.body, + vec![ + // const { createClientModuleProxy } = require(\'react-server-dom-webpack/server.node\'); + ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Const, + decls: vec![ + VarDeclarator { + span: DUMMY_SP, + name: Pat::Object(ObjectPat { + span: DUMMY_SP, + props: vec![ObjectPatProp::Assign(AssignPatProp { + span: DUMMY_SP, + key: quote_ident!("createClientModuleProxy"), + value: None, + })], + optional: false, + type_ann: None, + }), + init: Some(Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: quote_ident!("require").as_callee(), + args: vec![quote_str!("react-server-dom-webpack/server.node").as_arg()], + type_args: Default::default(), + }))), + definite: false, + }, + ], + declare: false, + })))), + // module.exports = createClientModuleProxy(moduleId), + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + left: PatOrExpr::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(quote_ident!("module"))), + prop: MemberProp::Ident(quote_ident!("exports")), + }))), + op: op!("="), + right: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: quote_ident!("createClientModuleProxy").as_callee(), + args: vec![file_path.as_arg()], + type_args: Default::default(), + })), + })), + })), + ].into_iter(), + ); + } + + fn collect_top_level_directives_and_imports(&mut self, module: &mut Module) -> (bool, bool, Vec) { + let mut imports: Vec = vec![]; + let mut finished_directives = false; + let mut is_client_entry = false; + let mut is_action_file = false; + + fn panic_both_directives(span: Span) { + // Error handle for both directives in the same file. + HANDLER.with(|handler| { + handler + .struct_span_err( + span, + "Cannot use both `use client` and `use server` in the same file.", + ) + .emit() + }) + } + + let _ = &module.body.retain(|item| { + match item { + ModuleItem::Stmt(stmt) => { + if !stmt.is_expr() { + finished_directives = true; + } + + match stmt.as_expr() { + Some(expr_stmt) => { + match &*expr_stmt.expr { + Expr::Lit(Lit::Str(Str { value, ..})) => { + if &**value == "use client" { + if !finished_directives { + is_client_entry = true; + + if is_action_file { + panic_both_directives(expr_stmt.span) + } + } else { + HANDLER.with(|handler| { + handler + .struct_span_err( + expr_stmt.span, + "The \"use client\" directive must be placed before other expressions. Move it to the top of the file to resolve this issue.", + ).emit() + }) + } + // Remove the directive. + return false; + } else if &**value == "use server" && !finished_directives { + is_action_file = true; + + if is_client_entry { + panic_both_directives(expr_stmt.span) + } + } + } + // Case `("use client;")`. + Expr::Paren(ParenExpr { expr, .. }) => { + finished_directives = true; + if let Expr::Lit(Lit::Str(Str { value, .. })) = &**expr { + if &**value == "use client" { + HANDLER.with(|handler| { + handler + .struct_span_err( + expr_stmt.span, + "\"use client\" must be a directive, and placed before other expressions. Remove the parentheses and move it to the top of the file to resolve this issue.", + ) + .emit() + }) + } + } + } + _ => { + // Other expression types. + finished_directives = true; + } + } + } + None => { + // Not an expression. + finished_directives = true; + } + } + } + // Collect import specifiers. + ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => { + let source = import.src.value.clone(); + let specifiers = import.specifiers.iter().map(|specifier| match specifier { + ImportSpecifier::Named(named) => match &named.imported { + Some(imported) => match &imported { + ModuleExportName::Ident(i) => (i.to_id().0, i.span), + ModuleExportName::Str(s) => (s.value.clone(), s.span), + }, + None => (named.local.to_id().0, named.local.span), + }, + ImportSpecifier::Default(d) => (js_word!(""), d.span), + ImportSpecifier::Namespace(n) => ("*".into(), n.span), + }) + .collect(); + imports.push(ModuleImports { source: (source, import.span), specifiers }); + finished_directives = true; + } + // Collect all export names. + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) => { + for specifier in &e.specifiers { + self.export_names.push(match specifier { + ExportSpecifier::Default(_) => "default".to_string(), + ExportSpecifier::Namespace(_) => "*".to_string(), + ExportSpecifier::Named(named) => match &named.exported { + Some(exported) => match &exported { + ModuleExportName::Ident(i) => i.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }, + _ => match &named.orig { + ModuleExportName::Ident(i) => i.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }, + }, + }) + } + finished_directives = true; + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, ..})) => { + match decl { + Decl::Class(ClassDecl { ident, .. }) => { + self.export_names.push(ident.sym.to_string()); + } + Decl::Fn(FnDecl { ident, .. }) => { + self.export_names.push(ident.sym.to_string()); + } + Decl::Var(var) => { + for decl in &var.decls { + if let Pat::Ident(ident) = &decl.name { + self.export_names.push(ident.sym.to_string()); + } + } + } + _ => {} + } + finished_directives = true; + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(_)) => { + self.export_names.push("default".to_string()); + finished_directives = true; + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(_)) => { + self.export_names.push("default".to_string()); + finished_directives = true; + } + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(_)) => { + self.export_names.push("*".to_string()); + } + _ => { + finished_directives = true; + } + } + true + }); + (is_client_entry, is_action_file, imports) + } + + fn assert_server_import(&self, imports: &Vec) { + for import in imports { + let source = import.source.0.clone(); + if self.invalid_server_imports.contains(&source) { + HANDLER.with(|handler| { + handler + .struct_span_err( + import.source.1, + format!("Cannot import \"{}\" in a server component.", source).as_str(), + ) + .emit() + }) + } + if source == *"react" { + for specifier in &import.specifiers { + if self.invalid_server_react_apis.contains(&specifier.0) { + HANDLER.with(|handler| { + handler + .struct_span_err( + specifier.1, + format!("Cannot use react API: \"{}\" in a server component.", specifier.0).as_str(), + ) + .emit() + }) + } + } + } + if source == *"ice" { + for specifier in &import.specifiers { + if self.invalid_server_ice_imports.contains(&specifier.0) { + HANDLER.with(|handler| { + handler + .struct_span_err( + specifier.1, + format!("Cannot use ice API: \"{}\" in a server component.", specifier.0).as_str(), + ) + .emit() + }) + } + } + } + } + } + + fn assert_client_import(&self, imports: &Vec) { + for import in imports { + let source = import.source.0.clone(); + if self.invalid_client_imports.contains(&source) { + HANDLER.with(|handler| { + handler + .struct_span_err( + import.source.1, + format!("Cannot import \"{}\" in a client component.", source).as_str(), + ) + .emit() + }) + } + } + } +} + +impl VisitMut for ReactServerComponent { + noop_visit_mut_type!(); + + fn visit_mut_module(&mut self, module: &mut Module) { + let (is_client_entry, is_action_file, imports) = self.collect_top_level_directives_and_imports(module); + if self.is_server { + if !is_client_entry { + self.assert_server_import(&imports); + } else { + // Proxy client module. + self.create_module_proxy(module); + return; + } + } else { + if !is_action_file { + self.assert_client_import(&imports); + } + // TODO: handle client entry in the future. + } + module.visit_mut_children_with(self) + } +} + +#[plugin_transform] +pub fn process_transform(program: Program, _metadata: TransformPluginProgramMetadata) -> Program { + let config = serde_json::from_str::( + &_metadata + .get_transform_plugin_config() + .expect("failed to get plugin config for react-server-component"), + ) + .expect("invalid config for react-server-component"); + let file_name = match _metadata.get_context(&TransformPluginMetadataContextKind::Filename) { + Some(s) => FileName::Real(s.into()), + None => FileName::Anon, + }; + program.fold_with(&mut react_server_component(file_name, config.is_server)) +} \ No newline at end of file diff --git a/packages/react-server-component/tests/fixture.rs b/packages/react-server-component/tests/fixture.rs new file mode 100644 index 0000000..dd82b14 --- /dev/null +++ b/packages/react-server-component/tests/fixture.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; +use testing::fixture; +use swc_core::{ + common::FileName, + ecma::transforms::testing::{ test_fixture, FixtureTestConfig } +}; +use swc_plugin_react_server_component::react_server_component; + +#[fixture("tests/fixture/server/**/input.js")] +fn fixture(input: PathBuf) { + let parent = input.parent().unwrap(); + let output = parent.join("output.js"); + + test_fixture( + Default::default(), + &|_t| { + react_server_component(FileName::Real("file_path.js".into()), true) + }, + &input, + &output, + FixtureTestConfig { + ..Default::default() + }, + ); +} diff --git a/packages/react-server-component/tests/fixture/server/server-component/input.js b/packages/react-server-component/tests/fixture/server/server-component/input.js new file mode 100644 index 0000000..2ec886c --- /dev/null +++ b/packages/react-server-component/tests/fixture/server/server-component/input.js @@ -0,0 +1,3 @@ +export default function () { + return null +} \ No newline at end of file diff --git a/packages/react-server-component/tests/fixture/server/server-component/output.js b/packages/react-server-component/tests/fixture/server/server-component/output.js new file mode 100644 index 0000000..2ec886c --- /dev/null +++ b/packages/react-server-component/tests/fixture/server/server-component/output.js @@ -0,0 +1,3 @@ +export default function () { + return null +} \ No newline at end of file diff --git a/packages/react-server-component/tests/fixture/server/use-client/input.js b/packages/react-server-component/tests/fixture/server/use-client/input.js new file mode 100644 index 0000000..f4a843e --- /dev/null +++ b/packages/react-server-component/tests/fixture/server/use-client/input.js @@ -0,0 +1,5 @@ +'use client'; + +export default function () { + return null; +} \ No newline at end of file diff --git a/packages/react-server-component/tests/fixture/server/use-client/output.js b/packages/react-server-component/tests/fixture/server/use-client/output.js new file mode 100644 index 0000000..f319a06 --- /dev/null +++ b/packages/react-server-component/tests/fixture/server/use-client/output.js @@ -0,0 +1,2 @@ +const { createClientModuleProxy } = require("react-server-dom-webpack/server.node"); +module.exports = createClientModuleProxy("file_path.js"); \ No newline at end of file diff --git a/packages/remove-export/Cargo.toml b/packages/remove-export/Cargo.toml index 02e7039..ac887e3 100644 --- a/packages/remove-export/Cargo.toml +++ b/packages/remove-export/Cargo.toml @@ -11,7 +11,7 @@ serde = "1" fxhash= "0.2.1" easy-error = "1.0.0" tracing = { version="0.1.34", features = ["release_max_level_info"] } -swc_core = { version = "0.79.56", features = [ +swc_core = { version = "0.83.10", features = [ "ecma_plugin_transform", "ecma_utils", "ecma_visit", @@ -19,8 +19,8 @@ swc_core = { version = "0.79.56", features = [ "ecma_parser", "common", ]} -swc_common = { version = "0.31.18", features = ["concurrent"] } +swc_common = { version = "0.32.1", features = ["concurrent"] } serde_json = {version = "1", features = ["unbounded_depth"]} [dev-dependencies] -testing = "0.33.21" +testing = "0.34.1"