diff --git a/Cargo.lock b/Cargo.lock index 0133cf263a80f..561cae8bfe128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3440,6 +3440,7 @@ dependencies = [ "either", "fxhash", "hex", + "next-transform-dynamic", "next-transform-font", "once_cell", "pathdiff", diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml index de1f102f2115f..ad12e9536cd3d 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -15,7 +15,6 @@ either = "1" fxhash = "0.2.1" hex = "0.4.3" once_cell = { workspace = true } -next-transform-font = {workspace = true} pathdiff = "0.2.0" regex = "1.5" rustc-hash = "1" @@ -24,6 +23,9 @@ serde_json = "1" sha1 = "0.10.1" tracing = { version = "0.1.37" } +next-transform-dynamic = { workspace = true } +next-transform-font = { workspace = true } + turbopack-binding = { workspace = true, features = [ "__swc_core", "__swc_core_next_core", diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 82bf191047d80..f810a4ceb0eb9 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -37,6 +37,7 @@ use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc}; use auto_cjs::contains_cjs; use either::Either; use fxhash::FxHashSet; +use next_transform_dynamic::{next_dynamic, NextDynamicMode}; use next_transform_font::next_font_loaders; use serde::Deserialize; use turbopack_binding::swc::{ @@ -58,7 +59,6 @@ mod auto_cjs; pub mod cjs_optimizer; pub mod disallow_re_export_all_in_page; pub mod named_import_transform; -pub mod next_dynamic; pub mod next_ssg; pub mod optimize_barrel; pub mod optimize_server_react; @@ -226,7 +226,7 @@ where !opts.disable_next_ssg ), amp_attributes::amp_attributes(), - next_dynamic::next_dynamic( + next_dynamic( opts.is_development, opts.is_server, match &opts.server_components { @@ -238,6 +238,7 @@ where }, _ => false, }, + NextDynamicMode::Webpack, file.name.clone(), opts.pages_dir.clone() ), diff --git a/packages/next-swc/crates/core/src/next_dynamic.rs b/packages/next-swc/crates/core/src/next_dynamic.rs deleted file mode 100644 index 7539c6945319f..0000000000000 --- a/packages/next-swc/crates/core/src/next_dynamic.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::path::{Path, PathBuf}; - -use pathdiff::diff_paths; -use turbopack_binding::swc::core::{ - common::{errors::HANDLER, FileName, DUMMY_SP}, - ecma::{ - ast::{ - ArrayLit, ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, Bool, CallExpr, Callee, Expr, - ExprOrSpread, Id, Ident, ImportDecl, ImportSpecifier, KeyValueProp, Lit, MemberExpr, - MemberProp, Null, ObjectLit, Prop, PropName, PropOrSpread, Str, Tpl, - }, - atoms::js_word, - utils::ExprFactory, - visit::{Fold, FoldWith}, - }, -}; - -pub fn next_dynamic( - is_development: bool, - is_server: bool, - is_server_components: bool, - filename: FileName, - pages_dir: Option, -) -> impl Fold { - NextDynamicPatcher { - is_development, - is_server, - is_server_components, - pages_dir, - filename, - dynamic_bindings: vec![], - is_next_dynamic_first_arg: false, - dynamically_imported_specifier: None, - } -} - -#[derive(Debug)] -struct NextDynamicPatcher { - is_development: bool, - is_server: bool, - is_server_components: 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()); - } - } - } - - decl - } - - fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr { - if self.is_next_dynamic_first_arg { - if let Callee::Import(..) = &expr.callee { - match &*expr.args[0].expr { - Expr::Lit(Lit::Str(Str { value, .. })) => { - self.dynamically_imported_specifier = Some(value.to_string()); - } - Expr::Tpl(Tpl { exprs, quasis, .. }) if exprs.is_empty() => { - self.dynamically_imported_specifier = Some(quasis[0].raw.to_string()); - } - _ => {} - } - } - return expr.fold_children_with(self); - } - let mut expr = expr.fold_children_with(self); - if let Callee::Expr(i) = &expr.callee { - if let Expr::Ident(identifier) = &**i { - if self.dynamic_bindings.contains(&identifier.to_id()) { - if expr.args.is_empty() { - 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, - "next/dynamic options must be an object literal.\nRead more: https://nextjs.org/docs/messages/invalid-dynamic-options-type", - ) - .emit(); - }); - 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; - - if self.dynamically_imported_specifier.is_none() { - return expr; - } - - // dev client or server: - // loadableGenerated: { - // modules: - // ["/project/src/file-being-transformed.js -> " + '../components/hello'] } - - // prod client - // loadableGenerated: { - // webpack: () => [require.resolveWeak('../components/hello')], - let generated = Box::new(Expr::Object(ObjectLit { - span: DUMMY_SP, - 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, - raw: None, - }))), - right: Box::new(Expr::Lit(Lit::Str(Str { - value: self - .dynamically_imported_specifier - .as_ref() - .unwrap() - .clone() - .into(), - span: DUMMY_SP, - raw: None, - }))), - })), - 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: Box::new(BlockStmtOrExpr::Expr(Box::new(Expr::Array( - ArrayLit { - elems: vec![Some(ExprOrSpread { - expr: Box::new(Expr::Call(CallExpr { - callee: Callee::Expr(Box::new(Expr::Member( - MemberExpr { - obj: Box::new(Expr::Ident(Ident { - sym: js_word!("require"), - span: DUMMY_SP, - optional: false, - })), - prop: MemberProp::Ident(Ident { - sym: "resolveWeak".into(), - span: DUMMY_SP, - optional: 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, - raw: None, - }))), - 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, - })), - })))] - }, - })); - - let mut props = - vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { - key: PropName::Ident(Ident::new("loadableGenerated".into(), DUMMY_SP)), - value: generated, - })))]; - - let mut has_ssr_false = false; - - if expr.args.len() == 2 { - if let Expr::Object(ObjectLit { - props: options_props, - .. - }) = &*expr.args[1].expr - { - for prop in options_props.iter() { - if let Some(KeyValueProp { key, value }) = match prop { - PropOrSpread::Prop(prop) => match &**prop { - Prop::KeyValue(key_value_prop) => Some(key_value_prop), - _ => None, - }, - _ => None, - } { - if let Some(Ident { - sym, - span: _, - optional: _, - }) = match key { - PropName::Ident(ident) => Some(ident), - _ => None, - } { - if sym == "ssr" { - if let Some(Lit::Bool(Bool { - value: false, - span: _, - })) = value.as_lit() - { - has_ssr_false = true - } - } - } - } - } - props.extend(options_props.iter().cloned()); - } - } - - if has_ssr_false && self.is_server && !self.is_server_components { - expr.args[0] = Lit::Null(Null { span: DUMMY_SP }).as_arg(); - } - - 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; - } - } - } - 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-swc/crates/core/tests/errors.rs b/packages/next-swc/crates/core/tests/errors.rs index 5ea07faf0ccf9..1bc4b09d92fba 100644 --- a/packages/next-swc/crates/core/tests/errors.rs +++ b/packages/next-swc/crates/core/tests/errors.rs @@ -2,13 +2,13 @@ use std::path::PathBuf; use next_swc::{ disallow_re_export_all_in_page::disallow_re_export_all_in_page, - next_dynamic::next_dynamic, next_ssg::next_ssg, react_server_components::server_components, server_actions::{ server_actions, {self}, }, }; +use next_transform_dynamic::{next_dynamic, NextDynamicMode}; use next_transform_font::{next_font_loaders, Config as FontLoaderConfig}; use turbopack_binding::swc::{ core::{ @@ -56,6 +56,7 @@ fn next_dynamic_errors(input: PathBuf) { true, false, false, + NextDynamicMode::Webpack, 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 b4b3ccdce78ff..61177ccf0f75c 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -4,7 +4,6 @@ use next_swc::{ amp_attributes::amp_attributes, cjs_optimizer::cjs_optimizer, named_import_transform::named_import_transform, - next_dynamic::next_dynamic, next_ssg::next_ssg, optimize_barrel::optimize_barrel, optimize_server_react::optimize_server_react, @@ -15,6 +14,7 @@ use next_swc::{ }, shake_exports::{shake_exports, Config as ShakeExportsConfig}, }; +use next_transform_dynamic::{next_dynamic, NextDynamicMode}; use next_transform_font::{next_font_loaders, Config as FontLoaderConfig}; use serde::de::DeserializeOwned; use turbopack_binding::swc::{ @@ -64,6 +64,7 @@ fn next_dynamic_fixture(input: PathBuf) { true, false, false, + NextDynamicMode::Webpack, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) @@ -79,6 +80,7 @@ fn next_dynamic_fixture(input: PathBuf) { false, false, false, + NextDynamicMode::Webpack, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) @@ -94,6 +96,7 @@ fn next_dynamic_fixture(input: PathBuf) { false, true, false, + NextDynamicMode::Webpack, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) @@ -116,6 +119,7 @@ fn app_dir_next_dynamic_fixture(input: PathBuf) { true, false, true, + NextDynamicMode::Webpack, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) @@ -131,6 +135,7 @@ fn app_dir_next_dynamic_fixture(input: PathBuf) { false, false, true, + NextDynamicMode::Webpack, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) @@ -146,6 +151,7 @@ fn app_dir_next_dynamic_fixture(input: PathBuf) { false, true, true, + NextDynamicMode::Webpack, FileName::Real(PathBuf::from("/some-project/src/some-file.js")), Some("/some-project/src".into()), ) diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index b7d70260e6a4d..7cd5bfd1b2163 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::{bail, Context, Result}; use next_core::{ app_structure::{ @@ -21,7 +23,8 @@ use next_core::{ next_edge::route_regex::get_named_middleware_regex, next_manifests::{ AppBuildManifest, AppPathsManifest, BuildManifest, ClientReferenceManifest, - EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2, PagesManifest, Regions, + EdgeFunctionDefinition, LoadableManifest, MiddlewareMatcher, MiddlewaresManifestV2, + PagesManifest, Regions, }, next_server::{ get_server_module_options_context, get_server_resolve_options_context, @@ -39,7 +42,7 @@ use turbopack_binding::{ turbopack::{ core::{ asset::{Asset, AssetContent}, - chunk::{ChunkingContext, EvaluatableAssets}, + chunk::{availability_info::AvailabilityInfo, ChunkingContext, EvaluatableAssets}, file_source::FileSource, module::Module, output::{OutputAsset, OutputAssets}, @@ -53,6 +56,10 @@ use turbopack_binding::{ }; use crate::{ + dynamic_imports::{ + collect_chunk_group, collect_evaluated_chunk_group, collect_next_dynamic_imports, + DynamicImportedChunks, + }, project::Project, route::{Endpoint, Route, Routes, WrittenEndpoint}, server_actions::create_server_actions_manifest, @@ -679,6 +686,67 @@ impl AppEndpoint { ))) } + async fn create_react_loadable_manifest( + dynamic_import_entries: Vc, + ty: &'static str, + node_root: Vc, + pathname: &str, + ) -> Result> { + let dynamic_import_entries = &*dynamic_import_entries.await?; + + let mut output = vec![]; + let mut loadable_manifest: HashMap = Default::default(); + + for (origin, dynamic_imports) in dynamic_import_entries.into_iter() { + let origin_path = &*origin.ident().path().await?; + + for (import, chunk_output) in dynamic_imports { + let chunk_output = chunk_output.await?; + output.extend(chunk_output.iter().copied()); + + let id = format!("{} -> {}", origin_path, import); + + let server_path = node_root.join("server".to_string()); + let server_path_value = server_path.await?; + let files = chunk_output + .iter() + .map(move |&file| { + let server_path_value = server_path_value.clone(); + async move { + Ok(server_path_value + .get_path_to(&*file.ident().path().await?) + .map(|path| path.to_string())) + } + }) + .try_flat_join() + .await?; + + let manifest_item = LoadableManifest { + id: id.clone(), + files, + }; + + loadable_manifest.insert(id, manifest_item); + } + } + + let loadable_path_prefix = get_asset_prefix_from_pathname(pathname); + let loadable_manifest = Vc::upcast(VirtualOutputAsset::new( + node_root.join(format!( + "server/app{loadable_path_prefix}/{ty}/react-loadable-manifest.json", + )), + AssetContent::file( + FileContent::Content(File::from(serde_json::to_string_pretty( + &loadable_manifest, + )?)) + .cell(), + ), + )); + + output.push(loadable_manifest); + Ok(Vc::cell(output)) + } + let endpoint_output = match app_entry.config.await?.runtime.unwrap_or_default() { NextRuntime::Edge => { // create edge chunks @@ -714,7 +782,7 @@ impl AppEndpoint { let files = chunking_context.evaluated_chunk_group( app_entry.rsc_entry.ident(), - Vc::cell(evaluatable_assets), + Vc::cell(evaluatable_assets.clone()), ); server_assets.extend(files.await?.iter().copied()); @@ -801,6 +869,24 @@ impl AppEndpoint { )?; server_assets.push(app_paths_manifest_output); + // create react-loadable-manifest for next/dynamic + let dynamic_import_modules = + collect_next_dynamic_imports(app_entry.rsc_entry).await?; + let dynamic_import_entries = collect_evaluated_chunk_group( + chunking_context, + dynamic_import_modules, + Vc::cell(evaluatable_assets), + ) + .await?; + let loadable_manifest_output = create_react_loadable_manifest( + dynamic_import_entries, + ty, + node_root, + &app_entry.pathname, + ) + .await?; + server_assets.extend(loadable_manifest_output.await?.iter().copied()); + AppEndpointOutput::Edge { files, server_assets: Vc::cell(server_assets), @@ -857,6 +943,25 @@ impl AppEndpoint { )?; server_assets.push(app_paths_manifest_output); + // create react-loadable-manifest for next/dynamic + let availability_info = Value::new(AvailabilityInfo::Root); + let dynamic_import_modules = + collect_next_dynamic_imports(app_entry.rsc_entry).await?; + let dynamic_import_entries = collect_chunk_group( + this.app_project.project().rsc_chunking_context(), + dynamic_import_modules, + availability_info, + ) + .await?; + let loadable_manifest_output = create_react_loadable_manifest( + dynamic_import_entries, + ty, + node_root, + &app_entry.pathname, + ) + .await?; + server_assets.extend(loadable_manifest_output.await?.iter().copied()); + AppEndpointOutput::NodeJs { rsc_chunk, server_assets: Vc::cell(server_assets), diff --git a/packages/next-swc/crates/next-api/src/dynamic_imports.rs b/packages/next-swc/crates/next-api/src/dynamic_imports.rs new file mode 100644 index 0000000000000..43d3268209d0f --- /dev/null +++ b/packages/next-swc/crates/next-api/src/dynamic_imports.rs @@ -0,0 +1,319 @@ +use std::collections::HashMap; + +use anyhow::{bail, Result}; +use indexmap::IndexMap; +use turbo_tasks::{ + graph::{GraphTraversal, NonDeterministic}, + Value, Vc, +}; +use turbopack_binding::{ + swc::core::ecma::{ + ast::{CallExpr, Callee, Expr, Ident, Lit}, + visit::{Visit, VisitWith}, + }, + turbopack::{ + build::BuildChunkingContext, + core::{ + chunk::{ + availability_info::AvailabilityInfo, ChunkableModule, ChunkingContext, + EvaluatableAssets, + }, + issue::{IssueSeverity, OptionIssueSource}, + module::Module, + output::OutputAssets, + reference::primary_referenced_modules, + reference_type::EcmaScriptModulesReferenceSubType, + resolve::{origin::PlainResolveOrigin, parse::Request, pattern::Pattern}, + }, + ecmascript::{ + chunk::{EcmascriptChunkPlaceable, EcmascriptChunkingContext}, + parse::ParseResult, + resolve::esm_resolve, + EcmascriptModuleAsset, + }, + }, +}; + +async fn collect_chunk_group_inner( + dynamic_import_entries: IndexMap>, DynamicImportedModules>, + build_chunk: F, +) -> Result> +where + F: Fn(Vc>) -> Vc, +{ + let mut chunks_hash: HashMap> = HashMap::new(); + let mut dynamic_import_chunks = IndexMap::new(); + + // Iterate over the collected import mappings, and create a chunk for each + // dynamic import. + for (origin_module, dynamic_imports) in dynamic_import_entries { + for (imported_raw_str, imported_module) in dynamic_imports { + let chunk = if let Some(chunk) = chunks_hash.get(&imported_raw_str) { + *chunk + } else { + let Some(chunk_item) = + Vc::try_resolve_sidecast::>(imported_module).await? + else { + bail!("module must be evaluatable"); + }; + + // [Note]: this seems to create duplicated chunks for the same module to the original import() call + // and the explicit chunk we ask in here. So there'll be at least 2 + // chunks for the same module, relying on + // naive hash to have additonal + // chunks in case if there are same modules being imported in differnt + // origins. + let chunk_group = build_chunk(chunk_item); + chunks_hash.insert(imported_raw_str.to_string(), chunk_group); + chunk_group + }; + + dynamic_import_chunks + .entry(origin_module) + .or_insert_with(Vec::new) + .push((imported_raw_str.clone(), chunk)); + } + } + + Ok(Vc::cell(dynamic_import_chunks)) +} + +pub(crate) async fn collect_chunk_group( + chunking_context: Vc, + dynamic_import_entries: IndexMap>, DynamicImportedModules>, + availability_info: Value, +) -> Result> { + collect_chunk_group_inner(dynamic_import_entries, |chunk_item| { + chunking_context.chunk_group(chunk_item, availability_info) + }) + .await +} + +pub(crate) async fn collect_evaluated_chunk_group( + chunking_context: Vc>, + dynamic_import_entries: IndexMap>, DynamicImportedModules>, + evaluatable_assets: Vc, +) -> Result> { + collect_chunk_group_inner(dynamic_import_entries, |chunk_item| { + chunking_context.evaluated_chunk_group(chunk_item.ident(), evaluatable_assets) + }) + .await +} + +/// Returns a mapping of the dynamic imports for each module, if the import is +/// wrapped in `next/dynamic`'s `dynamic()`. Refer https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-named-exports for the usecases. +/// +/// If an import is specified as dynamic, next.js does few things: +/// - Runs a next_dynamic transform to the source file (https://github.com/vercel/next.js/blob/ae1b89984d26b2af3658001fa19a19e1e77c312d/packages/next-swc/crates/next-transform-dynamic/src/lib.rs#L22) +/// - This transform will inject `loadableGenerated` property, which contains the list of the import ids in the form of `${origin} -> ${imported}`. +/// (https://github.com/vercel/next.js/blob/ae1b89984d26b2af3658001fa19a19e1e77c312d/packages/next-swc/crates/next-transform-dynamic/tests/fixture/wrapped-import/output-webpack-dev.js#L5) +/// - Emits `react-loadable-manifest.json` which contains the mapping of the +/// import ids to the chunk ids. +/// - Webpack: (https://github.com/vercel/next.js/blob/ae1b89984d26b2af3658001fa19a19e1e77c312d/packages/next/src/build/webpack/plugins/react-loadable-plugin.ts) +/// - Turbopack: ( https://github.com/vercel/next.js/pull/56389/files#diff-3cac9d9bfe73e0619e6407f21f6fe652da0719d0ec9074ff813ad3e416d0eb1a +/// / https://github.com/vercel/next.js/pull/56389/files#diff-791951bbe1fa09bcbad9be9173412d0848168f7d658758f11b6e8888a021552c +/// / https://github.com/vercel/next.js/pull/56389/files#diff-c33f6895801329243dd3f627c69da259bcab95c2c9d12993152842591931ff01R557 +/// ) +/// - When running an application, +/// - Server reads generated `react-loadable-manifest.json`, sets dynamicImportIds with the mapping of the import ids, and dynamicImports to the actual corresponding chunks. +/// (https://github.com/vercel/next.js/blob/ad42b610c25b72561ad367b82b1c7383fd2a5dd2/packages/next/src/server/load-components.ts#L119 / +/// https://github.com/vercel/next.js/blob/ad42b610c25b72561ad367b82b1c7383fd2a5dd2/packages/next/src/server/render.tsx#L1417C7-L1420) +/// - Server embeds those into __NEXT_DATA__ and sent to the client. (https://github.com/vercel/next.js/blob/ad42b610c25b72561ad367b82b1c7383fd2a5dd2/packages/next/src/server/render.tsx#L1453) +/// - When client boots up, pass it to the client preload (https://github.com/vercel/next.js/blob/ad42b610c25b72561ad367b82b1c7383fd2a5dd2/packages/next/src/client/index.tsx#L943) +/// - Loadable runtime injects preload fn to wait until all the dynamic components are being loaded, this ensures hydration mismatch won't occur +/// (https://github.com/vercel/next.js/blob/ad42b610c25b72561ad367b82b1c7383fd2a5dd2/packages/next/src/shared/lib/loadable.shared-runtime.tsx#L281) +pub(crate) async fn collect_next_dynamic_imports( + entry: Vc>, +) -> Result>, DynamicImportedModules>> { + // Traverse referenced modules graph, collect all of the dynamic imports: + // - Read the Program AST of the Module, this is the origin (A) + // - If there's `dynamic(import(B))`, then B is the module that is being + // imported + // Returned import mappings are in the form of + // (Module, Vec<(B, Module)>) (where B is the raw import source string, + // and Module is the actual resolved Module) + let imported_modules_mapping = NonDeterministic::new() + .skip_duplicates() + .visit([Vc::upcast(entry)], get_referenced_modules) + .await + .completed()? + .into_inner() + .into_iter() + .map(build_dynamic_imports_map_for_module); + + // Consolidate import mappings into a single indexmap + let mut import_mappings: IndexMap>, DynamicImportedModules> = + IndexMap::new(); + + for module_mapping in imported_modules_mapping { + if let Some(module_mapping) = &*module_mapping.await? { + let (origin_module, dynamic_imports) = &*module_mapping.await?; + import_mappings + .entry(*origin_module) + .or_insert_with(Vec::new) + .append(&mut dynamic_imports.clone()) + } + } + + Ok(import_mappings) +} + +async fn get_referenced_modules( + parent: Vc>, +) -> Result>> + Send> { + primary_referenced_modules(parent) + .await + .map(|modules| modules.clone_value().into_iter()) +} + +#[turbo_tasks::function] +async fn build_dynamic_imports_map_for_module( + module: Vc>, +) -> Result> { + let Some(ecmascript_asset) = + Vc::try_resolve_downcast_type::(module).await? + else { + return Ok(OptionDynamicImportsMap::none()); + }; + + // https://github.com/vercel/next.js/pull/56389#discussion_r1349336374 + // don't emit specific error as we expect there's a parse error already reported + let ParseResult::Ok { program, .. } = &*ecmascript_asset.parse().await? else { + return Ok(OptionDynamicImportsMap::none()); + }; + + // Reading the Program AST, collect raw imported module str if it's wrapped in + // dynamic() + let mut visitor = DynamicImportVisitor::new(); + program.visit_with(&mut visitor); + + if visitor.import_sources.is_empty() { + return Ok(OptionDynamicImportsMap::none()); + } + + let mut import_sources = vec![]; + for import in visitor.import_sources.drain(..) { + // Using the given `Module` which is the origin of the dynamic import, trying to + // resolve the module that is being imported. + let dynamic_imported_resolved_module = *esm_resolve( + Vc::upcast(PlainResolveOrigin::new( + ecmascript_asset.await?.asset_context, + module.ident().path(), + )), + Request::parse(Value::new(Pattern::Constant(import.to_string()))), + Value::new(EcmaScriptModulesReferenceSubType::Undefined), + OptionIssueSource::none(), + IssueSeverity::Error.cell(), + ) + .first_module() + .await?; + + if let Some(dynamic_imported_resolved_module) = dynamic_imported_resolved_module { + import_sources.push((import, dynamic_imported_resolved_module)); + } + } + + Ok(Vc::cell(Some(Vc::cell((module, import_sources))))) +} + +/// A visitor to check if there's import to `next/dynamic`, then collecting the +/// import wrapped with dynamic() via CollectImportSourceVisitor. +struct DynamicImportVisitor { + dynamic_ident: Option, + pub import_sources: Vec, +} + +impl DynamicImportVisitor { + fn new() -> Self { + Self { + import_sources: vec![], + dynamic_ident: None, + } + } +} + +impl Visit for DynamicImportVisitor { + fn visit_import_decl(&mut self, decl: &turbopack_binding::swc::core::ecma::ast::ImportDecl) { + // find import decl from next/dynamic, i.e import dynamic from 'next/dynamic' + if decl.src.value == *"next/dynamic" { + if let Some(specifier) = decl.specifiers.first().and_then(|s| s.as_default()) { + self.dynamic_ident = Some(specifier.local.clone()); + } + } + } + + fn visit_call_expr(&mut self, call_expr: &CallExpr) { + // Collect imports if the import call is wrapped in the call dynamic() + if let Callee::Expr(ident) = &call_expr.callee { + if let Expr::Ident(ident) = &**ident { + if let Some(dynamic_ident) = &self.dynamic_ident { + if ident.sym == *dynamic_ident.sym { + let mut collect_import_source_visitor = CollectImportSourceVisitor::new(); + call_expr.visit_children_with(&mut collect_import_source_visitor); + + if let Some(import_source) = collect_import_source_visitor.import_source { + self.import_sources.push(import_source); + } + } + } + } + } + + call_expr.visit_children_with(self); + } +} + +/// A visitor to collect import source string from import('path/to/module') +struct CollectImportSourceVisitor { + import_source: Option, +} + +impl CollectImportSourceVisitor { + fn new() -> Self { + Self { + import_source: None, + } + } +} + +impl Visit for CollectImportSourceVisitor { + fn visit_call_expr(&mut self, call_expr: &CallExpr) { + // find import source from import('path/to/module') + // [NOTE]: Turbopack does not support webpack-specific comment directives, i.e + // import(/* webpackChunkName: 'hello1' */ '../../components/hello3') + // Renamed chunk in the comment will be ignored. + if let Callee::Import(_import) = call_expr.callee { + if let Some(arg) = call_expr.args.first() { + if let Expr::Lit(Lit::Str(str_)) = &*arg.expr { + self.import_source = Some(str_.value.to_string()); + } + } + } + + // Don't need to visit children, we expect import() won't have any + // nested calls as dynamic() should be statically analyzable import. + } +} + +pub type DynamicImportedModules = Vec<(String, Vc>)>; +pub type DynamicImportedOutputAssets = Vec<(String, Vc)>; + +/// A struct contains mapping for the dynamic imports to construct chunk per +/// each individual module (Origin Module, Vec<(ImportSourceString, Module)>) +#[turbo_tasks::value(transparent)] +pub struct DynamicImportsMap(pub (Vc>, DynamicImportedModules)); + +/// An Option wrapper around [DynamicImportsMap]. +#[turbo_tasks::value(transparent)] +pub struct OptionDynamicImportsMap(Option>); + +#[turbo_tasks::value_impl] +impl OptionDynamicImportsMap { + #[turbo_tasks::function] + pub fn none() -> Vc { + Vc::cell(None) + } +} + +#[turbo_tasks::value(transparent)] +pub struct DynamicImportedChunks(pub IndexMap>, DynamicImportedOutputAssets>); diff --git a/packages/next-swc/crates/next-api/src/lib.rs b/packages/next-swc/crates/next-api/src/lib.rs index 2a83ab2d75eba..0e7d7c7b04e6b 100644 --- a/packages/next-swc/crates/next-api/src/lib.rs +++ b/packages/next-swc/crates/next-api/src/lib.rs @@ -3,6 +3,7 @@ #![feature(async_fn_in_trait)] mod app; +mod dynamic_imports; mod entrypoints; mod middleware; mod pages; diff --git a/packages/next-swc/crates/next-api/src/pages.rs b/packages/next-swc/crates/next-api/src/pages.rs index b3fd51f922442..0401a2281d37a 100644 --- a/packages/next-swc/crates/next-api/src/pages.rs +++ b/packages/next-swc/crates/next-api/src/pages.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::{bail, Context, Result}; use indexmap::IndexMap; use next_core::{ @@ -11,8 +13,8 @@ use next_core::{ next_dynamic::NextDynamicTransition, next_edge::route_regex::get_named_middleware_regex, next_manifests::{ - BuildManifest, EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2, - PagesManifest, + BuildManifest, EdgeFunctionDefinition, LoadableManifest, MiddlewareMatcher, + MiddlewaresManifestV2, PagesManifest, }, next_pages::create_page_ssr_entry_module, next_server::{ @@ -37,7 +39,7 @@ use turbopack_binding::{ build::BuildChunkingContext, core::{ asset::AssetContent, - chunk::{ChunkingContext, EvaluatableAssets}, + chunk::{availability_info::AvailabilityInfo, ChunkingContext, EvaluatableAssets}, context::AssetContext, file_source::FileSource, issue::{IssueSeverity, OptionIssueSource}, @@ -63,6 +65,10 @@ use turbopack_binding::{ }; use crate::{ + dynamic_imports::{ + collect_chunk_group, collect_evaluated_chunk_group, collect_next_dynamic_imports, + DynamicImportedChunks, + }, project::Project, route::{Endpoint, Route, Routes, WrittenEndpoint}, server_paths::all_server_paths, @@ -609,9 +615,21 @@ impl PageEndpoint { evaluatable_assets.push(evaluatable); let edge_files = edge_chunking_context - .evaluated_chunk_group(ssr_module.ident(), Vc::cell(evaluatable_assets)); + .evaluated_chunk_group(ssr_module.ident(), Vc::cell(evaluatable_assets.clone())); + + let dynamic_import_modules = collect_next_dynamic_imports(ssr_module).await?; + let dynamic_import_entries = collect_evaluated_chunk_group( + edge_chunking_context, + dynamic_import_modules, + Vc::cell(evaluatable_assets.clone()), + ) + .await?; - Ok(SsrChunk::Edge { files: edge_files }.cell()) + Ok(SsrChunk::Edge { + files: edge_files, + dynamic_import_entries, + } + .cell()) } else { let ssr_module = create_page_ssr_entry_module( this.pathname, @@ -633,8 +651,15 @@ impl PageEndpoint { runtime_entries, ); + let availability_info = Value::new(AvailabilityInfo::Root); + let dynamic_import_modules = collect_next_dynamic_imports(ssr_module).await?; + let dynamic_import_entries = + collect_chunk_group(chunking_context, dynamic_import_modules, availability_info) + .await?; + Ok(SsrChunk::NodeJs { entry: ssr_entry_chunk, + dynamic_import_entries, } .cell()) } @@ -728,6 +753,78 @@ impl PageEndpoint { ))) } + #[turbo_tasks::function] + async fn react_loadable_manifest( + self: Vc, + dynamic_import_entries: Vc, + ) -> Result> { + let this = self.await?; + let node_root = this.pages_project.project().node_root(); + let pages_dir = this.pages_project.pages_dir().await?; + + let dynamic_import_entries = &*dynamic_import_entries.await?; + + let mut output = vec![]; + let mut loadable_manifest: HashMap = Default::default(); + for (origin, dynamic_imports) in dynamic_import_entries.into_iter() { + let origin_path = &*origin.ident().path().await?; + + for (import, chunk_output) in dynamic_imports { + let chunk_output = chunk_output.await?; + output.extend(chunk_output.iter().copied()); + + // https://github.com/vercel/next.js/blob/b7c85b87787283d8fb86f705f67bdfabb6b654bb/packages/next-swc/crates/next-transform-dynamic/src/lib.rs#L230 + // For the pages dir, next_dynamic transform puts relative paths to the pages + // dir for the origin import. + let id = format!( + "{} -> {}", + pages_dir + .get_path_to(origin_path) + .map_or_else(|| origin_path.to_string(), |path| path.to_string()), + import + ); + + let server_path = node_root.join("server".to_string()); + let server_path_value = server_path.await?; + let files = chunk_output + .iter() + .map(move |file| { + let server_path_value = server_path_value.clone(); + async move { + Ok(server_path_value + .get_path_to(&*file.ident().path().await?) + .map(|path| path.to_string())) + } + }) + .try_flat_join() + .await?; + + let manifest_item = LoadableManifest { + id: id.clone(), + files, + }; + + loadable_manifest.insert(id, manifest_item); + } + } + + let loadable_path_prefix = get_asset_prefix_from_pathname(&this.pathname.await?); + let loadable_manifest = Vc::upcast(VirtualOutputAsset::new( + node_root.join(format!( + "server/pages{loadable_path_prefix}/react-loadable-manifest.json" + )), + AssetContent::file( + FileContent::Content(File::from(serde_json::to_string_pretty( + &loadable_manifest, + )?)) + .cell(), + ), + )); + + output.push(loadable_manifest); + Ok(Vc::cell(output)) + } + #[turbo_tasks::function] async fn build_manifest( self: Vc, @@ -796,18 +893,27 @@ impl PageEndpoint { }; let page_output = match *ssr_chunk.await? { - SsrChunk::NodeJs { entry } => { + SsrChunk::NodeJs { + entry, + dynamic_import_entries, + } => { let pages_manifest = self.pages_manifest(entry); server_assets.push(pages_manifest); server_assets.push(entry); + let loadable_manifest_output = self.react_loadable_manifest(dynamic_import_entries); + server_assets.extend(loadable_manifest_output.await?.iter().copied()); + PageEndpointOutput::NodeJs { entry_chunk: entry, server_assets: Vc::cell(server_assets), client_assets: Vc::cell(client_assets), } } - SsrChunk::Edge { files } => { + SsrChunk::Edge { + files, + dynamic_import_entries, + } => { let node_root = this.pages_project.project().node_root(); let files_value = files.await?; if let Some(&file) = files_value.first() { @@ -867,6 +973,9 @@ impl PageEndpoint { )); server_assets.push(middleware_manifest_v2); + let loadable_manifest_output = self.react_loadable_manifest(dynamic_import_entries); + server_assets.extend(loadable_manifest_output.await?.iter().copied()); + PageEndpointOutput::Edge { files, server_assets: Vc::cell(server_assets), @@ -1001,6 +1110,12 @@ impl PageEndpointOutput { #[turbo_tasks::value] pub enum SsrChunk { - NodeJs { entry: Vc> }, - Edge { files: Vc }, + NodeJs { + entry: Vc>, + dynamic_import_entries: Vc, + }, + Edge { + files: Vc, + dynamic_import_entries: Vc, + }, } diff --git a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs index 5d6a452be39e6..86042ed18fe2f 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs @@ -145,6 +145,17 @@ pub struct AppPathsManifest { pub node_server_app_paths: PagesManifest, } +// A struct represent a single entry in react-loadable-manifest.json. +// The manifest is in a format of: +// { [`${origin} -> ${imported}`]: { id: `${origin} -> ${imported}`, files: +// string[] } } +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct LoadableManifest { + pub id: String, + pub files: Vec, +} + #[derive(Serialize, Default, Debug)] #[serde(rename_all = "camelCase")] pub struct ServerReferenceManifest { diff --git a/packages/next-swc/crates/next-core/src/next_shared/transforms/next_dynamic.rs b/packages/next-swc/crates/next-core/src/next_shared/transforms/next_dynamic.rs index 588ad932c600d..c0267fb1d0191 100644 --- a/packages/next-swc/crates/next-core/src/next_shared/transforms/next_dynamic.rs +++ b/packages/next-swc/crates/next-core/src/next_shared/transforms/next_dynamic.rs @@ -65,12 +65,7 @@ impl CustomTransformer for NextJsDynamic { }, self.is_server, self.is_server_components, - NextDynamicMode::Turbopack { - dynamic_transition_name: match self.mode { - NextMode::Development => "next-client-chunks".to_string(), - NextMode::Build => "next-dynamic".to_string(), - }, - }, + NextDynamicMode::Webpack, FileName::Real(ctx.file_path_str.into()), self.pages_dir.clone(), )); diff --git a/packages/next-swc/crates/next-transform-dynamic/src/lib.rs b/packages/next-swc/crates/next-transform-dynamic/src/lib.rs index fc630e6c44113..9136fd78a336d 100644 --- a/packages/next-swc/crates/next-transform-dynamic/src/lib.rs +++ b/packages/next-swc/crates/next-transform-dynamic/src/lib.rs @@ -19,6 +19,11 @@ use swc_core::{ quote, }; +/// Creates a SWC visitor to transform `next/dynamic` calls to have the +/// corresponding `loadableGenerated` property. +/// +/// [NOTE] We do not use `NextDynamicMode::Turbopack` yet. It isn't compatible +/// with current loadable manifest, which causes hydration errors. pub fn next_dynamic( is_development: bool, is_server: bool, @@ -89,6 +94,7 @@ enum NextDynamicPatcherState { Webpack, /// In Turbo mode, contains a list of modules that need to be imported with /// the given transition under a particular ident. + #[allow(unused)] Turbopack { dynamic_transition_name: String, imports: Vec, diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 17562efdcac23..55d55a6f7ce43 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -78,6 +78,7 @@ import { PAGES_MANIFEST, PHASE_DEVELOPMENT_SERVER, SERVER_REFERENCE_MANIFEST, + REACT_LOADABLE_MANIFEST, } from '../../../shared/lib/constants' import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' @@ -117,6 +118,7 @@ import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route import { clearModuleContext } from '../render-server' import type { ActionManifest } from '../../../build/webpack/plugins/flight-client-entry-plugin' import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path' +import type { LoadableManifest } from '../../load-components' const wsServer = new ws.Server({ noServer: true }) @@ -503,6 +505,7 @@ async function startWatcher(opts: SetupOpts) { ws, Map> >() + const loadbleManifests = new Map() const clients = new Set() async function loadMiddlewareManifest( @@ -560,6 +563,16 @@ async function startWatcher(opts: SetupOpts) { ) } + async function loadLoadableManifest( + pageName: string, + type: 'app' | 'pages' = 'pages' + ): Promise { + loadbleManifests.set( + pageName, + await loadPartialManifest(REACT_LOADABLE_MANIFEST, pageName, type) + ) + } + const buildingReported = new Set() async function changeSubscription( @@ -697,6 +710,14 @@ async function startWatcher(opts: SetupOpts) { return manifest } + function mergeLoadableManifests(manifests: Iterable) { + const manifest: LoadableManifest = {} + for (const m of manifests) { + Object.assign(manifest, m) + } + return manifest + } + async function writeFileAtomic( filePath: string, content: string @@ -866,13 +887,14 @@ async function startWatcher(opts: SetupOpts) { ) } - async function writeOtherManifests(): Promise { - const loadableManifestPath = path.join( - distDir, - 'react-loadable-manifest.json' - ) + async function writeLoadableManifest(): Promise { + const loadableManifest = mergeLoadableManifests(loadbleManifests.values()) + const loadableManifestPath = path.join(distDir, REACT_LOADABLE_MANIFEST) deleteCache(loadableManifestPath) - await writeFileAtomic(loadableManifestPath, JSON.stringify({}, null, 2)) + await writeFileAtomic( + loadableManifestPath, + JSON.stringify(loadableManifest, null, 2) + ) } async function subscribeToHmrEvents(id: string, client: ws) { @@ -1052,8 +1074,8 @@ async function startWatcher(opts: SetupOpts) { await writeAppPathsManifest() await writeMiddlewareManifest() await writeActionManifest() - await writeOtherManifests() await writeFontManifest() + await writeLoadableManifest() const turbopackHotReloader: NextJsHotReloaderInterface = { turbopackProject: project, @@ -1220,7 +1242,7 @@ async function startWatcher(opts: SetupOpts) { await writeFallbackBuildManifest() await writePagesManifest() await writeMiddlewareManifest() - await writeOtherManifests() + await writeLoadableManifest() return } @@ -1329,12 +1351,13 @@ async function startWatcher(opts: SetupOpts) { } else { middlewareManifests.delete(page) } + await loadLoadableManifest(page, 'pages') await writeBuildManifest(opts.fsChecker.rewrites) await writeFallbackBuildManifest() await writePagesManifest() await writeMiddlewareManifest() - await writeOtherManifests() + await writeLoadableManifest() processIssues(page, page, writtenEndpoint) @@ -1358,10 +1381,11 @@ async function startWatcher(opts: SetupOpts) { } else { middlewareManifests.delete(page) } + await loadLoadableManifest(page, 'pages') await writePagesManifest() await writeMiddlewareManifest() - await writeOtherManifests() + await writeLoadableManifest() processIssues(page, page, writtenEndpoint) @@ -1395,7 +1419,7 @@ async function startWatcher(opts: SetupOpts) { await writeAppPathsManifest() await writeMiddlewareManifest() await writeActionManifest() - await writeOtherManifests() + await writeLoadableManifest() processIssues(page, page, writtenEndpoint, true) @@ -1420,7 +1444,7 @@ async function startWatcher(opts: SetupOpts) { await writeAppPathsManifest() await writeMiddlewareManifest() await writeMiddlewareManifest() - await writeOtherManifests() + await writeLoadableManifest() processIssues(page, page, writtenEndpoint, true) diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 63e563b13a2d3..cce641eb998f2 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -33,6 +33,17 @@ export type ManifestItem = { export type ReactLoadableManifest = { [moduleId: string]: ManifestItem } +/** + * A manifest entry type for the react-loadable-manifest.json. + * + * The whole manifest.json is a type of `Record` + * where pathName is a string-based key points to the path of the page contains + * each dynamic imports. + */ +export interface LoadableManifest { + [k: string]: { id: string | number; files: string[] } +} + export type LoadComponentsReturnType = { Component: NextComponentType pageConfig: PageConfig diff --git a/test/development/basic/next-dynamic.test.ts b/test/development/basic/next-dynamic.test.ts index 5294624adeb1d..efbd2c99c66d0 100644 --- a/test/development/basic/next-dynamic.test.ts +++ b/test/development/basic/next-dynamic.test.ts @@ -237,32 +237,35 @@ describe.each([ }) } }) + // Turbopack doesn't have this feature. + ;(process.env.TURBOPACK ? describe.skip : describe)( + 'custom chunkfilename', + () => { + it('should render the correct filename', async () => { + const $ = await get$(basePath + '/dynamic/chunkfilename') + expect($('body').text()).toMatch(/test chunkfilename/) + expect($('html').html()).toMatch(/hello-world\.js/) + }) - describe('custom chunkfilename', () => { - it('should render the correct filename', async () => { - const $ = await get$(basePath + '/dynamic/chunkfilename') - expect($('body').text()).toMatch(/test chunkfilename/) - expect($('html').html()).toMatch(/hello-world\.js/) - }) - - it('should render the component on client side', async () => { - let browser - try { - browser = await webdriver( - next.url, - basePath + '/dynamic/chunkfilename' - ) - await check( - () => browser.elementByCss('body').text(), - /test chunkfilename/ - ) - } finally { - if (browser) { - await browser.close() + it('should render the component on client side', async () => { + let browser + try { + browser = await webdriver( + next.url, + basePath + '/dynamic/chunkfilename' + ) + await check( + () => browser.elementByCss('body').text(), + /test chunkfilename/ + ) + } finally { + if (browser) { + await browser.close() + } } - } - }) - }) + }) + } + ) describe('custom loading', () => { it('should render custom loading on the server side when `ssr:false` and `loading` is provided', async () => { @@ -289,45 +292,49 @@ describe.each([ }) }) - describe('Multiple modules', () => { - it('should only include the rendered module script tag', async () => { - const $ = await get$(basePath + '/dynamic/multiple-modules') - const html = $('html').html() - expect(html).toMatch(/hello1\.js/) - expect(html).not.toMatch(/hello2\.js/) - }) - - it('should only load the rendered module in the browser', async () => { - let browser - try { - browser = await webdriver( - next.url, - basePath + '/dynamic/multiple-modules' - ) - const html = await browser.eval( - 'document.documentElement.innerHTML' - ) + // TODO: Make this test work with Turbopack. Currently the test relies on `chunkFileName` which is not supported by Turbopack. + ;(process.env.TURBOPACK ? describe.skip : describe)( + 'Multiple modules', + () => { + it('should only include the rendered module script tag', async () => { + const $ = await get$(basePath + '/dynamic/multiple-modules') + const html = $('html').html() expect(html).toMatch(/hello1\.js/) expect(html).not.toMatch(/hello2\.js/) - } finally { - if (browser) { - await browser.close() + }) + + it('should only load the rendered module in the browser', async () => { + let browser + try { + browser = await webdriver( + next.url, + basePath + '/dynamic/multiple-modules' + ) + const html = await browser.eval( + 'document.documentElement.innerHTML' + ) + expect(html).toMatch(/hello1\.js/) + expect(html).not.toMatch(/hello2\.js/) + } finally { + if (browser) { + await browser.close() + } } - } - }) + }) - it('should only render one bundle if component is used multiple times', async () => { - const $ = await get$(basePath + '/dynamic/multiple-modules') - const html = $('html').html() - try { - expect(html.match(/chunks[\\/]hello1\.js/g).length).toBe(1) - expect(html).not.toMatch(/hello2\.js/) - } catch (err) { - console.error(html) - throw err - } - }) - }) + it('should only render one bundle if component is used multiple times', async () => { + const $ = await get$(basePath + '/dynamic/multiple-modules') + const html = $('html').html() + try { + expect(html.match(/chunks[\\/]hello1\.js/g).length).toBe(1) + expect(html).not.toMatch(/hello2\.js/) + } catch (err) { + console.error(html) + throw err + } + }) + } + ) }) } ) diff --git a/test/integration/next-dynamic-lazy-compilation/test/index.test.js b/test/integration/next-dynamic-lazy-compilation/test/index.test.js index b075256aba2c6..c441a54635a4c 100644 --- a/test/integration/next-dynamic-lazy-compilation/test/index.test.js +++ b/test/integration/next-dynamic-lazy-compilation/test/index.test.js @@ -46,31 +46,38 @@ function runTests() { }) } -describe('next/dynamic', () => { - describe('dev mode', () => { - beforeAll(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterAll(() => killApp(app)) - - runTests(true) - }) - ;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => { - beforeAll(async () => { - await runNextCommand(['build', appDir]) - - app = nextServer({ - dir: appDir, - dev: false, - quiet: true, +// This test is not needed for Turbopack as it relies on an experimental webpack feature. +;(process.env.TURBOPACK ? describe.skip : describe)( + 'next/dynamic lazy compilation', + () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) }) + afterAll(() => killApp(app)) - server = await startApp(app) - appPort = server.address().port + runTests(true) }) - afterAll(() => stopApp(server)) + ;(process.env.TURBOPACK ? describe.skip : describe)( + 'production mode', + () => { + beforeAll(async () => { + await runNextCommand(['build', appDir]) - runTests() - }) -}) + app = nextServer({ + dir: appDir, + dev: false, + quiet: true, + }) + + server = await startApp(app) + appPort = server.address().port + }) + afterAll(() => stopApp(server)) + + runTests() + } + ) + } +)