next/dynamic: Accept pagesDir (#29055)

* Add pages_dir

* Add `pages_dir` to `next/dynamic` pass

* Dep

* Fix next/dynamic psss

* Fix

* Update test refs

* Add a test
This commit is contained in:
강동윤 2021-09-13 20:37:07 +09:00 committed by GitHub
parent c38e702347
commit fd2af1422d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 311 additions and 250 deletions

View file

@ -706,6 +706,7 @@ dependencies = [
"napi-build",
"napi-derive",
"path-clean",
"pathdiff",
"regex",
"retain_mut",
"serde",

View file

@ -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]

View file

@ -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<PathBuf>) -> impl Fold {
NextDynamicPatcher {
pages_dir,
filename,
dynamic_bindings: vec![],
}
}
#[derive(Debug)]
struct NextDynamicPatcher {
filename: FileName,
dynamic_bindings: Vec<Id>,
pages_dir: Option<PathBuf>,
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),
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()
}

View file

@ -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<PathBuf>,
}
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<JsObject> {
))
})
}
#[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);
}

View file

@ -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::<SingleThreadedComments>(
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::<SingleThreadedComments>(
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,
);
}

View file

@ -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'],
},
})

View file

@ -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')

View file

@ -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'],
},
})

View file

@ -1,12 +1,13 @@
import dynamic from 'next/dynamic'
const DynamicComponentWithCustomLoading = dynamic(
() => import('../components/hello'),
{ loading: () => <p>...</p> },
{
loading: () => <p>...</p>,
},
{
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: () => <p>...</p>,
}