From 7c87468aa3dc0157105ae0e17197134df833f983 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 15 Sep 2026 21:49:13 +0200 Subject: [PATCH 1/8] [turbopack] Use options object for ESM references (#98680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Introduce `EsmAssetReferenceOptions`, an explicit options object for ESM asset references. ### Why? `EsmAssetReference::new` and `new_pure` accepted a long positional list of behavioral settings. Call sites were difficult to scan and easy to misconfigure as the reference grows new behavior. This is intentionally a behavior-neutral, independently reviewable base for the two existing tree-shaking PRs above it: 1. This PR centralizes constructor configuration. 2. #96282 uses the options object to configure re-export usage forwarding. 3. #96396 tracks static member reads through namespace-valued re-exports. ### How? Keep the three reference-identity inputs (`module`, `origin`, and `request`) positional, and move behavioral settings—source location, annotations, target part, import usage, externals, module-fragment mode, and resolve override—into a named options struct. The two constructor call sites now declare their settings by field name. The refactor removes the `too_many_arguments` allowances without changing resolution or binding-usage behavior. ### Testing - `cargo check -p turbopack-ecmascript` - Analyzer graph tests: 60/60 - The stacked focused execution/snapshot tests, 2/2 usage-lattice units, `build-all`, and HMR regression are verified on the upper PRs. Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> --- .../src/references/esm/base.rs | 75 +++++++------------ .../src/references/esm/mod.rs | 2 +- .../src/references/exports.rs | 72 ++++++++++-------- .../src/references/mod.rs | 34 +++++---- 4 files changed, 87 insertions(+), 96 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index cb528c32f99c..dc981bdbe072 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -443,6 +443,20 @@ pub struct EsmAssetReference { extras: Option>, } +/// Construction options for an [`EsmAssetReference`]. +/// +/// The module, origin, and request stay as constructor arguments because they identify the +/// reference. The remaining behavior is named here so call sites don't depend on argument order. +pub struct EsmAssetReferenceOptions { + pub issue_source: IssueSource, + pub annotations: Option, + pub export_name: Option, + pub import_usage: ImportUsage, + pub import_externals: bool, + pub module_fragments_enabled: bool, + pub resolve_override: Option>>, +} + /// Optional extra state for an [`EsmAssetReference`] that is rarely present: the few values /// extracted from `ImportAnnotations` (the full `ImportAnnotations` — a `BTreeMap` plus several /// `Option`s — is not retained) plus a `resolve_override` from matched inner assets. @@ -499,20 +513,23 @@ impl EsmReferenceExtras { } impl EsmAssetReference { - #[allow(clippy::too_many_arguments)] async fn new_inner( module: ResolvedVc, origin: ResolvedVc>, request: RcStr, - issue_source: IssueSource, - annotations: Option, - export_name: Option, - import_usage: ImportUsage, - import_externals: bool, - module_fragments_enabled: bool, - resolve_override: Option>>, + options: EsmAssetReferenceOptions, is_pure_import: bool, ) -> Result { + let EsmAssetReferenceOptions { + issue_source, + annotations, + export_name, + import_usage, + import_externals, + module_fragments_enabled, + resolve_override, + } = options; + // Apply any annotation-driven transition eagerly so the stored origin is final and the // `annotations` don't need to be retained on the reference. let origin = if let Some(transition) = annotations.as_ref().and_then(|a| a.transition()) { @@ -538,60 +555,26 @@ impl EsmAssetReference { }) } - #[allow(clippy::too_many_arguments)] pub async fn new( module: ResolvedVc, origin: ResolvedVc>, request: RcStr, - issue_source: IssueSource, - annotations: Option, - export_name: Option, - import_usage: ImportUsage, - import_externals: bool, - module_fragments_enabled: bool, - resolve_override: Option>>, + options: EsmAssetReferenceOptions, ) -> Result { Self::new_inner( - module, - origin, - request, - issue_source, - annotations, - export_name, - import_usage, - import_externals, - module_fragments_enabled, - resolve_override, - /* is_pure_import */ false, + module, origin, request, options, /* is_pure_import */ false, ) .await } - #[allow(clippy::too_many_arguments)] pub async fn new_pure( module: ResolvedVc, origin: ResolvedVc>, request: RcStr, - issue_source: IssueSource, - annotations: Option, - export_name: Option, - import_usage: ImportUsage, - import_externals: bool, - module_fragments_enabled: bool, - resolve_override: Option>>, + options: EsmAssetReferenceOptions, ) -> Result { Self::new_inner( - module, - origin, - request, - issue_source, - annotations, - export_name, - import_usage, - import_externals, - module_fragments_enabled, - resolve_override, - /* is_pure_import */ true, + module, origin, request, options, /* is_pure_import */ true, ) .await } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs index 96fe9eb88a7d..c118ca5dbc35 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod module_item; pub(crate) mod url; pub use self::{ - base::EsmAssetReference, + base::{EsmAssetReference, EsmAssetReferenceOptions}, binding::EsmBinding, dynamic::EsmAsyncAssetReference, export::{EsmExport, EsmExports, FoundExportType, Liveness}, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs index 3f114f8fe65a..db6f8f4d4163 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs @@ -20,7 +20,7 @@ use crate::{ parse::ParseResult, references::{ TURBOPACK_HELPER_WTF8, - esm::{EsmAssetReference, EsmExports}, + esm::{EsmAssetReference, EsmAssetReferenceOptions, EsmExports}, type_issue::SpecifiedModuleTypeIssue, }, runtime_functions::{TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE}, @@ -100,41 +100,47 @@ pub async fn compute_ecmascript_module_exports( module, ResolvedVc::upcast(module), RcStr::from(&*r.module_path.to_string_lossy()), - IssueSource::from_swc_offsets(source, r.span.lo.to_u32(), r.span.hi.to_u32()), - r.annotations.as_ref().map(|a| (**a).clone()), - match &r.imported_symbol { - &ImportedSymbol::ModuleEvaluation => { - should_add_evaluation = true; - Some(ModulePart::evaluation()) - } - ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())), - ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => { - if !options.module_fragments_enabled { - bail!( - "Internal imports only exist in reexports only mode when \ - importing {:?} from {}", - r.imported_symbol, - r.module_path.to_string_lossy() - ); - } - if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) { + EsmAssetReferenceOptions { + issue_source: IssueSource::from_swc_offsets( + source, + r.span.lo.to_u32(), + r.span.hi.to_u32(), + ), + annotations: r.annotations.as_ref().map(|a| (**a).clone()), + export_name: match &r.imported_symbol { + &ImportedSymbol::ModuleEvaluation => { should_add_evaluation = true; + Some(ModulePart::evaluation()) } - Some(ModulePart::internal(*part_id)) - } - ImportedSymbol::Exports => { - options.module_fragments_enabled.then(ModulePart::exports) - } + ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())), + ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => { + if !options.module_fragments_enabled { + bail!( + "Internal imports only exist in reexports only mode when \ + importing {:?} from {}", + r.imported_symbol, + r.module_path.to_string_lossy() + ); + } + if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) { + should_add_evaluation = true; + } + Some(ModulePart::internal(*part_id)) + } + ImportedSymbol::Exports => { + options.module_fragments_enabled.then(ModulePart::exports) + } + }, + import_usage: eval_context + .imports + .import_usage + .get(&i) + .cloned() + .unwrap_or_default(), + import_externals, + module_fragments_enabled: options.module_fragments_enabled, + resolve_override, }, - eval_context - .imports - .import_usage - .get(&i) - .cloned() - .unwrap_or_default(), - import_externals, - options.module_fragments_enabled, - resolve_override, ) .await? .resolved_cell(); diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 7b9dfe8b0f3a..f47f161aabec 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -134,9 +134,9 @@ use crate::{ dynamic_expression::DynamicExpression, emit_collect::{CollectReference, EmitReference}, esm::{ - EsmAssetReference, EsmAsyncAssetReference, EsmBinding, ImportMetaBinding, - ImportMetaRef, UrlAssetReference, UrlRewriteBehavior, base::EsmAssetReferences, - module_id::EsmModuleIdAssetReference, + EsmAssetReference, EsmAssetReferenceOptions, EsmAsyncAssetReference, EsmBinding, + ImportMetaBinding, ImportMetaRef, UrlAssetReference, UrlRewriteBehavior, + base::EsmAssetReferences, module_id::EsmModuleIdAssetReference, }, exports::{EcmascriptExportsAnalysis, compute_ecmascript_module_exports}, exports_info::{ExportsInfoBinding, ExportsInfoRef}, @@ -3739,19 +3739,21 @@ async fn handle_free_var_reference( state.origin }, request.clone(), - IssueSource::from_swc_offsets( - state.source, - span.lo.to_u32(), - span.hi.to_u32(), - ), - Default::default(), - export.clone().map(ModulePart::export), - // TODO This could be optimized. E.g. referencing `Buffer` in some top - // level function could set ImportUsage properly here - ImportUsage::TopLevel, - state.import_externals, - state.module_fragments_enabled, - None, + EsmAssetReferenceOptions { + issue_source: IssueSource::from_swc_offsets( + state.source, + span.lo.to_u32(), + span.hi.to_u32(), + ), + annotations: Default::default(), + export_name: export.clone().map(ModulePart::export), + // TODO This could be optimized. E.g. referencing `Buffer` in some top + // level function could set ImportUsage properly here + import_usage: ImportUsage::TopLevel, + import_externals: state.import_externals, + module_fragments_enabled: state.module_fragments_enabled, + resolve_override: None, + }, ) .await? .resolved_cell()) From 4bb6fc4b90e96c4d114ecaaf69bc1a56bd7261bb Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 15 Sep 2026 14:49:14 -0500 Subject: [PATCH 2/8] [turbopack] Forward usage information across re-exports (#96282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What? This PR improves tree-shaking across whole-namespace re-exports. It is the middle PR in the stack: #98680 provides the independently reviewable `EsmAssetReferenceOptions` constructor refactor requested during review, and #96396 builds on this PR to narrow static member reads on namespace-valued named exports. A representative case is a CommonJS re-export followed by an ESM star barrel: ```js // index.js const { m } = require('./cjs') // cjs.js module.exports = require('./barrel') // CJS re-export // barrel.js export * from './base' // ESM star barrel // base.js export const m = 1 export const n = 2 // nobody ever uses this ``` Only `m` is observed by the entrypoint, so that usage can now flow through both re-export layers to `base.js` and allow `n` to be removed. ## Why? Turbopack now has a generic `ExportUsage::Passthrough` mechanism, introduced on `canary` for client-component and Next.js module proxies. It carries the referencing module's accumulated used-export set into a transparent target. However, CommonJS whole-module forwarding and ordinary ESM star barrels do not opt into that mechanism. Applying this PR's tests to plain `canary` showed that all runtime values are already correct, but unused exports remain retained in four important shapes: - an ESM named consumer through `module.exports = require(ESM)`; - a deep CommonJS forwarding chain; - CommonJS forwarding into an ESM `export *` barrel; - a CommonJS diamond with disjoint used exports. This PR therefore remains a tree-shaking precision change rather than introducing a second forwarding model. There is also an independent name-observability concern. A CommonJS forwarder exposes a raw namespace object's properties under their original names, while named consumers of an ESM `export *` can be rewritten if an export is mangled. Namespace provenance must survive multiple passthrough hops without unnecessarily disabling mangling for ordinary ESM named forwarding. ## How? ### Configure forwarding at construction time #98680 replaces `EsmAssetReference`'s positional behavior arguments with `EsmAssetReferenceOptions`. This PR extends that options object with an optional passthrough mode. ESM star references receive `Some(false)` directly when constructed; free-variable references receive `None`; and the existing annotation-driven client-proxy path still contributes `Some(true)`. The two sources merge conservatively: an annotation that exposes the namespace's original names cannot be weakened by a syntax-driven `Some(false)` on the same reference. The former mutating `mark_namespace_reexport()` API is removed. ### Reuse the generic passthrough fixed point The implementation uses canary's existing model: ```rust ExportUsage::Passthrough { namespace_object_may_escape: bool, } ``` The boolean describes whether the edge itself exposes the target namespace's original property names: - `module.exports = require('…')` records `Passthrough { namespace_object_may_escape: true }` directly in the existing CommonJS import-usage map; - `export * from '…'` records `Passthrough { namespace_object_may_escape: false }`, because statically known ESM consumers can be rewritten when target exports are mangled. No parallel `TargetExportUsage` type, graph field, call parameter, or second fixed-point implementation is added. Detection is limited to a simple assignment using the unresolved/global `module` and `require` bindings. Shadowed bindings, member requires, and compound assignments retain their existing behavior. ### Propagate keys and namespace provenance independently A passthrough edge merges the parent module's resolved `Evaluation`, `Exports`, or `All` state into its target. The merge remains monotonic and idempotent, so chains, cycles, and diamonds converge. Namespace provenance is carried independently of the used key set: - an edge with `namespace_object_may_escape: true` makes the target's original names observable; - every passthrough also carries namespace provenance that already reached its parent; - a newly propagated provenance bit retriggers traversal even when the used key set did not change. This preserves original names through multi-hop namespace reads while keeping unrelated ordinary named exports mangleable. Passthrough references are also exposed correctly to scope-hoisted merged modules. ## Testing Enabled execution coverage includes: - deep CommonJS chains, CommonJS → ESM star barrels, cycles, diamonds, and overwrite behavior; - ESM named/default/namespace import interop through `module.exports = require(ESM)`; - opaque whole-namespace and evaluation-only consumption, including single evaluation; - native `export *`, default/local-shadow filtering, and the optimization-disabled mode; - namespace provenance across forwarding edges and a multi-hop diamond; - both mangling safety and non-pessimization for ordinary named forwarding; - shadowed `module`/`require`, member-require, and non-simple assignment boundaries; - runtime counterparts for every nested precision case that remains skipped. Known optimization-only limitations remain under `__skipped__/deep-reexports`: nested `export * as` through CJS, namespace-property unwrapping in a CJS assignment, depth-two nested namespaces, and unwrapping after a forwarding hop. Their runtime semantics are covered by the enabled `reexport-forwarding-nested-runtime` fixture. Verification on the rebased stack: - plain-canary reassessment: 7/9 fixtures, with only four expected precision failures; - integrated bottom layer: 9/9 forwarding fixtures; - passthrough unit tests: 2/2, plus the six-case construction-option merge lattice; - combined stack: 14/14 focused execution/snapshot tests and 60/60 analyzer graph tests; - affected Cargo checks, `next-core` check, `build-all`, and the forwarding/namespace HMR test pass. Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> --- .../src/module_graph/binding_usage_info.rs | 38 +++++++--- .../src/module_graph/merged_modules.rs | 4 +- .../crates/turbopack-core/src/resolve/mod.rs | 9 ++- .../src/analyzer/imports.rs | 18 ++++- .../src/references/esm/base.rs | 76 +++++++++++++++---- .../src/references/exports.rs | 4 + .../src/references/mod.rs | 1 + .../input/forwarded-barrel.js | 1 + .../input/forwarded-provider.js | 6 ++ .../input/forwarded-source.js | 1 + .../mangle-escaping-namespace/input/index.js | 26 +++++++ .../input/mangleable-forwarded-barrel.js | 1 + .../input/mangleable-forwarded-source.js | 6 ++ .../input/multi-hop-1.js | 1 + .../input/multi-hop-2.js | 1 + .../input/multi-hop-provider.js | 9 +++ .../input/multi-hop-source.js | 6 ++ .../star-reexport-filtering/input/barrel.js | 3 + .../star-reexport-filtering/input/index.js | 8 ++ .../star-reexport-filtering/input/source.js | 5 ++ .../star-reexport-forwarding/input/barrel.js | 1 + .../star-reexport-forwarding/input/base.js} | 0 .../star-reexport-forwarding/input/index.js | 9 +++ .../__skipped__/deep-reexports/input/index.js | 45 +---------- .../input/forwarder.js | 1 + .../input/index.js | 23 ++++++ .../input/side-effect-forwarder.js | 1 + .../input/side-effect-source.js | 4 + .../input/source.js | 6 ++ .../reexport-forwarding-disabled/options.json | 3 + .../input/forwarder.js | 1 + .../input/index.js | 7 ++ .../input/source.js | 5 ++ .../options.json | 3 + .../input/b-leaf.js | 4 + .../input/b-mid.js | 1 + .../input/b-ns.js | 1 + .../input/c-leaf.js | 4 + .../input/c-mid.js | 1 + .../input/c-ns.js | 1 + .../input/f-cjs.js | 1 + .../input/f-inner.js | 1 + .../input/f-leaf.js | 4 + .../input/f-outer.js | 1 + .../input/index.js | 15 ++++ .../input/j-hop1.js | 1 + .../input/j-hop2.js | 1 + .../input/j-leaf.js | 4 + .../input/j-ns.js | 1 + .../options.json | 3 + .../input/forwarder.js | 1 + .../input/index.js | 23 ++++++ .../input/side-effect-forwarder.js | 1 + .../input/side-effect-source.js | 4 + .../input/source.js | 6 ++ .../reexport-forwarding-runtime/options.json | 3 + .../input/compound.js | 2 + .../reexport-forwarding-syntax/input/index.js | 11 +++ .../input/member.js | 1 + .../input/shadowed.js | 6 ++ .../reexport-forwarding-syntax/input/value.js | 1 + .../reexport-forwarding-syntax/options.json | 3 + .../input/a-l2.js | 0 .../input/a-l3.js | 0 .../input/a-l4.js | 0 .../input/a-src.js | 0 .../input/d-barrel.js | 0 .../reexport-forwarding/input/d-base.js | 4 + .../input/d-cjs.js | 0 .../input/e-one.js | 0 .../input/e-two.js | 0 .../input/g-leaf.js | 0 .../input/g-p1.js | 0 .../input/g-p2.js | 0 .../input/h-a.js | 0 .../input/h-b.js | 0 .../reexport-forwarding/input/index.js | 42 ++++++++++ .../reexport-forwarding/options.json | 3 + 78 files changed, 415 insertions(+), 73 deletions(-) create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js rename turbopack/crates/turbopack-tests/tests/execution/{webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-base.js => turbopack/tree-shaking/star-reexport-forwarding/input/base.js} (100%) create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/a-l2.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/a-l3.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/a-l4.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/a-src.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/d-barrel.js (100%) create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/d-cjs.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/e-one.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/e-two.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/g-leaf.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/g-p1.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/g-p2.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/h-a.js (100%) rename turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/{__skipped__/deep-reexports => reexport-forwarding}/input/h-b.js (100%) create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json diff --git a/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs b/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs index b3e777954d5b..c1015926345a 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs @@ -270,17 +270,25 @@ pub async fn compute_binding_usage_info( ExportUsage::Passthrough { namespace_object_may_escape, } => { - if *namespace_object_may_escape { - partial_namespace_modules.insert(target); - } + // Passthrough edges always carry namespace provenance that already reached + // their parent. Some edges (for example a forwarded CommonJS namespace) + // additionally expose the target's original property names themselves. + let namespace_changed = if *namespace_object_may_escape + || partial_namespace_modules.contains(&parent) + { + partial_namespace_modules.insert(target) + } else { + false + }; let passthrough_usage = used_exports .get(&parent) .context("parent module must have usage info")? .clone(); - used_exports + let usage_changed = used_exports .entry(target) .or_default() - .add_usage_info(&passthrough_usage) + .add_usage_info(&passthrough_usage); + namespace_changed || usage_changed } export_usage => { if matches!(export_usage, ExportUsage::PartialNamespaceObject(_)) { @@ -292,8 +300,8 @@ pub async fn compute_binding_usage_info( } }; if changed || is_first_visit { - // First visit, or the used exports changed. This can cause more imports to get - // used downstream. + // First visit, or the used exports/namespace provenance changed. Either can + // cause more imports to become used downstream. Ok(GraphTraversalAction::Continue) } else { Ok(GraphTraversalAction::Skip) @@ -485,13 +493,21 @@ mod tests { use crate::resolve::ExportUsage; #[test] - fn resolved_usage_merges_exports() { - let mut usage = ModuleExportUsageInfo::Exports([rcstr!("first")].into_iter().collect()); - let additional = ModuleExportUsageInfo::Exports([rcstr!("second")].into_iter().collect()); + fn resolved_usage_join_is_monotonic() { + let mut usage = ModuleExportUsageInfo::Evaluation; + let first = ModuleExportUsageInfo::Exports([rcstr!("first")].into_iter().collect()); + let second = ModuleExportUsageInfo::Exports([rcstr!("second")].into_iter().collect()); - assert!(usage.add_usage_info(&additional)); + assert!(!usage.add_usage_info(&ModuleExportUsageInfo::Evaluation)); + assert!(usage.add_usage_info(&first)); assert!(usage.is_export_used(&rcstr!("first"))); + assert!(!usage.add_usage_info(&first)); + assert!(usage.add_usage_info(&second)); assert!(usage.is_export_used(&rcstr!("second"))); + assert!(!usage.add_usage_info(&ModuleExportUsageInfo::Evaluation)); + assert!(usage.add_usage_info(&ModuleExportUsageInfo::All)); + assert!(!usage.add_usage_info(&second)); + assert!(matches!(usage, ModuleExportUsageInfo::All)); } #[test] diff --git a/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs b/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs index 69e6008d6023..820340929a87 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/merged_modules.rs @@ -534,7 +534,9 @@ pub async fn compute_merged_modules(module_graph: Vc) -> Result { node.visit_children_with(self); } + fn visit_assign_expr(&mut self, node: &AssignExpr) { + if node.op == AssignOp::Assign + && let AssignTarget::Simple(SimpleAssignTarget::Member(target)) = &node.left + && is_module_dot_exports(target, self.unresolved_mark) + && let Some(call) = as_require_call(&node.right, self.unresolved_mark) + { + self.data.cjs_imports.resolved.insert( + call.span.lo, + ExportUsage::Passthrough { + namespace_object_may_escape: true, + }, + ); + } + node.visit_children_with(self); + } + fn visit_expr_stmt(&mut self, node: &ExprStmt) { // A bare `require("…")` statement discards its result → evaluation only. if let Some(call) = as_require_call(&node.expr, self.unresolved_mark) { diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index dc981bdbe072..f3a160464121 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -454,6 +454,9 @@ pub struct EsmAssetReferenceOptions { pub import_usage: ImportUsage, pub import_externals: bool, pub module_fragments_enabled: bool, + /// Explicit export-usage passthrough mode for syntax-driven forwarding. `Some(false)` forwards + /// used names without exposing the target namespace's original property names. + pub export_usage_passthrough: Option, pub resolve_override: Option>>, } @@ -484,17 +487,28 @@ struct EsmReferenceExtras { module_type: Option, /// The chunking-type annotation (drives `chunking_type`). chunking_type: Option, - /// Whether the importing module's used exports should be forwarded to the target. - export_usage_passthrough: bool, + /// Whether the importing module's used exports should be forwarded to the target. The boolean + /// records whether this edge itself exposes the target namespace's original property names. + export_usage_passthrough: Option, /// A module to resolve to directly, bypassing resolution (from a matched inner asset). resolve_override: Option>>, } +fn merge_export_usage_passthrough( + explicit: Option, + annotation_passthrough: bool, +) -> Option { + // An annotation is an explicit request to expose the namespace's original names, so it must + // not be weakened by a syntax-driven passthrough mode on the same reference. + annotation_passthrough.then_some(true).or(explicit) +} + impl EsmReferenceExtras { /// Builds the extras from import annotations and a resolve override, returning `None` (rather /// than an all-empty box) when nothing relevant is present — the common case. fn new( annotations: Option<&ImportAnnotations>, + export_usage_passthrough: Option, resolve_override: Option>>, ) -> Option> { let extras = EsmReferenceExtras { @@ -505,13 +519,40 @@ impl EsmReferenceExtras { .and_then(|a| a.module_type()) .map(|m| RcStr::from(&*m.to_string_lossy())), chunking_type: annotations.and_then(|a| a.chunking_type()), - export_usage_passthrough: annotations.is_some_and(|a| a.export_usage_passthrough()), + export_usage_passthrough: merge_export_usage_passthrough( + export_usage_passthrough, + annotations.is_some_and(|a| a.export_usage_passthrough()), + ), resolve_override, }; (extras != EsmReferenceExtras::default()).then(|| Box::new(extras)) } } +#[cfg(test)] +mod tests { + use super::merge_export_usage_passthrough; + + #[test] + fn annotation_passthrough_cannot_be_weakened() { + assert_eq!(merge_export_usage_passthrough(None, false), None); + assert_eq!( + merge_export_usage_passthrough(Some(false), false), + Some(false) + ); + assert_eq!( + merge_export_usage_passthrough(Some(true), false), + Some(true) + ); + assert_eq!(merge_export_usage_passthrough(None, true), Some(true)); + assert_eq!( + merge_export_usage_passthrough(Some(false), true), + Some(true) + ); + assert_eq!(merge_export_usage_passthrough(Some(true), true), Some(true)); + } +} + impl EsmAssetReference { async fn new_inner( module: ResolvedVc, @@ -527,6 +568,7 @@ impl EsmAssetReference { import_usage, import_externals, module_fragments_enabled, + export_usage_passthrough, resolve_override, } = options; @@ -551,7 +593,11 @@ impl EsmAssetReference { import_externals, module_fragments_enabled, is_pure_import, - extras: EsmReferenceExtras::new(annotations.as_ref(), resolve_override), + extras: EsmReferenceExtras::new( + annotations.as_ref(), + export_usage_passthrough, + resolve_override, + ), }) } @@ -722,22 +768,22 @@ impl ModuleReference for EsmAssetReference { } fn binding_usage(&self) -> BindingUsage { + let export_usage_passthrough = self + .extras + .as_deref() + .and_then(|extras| extras.export_usage_passthrough); BindingUsage { import: self.import_usage.clone(), - export: match &self.export_name { + export: match (&self.export_name, export_usage_passthrough) { // Evaluation references preserve their side-effect-only semantics even when the // corresponding import forwards export usage. - Some(ModulePart::Evaluation) => ExportUsage::Evaluation, - _ if self - .extras - .as_deref() - .is_some_and(|extras| extras.export_usage_passthrough) => - { - ExportUsage::Passthrough { - namespace_object_may_escape: true, - } + (Some(ModulePart::Evaluation), _) => ExportUsage::Evaluation, + (_, Some(namespace_object_may_escape)) => ExportUsage::Passthrough { + namespace_object_may_escape, + }, + (Some(ModulePart::Export(export_name)), _) => { + ExportUsage::Named(export_name.clone()) } - Some(ModulePart::Export(export_name)) => ExportUsage::Named(export_name.clone()), _ => ExportUsage::All, }, } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs index db6f8f4d4163..36b8761bcc06 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs @@ -1,4 +1,5 @@ use anyhow::{Result, bail}; +use rustc_hash::FxHashSet; use swc_core::{ common::source_map::SmallPos, ecma::ast::{Expr, Ident, ImportDecl, MemberProp, Program, Stmt}, @@ -80,6 +81,8 @@ pub async fn compute_ecmascript_module_exports( let mut esm_reexport_reference_idxs: Vec = vec![]; let mut esm_evaluation_reference_idxs: Vec = vec![]; + let namespace_reexports: FxHashSet = + eval_context.imports.reexport_namespaces().collect(); let span = tracing::trace_span!("esm import references"); let import_references = async { @@ -139,6 +142,7 @@ pub async fn compute_ecmascript_module_exports( .unwrap_or_default(), import_externals, module_fragments_enabled: options.module_fragments_enabled, + export_usage_passthrough: namespace_reexports.contains(&i).then_some(false), resolve_override, }, ) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index f47f161aabec..2ce234390ebd 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -3752,6 +3752,7 @@ async fn handle_free_var_reference( import_usage: ImportUsage::TopLevel, import_externals: state.import_externals, module_fragments_enabled: state.module_fragments_enabled, + export_usage_passthrough: None, resolve_override: None, }, ) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js new file mode 100644 index 000000000000..1a3ca11a653e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-barrel.js @@ -0,0 +1 @@ +export * from './forwarded-source' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js new file mode 100644 index 000000000000..eb310bb6014f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-provider.js @@ -0,0 +1,6 @@ +import * as forwarded from './forwarded-barrel' + +export function readForwardedNamespace() { + const { veryLongForwardedExportName } = forwarded + return veryLongForwardedExportName +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js new file mode 100644 index 000000000000..d40a6c78c50c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/forwarded-source.js @@ -0,0 +1 @@ +export const veryLongForwardedExportName = 'forwarded-value' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js index 63ca1d873642..5fe7b8447bff 100644 --- a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js @@ -1,4 +1,10 @@ import { getEnums } from './provider' +import { multiHopExportsInfo, readMultiHopDiamond } from './multi-hop-provider' +import { readForwardedNamespace } from './forwarded-provider' +import { + forwardedExportsInfo, + veryLongMangleableForwardedExportName, +} from './mangleable-forwarded-barrel' import { enumsNs } from './reexport' import { getCjs } from './cjs-provider' import { read } from './destr' @@ -47,6 +53,26 @@ it('should keep an escaped CommonJS namespace interop correct', () => { expect(ns.CJS_B).toBe('cjs-b') }) +it('should preserve namespace reads through an export-star forwarding edge', () => { + expect(readForwardedNamespace()).toBe('forwarded-value') +}) + +it('should preserve namespace names through a multi-hop diamond', () => { + expect(readMultiHopDiamond()).toEqual(['multi-hop', 'multi-hop']) + expect(multiHopExportsInfo.veryLongMultiHopExportName.canMangle).toBe(false) + expect(multiHopExportsInfo.veryLongMultiHopExportName.mangledName).toBeNull() +}) + +it('should still mangle ordinary named reads through an export-star forwarding edge', () => { + expect(veryLongMangleableForwardedExportName).toBe('mangleable-forwarded') + expect( + forwardedExportsInfo.veryLongMangleableForwardedExportName.canMangle + ).toBe(true) + expect( + forwardedExportsInfo.veryLongMangleableForwardedExportName.mangledName + ).not.toBe('veryLongMangleableForwardedExportName') +}) + it('should still mangle a sibling module that does not escape', () => { expect(someLongExportName).toBe('mangled-1') expect(anotherLongExportName).toBe('mangled-2') diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js new file mode 100644 index 000000000000..545ad5d0c607 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-barrel.js @@ -0,0 +1 @@ +export * from './mangleable-forwarded-source' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js new file mode 100644 index 000000000000..c48fe7f665de --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable-forwarded-source.js @@ -0,0 +1,6 @@ +export const veryLongMangleableForwardedExportName = 'mangleable-forwarded' + +export const forwardedExportsInfo = { + veryLongMangleableForwardedExportName: + __webpack_exports_info__.veryLongMangleableForwardedExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js new file mode 100644 index 000000000000..3521b680ed5e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-1.js @@ -0,0 +1 @@ +export * from './multi-hop-source' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js new file mode 100644 index 000000000000..02f98bc5ae56 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-2.js @@ -0,0 +1 @@ +export * from './multi-hop-1' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js new file mode 100644 index 000000000000..a313bc01aca2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-provider.js @@ -0,0 +1,9 @@ +import * as namespacePath from './multi-hop-2' +import { veryLongMultiHopExportName as namedPath } from './multi-hop-source' + +export { multiHopExportsInfo } from './multi-hop-source' + +export function readMultiHopDiamond() { + const { veryLongMultiHopExportName } = namespacePath + return [veryLongMultiHopExportName, namedPath] +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js new file mode 100644 index 000000000000..5d3ff238c65d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/multi-hop-source.js @@ -0,0 +1,6 @@ +export const veryLongMultiHopExportName = 'multi-hop' + +export const multiHopExportsInfo = { + veryLongMultiHopExportName: + __webpack_exports_info__.veryLongMultiHopExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js new file mode 100644 index 000000000000..9c091ae7e51d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/barrel.js @@ -0,0 +1,3 @@ +export * from './source' +export const shadowed = 'local' +export default 'local-default' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js new file mode 100644 index 000000000000..db20ad3c24d6 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/index.js @@ -0,0 +1,8 @@ +import value, { shadowed, shadowedUsed, defaultUsed } from './barrel' + +it('does not forward locally shadowed or default names through export star', () => { + expect(value).toBe('local-default') + expect(shadowed).toBe('local') + expect(shadowedUsed).toBe(false) + expect(defaultUsed).toBe(false) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js new file mode 100644 index 000000000000..17df716f695f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-filtering/input/source.js @@ -0,0 +1,5 @@ +export const shadowed = 'source' +export default 'source-default' + +export const shadowedUsed = __webpack_exports_info__.shadowed.used +export const defaultUsed = __webpack_exports_info__.default.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js new file mode 100644 index 000000000000..85e56520d9ba --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/barrel.js @@ -0,0 +1 @@ +export * from './base' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-base.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/base.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-base.js rename to turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/base.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js new file mode 100644 index 000000000000..654e842c8aaf --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/tree-shaking/star-reexport-forwarding/input/index.js @@ -0,0 +1,9 @@ +import { m, mUsed, nUsed } from './barrel' + +it('should keep narrowing exports through an `export *` barrel', () => { + expect(m).toBe(1) + if (process.env.NODE_ENV === 'production') { + expect(mUsed).toBe(true) + expect(nUsed).toBe(false) + } +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js index 04ea9d21c335..660a7408f37c 100644 --- a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/index.js @@ -1,11 +1,6 @@ -it('A: should tree-shake through a 4-level CJS star-reexport chain', () => { - const d = require('./a-l4') - expect(d.a).toBe(1) - if (process.env.NODE_ENV === 'production') { - expect(d.aUsed).toBe(true) - expect(d.bUsed).toBe(false) // b is never read across the chain - } -}) +// Runtime semantics for these cases are covered by the enabled +// `reexport-forwarding-nested-runtime` fixture. This fixture remains skipped only because the +// used/unused precision assertions below are not implemented yet. it('B: should tree-shake a nested namespace reexported through CJS', () => { const m = require('./b-mid') @@ -25,20 +20,6 @@ it('C: should tree-shake a sub-namespace star-reexport (ids != [])', () => { } }) -it('D: should tree-shake a CJS star-reexport of an ESM `export *` barrel', () => { - const d = require('./d-cjs') - expect(d.m).toBe(1) - if (process.env.NODE_ENV === 'production') { - expect(d.mUsed).toBe(true) - expect(d.nUsed).toBe(false) - } -}) - -it('E: should handle circular CJS star-reexports at runtime', () => { - const one = require('./e-one') - expect(one.second).toBe(2) -}) - it('F: should tree-shake a depth-2 nested namespace reexported through CJS', () => { const m = require('./f-cjs') expect(m.mid.deep.x).toBe(1) @@ -48,19 +29,6 @@ it('F: should tree-shake a depth-2 nested namespace reexported through CJS', () } }) -it('G: should tree-shake a diamond of CJS reexports with disjoint usage', () => { - const a = require('./g-p1').a - const b = require('./g-p2').b - expect(a).toBe(1) - expect(b).toBe(2) - if (process.env.NODE_ENV === 'production') { - // each property is pulled through a different reexport path - expect(require('./g-p1').aUsed).toBe(true) - expect(require('./g-p2').bUsed).toBe(true) - expect(require('./g-p1').cUsed).toBe(false) // c used through neither path - } -}) - it('J: should tree-shake when ids unwrap a namespace after a star hop', () => { const m = require('./j-hop2') expect(m.w).toBe(1) @@ -69,10 +37,3 @@ it('J: should tree-shake when ids unwrap a namespace after a star hop', () => { expect(m.zUsed).toBe(false) } }) - -it('H: should resolve mutually circular CJS star-reexports at runtime', () => { - const a = require('./h-a') - expect(a.onlyB).toBe('B') - expect(a.alsoB).toBe('B2') - expect('onlyA' in a).toBe(false) // overwritten by module.exports = require("./h-b") -}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js new file mode 100644 index 000000000000..3fc8e76cf6fb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/forwarder.js @@ -0,0 +1 @@ +module.exports = require('./source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js new file mode 100644 index 000000000000..676890e46d86 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/index.js @@ -0,0 +1,23 @@ +import sourceDefault, { named } from './forwarder' +import * as namespace from './forwarder' +import './side-effect-forwarder' + +it('preserves ESM default, named, and namespace interop through a CJS forwarder', () => { + expect(sourceDefault).toBe('default') + expect(named).toBe('named') + expect(namespace.named).toBe('named') + expect(namespace.other).toBe('other') + expect(namespace.default).toBe('default') +}) + +it('forwards opaque whole-namespace usage and evaluates the target once', () => { + const opaque = Object(require('./forwarder')) + expect(Object.keys(opaque)).toEqual( + expect.arrayContaining(['default', 'named', 'other']) + ) + expect(globalThis.__forwarded_side_effects).toBe(1) +}) + +it('preserves evaluation through a forwarder when no exports are read', () => { + expect(globalThis.__side_effect_only_forwarded).toBe(1) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js new file mode 100644 index 000000000000..8aee6d5ad46e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-forwarder.js @@ -0,0 +1 @@ +module.exports = require('./side-effect-source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js new file mode 100644 index 000000000000..93167d0434f2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/side-effect-source.js @@ -0,0 +1,4 @@ +globalThis.__side_effect_only_forwarded = + (globalThis.__side_effect_only_forwarded ?? 0) + 1 + +export const neverRead = 'never-read' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js new file mode 100644 index 000000000000..95f272b07c93 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/input/source.js @@ -0,0 +1,6 @@ +globalThis.__forwarded_side_effects ??= 0 +globalThis.__forwarded_side_effects++ + +export const named = 'named' +export const other = 'other' +export default 'default' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json new file mode 100644 index 000000000000..58db730e83cd --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-disabled/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js new file mode 100644 index 000000000000..3fc8e76cf6fb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/forwarder.js @@ -0,0 +1 @@ +module.exports = require('./source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js new file mode 100644 index 000000000000..76c965069bed --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/index.js @@ -0,0 +1,7 @@ +import { used, usedInfo, unusedInfo } from './forwarder' + +it('supports an ESM named import through a CommonJS whole-module reexport', () => { + expect(used).toBe('used') + expect(usedInfo).toBe(true) + expect(unusedInfo).toBe(false) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js new file mode 100644 index 000000000000..e5ae8848e014 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/input/source.js @@ -0,0 +1,5 @@ +export const used = 'used' +export const unused = 'unused' + +export const usedInfo = __webpack_exports_info__.used.used +export const unusedInfo = __webpack_exports_info__.unused.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-esm-consumer/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js new file mode 100644 index 000000000000..c98fa3c2b952 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-leaf.js @@ -0,0 +1,4 @@ +export const x = 1 +export const y = 2 +export const xUsed = __webpack_exports_info__.x.used +export const yUsed = __webpack_exports_info__.y.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js new file mode 100644 index 000000000000..5a247da56d51 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-mid.js @@ -0,0 +1 @@ +module.exports = require('./b-ns') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js new file mode 100644 index 000000000000..db62ca44fe99 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/b-ns.js @@ -0,0 +1 @@ +export * as inner from './b-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js new file mode 100644 index 000000000000..ce7273ae6136 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-leaf.js @@ -0,0 +1,4 @@ +export const p = 1 +export const q = 2 +export const pUsed = __webpack_exports_info__.p.used +export const qUsed = __webpack_exports_info__.q.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js new file mode 100644 index 000000000000..50fdee61a9dd --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-mid.js @@ -0,0 +1 @@ +module.exports = require('./c-ns').bag diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js new file mode 100644 index 000000000000..243d43e208fe --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/c-ns.js @@ -0,0 +1 @@ +export * as bag from './c-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js new file mode 100644 index 000000000000..15446cb46fe1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-cjs.js @@ -0,0 +1 @@ +module.exports = require('./f-outer') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js new file mode 100644 index 000000000000..0bf45b414466 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-inner.js @@ -0,0 +1 @@ +export * as deep from './f-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js new file mode 100644 index 000000000000..c98fa3c2b952 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-leaf.js @@ -0,0 +1,4 @@ +export const x = 1 +export const y = 2 +export const xUsed = __webpack_exports_info__.x.used +export const yUsed = __webpack_exports_info__.y.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js new file mode 100644 index 000000000000..dc4b91342a1e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/f-outer.js @@ -0,0 +1 @@ +export * as mid from './f-inner' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js new file mode 100644 index 000000000000..2001fa74f63a --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/index.js @@ -0,0 +1,15 @@ +it('preserves a nested namespace reexported through CJS', () => { + expect(require('./b-mid').inner.x).toBe(1) +}) + +it('preserves a namespace member assigned through CJS', () => { + expect(require('./c-mid').p).toBe(1) +}) + +it('preserves a depth-two nested namespace reexported through CJS', () => { + expect(require('./f-cjs').mid.deep.x).toBe(1) +}) + +it('preserves namespace unwrapping after a forwarding hop', () => { + expect(require('./j-hop2').w).toBe(1) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js new file mode 100644 index 000000000000..946d811250e8 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop1.js @@ -0,0 +1 @@ +module.exports = require('./j-ns') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js new file mode 100644 index 000000000000..f669b25b82cf --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-hop2.js @@ -0,0 +1 @@ +module.exports = require('./j-hop1').box diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js new file mode 100644 index 000000000000..c5184fcce3ab --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-leaf.js @@ -0,0 +1,4 @@ +export const w = 1 +export const z = 2 +export const wUsed = __webpack_exports_info__.w.used +export const zUsed = __webpack_exports_info__.z.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js new file mode 100644 index 000000000000..a8ad4960868b --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/input/j-ns.js @@ -0,0 +1 @@ +export * as box from './j-leaf' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-nested-runtime/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js new file mode 100644 index 000000000000..3fc8e76cf6fb --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/forwarder.js @@ -0,0 +1 @@ +module.exports = require('./source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js new file mode 100644 index 000000000000..676890e46d86 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/index.js @@ -0,0 +1,23 @@ +import sourceDefault, { named } from './forwarder' +import * as namespace from './forwarder' +import './side-effect-forwarder' + +it('preserves ESM default, named, and namespace interop through a CJS forwarder', () => { + expect(sourceDefault).toBe('default') + expect(named).toBe('named') + expect(namespace.named).toBe('named') + expect(namespace.other).toBe('other') + expect(namespace.default).toBe('default') +}) + +it('forwards opaque whole-namespace usage and evaluates the target once', () => { + const opaque = Object(require('./forwarder')) + expect(Object.keys(opaque)).toEqual( + expect.arrayContaining(['default', 'named', 'other']) + ) + expect(globalThis.__forwarded_side_effects).toBe(1) +}) + +it('preserves evaluation through a forwarder when no exports are read', () => { + expect(globalThis.__side_effect_only_forwarded).toBe(1) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js new file mode 100644 index 000000000000..8aee6d5ad46e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-forwarder.js @@ -0,0 +1 @@ +module.exports = require('./side-effect-source') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js new file mode 100644 index 000000000000..93167d0434f2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/side-effect-source.js @@ -0,0 +1,4 @@ +globalThis.__side_effect_only_forwarded = + (globalThis.__side_effect_only_forwarded ?? 0) + 1 + +export const neverRead = 'never-read' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js new file mode 100644 index 000000000000..95f272b07c93 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/input/source.js @@ -0,0 +1,6 @@ +globalThis.__forwarded_side_effects ??= 0 +globalThis.__forwarded_side_effects++ + +export const named = 'named' +export const other = 'other' +export default 'default' diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-runtime/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js new file mode 100644 index 000000000000..3bfa0db43482 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/compound.js @@ -0,0 +1,2 @@ +module.exports = null +module.exports ??= require('./value') diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js new file mode 100644 index 000000000000..aea9ffe3c074 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/index.js @@ -0,0 +1,11 @@ +it('does not classify shadowed module and require bindings as forwarding', () => { + expect(require('./shadowed').value).toBe('shadowed') +}) + +it('preserves a require member assigned to module.exports', () => { + expect(require('./member').value).toBe('inner') +}) + +it('preserves non-simple module.exports assignments', () => { + expect(require('./compound').value).toBe('value') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js new file mode 100644 index 000000000000..e5715e61211c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/member.js @@ -0,0 +1 @@ +module.exports = require('./value').inner diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js new file mode 100644 index 000000000000..072f11d48ac1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/shadowed.js @@ -0,0 +1,6 @@ +function create(module, require) { + module.exports = require('./ignored') + return module.exports +} + +module.exports = create({}, () => ({ value: 'shadowed' })) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js new file mode 100644 index 000000000000..b8bb20dc51b7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/input/value.js @@ -0,0 +1 @@ +module.exports = { value: 'value', inner: { value: 'inner' } } diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding-syntax/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l2.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l2.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l2.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l2.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l3.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l3.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l3.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l3.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l4.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l4.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-l4.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-l4.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-src.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-src.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/a-src.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/a-src.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-barrel.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-barrel.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-barrel.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-barrel.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js new file mode 100644 index 000000000000..4deb9f5985f4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-base.js @@ -0,0 +1,4 @@ +export const m = 1 +export const n = 2 +export const mUsed = __webpack_exports_info__.m.used +export const nUsed = __webpack_exports_info__.n.used diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-cjs.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-cjs.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/d-cjs.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/d-cjs.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-one.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-one.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-one.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-one.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-two.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-two.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/e-two.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/e-two.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-leaf.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-leaf.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-leaf.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-leaf.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p1.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p1.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p1.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p1.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p2.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p2.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/g-p2.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/g-p2.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-a.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-a.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-a.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-a.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-b.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-b.js similarity index 100% rename from turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/__skipped__/deep-reexports/input/h-b.js rename to turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/h-b.js diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js new file mode 100644 index 000000000000..955c41cb31b4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/input/index.js @@ -0,0 +1,42 @@ +it('A: should tree-shake through a 4-level CJS star-reexport chain', () => { + const d = require('./a-l4') + expect(d.a).toBe(1) + if (process.env.NODE_ENV === 'production') { + expect(d.aUsed).toBe(true) + expect(d.bUsed).toBe(false) // b is never read across the chain + } +}) + +it('D: should tree-shake a CJS star-reexport of an ESM `export *` barrel', () => { + const d = require('./d-cjs') + expect(d.m).toBe(1) + if (process.env.NODE_ENV === 'production') { + expect(d.mUsed).toBe(true) + expect(d.nUsed).toBe(false) + } +}) + +it('E: should handle circular CJS star-reexports at runtime', () => { + const one = require('./e-one') + expect(one.second).toBe(2) +}) + +it('G: should tree-shake a diamond of CJS reexports with disjoint usage', () => { + const a = require('./g-p1').a + const b = require('./g-p2').b + expect(a).toBe(1) + expect(b).toBe(2) + if (process.env.NODE_ENV === 'production') { + // each property is pulled through a different reexport path + expect(require('./g-p1').aUsed).toBe(true) + expect(require('./g-p2').bUsed).toBe(true) + expect(require('./g-p1').cUsed).toBe(false) // c used through neither path + } +}) + +it('H: should resolve mutually circular CJS star-reexports at runtime', () => { + const a = require('./h-a') + expect(a.onlyB).toBe('B') + expect(a.alsoB).toBe('B2') + expect('onlyA' in a).toBe(false) // overwritten by module.exports = require("./h-b") +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json new file mode 100644 index 000000000000..28f40b021c90 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/webpack/cjs-tree-shaking/reexport-forwarding/options.json @@ -0,0 +1,3 @@ +{ + "cjsTreeShaking": true +} From 357b2b3c4ca702986ac88d4fec62709f2d5dc716 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 15 Sep 2026 22:19:48 +0200 Subject: [PATCH 3/8] Use pnpm for isolated test installs (#98425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Migrate the five local isolated test installs that explicitly used npm to pnpm. The Nx fixture now has pnpm workspace metadata, while filesystem-layout-sensitive fixtures use pnpm's hoisted linker with copied package files. ### Why? The npm-based Nx install bypassed the repository's centralized supply-chain protections and could select a package immediately after publication, including temporarily incomplete multi-package releases. Using pnpm makes isolated installs inherit the repository's `minimumReleaseAge`, exclusions, and exotic-subdependency policy. The other npm installs depended on npm-style real package directories. On Node versions affected by nodejs/node#65113, hoisted/copy mode preserves that layout without leaving these fixtures outside the shared pnpm security configuration; fixed Node releases use normal pnpm linking. ### How? - Use normal pnpm workspace resolution for the Nx fixture. - Use `node-linker=hoisted` and `package-import-method=copy` for filesystem tests only on affected Node releases; Node 24.21+ and 26.8+ use normal linking. Node 20 CI keeps the workaround because no fixed Node 20 release exists. - Validate local `@next/env` tarballs through the lockfile when hoisted installs do not expose pnpm's virtual-store path marker. - Keep the deployment-environment npm install unchanged. ### Verification - `pnpm build-all` - `pnpm types` - A 9-version throwaway assertion verified the affected/fixed Node release matrix - `pnpm test-dev-turbo test/e2e/app-dir/nx-handling/nx-handling.test.ts test/e2e/handle-non-hoisted-swc-helpers/index.test.ts test/e2e/filesystem-cache/filesystem-cache.test.ts test/e2e/filesystem-cache/warm-restart-task-stats.test.ts test/e2e/filesystem-cache/evict-after-snapshot.test.ts` — all 25 tests passed after installing the sandbox's missing Playwright browser - Production Turbopack: Nx, non-hoisted SWC helper, build-cache-default, and warm restart passed (9/9) - `filesystem-cache.test.ts` production baseline: 15/17 passed; the same two cache-growth bounds fail under both the unchanged npm fixture and the pnpm fixture at nearly identical percentages, so they are pre-existing sandbox-specific failures - Generated-layout inspection: no package symlinks outside expected `.bin` command shims; package files are copied; `node_modules/.pnpm` is metadata-only --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> --- test/e2e/app-dir/nx-handling/.npmrc | 1 - .../app-dir/nx-handling/nx-handling.test.ts | 6 +- test/e2e/app-dir/nx-handling/package.json | 5 +- .../app-dir/nx-handling/pnpm-workspace.yaml | 2 + .../evict-after-snapshot.test.ts | 24 ++++---- .../filesystem-cache/filesystem-cache.test.ts | 29 ++++----- .../warm-restart-task-stats.test.ts | 30 ++++------ .../index.test.ts | 18 ++---- test/lib/create-next-install.js | 60 ++++++++++++++++++- test/lib/pnpm-realpath-workaround.ts | 21 +++++++ 10 files changed, 122 insertions(+), 74 deletions(-) delete mode 100644 test/e2e/app-dir/nx-handling/.npmrc create mode 100644 test/e2e/app-dir/nx-handling/pnpm-workspace.yaml create mode 100644 test/lib/pnpm-realpath-workaround.ts diff --git a/test/e2e/app-dir/nx-handling/.npmrc b/test/e2e/app-dir/nx-handling/.npmrc deleted file mode 100644 index 521a9f7c0773..000000000000 --- a/test/e2e/app-dir/nx-handling/.npmrc +++ /dev/null @@ -1 +0,0 @@ -legacy-peer-deps=true diff --git a/test/e2e/app-dir/nx-handling/nx-handling.test.ts b/test/e2e/app-dir/nx-handling/nx-handling.test.ts index 548144c0abd6..e94cbc8cc3fd 100644 --- a/test/e2e/app-dir/nx-handling/nx-handling.test.ts +++ b/test/e2e/app-dir/nx-handling/nx-handling.test.ts @@ -4,14 +4,12 @@ describe('nx-handling', () => { const { next } = nextTestSetup({ skipDeployment: true, files: __dirname, - installCommand: 'npm i', - buildCommand: 'npm run build', - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + buildCommand: 'pnpm run build', + startCommand: isNextDev ? 'pnpm run dev' : 'pnpm run start', packageJson: { name: '@nx-next/source', version: '0.0.0', private: true, - packageManager: 'npm@10.9.2', scripts: { build: 'rm -rf dist; nx run next-nx-test:build', dev: 'nx run next-nx-test:dev', diff --git a/test/e2e/app-dir/nx-handling/package.json b/test/e2e/app-dir/nx-handling/package.json index 4c2d75109faf..f239bbd47ed6 100644 --- a/test/e2e/app-dir/nx-handling/package.json +++ b/test/e2e/app-dir/nx-handling/package.json @@ -6,7 +6,7 @@ "build": "nx run next-nx-test:build", "dev": "nx run next-nx-test:dev", "start": "nx run next-nx-test:serve:production", - "repro": "rm -rf dist && npm run build && npm run start" + "repro": "rm -rf dist && pnpm run build && pnpm run start" }, "private": true, "dependencies": { @@ -27,6 +27,5 @@ }, "workspaces": [ "apps/*" - ], - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + ] } diff --git a/test/e2e/app-dir/nx-handling/pnpm-workspace.yaml b/test/e2e/app-dir/nx-handling/pnpm-workspace.yaml new file mode 100644 index 000000000000..8ab3e17a0de1 --- /dev/null +++ b/test/e2e/app-dir/nx-handling/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'apps/*' diff --git a/test/e2e/filesystem-cache/evict-after-snapshot.test.ts b/test/e2e/filesystem-cache/evict-after-snapshot.test.ts index a25e8900c97b..0eb6c9485e96 100644 --- a/test/e2e/filesystem-cache/evict-after-snapshot.test.ts +++ b/test/e2e/filesystem-cache/evict-after-snapshot.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { getPnpmRealpathWorkaround } from '../../lib/pnpm-realpath-workaround' import { retry, waitFor } from 'next-test-utils' // Eviction requires the dev server (HMR) and persistent caching (Turbopack). @@ -8,25 +9,20 @@ import { retry, waitFor } from 'next-test-utils' // @force-gate !deploy // @force-gate dev describe('evict-after-snapshot', () => { - const envVars = [ - 'ENABLE_CACHING=1', - 'TURBO_ENGINE_IGNORE_DIRTY=1', - 'TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000', + const env = { + ENABLE_CACHING: '1', + TURBO_ENGINE_IGNORE_DIRTY: '1', + TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS: '1000', // Persist even tiny snapshots so the test doesn't depend on the // minimum-compilation-time threshold. - 'TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS=0', - 'ENABLE_EVICTION=1', - ].join(' ') + TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS: '0', + ENABLE_EVICTION: '1', + } const { next } = nextTestSetup({ files: __dirname, - packageJson: { - scripts: { - dev: `${envVars} next dev`, - }, - }, - installCommand: 'npm i', - startCommand: 'npm run dev', + overrideFiles: getPnpmRealpathWorkaround(), + env, }) async function waitForSnapshotAndEviction() { diff --git a/test/e2e/filesystem-cache/filesystem-cache.test.ts b/test/e2e/filesystem-cache/filesystem-cache.test.ts index f925e531ade5..031961ec3c33 100644 --- a/test/e2e/filesystem-cache/filesystem-cache.test.ts +++ b/test/e2e/filesystem-cache/filesystem-cache.test.ts @@ -5,6 +5,7 @@ import fs from 'fs/promises' import { existsSync } from 'fs' import path from 'path' import { parseTraceEvents } from '../../lib/parse-trace-file' +import { getPnpmRealpathWorkaround } from '../../lib/pnpm-realpath-workaround' async function getDirectorySize(dirPath: string): Promise { try { @@ -39,32 +40,22 @@ for (const cacheEnabled of [false, true]) { delete process.env.NEXT_PUBLIC_ENV_VAR }) - let envVars = [ - `ENABLE_CACHING=${cacheEnabled ? '1' : ''}`, + const env = { + ENABLE_CACHING: cacheEnabled ? '1' : '', // Make it easier to run in development, test directories are cleared between runs already so this is safe. - `TURBO_ENGINE_IGNORE_DIRTY=1`, + TURBO_ENGINE_IGNORE_DIRTY: '1', // decrease the idle timeout to make the test more reliable - `TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000`, + TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS: '1000', // persist even tiny snapshots so the test doesn't depend on the // minimum-compilation-time threshold - `TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS=0`, - ].join(' ') + TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS: '0', + } const { next, isTurbopack } = nextTestSetup({ files: __dirname, - packageJson: { - packageManager: 'npm@10.9.2', - scripts: { - build: `${envVars} next build`, - dev: `${envVars} next dev`, - start: 'next start', - }, - }, - // We need to use npm here as pnpms symlinks trigger a weird bug (kernel bug?) - installCommand: 'npm i', - // Next is always started with caching, but this can disable it for the followup restarts - buildCommand: `npm run build`, - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + overrideFiles: getPnpmRealpathWorkaround(), + // Pass the cache setting through every harness-managed build and restart. + env, }) beforeAll(() => { diff --git a/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts b/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts index 1842e6d8fbf7..40417fb14b59 100644 --- a/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts +++ b/test/e2e/filesystem-cache/warm-restart-task-stats.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup, isNextDev } from 'e2e-utils' +import { getPnpmRealpathWorkaround } from '../../lib/pnpm-realpath-workaround' import { waitFor } from 'next-test-utils' import fs from 'fs/promises' import path from 'path' @@ -30,31 +31,22 @@ const STATS_RELATIVE_PATH = '.next/warm-restart-task-stats.json' // @force-gate !deploy // @force-gate turbopack describe('warm-restart task statistics', () => { - const env = [ - 'ENABLE_CACHING=1', - 'TURBO_ENGINE_IGNORE_DIRTY=1', - 'TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000', + const env = { + ENABLE_CACHING: '1', + TURBO_ENGINE_IGNORE_DIRTY: '1', + TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS: '1000', // Persist even tiny snapshots so the test doesn't depend on the // minimum-compilation-time threshold. - 'TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS=0', - `NEXT_TURBOPACK_TASK_STATISTICS=${STATS_RELATIVE_PATH}`, + TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS: '0', + NEXT_TURBOPACK_TASK_STATISTICS: STATS_RELATIVE_PATH, // Wait for turbo-tasks to persist the cache before the dev process exits. - 'NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN=1', - ].join(' ') + NEXT_DEV_WAIT_FOR_TURBOPACK_SHUTDOWN: '1', + } const { next } = nextTestSetup({ files: __dirname, - packageJson: { - packageManager: 'npm@10.9.2', - scripts: { - build: `${env} next build`, - dev: `${env} next dev`, - start: 'next start', - }, - }, - installCommand: 'npm i', - buildCommand: 'npm run build', - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + overrideFiles: getPnpmRealpathWorkaround(), + env, }) beforeAll(() => { diff --git a/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts b/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts index 18db69d87705..ca83031461b9 100644 --- a/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts +++ b/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts @@ -1,9 +1,13 @@ -import { isNextDev, nextTestSetup } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { renderViaHTTP } from 'next-test-utils' describe('handle-non-hoisted-swc-helpers', () => { const { next } = nextTestSetup({ files: { + '.npmrc': `# The helper move below needs real package directories, not pnpm symlinks. +node-linker=hoisted +package-import-method=copy +`, 'pages/index.js': ` export default function Page() { return

hello world

@@ -20,18 +24,8 @@ describe('handle-non-hoisted-swc-helpers', () => { } `, }, - packageJson: { - packageManager: 'npm@10.9.2', - scripts: { - build: 'next build', - dev: 'next dev', - start: 'next start', - }, - }, installCommand: - 'npm install; mkdir -p node_modules/next/node_modules/@swc; mv node_modules/@swc/helpers node_modules/next/node_modules/@swc/', - buildCommand: 'npm run build', - startCommand: isNextDev ? 'npm run dev' : 'npm run start', + 'pnpm install && mkdir -p node_modules/next/node_modules/@swc && mv node_modules/@swc/helpers node_modules/next/node_modules/@swc/', dependencies: {}, }) diff --git a/test/lib/create-next-install.js b/test/lib/create-next-install.js index 2836852667e5..fe6941ece7a7 100644 --- a/test/lib/create-next-install.js +++ b/test/lib/create-next-install.js @@ -139,6 +139,49 @@ async function applyWorkspaceOverrides(installDir, isolationRoot, overrides) { await fs.writeFile(workspaceFile, yaml.dump(workspaceConfig)) } +/** + * pnpm's hoisted linker does not expose the `@pkg+name@file` virtual-store + * path used by the default linker. Verify the exact local tarball through the + * lockfile instead. + * + * @param {string} installDir + * @param {string} packageName + * @param {string} expectedTarballPath + * @returns {Promise} + */ +async function lockfileResolvesLocalTarball( + installDir, + packageName, + expectedTarballPath +) { + const lockfile = /** @type {Record} */ ( + yaml.load( + await fs.readFile(path.join(installDir, 'pnpm-lock.yaml'), 'utf8') + ) + ) + const expectedRealpath = await fs.realpath(expectedTarballPath) + + for (const [key, pkg] of Object.entries(lockfile.packages || {})) { + const tarball = pkg?.resolution?.tarball + if ( + !key.startsWith(`${packageName}@file:`) || + typeof tarball !== 'string' || + !tarball.startsWith('file:') + ) { + continue + } + + const resolvedTarball = path.resolve( + installDir, + tarball.slice('file:'.length) + ) + if ((await fs.realpath(resolvedTarball)) === expectedRealpath) { + return true + } + } + return false +} + /** * @param {import('next/dist/trace').Span} parentSpan * @returns {Promise>} @@ -368,7 +411,9 @@ async function createNextInstall({ .traceAsyncFn(() => installDependencies(installDir, tmpDir)) // `@next/env` is a dependency of `next`, so it only resolves to the - // local tarball if the overrides were applied. + // local tarball if the overrides were applied. Every generic isolated + // install reaches this guard, but the lockfile fallback short-circuits + // off when the default linker exposes its virtual-store path. if (!combinedDependencies['@next/env']) { const envDir = await fs.realpath( path.join( @@ -376,7 +421,18 @@ async function createNextInstall({ '../@next/env' ) ) - if (!envDir.includes('@next+env@file')) { + const envTarballPath = pkgPaths.get('@next/env') + if ( + !envDir.includes('@next+env@file') && + !( + envTarballPath && + (await lockfileResolvesLocalTarball( + installDir, + '@next/env', + envTarballPath + )) + ) + ) { throw new Error( `@next/env resolved from the npm registry instead of the local tarball (${envDir}), ` + 'the workspace overrides were not applied to the install' diff --git a/test/lib/pnpm-realpath-workaround.ts b/test/lib/pnpm-realpath-workaround.ts new file mode 100644 index 000000000000..a3b7b078ad16 --- /dev/null +++ b/test/lib/pnpm-realpath-workaround.ts @@ -0,0 +1,21 @@ +const PNPM_HOISTED_CONFIG = `# Work around the Node.js realpath bug fixed in 24.21.0 and 26.8.0. +# https://github.com/nodejs/node/pull/65113 +node-linker=hoisted +package-import-method=copy +` + +function hasNodeRealpathFix(nodeVersion: string): boolean { + const [major, minor] = nodeVersion.split('.').map(Number) + return ( + (major === 24 && minor >= 21) || major > 26 || (major === 26 && minor >= 8) + ) +} + +export function getPnpmRealpathWorkaround( + nodeVersion = process.versions.node +): Record | undefined { + if (hasNodeRealpathFix(nodeVersion)) { + return undefined + } + return { '.npmrc': PNPM_HOISTED_CONFIG } +} From 9487264fdae5a6f71105f6d585c0c51ca2056602 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Tue, 15 Sep 2026 14:15:45 -0700 Subject: [PATCH 4/8] test(turbopack): add strong read operation roots (#98248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace top-level consistency suppression in read-ref and trait-ref cell tests with explicit operation roots. The operations separately cover counter creation, counter reads, and trait upcasts while preserving the tests’ warm-cache and snapshot behavior. ## Verification - `cargo test -p turbo-tasks-backend --test read_ref_cell --test trait_ref_cell` - `cargo fmt --all -- --check` --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> --- .../tests/read_ref_cell.rs | 9 ++---- .../tests/trait_ref_cell.rs | 28 ++++++++++++------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs b/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs index eb1242fc4bac..74f9e149442b 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs @@ -5,10 +5,7 @@ use std::{collections::HashSet, mem::take, sync::Mutex}; use anyhow::Result; -use turbo_tasks::{ - Invalidator, ReadRef, Vc, get_invalidator, - unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks, -}; +use turbo_tasks::{Invalidator, ReadRef, Vc, get_invalidator, with_turbo_tasks}; use turbo_tasks_testing::{Registration, register, run_once}; static REGISTRATION: Registration = register!(); @@ -16,7 +13,6 @@ static REGISTRATION: Registration = register!(); #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_read_ref() { run_once(®ISTRATION, async || { - unmark_top_level_task_may_leak_eventually_consistent_state(); let counter = Counter::cell(Counter { value: Mutex::new((0, Default::default())), }); @@ -38,7 +34,8 @@ async fn test_read_ref() { // However, `local_counter_value` will point to the value of `counter_value` // at the time it was turned into a trait reference (just like a `ReadRef` // would). - let local_counter_value = ReadRef::cell(counter_value.await?).get_value(); + let local_counter_value = + ReadRef::cell(counter_value.strongly_consistent().await?).get_value(); counter.await?.incr(); diff --git a/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs b/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs index dd44ed3706e8..64748e1cecbf 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs @@ -5,10 +5,7 @@ use std::{collections::HashSet, mem::take, sync::Mutex}; use anyhow::Result; -use turbo_tasks::{ - Invalidator, TraitRef, Vc, get_invalidator, - unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks, -}; +use turbo_tasks::{Invalidator, ResolvedVc, TraitRef, Vc, get_invalidator, with_turbo_tasks}; use turbo_tasks_testing::{Registration, register, run_once}; static REGISTRATION: Registration = register!(); @@ -16,8 +13,19 @@ static REGISTRATION: Registration = register!(); #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn trait_ref() { run_once(®ISTRATION, async || { - unmark_top_level_task_may_leak_eventually_consistent_state(); - let counter = Counter::cell(Counter { + #[turbo_tasks::function(operation, root)] + fn counter_trait_operation(counter: ResolvedVc) -> Vc> { + Vc::upcast(*counter) + } + + #[turbo_tasks::function(operation, root)] + fn counter_value_trait_operation( + counter: ResolvedVc, + ) -> Vc> { + Vc::upcast(counter.get_value()) + } + + let counter = Counter::resolved_cell(Counter { value: Mutex::new((0, Default::default())), }); @@ -32,8 +40,8 @@ async fn trait_ref() { assert_eq!(*counter_value.strongly_consistent().await?, 1); // `ref_counter` will still point to the same `counter` instance as `counter`. - let trait_ref_counter = Vc::upcast::>(counter) - .into_trait_ref() + let trait_ref_counter = counter_trait_operation(counter) + .read_trait_strongly_consistent() .await?; let ref_counter = TraitRef::cell(trait_ref_counter.clone()); let ref_counter_value = ref_counter.get_value(); @@ -42,8 +50,8 @@ async fn trait_ref() { // at the time it was turned into a trait reference (just like a `ReadRef` // would). let local_counter_value = TraitRef::cell( - Vc::upcast::>(counter_value) - .into_trait_ref() + counter_value_trait_operation(counter) + .read_trait_strongly_consistent() .await?, ) .get_value(); From 69d11874f656f29505932a1b27b7b127c616501e Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 15 Sep 2026 23:36:07 +0200 Subject: [PATCH 5/8] Fix constant inlining in shorthand properties (#98662) ### What? Fixes constant replacement in object shorthand properties and adds focused Turbopack execution coverage for the existing analyzer-aware cross-module constants path. ### Why? This is a pre-existing bug in `ConstantValueCodeGen`, which is shared by the current analyzer-aware/opt-in constants implementation and the newer codegen-only export-inlining work. When an imported constant is used as `{ VALUE }`, the AST path ends at `Prop::Shorthand`; the old expression-only visitor could replace the enclosing object literal, collapsing `{ FIRST, SECOND }` to a single primitive. This fix is intentionally independent of the export-inlining feature stack so existing `turbopackCrossModuleConstants` users receive and review the correction separately. ### How? When the code-generation path ends at a shorthand property, it now mirrors `EsmBinding`: the shorthand is expanded to a key/value property, preserving the original key and generating the compile-time constant only for the value. Other expression paths are unchanged, and generated values retain the standard compile-time marker. The execution fixture uses only analyzer-aware constants. Without the fix it returns `"second"` instead of `{ FIRST: "first", SECOND: "second" }`; with the fix it verifies the runtime object, both generated keys, and both marked inlined values. ### Verification - `NODE_PATH=/vercel/sandbox/test-deps/node_modules cargo test -p turbopack-tests --test execution cross_module_constants_shorthand -- --nocapture` - `cargo test -p turbopack-tests --test snapshot cross_module -- --nocapture` - `cargo check -p turbopack-ecmascript -p turbopack-tests --tests` - `cargo fmt --all -- --check` Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> --- .../src/references/constant_value.rs | 37 +++++++++++++++++-- .../input/index.js | 14 +++++++ .../node_modules/shorthand-constants/index.js | 2 + .../shorthand-constants/package.json | 3 ++ .../options.json | 5 +++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json diff --git a/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs b/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs index ec74c434f843..142b0485be0e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/constant_value.rs @@ -7,6 +7,7 @@ use swc_core::{ ArrayLit, EsVersion, Expr, KeyValueProp, Lit, ObjectLit, Prop, PropName, Regex, Str, }, parser::{Syntax, parse_file_as_expr}, + visit::fields::PropField, }, quote, }; @@ -37,12 +38,40 @@ impl ConstantValueCodeGen { _chunking_context: Vc>, ) -> Result { let value = self.value.clone(); + let mut visitors = Vec::new(); + let mut ast_path = self.path.0.clone(); - let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| { - *expr = value_to_expr(&value); - }); + if matches!( + ast_path.last(), + Some(swc_core::ecma::visit::AstParentKind::Prop( + PropField::Shorthand + )) + ) { + ast_path.pop(); + visitors.push(create_visitor!( + exact, + ast_path, + visit_mut_prop, + |prop: &mut Prop| { + if let Prop::Shorthand(ident) = prop { + *prop = Prop::KeyValue(KeyValueProp { + key: PropName::Ident(ident.clone().into()), + value: value_to_expr(&value).into(), + }); + } + } + )); + } else { + visitors.push(create_visitor!( + self.path, + visit_mut_expr, + |expr: &mut Expr| { + *expr = value_to_expr(&value); + } + )); + } - Ok(CodeGeneration::visitors(vec![visitor])) + Ok(CodeGeneration::visitors(visitors)) } } diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js new file mode 100644 index 000000000000..0ab554ce5155 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/index.js @@ -0,0 +1,14 @@ +import { FIRST, SECOND } from 'shorthand-constants' + +function readConstants() { + return { FIRST, SECOND } +} + +it('inlines analyzer-aware constants in shorthand properties', () => { + expect(readConstants()).toEqual({ FIRST: 'first', SECOND: 'second' }) + + const source = readConstants.toString() + expect(source).toContain('FIRST:') + expect(source).toContain('SECOND:') + expect(source).toContain('TURBOPACK compile-time value') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js new file mode 100644 index 000000000000..838b5037fe4c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/index.js @@ -0,0 +1,2 @@ +export const FIRST = 'first' +export const SECOND = 'second' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json new file mode 100644 index 000000000000..a43829151e14 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/input/node_modules/shorthand-constants/package.json @@ -0,0 +1,3 @@ +{ + "sideEffects": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json new file mode 100644 index 000000000000..08185d7558d7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/optimization/cross-module-constants-shorthand/options.json @@ -0,0 +1,5 @@ +{ + "scopeHoisting": false, + "mangleExportNames": false, + "crossModuleConstants": true +} From f1285cec5f6db387f845da3cac27666b492b397e Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 16 Sep 2026 01:15:46 +0200 Subject: [PATCH 6/8] Use default pnpm package import method (#98703) ### What? Use pnpm's default package import method in the two hoisted isolated-test configurations added by #98425. ### Why? `package-import-method` controls how regular package files are materialized from pnpm's store: by reflink, hardlink, or copy. Those choices do not affect `realpath`, so forcing copies is unrelated to the Node.js symlink-resolution workaround and unnecessarily disables pnpm's more efficient defaults. ### How? Keep `node-linker=hoisted`, which is the setting responsible for producing npm-style real package directories, while removing the independent copy policy. The Node-version gate, local-tarball validation, release-age policy, and fixture behavior remain unchanged. ### Verification - Hoisted-only scratch install produced real package directories, no package symlinks outside `.bin`, and a metadata-only `.pnpm` - Production Turbopack: non-hoisted SWC helper and warm-restart task stats passed (2/2) - `pnpm build` - `pnpm types` - Prettier, ESLint, and `git diff --check` Follow-up to #98425 and https://github.com/vercel/next.js/pull/98425#discussion_r4020168942. Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> --- test/e2e/handle-non-hoisted-swc-helpers/index.test.ts | 1 - test/lib/pnpm-realpath-workaround.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts b/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts index ca83031461b9..df105c740c6c 100644 --- a/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts +++ b/test/e2e/handle-non-hoisted-swc-helpers/index.test.ts @@ -6,7 +6,6 @@ describe('handle-non-hoisted-swc-helpers', () => { files: { '.npmrc': `# The helper move below needs real package directories, not pnpm symlinks. node-linker=hoisted -package-import-method=copy `, 'pages/index.js': ` export default function Page() { diff --git a/test/lib/pnpm-realpath-workaround.ts b/test/lib/pnpm-realpath-workaround.ts index a3b7b078ad16..40b288521c8c 100644 --- a/test/lib/pnpm-realpath-workaround.ts +++ b/test/lib/pnpm-realpath-workaround.ts @@ -1,7 +1,6 @@ const PNPM_HOISTED_CONFIG = `# Work around the Node.js realpath bug fixed in 24.21.0 and 26.8.0. # https://github.com/nodejs/node/pull/65113 node-linker=hoisted -package-import-method=copy ` function hasNodeRealpathFix(nodeVersion: string): boolean { From 2a3bf9ae2105f4159003772a1da9e2c9137af597 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 16 Sep 2026 01:18:37 +0200 Subject: [PATCH 7/8] fix(turbopack): trace cyclic modules to explicit entries (#98661) ### What? Makes Turbopack import tracing recognize the module graph's explicit entries as roots, including when an entry participates in an import cycle. Adds focused graph-level coverage and a Next.js production fixture that exercises issue formatting through a cyclic graph. If a malformed graph still has no path to any explicit entry, tracing now returns a minimal trace and emits a bug-severity issue instead of panicking. ### Why? The import tracer inferred roots from nodes with no incoming edges. A valid cycle that points back to an entry gives every node an incoming edge, so the tracer could not find a root and panicked while formatting another diagnostic. The graph already records its entries explicitly, making them the authoritative and cycle-safe definition of a root. The same topology-based assumption affected entry membership checks, so those now use the explicit entry list as well. ### How? - Resolve import-trace paths against node indices derived from `GraphEntries`. - Preserve a defensive fallback for malformed graphs and classify that invariant violation as an implementation bug. - Cover cyclic entry paths, malformed rootless graphs, absent paths, and the product-level issue-formatting path. Fixes #98205 --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> --- .../app/dependency.ts | 7 + .../app/layout.tsx | 9 + .../turbopack-module-graph-cycle/app/page.tsx | 5 + .../next.config.ts | 5 + .../turbopack-module-graph-cycle.test.ts | 17 ++ .../turbopack-core/src/module_graph/mod.rs | 243 ++++++++++++++++-- 6 files changed, 267 insertions(+), 19 deletions(-) create mode 100644 test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts create mode 100644 test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx create mode 100644 test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx create mode 100644 test/production/app-dir/turbopack-module-graph-cycle/next.config.ts create mode 100644 test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts diff --git a/test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts b/test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts new file mode 100644 index 000000000000..58ac0b09de0b --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/app/dependency.ts @@ -0,0 +1,7 @@ +import Page from './page' +import './missing.css' + +// This reference back to the page puts the graph's explicit entry in a cycle. +void Page + +export const message = 'module graph cycle' diff --git a/test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx b/test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx new file mode 100644 index 000000000000..7c3f422f0039 --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/app/layout.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react' + +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx b/test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx new file mode 100644 index 000000000000..fc07a2615b9c --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/app/page.tsx @@ -0,0 +1,5 @@ +import { message } from './dependency' + +export default function Page() { + return

{message}

+} diff --git a/test/production/app-dir/turbopack-module-graph-cycle/next.config.ts b/test/production/app-dir/turbopack-module-graph-cycle/next.config.ts new file mode 100644 index 000000000000..e4f5738a310b --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts b/test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts new file mode 100644 index 000000000000..930b1a53f891 --- /dev/null +++ b/test/production/app-dir/turbopack-module-graph-cycle/turbopack-module-graph-cycle.test.ts @@ -0,0 +1,17 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('turbopack-module-graph-cycle', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + }) + + it('reports compilation issues for a module cycle without panicking', async () => { + const { exitCode, cliOutput } = await next.build() + + expect(exitCode).toBe(1) + expect(cliOutput).toContain("Can't resolve './missing.css'") + expect(cliOutput).not.toContain('there must be a path to a root') + expect(cliOutput).not.toContain('Module graph is missing an entry point') + }) +}) diff --git a/turbopack/crates/turbopack-core/src/module_graph/mod.rs b/turbopack/crates/turbopack-core/src/module_graph/mod.rs index 6e48926ad71d..18a79a93c285 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/mod.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/mod.rs @@ -3,6 +3,7 @@ use std::{ future::Future, iter::FusedIterator, ops::Deref, + sync::OnceLock, }; use anyhow::{Context, Result, bail}; @@ -10,12 +11,12 @@ use bincode::{Decode, Encode}; use petgraph::{ Direction, graph::{DiGraph, EdgeIndex, NodeIndex}, - visit::{EdgeRef, IntoNeighbors, IntoNodeReferences, NodeIndexable, Reversed}, + visit::{EdgeRef, IntoNodeReferences, NodeIndexable, Reversed}, }; use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use tracing::{Instrument, Level, Span}; -use turbo_rcstr::RcStr; +use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ CollectiblesSource, FxIndexMap, NonLocalValue, OperationVc, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc, @@ -27,7 +28,10 @@ use turbo_tasks_fs::FileSystemPath; use crate::{ chunk::{AsyncModuleInfo, ChunkingContext, ChunkingType, TracedMode}, - issue::{ImportTracer, ImportTraces, Issue}, + issue::{ + ImportTracer, ImportTraces, Issue, IssueExt, IssueSeverity, StyledString, + analyze::AnalyzeIssue, + }, module::Module, module_graph::{ async_module_info::{AsyncModulesInfo, compute_async_module_info}, @@ -292,7 +296,7 @@ impl GraphEntries { } #[turbo_tasks::value(cell = "new", eq = "manual")] -#[derive(Clone, Default)] +#[derive(Default)] pub struct SingleModuleGraph { pub graph: TracedDiGraph, @@ -310,7 +314,26 @@ pub struct SingleModuleGraph { modules: FxHashMap>, NodeIndex>, #[turbo_tasks(trace_ignore)] - pub entries: GraphEntries, + entries: GraphEntries, + + /// Derived from `entries` and `modules`. Both are immutable after graph construction, and node + /// indices are stable because graph nodes are never removed. + #[turbo_tasks(debug_ignore, trace_ignore)] + #[bincode(skip, default = "OnceLock::new")] + entry_nodes: OnceLock>, +} + +impl Clone for SingleModuleGraph { + fn clone(&self) -> Self { + Self { + graph: self.graph.clone(), + number_of_modules: self.number_of_modules, + modules: self.modules.clone(), + entries: self.entries.clone(), + // Never carry derived state into a clone that might be modified before being stored. + entry_nodes: OnceLock::new(), + } + } } #[derive( @@ -502,6 +525,7 @@ impl SingleModuleGraph { number_of_modules, modules, entries: entries.clone(), + entry_nodes: OnceLock::new(), } .cell(); @@ -520,16 +544,23 @@ impl SingleModuleGraph { }) } - /// Returns true if the given module is in this graph and is an entry module + fn entry_nodes(&self) -> &FxHashSet { + self.entry_nodes.get_or_init(|| { + self.entries + .all_modules() + .filter_map(|module| self.modules.get(&module).copied()) + .collect() + }) + } + + /// Returns true if the given module is in this graph and is an entry module. + /// + /// Entry modules are tracked explicitly because an entry can have incoming edges when it is + /// part of a module cycle. pub fn has_entry_module(&self, module: ResolvedVc>) -> bool { - if let Some(index) = self.modules.get(&module) { - self.graph - .edges_directed(*index, Direction::Incoming) - .next() - .is_none() - } else { - false - } + self.modules + .get(&module) + .is_some_and(|index| self.entry_nodes().contains(index)) } /// Iterate over graph entry points @@ -712,6 +743,9 @@ impl ImportTracer for ModuleGraphImportTracer { let graph = &*self.await?.graph.await?; let reversed_graph = Reversed(&graph.graph.0); + // A graph entry may have incoming edges when it participates in a cycle, so roots cannot + // be inferred from graph topology alone. + let root_nodes = graph.entry_nodes(); return Ok(ImportTraces::cell(ImportTraces( modules .iter() @@ -721,11 +755,11 @@ impl ImportTracer for ModuleGraphImportTracer { // from a different graph than graph`. Just error out. bail!("inconsistent read?") }; - // compute the path from this index to a root of the graph. - let Some((_, path)) = petgraph::algo::astar( + // Compute the path from this index to an explicit root of the graph. + let path = match petgraph::algo::astar( &reversed_graph, module_idx, - |n| reversed_graph.neighbors(n).next().is_none(), + |n| root_nodes.contains(&n), // Edge weights |e| match e.weight().chunking_type { // Prefer following normal imports/requires when we can @@ -746,8 +780,31 @@ impl ImportTracer for ModuleGraphImportTracer { // solution would be a hand written implementation of dijkstras so we can // hoist redundant work out of this loop. |_| 0, - ) else { - unreachable!("there must be a path to a root"); + ) { + Some((_, path)) => path, + None => { + let module = graph + .graph + .node_weight(module_idx) + .expect("module index must be present in the graph") + .module(); + AnalyzeIssue::new( + IssueSeverity::Bug, + module.ident(), + Vc::cell(rcstr!("Module graph is missing an entry point")), + StyledString::Text(rcstr!( + "The module cannot reach any of the explicit entry points in \ + its module graph." + )) + .cell(), + None, + None, + ) + .to_resolved() + .await? + .emit(); + vec![module_idx] + } }; // Represent the path as a sequence of AssetIdents @@ -1995,12 +2052,160 @@ pub mod tests { use crate::{ asset::{Asset, AssetContent}, ident::AssetIdent, + issue::{CollectibleIssuesExt, IssueSeverity}, module::{Module, ModuleSideEffects}, module_graph::chunk_group_info::EntryHeuristics, reference::{ModuleReference, ModuleReferences}, resolve::ModuleResolveResult, }; + #[turbo_tasks::value(shared)] + struct ImportTraceTestResult { + has_entry: bool, + traces: Vec>, + missing_traces: Vec>, + } + + #[turbo_tasks::function(operation, root)] + async fn import_trace_test_operation(rootless: bool) -> Result> { + let fs = VirtualFileSystem::new_with_name(rcstr!("test")); + let root = fs.root().await?; + let repo = TestRepo::new( + &root, + [ + ("entry.js", vec!["dependency.js"]), + ("dependency.js", vec!["entry.js"]), + ], + ); + let entry = Vc::upcast::>(MockModule::new(root.join("entry.js")?, repo)) + .to_resolved() + .await?; + let graph = SingleModuleGraph::new_with_entries( + GraphEntries::resolved_cell(GraphEntries::new( + vec![ChunkGroupEntry::Entry { + modules: vec![entry], + heuristics: EntryHeuristics::default(), + }], + vec![], + )), + false, + false, + ) + .connect() + .to_resolved() + .await?; + let graph = if rootless { + // Initialize the source graph's cache before cloning to ensure clones reset derived + // state instead of retaining entry indices that could become stale after modification. + let _ = graph.await?.entry_nodes(); + let mut graph = (*graph.await?).clone(); + graph.entries = GraphEntries::default(); + graph.resolved_cell() + } else { + graph + }; + + let has_entry = graph.await?.has_entry_module(entry); + let tracer = ModuleGraphImportTracer::new(*graph); + let traces = tracer + .get_traces(root.join("dependency.js")?) + .await? + .0 + .iter() + .map(|trace| trace.iter().map(|ident| ident.path.path.clone()).collect()) + .collect(); + let missing_traces = tracer + .get_traces(root.join("missing.js")?) + .await? + .0 + .iter() + .map(|trace| trace.iter().map(|ident| ident.path.path.clone()).collect()) + .collect(); + + Ok(ImportTraceTestResult { + has_entry, + traces, + missing_traces, + } + .cell()) + } + + #[turbo_tasks::value(shared)] + struct ImportTraceIssues { + issues: Vec<(IssueSeverity, RcStr)>, + } + + #[turbo_tasks::function(operation, root)] + async fn import_trace_issues_operation( + trace_operation: OperationVc, + ) -> Result> { + let _ = trace_operation.connect().await?; + let issues = trace_operation + .peek_issues() + .iter() + .map(async |issue| { + let issue = issue.into_trait_ref().await?; + Ok(( + issue.severity(), + issue.title().await?.to_unstyled_string().into(), + )) + }) + .try_join() + .await?; + Ok(ImportTraceIssues { issues }.cell()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_import_trace_uses_explicit_entry_as_cycle_root() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + tt.run_once(async { + let result = import_trace_test_operation(false) + .read_strongly_consistent() + .await?; + assert!(result.has_entry); + assert_eq!( + result.traces, + vec![vec![rcstr!("dependency.js"), rcstr!("entry.js")]] + ); + assert!(result.missing_traces.is_empty()); + Ok(()) + }) + .await + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_cloned_rootless_import_trace_resets_cache_and_emits_bug_issue() { + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + tt.run_once(async { + let trace_operation = import_trace_test_operation(true); + let result = trace_operation.read_strongly_consistent().await?; + assert!(!result.has_entry); + assert_eq!(result.traces, vec![vec![rcstr!("dependency.js")]]); + assert!(result.missing_traces.is_empty()); + + let issues = import_trace_issues_operation(trace_operation) + .read_strongly_consistent() + .await?; + assert_eq!( + issues.issues, + vec![( + IssueSeverity::Bug, + rcstr!("Module graph is missing an entry point") + )] + ); + Ok(()) + }) + .await + .unwrap(); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_traverse_dfs_from_entries_diamond() { run_graph_test( From 90515901d61def46f66de9116f22e19e3ca6370d Mon Sep 17 00:00:00 2001 From: "next-js-bot[bot]" <279046576+next-js-bot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:22:42 +0000 Subject: [PATCH 8/8] v16.4.0-canary.32 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/devlow-bench/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-internal/package.json | 2 +- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-playwright/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-routing/package.json | 2 +- packages/next-rspack/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 ++++++------- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 22 ++++++++++---------- 22 files changed, 40 insertions(+), 40 deletions(-) diff --git a/lerna.json b/lerna.json index bdfd0386c650..0d7773a7c159 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.4.0-canary.31" + "version": "16.4.0-canary.32" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 5ec6f3869de2..cba79da5f1ca 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/devlow-bench/package.json b/packages/devlow-bench/package.json index 975c9765b37f..a67324f10aa3 100644 --- a/packages/devlow-bench/package.json +++ b/packages/devlow-bench/package.json @@ -1,6 +1,6 @@ { "name": "@vercel/devlow-bench", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "Benchmarking tool for the developer workflow", "repository": { "type": "git", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 6335d59fd00e..71572654a38c 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.4.0-canary.31", + "@next/eslint-plugin-next": "16.4.0-canary.32", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 300eed797af8..96f010580e99 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 8df9824eaa81..a78eb0b28f2e 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index bb8915030571..44a4e067b9d7 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 2c9b4373609d..227d7d11aca6 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 3499fe182163..39d34f894f1f 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 48d37af8a78a..cc3d72f986a1 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index daa642478fa9..d397518712af 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index 1f02e311dc3c..951d479bb5a8 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-playwright" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 501ae32f2312..cac33410c6f1 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 678d389f9a6d..802128fba60f 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 45a2fa4ef10b..a362d50cac07 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 163124c2063b..f8f1dad759c7 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index f31784109268..000b03bd85d8 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 936f9b4bca3c..7fa00e0f3cc8 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 2ff94d008a2c..d4766b565699 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,7 +100,7 @@ ] }, "dependencies": { - "@next/env": "16.4.0-canary.31", + "@next/env": "16.4.0-canary.32", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -164,11 +164,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.42.5", "@napi-rs/triples": "1.2.0", - "@next/font": "16.4.0-canary.31", - "@next/polyfill-module": "16.4.0-canary.31", - "@next/polyfill-nomodule": "16.4.0-canary.31", - "@next/react-refresh-utils": "16.4.0-canary.31", - "@next/swc": "16.4.0-canary.31", + "@next/font": "16.4.0-canary.32", + "@next/polyfill-module": "16.4.0-canary.32", + "@next/polyfill-nomodule": "16.4.0-canary.32", + "@next/react-refresh-utils": "16.4.0-canary.32", + "@next/swc": "16.4.0-canary.32", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.61.0", "@rspack/core": "1.6.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 9ba08397915b..70d2c21fd1b1 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index f37fb3f76b7a..e3f25a9cf5b4 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.4.0-canary.31", + "version": "16.4.0-canary.32", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.4.0-canary.31", + "next": "16.4.0-canary.32", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30fffaf7bc1c..0d3a0076d07e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1012,7 +1012,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1095,7 +1095,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-env '@swc/helpers': specifier: 0.5.23 @@ -1216,19 +1216,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../font '@next/polyfill-module': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../react-refresh-utils '@next/swc': - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1944,7 +1944,7 @@ importers: devDependencies: '@napi-rs/cli': specifier: 3.7.2 - version: 3.7.2(@emnapi/runtime@1.11.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0) + version: 3.7.2(@emnapi/runtime@1.9.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0) cross-env: specifier: 6.0.3 version: 6.0.3 @@ -1971,7 +1971,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.4.0-canary.31 + specifier: 16.4.0-canary.32 version: link:../next outdent: specifier: 0.8.0 @@ -21842,7 +21842,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/cli@3.7.2(@emnapi/runtime@1.11.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0)': + '@napi-rs/cli@3.7.2(@emnapi/runtime@1.9.2)(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab))(node-addon-api@6.1.0)': dependencies: '@inquirer/prompts': 8.5.2(@types/node@20.17.7(patch_hash=fe6957869a9b616ec526ededb72bbe846b863bd2349204218772df5c1e62d6ab)) '@napi-rs/cross-toolchain': 1.0.3 @@ -21857,7 +21857,7 @@ snapshots: semver: 7.8.5 typanion: 3.14.0 optionalDependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.9.2 transitivePeerDependencies: - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7'