From 3c4eee875b070576f03a6ffb62d415d89f3c9c25 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 14 Dec 2021 11:33:04 +0100 Subject: [PATCH] fixes to allow lazy compilation for import() (#32441) --- .eslintignore | 1 + .prettierignore | 1 + packages/next-swc/crates/core/src/lib.rs | 10 +- .../next-swc/crates/core/src/next_dynamic.rs | 458 +++++++++--------- packages/next-swc/crates/core/tests/errors.rs | 2 + .../next-swc/crates/core/tests/fixture.rs | 20 +- .../duplicated-imports/output-dev.js | 12 + .../{output.js => output-prod.js} | 2 - .../member-with-same-name/output-dev.js | 8 + .../{output.js => output-prod.js} | 1 - .../next-dynamic/no-options/output-dev.js | 6 + .../no-options/{output.js => output-prod.js} | 1 - .../next-dynamic/with-options/output-dev.js | 10 + .../{output.js => output-prod.js} | 4 - .../next-dynamic/wrapped-import/output-dev.js | 12 + .../{output.js => output-prod.js} | 4 - packages/next-swc/crates/core/tests/full.rs | 1 + .../babel/plugins/react-loadable-plugin.ts | 50 +- packages/next/build/swc/options.js | 2 + packages/next/build/webpack-config.ts | 16 + .../webpack/plugins/react-loadable-plugin.ts | 15 +- .../webpack/packages/lazy-compilation-node.js | 40 ++ .../webpack/packages/lazy-compilation-web.js | 74 +++ .../compiled/webpack/lazy-compilation-node.js | 70 +-- .../compiled/webpack/lazy-compilation-web.js | 19 +- packages/next/shared/lib/loadable.js | 24 +- packages/next/taskfile.js | 2 +- scripts/check-pre-compiled.bat | 8 + scripts/check-pre-compiled.sh | 2 + .../basic-basepath/next-dynamic.test.ts | 4 +- test/development/basic/next-dynamic.test.ts | 4 +- .../app-document-add-hmr/test/index.test.js | 23 +- .../next-dynamic-lazy-compilation/.babelrc | 3 + .../apples/index.js | 1 + .../components/four.js | 3 + .../components/one.js | 6 + .../components/three.js | 3 + .../components/two.js | 6 + .../next.config.js | 8 + .../pages/index.js | 48 ++ .../test/index.test.js | 73 +++ test/unit/next-babel-loader-prod.test.ts | 2 +- 42 files changed, 687 insertions(+), 372 deletions(-) create mode 100644 packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-dev.js rename packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/{output.js => output-prod.js} (77%) create mode 100644 packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-dev.js rename packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/{output.js => output-prod.js} (83%) create mode 100644 packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-dev.js rename packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/{output.js => output-prod.js} (77%) create mode 100644 packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-dev.js rename packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/{output.js => output-prod.js} (75%) create mode 100644 packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-dev.js rename packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/{output.js => output-prod.js} (75%) create mode 100644 packages/next/bundles/webpack/packages/lazy-compilation-node.js create mode 100644 packages/next/bundles/webpack/packages/lazy-compilation-web.js create mode 100644 scripts/check-pre-compiled.bat create mode 100644 test/integration/next-dynamic-lazy-compilation/.babelrc create mode 100644 test/integration/next-dynamic-lazy-compilation/apples/index.js create mode 100644 test/integration/next-dynamic-lazy-compilation/components/four.js create mode 100644 test/integration/next-dynamic-lazy-compilation/components/one.js create mode 100644 test/integration/next-dynamic-lazy-compilation/components/three.js create mode 100644 test/integration/next-dynamic-lazy-compilation/components/two.js create mode 100644 test/integration/next-dynamic-lazy-compilation/next.config.js create mode 100644 test/integration/next-dynamic-lazy-compilation/pages/index.js create mode 100644 test/integration/next-dynamic-lazy-compilation/test/index.test.js diff --git a/.eslintignore b/.eslintignore index 40f0064d34df0c3..c0a1165bb2d41ff 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,6 +12,7 @@ examples/with-jest/** examples/with-mobx-state-tree/** examples/with-mobx/** packages/next/bundles/webpack/packages/*.runtime.js +packages/next/bundles/webpack/packages/lazy-compilation-*.js packages/next/compiled/**/* packages/react-refresh-utils/**/*.js packages/react-dev-overlay/lib/** diff --git a/.prettierignore b/.prettierignore index f63e5c00ba3f4c4..3f2ee9a47ef8c9a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ node_modules **/_next/** **/dist/** packages/next/bundles/webpack/packages/*.runtime.js +packages/next/bundles/webpack/packages/lazy-compilation-*.js packages/next/compiled/** packages/react-refresh-utils/**/*.js packages/react-refresh-utils/**/*.d.ts diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 34ae493397ece91..e2ba8d623dcdae2 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -79,6 +79,9 @@ pub struct TransformOptions { #[serde(default)] pub is_development: bool, + #[serde(default)] + pub is_server: bool, + #[serde(default)] pub styled_components: Option, @@ -113,7 +116,12 @@ pub fn custom_before_pass(file: Arc, opts: &TransformOptions) -> imp }, Optional::new(next_ssg::next_ssg(), !opts.disable_next_ssg), amp_attributes::amp_attributes(), - next_dynamic::next_dynamic(file.name.clone(), opts.pages_dir.clone()), + next_dynamic::next_dynamic( + opts.is_development, + opts.is_server, + file.name.clone(), + opts.pages_dir.clone() + ), Optional::new( page_config::page_config(opts.is_development, opts.is_page_file), !opts.disable_page_config diff --git a/packages/next-swc/crates/core/src/next_dynamic.rs b/packages/next-swc/crates/core/src/next_dynamic.rs index 37ea743f97a844f..dd67b916e13bbc0 100644 --- a/packages/next-swc/crates/core/src/next_dynamic.rs +++ b/packages/next-swc/crates/core/src/next_dynamic.rs @@ -4,93 +4,105 @@ 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, pages_dir: Option) -> impl Fold { - NextDynamicPatcher { - pages_dir, - filename, - dynamic_bindings: vec![], - is_next_dynamic_first_arg: false, - dynamically_imported_specifier: None, - } +pub fn next_dynamic( + is_development: bool, + is_server: bool, + filename: FileName, + pages_dir: Option, +) -> impl Fold { + NextDynamicPatcher { + is_development, + is_server, + pages_dir, + filename, + dynamic_bindings: vec![], + is_next_dynamic_first_arg: false, + dynamically_imported_specifier: None, + } } #[derive(Debug)] struct NextDynamicPatcher { - pages_dir: Option, - filename: FileName, - dynamic_bindings: Vec, - is_next_dynamic_first_arg: bool, - dynamically_imported_specifier: Option, + is_development: bool, + is_server: bool, + pages_dir: Option, + filename: FileName, + dynamic_bindings: Vec, + is_next_dynamic_first_arg: bool, + dynamically_imported_specifier: Option, } 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()); + 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 - } + decl + } - fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr { - if self.is_next_dynamic_first_arg { - if let ExprOrSuper::Expr(e) = &expr.callee { - if let Expr::Ident(Ident { sym, .. }) = &**e { - if sym == "import" { - if let Expr::Lit(Lit::Str(Str { value, .. })) = &*expr.args[0].expr { - self.dynamically_imported_specifier = Some(value.to_string()); + fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr { + if self.is_next_dynamic_first_arg { + if let ExprOrSuper::Expr(e) = &expr.callee { + if let Expr::Ident(Ident { sym, .. }) = &**e { + if sym == "import" { + if let Expr::Lit(Lit::Str(Str { value, .. })) = &*expr.args[0].expr { + self.dynamically_imported_specifier = Some(value.to_string()); + } + } + } } - } + return expr.fold_children_with(self); } - } - return expr.fold_children_with(self); - } - 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() - }); - return expr; - } else if expr.args.len() > 2 { - HANDLER.with(|handler| { - handler - .struct_span_err(identifier.span, "next/dynamic only accepts 2 arguments") - .emit() - }); - return expr; - } - if expr.args.len() == 2 { - match &*expr.args[1].expr { - Expr::Object(_) => {}, - _ => { - HANDLER.with(|handler| { + 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() + }); + return expr; + } else if expr.args.len() > 2 { + HANDLER.with(|handler| { + handler + .struct_span_err( + identifier.span, + "next/dynamic only accepts 2 arguments", + ) + .emit() + }); + return expr; + } + if expr.args.len() == 2 { + match &*expr.args[1].expr { + Expr::Object(_) => {} + _ => { + HANDLER.with(|handler| { handler .struct_span_err( identifier.span, @@ -98,170 +110,182 @@ impl Fold for NextDynamicPatcher { ) .emit(); }); - return expr; - } - } - } + return expr; + } + } + } - self.is_next_dynamic_first_arg = true; - expr.args[0].expr = expr.args[0].expr.clone().fold_with(self); - self.is_next_dynamic_first_arg = false; + self.is_next_dynamic_first_arg = true; + expr.args[0].expr = expr.args[0].expr.clone().fold_with(self); + self.is_next_dynamic_first_arg = false; + if let None = self.dynamically_imported_specifier { + return expr; + } - if let None = self.dynamically_imported_specifier { - return expr; - } + // dev client or server: + // loadableGenerated: { + // modules: + // ["/project/src/file-being-transformed.js -> " + '../components/hello'] } - // 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 - .dynamically_imported_specifier - .as_ref() - .unwrap() - .clone() - .into(), - span: DUMMY_SP, - kind: StrKind::Synthesized {}, - has_escape: false, - }))), - spread: None, - }], + // prod client + // loadableGenerated: { + // webpack: () => [require.resolveWeak('../components/hello')], + let generated = Box::new(Expr::Object(ObjectLit { 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: self - .dynamically_imported_specifier - .as_ref() - .unwrap() - .clone() - .into(), - span: DUMMY_SP, - kind: StrKind::Normal { - contains_quote: false, + props: if self.is_development || self.is_server { + vec![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: self + .dynamically_imported_specifier + .as_ref() + .unwrap() + .clone() + .into(), + span: DUMMY_SP, + kind: StrKind::Normal { + contains_quote: false, + }, + has_escape: false, + }))), + })), + spread: None, + })], + span: DUMMY_SP, + })), + })))] + } else { + 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 + .dynamically_imported_specifier + .as_ref() + .unwrap() + .clone() + .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, + })), + })))] }, - 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, - })))]; + 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()); - } - } + 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, - })), - }; + let second_arg = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props, + })), + }; - if expr.args.len() == 2 { - expr.args[1] = second_arg; - } else { - expr.args.push(second_arg) - } - self.dynamically_imported_specifier = None; + if expr.args.len() == 2 { + expr.args[1] = second_arg; + } else { + expr.args.push(second_arg) + } + self.dynamically_imported_specifier = None; + } + } } - } + expr } - expr - } } fn rel_filename(base: Option<&Path>, file: &FileName) -> String { - let base = match base { - Some(v) => v, - None => return file.to_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 file = match file { + FileName::Real(v) => v, + _ => { + return file.to_string(); + } + }; - let rel_path = diff_paths(&file, base); + let rel_path = diff_paths(&file, base); - let rel_path = match rel_path { - Some(v) => v, - None => return file.display().to_string(), - }; + let rel_path = match rel_path { + Some(v) => v, + None => return file.display().to_string(), + }; - rel_path.display().to_string() + rel_path.display().to_string() } diff --git a/packages/next-swc/crates/core/tests/errors.rs b/packages/next-swc/crates/core/tests/errors.rs index a488d8158a40b95..3a003879523ad86 100644 --- a/packages/next-swc/crates/core/tests/errors.rs +++ b/packages/next-swc/crates/core/tests/errors.rs @@ -32,6 +32,8 @@ fn next_dynamic_errors(input: PathBuf) { syntax(), &|_tr| { next_dynamic( + true, + false, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 5483f11442cd6be..863cfb5e5e01845 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -33,17 +33,33 @@ fn amp_attributes_fixture(input: PathBuf) { #[fixture("tests/fixture/next-dynamic/**/input.js")] fn next_dynamic_fixture(input: PathBuf) { - let output = input.parent().unwrap().join("output.js"); + let output_dev = input.parent().unwrap().join("output-dev.js"); + let output_prod = input.parent().unwrap().join("output-prod.js"); test_fixture( syntax(), &|_tr| { next_dynamic( + true, + false, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) }, &input, - &output, + &output_dev, + ); + test_fixture( + syntax(), + &|_tr| { + next_dynamic( + false, + false, + FileName::Real(PathBuf::from("/some-project/src/some-file.js")), + Some("/some-project/src".into()), + ) + }, + &input, + &output_prod, ); } diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-dev.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-dev.js new file mode 100644 index 000000000000000..871bf8fb3896749 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-dev.js @@ -0,0 +1,12 @@ +import dynamic1 from 'next/dynamic' +import dynamic2 from 'next/dynamic' +const DynamicComponent1 = dynamic1(() => import('../components/hello1'), { + loadableGenerated: { + modules: ['some-file.js -> ' + '../components/hello1'], + }, +}) +const DynamicComponent2 = dynamic2(() => import('../components/hello2'), { + loadableGenerated: { + modules: ['some-file.js -> ' + '../components/hello2'], + }, +}) diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-prod.js similarity index 77% rename from packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output.js rename to packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-prod.js index c7b0f971ca69a56..b39ea332cfc4bc2 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/duplicated-imports/output-prod.js @@ -3,12 +3,10 @@ import dynamic2 from 'next/dynamic' const DynamicComponent1 = dynamic1(() => import('../components/hello1'), { loadableGenerated: { webpack: () => [require.resolveWeak('../components/hello1')], - modules: ['some-file.js -> ' + '../components/hello1'], }, }) const DynamicComponent2 = dynamic2(() => import('../components/hello2'), { loadableGenerated: { webpack: () => [require.resolveWeak('../components/hello2')], - modules: ['some-file.js -> ' + '../components/hello2'], }, }) diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-dev.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-dev.js new file mode 100644 index 000000000000000..c3f5016f39f3782 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-dev.js @@ -0,0 +1,8 @@ +import dynamic from 'next/dynamic' +import somethingElse from 'something-else' +const DynamicComponent = dynamic(() => import('../components/hello'), { + loadableGenerated: { + modules: ['some-file.js -> ' + '../components/hello'], + }, +}) +somethingElse.dynamic('should not be transformed') diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-prod.js similarity index 83% rename from packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output.js rename to packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-prod.js index 37942c10fe974a0..57b44c9e010f99a 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/member-with-same-name/output-prod.js @@ -3,7 +3,6 @@ import somethingElse from 'something-else' const DynamicComponent = dynamic(() => import('../components/hello'), { loadableGenerated: { webpack: () => [require.resolveWeak('../components/hello')], - modules: ['some-file.js -> ' + '../components/hello'], }, }) somethingElse.dynamic('should not be transformed') diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-dev.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-dev.js new file mode 100644 index 000000000000000..21763e18aa102d0 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-dev.js @@ -0,0 +1,6 @@ +import dynamic from 'next/dynamic' +const DynamicComponent = dynamic(() => import('../components/hello'), { + loadableGenerated: { + modules: ['some-file.js -> ' + '../components/hello'], + }, +}) diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-prod.js similarity index 77% rename from packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output.js rename to packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-prod.js index 588306b604cbd7c..345a3aa8aa80b97 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/no-options/output-prod.js @@ -2,6 +2,5 @@ import dynamic from 'next/dynamic' const DynamicComponent = dynamic(() => import('../components/hello'), { loadableGenerated: { webpack: () => [require.resolveWeak('../components/hello')], - modules: ['some-file.js -> ' + '../components/hello'], }, }) diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-dev.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-dev.js new file mode 100644 index 000000000000000..7c9d62d7225a247 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-dev.js @@ -0,0 +1,10 @@ +import dynamic from "next/dynamic"; +const DynamicComponentWithCustomLoading = dynamic(()=>import("../components/hello") +, { + loadableGenerated: { + modules: [ + "some-file.js -> " + "../components/hello" + ] + }, + loading: ()=>

...

+}); diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-prod.js similarity index 75% rename from packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output.js rename to packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-prod.js index 9cc98fe22b4ca08..cee511f2ded65ea 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/with-options/output-prod.js @@ -5,10 +5,6 @@ const DynamicComponentWithCustomLoading = dynamic(()=>import("../components/hell webpack: ()=>[ require.resolveWeak("../components/hello") ] - , - modules: [ - "some-file.js -> " + "../components/hello" - ] }, loading: ()=>

...

}); diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-dev.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-dev.js new file mode 100644 index 000000000000000..4bc3eae2c5ae283 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-dev.js @@ -0,0 +1,12 @@ +import dynamic from "next/dynamic"; +const DynamicComponent = dynamic(()=>handleImport(import("./components/hello")) +, { + loadableGenerated: { + modules: [ + "some-file.js -> " + "./components/hello" + ] + }, + loading: ()=>null + , + ssr: false +}); \ No newline at end of file diff --git a/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output.js b/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-prod.js similarity index 75% rename from packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output.js rename to packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-prod.js index 935a0e6b265eca7..f0c6a05c5433867 100644 --- a/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output.js +++ b/packages/next-swc/crates/core/tests/fixture/next-dynamic/wrapped-import/output-prod.js @@ -5,10 +5,6 @@ const DynamicComponent = dynamic(()=>handleImport(import("./components/hello")) webpack: ()=>[ require.resolveWeak("./components/hello") ] - , - modules: [ - "some-file.js -> " + "./components/hello" - ] }, loading: ()=>null , diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs index 2ef9b8b446882d3..773aad547d8d39d 100644 --- a/packages/next-swc/crates/core/tests/full.rs +++ b/packages/next-swc/crates/core/tests/full.rs @@ -55,6 +55,7 @@ fn test(input: &Path, minify: bool) { pages_dir: None, is_page_file: false, is_development: true, + is_server: false, styled_components: Some(assert_json("{}")), remove_console: None, react_remove_properties: None, diff --git a/packages/next/build/babel/plugins/react-loadable-plugin.ts b/packages/next/build/babel/plugins/react-loadable-plugin.ts index 61a3b3bf26b6a57..4169ae4e4de0602 100644 --- a/packages/next/build/babel/plugins/react-loadable-plugin.ts +++ b/packages/next/build/babel/plugins/react-loadable-plugin.ts @@ -168,29 +168,35 @@ export default function ({ options.node.properties.push( t.objectProperty( t.identifier('loadableGenerated'), - t.objectExpression([ - t.objectProperty( - t.identifier('webpack'), - t.arrowFunctionExpression( - [], - t.arrayExpression( - dynamicImports.map((dynamicImport) => { - return t.callExpression( - t.memberExpression( - t.identifier('require'), - t.identifier('resolveWeak') - ), - [dynamicImport] + t.objectExpression( + state.file.opts.caller?.isDev || + state.file.opts.caller?.isServer + ? [ + t.objectProperty( + t.identifier('modules'), + t.arrayExpression(dynamicKeys) + ), + ] + : [ + t.objectProperty( + t.identifier('webpack'), + t.arrowFunctionExpression( + [], + t.arrayExpression( + dynamicImports.map((dynamicImport) => { + return t.callExpression( + t.memberExpression( + t.identifier('require'), + t.identifier('resolveWeak') + ), + [dynamicImport] + ) + }) + ) ) - }) - ) - ) - ), - t.objectProperty( - t.identifier('modules'), - t.arrayExpression(dynamicKeys) - ), - ]) + ), + ] + ) ) ) diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 8ebbf96fce006a5..f163fc110c9f797 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -141,6 +141,7 @@ export function getLoaderSWCOptions({ disableNextSsg: true, disablePageConfig: true, isDevelopment: development, + isServer, pagesDir, isPageFile, env: { @@ -165,6 +166,7 @@ export function getLoaderSWCOptions({ : {}), disableNextSsg: !isPageFile, isDevelopment: development, + isServer, pagesDir, isPageFile, } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index c50df8b8c0340ca..11d14bec1bb1479 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1402,6 +1402,7 @@ export default async function getBaseWebpackConfig( runtimeAsset: hasConcurrentFeatures ? `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js` : undefined, + dev, }), targetWeb && new DropClientPage(), config.outputFileTracing && @@ -1733,6 +1734,21 @@ export default async function getBaseWebpackConfig( devtoolRevertWarning(originalDevtool) } + // eslint-disable-next-line no-shadow + const webpack5Config = webpackConfig as webpack5.Configuration + + // disable lazy compilation of entries as next.js has it's own method here + if (webpack5Config.experiments?.lazyCompilation === true) { + webpack5Config.experiments.lazyCompilation = { + entries: false, + } + } else if ( + typeof webpack5Config.experiments?.lazyCompilation === 'object' && + webpack5Config.experiments.lazyCompilation.entries !== false + ) { + webpack5Config.experiments.lazyCompilation.entries = false + } + if (typeof (webpackConfig as any).then === 'function') { console.warn( '> Promise returned in next config. https://nextjs.org/docs/messages/promise-in-next-config' diff --git a/packages/next/build/webpack/plugins/react-loadable-plugin.ts b/packages/next/build/webpack/plugins/react-loadable-plugin.ts index 63b33469c0b5b29..18fd1e283eec28d 100644 --- a/packages/next/build/webpack/plugins/react-loadable-plugin.ts +++ b/packages/next/build/webpack/plugins/react-loadable-plugin.ts @@ -53,7 +53,8 @@ function getChunkGroupFromBlock( function buildManifest( _compiler: webpack.Compiler, compilation: webpack.compilation.Compilation, - pagesDir: string + pagesDir: string, + dev: boolean ) { let manifest: { [k: string]: { id: string | number; files: string[] } } = {} @@ -125,7 +126,7 @@ function buildManifest( // next/dynamic so they are loaded by the same technique // add the id and files to the manifest - const id = getModuleId(compilation, module) + const id = dev ? key : getModuleId(compilation, module) manifest[key] = { id, files: Array.from(files) } } } @@ -146,19 +147,27 @@ export class ReactLoadablePlugin { private filename: string private pagesDir: string private runtimeAsset?: string + private dev: boolean constructor(opts: { filename: string pagesDir: string runtimeAsset?: string + dev: boolean }) { this.filename = opts.filename this.pagesDir = opts.pagesDir this.runtimeAsset = opts.runtimeAsset + this.dev = opts.dev } createAssets(compiler: any, compilation: any, assets: any) { - const manifest = buildManifest(compiler, compilation, this.pagesDir) + const manifest = buildManifest( + compiler, + compilation, + this.pagesDir, + this.dev + ) // @ts-ignore: TODO: remove when webpack 5 is stable assets[this.filename] = new sources.RawSource( JSON.stringify(manifest, null, 2) diff --git a/packages/next/bundles/webpack/packages/lazy-compilation-node.js b/packages/next/bundles/webpack/packages/lazy-compilation-node.js new file mode 100644 index 000000000000000..5dd417b7b0aded7 --- /dev/null +++ b/packages/next/bundles/webpack/packages/lazy-compilation-node.js @@ -0,0 +1,40 @@ +/* global __resourceQuery */ + +"use strict"; + +var urlBase = decodeURIComponent(__resourceQuery.slice(1)); +exports.keepAlive = function (options) { + var data = options.data; + var onError = options.onError; + var active = options.active; + var module = options.module; + var response; + var request = ( + urlBase.startsWith("https") ? require("https") : require("http") + ).request( + urlBase + data, + { + agent: false, + headers: { accept: "text/event-stream" } + }, + function (res) { + response = res; + response.on("error", errorHandler); + if (!active && !module.hot) { + console.log( + "Hot Module Replacement is not enabled. Waiting for process restart..." + ); + } + } + ); + function errorHandler(err) { + err.message = + "Problem communicating active modules to the server: " + err.message; + onError(err); + } + request.on("error", errorHandler); + request.end(); + return function () { + response.destroy(); + }; +}; diff --git a/packages/next/bundles/webpack/packages/lazy-compilation-web.js b/packages/next/bundles/webpack/packages/lazy-compilation-web.js new file mode 100644 index 000000000000000..62d955c5a22bf9c --- /dev/null +++ b/packages/next/bundles/webpack/packages/lazy-compilation-web.js @@ -0,0 +1,74 @@ +/* global __resourceQuery */ + +"use strict"; + +if (typeof EventSource !== "function") { + throw new Error( + "Environment doesn't support lazy compilation (requires EventSource)" + ); +} + +var urlBase = decodeURIComponent(__resourceQuery.slice(1)); +var activeEventSource; +var activeKeys = new Map(); +var errorHandlers = new Set(); + +var updateEventSource = function updateEventSource() { + if (activeEventSource) activeEventSource.close(); + if (activeKeys.size) { + activeEventSource = new EventSource( + urlBase + Array.from(activeKeys.keys()).join("@") + ); + activeEventSource.onerror = function (event) { + errorHandlers.forEach(function (onError) { + onError( + new Error( + "Problem communicating active modules to the server: " + + event.message + + " " + + event.filename + + ":" + + event.lineno + + ":" + + event.colno + + " " + + event.error + ) + ); + }); + }; + } else { + activeEventSource = undefined; + } +}; + +exports.keepAlive = function (options) { + var data = options.data; + var onError = options.onError; + var active = options.active; + var module = options.module; + errorHandlers.add(onError); + var value = activeKeys.get(data) || 0; + activeKeys.set(data, value + 1); + if (value === 0) { + updateEventSource(); + } + if (!active && !module.hot) { + console.log( + "Hot Module Replacement is not enabled. Waiting for process restart..." + ); + } + + return function () { + errorHandlers.delete(onError); + setTimeout(function () { + var value = activeKeys.get(data); + if (value === 1) { + activeKeys.delete(data); + updateEventSource(); + } else { + activeKeys.set(data, value - 1); + } + }, 1000); + }; +}; diff --git a/packages/next/compiled/webpack/lazy-compilation-node.js b/packages/next/compiled/webpack/lazy-compilation-node.js index fbfffb4c9081fb4..5dd417b7b0aded7 100644 --- a/packages/next/compiled/webpack/lazy-compilation-node.js +++ b/packages/next/compiled/webpack/lazy-compilation-node.js @@ -1,67 +1,6 @@ -/******/ (function() { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 685: -/***/ (function(module) { - -module.exports = require("http"); - -/***/ }), - -/***/ 687: -/***/ (function(module) { - -module.exports = require("https"); - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __nccwpck_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ var threw = true; -/******/ try { -/******/ __webpack_modules__[moduleId](module, module.exports, __nccwpck_require__); -/******/ threw = false; -/******/ } finally { -/******/ if(threw) delete __webpack_module_cache__[moduleId]; -/******/ } -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat */ -/******/ -/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. -!function() { -var exports = __webpack_exports__; -var __resourceQuery = ""; /* global __resourceQuery */ - +"use strict"; var urlBase = decodeURIComponent(__resourceQuery.slice(1)); exports.keepAlive = function (options) { @@ -71,7 +10,7 @@ exports.keepAlive = function (options) { var module = options.module; var response; var request = ( - urlBase.startsWith("https") ? __nccwpck_require__(687) : __nccwpck_require__(685) + urlBase.startsWith("https") ? require("https") : require("http") ).request( urlBase + data, { @@ -99,8 +38,3 @@ exports.keepAlive = function (options) { response.destroy(); }; }; - -}(); -module.exports = __webpack_exports__; -/******/ })() -; \ No newline at end of file diff --git a/packages/next/compiled/webpack/lazy-compilation-web.js b/packages/next/compiled/webpack/lazy-compilation-web.js index af34c5a5a5e59f8..62d955c5a22bf9c 100644 --- a/packages/next/compiled/webpack/lazy-compilation-web.js +++ b/packages/next/compiled/webpack/lazy-compilation-web.js @@ -1,18 +1,6 @@ -/******/ (function() { // webpackBootstrap -/******/ "use strict"; -/******/ /* webpack/runtime/compat */ -/******/ -/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry need to be wrapped in an IIFE because it uses a non-standard name for the exports (exports). -!function() { -var exports = __webpack_exports__; -var __resourceQuery = ""; /* global __resourceQuery */ - +"use strict"; if (typeof EventSource !== "function") { throw new Error( @@ -84,8 +72,3 @@ exports.keepAlive = function (options) { }, 1000); }; }; - -}(); -module.exports = __webpack_exports__; -/******/ })() -; \ No newline at end of file diff --git a/packages/next/shared/lib/loadable.js b/packages/next/shared/lib/loadable.js index fbeaf9480f6f4e6..ea2a1fe75450035 100644 --- a/packages/next/shared/lib/loadable.js +++ b/packages/next/shared/lib/loadable.js @@ -95,21 +95,17 @@ function createLoadableComponent(loadFn, options) { } // Client only - if ( - !initialized && - typeof window !== 'undefined' && - typeof opts.webpack === 'function' && - typeof require.resolveWeak === 'function' && - !opts.suspense - ) { - const moduleIds = opts.webpack() - READY_INITIALIZERS.push((ids) => { - for (const moduleId of moduleIds) { - if (ids.indexOf(moduleId) !== -1) { - return init() + if (!initialized && typeof window !== 'undefined' && !opts.suspense) { + const moduleIds = opts.webpack ? opts.webpack() : opts.modules + if (moduleIds) { + READY_INITIALIZERS.push((ids) => { + for (const moduleId of moduleIds) { + if (ids.indexOf(moduleId) !== -1) { + return init() + } } - } - }) + }) + } } function LoadableImpl(props, ref) { diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 570a7c01babf3fa..e26df1b7f92d15c 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1007,7 +1007,6 @@ export async function ncc(task, opts) { 'ncc_unistore', 'ncc_web_vitals', 'ncc_webpack_bundle5', - 'ncc_webpack_bundle_packages', 'ncc_webpack_sources1', 'ncc_webpack_sources3', 'ncc_ws', @@ -1021,6 +1020,7 @@ export async function ncc(task, opts) { ], opts ) + await task.parallel(['ncc_webpack_bundle_packages'], opts) await task.parallel(['ncc_babel_bundle_packages'], opts) await task.parallel(['copy_react_server_dom_webpack']) } diff --git a/scripts/check-pre-compiled.bat b/scripts/check-pre-compiled.bat new file mode 100644 index 000000000000000..87d58592b37d7eb --- /dev/null +++ b/scripts/check-pre-compiled.bat @@ -0,0 +1,8 @@ +copy node_modules\webpack5\lib\hmr\HotModuleReplacement.runtime.js packages\next\bundles\webpack\packages\ +copy node_modules\webpack5\lib\hmr\JavascriptHotModuleReplacement.runtime.js packages\next\bundles\webpack\packages\ +copy node_modules\webpack5\hot\lazy-compilation-node.js packages\next\bundles\webpack\packages\ +copy node_modules\webpack5\hot\lazy-compilation-web.js packages\next\bundles\webpack\packages\ +yarn --cwd packages/next ncc-compiled + +rem Make sure to exit with 1 if there are changes after running ncc-compiled +rem step to ensure we get any changes committed diff --git a/scripts/check-pre-compiled.sh b/scripts/check-pre-compiled.sh index f153e611a4be248..e860874e2e08260 100755 --- a/scripts/check-pre-compiled.sh +++ b/scripts/check-pre-compiled.sh @@ -2,6 +2,8 @@ cp node_modules/webpack5/lib/hmr/HotModuleReplacement.runtime.js packages/next/bundles/webpack/packages/ cp node_modules/webpack5/lib/hmr/JavascriptHotModuleReplacement.runtime.js packages/next/bundles/webpack/packages/ +cp node_modules/webpack5/hot/lazy-compilation-node.js packages/next/bundles/webpack/packages/ +cp node_modules/webpack5/hot/lazy-compilation-web.js packages/next/bundles/webpack/packages/ yarn --cwd packages/next ncc-compiled # Make sure to exit with 1 if there are changes after running ncc-compiled diff --git a/test/development/basic-basepath/next-dynamic.test.ts b/test/development/basic-basepath/next-dynamic.test.ts index 84e10fd4f7bc414..1d96cb64a856718 100644 --- a/test/development/basic-basepath/next-dynamic.test.ts +++ b/test/development/basic-basepath/next-dynamic.test.ts @@ -32,7 +32,7 @@ describe('basic next/dynamic usage', () => { const $ = await get$('/docs/dynamic/ssr') // Make sure the client side knows it has to wait for the bundle expect(JSON.parse($('#__NEXT_DATA__').html()).dynamicIds).toContain( - './components/hello1.js' + 'dynamic/ssr.js -> ../../components/hello1' ) expect($('body').text()).toMatch(/Hello World 1/) }) @@ -41,7 +41,7 @@ describe('basic next/dynamic usage', () => { const $ = await get$('/docs/dynamic/function') // Make sure the client side knows it has to wait for the bundle expect(JSON.parse($('#__NEXT_DATA__').html()).dynamicIds).toContain( - './components/hello1.js' + 'dynamic/function.js -> ../../components/hello1' ) expect($('body').text()).toMatch(/Hello World 1/) }) diff --git a/test/development/basic/next-dynamic.test.ts b/test/development/basic/next-dynamic.test.ts index ceb228dcd202257..b26c4ecb7c7bb10 100644 --- a/test/development/basic/next-dynamic.test.ts +++ b/test/development/basic/next-dynamic.test.ts @@ -29,7 +29,7 @@ describe('basic next/dynamic usage', () => { const $ = await get$('/dynamic/ssr') // Make sure the client side knows it has to wait for the bundle expect(JSON.parse($('#__NEXT_DATA__').html()).dynamicIds).toContain( - './components/hello1.js' + 'dynamic/ssr.js -> ../../components/hello1' ) expect($('body').text()).toMatch(/Hello World 1/) }) @@ -38,7 +38,7 @@ describe('basic next/dynamic usage', () => { const $ = await get$('/dynamic/function') // Make sure the client side knows it has to wait for the bundle expect(JSON.parse($('#__NEXT_DATA__').html()).dynamicIds).toContain( - './components/hello1.js' + 'dynamic/function.js -> ../../components/hello1' ) expect($('body').text()).toMatch(/Hello World 1/) }) diff --git a/test/integration/app-document-add-hmr/test/index.test.js b/test/integration/app-document-add-hmr/test/index.test.js index ac7c86769c4f229..af24d990203a246 100644 --- a/test/integration/app-document-add-hmr/test/index.test.js +++ b/test/integration/app-document-add-hmr/test/index.test.js @@ -7,7 +7,6 @@ import { killApp, findPort, launchApp, check } from 'next-test-utils' const appDir = join(__dirname, '../') const appPage = join(appDir, 'pages/_app.js') -const indexPage = join(appDir, 'pages/index.js') const documentPage = join(appDir, 'pages/_document.js') let appPort @@ -21,10 +20,8 @@ describe('_app/_document add HMR', () => { afterAll(() => killApp(app)) it('should HMR when _app is added', async () => { - let indexContent = await fs.readFile(indexPage) + const browser = await webdriver(appPort, '/') try { - const browser = await webdriver(appPort, '/') - const html = await browser.eval('document.documentElement.innerHTML') expect(html).not.toContain('custom _app') expect(html).toContain('index page') @@ -50,16 +47,19 @@ describe('_app/_document add HMR', () => { : html }, 'success') } finally { - await fs.writeFile(indexPage, indexContent) await fs.remove(appPage) + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return !html.includes('custom _app') && html.includes('index page') + ? 'restored' + : html + }, 'restored') } }) it('should HMR when _document is added', async () => { - let indexContent = await fs.readFile(indexPage) + const browser = await webdriver(appPort, '/') try { - const browser = await webdriver(appPort, '/') - const html = await browser.eval('document.documentElement.innerHTML') expect(html).not.toContain('custom _document') expect(html).toContain('index page') @@ -101,8 +101,13 @@ describe('_app/_document add HMR', () => { : html }, 'success') } finally { - await fs.writeFile(indexPage, indexContent) await fs.remove(documentPage) + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return !html.includes('custom _document') && html.includes('index page') + ? 'restored' + : html + }, 'restored') } }) }) diff --git a/test/integration/next-dynamic-lazy-compilation/.babelrc b/test/integration/next-dynamic-lazy-compilation/.babelrc new file mode 100644 index 000000000000000..1ff94f7ed28e16b --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["next/babel"] +} diff --git a/test/integration/next-dynamic-lazy-compilation/apples/index.js b/test/integration/next-dynamic-lazy-compilation/apples/index.js new file mode 100644 index 000000000000000..44484a5709f812e --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/apples/index.js @@ -0,0 +1 @@ +export default 'foobar' diff --git a/test/integration/next-dynamic-lazy-compilation/components/four.js b/test/integration/next-dynamic-lazy-compilation/components/four.js new file mode 100644 index 000000000000000..16e94e71a96f46d --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/components/four.js @@ -0,0 +1,3 @@ +export default () => { + return '4' +} diff --git a/test/integration/next-dynamic-lazy-compilation/components/one.js b/test/integration/next-dynamic-lazy-compilation/components/one.js new file mode 100644 index 000000000000000..e918bc462a90148 --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/components/one.js @@ -0,0 +1,6 @@ +import something from '../apples' +export default () => { + // have to do something with module so it is not tree shaken + console.log(something) + return '1' +} diff --git a/test/integration/next-dynamic-lazy-compilation/components/three.js b/test/integration/next-dynamic-lazy-compilation/components/three.js new file mode 100644 index 000000000000000..922a87d54f50c6a --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/components/three.js @@ -0,0 +1,3 @@ +export default () => { + return '3' +} diff --git a/test/integration/next-dynamic-lazy-compilation/components/two.js b/test/integration/next-dynamic-lazy-compilation/components/two.js new file mode 100644 index 000000000000000..86e1dd0470a3348 --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/components/two.js @@ -0,0 +1,6 @@ +import something from '../apples' +export default () => { + // have to do something with module so it is not tree shaken + console.log(something) + return '2' +} diff --git a/test/integration/next-dynamic-lazy-compilation/next.config.js b/test/integration/next-dynamic-lazy-compilation/next.config.js new file mode 100644 index 000000000000000..48e0def90717358 --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/next.config.js @@ -0,0 +1,8 @@ +module.exports = { + webpack(config, { isServer, dev }) { + if (!isServer && dev) { + config.experiments.lazyCompilation = true + } + return config + }, +} diff --git a/test/integration/next-dynamic-lazy-compilation/pages/index.js b/test/integration/next-dynamic-lazy-compilation/pages/index.js new file mode 100644 index 000000000000000..19e451f77f34b3e --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/pages/index.js @@ -0,0 +1,48 @@ +import dynamic from 'next/dynamic' +import { useState, useEffect } from 'react' +import FourDirect from '../components/four' + +const One = dynamic(() => import('../components/one')) +const Two = dynamic(() => import('../components/two')) +const Three = dynamic(() => import('../components/three')) +const Four = dynamic(() => import('../components/four')) + +if (typeof window !== 'undefined') { + window.caughtErrors = '' + const origError = console.error + + console.error = function (...args) { + window.caughtErrors += args.join(' ') + origError(...args) + } +} + +const BEFORE_HYDRATION = + typeof document !== 'undefined' && document.getElementById('foo').innerHTML + +const Index = () => { + const [firstRender, setFirstRender] = useState('the-server-value') + const [beforeHydration, setBeforeHydration] = useState( + 'the-second-server-value' + ) + useEffect(() => { + setFirstRender(document.getElementById('foo').innerHTML) + setBeforeHydration(BEFORE_HYDRATION) + }, []) + + return ( + <> +
+ Index + + + + + +
+
{firstRender}
+
{beforeHydration}
+ + ) +} +export default Index diff --git a/test/integration/next-dynamic-lazy-compilation/test/index.test.js b/test/integration/next-dynamic-lazy-compilation/test/index.test.js new file mode 100644 index 000000000000000..e76b1176b91ee68 --- /dev/null +++ b/test/integration/next-dynamic-lazy-compilation/test/index.test.js @@ -0,0 +1,73 @@ +/* eslint-env jest */ + +import webdriver from 'next-webdriver' +import { join } from 'path' +import { + renderViaHTTP, + findPort, + launchApp, + killApp, + runNextCommand, + nextServer, + startApp, + stopApp, +} from 'next-test-utils' + +let app +let appPort +let server +const appDir = join(__dirname, '../') + +function runTests() { + it('should render server value', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toMatch(/the-server-value/i) + expect(html).toMatch(/the-second-server-value/i) + }) + + it('should render dynamic server rendered values before hydration', async () => { + const browser = await webdriver(appPort, '/') + const text = await browser.elementByCss('#before-hydration').text() + + expect(text).toBe('Index12344') + expect(await browser.eval('window.caughtErrors')).toBe('') + }) + + it('should render dynamic server rendered values on client mount', async () => { + const browser = await webdriver(appPort, '/') + const text = await browser.elementByCss('#first-render').text() + + expect(text).toBe('Index12344') + expect(await browser.eval('window.caughtErrors')).toBe('') + }) +} + +describe('next/dynamic', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests(true) + }) + + describe('production mode', () => { + beforeAll(async () => { + await runNextCommand(['build', appDir]) + + app = nextServer({ + dir: appDir, + dev: false, + quiet: true, + }) + + server = await startApp(app) + appPort = server.address().port + }) + afterAll(() => stopApp(server)) + + runTests() + }) +}) diff --git a/test/unit/next-babel-loader-prod.test.ts b/test/unit/next-babel-loader-prod.test.ts index 4551d037897042a..e8d62bb7a9c6b43 100644 --- a/test/unit/next-babel-loader-prod.test.ts +++ b/test/unit/next-babel-loader-prod.test.ts @@ -193,7 +193,7 @@ describe('next-babel-loader', () => { expect( code.replace(/modules: \[".*?"/, 'modules:["/path/to/page"') ).toMatchInlineSnapshot( - `"var _jsxFileName = \\"index.js\\";import React from \\"react\\";var __jsx = React.createElement;import dynamic from 'next/dynamic';const Comp = dynamic(() => import('comp'), { loadableGenerated: { webpack: () => [require.resolveWeak('comp')], modules:[\\"/path/to/page\\" + 'comp'] }});export default function Page(props) { return __jsx(Comp, { __self: this, __source: { fileName: _jsxFileName, lineNumber: 7, columnNumber: 18 } });}"` + `"var _jsxFileName = \\"index.js\\";import React from \\"react\\";var __jsx = React.createElement;import dynamic from 'next/dynamic';const Comp = dynamic(() => import('comp'), { loadableGenerated: { webpack: () => [require.resolveWeak('comp')] }});export default function Page(props) { return __jsx(Comp, { __self: this, __source: { fileName: _jsxFileName, lineNumber: 7, columnNumber: 18 } });}"` ) })