diff --git a/crates/next-core/src/next_manifests/client_reference_manifest.rs b/crates/next-core/src/next_manifests/client_reference_manifest.rs index 96766f3a2864a..4c15d694c7f5a 100644 --- a/crates/next-core/src/next_manifests/client_reference_manifest.rs +++ b/crates/next-core/src/next_manifests/client_reference_manifest.rs @@ -104,16 +104,6 @@ impl ClientReferenceManifest { (Vec::new(), false) }; - 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, - r#async: client_is_async, - }, - ); - if let Some(ssr_chunking_context) = ssr_chunking_context { let ssr_chunk_item = ecmascript_client_reference .ssr_module @@ -154,6 +144,19 @@ impl ClientReferenceManifest { (Vec::new(), false) }; + 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, + // This should of course be client_is_async, but SSR can become async + // due to ESM externals, and the ssr_manifest_node is currently ignored + // by React. + r#async: client_is_async || ssr_is_async, + }, + ); + let mut ssr_manifest_node = ManifestNode::default(); ssr_manifest_node.module_exports.insert( "*".into(), @@ -161,7 +164,8 @@ impl ClientReferenceManifest { name: "*".into(), id: (&*ssr_module_id).into(), chunks: ssr_chunks_paths, - r#async: ssr_is_async, + // See above + r#async: client_is_async || ssr_is_async, }, ); diff --git a/crates/next-core/src/next_server/context.rs b/crates/next-core/src/next_server/context.rs index a0865d56f4856..2aa23c032e32c 100644 --- a/crates/next-core/src/next_server/context.rs +++ b/crates/next-core/src/next_server/context.rs @@ -182,8 +182,7 @@ pub async fn get_server_resolve_options_context( project_path, project_path.root(), ExternalPredicate::Only(Vc::cell(external_packages)).cell(), - // 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 { .. }), + *next_config.import_externals().await?, ); let mut custom_conditions = vec![mode.await?.condition().to_string().into()]; 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 7faa89f365921..4959d506df187 100644 --- a/test/e2e/app-dir/app-external/app-external.test.ts +++ b/test/e2e/app-dir/app-external/app-external.test.ts @@ -264,6 +264,11 @@ describe('app dir - external dependency', () => { expect(html).toContain('hello') }) + it('should support client module references with SSR-only ESM externals', async () => { + const html = await next.render('/esm-client-ref-external') + expect(html).toContain('client external-pure-esm-lib') + }) + it('should support exporting multiple star re-exports', async () => { const html = await next.render('/wildcard') expect(html).toContain('Foo') diff --git a/test/e2e/app-dir/app-external/app/esm-client-ref-external/client.js b/test/e2e/app-dir/app-external/app/esm-client-ref-external/client.js new file mode 100644 index 0000000000000..11fe92ee0aa66 --- /dev/null +++ b/test/e2e/app-dir/app-external/app/esm-client-ref-external/client.js @@ -0,0 +1,7 @@ +'use client' + +import name from 'esm' + +export function Client() { + return
{`client ${name}`}
+} diff --git a/test/e2e/app-dir/app-external/app/esm-client-ref-external/page.js b/test/e2e/app-dir/app-external/app/esm-client-ref-external/page.js new file mode 100644 index 0000000000000..e37844b0cb6f9 --- /dev/null +++ b/test/e2e/app-dir/app-external/app/esm-client-ref-external/page.js @@ -0,0 +1,9 @@ +import { Client } from './client' + +export default function Page() { + return ( +

+ +

+ ) +} diff --git a/test/e2e/app-dir/app-external/next.config.js b/test/e2e/app-dir/app-external/next.config.js index 2be2c35d204f7..53a9abbb2dd79 100644 --- a/test/e2e/app-dir/app-external/next.config.js +++ b/test/e2e/app-dir/app-external/next.config.js @@ -5,5 +5,6 @@ module.exports = { 'conditional-exports-optout', 'dual-pkg-optout', 'transitive-external', + 'esm', ], } diff --git a/test/e2e/app-dir/app-external/node_modules/esm/index.js b/test/e2e/app-dir/app-external/node_modules/esm/index.js new file mode 100644 index 0000000000000..44ebc3bb69a55 --- /dev/null +++ b/test/e2e/app-dir/app-external/node_modules/esm/index.js @@ -0,0 +1 @@ +export default 'external-pure-esm-lib' diff --git a/test/e2e/app-dir/app-external/node_modules/esm/package.json b/test/e2e/app-dir/app-external/node_modules/esm/package.json new file mode 100644 index 0000000000000..28e738c6e30f8 --- /dev/null +++ b/test/e2e/app-dir/app-external/node_modules/esm/package.json @@ -0,0 +1,5 @@ +{ + "name": "esm", + "type": "module", + "exports": "./index.js" +} diff --git a/test/e2e/esm-externals/esm-externals.test.ts b/test/e2e/esm-externals/esm-externals.test.ts index 329b3176ac804..dc6d827129669 100644 --- a/test/e2e/esm-externals/esm-externals.test.ts +++ b/test/e2e/esm-externals/esm-externals.test.ts @@ -43,9 +43,7 @@ describe('esm-externals', () => { // App dir describe.each(['/server', '/client'])('app dir url %s', (url) => { const expectedHtml = isTurbopack - ? url === '/client' - ? 'Hello Wrong+Wrong+Alternative' - : 'Hello World+World+World' + ? 'Hello World+World+World' : 'Hello World+World+Alternative' const expectedText = isTurbopack