Skip to content

Commit

Permalink
Add next dynamic swc transform (#27745)
Browse files Browse the repository at this point in the history
Ported `packages/next/build/babel/plugins/react-loadable-plugin.ts` to swc
  • Loading branch information
padmaia committed Aug 6, 2021
1 parent 62707d4 commit 7992b14
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/next/build/swc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use swc_common::{

mod amp_attributes;
mod hook_optimizer;
mod next_dynamic;
pub mod next_ssg;
mod transform;
mod util;
Expand Down
214 changes: 214 additions & 0 deletions packages/next/build/swc/src/next_dynamic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
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,
};
use swc_ecmascript::utils::{
ident::{Id, IdentLike},
HANDLER,
};
use swc_ecmascript::visit::{Fold, FoldWith};

pub fn next_dynamic(filename: FileName) -> impl Fold {
NextDynamicPatcher {
filename,
dynamic_bindings: vec![],
}
}

#[derive(Debug)]
struct NextDynamicPatcher {
filename: FileName,
dynamic_bindings: Vec<Id>,
}

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),
..
}) = &*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!("{} -> ", 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
}
}
9 changes: 8 additions & 1 deletion packages/next/build/swc/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::{
amp_attributes::amp_attributes,
complete_output, get_compiler,
hook_optimizer::hook_optimizer,
next_dynamic::next_dynamic,
next_ssg::next_ssg,
util::{CtxtExt, MapErr},
};
Expand Down Expand Up @@ -174,7 +175,13 @@ fn process_js_custom(
}
};
let config = BuiltConfig {
pass: chain!(hook_optimizer(), next_ssg(), amp_attributes(), config.pass),
pass: chain!(
hook_optimizer(),
next_ssg(),
amp_attributes(),
next_dynamic(source.name.clone()),
config.pass
),
syntax: config.syntax,
target: config.target,
minify: config.minify,
Expand Down
21 changes: 20 additions & 1 deletion packages/next/build/swc/tests/fixture.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use self::amp_attributes::amp_attributes;
use self::next_dynamic::next_dynamic;
use self::next_ssg::next_ssg;
use std::path::PathBuf;
use swc_common::{chain, comments::SingleThreadedComments};
use swc_common::{chain, comments::SingleThreadedComments, FileName};
use swc_ecma_transforms_testing::{test, test_fixture};
use swc_ecmascript::{
parser::{EsConfig, Syntax},
Expand All @@ -11,12 +12,15 @@ use testing::fixture;

#[path = "../src/amp_attributes.rs"]
mod amp_attributes;
#[path = "../src/next_dynamic.rs"]
mod next_dynamic;
#[path = "../src/next_ssg.rs"]
mod next_ssg;

fn syntax() -> Syntax {
Syntax::Es(EsConfig {
jsx: true,
dynamic_import: true,
..Default::default()
})
}
Expand All @@ -27,6 +31,21 @@ fn amp_attributes_fixture(input: PathBuf) {
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,
);
}

#[fixture("tests/fixture/ssg/**/input.js")]
fn next_ssg_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import dynamic1 from 'next/dynamic'
import dynamic2 from 'next/dynamic'

const DynamicComponent1 = dynamic1(() => import('../components/hello1'))
const DynamicComponent2 = dynamic2(() => import('../components/hello2'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import dynamic1 from 'next/dynamic'
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'],
},
})
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'],
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
import somethingElse from 'something-else'

const DynamicComponent = dynamic(() => import('../components/hello'))
somethingElse.dynamic('should not be transformed')
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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'],
},
})
somethingElse.dynamic('should not be transformed')
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('../components/hello'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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'],
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import dynamic from 'next/dynamic'

const DynamicComponentWithCustomLoading = dynamic(
() => import('../components/hello'),
{ loading: () => <p>...</p> }
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import dynamic from 'next/dynamic'

const DynamicComponentWithCustomLoading = dynamic(
() => import('../components/hello'),
{ loading: () => <p>...</p> },
{
loadableGenerated: {
webpack: () => [require.resolveWeak('/some-project/src/some-file.js')],
modules: ['/some-project/src/some-file.js -> ' + '../components/hello'],
},
loading: () => <p>...</p>,
}
)
Binary file modified packages/next/native/next-swc.darwin-x64.node
Binary file not shown.

0 comments on commit 7992b14

Please sign in to comment.