diff --git a/packages/next-swc/crates/next-api/src/pages.rs b/packages/next-swc/crates/next-api/src/pages.rs index 2bb866693219c..dadc91c93f5fa 100644 --- a/packages/next-swc/crates/next-api/src/pages.rs +++ b/packages/next-swc/crates/next-api/src/pages.rs @@ -924,7 +924,7 @@ impl PageEndpoint { ) -> Result> { let this = self.await?; let node_root = this.pages_project.project().node_root(); - let pages_dir = this.pages_project.pages_dir().await?; + let project_src_dir = this.pages_project.pages_dir().parent().await?; let dynamic_import_entries = &*dynamic_import_entries.await?; @@ -937,12 +937,12 @@ impl PageEndpoint { 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 + // https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/plugins/react-loadable-plugin.ts + // next_dynamic transform puts relative paths to the project dir // dir for the origin import. let id = format!( "{} -> {}", - pages_dir + project_src_dir .get_path_to(origin_path) .map_or_else(|| origin_path.to_string(), |path| path.to_string()), import diff --git a/packages/next-swc/crates/next-core/src/next_client/transforms.rs b/packages/next-swc/crates/next-core/src/next_client/transforms.rs index ac11e8daf43d3..19ec2d86926d9 100644 --- a/packages/next-swc/crates/next-core/src/next_client/transforms.rs +++ b/packages/next-swc/crates/next-core/src/next_client/transforms.rs @@ -40,7 +40,7 @@ pub async fn get_next_client_transforms_rules( let mdx_rs = *next_config.mdx_rs().await?; rules.push(get_next_font_transform_rule(mdx_rs)); - let pages_dir = match context_ty { + let pages_or_app_dir = match context_ty { ClientContextType::Pages { pages_dir } => { if !foreign_code { rules.push( @@ -59,12 +59,12 @@ pub async fn get_next_client_transforms_rules( } Some(pages_dir) } - ClientContextType::App { .. } => { + ClientContextType::App { app_dir } => { rules.push(get_server_actions_transform_rule( ActionsTransform::Client, mdx_rs, )); - None + Some(app_dir) } ClientContextType::Fallback | ClientContextType::Other => None, }; @@ -74,7 +74,9 @@ pub async fn get_next_client_transforms_rules( rules.push(get_next_cjs_optimizer_rule(mdx_rs)); rules.push(get_next_pure_rule(mdx_rs)); - rules.push(get_next_dynamic_transform_rule(false, false, pages_dir, mode, mdx_rs).await?); + rules.push( + get_next_dynamic_transform_rule(false, false, pages_or_app_dir, mode, mdx_rs).await?, + ); rules.push(get_next_image_rule()); rules.push(get_next_page_static_info_assert_rule( diff --git a/packages/next-swc/crates/next-core/src/next_server/transforms.rs b/packages/next-swc/crates/next-core/src/next_server/transforms.rs index 12226a087062e..0c0bda597a658 100644 --- a/packages/next-swc/crates/next-core/src/next_server/transforms.rs +++ b/packages/next-swc/crates/next-core/src/next_server/transforms.rs @@ -50,7 +50,7 @@ pub async fn get_next_server_transforms_rules( )); } - let (is_server_components, pages_dir) = match context_ty { + let (is_server_components, pages_or_app_dir) = match context_ty { ServerContextType::Pages { pages_dir } | ServerContextType::PagesApi { pages_dir } => { if !foreign_code { rules.push(get_next_disallow_export_all_in_page_rule( @@ -77,7 +77,7 @@ pub async fn get_next_server_transforms_rules( } (false, Some(pages_dir)) } - ServerContextType::AppSSR { .. } => { + ServerContextType::AppSSR { app_dir } => { // Yah, this is SSR, but this is still treated as a Client transform layer. // need to apply to foreign code too rules.push(get_server_actions_transform_rule( @@ -85,10 +85,12 @@ pub async fn get_next_server_transforms_rules( mdx_rs, )); - (false, None) + (false, Some(app_dir)) } ServerContextType::AppRSC { - client_transition, .. + app_dir, + client_transition, + .. } => { rules.push(get_server_actions_transform_rule( ActionsTransform::Server, @@ -100,7 +102,7 @@ pub async fn get_next_server_transforms_rules( client_transition, )); } - (true, None) + (true, Some(app_dir)) } ServerContextType::AppRoute { .. } => (false, None), ServerContextType::Middleware { .. } | ServerContextType::Instrumentation { .. } => { @@ -110,8 +112,14 @@ pub async fn get_next_server_transforms_rules( if !foreign_code { rules.push( - get_next_dynamic_transform_rule(true, is_server_components, pages_dir, mode, mdx_rs) - .await?, + get_next_dynamic_transform_rule( + true, + is_server_components, + pages_or_app_dir, + mode, + mdx_rs, + ) + .await?, ); rules.push(get_next_amp_attr_rule(mdx_rs)); 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 0ca77ceec8910..4e00b7fd3e11d 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 @@ -26,14 +26,14 @@ use crate::mode::NextMode; pub async fn get_next_dynamic_transform_rule( is_server_compiler: bool, is_react_server_layer: bool, - pages_dir: Option>, + pages_or_app_dir: Option>, mode: Vc, enable_mdx_rs: bool, ) -> Result { let dynamic_transform = EcmascriptInputTransform::Plugin(Vc::cell(Box::new(NextJsDynamic { is_server_compiler, is_react_server_layer, - pages_dir: match pages_dir { + pages_or_app_dir: match pages_or_app_dir { None => None, Some(path) => Some(path.await?.path.clone().into()), }, @@ -52,7 +52,7 @@ pub async fn get_next_dynamic_transform_rule( struct NextJsDynamic { is_server_compiler: bool, is_react_server_layer: bool, - pages_dir: Option, + pages_or_app_dir: Option, mode: NextMode, } @@ -67,7 +67,7 @@ impl CustomTransformer for NextJsDynamic { false, NextDynamicMode::Webpack, FileName::Real(ctx.file_path_str.into()), - self.pages_dir.clone(), + self.pages_or_app_dir.clone(), )); Ok(()) diff --git a/packages/next-swc/crates/next-custom-transforms/src/chain_transforms.rs b/packages/next-swc/crates/next-custom-transforms/src/chain_transforms.rs index 9872b639adc5b..f7557dd237d32 100644 --- a/packages/next-swc/crates/next-custom-transforms/src/chain_transforms.rs +++ b/packages/next-swc/crates/next-custom-transforms/src/chain_transforms.rs @@ -217,7 +217,7 @@ where opts.prefer_esm, NextDynamicMode::Webpack, file.name.clone(), - opts.pages_dir.clone() + opts.pages_dir.clone().or_else(|| opts.app_dir.clone()), ), Optional::new( crate::transforms::page_config::page_config(opts.is_development, opts.is_page_file), diff --git a/packages/next-swc/crates/next-custom-transforms/src/transforms/dynamic.rs b/packages/next-swc/crates/next-custom-transforms/src/transforms/dynamic.rs index 953553575c11d..bceb73602a2c6 100644 --- a/packages/next-swc/crates/next-custom-transforms/src/transforms/dynamic.rs +++ b/packages/next-swc/crates/next-custom-transforms/src/transforms/dynamic.rs @@ -28,14 +28,14 @@ pub fn next_dynamic( prefer_esm: bool, mode: NextDynamicMode, filename: FileName, - pages_dir: Option, + pages_or_app_dir: Option, ) -> impl Fold { NextDynamicPatcher { is_development, is_server_compiler, is_react_server_layer, prefer_esm, - pages_dir, + pages_or_app_dir, filename, dynamic_bindings: vec![], is_next_dynamic_first_arg: false, @@ -81,7 +81,7 @@ struct NextDynamicPatcher { is_server_compiler: bool, is_react_server_layer: bool, prefer_esm: bool, - pages_dir: Option, + pages_or_app_dir: Option, filename: FileName, dynamic_bindings: Vec, is_next_dynamic_first_arg: bool, @@ -216,6 +216,11 @@ impl Fold for NextDynamicPatcher { return expr; }; + let project_dir = match self.pages_or_app_dir.as_deref() { + Some(pages_or_app) => pages_or_app.parent(), + _ => None, + }; + // dev client or server: // loadableGenerated: { // modules: @@ -233,7 +238,7 @@ impl Fold for NextDynamicPatcher { "$left + $right" as Expr, left: Expr = format!( "{} -> ", - rel_filename(self.pages_dir.as_deref(), &self.filename) + rel_filename(project_dir, &self.filename) ) .into(), right: Expr = dynamically_imported_specifier.clone().into(), diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-dev.js index 80026d8666766..bf30df8a5018d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-dev.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; export const NextDynamicNoSSRServerComponent = dynamic(()=>import('../text-dynamic-no-ssr-server'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../text-dynamic-no-ssr-server" + "src/some-file.js -> " + "../text-dynamic-no-ssr-server" ] }, ssr: false diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server-client-layer.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server-client-layer.js index 80026d8666766..bf30df8a5018d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server-client-layer.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server-client-layer.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; export const NextDynamicNoSSRServerComponent = dynamic(()=>import('../text-dynamic-no-ssr-server'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../text-dynamic-no-ssr-server" + "src/some-file.js -> " + "../text-dynamic-no-ssr-server" ] }, ssr: false diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server.js index 80026d8666766..bf30df8a5018d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic-app-dir/no-ssr/output-server.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; export const NextDynamicNoSSRServerComponent = dynamic(()=>import('../text-dynamic-no-ssr-server'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../text-dynamic-no-ssr-server" + "src/some-file.js -> " + "../text-dynamic-no-ssr-server" ] }, ssr: false diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-dev.js index baa31103eaaac..95651812adb2f 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-dev.js @@ -1,18 +1,16 @@ import dynamic1 from 'next/dynamic'; import dynamic2 from 'next/dynamic'; -const DynamicComponent1 = dynamic1(()=>import('../components/hello1') -, { +const DynamicComponent1 = dynamic1(()=>import('../components/hello1'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello1" + "src/some-file.js -> " + "../components/hello1" ] } }); -const DynamicComponent2 = dynamic2(()=>import('../components/hello2') -, { +const DynamicComponent2 = dynamic2(()=>import('../components/hello2'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello2" + "src/some-file.js -> " + "../components/hello2" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-server.js index baa31103eaaac..95651812adb2f 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/duplicated-imports/output-server.js @@ -1,18 +1,16 @@ import dynamic1 from 'next/dynamic'; import dynamic2 from 'next/dynamic'; -const DynamicComponent1 = dynamic1(()=>import('../components/hello1') -, { +const DynamicComponent1 = dynamic1(()=>import('../components/hello1'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello1" + "src/some-file.js -> " + "../components/hello1" ] } }); -const DynamicComponent2 = dynamic2(()=>import('../components/hello2') -, { +const DynamicComponent2 = dynamic2(()=>import('../components/hello2'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello2" + "src/some-file.js -> " + "../components/hello2" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-dev.js index 80026d8666766..bf30df8a5018d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-dev.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; export const NextDynamicNoSSRServerComponent = dynamic(()=>import('../text-dynamic-no-ssr-server'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../text-dynamic-no-ssr-server" + "src/some-file.js -> " + "../text-dynamic-no-ssr-server" ] }, ssr: false diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-server.js index 80026d8666766..bf30df8a5018d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/issue-48098/output-server.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; export const NextDynamicNoSSRServerComponent = dynamic(()=>import('../text-dynamic-no-ssr-server'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../text-dynamic-no-ssr-server" + "src/some-file.js -> " + "../text-dynamic-no-ssr-server" ] }, ssr: false diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-dev.js index d57f2d8cfe9f4..8d34e5a93897d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-dev.js @@ -1,10 +1,9 @@ import dynamic from 'next/dynamic'; import somethingElse from 'something-else'; -const DynamicComponent = dynamic(()=>import('../components/hello') -, { +const DynamicComponent = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-server.js index d57f2d8cfe9f4..8d34e5a93897d 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/member-with-same-name/output-server.js @@ -1,10 +1,9 @@ import dynamic from 'next/dynamic'; import somethingElse from 'something-else'; -const DynamicComponent = dynamic(()=>import('../components/hello') -, { +const DynamicComponent = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-dev.js index c134b15124114..2ad6a3e14d655 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-dev.js @@ -1,9 +1,8 @@ import dynamic from 'next/dynamic'; -const DynamicComponent = dynamic(()=>import('../components/hello') -, { +const DynamicComponent = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-server.js index c134b15124114..2ad6a3e14d655 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/no-options/output-server.js @@ -1,9 +1,8 @@ import dynamic from 'next/dynamic'; -const DynamicComponent = dynamic(()=>import('../components/hello') -, { +const DynamicComponent = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-dev.js index 57f30d46f41de..b5bdb2b6e8373 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-dev.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; const DynamicComponent = dynamic(()=>import(`../components/hello`), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-server.js index 57f30d46f41de..b5bdb2b6e8373 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/template-literal/output-server.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; const DynamicComponent = dynamic(()=>import(`../components/hello`), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] } }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-dev.js index d4d144e1e364c..2a67737f80462 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-dev.js @@ -1,27 +1,24 @@ import dynamic from 'next/dynamic'; -const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hello') -, { +const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] }, - loading: ()=>

...

+ loading: ()=>

...

}); -const DynamicClientOnlyComponent = dynamic(()=>import('../components/hello') -, { +const DynamicClientOnlyComponent = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] }, ssr: false }); -const DynamicClientOnlyComponentWithSuspense = dynamic(()=>import('../components/hello') -, { +const DynamicClientOnlyComponentWithSuspense = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] }, ssr: false, diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-server.js index cd8d7e0b6d15a..2a67737f80462 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/with-options/output-server.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] }, loading: ()=>

