vercel/next.js · #99115
Use graph-backed codegen for client proxies and WASM loaders
crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs27 + / 29 −
@@ -1,6 +1,6 @@ use std::{io::Write, iter::once}; -use anyhow::{Context, Result, bail};+use anyhow::{Result, bail}; use async_trait::async_trait; use indoc::writedoc; use turbo_rcstr::{RcStr, rcstr};@@ -29,6 +29,7 @@ use turbopack_ecmascript::{ EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptChunkType, EcmascriptExports, },+ chunk_item_content_with_code_from, runtime_functions::TURBOPACK_EXPORT_NAMESPACE, utils::StringifyJs, };@@ -258,26 +259,19 @@ impl Module for EcmascriptClientReferenceModule { #[turbo_tasks::value_impl] impl ChunkableModule for EcmascriptClientReferenceModule { #[turbo_tasks::function]- async fn as_chunk_item(+ fn as_chunk_item( self: ResolvedVc<Self>,- module_graph: Vc<ModuleGraph>,+ module_graph: ResolvedVc<ModuleGraph>, chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,- ) -> Result<Vc<Box<dyn ChunkItem>>> {- let item = self- .proxy_module()- .as_chunk_item(module_graph, *chunking_context);- let ecmascript_item =- ResolvedVc::try_downcast::<Box<dyn EcmascriptChunkItem>>(item.to_resolved().await?)- .context("EcmascriptModuleAsset must implement EcmascriptChunkItem")?;-- Ok(Vc::upcast(+ ) -> Vc<Box<dyn ChunkItem>> {+ Vc::upcast( EcmascriptClientReferenceProxyChunkItem { inner_module: self,- inner_chunk_item: ecmascript_item,+ module_graph, chunking_context, } .cell(),- ))+ ) } } @@ -291,25 +285,28 @@ impl EcmascriptChunkPlaceable for EcmascriptClientReferenceModule { } #[turbo_tasks::function]- fn chunk_item_content(- self: Vc<Self>,- _chunking_context: Vc<Box<dyn ChunkingContext>>,+ async fn chunk_item_content(+ self: ResolvedVc<Self>,+ chunking_context: ResolvedVc<Box<dyn ChunkingContext>>, _module_graph: Vc<ModuleGraph>,- _async_module_info: Option<Vc<AsyncModuleInfo>>,+ async_module_info: Option<Vc<AsyncModuleInfo>>, _estimated: bool, ) -> Result<Vc<EcmascriptChunkItemContent>> {- bail!("Attempted to get chunk_item_content for EcmascriptClientReferenceModule")+ Ok(chunk_item_content_with_code_from(+ *ResolvedVc::upcast(self),+ self.proxy_module(),+ *chunking_context,+ async_module_info,+ )) } } -/// This wrapper only exists to overwrite the `asset_ident` method of the-/// wrapped [`Vc<Box<dyn EcmascriptChunkItem>>`]. Otherwise, the asset ident of-/// the chunk item would not be the same as the asset ident of the-/// [`Vc<EcmascriptClientReferenceModule>`].+/// This wrapper uses the client-reference module's graph identity for both its asset identifier+/// and code generation. The generated proxy supplies the code, but isn't itself in the graph. #[turbo_tasks::value] struct EcmascriptClientReferenceProxyChunkItem { inner_module: ResolvedVc<EcmascriptClientReferenceModule>,- inner_chunk_item: ResolvedVc<Box<dyn EcmascriptChunkItem>>,+ module_graph: ResolvedVc<ModuleGraph>, chunking_context: ResolvedVc<Box<dyn ChunkingContext>>, } @@ -345,11 +342,12 @@ impl EcmascriptChunkItem for EcmascriptClientReferenceProxyChunkItem { async_module_info: Option<Vc<AsyncModuleInfo>>, estimated: bool, ) -> Result<Vc<EcmascriptChunkItemContent>> {- self.inner_chunk_item- .into_trait_ref()- .await?- .content_with_async_module_info(async_module_info, estimated)- .await+ Ok(self.inner_module.chunk_item_content(+ *self.chunking_context,+ *self.module_graph,+ async_module_info,+ estimated,+ )) } } test/production/app-dir/proxy-pruned-client-reference/app/layout.tsxadded9 + / 0 −
@@ -0,0 +1,9 @@+import type { ReactNode } from 'react'++export default function Root({ children }: { children: ReactNode }) {+ return (+ <html>+ <body>{children}</body>+ </html>+ )+}test/production/app-dir/proxy-pruned-client-reference/app/not-found.tsxadded4 + / 0 −
@@ -0,0 +1,4 @@+export default async function NotFound() {+ const { bar } = await import('../packages/fixture/src/index.js')+ return <main>{bar} not found</main>+}test/production/app-dir/proxy-pruned-client-reference/app/page.tsxadded4 + / 0 −
@@ -0,0 +1,4 @@+export default async function Page() {+ const { bar } = await import('../packages/fixture/src/index.js')+ return <main>{bar}</main>+}test/production/app-dir/proxy-pruned-client-reference/packages/fixture/package.jsonadded5 + / 0 −
@@ -0,0 +1,5 @@+{+ "name": "fixture",+ "type": "module",+ "sideEffects": false+}test/production/app-dir/proxy-pruned-client-reference/packages/fixture/src/foo.jsadded5 + / 0 −
@@ -0,0 +1,5 @@+'use client'++export function foo() {+ return null+}test/production/app-dir/proxy-pruned-client-reference/packages/fixture/src/index.jsadded2 + / 0 −
@@ -0,0 +1,2 @@+export * from './foo.js'+export const bar = 'bar'test/production/app-dir/proxy-pruned-client-reference/proxy-pruned-client-reference.test.tsadded15 + / 0 −
@@ -0,0 +1,15 @@+import { nextTestSetup } from 'e2e-utils'++describe('pruned client-reference proxy', () => {+ const { next } = nextTestSetup({ files: __dirname })++ // The barrel also exports a client function. Expanding that export creates an RSC proxy, but+ // neither route uses it, so its locals module has no module ID in the chunk graph.+ it('builds when a dynamic barrel import only uses a server export', async () => {+ const page = await next.render$('/')+ expect(page('main').text()).toBe('bar')++ const notFound = await next.render$('/missing')+ expect(notFound('main').text()).toBe('bar not found')+ })+})turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs4 + / 11 −
@@ -83,17 +83,10 @@ impl BindingUsageInfo { ) -> Result<Vc<ModuleExportUsage>> { let is_circuit_breaker = self.export_circuit_breakers.contains_key(&module).await?; let Some(exports) = self.used_exports.get(&module).await? else {- // There are some module that are codegened, but not referenced in the module graph,- let ident = module.ident_string().await?;- if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {- // Both the turbopack-wasm `ModuleChunkItem` and `EcmascriptClientReferenceModule`- // do `self.slightly_different_module().as_chunk_item()`, so the- // module that codegen sees isn't actually in the module graph.- // TODO fix these cases- return Ok(ModuleExportUsage::unknown());- }-- bail!("export usage not found for module: {ident:?}");+ bail!(+ "export usage not found for module: {:?}",+ module.ident_string().await?+ ); }; let namespace_object_may_escape = self.partial_namespace_modules.contains_key(&module).await?;turbopack/crates/turbopack-ecmascript/src/lib.rs30 + / 0 −
@@ -1067,6 +1067,36 @@ pub struct EcmascriptModuleContentOptions { async_module_info: Option<ResolvedVc<AsyncModuleInfo>>, } +/// Generate the code of a processed virtual module under the identity of the module in the+/// graph. The code source supplies its parsed program, references, exports and async behavior;+/// the graph module supplies the identity for export usage and other code generation.+#[turbo_tasks::function]+pub async fn chunk_item_content_with_code_from(+ module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,+ code_source: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,+ chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,+ async_module_info: Option<Vc<AsyncModuleInfo>>,+) -> Result<Vc<EcmascriptChunkItemContent>> {+ let Some(analyzable) = ResolvedVc::try_sidecast::<Box<dyn EcmascriptAnalyzable>>(code_source)+ else {+ bail!("virtual ECMAScript code source must be analyzable");+ };+ let source_options = analyzable+ .module_content_options(*chunking_context, async_module_info)+ .await?;+ let mut options = (*source_options).clone();+ options.module = module;+ let content = EcmascriptModuleContent::new(options.cell());+ let async_module_options = code_source+ .get_async_module()+ .module_options(async_module_info);+ Ok(EcmascriptChunkItemContent::new(+ content,+ *chunking_context,+ async_module_options,+ ))+}+ impl EcmascriptModuleContentOptions { async fn merged_code_gens( &self,turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source-no-hoisting/input/barrel.jsadded2 + / 0 −
@@ -0,0 +1,2 @@+export { value } from './used'+export { unused } from './unused'turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source-no-hoisting/input/index.jsadded4 + / 0 −
@@ -0,0 +1,4 @@+it('registers the retained source but does not look up the pruned one', async () => {+ const { value } = await import('./barrel')+ expect(value).toBe('value')+})turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source-no-hoisting/input/unused.jsadded1 + / 0 −
@@ -0,0 +1 @@+export const unused = 'unused'turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source-no-hoisting/input/used.jsadded1 + / 0 −
@@ -0,0 +1 @@+export const value = 'value'turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source-no-hoisting/options.jsonadded5 + / 0 −
@@ -0,0 +1,5 @@+{+ "followReexports": true,+ "scopeHoisting": false,+ "globalModuleIds": true+}turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source/input/barrel.jsadded2 + / 0 −
@@ -0,0 +1,2 @@+export { value } from './used'+export { unused } from './unused'turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source/input/index.jsadded4 + / 0 −
@@ -0,0 +1,4 @@+it('registers a partially used re-export facade whose other source was pruned', async () => {+ const { value } = await import('./barrel')+ expect(value).toBe('value')+})turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source/input/unused.jsadded1 + / 0 −
@@ -0,0 +1 @@+export const unused = 'unused'turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source/input/used.jsadded1 + / 0 −
@@ -0,0 +1 @@+export const value = 'value'turbopack/crates/turbopack-tests/tests/execution/turbopack/reexport-registration/pruned-source/options.jsonadded5 + / 0 −
@@ -0,0 +1,5 @@+{+ "followReexports": true,+ "scopeHoisting": true,+ "globalModuleIds": true+}turbopack/crates/turbopack-wasm/src/module_asset.rs11 + / 9 −
@@ -21,6 +21,7 @@ use turbopack_ecmascript::{ EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports, ecmascript_chunk_item, },+ chunk_item_content_with_code_from, references::async_module::OptionAsyncModule, }; @@ -224,11 +225,11 @@ impl EcmascriptChunkPlaceable for WebAssemblyModuleAsset { #[turbo_tasks::function] async fn chunk_item_content(- self: Vc<Self>,- chunking_context: Vc<Box<dyn ChunkingContext>>,- module_graph: Vc<ModuleGraph>,+ self: ResolvedVc<Self>,+ chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,+ _module_graph: Vc<ModuleGraph>, async_module_info: Option<Vc<AsyncModuleInfo>>,- estimated: bool,+ _estimated: bool, ) -> Result<Vc<EcmascriptChunkItemContent>> { if matches!( *chunking_context.chunk_loading().await?,@@ -240,12 +241,13 @@ impl EcmascriptChunkPlaceable for WebAssemblyModuleAsset { ); } - // Delegate to the loader's chunk item content- Ok(self.loader().chunk_item_content(- chunking_context,- module_graph,+ // Generate the loader's code under this module's graph identity. The loader is a+ // separately processed virtual module and has no export-usage entry of its own.+ Ok(chunk_item_content_with_code_from(+ *ResolvedVc::upcast(self),+ self.loader(),+ *chunking_context, async_module_info,- estimated, )) }