vercel/next.js · #99115

Use graph-backed codegen for client proxies and WASM loaders

sokra · merged Sep 24, 202621 files · 142 + / 49 −
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,         ))     }