...

@@ -10,7 +10,7 @@ const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hell const DynamicClientOnlyComponent = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] }, ssr: false @@ -18,7 +18,7 @@ const DynamicClientOnlyComponent = dynamic(()=>import('../components/hello'), { const DynamicClientOnlyComponentWithSuspense = dynamic(()=>import('../components/hello'), { loadableGenerated: { modules: [ - "some-file.js -> " + "../components/hello" + "src/some-file.js -> " + "../components/hello" ] }, ssr: false, diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-dev.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-dev.js index 73a93be3a5378..5ea32b1c628b3 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-dev.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-dev.js @@ -1,12 +1,10 @@ import dynamic from 'next/dynamic'; -const DynamicComponent = dynamic(()=>handleImport(import('./components/hello')) -, { +const DynamicComponent = dynamic(()=>handleImport(import('./components/hello')), { loadableGenerated: { modules: [ - "some-file.js -> " + "./components/hello" + "src/some-file.js -> " + "./components/hello" ] }, - loading: ()=>null - , + loading: ()=>null, ssr: false }); diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-server.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-server.js index 95b51047d831f..5ea32b1c628b3 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-server.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/next-dynamic/wrapped-import/output-server.js @@ -2,7 +2,7 @@ import dynamic from 'next/dynamic'; const DynamicComponent = dynamic(()=>handleImport(import('./components/hello')), { loadableGenerated: { modules: [ - "some-file.js -> " + "./components/hello" + "src/some-file.js -> " + "./components/hello" ] }, loading: ()=>null, diff --git a/packages/next/src/build/babel/loader/get-config.ts b/packages/next/src/build/babel/loader/get-config.ts index ce5361389c9d6..ed52747491777 100644 --- a/packages/next/src/build/babel/loader/get-config.ts +++ b/packages/next/src/build/babel/loader/get-config.ts @@ -260,7 +260,7 @@ function getFreshConfig( filename: string, inputSourceMap?: object | null ) { - let { isServer, pagesDir, development, hasJsxRuntime, configFile } = + let { isServer, pagesDir, development, hasJsxRuntime, configFile, cwd } = loaderOptions let customConfig: any = configFile @@ -329,6 +329,7 @@ function getFreshConfig( supportsTopLevelAwait: true, isServer, + cwd, pagesDir, isDev: development, hasJsxRuntime, diff --git a/packages/next/src/build/babel/plugins/react-loadable-plugin.ts b/packages/next/src/build/babel/plugins/react-loadable-plugin.ts index 884e6f3b1ed26..9ef7fe50af5bd 100644 --- a/packages/next/src/build/babel/plugins/react-loadable-plugin.ts +++ b/packages/next/src/build/babel/plugins/react-loadable-plugin.ts @@ -168,9 +168,9 @@ export default function ({ t.binaryExpression( '+', t.stringLiteral( - (state.file.opts.caller?.pagesDir + (state.file.opts.caller?.cwd ? relativePath( - state.file.opts.caller.pagesDir, + state.file.opts.caller.cwd, state.file.opts.filename ) : state.file.opts.filename) + ' -> ' diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 6b91198db1672..66b4fed38e6c1 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1737,6 +1737,7 @@ export default async function getBaseWebpackConfig( new ReactLoadablePlugin({ filename: REACT_LOADABLE_MANIFEST, pagesDir, + appDir, runtimeAsset: `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js`, dev, }), diff --git a/packages/next/src/build/webpack/plugins/react-loadable-plugin.ts b/packages/next/src/build/webpack/plugins/react-loadable-plugin.ts index 8699421b559d3..83c814a2b5933 100644 --- a/packages/next/src/build/webpack/plugins/react-loadable-plugin.ts +++ b/packages/next/src/build/webpack/plugins/react-loadable-plugin.ts @@ -53,14 +53,12 @@ function getChunkGroupFromBlock( function buildManifest( _compiler: webpack.Compiler, compilation: webpack.Compilation, - pagesDir: string | undefined, + projectSrcDir: string | undefined, dev: boolean ) { - // If there's no pagesDir, output an empty manifest - if (!pagesDir) { + if (!projectSrcDir) { return {} } - let manifest: { [k: string]: { id: string | number; files: string[] } } = {} // This is allowed: @@ -91,7 +89,7 @@ function buildManifest( // We construct a "unique" key from origin module and request // It's not perfect unique, but that will be fine for us. // We also need to construct the same in the babel plugin. - const key = `${path.relative(pagesDir, originRequest)} -> ${ + const key = `${path.relative(projectSrcDir, originRequest)} -> ${ dependency.request }` @@ -150,30 +148,34 @@ function buildManifest( export class ReactLoadablePlugin { private filename: string - private pagesDir?: string + private pagesOrAppDir: string | undefined private runtimeAsset?: string private dev: boolean constructor(opts: { filename: string pagesDir?: string + appDir?: string runtimeAsset?: string dev: boolean }) { this.filename = opts.filename - this.pagesDir = opts.pagesDir + this.pagesOrAppDir = opts.pagesDir || opts.appDir this.runtimeAsset = opts.runtimeAsset this.dev = opts.dev } createAssets(compiler: any, compilation: any, assets: any) { + const projectSrcDir = this.pagesOrAppDir + ? path.dirname(this.pagesOrAppDir) + : undefined const manifest = buildManifest( compiler, compilation, - this.pagesDir, + projectSrcDir, 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/src/client/components/request-async-storage.external.ts b/packages/next/src/client/components/request-async-storage.external.ts index 7707485bccb7a..bcaddf009fb42 100644 --- a/packages/next/src/client/components/request-async-storage.external.ts +++ b/packages/next/src/client/components/request-async-storage.external.ts @@ -11,6 +11,8 @@ export interface RequestStore { readonly cookies: ReadonlyRequestCookies readonly mutableCookies: ResponseCookies readonly draftMode: DraftModeProvider + readonly reactLoadableManifest: Record + readonly assetPrefix: string } export type RequestAsyncStorage = AsyncLocalStorage diff --git a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts index 8eccc19d120a4..c0d12f6873fcd 100644 --- a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts @@ -130,6 +130,8 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< return cache.draftMode }, + reactLoadableManifest: renderOpts?.reactLoadableManifest || {}, + assetPrefix: renderOpts?.assetPrefix || '', } return storage.run(store, callback, store) diff --git a/packages/next/src/shared/lib/app-dynamic.tsx b/packages/next/src/shared/lib/app-dynamic.tsx index 9efbc8aed56b6..651e2dd1ffe5d 100644 --- a/packages/next/src/shared/lib/app-dynamic.tsx +++ b/packages/next/src/shared/lib/app-dynamic.tsx @@ -19,6 +19,7 @@ export type DynamicOptions

= LoadableGeneratedOptions & { loading?: (loadingProps: DynamicOptionsLoadingProps) => JSX.Element | null loader?: Loader

loadableGenerated?: LoadableGeneratedOptions + modules?: string[] ssr?: boolean } @@ -34,7 +35,7 @@ export default function dynamic

( dynamicOptions: DynamicOptions

| Loader

, options?: DynamicOptions

): React.ComponentType

{ - const loadableOptions: LoadableOptions

= { + let loadableOptions: LoadableOptions

= { // A loading component is not required, so we default it loading: ({ error, isLoading, pastDelay }) => { if (!pastDelay) return null @@ -60,5 +61,13 @@ export default function dynamic

( loadableOptions.loader = dynamicOptions } - return Loadable({ ...loadableOptions, ...options }) + const mergedOptions = { + ...loadableOptions, + ...options, + } + + return Loadable({ + ...mergedOptions, + modules: mergedOptions.loadableGenerated?.modules, + }) } diff --git a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx index 56c65c98bae45..1aa448a61e366 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx @@ -1,6 +1,7 @@ import { Suspense, lazy } from 'react' import { BailoutToCSR } from './dynamic-bailout-to-csr' import type { ComponentModule } from './types' +import { PreloadCss } from './preload-css' // Normalize loader to return the module as form { default: Component } for `React.lazy`. // Also for backward compatible since next/dynamic allows to resolve a component directly with loader @@ -34,6 +35,7 @@ interface LoadableOptions { loader?: () => Promise | ComponentModule> loading?: React.ComponentType | null ssr?: boolean + modules?: string[] } function Loadable(options: LoadableOptions) { @@ -47,7 +49,13 @@ function Loadable(options: LoadableOptions) { ) : null const children = opts.ssr ? ( - + <> + {/* During SSR, we need to preload the CSS from the dynamic component to avoid flash of unstyled content */} + {typeof window === 'undefined' ? ( + + ) : null} + + ) : ( diff --git a/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx b/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx new file mode 100644 index 0000000000000..cc1549e970cc0 --- /dev/null +++ b/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx @@ -0,0 +1,48 @@ +'use client' + +export function PreloadCss({ moduleIds }: { moduleIds: string[] | undefined }) { + // Early return in client compilation and only load requestStore on server side + if (typeof window !== 'undefined') { + return null + } + const { + getExpectedRequestStore, + } = require('../../../client/components/request-async-storage.external') + const requestStore = getExpectedRequestStore() + + const allFiles = [] + + // Search the current dynamic call unique key id in react loadable manifest, + // and find the corresponding CSS files to preload + if (requestStore.reactLoadableManifest && moduleIds) { + const manifest = requestStore.reactLoadableManifest + for (const key of moduleIds) { + if (!manifest[key]) continue + const cssFiles = manifest[key].files.filter((file: string) => + file.endsWith('.css') + ) + allFiles.push(...cssFiles) + } + } + + if (allFiles.length === 0) { + return null + } + + return ( + <> + {allFiles.map((file) => { + return ( + + ) + })} + + ) +} diff --git a/packages/next/src/shared/lib/lazy-dynamic/types.ts b/packages/next/src/shared/lib/lazy-dynamic/types.ts index 64b8209cbfd2b..ad5eb8407120d 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/types.ts +++ b/packages/next/src/shared/lib/lazy-dynamic/types.ts @@ -6,11 +6,9 @@ export declare type LoaderComponent

= Promise< export declare type Loader

= () => LoaderComponent

-export type LoaderMap = { [module: string]: () => Loader } - export type LoadableGeneratedOptions = { webpack?(): any - modules?(): LoaderMap + modules?: string[] } export type DynamicOptionsLoadingProps = { diff --git a/test/development/basic/next-dynamic.test.ts b/test/development/basic/next-dynamic.test.ts index 1613de327fb06..1ad7d8e39c09a 100644 --- a/test/development/basic/next-dynamic.test.ts +++ b/test/development/basic/next-dynamic.test.ts @@ -75,7 +75,7 @@ describe.each([ // Make sure the client side knows it has to wait for the bundle expect( JSON.parse($('#__NEXT_DATA__').html()).dynamicIds - ).toContain('dynamic/ssr.js -> ../../components/hello1') + ).toContain('pages/dynamic/ssr.js -> ../../components/hello1') expect($('body').text()).toMatch(/Hello World 1/) }) @@ -84,7 +84,9 @@ describe.each([ // Make sure the client side knows it has to wait for the bundle expect( JSON.parse($('#__NEXT_DATA__').html()).dynamicIds - ).toContain('dynamic/function.js -> ../../components/hello1') + ).toContain( + 'pages/dynamic/function.js -> ../../components/hello1' + ) expect($('body').text()).toMatch(/Hello World 1/) }) diff --git a/test/integration/react-18/test/basics.js b/test/integration/react-18/test/basics.js index 00c46440900b8..ae7f6236e16eb 100644 --- a/test/integration/react-18/test/basics.js +++ b/test/integration/react-18/test/basics.js @@ -40,7 +40,7 @@ export default (context, env) => { const { dynamicIds } = JSON.parse($('#__NEXT_DATA__').html()) if (env === 'dev') { - expect(dynamicIds).toContain(`${page}.js -> ../components/foo`) + expect(dynamicIds).toContain(`pages/${page}.js -> ../components/foo`) } else { expect(dynamicIds.length).toBe(1) } diff --git a/test/production/app-dir/dynamic-css/app/another/page.js b/test/production/app-dir/dynamic-css/app/another/page.js new file mode 100644 index 0000000000000..79dfc5aa6a3e1 --- /dev/null +++ b/test/production/app-dir/dynamic-css/app/another/page.js @@ -0,0 +1,11 @@ +'use client' + +import dynamicApi from 'next/dynamic' + +const AsyncBar = dynamicApi(() => import('../../components/bar')) + +export default function Page() { + return +} + +export const dynamic = 'force-dynamic' diff --git a/test/production/app-dir/dynamic-css/app/layout.js b/test/production/app-dir/dynamic-css/app/layout.js new file mode 100644 index 0000000000000..803f17d863c8a --- /dev/null +++ b/test/production/app-dir/dynamic-css/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir/dynamic-css/app/page.js b/test/production/app-dir/dynamic-css/app/page.js new file mode 100644 index 0000000000000..1ab029456d1a4 --- /dev/null +++ b/test/production/app-dir/dynamic-css/app/page.js @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default function Page() { + return ( +

+ + SSR + +
+ ) +} diff --git a/test/production/app-dir/dynamic-css/app/ssr/page.js b/test/production/app-dir/dynamic-css/app/ssr/page.js new file mode 100644 index 0000000000000..e67a184fafc88 --- /dev/null +++ b/test/production/app-dir/dynamic-css/app/ssr/page.js @@ -0,0 +1,11 @@ +'use client' + +import dynamicApi from 'next/dynamic' + +const AsyncFoo = dynamicApi(() => import('../../components/foo')) + +export default function Page() { + return +} + +export const dynamic = 'force-dynamic' diff --git a/test/production/app-dir/dynamic-css/components/bar.css b/test/production/app-dir/dynamic-css/components/bar.css new file mode 100644 index 0000000000000..87902f84d33af --- /dev/null +++ b/test/production/app-dir/dynamic-css/components/bar.css @@ -0,0 +1,3 @@ +.text { + border: 1px solid blue; +} diff --git a/test/production/app-dir/dynamic-css/components/bar.js b/test/production/app-dir/dynamic-css/components/bar.js new file mode 100644 index 0000000000000..a4f561c225384 --- /dev/null +++ b/test/production/app-dir/dynamic-css/components/bar.js @@ -0,0 +1,5 @@ +import './bar.css' + +export default function Bar() { + return

bar

+} diff --git a/test/production/app-dir/dynamic-css/components/foo.css b/test/production/app-dir/dynamic-css/components/foo.css new file mode 100644 index 0000000000000..3efa99e6fb171 --- /dev/null +++ b/test/production/app-dir/dynamic-css/components/foo.css @@ -0,0 +1,3 @@ +.text { + color: red; +} diff --git a/test/production/app-dir/dynamic-css/components/foo.js b/test/production/app-dir/dynamic-css/components/foo.js new file mode 100644 index 0000000000000..27d1db7b17176 --- /dev/null +++ b/test/production/app-dir/dynamic-css/components/foo.js @@ -0,0 +1,5 @@ +import './foo.css' + +export default function Foo() { + return

foo

+} diff --git a/test/production/app-dir/dynamic-css/index.test.ts b/test/production/app-dir/dynamic-css/index.test.ts new file mode 100644 index 0000000000000..2892e69fbc466 --- /dev/null +++ b/test/production/app-dir/dynamic-css/index.test.ts @@ -0,0 +1,51 @@ +import { createNextDescribe } from 'e2e-utils' +import { retry } from 'next-test-utils' + +createNextDescribe( + 'app dir - dynamic css', + { + files: __dirname, + skipDeployment: true, + }, + ({ next }) => { + it('should preload css of dynamic component during SSR', async () => { + const $ = await next.render$('/ssr') + const cssLinks = $('link[rel="stylesheet"]') + expect(cssLinks.attr('href')).toContain('.css') + }) + + it('should only apply corresponding css for page loaded that /ssr', async () => { + const browser = await next.browser('/ssr') + await retry(async () => { + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('.text')).color` + ) + ).toBe('rgb(255, 0, 0)') + // Default border width, which is not effected by bar.css that is not loaded in /ssr + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('.text')).borderWidth` + ) + ).toBe('0px') + }) + }) + + it('should only apply corresponding css for page loaded that /another', async () => { + const browser = await next.browser('/another') + await retry(async () => { + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('.text')).color` + ) + ).not.toBe('rgb(255, 0, 0)') + // Default border width, which is not effected by bar.css that is not loaded in /ssr + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('.text')).borderWidth` + ) + ).toBe('1px') + }) + }) + } +) diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index f7d992a772da8..35ef4e97626ad 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -14515,6 +14515,12 @@ "flakey": [], "runtimeError": false }, + "test/production/app-dir/dynamic-css/index.test.ts": { + "passed": [], + "failed": [ + "app dir - dynamic css should preload css of dynamic component during SSR" + ] + }, "test/production/app-dir-edge-runtime-with-wasm/index.test.ts": { "passed": ["app-dir edge runtime with wasm should have built"], "failed": [],