diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index d5b9a22efd..f8b97d27fa 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -848,13 +848,13 @@ impl AppEndpoint { { entry_client_chunks.extend(chunks.await?.iter().copied()); } - for chunks in client_references_chunks_ref + for (chunks, _) in client_references_chunks_ref .client_component_client_chunks .values() { client_assets.extend(chunks.await?.iter().copied()); } - for chunks in client_references_chunks_ref + for (chunks, _) in client_references_chunks_ref .client_component_ssr_chunks .values() { @@ -948,7 +948,7 @@ impl AppEndpoint { // initialization let client_references_chunks = &*client_references_chunks.await?; - for &ssr_chunks in client_references_chunks + for (ssr_chunks, _) in client_references_chunks .client_component_ssr_chunks .values() { diff --git a/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs b/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs index 2845c26fae..484d0920a1 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs @@ -29,8 +29,10 @@ fn client_modules_ssr_modifier() -> Vc { #[turbo_tasks::value] pub struct ClientReferencesChunks { - pub client_component_client_chunks: IndexMap>, - pub client_component_ssr_chunks: IndexMap>, + pub client_component_client_chunks: + IndexMap, AvailabilityInfo)>, + pub client_component_ssr_chunks: + IndexMap, AvailabilityInfo)>, pub layout_segment_client_chunks: IndexMap, Vc>, } @@ -63,23 +65,47 @@ pub async fn get_app_client_references_chunks( ) => { let ecmascript_client_reference_ref = ecmascript_client_reference.await?; - ( - client_chunking_context.root_chunk_group_assets(Vc::upcast( + + let client_chunk_group = client_chunking_context + .root_chunk_group(Vc::upcast( ecmascript_client_reference_ref.client_module, - )), - ssr_chunking_context.map(|ssr_chunking_context| { - ssr_chunking_context.root_chunk_group_assets(Vc::upcast( - ecmascript_client_reference_ref.ssr_module, + )) + .await?; + + ( + ( + client_chunk_group.assets, + client_chunk_group.availability_info, + ), + if let Some(ssr_chunking_context) = ssr_chunking_context { + let ssr_chunk_group = ssr_chunking_context + .root_chunk_group(Vc::upcast( + ecmascript_client_reference_ref.ssr_module, + )) + .await?; + + Some(( + ssr_chunk_group.assets, + ssr_chunk_group.availability_info, )) - }), + } else { + None + }, ) } ClientReferenceType::CssClientReference(css_client_reference) => { let css_client_reference_ref = css_client_reference.await?; - ( - client_chunking_context.root_chunk_group_assets(Vc::upcast( + let client_chunk_group = client_chunking_context + .root_chunk_group(Vc::upcast( css_client_reference_ref.client_module, - )), + )) + .await?; + + ( + ( + client_chunk_group.assets, + client_chunk_group.availability_info, + ), None, ) } @@ -162,6 +188,7 @@ pub async fn get_app_client_references_chunks( }) .try_flat_join() .await?; + let ssr_chunk_group = if !ssr_modules.is_empty() { ssr_chunking_context.map(|ssr_chunking_context| { let _span = tracing::info_span!( @@ -182,6 +209,7 @@ pub async fn get_app_client_references_chunks( } else { None }; + let client_modules = client_reference_types .iter() .map(|client_reference_ty| async move { @@ -238,8 +266,10 @@ pub async fn get_app_client_references_chunks( if let ClientReferenceType::EcmascriptClientReference(_) = client_reference_ty { - client_component_client_chunks - .insert(client_reference_ty, client_chunks); + client_component_client_chunks.insert( + client_reference_ty, + (client_chunks, client_chunk_group.availability_info), + ); } } } @@ -259,7 +289,10 @@ pub async fn get_app_client_references_chunks( if let ClientReferenceType::EcmascriptClientReference(_) = client_reference_ty { - client_component_ssr_chunks.insert(client_reference_ty, ssr_chunks); + client_component_ssr_chunks.insert( + client_reference_ty, + (ssr_chunks, ssr_chunk_group.availability_info), + ); } } } diff --git a/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs b/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs index 082f35cba9..d1db781d53 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs @@ -5,7 +5,10 @@ use turbo_tasks_fs::{File, FileSystemPath}; use turbopack_binding::turbopack::{ core::{ asset::AssetContent, - chunk::{ChunkItemExt, ChunkableModule, ChunkingContext, ModuleId as TurbopackModuleId}, + chunk::{ + availability_info::AvailabilityInfo, ChunkItem, ChunkItemExt, ChunkableModule, + ChunkingContext, ModuleId as TurbopackModuleId, + }, output::OutputAsset, virtual_output::VirtualOutputAsset, }, @@ -66,62 +69,70 @@ impl ClientReferenceManifest { .to_string() .await?; - let client_module_id = ecmascript_client_reference + let client_chunk_item = ecmascript_client_reference .client_module - .as_chunk_item(Vc::upcast(client_chunking_context)) - .id() - .await?; + .as_chunk_item(Vc::upcast(client_chunking_context)); - let client_chunks_paths = if let Some(client_chunks) = client_references_chunks - .client_component_client_chunks - .get(&app_client_reference_ty) - { - let client_chunks = client_chunks.await?; - let client_chunks_paths = client_chunks - .iter() - .map(|chunk| chunk.ident().path()) - .try_join() - .await?; + let client_module_id = client_chunk_item.id().await?; + + let (client_chunks_paths, client_is_async) = + if let Some((client_chunks, client_availability_info)) = + client_references_chunks + .client_component_client_chunks + .get(&app_client_reference_ty) + { + let client_chunks = client_chunks.await?; + let client_chunks_paths = client_chunks + .iter() + .map(|chunk| chunk.ident().path()) + .try_join() + .await?; + + let chunk_paths = client_chunks_paths + .iter() + .filter_map(|chunk_path| client_relative_path.get_path_to(chunk_path)) + .map(ToString::to_string) + // It's possible that a chunk also emits CSS files, that will + // be handled separatedly. + .filter(|path| path.ends_with(".js")) + .map(RcStr::from) + .collect::>(); + + let is_async = + is_item_async(client_availability_info, client_chunk_item).await?; + + (chunk_paths, is_async) + } else { + (Vec::new(), false) + }; - client_chunks_paths - .iter() - .filter_map(|chunk_path| client_relative_path.get_path_to(chunk_path)) - .map(ToString::to_string) - // It's possible that a chunk also emits CSS files, that will - // be handled separatedly. - .filter(|path| path.ends_with(".js")) - .map(RcStr::from) - .collect::>() - } else { - Vec::new() - }; entry_manifest.client_modules.module_exports.insert( get_client_reference_module_key(&server_path, "*"), ManifestNodeEntry { name: "*".into(), id: (&*client_module_id).into(), chunks: client_chunks_paths, - // TODO(WEB-434) - r#async: false, + r#async: client_is_async, }, ); if let Some(ssr_chunking_context) = ssr_chunking_context { - let ssr_module_id = ecmascript_client_reference + let ssr_chunk_item = ecmascript_client_reference .ssr_module - .as_chunk_item(Vc::upcast(ssr_chunking_context)) - .id() - .await?; + .as_chunk_item(Vc::upcast(ssr_chunking_context)); - let ssr_chunks_paths = if runtime == NextRuntime::Edge { + let ssr_module_id = ssr_chunk_item.id().await?; + + let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge { // the chunks get added to the middleware-manifest.json instead // of this file because the // edge runtime doesn't support dynamically // loading chunks. - Vec::new() - } else if let Some(ssr_chunks) = client_references_chunks - .client_component_ssr_chunks - .get(&app_client_reference_ty) + (Vec::new(), false) + } else if let Some((ssr_chunks, ssr_availability_info)) = + client_references_chunks + .client_component_ssr_chunks + .get(&app_client_reference_ty) { let ssr_chunks = ssr_chunks.await?; @@ -131,15 +142,20 @@ impl ClientReferenceManifest { .try_join() .await?; - ssr_chunks_paths + let chunk_paths = ssr_chunks_paths .iter() .filter_map(|chunk_path| node_root_ref.get_path_to(chunk_path)) .map(ToString::to_string) .map(RcStr::from) - .collect::>() + .collect::>(); + + let is_async = is_item_async(ssr_availability_info, ssr_chunk_item).await?; + + (chunk_paths, is_async) } else { - Vec::new() + (Vec::new(), false) }; + let mut ssr_manifest_node = ManifestNode::default(); ssr_manifest_node.module_exports.insert( "*".into(), @@ -147,8 +163,7 @@ impl ClientReferenceManifest { name: "*".into(), id: (&*ssr_module_id).into(), chunks: ssr_chunks_paths, - // TODO(WEB-434) - r#async: false, + r#async: ssr_is_async, }, ); @@ -253,3 +268,18 @@ pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> format!("{}#{}", server_path, export_name).into() } } + +async fn is_item_async( + availability_info: &AvailabilityInfo, + chunk_item: Vc>, +) -> Result { + let Some(available_chunk_items) = availability_info.available_chunk_items() else { + return Ok(false); + }; + + let Some(info) = &*available_chunk_items.get(chunk_item).await? else { + return Ok(false); + }; + + Ok(info.is_async) +} diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index 9022efc48d..760c661ab6 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -174,14 +174,15 @@ pub async fn get_server_resolve_options_context( external_packages.retain(|item| !transpile_packages.contains(item)); + let ty = ty.into_value(); + let server_external_packages_plugin = ExternalCjsModulesResolvePlugin::new( project_path, project_path.root(), ExternalPredicate::Only(Vc::cell(external_packages)).cell(), - // TODO(sokra) esmExternals support - false, + // app-ssr can't have esm externals as that would make the module async on the server only + *next_config.import_externals().await? && !matches!(ty, ServerContextType::AppSSR { .. }), ); - let ty = ty.into_value(); let mut custom_conditions = vec![mode.await?.condition().to_string().into()]; custom_conditions.extend( diff --git a/test/e2e/app-dir/app-external/app-external.test.ts b/test/e2e/app-dir/app-external/app-external.test.ts index 61a398d84b..d7ad6d5aec 100644 --- a/test/e2e/app-dir/app-external/app-external.test.ts +++ b/test/e2e/app-dir/app-external/app-external.test.ts @@ -16,7 +16,7 @@ async function resolveStreamResponse(response: any, onData?: any) { } describe('app dir - external dependency', () => { - const { next, skipped, isTurbopack } = nextTestSetup({ + const { next, skipped } = nextTestSetup({ files: __dirname, dependencies: { swr: 'latest', @@ -286,10 +286,7 @@ describe('app dir - external dependency', () => { browser.elementByCss('#dual-pkg-outout button').click() await check(async () => { const text = await browser.elementByCss('#dual-pkg-outout p').text() - // TODO: enable esm externals for app router in turbopack for actions - expect(text).toBe( - isTurbopack ? 'dual-pkg-optout:cjs' : 'dual-pkg-optout:mjs' - ) + expect(text).toBe('dual-pkg-optout:mjs') return 'success' }, /success/) }) diff --git a/test/e2e/esm-externals/esm-externals.test.ts b/test/e2e/esm-externals/esm-externals.test.ts index be5b8340b1..329b3176ac 100644 --- a/test/e2e/esm-externals/esm-externals.test.ts +++ b/test/e2e/esm-externals/esm-externals.test.ts @@ -8,65 +8,63 @@ describe('esm-externals', () => { const { next, isTurbopack } = nextTestSetup({ files: __dirname, }) + // Pages - { - const urls = ['/static', '/ssr', '/ssg'] + describe.each(['/static', '/ssr', '/ssg'])('pages url %s', (url) => { + // For invalid esm packages that have "import" pointing to a non-esm-flagged module + // webpack is using the CJS version instead, but Turbopack is opting out of + // externalizing and bundling the non-esm-flagged module. + const expectedHtml = isTurbopack + ? 'Hello World+World+World+World+World+World' + : url === '/static' + ? 'Hello World+World+Alternative+World+World+World' + : 'Hello World+World+Alternative+World+World+Alternative' - for (const url of urls) { - // For invalid esm packages that have "import" pointing to a non-esm-flagged module - // webpack is using the CJS version instead but Turbopack is opting out of - // externalizing and bundling the non-esm-flagged module. - const expectedHtml = isTurbopack - ? /Hello World\+World\+World\+World\+World\+World/ - : url === '/static' - ? /Hello World\+World\+Alternative\+World\+World\+World/ - : /Hello World\+World\+Alternative\+World\+World\+Alternative/ - // On client side, webpack always bundlings so it uses the non-esm-flagged module too. - const expectedText = - isTurbopack || url === '/static' - ? /Hello World\+World\+World\+World\+World\+World/ - : /Hello World\+World\+World\+World\+World\+Alternative/ + // On the client side, webpack always bundles, so it uses the non-esm-flagged module too. + const expectedText = + isTurbopack || url === '/static' + ? 'Hello World+World+World+World+World+World' + : 'Hello World+World+World+World+World+Alternative' - it(`should return the correct SSR HTML for ${url}`, async () => { - const res = await next.fetch(url) - const html = await res.text() - expect(normalize(html)).toMatch(expectedHtml) - }) + it('should return the correct SSR HTML', async () => { + const $ = await next.render$(url) + const body = $('body > div > div').html() + expect(normalize(body)).toEqual(expectedHtml) + }) - it(`should render the correct page for ${url}`, async () => { - const browser = await next.browser(url) - expect(await browser.elementByCss('body').text()).toMatch(expectedText) - }) - } - } + it('should render the correct page', async () => { + const browser = await next.browser(url) + expect(await browser.elementByCss('body > div').text()).toEqual( + expectedText + ) + }) + }) // App dir - { - // TODO App Dir doesn't use esmExternals: true correctly for Turbopack - // so we only verify that the page doesn't crash, but ignore the actual content - // const expectedHtml = isTurbopack ? /Hello World\+World\+World/ : /Hello World\+World\+Alternative/ + describe.each(['/server', '/client'])('app dir url %s', (url) => { const expectedHtml = isTurbopack - ? /Hello Wrong\+Wrong\+Alternative/ - : /Hello World\+World\+Alternative/ - const urls = ['/server', '/client'] + ? url === '/client' + ? 'Hello Wrong+Wrong+Alternative' + : 'Hello World+World+World' + : 'Hello World+World+Alternative' - for (const url of urls) { - const expectedText = - url !== '/server' - ? /Hello World\+World\+World/ - : isTurbopack - ? /Hello Wrong\+Wrong\+Alternative/ - : /Hello World\+World\+Alternative/ - it(`should return the correct SSR HTML for ${url}`, async () => { - const res = await next.fetch(url) - const html = await res.text() - expect(normalize(html)).toMatch(expectedHtml) - }) + const expectedText = isTurbopack + ? 'Hello World+World+World' + : url === '/client' + ? 'Hello World+World+World' + : 'Hello World+World+Alternative' - it(`should render the correct page for ${url}`, async () => { - const browser = await next.browser(url) - expect(await browser.elementByCss('body').text()).toMatch(expectedText) - }) - } - } + it('should return the correct SSR HTML', async () => { + const $ = await next.render$(url) + const body = $('body > div').html() + expect(normalize(body)).toEqual(expectedHtml) + }) + + it('should render the correct page', async () => { + const browser = await next.browser(url) + expect(await browser.elementByCss('body > div').text()).toEqual( + expectedText + ) + }) + }) })