vercel/next.js · #100444
Turbopack: propagate late partial namespace provenance to passthrough targets
turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs277 + / 6 −
@@ -284,12 +284,19 @@ pub async fn compute_binding_usage_info( namespace_changed || usage_changed } export_usage => {- if matches!(export_usage, ExportUsage::PartialNamespaceObject(_)) {- // `target` is read through a namespace value. We know which names are- // used, but not that every read was lowered to a direct named access.- partial_namespace_modules.insert(target);- }- used_exports.entry(target).or_default().add(export_usage)+ // A partial namespace edge means `target` is read through a namespace+ // value. We know which names are used, but not that every read was+ // lowered to a direct named access.+ //+ // The provenance can be new even when the used names are not (the target+ // already uses `All` or these names), and it still has to be forwarded+ // over the target's passthrough edges, so it counts as a change.+ let namespace_changed =+ matches!(export_usage, ExportUsage::PartialNamespaceObject(_))+ && partial_namespace_modules.insert(target);+ let usage_changed =+ used_exports.entry(target).or_default().add(export_usage);+ namespace_changed || usage_changed } }; if changed || is_first_visit {@@ -513,3 +520,267 @@ mod tests { assert!(matches!(usage, ModuleExportUsageInfo::All)); } }++#[cfg(test)]+mod graph_tests {+ use anyhow::Result;+ use rustc_hash::FxHashMap;+ use turbo_rcstr::{RcStr, rcstr};+ use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};+ use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};+ use turbo_tasks_fs::{FileSystem, FileSystemPath, VirtualFileSystem};++ use super::compute_binding_usage_info;+ use crate::{+ asset::{Asset, AssetContent},+ ident::AssetIdent,+ module::{Module, ModuleSideEffects},+ module_graph::{+ GraphEntries, ModuleGraph, SingleModuleGraph,+ chunk_group_info::{ChunkGroupEntry, EntryHeuristics},+ },+ reference::{ModuleReference, ModuleReferences, SingleChunkableModuleReference},+ resolve::ExportUsage,+ };++ /// Module name -> outgoing edges `(target name, export usage of the edge)`, in reference order.+ type Edges = Vec<(RcStr, Vec<(RcStr, ExportUsage)>)>;++ #[turbo_tasks::value(shared)]+ struct UsageTestRepo {+ edges: FxHashMap<RcStr, Vec<(RcStr, ExportUsage)>>,+ }++ #[turbo_tasks::value]+ struct UsageTestModule {+ root: FileSystemPath,+ name: RcStr,+ repo: ResolvedVc<UsageTestRepo>,+ }++ #[turbo_tasks::value_impl]+ impl UsageTestModule {+ /// Interned through a task so that every reference to the same name yields the same module.+ #[turbo_tasks::function]+ fn new(root: FileSystemPath, name: RcStr, repo: ResolvedVc<UsageTestRepo>) -> Vc<Self> {+ Self { root, name, repo }.cell()+ }+ }++ #[turbo_tasks::value_impl]+ impl Asset for UsageTestModule {+ #[turbo_tasks::function]+ fn content(&self) -> Vc<AssetContent> {+ panic!("UsageTestModule::content shouldn't be called")+ }+ }++ #[turbo_tasks::value_impl]+ impl Module for UsageTestModule {+ #[turbo_tasks::function]+ fn ident(&self) -> Result<Vc<AssetIdent>> {+ Ok(AssetIdent::from_path(self.root.join(&self.name)?).into_vc())+ }++ #[turbo_tasks::function]+ fn source(&self) -> Vc<crate::source::OptionSource> {+ Vc::cell(None)+ }++ #[turbo_tasks::function]+ async fn references(&self) -> Result<Vc<ModuleReferences>> {+ let repo = self.repo.await?;+ let references = repo+ .edges+ .get(&self.name)+ .into_iter()+ .flatten()+ .map(async |(target, export)| {+ let target =+ UsageTestModule::new(self.root.clone(), target.clone(), *self.repo);+ Ok(ResolvedVc::upcast::<Box<dyn ModuleReference>>(+ SingleChunkableModuleReference::new(+ Vc::upcast(target),+ rcstr!("test-dep"),+ export_usage_vc(export),+ )+ .to_resolved()+ .await?,+ ))+ })+ .try_join()+ .await?;+ Ok(Vc::cell(references))+ }++ #[turbo_tasks::function]+ fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {+ ModuleSideEffects::SideEffectful.cell()+ }+ }++ /// `ExportUsage` cells are private to `resolve`, so go through its public constructors.+ fn export_usage_vc(export: &ExportUsage) -> Vc<ExportUsage> {+ match export {+ ExportUsage::All => ExportUsage::all(),+ ExportUsage::Named(name) => ExportUsage::named(name.clone()),+ ExportUsage::PartialNamespaceObject(names) => {+ ExportUsage::partial_namespace_object(names.to_vec())+ }+ ExportUsage::Evaluation => ExportUsage::evaluation(),+ ExportUsage::Passthrough {+ namespace_object_may_escape,+ } => ExportUsage::passthrough(*namespace_object_may_escape),+ }+ }++ /// `(module name, namespace_object_may_escape)` for every module in the graph, sorted by name.+ #[turbo_tasks::value(transparent)]+ struct NamespaceEscapes(Vec<(RcStr, bool)>);++ #[turbo_tasks::function(operation, root)]+ async fn namespace_escapes_operation(+ namespace_path_is_longer: bool,+ plain_usage_is_all: bool,+ ) -> Result<Vc<NamespaceEscapes>> {+ let plain_usage = if plain_usage_is_all {+ ExportUsage::All+ } else {+ ExportUsage::Named(rcstr!("a"))+ };+ let edges = namespace_graph(namespace_path_is_longer, plain_usage);+ let root = VirtualFileSystem::new_with_name(rcstr!("test"))+ .root()+ .owned()+ .await?;+ let names = edges+ .iter()+ .flat_map(|(from, to)| {+ std::iter::once(from.clone()).chain(to.iter().map(|(t, _)| t.clone()))+ })+ .collect::<std::collections::BTreeSet<_>>();+ let repo = UsageTestRepo {+ edges: edges.into_iter().collect(),+ }+ .resolved_cell();+ let module = async |name: &RcStr| -> Result<ResolvedVc<Box<dyn Module>>> {+ Ok(ResolvedVc::upcast(+ UsageTestModule::new(root.clone(), name.clone(), *repo)+ .to_resolved()+ .await?,+ ))+ };++ let single_graph = SingleModuleGraph::new_with_entries(+ GraphEntries::resolved_cell(GraphEntries::new(+ vec![ChunkGroupEntry::Entry {+ modules: vec![module(&rcstr!("entry")).await?],+ heuristics: EntryHeuristics::default(),+ }],+ vec![],+ )),+ false,+ true,+ );+ let graph = ModuleGraph::from_graphs(vec![single_graph], None);+ let binding_usage = compute_binding_usage_info(graph, false).connect().await?;++ let escapes = names+ .iter()+ .map(async |name| {+ let usage = binding_usage+ .used_exports(module(name).await?)+ .await?+ .await?;+ Ok((name.clone(), usage.namespace_object_may_escape))+ })+ .try_join()+ .await?;+ Ok(Vc::cell(escapes))+ }++ async fn namespace_escapes(+ namespace_path_is_longer: bool,+ plain_usage_is_all: bool,+ ) -> Vec<(RcStr, bool)> {+ let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(+ BackendOptions::default(),+ noop_backing_storage(),+ ));+ tt.run_once(async move {+ let escapes = namespace_escapes_operation(namespace_path_is_longer, plain_usage_is_all)+ .read_strongly_consistent()+ .await?;+ Ok((*escapes).clone())+ })+ .await+ .unwrap()+ }++ fn namespace_a() -> ExportUsage {+ ExportUsage::PartialNamespaceObject([rcstr!("a")].into_iter().collect())+ }++ fn reexport_all() -> ExportUsage {+ // `export * from "y"`+ ExportUsage::Passthrough {+ namespace_object_may_escape: false,+ }+ }++ /// `x` is reached directly from `entry` and through `entry -> m1 -> m2 -> x`; `x -> y` is an+ /// `export *` passthrough edge.+ ///+ /// When `namespace_path_is_longer` is true, the plain edge reaches `x` first and its+ /// passthrough is processed before the namespace edge. In that order, the namespace usage does+ /// not add any export to `x`. Otherwise namespace provenance reaches `x` first.+ fn namespace_graph(namespace_path_is_longer: bool, plain_usage: ExportUsage) -> Edges {+ let (direct_usage, late_usage) = if namespace_path_is_longer {+ (plain_usage, namespace_a())+ } else {+ (namespace_a(), plain_usage)+ };+ vec![+ (+ rcstr!("entry"),+ vec![+ (rcstr!("x"), direct_usage),+ (rcstr!("m1"), ExportUsage::All),+ ],+ ),+ (rcstr!("m1"), vec![(rcstr!("m2"), ExportUsage::All)]),+ (rcstr!("m2"), vec![(rcstr!("x"), late_usage)]),+ (rcstr!("x"), vec![(rcstr!("y"), reexport_all())]),+ ]+ }++ fn expected_escapes() -> Vec<(RcStr, bool)> {+ vec![+ (rcstr!("entry"), false),+ (rcstr!("m1"), false),+ (rcstr!("m2"), false),+ (rcstr!("x"), true),+ (rcstr!("y"), true),+ ]+ }++ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]+ async fn late_namespace_usage_of_all_module_reaches_passthrough_target() {+ assert_eq!(namespace_escapes(true, true).await, expected_escapes());+ }++ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]+ async fn late_namespace_usage_of_already_used_name_reaches_passthrough_target() {+ assert_eq!(namespace_escapes(true, false).await, expected_escapes());+ }++ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]+ async fn early_namespace_usage_of_all_module_reaches_passthrough_target() {+ assert_eq!(namespace_escapes(false, true).await, expected_escapes());+ }++ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]+ async fn early_namespace_usage_of_already_used_name_reaches_passthrough_target() {+ assert_eq!(namespace_escapes(false, false).await, expected_escapes());+ }+}turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/input/barrel.jsadded2 + / 0 −
@@ -0,0 +1,2 @@+// Forwards the importer's export usage (and namespace provenance) to `source.js`.+export * from './source'turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/input/hop1.jsadded7 + / 0 −
@@ -0,0 +1,7 @@+import { readLate as readFromHop2 } from './hop2'++// Real code (not a re-export) so this hop stays in the graph and the namespace read in+// `provider.js` is reached later than the direct named import in `index.js`.+export function readLate() {+ return readFromHop2()+}turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/input/hop2.jsadded5 + / 0 −
@@ -0,0 +1,5 @@+import { readLate as readFromProvider } from './provider'++export function readLate() {+ return readFromProvider()+}turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/input/index.jsadded29 + / 0 −
@@ -0,0 +1,29 @@+// `barrel.js` is reached twice:+//+// 1. directly from here with named imports (short path), so its `export *` passthrough edge to+// `source.js` is processed first, without any namespace provenance, and+// 2. through `hop1 -> hop2 -> provider` (long path), where `provider.js` reads the barrel through a+// partial namespace object. That read uses no names the barrel didn't already use.+//+// The late namespace read must still reach `source.js`, so its exports keep their original names.+//+// `options.json` disables `followReexports`, so the named imports below target the barrel instead+// of being resolved straight to `source.js`, and `mangleViaMaterializedNamespaceObject`, so the+// dynamic import reads the barrel itself rather than a materialized namespace facade.++import { veryLongLateNamespaceExportName, sourceExportsInfo } from './barrel'+import { readLate } from './hop1'++it('should read the value through the direct import and the late namespace read', async () => {+ expect(veryLongLateNamespaceExportName).toBe('late-value')+ expect(await readLate()).toBe('late-value')+})++it('should keep original names of the export-star source', () => {+ expect(sourceExportsInfo.veryLongLateNamespaceExportName.canMangle).toBe(+ false+ )+ expect(sourceExportsInfo.veryLongLateNamespaceExportName.mangledName).toBe(+ null+ )+})turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/input/provider.jsadded8 + / 0 −
@@ -0,0 +1,8 @@+// An enumerated dynamic import is a partial namespace read of the barrel: the used names are known,+// but the promise resolves to the namespace object, which exposes the original names.+export async function readLate() {+ const { veryLongLateNamespaceExportName } = await import(+ /* webpackExports: ["veryLongLateNamespaceExportName"] */ './barrel'+ )+ return veryLongLateNamespaceExportName+}turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/input/source.jsadded6 + / 0 −
@@ -0,0 +1,6 @@+export const veryLongLateNamespaceExportName = 'late-value'++export const sourceExportsInfo = {+ veryLongLateNamespaceExportName:+ __webpack_exports_info__.veryLongLateNamespaceExportName,+}turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-late-namespace-passthrough/options.jsonadded7 + / 0 −
@@ -0,0 +1,7 @@+{+ "followReexports": false,+ "mangleExportNames": true,+ "mangleViaMaterializedNamespaceObject": false,+ "minify": true,+ "scopeHoisting": false+}