diff --git a/packages/next/build/swc/Cargo.lock b/packages/next/build/swc/Cargo.lock index d74f701c0b..469bf56def 100644 --- a/packages/next/build/swc/Cargo.lock +++ b/packages/next/build/swc/Cargo.lock @@ -706,6 +706,7 @@ dependencies = [ "napi-build", "napi-derive", "path-clean", + "pathdiff", "regex", "retain_mut", "serde", diff --git a/packages/next/build/swc/Cargo.toml b/packages/next/build/swc/Cargo.toml index e3b554a09f..94bdccbb1a 100644 --- a/packages/next/build/swc/Cargo.toml +++ b/packages/next/build/swc/Cargo.toml @@ -24,6 +24,7 @@ swc_ecma_preset_env = "0.42" fxhash = "0.2.1" retain_mut = "0.1.3" log = "0.4.14" +pathdiff = "0.2.0" [build-dependencies] diff --git a/packages/next/build/swc/src/next_dynamic.rs b/packages/next/build/swc/src/next_dynamic.rs index fb49d9872b..68fdb3f0ac 100644 --- a/packages/next/build/swc/src/next_dynamic.rs +++ b/packages/next/build/swc/src/next_dynamic.rs @@ -1,214 +1,261 @@ +use std::path::{Path, PathBuf}; + +use pathdiff::diff_paths; use swc_atoms::js_word; use swc_common::{FileName, DUMMY_SP}; use swc_ecmascript::ast::{ - ArrayLit, ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, CallExpr, Expr, ExprOrSpread, - ExprOrSuper, Ident, ImportDecl, ImportSpecifier, KeyValueProp, Lit, MemberExpr, ObjectLit, Prop, - PropName, PropOrSpread, Str, StrKind, + ArrayLit, ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, CallExpr, Expr, ExprOrSpread, + ExprOrSuper, Ident, ImportDecl, ImportSpecifier, KeyValueProp, Lit, MemberExpr, ObjectLit, + Prop, PropName, PropOrSpread, Str, StrKind, }; use swc_ecmascript::utils::{ - ident::{Id, IdentLike}, - HANDLER, + ident::{Id, IdentLike}, + HANDLER, }; use swc_ecmascript::visit::{Fold, FoldWith}; -pub fn next_dynamic(filename: FileName) -> impl Fold { - NextDynamicPatcher { - filename, - dynamic_bindings: vec![], - } +pub fn next_dynamic(filename: FileName, pages_dir: Option) -> impl Fold { + NextDynamicPatcher { + pages_dir, + filename, + dynamic_bindings: vec![], + } } #[derive(Debug)] struct NextDynamicPatcher { - filename: FileName, - dynamic_bindings: Vec, + pages_dir: Option, + filename: FileName, + dynamic_bindings: Vec, } impl Fold for NextDynamicPatcher { - fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl { - let ImportDecl { - ref src, - ref specifiers, - .. - } = decl; - if &src.value == "next/dynamic" { - for specifier in specifiers { - if let ImportSpecifier::Default(default_specifier) = specifier { - self.dynamic_bindings.push(default_specifier.local.to_id()); - } - } - } - - decl - } - - fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr { - let mut expr = expr.fold_children_with(self); - if let ExprOrSuper::Expr(i) = &expr.callee { - if let Expr::Ident(identifier) = &**i { - if self.dynamic_bindings.contains(&identifier.to_id()) { - if expr.args.len() == 0 { - HANDLER.with(|handler| { - handler - .struct_span_err( - identifier.span, - "next/dynamic requires at least one argument", - ) - .emit() - }); - } else if expr.args.len() > 2 { - HANDLER.with(|handler| { - handler - .struct_span_err(identifier.span, "next/dynamic only accepts 2 arguments") - .emit() - }); - } - - let mut import_specifier = None; - if let Expr::Arrow(ArrowExpr { - body: BlockStmtOrExpr::Expr(e), + fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl { + let ImportDecl { + ref src, + ref specifiers, .. - }) = &*expr.args[0].expr - { - if let Expr::Call(CallExpr { - args: a, callee, .. - }) = &**e - { - if let ExprOrSuper::Expr(e) = callee { - if let Expr::Ident(Ident { sym, .. }) = &**e { - if sym == "import" { - if a.len() == 0 { - // Do nothing, import_specifier will remain None - // triggering error below - } else if let Expr::Lit(Lit::Str(Str { value, .. })) = &*a[0].expr { - import_specifier = Some(value.clone()); - } - } + } = decl; + if &src.value == "next/dynamic" { + for specifier in specifiers { + if let ImportSpecifier::Default(default_specifier) = specifier { + self.dynamic_bindings.push(default_specifier.local.to_id()); } - } } - } - - if let None = import_specifier { - HANDLER.with(|handler| { - handler - .struct_span_err( - identifier.span, - "First argument for next/dynamic must be an arrow function returning a valid \ - dynamic import call e.g. `dynamic(() => import('../some-component'))`", - ) - .emit() - }); - } - - // loadableGenerated: { - // webpack: () => [require.resolveWeak('../components/hello')], - // modules: - // ["/project/src/file-being-transformed.js -> " + '../components/hello'] } - let generated = Box::new(Expr::Object(ObjectLit { - span: DUMMY_SP, - props: vec![ - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(Ident::new("webpack".into(), DUMMY_SP)), - value: Box::new(Expr::Arrow(ArrowExpr { - params: vec![], - body: BlockStmtOrExpr::Expr(Box::new(Expr::Array(ArrayLit { - elems: vec![Some(ExprOrSpread { - expr: Box::new(Expr::Call(CallExpr { - callee: ExprOrSuper::Expr(Box::new(Expr::Member(MemberExpr { - obj: ExprOrSuper::Expr(Box::new(Expr::Ident(Ident { - sym: js_word!("require"), - span: DUMMY_SP, - optional: false, - }))), - prop: Box::new(Expr::Ident(Ident { - sym: "resolveWeak".into(), - span: DUMMY_SP, - optional: false, - })), - computed: false, - span: DUMMY_SP, - }))), - args: vec![ExprOrSpread { - expr: Box::new(Expr::Lit(Lit::Str(Str { - value: self.filename.to_string().into(), - span: DUMMY_SP, - kind: StrKind::Synthesized {}, - has_escape: false, - }))), - spread: None, - }], - span: DUMMY_SP, - type_args: None, - })), - spread: None, - })], - span: DUMMY_SP, - }))), - is_async: false, - is_generator: false, - span: DUMMY_SP, - return_type: None, - type_params: None, - })), - }))), - PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(Ident::new("modules".into(), DUMMY_SP)), - value: Box::new(Expr::Array(ArrayLit { - elems: vec![Some(ExprOrSpread { - expr: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::Add, - left: Box::new(Expr::Lit(Lit::Str(Str { - value: format!("{} -> ", self.filename).into(), - span: DUMMY_SP, - kind: StrKind::Synthesized {}, - has_escape: false, - }))), - right: Box::new(Expr::Lit(Lit::Str(Str { - value: import_specifier.unwrap(), - span: DUMMY_SP, - kind: StrKind::Normal { - contains_quote: false, - }, - has_escape: false, - }))), - })), - spread: None, - })], - span: DUMMY_SP, - })), - }))), - ], - })); - - let mut props = vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(Ident::new("loadableGenerated".into(), DUMMY_SP)), - value: generated, - })))]; - - if expr.args.len() == 2 { - if let Expr::Object(ObjectLit { - props: options_props, - .. - }) = &*expr.args[1].expr - { - props.extend(options_props.iter().cloned()); - } - } - - let second_arg = ExprOrSpread { - spread: None, - expr: Box::new(Expr::Object(ObjectLit { - span: DUMMY_SP, - props, - })), - }; - - expr.args.push(second_arg); } - } + + decl + } + + fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr { + let mut expr = expr.fold_children_with(self); + if let ExprOrSuper::Expr(i) = &expr.callee { + if let Expr::Ident(identifier) = &**i { + if self.dynamic_bindings.contains(&identifier.to_id()) { + if expr.args.len() == 0 { + HANDLER.with(|handler| { + handler + .struct_span_err( + identifier.span, + "next/dynamic requires at least one argument", + ) + .emit() + }); + } else if expr.args.len() > 2 { + HANDLER.with(|handler| { + handler + .struct_span_err( + identifier.span, + "next/dynamic only accepts 2 arguments", + ) + .emit() + }); + } + + let mut import_specifier = None; + if let Expr::Arrow(ArrowExpr { + body: BlockStmtOrExpr::Expr(e), + .. + }) = &*expr.args[0].expr + { + if let Expr::Call(CallExpr { + args: a, callee, .. + }) = &**e + { + if let ExprOrSuper::Expr(e) = callee { + if let Expr::Ident(Ident { sym, .. }) = &**e { + if sym == "import" { + if a.len() == 0 { + // Do nothing, import_specifier will + // remain None + // triggering error below + } else if let Expr::Lit(Lit::Str(Str { value, .. })) = + &*a[0].expr + { + import_specifier = Some(value.clone()); + } + } + } + } + } + } + + if let None = import_specifier { + HANDLER.with(|handler| { + handler + .struct_span_err( + identifier.span, + "First argument for next/dynamic must be an arrow function \ + returning a valid dynamic import call e.g. `dynamic(() => \ + import('../some-component'))`", + ) + .emit() + }); + } + + // loadableGenerated: { + // webpack: () => [require.resolveWeak('../components/hello')], + // modules: + // ["/project/src/file-being-transformed.js -> " + '../components/hello'] } + let generated = Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![ + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("webpack".into(), DUMMY_SP)), + value: Box::new(Expr::Arrow(ArrowExpr { + params: vec![], + body: BlockStmtOrExpr::Expr(Box::new(Expr::Array(ArrayLit { + elems: vec![Some(ExprOrSpread { + expr: Box::new(Expr::Call(CallExpr { + callee: ExprOrSuper::Expr(Box::new(Expr::Member( + MemberExpr { + obj: ExprOrSuper::Expr(Box::new( + Expr::Ident(Ident { + sym: js_word!("require"), + span: DUMMY_SP, + optional: false, + }), + )), + prop: Box::new(Expr::Ident(Ident { + sym: "resolveWeak".into(), + span: DUMMY_SP, + optional: false, + })), + computed: false, + span: DUMMY_SP, + }, + ))), + args: vec![ExprOrSpread { + expr: Box::new(Expr::Lit(Lit::Str(Str { + value: self.filename.to_string().into(), + span: DUMMY_SP, + kind: StrKind::Synthesized {}, + has_escape: false, + }))), + spread: None, + }], + span: DUMMY_SP, + type_args: None, + })), + spread: None, + })], + span: DUMMY_SP, + }))), + is_async: false, + is_generator: false, + span: DUMMY_SP, + return_type: None, + type_params: None, + })), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("modules".into(), DUMMY_SP)), + value: Box::new(Expr::Array(ArrayLit { + elems: vec![Some(ExprOrSpread { + expr: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::Add, + left: Box::new(Expr::Lit(Lit::Str(Str { + value: format!( + "{} -> ", + rel_filename( + self.pages_dir.as_deref(), + &self.filename + ) + ) + .into(), + span: DUMMY_SP, + kind: StrKind::Synthesized {}, + has_escape: false, + }))), + right: Box::new(Expr::Lit(Lit::Str(Str { + value: import_specifier.unwrap(), + span: DUMMY_SP, + kind: StrKind::Normal { + contains_quote: false, + }, + has_escape: false, + }))), + })), + spread: None, + })], + span: DUMMY_SP, + })), + }))), + ], + })); + + let mut props = + vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("loadableGenerated".into(), DUMMY_SP)), + value: generated, + })))]; + + if expr.args.len() == 2 { + if let Expr::Object(ObjectLit { + props: options_props, + .. + }) = &*expr.args[1].expr + { + props.extend(options_props.iter().cloned()); + } + } + + let second_arg = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props, + })), + }; + + expr.args.push(second_arg); + } + } + } + expr } - expr - } +} + +fn rel_filename(base: Option<&Path>, file: &FileName) -> String { + let base = match base { + Some(v) => v, + None => return file.to_string(), + }; + + let file = match file { + FileName::Real(v) => v, + _ => { + return file.to_string(); + } + }; + + let rel_path = diff_paths(&file, base); + + let rel_path = match rel_path { + Some(v) => v, + None => return file.display().to_string(), + }; + + rel_path.display().to_string() } diff --git a/packages/next/build/swc/src/transform.rs b/packages/next/build/swc/src/transform.rs index 4136d7c0ab..efe40f7766 100644 --- a/packages/next/build/swc/src/transform.rs +++ b/packages/next/build/swc/src/transform.rs @@ -37,7 +37,7 @@ use crate::{ use anyhow::{Context as _, Error}; use napi::{CallContext, Env, JsBoolean, JsObject, JsString, Task}; use serde::Deserialize; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use swc::{try_with_handler, Compiler, TransformOutput}; use swc_common::{chain, pass::Optional, FileName, SourceFile}; use swc_ecmascript::ast::Program; @@ -51,13 +51,16 @@ pub enum Input { } #[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] +#[serde(rename_all = "camelCase")] pub struct TransformOptions { #[serde(flatten)] - swc: swc::config::Options, + pub swc: swc::config::Options, #[serde(default)] pub disable_next_ssg: bool, + + #[serde(default)] + pub pages_dir: Option, } pub struct TransformTask { @@ -78,7 +81,7 @@ impl Task for TransformTask { hook_optimizer(), Optional::new(next_ssg(), !self.options.disable_next_ssg), amp_attributes(), - next_dynamic(s.name.clone()), + next_dynamic(s.name.clone(), self.options.pages_dir.clone()), ); self.c.process_js_with_custom_pass( s.clone(), @@ -175,3 +178,12 @@ pub fn transform_sync(cx: CallContext) -> napi::Result { )) }) } + +#[test] +fn test_deser() { + const JSON_STR: &str = r#"{"jsc":{"parser":{"syntax":"ecmascript","dynamicImport":true,"jsx":true},"transform":{"react":{"runtime":"automatic","pragma":"React.createElement","pragmaFrag":"React.Fragment","throwIfNamespace":true,"development":false,"useBuiltins":true}},"target":"es5"},"filename":"/Users/timneutkens/projects/next.js/packages/next/dist/client/next.js","sourceMaps":false,"sourceFileName":"/Users/timneutkens/projects/next.js/packages/next/dist/client/next.js"}"#; + + let tr: TransformOptions = serde_json::from_str(&JSON_STR).unwrap(); + + println!("{:#?}", tr); +} diff --git a/packages/next/build/swc/tests/fixture.rs b/packages/next/build/swc/tests/fixture.rs index fb30db8efc..366971878e 100644 --- a/packages/next/build/swc/tests/fixture.rs +++ b/packages/next/build/swc/tests/fixture.rs @@ -5,8 +5,8 @@ use std::path::PathBuf; use swc_common::{chain, comments::SingleThreadedComments, FileName}; use swc_ecma_transforms_testing::{test, test_fixture}; use swc_ecmascript::{ - parser::{EsConfig, Syntax}, - transforms::react::jsx, + parser::{EsConfig, Syntax}, + transforms::react::jsx, }; use testing::fixture; @@ -18,59 +18,60 @@ mod next_dynamic; mod next_ssg; fn syntax() -> Syntax { - Syntax::Es(EsConfig { - jsx: true, - dynamic_import: true, - ..Default::default() - }) + Syntax::Es(EsConfig { + jsx: true, + dynamic_import: true, + ..Default::default() + }) } #[fixture("tests/fixture/amp/**/input.js")] fn amp_attributes_fixture(input: PathBuf) { - let output = input.parent().unwrap().join("output.js"); - test_fixture(syntax(), &|_tr| amp_attributes(), &input, &output); + let output = input.parent().unwrap().join("output.js"); + test_fixture(syntax(), &|_tr| amp_attributes(), &input, &output); } #[fixture("tests/fixture/next-dynamic/**/input.js")] fn next_dynamic_fixture(input: PathBuf) { - let output = input.parent().unwrap().join("output.js"); - test_fixture( - syntax(), - &|_tr| { - next_dynamic(FileName::Real(PathBuf::from( - "/some-project/src/some-file.js", - ))) - }, - &input, - &output, - ); + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|_tr| { + next_dynamic( + FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + Some("/some-project/src".into()), + ) + }, + &input, + &output, + ); } #[fixture("tests/fixture/ssg/**/input.js")] fn next_ssg_fixture(input: PathBuf) { - let output = input.parent().unwrap().join("output.js"); - test_fixture( - syntax(), - &|tr| { - let jsx = jsx::( - tr.cm.clone(), - None, - swc_ecmascript::transforms::react::Options { - next: false, - runtime: None, - import_source: "".into(), - pragma: "__jsx".into(), - pragma_frag: "__jsxFrag".into(), - throw_if_namespace: false, - development: false, - use_builtins: true, - use_spread: true, - refresh: Default::default(), + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|tr| { + let jsx = jsx::( + tr.cm.clone(), + None, + swc_ecmascript::transforms::react::Options { + next: false, + runtime: None, + import_source: "".into(), + pragma: "__jsx".into(), + pragma_frag: "__jsxFrag".into(), + throw_if_namespace: false, + development: false, + use_builtins: true, + use_spread: true, + refresh: Default::default(), + }, + ); + chain!(next_ssg(), jsx) }, - ); - chain!(next_ssg(), jsx) - }, - &input, - &output, - ); + &input, + &output, + ); } diff --git a/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js b/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js index e281495c7f..bcd3050394 100644 --- a/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js +++ b/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js @@ -3,12 +3,12 @@ import dynamic2 from 'next/dynamic' const DynamicComponent1 = dynamic1(() => import('../components/hello1'), { loadableGenerated: { webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], - modules: ['/some-project/src/some-file.js -> ' + '../components/hello1'], + modules: ['some-file.js -> ' + '../components/hello1'], }, }) const DynamicComponent2 = dynamic2(() => import('../components/hello2'), { loadableGenerated: { webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], - modules: ['/some-project/src/some-file.js -> ' + '../components/hello2'], + modules: ['some-file.js -> ' + '../components/hello2'], }, }) diff --git a/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js b/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js index 484b4e9871..8d1cffdc61 100644 --- a/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js +++ b/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js @@ -1,10 +1,9 @@ import dynamic from 'next/dynamic' import somethingElse from 'something-else' - const DynamicComponent = dynamic(() => import('../components/hello'), { loadableGenerated: { webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], - modules: ['/some-project/src/some-file.js -> ' + '../components/hello'], + modules: ['some-file.js -> ' + '../components/hello'], }, }) somethingElse.dynamic('should not be transformed') diff --git a/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js b/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js index 2426dbf7ad..0692723edc 100644 --- a/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js +++ b/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js @@ -1,8 +1,7 @@ import dynamic from 'next/dynamic' - const DynamicComponent = dynamic(() => import('../components/hello'), { loadableGenerated: { webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], - modules: ['/some-project/src/some-file.js -> ' + '../components/hello'], + modules: ['some-file.js -> ' + '../components/hello'], }, }) diff --git a/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js b/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js index 458a440448..78e4ee3db4 100644 --- a/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js +++ b/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js @@ -1,12 +1,13 @@ import dynamic from 'next/dynamic' - const DynamicComponentWithCustomLoading = dynamic( () => import('../components/hello'), - { loading: () =>

...

}, + { + loading: () =>

...

, + }, { loadableGenerated: { webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], - modules: ['/some-project/src/some-file.js -> ' + '../components/hello'], + modules: ['some-file.js -> ' + '../components/hello'], }, loading: () =>

...

, }