From 451985dcde65b7426599bbea423d6ad86d4c819e Mon Sep 17 00:00:00 2001 From: "next-js-bot[bot]" <279046576+next-js-bot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:54:00 +0000 Subject: [PATCH 1/5] v16.4.0-canary.10 --- 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 | 16 ++++++++-------- 22 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lerna.json b/lerna.json index 04824ab1f7a9..1abd07f907ce 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.4.0-canary.9" + "version": "16.4.0-canary.10" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 71258a474d2c..cf56b456fd6d 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.9", + "version": "16.4.0-canary.10", "keywords": [ "react", "next", diff --git a/packages/devlow-bench/package.json b/packages/devlow-bench/package.json index 303e4ae2ee51..293b6a02d0ae 100644 --- a/packages/devlow-bench/package.json +++ b/packages/devlow-bench/package.json @@ -1,7 +1,7 @@ { "name": "@vercel/devlow-bench", "private": true, - "version": "16.4.0-canary.9", + "version": "16.4.0-canary.10", "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 542da0d12882..c03c631fc11e 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.9", + "version": "16.4.0-canary.10", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.4.0-canary.9", + "@next/eslint-plugin-next": "16.4.0-canary.10", "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 e3f646e8057a..187883fffc01 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.9", + "version": "16.4.0-canary.10", "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 f4d15882b4ec..f8847e7785ff 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.9", + "version": "16.4.0-canary.10", "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 cdeead0ad9d5..b19dcdbcad2c 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.9", + "version": "16.4.0-canary.10", "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 718ddf415121..09409943ab50 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.9", + "version": "16.4.0-canary.10", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index e3241759aa4a..bd5c6dc82cfd 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.9", + "version": "16.4.0-canary.10", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 8ab949ff0093..50daed34841d 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.9", + "version": "16.4.0-canary.10", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 02f27860793d..4e19075d1408 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.9", + "version": "16.4.0-canary.10", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index ecba30b4d73e..9656d7411628 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.9", + "version": "16.4.0-canary.10", "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 98a9a7760487..f3f3ddc055f0 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.9", + "version": "16.4.0-canary.10", "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 b80f09efc720..27bbfee20b09 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.9", + "version": "16.4.0-canary.10", "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 ca359cb692e9..be939f2fde28 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.9", + "version": "16.4.0-canary.10", "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 6da1f2bb499f..b93ef8b3eb46 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.9", + "version": "16.4.0-canary.10", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index ba0514f5c0cb..560c76396d82 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.9", + "version": "16.4.0-canary.10", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 6a36849bcd7c..59286d533d02 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.9", + "version": "16.4.0-canary.10", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 71d761b7aa49..321ba3dc6367 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.4.0-canary.9", + "version": "16.4.0-canary.10", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,7 +100,7 @@ ] }, "dependencies": { - "@next/env": "16.4.0-canary.9", + "@next/env": "16.4.0-canary.10", "@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.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.4.0-canary.9", - "@next/polyfill-module": "16.4.0-canary.9", - "@next/polyfill-nomodule": "16.4.0-canary.9", - "@next/react-refresh-utils": "16.4.0-canary.9", - "@next/swc": "16.4.0-canary.9", + "@next/font": "16.4.0-canary.10", + "@next/polyfill-module": "16.4.0-canary.10", + "@next/polyfill-nomodule": "16.4.0-canary.10", + "@next/react-refresh-utils": "16.4.0-canary.10", + "@next/swc": "16.4.0-canary.10", "@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 35c9cee16be4..3df93e3f67e7 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.9", + "version": "16.4.0-canary.10", "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 c4ca16a8e89d..7d450309beea 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.9", + "version": "16.4.0-canary.10", "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.9", + "next": "16.4.0-canary.10", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9d7630ecc87..0ae4a6114100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1024,7 +1024,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1107,7 +1107,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../next-env '@swc/helpers': specifier: 0.5.23 @@ -1228,19 +1228,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../font '@next/polyfill-module': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../react-refresh-utils '@next/swc': - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1983,7 +1983,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.4.0-canary.9 + specifier: 16.4.0-canary.10 version: link:../next outdent: specifier: 0.8.0 From a84bc8de2a8c84cd027ff4d70aaff8273a36e1f2 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Fri, 28 Aug 2026 07:46:16 +0200 Subject: [PATCH 2/5] Turbopack: mangle exported names for smaller bundle sizes (#97672) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Adds export-name mangling to Turbopack, behind a new experimental option `experimental.turbopackMangleExportNames` (default `false`). It is independent of minification: `--no-mangling` is a minifier flag and does not affect it. When enabled, each ECMAScript module's *used* export names — including `default` and `__esModule` — are replaced by short keys in the emitted output, both where the module registers its exports and where every consumer reads them. Modules whose export names could be observed by user code keep their original names, decided per module. This is a reland of #89060 (on top of the already-merged #89406), originally written by Matt Mastracci, who is credited as a co-author on the commit. Stacked on top: #97676 flips the default to `true` on canary releases, so Next.js's own CI exercises the feature broadly before it is considered for stable. ### Why? Bundle size. A module's export keys exist only to link modules together: the producing module emits `{ someVeryLongExportName: … }` and every consumer reads `ns["someVeryLongExportName"]`. Both sides are generated by us, so as long as producer and consumer agree — and the name isn't observable from user code — the key can be a single character. Long export names are extremely common in real dependency graphs (icon sets, utility packages, barrel files), and each one is paid for once in the module that defines it and once per importing module. ### How? **Ported, not rebased.** `canary` is ~2500 commits past the original stack's base, and the files it touched were independently rewritten in the meantime (export-analysis refactor #92781, CJS analysis for scope hoisting #95826, the `module_fragments` subsystem #95978). A probe rebase produced 16 conflicting files on the first commit alone, so the original branches were used as a reference implementation — for intent, the identifier alphabet, and test coverage — and the feature was rebuilt on today's infrastructure. #89561 from the original stack (erasing the Next.js wrapper module types) is deliberately **not** part of this change; it turned out to be unnecessary, because those modules already declare whole-module export usage and therefore back off on their own. **The name table** (`references/esm/mangle/table.rs`) hashes each name into a table of all valid JS identifiers of the smallest length that fits the name set — 15 exports get single-character keys — and resolves collisions by open addressing. Hashing rather than assigning `a`, `b`, `c`, … is what keeps names stable: an unrelated edit elsewhere in the module doesn't renumber every other export, and a collision only perturbs its own cluster. Assignment happens in two passes: every name that is *already* a valid identifier at the chosen length keeps itself and reserves its bucket first, and only then is anything hashed — so an export called `a` keeps `a`, and nothing else can be assigned it. Both passes iterate in sorted order, so the mapping depends only on the set of names. A module with exactly **one** mangleable export is special-cased to a fixed key, `f`, rather than a hashed one. `f` is the most common character in JS keywords (`if`, `for`, `function`), and every single-export module in the graph then emits the same `.f` / `.f()` byte sequences, which gzip's back-references pick up across the whole bundle — a bigger win than hashing, at the cost of that one key changing when a second export is added. A fixed list, `RESERVED_KEYS`, is withheld from every table for two different reasons: JS reserved words (`if`, `in`, `do`, `for`, `let`, `new`, `try`, `var`) are legal as quoted property keys but a minifier will not fold `ns["if"]` into the shorter `ns.if`, so handing one out costs bytes instead of saving them; and `__esModule` is withheld because the runtime's `esm()` helper defines that property on every module's exports object regardless of what the module itself exports, so an assigned key landing on it would collide. (`default` needs no such protection — once it is mangled like any other export, nothing else emits a property under that literal name.) **One source of truth for the mapping.** `mangled_export_names(module, chunking_context)` is a turbo-task that both the producing side (`EsmExports::code_generation`) and the consuming side (`ReferencedAssetIdent::Module`, the single place a cross-module export access is materialized) ask for the *target* module's map. Neither side computes a table of its own, so they cannot disagree, and the task derives export usage from the chunking context itself rather than accepting it as an argument, so a caller can't supply usage from the wrong graph. Re-export chains need no special handling, because the consumer side already resolves through re-exports to the module that produces the binding. The mangling decision itself lives on `EsmExports` as a `mangle_export_names: bool` field, rather than a separate trait method every module type has to override. A module that derives its exports from another one (a facade, a locals module, a part, a rename) inherits the flag with the data, which removed seven hand-written delegations and the possibility of a new wrapper type forgetting one. **A mangling decision must not cross module identities.** A few module types hand out *another* module's exports value as their own (the WASM loader module, the module-fragments side-effects wrapper, the client-reference proxy). If that borrowed value carried a real mangling decision, the producing and consuming sides would key their lookups on two different modules and could compute two different keys for the same export — this actually broke every WASM- and `@vercel/og`-based test once the default-on layer exercised it in CI. `EcmascriptExports::borrowed()` is the one place this is handled: it always returns an unmangled view, and every such pass-through site uses it. **Back-off is per module**, built on the export-usage information that landed after the original PR (`BindingUsageInfo` / `ModuleExportUsageInfo`) rather than the original's locals/facade-split heuristic. A module keeps its names when its usage is `All` (a namespace import that couldn't be lowered, a computed property access, an unresolvable `export *`, or a chunk-group entry — which covers client references and the Next.js wrapper modules), when it is read through a namespace value at all, when its exports are dynamic or not statically known ESM, or when names aren't being mangled in this build. `__webpack_exports_info__` gains `canMangle` and `mangledName` per export, which is how a running test can observe the mapping; with the option off it emits exactly what it emitted before. ### Testing - `turbo-tasks-hash`/table unit tests: encode/decode round-trip, degenerate-name rejection, table sizing, the single-export fixed key (including its own reservation), reserved-word withholding (including a reserved bucket-count test that stays in sync with the reserved list), the preserved-name pass running before any hashing, uniqueness under heavy collision, order independence, wrap-around probing, and same-tier stability. - 13 `turbopack-tests` execution fixtures under `tests/execution/turbopack/exports/mangle-*`, several ported from the original PR and from webpack's `test/configCases/mangle`: named imports, re-export chains and default exports (including one literally named `__esModule`), escaping namespaces (`Object.keys`, `delete ns.missing`, `export * as`, CJS interop), destructuring, prototype-shadowing names (`toString`, `$1`, `__1`), a 60-export two-character table, dynamic `import()` with `webpackExports` / `turbopackExports`, a CommonJS consumer of an ESM module, dynamic re-exports, scope hoisting on and off, and a control with the option off. - 3 committed snapshot fixtures under `tests/snapshot/mangle-exports`, so the emitted keys, the back-off, and the fixed single-export key are visible in review. - One fixture under `__skipped__`, which the harness asserts *fails*, recording the namespace-materialization gap below. - Full suite: 546 unit + 272 execution + 125 snapshot tests pass, with no snapshot churn across the several refactors this PR went through in review. - Verified against real builds: targeted app-dir, worker, WASM, and `@vercel/og`-based e2e suites pass with the option forced on (the failures that remain are external-network tests that fail identically with it off), and a small two-page app shrank by 0.76% of total emitted JS / 0.53% gzipped. ### Known limitations, each intentional - **A module read through `import * as ns` is never mangled**, even when every read is statically tracked, because the analysis doesn't yet distinguish a lowered named read from a materialized namespace object. Namespace imports are common, so this leaves real wins on the table; unlocking it is the highest-value follow-up. - **Escaping namespaces back off entirely.** Webpack instead keeps mangling and materializes a namespace object keyed by the original names. That is the `__skipped__` fixture: implementing it turns the suite red until the fixture is moved out. - **CommonJS export mangling is out of scope.** The producing side is reachable, but the consuming accesses live in user source and nothing rewrites them today; it needs its own design pass. Closes PACK-435 Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> --------- 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> --- crates/next-core/src/next_client/context.rs | 1 + .../ecmascript_client_reference_module.rs | 4 +- crates/next-core/src/next_config.rs | 11 + crates/next-core/src/next_server/context.rs | 1 + docs/01-app/03-api-reference/08-turbopack.mdx | 41 +- packages/next/src/server/config-schema.ts | 1 + packages/next/src/server/config-shared.ts | 10 + .../src/module_graph/binding_usage_info.rs | 30 + .../src/chunk/placeable.rs | 32 + .../src/collect_module.rs | 45 +- .../crates/turbopack-ecmascript/src/lib.rs | 4 + .../module_fragments/side_effects/module.rs | 3 +- .../src/references/emit_collect.rs | 28 +- .../src/references/esm/base.rs | 12 +- .../src/references/esm/export.rs | 21 +- .../src/references/esm/mangle/mod.rs | 151 ++++ .../src/references/esm/mangle/table.rs | 655 ++++++++++++++++++ .../src/references/esm/mod.rs | 2 + .../src/references/exports.rs | 4 + .../src/references/exports_info.rs | 38 +- .../turbopack-ecmascript/src/rename/module.rs | 6 + .../side_effect_optimization/facade/module.rs | 1 + .../side_effect_optimization/locals/module.rs | 1 + .../src/worker_chunk/module.rs | 28 +- .../crates/turbopack-tests/tests/execution.rs | 4 + .../input/enums.js | 8 + .../input/index.js | 45 ++ .../input/provider.js | 4 + .../options.json | 5 + .../exports/mangle-basic/input/index.js | 45 ++ .../mangle-basic/input/named-exports.js | 23 + .../exports/mangle-basic/options.json | 5 + .../mangle-cjs-consumer/input/cjs-consumer.js | 6 + .../exports/mangle-cjs-consumer/input/esm.js | 9 + .../mangle-cjs-consumer/input/index.js | 16 + .../exports/mangle-cjs-consumer/options.json | 6 + .../mangle-default/input/es-module-name.js | 10 + .../exports/mangle-default/input/index.js | 34 + .../exports/mangle-default/input/lib.js | 14 + .../exports/mangle-default/input/reexport.js | 3 + .../exports/mangle-default/options.json | 5 + .../mangle-destructuring/input/index.js | 32 + .../mangle-destructuring/input/module.js | 9 + .../exports/mangle-destructuring/options.json | 5 + .../input/dynamic-cjs.js | 5 + .../mangle-dynamic-exports/input/index.js | 16 + .../mangle-dynamic-exports/input/reexport.js | 5 + .../unexpected export __star__-aa3e7d.txt | 9 + .../mangle-dynamic-exports/options.json | 5 + .../input/cjs-provider.js | 5 + .../mangle-escaping-namespace/input/cjs.js | 2 + .../mangle-escaping-namespace/input/destr.js | 8 + .../mangle-escaping-namespace/input/enums.js | 8 + .../mangle-escaping-namespace/input/index.js | 57 ++ .../input/mangleable.js | 10 + .../input/provider.js | 5 + .../input/reexport.js | 2 + .../mangle-escaping-namespace/options.json | 5 + .../exports/mangle-hoisted/input/index.js | 20 + .../exports/mangle-hoisted/input/lazy.js | 3 + .../exports/mangle-hoisted/input/shared.js | 9 + .../exports/mangle-hoisted/options.json | 5 + .../mangle-magic-comments/input/index.js | 24 + .../mangle-magic-comments/input/lazy.js | 12 + .../mangle-magic-comments/options.json | 5 + .../mangle-many-exports/input/consume.js | 128 ++++ .../mangle-many-exports/input/generate.mjs | 45 ++ .../mangle-many-exports/input/index.js | 40 ++ .../mangle-many-exports/input/many-named.js | 123 ++++ .../exports/mangle-many-exports/input/many.js | 123 ++++ .../exports/mangle-many-exports/options.json | 5 + .../exports/mangle-minify-off/input/index.js | 37 + .../mangle-minify-off/input/named-exports.js | 23 + .../exports/mangle-minify-off/options.json | 5 + .../exports/mangle-object-prop/input/index.js | 38 + .../exports/mangle-object-prop/input/lib.js | 20 + .../exports/mangle-object-prop/options.json | 5 + .../exports/mangle-off/input/index.js | 35 + .../exports/mangle-off/input/named-exports.js | 23 + .../turbopack/exports/mangle-off/options.json | 5 + .../exports/mangle-reexport-chain/input/a.js | 15 + .../exports/mangle-reexport-chain/input/b.js | 6 + .../exports/mangle-reexport-chain/input/c.js | 12 + .../mangle-reexport-chain/input/index.js | 40 ++ .../mangle-reexport-chain/options.json | 5 + .../crates/turbopack-tests/tests/snapshot.rs | 4 + .../escaping-namespace/input/escaping.js | 3 + .../escaping-namespace/input/index.js | 6 + .../escaping-namespace/input/mangled.js | 3 + .../escaping-namespace/options.json | 12 + ...orts_escaping-namespace_input_1ysta13._.js | 3 + ..._escaping-namespace_input_1ysta13._.js.map | 1 + .../output/[turbopack]_runtime.js | 4 + .../output/[turbopack]_runtime.js.map | 5 + .../escaping-namespace/output/index.entry.js | 4 + .../output/index.entry.js.map | 5 + .../named-and-preserved/input/index.js | 15 + .../named-and-preserved/input/lib.js | 9 + .../named-and-preserved/options.json | 12 + ...rts_named-and-preserved_input_0pbreft._.js | 3 + ...named-and-preserved_input_0pbreft._.js.map | 1 + .../output/[turbopack]_runtime.js | 4 + .../output/[turbopack]_runtime.js.map | 5 + .../named-and-preserved/output/index.entry.js | 4 + .../output/index.entry.js.map | 5 + .../single-export/input/index.js | 3 + .../single-export/input/lone.js | 6 + .../mangle-exports/single-export/options.json | 12 + ...e-exports_single-export_input_1mf78g4._.js | 3 + ...ports_single-export_input_1mf78g4._.js.map | 1 + .../output/[turbopack]_runtime.js | 4 + .../output/[turbopack]_runtime.js.map | 5 + .../single-export/output/index.entry.js | 4 + .../single-export/output/index.entry.js.map | 5 + .../crates/turbopack-wasm/src/module_asset.rs | 4 +- .../turbopack/src/module_options/mod.rs | 2 + .../module_options/module_options_context.rs | 6 + 117 files changed, 2482 insertions(+), 52 deletions(-) create mode 100644 turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/mod.rs create mode 100644 turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/table.rs create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/input/enums.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/input/provider.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/named-exports.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/cjs-consumer.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/esm.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/es-module-name.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/lib.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/reexport.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/module.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/reexport.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/issues/unexpected export __star__-aa3e7d.txt create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs-provider.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/destr.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/enums.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/provider.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/reexport.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/lazy.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/shared.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/lazy.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/consume.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/generate.mjs create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many-named.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/named-exports.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/lib.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/named-exports.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/a.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/b.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/c.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/options.json create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js.map create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js create mode 100644 turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js.map diff --git a/crates/next-core/src/next_client/context.rs b/crates/next-core/src/next_client/context.rs index 39078f553e97..937d61fac645 100644 --- a/crates/next-core/src/next_client/context.rs +++ b/crates/next-core/src/next_client/context.rs @@ -376,6 +376,7 @@ pub async fn get_client_module_options_context( source_maps, infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?, cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?, + mangle_export_names: *next_config.turbopack_mangle_export_names().await?, cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?, cross_module_constants: *next_config.turbopack_cross_module_constants().await?, preset_env_config, diff --git a/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs b/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs index 6432e4d30c18..e4a8a534da53 100644 --- a/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs +++ b/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs @@ -285,7 +285,9 @@ impl ChunkableModule for EcmascriptClientReferenceModule { impl EcmascriptChunkPlaceable for EcmascriptClientReferenceModule { #[turbo_tasks::function] fn get_exports(self: Vc) -> Vc { - self.proxy_module().get_exports() + // Borrowed from the proxy module, a separate module identity, so they must not carry a + // mangling decision — see `EcmascriptExports::borrowed`. + self.proxy_module().get_exports().borrowed() } #[turbo_tasks::function] diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 48bc92785f46..bc055e5f56dd 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -1446,6 +1446,8 @@ pub struct ExperimentalConfig { turbopack_infer_module_side_effects: Option, /// Enable tree shaking of unused exports from static CommonJS modules. Defaults to false. turbopack_cjs_tree_shaking: Option, + /// Shorten ("mangle") the export names modules expose to each other. Defaults to false. + turbopack_mangle_export_names: Option, /// Enable scope hoisting of static CommonJS modules. Defaults to false. turbopack_cjs_scope_hoisting: Option, /// Enable cross-module constant inlining. Defaults to false. @@ -2587,6 +2589,15 @@ impl NextConfig { ) } + #[turbo_tasks::function] + pub fn turbopack_mangle_export_names(&self) -> Vc { + Vc::cell( + self.experimental + .turbopack_mangle_export_names + .unwrap_or(false), + ) + } + #[turbo_tasks::function] pub fn turbopack_cjs_scope_hoisting(&self) -> Vc { Vc::cell( diff --git a/crates/next-core/src/next_server/context.rs b/crates/next-core/src/next_server/context.rs index e0116eb344e2..563351cb8a5a 100644 --- a/crates/next-core/src/next_server/context.rs +++ b/crates/next-core/src/next_server/context.rs @@ -551,6 +551,7 @@ pub async fn get_server_module_options_context( source_maps, infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?, cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?, + mangle_export_names: *next_config.turbopack_mangle_export_names().await?, cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?, cross_module_constants: *next_config.turbopack_cross_module_constants().await?, ..Default::default() diff --git a/docs/01-app/03-api-reference/08-turbopack.mdx b/docs/01-app/03-api-reference/08-turbopack.mdx index f76f46cbb6a7..318765dc7c82 100644 --- a/docs/01-app/03-api-reference/08-turbopack.mdx +++ b/docs/01-app/03-api-reference/08-turbopack.mdx @@ -422,26 +422,27 @@ Turbopack can be configured via `next.config.js` (or `next.config.ts`) under the Additionally, the following experimental options are available under `experimental` in `next.config.js`: -| Option | Description | Default (dev) | Default (build) | -| ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------------------- | -| [`turbopackFileSystemCacheForDev`](/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache) | Enable filesystem cache for the dev server. | `true` | N/A | -| [`turbopackFileSystemCacheForBuild`](/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache) | Enable filesystem cache for builds. | N/A | `true`1 | -| `turbopackMinify` | Enable minification. Accepts a boolean, or `{ server, client, edge }` to configure each environment separately. | `false` | `true` | -| `turbopackSourceMaps` | Enable source maps. | `true` | `productionBrowserSourceMaps` | -| `turbopackInputSourceMaps` | Enable extraction of source maps from input files. | `true` | `true` | -| `turbopackModuleFragments` | Currently in active development. This splits modules into fragments and chunks only import the used fragments of the modules. | `false` | `false` | -| `turbopackRemoveUnusedImports` | Enable removing unused imports. Requires `turbopackRemoveUnusedExports`. | `false` | `true` | -| `turbopackRemoveUnusedExports` | Enable removing unused exports. | `false` | `true` | -| `turbopackInferModuleSideEffects` | Enable local analysis to infer side-effect-free modules for better tree shaking. | `true` | `true` | -| `turbopackScopeHoisting` | Enable scope hoisting. Always disabled in dev mode. | `false` | `true` | -| `turbopackClientSideNestedAsyncChunking` | Enable nested async chunking for client-side assets. | `false` | `true` | -| `turbopackServerSideNestedAsyncChunking` | Enable nested async chunking for server-side assets. | `false` | `false` | -| `turbopackImportTypeBytes` | Enable support for `with {type: "bytes"}` for ESM imports. | `false` | `false` | -| `turbopackUseBuiltinBabel` | Enable automatic Babel loader configuration when a Babel config file is present. | `true` | `true` | -| `turbopackUseBuiltinSass` | Enable automatic Sass loader configuration. | `true` | `true` | -| `turbopackModuleIds` | Module ID strategy: `'named'` or `'deterministic'`. | `'named'` | `'deterministic'` | -| [`turbopackLocalPostcssConfig`](/docs/app/api-reference/config/next-config-js/turbopackLocalPostcssConfig) | Resolve `postcss.config.js` from the CSS file's directory first, then the project root. | `false` | `false` | -| `turbopackWorkerAssetPrefix` | Custom asset prefix for Web Worker URLs (entrypoint + module chunks), overriding `assetPrefix`. Mirrors webpack's `output.workerPublicPath`. | `undefined` | `undefined` | +| Option | Description | Default (dev) | Default (build) | +| ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ----------------------------- | +| [`turbopackFileSystemCacheForDev`](/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache) | Enable filesystem cache for the dev server. | `true` | N/A | +| [`turbopackFileSystemCacheForBuild`](/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache) | Enable filesystem cache for builds. | N/A | `true`1 | +| `turbopackMinify` | Enable minification. Accepts a boolean, or `{ server, client, edge }` to configure each environment separately. | `false` | `true` | +| `turbopackSourceMaps` | Enable source maps. | `true` | `productionBrowserSourceMaps` | +| `turbopackInputSourceMaps` | Enable extraction of source maps from input files. | `true` | `true` | +| `turbopackModuleFragments` | Currently in active development. This splits modules into fragments and chunks only import the used fragments of the modules. | `false` | `false` | +| `turbopackRemoveUnusedImports` | Enable removing unused imports. Requires `turbopackRemoveUnusedExports`. | `false` | `true` | +| `turbopackRemoveUnusedExports` | Enable removing unused exports. | `false` | `true` | +| `turbopackMangleExportNames` | Shorten the export names modules expose to each other, to reduce bundle size. A module whose export names can be observed (an escaping namespace object, a dynamic `import()`, a CommonJS `require()`) keeps its original names. | `false` | `false` | +| `turbopackInferModuleSideEffects` | Enable local analysis to infer side-effect-free modules for better tree shaking. | `true` | `true` | +| `turbopackScopeHoisting` | Enable scope hoisting. Always disabled in dev mode. | `false` | `true` | +| `turbopackClientSideNestedAsyncChunking` | Enable nested async chunking for client-side assets. | `false` | `true` | +| `turbopackServerSideNestedAsyncChunking` | Enable nested async chunking for server-side assets. | `false` | `false` | +| `turbopackImportTypeBytes` | Enable support for `with {type: "bytes"}` for ESM imports. | `false` | `false` | +| `turbopackUseBuiltinBabel` | Enable automatic Babel loader configuration when a Babel config file is present. | `true` | `true` | +| `turbopackUseBuiltinSass` | Enable automatic Sass loader configuration. | `true` | `true` | +| `turbopackModuleIds` | Module ID strategy: `'named'` or `'deterministic'`. | `'named'` | `'deterministic'` | +| [`turbopackLocalPostcssConfig`](/docs/app/api-reference/config/next-config-js/turbopackLocalPostcssConfig) | Resolve `postcss.config.js` from the CSS file's directory first, then the project root. | `false` | `false` | +| `turbopackWorkerAssetPrefix` | Custom asset prefix for Web Worker URLs (entrypoint + module chunks), overriding `assetPrefix`. Mirrors webpack's `output.workerPublicPath`. | `undefined` | `undefined` | **Table notes:** diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index e50c6fcf8a44..1cb970fee817 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -423,6 +423,7 @@ export const experimentalSchema = { turbopackModuleIds: z.enum(['named', 'deterministic']).optional(), turbopackInferModuleSideEffects: z.boolean().optional(), turbopackCjsTreeShaking: z.boolean().optional(), + turbopackMangleExportNames: z.boolean().optional(), turbopackCjsScopeHoisting: z.boolean().optional(), turbopackCrossModuleConstants: z.boolean().optional(), turbopackServerFastRefresh: z.boolean().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index d4d39625cd1c..7c69f5125e30 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -1011,6 +1011,16 @@ export interface ExperimentalConfig { */ turbopackCjsTreeShaking?: boolean + /** + * Shorten ("mangle") the export names modules expose to each other in Turbopack, to reduce + * bundle size. Only affects the keys used to link modules together: a module whose export names + * can be observed by user code (a namespace object that escapes, a dynamic `import()`, a + * CommonJS `require()`) keeps its original names. + * + * Defaults to `false` + */ + turbopackMangleExportNames?: boolean + /** * Enable scope hoisting of static CommonJS modules. * 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 bd808340f541..8c1d110f2924 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 @@ -23,6 +23,18 @@ pub struct UsedExportsMap(FxHashMap>, ModuleExportUsa #[turbo_tasks::value(transparent, cell = "keyed")] pub struct ExportCircuitBreakers(FxHashSet>>); +/// Modules that are read through a *partial namespace object* — an +/// [`ExportUsage::PartialNamespaceObject`] edge points at them. +/// +/// The used export names are still known individually (that is why the usage stays +/// [`ModuleExportUsageInfo::Exports`]), but the reads went through a namespace value: a namespace +/// binding's member reads or destructuring, or the object a dynamic `import()` resolves to. Some of +/// those reads are lowered to direct named accesses and some are not, and this set does not +/// distinguish them — so a consumer that wants to rename the module's export keys has to assume the +/// original names may still be read somewhere, and leave them alone. +#[turbo_tasks::value(transparent, cell = "keyed")] +pub struct PartialNamespaceModules(FxHashSet>>); + #[turbo_tasks::value] #[derive(Clone, Default, Debug)] pub struct BindingUsageInfo { @@ -32,6 +44,7 @@ pub struct BindingUsageInfo { used_exports: ResolvedVc, export_circuit_breakers: ResolvedVc, + partial_namespace_modules: ResolvedVc, } #[turbo_tasks::value(transparent)] @@ -42,6 +55,9 @@ pub struct ModuleExportUsage { pub export_usage: ResolvedVc, // Whether this module exists in an import cycle and has been selected to break the cycle. pub is_circuit_breaker: bool, + /// Whether this module is read through a namespace value somewhere, which means one of those + /// reads may still use an original export name. See [`PartialNamespaceModules`]. + pub namespace_object_may_escape: bool, } #[turbo_tasks::value_impl] impl ModuleExportUsage { @@ -50,6 +66,7 @@ impl ModuleExportUsage { Ok(Self { export_usage: ModuleExportUsageInfo::all().to_resolved().await?, is_circuit_breaker: true, + namespace_object_may_escape: true, } .cell()) } @@ -78,9 +95,12 @@ impl BindingUsageInfo { bail!("export usage not found for module: {ident:?}"); }; + let namespace_object_may_escape = + self.partial_namespace_modules.contains_key(&module).await?; Ok(ModuleExportUsage { export_usage: (*exports).clone().resolved_cell(), is_circuit_breaker, + namespace_object_may_escape, } .cell()) } @@ -108,6 +128,7 @@ pub async fn compute_binding_usage_info( async move { let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default(); + let mut partial_namespace_modules = FxHashSet::default(); #[cfg(debug_assertions)] let mut debug_unused_references_name = FxHashSet::<( ResolvedVc>, @@ -246,6 +267,14 @@ pub async fn compute_binding_usage_info( let entry = used_exports.entry(target); let is_first_visit = matches!(entry, Entry::Vacant(_)); + if matches!( + &ref_data.binding_usage.export, + ExportUsage::PartialNamespaceObject(_) + ) { + // `target` is read through a namespace value. We know which names are used, but + // not that every read of them was lowered to a direct named access. + partial_namespace_modules.insert(target); + } if entry.or_default().add(&ref_data.binding_usage.export) || is_first_visit { // First visit, or the used exports changed. This can cause more imports to get // used downstream. @@ -336,6 +365,7 @@ pub async fn compute_binding_usage_info( unused_references_edges, used_exports: ResolvedVc::cell(used_exports), export_circuit_breakers: ResolvedVc::cell(export_circuit_breakers), + partial_namespace_modules: ResolvedVc::cell(partial_namespace_modules), } .cell()) } diff --git a/turbopack/crates/turbopack-ecmascript/src/chunk/placeable.rs b/turbopack/crates/turbopack-ecmascript/src/chunk/placeable.rs index f1c0e1302e45..417eab76a9be 100644 --- a/turbopack/crates/turbopack-ecmascript/src/chunk/placeable.rs +++ b/turbopack/crates/turbopack-ecmascript/src/chunk/placeable.rs @@ -288,6 +288,38 @@ pub enum EcmascriptExports { #[turbo_tasks::value_impl] impl EcmascriptExports { + /// A view of these exports for a module that *borrows* them from another module, i.e. whose + /// `get_exports` hands out the exports value of some other module verbatim. + /// + /// Export mangling is decided per module: the producing side keys on the module whose code + /// generation emits the export object, and the consuming side on the module it imports from. + /// An exports value shared by two module identities would let those two sides compute + /// different keys for the same export, so a borrowed view is always unmangled — which both + /// sides agree on. + #[turbo_tasks::function] + pub async fn borrowed(self: Vc) -> Result> { + let this = self.await?; + Ok(match &*this { + EcmascriptExports::EsmExports(exports) => { + let exports = exports.await?; + if !exports.mangle_export_names { + return Ok(self); + } + EcmascriptExports::EsmExports( + EsmExports { + exports: exports.exports.clone(), + star_exports: exports.star_exports.clone(), + mangle_export_names: false, + } + .resolved_cell(), + ) + .cell() + } + // Nothing else carries a mangling decision. + _ => self, + }) + } + /// Returns whether this module should be split into separate locals and facade modules. /// /// Splitting is enabled when the module has re-exports (star exports or imported bindings), diff --git a/turbopack/crates/turbopack-ecmascript/src/collect_module.rs b/turbopack/crates/turbopack-ecmascript/src/collect_module.rs index 7cad88609cf4..14393d592d1c 100644 --- a/turbopack/crates/turbopack-ecmascript/src/collect_module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/collect_module.rs @@ -22,11 +22,20 @@ use crate::{ EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports, ecmascript_chunk_item, }, - references::esm::{EsmExport, EsmExports, Liveness}, + references::esm::{EsmExport, EsmExports, Liveness, mangle::generated_export_key}, runtime_functions::{TURBOPACK_ESM, TURBOPACK_IMPORT}, utils::StringifyJs, }; +/// The single export of the generated collect module. +/// +/// Both the producer below and the read side in `references::emit_collect` resolve the key this is +/// actually *emitted* under through `generated_export_key`, rather than assuming the source name, +/// so the two cannot drift apart. Today that always resolves back to this name — see +/// `get_exports` for why — but wiring it through means mangling starts applying to both sides +/// together the moment it becomes possible, with no further change here. +pub const COLLECT_LIST_EXPORT: RcStr = rcstr!("getList"); + #[turbo_tasks::value] pub struct EcmascriptCollectModule { parent_module: ResolvedVc>, @@ -165,11 +174,20 @@ impl EcmascriptChunkPlaceable for EcmascriptCollectModuleWithChunkGroup { EcmascriptExports::EsmExports( EsmExports { exports: [( - "getList".into(), - EsmExport::LocalBinding(rcstr!("getList"), Liveness::Constant), + COLLECT_LIST_EXPORT, + EsmExport::LocalBinding(COLLECT_LIST_EXPORT, Liveness::Constant), )] .into(), star_exports: vec![], + // This module is code-generated but never referenced in the module graph (it is + // reached through `collected_modules`), so it has no entry in the graph's + // used-export map and `module_export_usage` cannot answer for it — the same class + // of module as the wasm loader and the client-reference proxy, which + // `BindingUsageInfo::used_exports` special-cases by ident with a `TODO fix these + // cases`. Mangling therefore cannot be computed here at all, rather than being + // merely undesirable. Keeping this `false` makes `mangled_export_names` answer + // before it consults the graph; flip it once this module is graph-tracked. + mangle_export_names: false, } .resolved_cell(), ) @@ -204,15 +222,16 @@ impl EcmascriptChunkPlaceable for EcmascriptCollectModuleWithChunkGroup { #[turbo_tasks::function] async fn chunk_item_content( - &self, + self: Vc, chunking_context: Vc>, module_graph: Vc, _async_module_info: Option>, _estimated: bool, ) -> Result> { + let this = self.await?; let chunk_item_id_strategy = chunking_context.chunk_item_id_strategy().await?; - let entries = self + let entries = this .entry_chunk_group .await? .into_iter() @@ -220,7 +239,7 @@ impl EcmascriptChunkPlaceable for EcmascriptCollectModuleWithChunkGroup { let collected_modules = module_graph.collected_modules(); let items = collected_modules - .get(&ResolvedVc::upcast(self.module)) + .get(&ResolvedVc::upcast(this.module)) .await?; let items = items .iter() @@ -262,11 +281,19 @@ impl EcmascriptChunkPlaceable for EcmascriptCollectModuleWithChunkGroup { code += "]);"; code += "function getList() { return data; }"; + // The *key* this is exposed under may be mangled; the local binding keeps its own name. + // `references::emit_collect` resolves the same key for the read side, so both agree. + let export = generated_export_key( + ResolvedVc::upcast(self.to_resolved().await?), + chunking_context, + &COLLECT_LIST_EXPORT, + ) + .await?; + writeln!( code, - "{TURBOPACK_ESM}([ - 'getList', ()=>getList -]);" + "{TURBOPACK_ESM}([\n {}, ()=>getList\n]);", + StringifyJs(&export), )?; Ok(EcmascriptChunkItemContent { diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index 4a0e8df7976a..748c46fa0dfa 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -242,6 +242,10 @@ pub struct EcmascriptOptions { pub infer_module_side_effects: bool, /// Whether to tree shake unused exports from static CommonJS modules. Defaults to false. pub cjs_tree_shaking: bool, + /// Whether to shorten ("mangle") the export names this module exposes to other modules, to + /// reduce output size. Defaults to false. See + /// `references::esm::mangle::mangled_export_names`. + pub mangle_export_names: bool, /// Whether to scope hoist static CommonJS modules. Defaults to false. pub cjs_scope_hoisting: bool, /// Whether to enable cross-module constant inlining. Defaults to false. diff --git a/turbopack/crates/turbopack-ecmascript/src/module_fragments/side_effects/module.rs b/turbopack/crates/turbopack-ecmascript/src/module_fragments/side_effects/module.rs index 35a35cf43994..5d7428d3d8dc 100644 --- a/turbopack/crates/turbopack-ecmascript/src/module_fragments/side_effects/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/module_fragments/side_effects/module.rs @@ -134,7 +134,8 @@ impl Module for SideEffectsModule { impl EcmascriptChunkPlaceable for SideEffectsModule { #[turbo_tasks::function] fn get_exports(&self) -> Vc { - self.resolved_as.get_exports() + // Borrowed from another module identity, so it must not carry a mangling decision. + self.resolved_as.get_exports().borrowed() } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack-ecmascript/src/references/emit_collect.rs b/turbopack/crates/turbopack-ecmascript/src/references/emit_collect.rs index 2b658797d0d7..f141917f117d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/emit_collect.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/emit_collect.rs @@ -1,8 +1,8 @@ use anyhow::{Result, bail}; use bincode::{Decode, Encode}; use swc_core::{ - ecma::ast::{Expr, Invalid}, - quote, + common::DUMMY_SP, + ecma::ast::{Expr, IdentName, Invalid, MemberExpr, MemberProp}, }; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ @@ -28,10 +28,11 @@ use turbopack_resolve::ecmascript::esm_resolve; use crate::{ analyzer::imports::ImportAnnotations, code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference}, - collect_module::EcmascriptCollectModule, + collect_module::{COLLECT_LIST_EXPORT, EcmascriptCollectModule}, create_visitor, references::{ AstPath, + esm::{base::ReferencedAsset, mangle::generated_export_key}, pattern_mapping::{PatternMapping, ResolveType}, removal::RemovalCodeGen, }, @@ -253,14 +254,27 @@ impl CollectReferenceCodeGen { .await?; let mut visitors = Vec::new(); + // The collect module's own `getList` export is mangled like any other, so resolve the key + // it is actually emitted under rather than hard-coding the source name. Both sides ask the + // same module, so they always agree; when that module keeps its original names this is + // just `getList` again. + let export = + match ReferencedAsset::from_resolve_result(self.reference.resolve_reference()).await? { + ReferencedAsset::Some(module) => { + generated_export_key(module, chunking_context, &COLLECT_LIST_EXPORT).await? + } + _ => COLLECT_LIST_EXPORT, + }; + visitors.push(create_visitor!( self.path, visit_mut_expr, |expr: &mut Expr| { - *expr = quote!( - "$v.getList" as Expr, - v: Expr = pm.create_require(Expr::Invalid(Invalid::default())) - ); + *expr = Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(pm.create_require(Expr::Invalid(Invalid::default()))), + prop: MemberProp::Ident(IdentName::new(export.as_str().into(), DUMMY_SP)), + }); } )); diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs index 5dd2fb8402e0..f5f1ca0bb83a 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -51,6 +51,7 @@ use crate::{ esm::{ EsmExport, export::{all_known_export_names, is_export_missing}, + mangle::generated_export_key, }, util::{SpecifiedChunkingType, throw_module_not_found_expr}, }, @@ -308,7 +309,16 @@ impl ReferencedAsset { Some(ReferencedAssetIdent::Module { namespace_ident: import_source.get_namespace_ident(chunking_context).await?, ctxt: None, - export, + // The target module may emit its exports under shortened keys. This is the + // only place a cross-module export access is materialized, and it resolves + // the same map the producing module uses (see + // `EsmExports::code_generation`), so the two always agree. + export: match &export { + Some(export) => { + Some(generated_export_key(*asset, chunking_context, export).await?) + } + None => None, + }, import_source, }) } diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs index 3dc2cc3644d5..a9ab98d89ff5 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/export.rs @@ -33,7 +33,7 @@ use crate::{ code_gen::{CodeGeneration, CodeGenerationHoistedStmt}, magic_identifier::MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM, module_fragments::part::module::EcmascriptModulePartAsset, - references::esm::base::ReferencedAsset, + references::esm::{base::ReferencedAsset, mangle::mangled_export_names}, runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM}, utils::module_id_to_lit, }; @@ -533,6 +533,10 @@ pub struct EsmExports { pub exports: FrozenMap, /// Unexpanded `export * from ...` statements (expanded in `expand_star_exports`) pub star_exports: Vec>>, + /// Whether the keys these exports are emitted under may be shortened. Carried with the exports + /// so a module deriving its exports from another (facade, locals, part, rename) inherits it. + /// `mangle::mangled_export_names` decides whether they actually are. + pub mangle_export_names: bool, } /// The expanded version of [`EsmExports`], the `exports` field here includes all exports that could @@ -571,6 +575,9 @@ impl EsmExports { EsmExports { exports: FrozenMap::from(exports), star_exports: vec![module_reference], + // These facades exist so that a host framework can find the wrapped module's + // exports by name, so their keys have to stay as written. + mangle_export_names: false, } .resolved_cell(), ) @@ -697,6 +704,9 @@ impl EsmExports { } let mut getters = Vec::new(); + // The keys this module's exports are emitted under. Consumers resolve the same map for this + // module (see `ReferencedAsset::get_ident_inner`), so both sides always agree. + let mangled_names = mangled_export_names(*module, chunking_context).await?; for (exported, local) in &expanded.exports { let exprs: ExportBinding = match local { EsmExport::Error => ExportBinding::Getter(quote!( @@ -839,7 +849,14 @@ impl EsmExports { getters.push(Some( Expr::Lit(Lit::Str(Str { span: DUMMY_SP, - value: exported.as_str().into(), + // The key this export is emitted under: the mangled one when this module's + // names are shortened, otherwise the original. + value: mangled_names + .as_ref() + .and_then(|names| names.get(exported)) + .unwrap_or(exported) + .as_str() + .into(), raw: None, })) .into(), diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/mod.rs new file mode 100644 index 000000000000..8e5d0e081e45 --- /dev/null +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/mod.rs @@ -0,0 +1,151 @@ +//! Shortening ("mangling") of the export names a module exposes to other modules. +//! +//! A module's export keys usually only exist to link modules together: the producing module emits +//! `{ someVeryLongExportName: () => … }` and every consumer reads +//! `ns["someVeryLongExportName"]`. Both sides are generated by us, so the key can be replaced by a +//! much shorter one — as long as producer and consumer agree, and as long as the name is not +//! observable from user code. +//! +//! [`mangled_export_names`] is the single source of truth for that mapping. Both the producer +//! ([`super::export::EsmExports::code_generation`]) and the consumer +//! ([`super::base::ReferencedAsset`]) ask it for the *target* module's map, so they cannot +//! disagree. It returns an empty map whenever the module has to keep its original names — the +//! same effect as mangling every name to itself, so callers never need to branch on "did mangling +//! even apply here", only look a name up and fall back to itself. +//! +//! Code generation that spells an export access out **as a string** — rather than going through +//! `ReferencedAssetIdent`, which handles this automatically — must resolve its key through +//! [`generated_export_key`]. A hard-coded `["someExport"]` in generated source silently misses as +//! soon as the target module's exports are mangled. + +mod table; + +use anyhow::Result; +use turbo_frozenmap::FrozenMap; +use turbo_rcstr::RcStr; +use turbo_tasks::{ResolvedVc, Vc}; +use turbopack_core::{ + chunk::ChunkingContext, module_graph::binding_usage_info::ModuleExportUsageInfo, +}; + +use self::table::shorten_to_unique_names; +use crate::chunk::{EcmascriptChunkPlaceable, EcmascriptExports}; + +/// A module's export name mapping: original export name -> the key actually used in the output. +/// +/// `None` means the module is not eligible for mangling at all and keeps every original name (see +/// [`mangled_export_names`] for the conditions). `Some` covers only the exports that were actually +/// *renamed* — an export that kept its own name because it was already short enough, or because it +/// lost every hash collision, is not a key in the map. So a missing key always means "use the +/// original name", and callers that only want the correct output key can ignore the distinction +/// entirely (see [`generated_export_key`]); the `canMangle` reporting in +/// [`crate::references::exports_info`] is the one caller that needs to tell "not eligible" from +/// "eligible but unchanged", which is exactly what the `Option` gives it. +#[turbo_tasks::value(transparent)] +pub struct MangledExportNames(pub Option>); + +/// Computes the mangled export names of `module`, or `None` when its exports must keep their +/// original names. +/// +/// Both the producing and the consuming side call this for the same `module`, which is what +/// guarantees they agree. The mapping covers the module's *used* exports: unused exports are not +/// emitted at all, so giving them names would only make the remaining names longer. +/// +/// A module keeps its original names when any of these hold: +/// +/// 1. mangling is disabled for the module that owns these exports, +/// 2. its export usage is [`ModuleExportUsageInfo::All`] — a namespace import, a computed property +/// access, an unresolvable `export *`, or a module referenced from outside the module graph +/// (entries, which are seeded with `All`). This is what a plain `import * as ns` reaches in +/// practice, whether or not the namespace object escapes: the reads are not enumerated, so the +/// usage widens to `All`. Note that widening propagates, so splitting such a module into a +/// facade and a locals module does not rescue the locals module, +/// 3. it is read through a namespace value whose used names *are* known — in practice a dynamic +/// `import()` with an enumerated export list (`import(/* webpackExports: [...] */ "…")`). We +/// know which names are used, but not that every read of them was lowered to a direct named +/// access, so an original name may still be read by user code, +/// 4. its exports are not statically known ECMAScript exports, or contain dynamic re-exports. +/// +/// None of these depend on the individual export, so "is `e` a candidate for mangling" is exactly +/// "is this `Some`, and is `e` used" — the per-name step (hashing into a short identifier) never +/// itself excludes a name. +#[turbo_tasks::function] +pub async fn mangled_export_names( + module: ResolvedVc>, + chunking_context: Vc>, +) -> Result> { + // (4) Only statically known ESM exports can be renamed. + let EcmascriptExports::EsmExports(exports) = *module.get_exports().await? else { + return Ok(Vc::cell(None)); + }; + + // (1) Disabled for the module these exports belong to. Deliberately independent of whether + // this build minifies at all: mangling and minification are two separate concerns, and a + // caller may want either without the other. + if !exports.await?.mangle_export_names { + return Ok(Vc::cell(None)); + } + + let usage = chunking_context + .module_export_usage(*ResolvedVc::upcast(module)) + .await?; + + // (3) An original name may still be read through a namespace value. + if usage.namespace_object_may_escape { + return Ok(Vc::cell(None)); + } + + // (2) We don't know which exports are used, so we don't know that all uses are ours. + let usage_info = usage.export_usage.await?; + let ModuleExportUsageInfo::Exports(used) = &*usage_info else { + return Ok(Vc::cell(None)); + }; + if used.is_empty() { + return Ok(Vc::cell(None)); + } + + // (4) `export * from "./some-dynamic-cjs"` is resolved at runtime by property access on the + // original names, and a used set that resolves to no actual export at all leaves nothing to + // mangle. + let expanded = exports.expand_exports(*usage.export_usage).await?; + if !expanded.dynamic_exports.is_empty() || expanded.exports.is_empty() { + return Ok(Vc::cell(None)); + } + + // Every emitted export is mangled, `default` and `__esModule` included. + // + // Neither is a runtime contract *as an export name*. The runtime paths that look `'default'` up + // by name — the CommonJS interop in `esmNamespaceObject` and the proxy traps behind + // `ensureDynamicExports` — only apply to CommonJS targets and to modules with dynamic + // re-exports, and both already keep their original names via the checks above. `__esModule` is + // how a CommonJS module signals interop *to* ESM, so an ESM module that happens to export that + // name is just an ordinary export. `__esModule` still cannot be *assigned* to some other + // export, because the runtime defines that property itself; see `RESERVED_KEYS` in `table`. + Ok(Vc::cell(Some(shorten_to_unique_names( + expanded.exports.keys(), + )))) +} + +/// The key that generated code has to use to read `export` from `module` — the mangled key when +/// the module's exports are mangled, and `export` itself otherwise. +/// +/// **Every** piece of code generation that materializes an export access as a string has to get its +/// key from here, or the access will miss when the module is mangled. That includes generated +/// source text (`__turbopack_require__(id)["default"](…)` and friends) as well as AST built by +/// hand. Code that goes through `ReferencedAssetIdent` (see `super::base`) is already covered. +/// +/// `module` must be the module that actually *produces* the export. For a re-export, resolve to the +/// producing module first (as `ReferencedAsset::get_ident_inner` does), because each module mangles +/// its own keys independently. +pub async fn generated_export_key( + module: ResolvedVc>, + chunking_context: Vc>, + export: &RcStr, +) -> Result { + let names = mangled_export_names(*module, chunking_context).await?; + Ok(names + .as_ref() + .and_then(|names| names.get(export)) + .cloned() + .unwrap_or_else(|| export.clone())) +} diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/table.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/table.rs new file mode 100644 index 000000000000..baa14b718759 --- /dev/null +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/mangle/table.rs @@ -0,0 +1,655 @@ +//! Deterministic assignment of short, valid JS identifiers to a set of names. +//! +//! Used to shorten module export keys: the keys only exist to link modules together, so as long as +//! producer and consumer agree on the same short key, the original (possibly very long) name never +//! has to appear in the output. +//! +//! The mapping is built so that names stay **stable** across unrelated changes — a plain sequential +//! assignment (`a`, `b`, `c`, …) would renumber everything whenever a name is added or removed. +//! Instead each name is hashed into a table of all valid identifiers of the chosen length, and +//! collisions are resolved by open addressing (take the next free bucket, wrapping around). With +//! many collisions this degrades to sequential assignment, but in the common case a name's short +//! form only depends on the name itself and the size of the table. +//! +//! The assigned names are only ever emitted as **property keys** — object-literal keys in the +//! generated export table, and bracket-access strings at the consuming side. They are never used as +//! bare binding identifiers (the merged / scope-hoisted path refers to the module's own local +//! variables, not to these keys), so a name that happens to spell a reserved word like `if` or `in` +//! is perfectly legal. It is avoided anyway — see `RESERVED_KEYS` in the parent module — because a +//! downstream minifier that folds `ns["name"]` into the shorter `ns.name` typically only does so +//! for a non-reserved identifier, so a keyword-shaped key never gets that benefit. + +use rustc_hash::FxHashSet; +use turbo_frozenmap::FrozenMap; +use turbo_rcstr::{RcStr, rcstr}; +use turbo_tasks_hash::hash_xxh3_hash64; + +/// Characters that may start an identifier (i.e. no digits). +const FIRST_CHARS: &[u8; 54] = b"_$ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +/// Characters that may appear in an identifier after the first character. +const REST_CHARS: &[u8; 64] = b"_$0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +/// The name assigned when a module has exactly one export to mangle. +/// +/// Always picking the same character compresses better than hashing would: `f` is the most common +/// character in JS keywords (`if`, `for`, `function`), and every single-export module in the graph +/// then emits the same `.f` / `.f()` byte sequences, which gzip's back-references pick up across +/// the whole bundle. The cost is that going from one export to two renames this one; that churn is +/// accepted deliberately in exchange for the compression. +const SINGLE_ITEM_IDENTIFIER: RcStr = rcstr!("f"); + +/// Keys that are never handed out, for two different reasons. +/// +/// The reserved words are legal as quoted keys, but a minifier will not fold `ns["if"]` into +/// `ns.if`, so handing one out costs bytes where any other key of the same length saves them. Only +/// two- and three-character words are listed: no single character is a reserved word, and by four +/// characters the table holds 200k+ buckets, so losing a name there is irrelevant. +/// +/// `__esModule` is a correctness case instead: `esm()` in the runtime does +/// `defineProp(exports, '__esModule', …)` for every ESM module, so that property is on the exports +/// object whatever the module's own exports are called, and an assigned key would collide with it. +/// (`default` needs no such treatment once it is mangled: nothing then emits a property under that +/// name, and the runtime paths that read it by name only see modules that keep their original +/// names.) +/// +/// [`reserved_in_table`] has to be kept in step with this list — there is a test for that. +const RESERVED_KEYS: &[&str] = &[ + // 2-character reserved words + "do", + "if", + "in", + // 3-character reserved words. `let` counts: the output is a module, and modules are strict. + "for", + "let", + "new", + "try", + "var", + // Defined on every module's exports object by the runtime, whatever the module's own exports + // are called, so an assigned key must not land on it. + "__esModule", +]; + +/// How many of [`RESERVED_KEYS`] occupy a bucket in a table of `len`-character identifiers. A key +/// only takes a bucket once the table is wide enough to hold it, so this is a running count by +/// length: nothing at one character, the three two-letter words at two, all eight reserved words +/// from three, and `__esModule` from ten. +const fn reserved_in_table(len: u32) -> u64 { + match len { + 0 | 1 => 0, + 2 => 3, + 3..=9 => 8, + _ => 9, + } +} + +/// The number of distinct values encodable in at most `len` characters, i.e. the capacity of the +/// table for that length. +/// +/// There are `54 * 64^(i-1)` strings of length `i` — the first character comes from the 54 that may +/// start an identifier, every later one from all 64 — so this is that summed over `1..=len`: +/// +/// ```text +/// capacity(len) = 54 * (64^len - 1) / 63 +/// ``` +/// +/// Precomputed rather than evaluated on the fly, because [`decode_js_identifier`] needs it for +/// every name it decodes and the formula costs a `pow` and a multiply. +/// [`capacity_for_len_matches_the_formula`] keeps the table honest. From `len == 11` the true value +/// exceeds `u64::MAX` (`54 * 64^10` alone is ≈ 6.2e19 against `u64::MAX`'s ≈ 1.8e19), so it +/// saturates there; no real export table comes anywhere near needing that many characters. +fn capacity_for_len(len: u32) -> u64 { + CAPACITY_FOR_LEN + .get(len as usize) + .copied() + .unwrap_or(u64::MAX) +} + +/// See [`capacity_for_len`]. +const CAPACITY_FOR_LEN: [u64; 11] = [ + 0, + 54, + 3_510, + 224_694, + 14_380_470, + 920_350_134, + 58_902_408_630, + 3_769_754_152_374, + 241_264_265_751_990, + 15_440_913_008_127_414, + 988_218_432_520_154_550, +]; + +/// Encodes a value to a valid JS identifier. Always returns at least one character. +/// +/// Values are partitioned by the length of their encoding: the lowest [`FIRST_CHARS`]`.len()` +/// values are the one-character strings, the next `54 * 64` are the two-character ones, and so on. +/// Peeling those groups off in order turns `value` into a plain index within its own length, which +/// is then written out most-significant character first. +fn encode_js_identifier(mut value: u64) -> String { + let mut len = 1usize; + let mut in_this_len = FIRST_CHARS.len() as u64; + while value >= in_this_len { + value -= in_this_len; + len += 1; + in_this_len = in_this_len.saturating_mul(REST_CHARS.len() as u64); + } + + let mut result = vec![0u8; len]; + // Fill from the tail, so the last character is the least significant digit. + for slot in result[1..].iter_mut().rev() { + *slot = REST_CHARS[(value % REST_CHARS.len() as u64) as usize]; + value /= REST_CHARS.len() as u64; + } + // Whatever is left is below `FIRST_CHARS.len()`, by construction of the length groups above. + result[0] = FIRST_CHARS[value as usize]; + + // SAFETY: FIRST_CHARS and REST_CHARS only contain ASCII + unsafe { String::from_utf8_unchecked(result) } +} + +/// Decodes an identifier back to the value [`encode_js_identifier`] would encode. +/// +/// Returns `None` only when the string contains a character outside the alphabets, or is too long +/// to be a `u64`. Every other non-empty string is the encoding of exactly one value: accumulating +/// `value * base + digit` and then adding the values taken by all shorter strings means a trailing +/// `_` is a digit like any other rather than a leading zero, so there are no degenerate encodings +/// to reject. That bijection is what lets an existing short export name keep itself without ever +/// colliding with a name assigned to something else. +/// +/// Panics on an empty string: export names are never empty, so that would be a caller bug rather +/// than an un-decodable name. +fn decode_js_identifier(s: &str) -> Option { + let bytes = s.as_bytes(); + let (&first, rest) = bytes + .split_first() + .expect("identifiers are never empty, so neither are export names"); + + let mut value = FIRST_CHARS.iter().position(|&c| c == first)? as u64; + for &b in rest { + let digit = REST_CHARS.iter().position(|&c| c == b)? as u64; + value = value + .checked_mul(REST_CHARS.len() as u64)? + .checked_add(digit)?; + } + + // Shift past every value that belongs to a shorter encoding. + value.checked_add(capacity_for_len(s.len() as u32 - 1)) +} + +/// The bucket `name` occupies in a table of `len`-character identifiers, if it occupies one: the +/// name has to fit, and it has to be a name the encoding could itself produce. +fn bucket_of(name: &str, len: u32) -> Option { + if name.len() as u32 > len { + return None; + } + decode_js_identifier(name) +} + +/// Assigns a short, unique identifier to each of `names`, deterministically. +/// +/// The returned map only has an entry for a name that actually changed — a name that already fit +/// and kept itself is simply absent, so a caller must treat a missing key as "keep the original +/// name", not as "not eligible" (that distinction, when it matters, has to come from elsewhere; +/// see the doc comment on [`super::MangledExportNames`]). +/// +/// [`RESERVED_KEYS`] are never handed out, even though they are not themselves assigned a short +/// name. +/// +/// A module with a single export to mangle is special-cased to [`SINGLE_ITEM_IDENTIFIER`], so that +/// every such module in the graph emits the same key and compresses together. +/// +/// The table is sized to the smallest identifier length that can hold every name that needs a +/// bucket, and assignment happens in two passes: +/// +/// 1. Every name that is *already* a valid identifier of at most that length keeps itself and +/// reserves its bucket, as does every reserved key that falls inside the table. This has to +/// happen for **all** names before anything is hashed, or a hashed name could take a bucket that +/// a later preserved name needs. +/// 2. The remaining names are hashed into the table, resolving collisions by open addressing. +/// +/// Both passes iterate in sorted order, so the result depends only on the *set* of names, never on +/// the order they arrive in. +pub fn shorten_to_unique_names<'a>( + names: impl IntoIterator, +) -> FrozenMap { + let mut names: Vec<&RcStr> = names.into_iter().collect(); + names.sort_unstable(); + names.dedup(); + + if names.is_empty() { + return FrozenMap::default(); + } + + // A lone export always gets the same name, which compresses better across modules than a + // hashed one would — unless it is already short enough to keep, in which case it is omitted + // entirely (see the doc comment on the return value below). + if let [name] = names[..] { + return if bucket_of(name, 1).is_some() { + FrozenMap::default() + } else { + FrozenMap::from_iter([((*name).clone(), SINGLE_ITEM_IDENTIFIER)]) + }; + } + + // Grow the table until it can hold the names *and* the reserved buckets that fall inside it. + // Reserving can only ever push us up by the number of reserved keys, so this terminates. + let mut len = 1; + while capacity_for_len(len) < names.len() as u64 + reserved_in_table(len) { + len += 1; + } + let capacity = capacity_for_len(len); + + // Only entries for names that actually changed are kept — see the doc comment on the return + // value below — so this is sized for the common case (most names get hashed) rather than for + // every name. + let mut result = Vec::with_capacity(names.len()); + let mut used = + FxHashSet::with_capacity_and_hasher(names.len() + RESERVED_KEYS.len(), Default::default()); + + for name in RESERVED_KEYS { + if let Some(bucket) = bucket_of(name, len) { + used.insert(bucket); + } + } + + // Pass 1: names that are already valid short identifiers keep themselves and claim their + // bucket — nothing to record, since keeping a name is exactly what an absent entry means. + // This must complete before any hashing happens. + let mut to_mangle = Vec::with_capacity(names.len()); + for name in names { + match bucket_of(name, len) { + // A name that is also reserved can't keep itself; it gets a fresh bucket below. + Some(bucket) if used.insert(bucket) => {} + _ => to_mangle.push(name), + } + } + + // Pass 2: hash the rest into the table, probing linearly on collision. The assigned identifier + // can never equal the original name here: either the name never encoded to a valid identifier + // at all (so it can't equal one now), or it did but lost its own bucket to something else in + // pass 1, and a different bucket always encodes to a different string. + for name in to_mangle { + let mut bucket = hash_xxh3_hash64(name.as_str()) % capacity; + while !used.insert(bucket) { + bucket = (bucket + 1) % capacity; + } + result.push((name.clone(), RcStr::from(encode_js_identifier(bucket)))); + } + + FrozenMap::from_iter(result) +} + +#[cfg(test)] +mod tests { + use rustc_hash::FxHashMap; + + use super::*; + + /// Test convenience wrapper: [`shorten_to_unique_names`] itself only returns entries for names + /// that actually changed (see its doc comment), but most of the tests below read more + /// naturally against a *complete* map — one entry per input name, with an absent one filled in + /// as identity — so this reconstructs that view. + fn shorten(names: &[&str]) -> FxHashMap { + let names: Vec = names.iter().map(|name| RcStr::from(*name)).collect(); + let mangled = shorten_to_unique_names(names.iter()); + names + .iter() + .map(|name| { + let mangled = mangled.get(name).cloned().unwrap_or_else(|| name.clone()); + (name.clone(), mangled) + }) + .collect() + } + + fn assert_all_unique(map: &FxHashMap) { + let mut values: Vec<_> = map.values().cloned().collect(); + values.sort(); + let before = values.len(); + values.dedup(); + assert_eq!( + values.len(), + before, + "mangled names are not unique: {map:?}" + ); + } + + fn numbered(count: usize) -> Vec { + (0..count).map(|i| format!("exportNumber{i:02}")).collect() + } + + #[test] + fn encode_decode_roundtrip() { + for value in [ + 0, + 1, + 52, + 53, + 54, + 55, + 100, + 1000, + 10_000, + 100_000, + u32::MAX as u64, + u64::MAX, + ] { + let encoded = encode_js_identifier(value); + assert_eq!( + decode_js_identifier(&encoded), + Some(value), + "roundtrip failed for {value} ({encoded})" + ); + } + } + + #[test] + fn encoding_is_injective() { + // The property the assignment relies on: distinct buckets never render to the same name, so + // a name preserved in pass 1 can never be handed out again in pass 2. A trailing `_` is an + // ordinary digit here, so there is nothing to exclude. + let mut seen = FxHashMap::default(); + for value in 0..30_000u64 { + let encoded = encode_js_identifier(value); + if let Some(previous) = seen.insert(encoded.clone(), value) { + panic!("{value} and {previous} both encode to {encoded}"); + } + } + } + + #[test] + fn every_name_in_the_alphabet_decodes() { + // A trailing `_` is no longer degenerate: it is a digit like any other, and these are + // distinct values rather than aliases of a shorter name. + assert_eq!(decode_js_identifier("_"), Some(0)); + assert_ne!(decode_js_identifier("__"), decode_js_identifier("_")); + assert_ne!(decode_js_identifier("a_"), decode_js_identifier("a")); + for name in ["_", "__", "a_", "a", "if", "__esModule"] { + let value = decode_js_identifier(name).expect("in the alphabet"); + assert_eq!(encode_js_identifier(value), name, "roundtrip via {name}"); + } + + // Only characters outside the alphabets fail to decode. + assert_eq!(decode_js_identifier("a-b"), None); + assert_eq!(decode_js_identifier("é"), None); + // Digits may not lead an identifier. + assert_eq!(decode_js_identifier("0a"), None); + } + + #[test] + #[should_panic(expected = "identifiers are never empty")] + fn empty_name_panics() { + // An empty export name is a caller bug, not an un-decodable name. + decode_js_identifier(""); + } + + #[test] + fn table_length_follows_capacity() { + assert_eq!(capacity_for_len(1), 54); + assert_eq!(capacity_for_len(2), 54 + 54 * 64); + + // The worked example: 15 exports fit in one character. + let names = numbered(15); + let map = shorten(&names.iter().map(String::as_str).collect::>()); + assert_eq!(map.len(), 15); + for mangled in map.values() { + assert_eq!( + mangled.chars().count(), + 1, + "expected 1 character: {mangled}" + ); + } + + // 55 names no longer fit, so the table grows a character. + let names = numbered(55); + let map = shorten(&names.iter().map(String::as_str).collect::>()); + assert_eq!(map.len(), 55); + assert!(map.values().any(|m| m.chars().count() == 2)); + } + + #[test] + fn short_names_are_preserved() { + let map = shorten(&["a", "longExportName"]); + assert_eq!(map.get("a").map(RcStr::as_str), Some("a")); + assert_ne!(map.get("longExportName").map(RcStr::as_str), Some("a")); + assert_all_unique(&map); + } + + #[test] + fn preserved_names_claim_their_bucket_before_hashing() { + // The preservable names deliberately sort *last*, so an implementation that assigned hashed + // names as it walked the list would already have handed out their buckets. + let mut names = numbered(40); + names.push("z".to_string()); + names.push("A".to_string()); + names.push("$".to_string()); + + let map = shorten(&names.iter().map(String::as_str).collect::>()); + assert_eq!(map.get("z").map(RcStr::as_str), Some("z")); + assert_eq!(map.get("A").map(RcStr::as_str), Some("A")); + assert_eq!(map.get("$").map(RcStr::as_str), Some("$")); + for (name, mangled) in &map { + if !matches!(name.as_str(), "z" | "A" | "$") { + assert!( + !matches!(mangled.as_str(), "z" | "A" | "$"), + "{name} was assigned the preserved name {mangled}" + ); + } + } + assert_all_unique(&map); + } + + #[test] + fn a_single_export_always_gets_the_same_name() { + // Every single-export module in the graph emits the same key, so the `.f` / `.f()` byte + // sequences repeat across the bundle and compress together. The cost is that adding a + // second export renames this one, which is accepted deliberately. + let one = shorten(&["someVeryLongExportName"]); + assert_eq!( + one.get("someVeryLongExportName").map(RcStr::as_str), + Some("f") + ); + assert_eq!( + shorten(&["aCompletelyDifferentName"]) + .get("aCompletelyDifferentName") + .map(RcStr::as_str), + Some("f"), + "a lone export should not depend on its own name" + ); + } + + #[test] + fn a_single_short_export_still_keeps_itself() { + assert_eq!(shorten(&["a"]).get("a").map(RcStr::as_str), Some("a")); + } + + #[test] + fn empty() { + assert!(shorten(&[]).is_empty()); + } + + #[test] + fn unique_under_heavy_collision() { + let names: Vec = (0..100).map(|i| format!("export_{i}")).collect(); + let map = shorten(&names.iter().map(String::as_str).collect::>()); + assert_eq!(map.len(), 100); + assert_all_unique(&map); + } + + #[test] + fn assignment_is_order_independent() { + let forward = shorten(&["foobar", "barbaz", "bazqux", "a", "reallyLongName"]); + let backward = shorten(&["reallyLongName", "a", "bazqux", "barbaz", "foobar"]); + assert_eq!(forward, backward); + } + + #[test] + fn duplicate_names_are_collapsed() { + let map = shorten(&["foobar", "foobar", "barbaz"]); + assert_eq!(map.len(), 2); + assert_all_unique(&map); + } + + #[test] + fn collisions_take_the_next_bucket() { + // Fill the single-character table completely: with 54 names every bucket is taken, so every + // name still gets a distinct single character — only possible if collisions probe on. + let names: Vec = (0..54).map(|i| format!("collidingExport{i:02}")).collect(); + let map = shorten(&names.iter().map(String::as_str).collect::>()); + assert_eq!(map.len(), 54); + assert_all_unique(&map); + for mangled in map.values() { + assert_eq!( + mangled.chars().count(), + 1, + "expected 1 character: {mangled}" + ); + } + assert_eq!(map.len(), FIRST_CHARS.len()); + } + + #[test] + fn probing_wraps_around_the_table() { + // Every bucket but one is claimed by a preserved name, so the hashed name has to wrap + // around the end of the value space to find the only free bucket. + let free = FIRST_CHARS[7] as char; + let mut names: Vec = FIRST_CHARS + .iter() + .map(|&c| (c as char).to_string()) + .filter(|c| c != &free.to_string()) + .collect(); + names.push("aNameThatNeedsMangling".to_string()); + + let map = shorten(&names.iter().map(String::as_str).collect::>()); + assert_eq!( + map.get("aNameThatNeedsMangling").map(RcStr::as_str), + Some(free.to_string().as_str()), + "the only free bucket should have been found by wrapping" + ); + assert_all_unique(&map); + } + + #[test] + fn adding_a_name_within_the_same_tier_keeps_others_stable() { + let before = numbered(20); + let mut after = before.clone(); + after.push("oneMoreExport".to_string()); + + let map_before = shorten(&before.iter().map(String::as_str).collect::>()); + let map_after = shorten(&after.iter().map(String::as_str).collect::>()); + + // Same tier, so the modulus is unchanged: only names in the collision cluster of the new + // name may move. + let moved = map_before + .iter() + .filter(|(name, mangled)| map_after.get(*name) != Some(*mangled)) + .count(); + assert!( + moved <= 2, + "adding one name moved {moved} of {} existing names", + map_before.len() + ); + } + + #[test] + fn reserved_names_are_never_assigned() { + // Enough names that the two-character table is crowded, so a reserved bucket would be + // reached if it were not withheld. + let names: Vec = (0..3000) + .map(|i| format!("collidingExport{i:04}")) + .collect(); + let names: Vec<&str> = names.iter().map(String::as_str).collect(); + let map = shorten(&names); + assert_eq!(map.len(), 3000); + assert_all_unique(&map); + for mangled in map.values() { + assert!( + !RESERVED_KEYS.contains(&mangled.as_str()), + "handed out the reserved key {mangled}" + ); + } + } + + #[test] + fn a_name_that_is_itself_reserved_does_not_keep_itself() { + // `in` would normally keep its own name, but it is reserved, so it gets a fresh bucket. + let map = shorten(&["in", "someLongExportName"]); + assert_ne!(map.get("in").map(RcStr::as_str), Some("in")); + assert_all_unique(&map); + } + + #[test] + fn reserved_bucket_count_matches_the_reserved_list() { + // `reserved_in_table` is maintained by hand; keep it honest against the list it summarizes. + for len in 1..=12u32 { + let counted = RESERVED_KEYS + .iter() + .filter(|name| bucket_of(name, len).is_some()) + .count() as u64; + assert_eq!( + reserved_in_table(len), + counted, + "reserved_in_table({len}) disagrees with RESERVED_KEYS" + ); + } + } + + #[test] + fn capacity_for_len_matches_the_formula() { + // `CAPACITY_FOR_LEN` is precomputed to keep it off `decode_js_identifier`'s hot path; keep + // it honest against the closed form it stands in for, `54 * (64^len - 1) / 63`. Computed in + // `u128` so this check doesn't itself rely on the saturation it is verifying. + for len in 0..CAPACITY_FOR_LEN.len() as u32 { + let first = FIRST_CHARS.len() as u128; + let rest = REST_CHARS.len() as u128; + let expected = first * (rest.pow(len) - 1) / (rest - 1); + assert_eq!( + capacity_for_len(len) as u128, + expected, + "capacity_for_len({len}) disagrees with the formula" + ); + } + // And confirm it still saturates beyond the precomputed range, rather than overflowing. + assert_eq!(capacity_for_len(CAPACITY_FOR_LEN.len() as u32), u64::MAX); + assert_eq!(capacity_for_len(1000), u64::MAX); + } + + #[test] + fn keyword_keys_are_never_handed_out() { + // `ns["if"]` is legal but a minifier will not fold it to `ns.if`, so it costs bytes. The + // words are withheld from the table like any other reserved key. + for keyword in RESERVED_KEYS.iter().filter(|k| k.len() <= 3) { + let value = decode_js_identifier(keyword) + .unwrap_or_else(|| panic!("{keyword} should be in the encoding's image")); + assert_eq!( + encode_js_identifier(value), + *keyword, + "{keyword} must round-trip, or withholding its bucket does nothing" + ); + } + + // Enough names to need two characters, which is where the words become reachable. + let names: Vec = (0..2000).map(|i| format!("exportNumber{i:04}")).collect(); + let names: Vec<&str> = names.iter().map(String::as_str).collect(); + let map = shorten(&names); + assert_all_unique(&map); + assert_eq!(map.len(), 2000); + for mangled in map.values() { + assert!( + !RESERVED_KEYS.contains(&mangled.as_str()), + "handed out the reserved key {mangled}" + ); + } + } + + #[test] + fn an_export_named_like_a_keyword_is_renamed_away_from_it() { + // Keeping `if` would emit `ns["if"]` (8 bytes); renaming it to a dot-accessible key gets + // `ns.ab` (5), so withholding the word is a win even for a source name that is already + // short enough to keep. + let map = shorten(&["if", "someLongExportName"]); + let mangled = map.get("if").unwrap(); + assert_ne!(mangled.as_str(), "if"); + assert_eq!(mangled.chars().count(), 1, "should still be a short key"); + assert_all_unique(&map); + } +} diff --git a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs index d5eef8f1f6a4..96fe9eb88a7d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/esm/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod base; pub(crate) mod binding; pub(crate) mod dynamic; pub(crate) mod export; +pub(crate) mod mangle; pub(crate) mod meta; pub(crate) mod module_id; pub(crate) mod module_item; @@ -12,6 +13,7 @@ pub use self::{ binding::EsmBinding, dynamic::EsmAsyncAssetReference, export::{EsmExport, EsmExports, FoundExportType, Liveness}, + mangle::generated_export_key, meta::{ImportMetaBinding, ImportMetaRef}, module_item::EsmModuleItem, url::{UrlAssetReference, UrlRewriteBehavior}, diff --git a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs index 80ced8a69aa5..3f114f8fe65a 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/exports.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/exports.rs @@ -179,6 +179,7 @@ pub async fn compute_ecmascript_module_exports( let esm_exports = EsmExports { exports: esm_exports, star_exports: esm_star_exports, + mangle_export_names: options.mangle_export_names, } .cell(); @@ -199,6 +200,7 @@ pub async fn compute_ecmascript_module_exports( EsmExports { exports: Default::default(), star_exports: Default::default(), + mangle_export_names: options.mangle_export_names, } .resolved_cell(), ) @@ -210,6 +212,7 @@ pub async fn compute_ecmascript_module_exports( EsmExports { exports: Default::default(), star_exports: Default::default(), + mangle_export_names: options.mangle_export_names, } .resolved_cell(), ), @@ -224,6 +227,7 @@ pub async fn compute_ecmascript_module_exports( EsmExports { exports: Default::default(), star_exports: Default::default(), + mangle_export_names: options.mangle_export_names, } .resolved_cell(), ) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/exports_info.rs b/turbopack/crates/turbopack-ecmascript/src/references/exports_info.rs index c1439d43dcf6..9d31f0f4d873 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/exports_info.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/exports_info.rs @@ -13,7 +13,7 @@ use crate::{ chunk::{EcmascriptChunkPlaceable, EcmascriptExports}, code_gen::{CodeGen, CodeGeneration}, create_visitor, magic_identifier, - references::AstPath, + references::{AstPath, esm::mangle::mangled_export_names}, }; /// Responsible for initializing the `ExportsInfoBinding` object binding, so that it may be @@ -44,18 +44,48 @@ impl ExportsInfoBinding { .module_export_usage(*ResolvedVc::upcast(module)) .await?; let export_usage_info = export_usage_info.export_usage.await?; + // The keys of `__webpack_exports_info__` stay the *original* export names — user code + // looks them up by name. The emitted key is reported as `mangledName` instead, which is + // always present alongside `canMangle` (`null` when `canMangle` is false), regardless of + // whether export mangling is enabled at all — see the `map` closure below for exactly + // what each of the three fields means. + let exports = exports.await?; + let mangled_names = mangled_export_names(*module, chunking_context).await?; - let props = if let EcmascriptExports::EsmExports(exports) = &*exports.await? { + let props = if let EcmascriptExports::EsmExports(exports) = &*exports { exports .await? .exports .keys() .map(|e| { - let used: Expr = export_usage_info.is_export_used(e).into(); + let is_used = export_usage_info.is_export_used(e); + let used: Expr = is_used.into(); + // `canMangle` is true exactly when this export is a genuine candidate for + // mangling: the module has to be eligible at all (which is what a `Some` map + // means — see `mangled_export_names`) and the export itself has to be used, as + // an unused export is never emitted and so was never a candidate. + // `mangledName` is then always a string — the assigned key when mangling + // actually renamed it, or the export's own name when it was considered but + // kept itself (e.g. already short enough) — and only `null` when `canMangle` + // is false. + let can_mangle_names = mangled_names.as_ref().filter(|_| is_used); + let can_mangle_expr: Expr = can_mangle_names.is_some().into(); + let mangled_name: Expr = + match can_mangle_names { + Some(names) => Expr::Lit(names.get(e).unwrap_or(e).as_str().into()), + None => Expr::Lit(swc_core::ecma::ast::Lit::Null( + swc_core::ecma::ast::Null { span: DUMMY_SP }, + )), + }; PropOrSpread::Prop(Box::new(swc_core::ecma::ast::Prop::KeyValue( KeyValueProp { key: PropName::Str(e.as_str().into()), - value: quote!("{ used: $v }" as Box, v: Expr = used), + value: quote!( + "{ used: $v, canMangle: $c, mangledName: $m }" as Box, + v: Expr = used, + c: Expr = can_mangle_expr, + m: Expr = mangled_name + ), }, ))) }) diff --git a/turbopack/crates/turbopack-ecmascript/src/rename/module.rs b/turbopack/crates/turbopack-ecmascript/src/rename/module.rs index ce5e5b7f0886..f44ad599177d 100644 --- a/turbopack/crates/turbopack-ecmascript/src/rename/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/rename/module.rs @@ -241,6 +241,12 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleRenameModule { let exports = EsmExports { exports: FrozenMap::from_unique_sorted_box(Box::new([export])), star_exports: Vec::new(), + // This module only re-exports one binding of `self.module` under a different name, so + // whether its own key may be shortened follows the module it renames. + mangle_export_names: match &*self.module.get_exports().await? { + EcmascriptExports::EsmExports(exports) => exports.await?.mangle_export_names, + _ => false, + }, } .resolved_cell(); Ok(EcmascriptExports::EsmExports(exports).cell()) diff --git a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs index b320f17ababa..2ac305180582 100644 --- a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/facade/module.rs @@ -243,6 +243,7 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleFacadeModule { let exports = EsmExports { exports: FrozenMap::from_unique_sorted_box(exports.into_boxed_slice()), star_exports: esm_exports.star_exports.clone(), + mangle_export_names: esm_exports.mangle_export_names, } .resolved_cell(); Ok(EcmascriptExports::EsmExports(exports).cell()) diff --git a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs index 12e6f412e58c..3d86cd2434cb 100644 --- a/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/side_effect_optimization/locals/module.rs @@ -184,6 +184,7 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleLocalsModule { let exports = EsmExports { exports: FrozenMap::from_unique_sorted_box(exports.into_boxed_slice()), star_exports: vec![], + mangle_export_names: esm_exports.mangle_export_names, } .resolved_cell(); Ok(EcmascriptExports::EsmExports(exports).cell()) diff --git a/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs b/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs index 3e5d6b0af405..c933b61a52a2 100644 --- a/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs +++ b/turbopack/crates/turbopack-ecmascript/src/worker_chunk/module.rs @@ -27,6 +27,7 @@ use crate::{ EcmascriptExports, data::EcmascriptChunkData, ecmascript_chunk_item, }, embed_js::embed_fs, + references::esm::generated_export_key, runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_REQUIRE}, utils::{StringifyJs, StringifyModuleId}, }; @@ -261,6 +262,10 @@ impl EcmascriptChunkPlaceable for WorkerLoaderModule { // otherwise we will induce a turbo tasks cycle. But we only need an // approximate solution. We'll use the same estimate for both web // and Node.js workers. + // + // That includes the export key: resolving the real one needs the chunking context, so + // the estimate uses the source name even when the helper's exports are mangled. It can + // only be off by a few characters. let fake_id = ModuleId::String(rcstr!("a_fake_module")); return Ok(EcmascriptChunkItemContent { inner_code: formatdoc! { @@ -277,10 +282,19 @@ impl EcmascriptChunkPlaceable for WorkerLoaderModule { .cell()); } - let create_worker_id = self - .create_worker_module() - .chunk_item_id(chunking_context) - .await?; + let create_worker_module = self.create_worker_module(); + let create_worker_id = create_worker_module.chunk_item_id(chunking_context).await?; + // The helper's `default` export is read here as a string, so it has to go through the same + // mapping the helper itself emits — a hard-coded `["default"]` misses once its exports are + // mangled. + let create_worker_export = match ResolvedVc::try_sidecast::>( + create_worker_module.to_resolved().await?, + ) { + Some(placeable) => { + generated_export_key(placeable, chunking_context, &rcstr!("default")).await? + } + None => rcstr!("default"), + }; let code = match this.worker_type { WorkerType::WebWorker | WorkerType::SharedWebWorker => { @@ -306,11 +320,12 @@ impl EcmascriptChunkPlaceable for WorkerLoaderModule { formatdoc! { r#" - {TURBOPACK_EXPORT_VALUE}({TURBOPACK_REQUIRE}({workers_module})["default"]({entrypoint}, {chunks})); + {TURBOPACK_EXPORT_VALUE}({TURBOPACK_REQUIRE}({workers_module})[{export:#}]({entrypoint}, {chunks})); "#, entrypoint = StringifyJs(&entrypoint_path), chunks = StringifyJs(&chunks_data), workers_module = StringifyModuleId(&create_worker_id), + export = StringifyJs(&create_worker_export), } } WorkerType::NodeWorkerThread => { @@ -338,10 +353,11 @@ impl EcmascriptChunkPlaceable for WorkerLoaderModule { // directory formatdoc! { r#" - {TURBOPACK_EXPORT_VALUE}({TURBOPACK_REQUIRE}({workers_module})["default"](__dirname + "/" + {worker_path:#})); + {TURBOPACK_EXPORT_VALUE}({TURBOPACK_REQUIRE}({workers_module})[{export:#}](__dirname + "/" + {worker_path:#})); "#, worker_path = StringifyJs(entry_path.file_name()), workers_module = StringifyModuleId(&create_worker_id), + export = StringifyJs(&create_worker_export), } } }; diff --git a/turbopack/crates/turbopack-tests/tests/execution.rs b/turbopack/crates/turbopack-tests/tests/execution.rs index 8fbfda911939..79d2c30c55af 100644 --- a/turbopack/crates/turbopack-tests/tests/execution.rs +++ b/turbopack/crates/turbopack-tests/tests/execution.rs @@ -280,6 +280,8 @@ struct TestOptions { infer_module_side_effects: bool, #[serde(default)] cjs_tree_shaking: bool, + #[serde(default)] + mangle_export_names: bool, #[serde(default = "default_true")] cross_module_constants: bool, #[serde(default)] @@ -312,6 +314,7 @@ impl Default for TestOptions { remove_unused_imports: default_true(), scope_hoisting: default_true(), cjs_tree_shaking: false, + mangle_export_names: false, cjs_scope_hoisting: false, cross_module_constants: true, infer_module_side_effects: default_true(), @@ -479,6 +482,7 @@ async fn run_test_operation(prepared_test: ResolvedVc) -> Result ns`), the analysis cannot enumerate the reads, so the +// module's usage widens to `ModuleExportUsageInfo::All` and mangling backs off on that. Forcing the +// facade / locals split for mangled modules does not help: the widened usage propagates through the +// facade to the locals module, so both keep their original keys. Materialization needs the facade to +// pin the original names *while* declaring a known, named usage of the locals module. +// +// Lives under `__skipped__`, so the harness asserts it still fails. When materialization is +// implemented this fixture starts passing, the suite goes red, and it should be moved out of +// `__skipped__`. + +import { getEnums } from './provider' + +it('should still mangle a module whose namespace escapes', () => { + const ns = getEnums() + // The namespace object keeps the original names... + expect(ns.ENUM_A).toBe('a-value') + expect(ns.ENUM_B).toBe('b-value') + expect(ns.default).toBe('default-value') + expect(Object.keys(ns).sort()).toEqual([ + 'ENUM_A', + 'ENUM_B', + 'default', + 'exportsInfo', + ]) + + // ...while the module's own export keys are still shortened. This is the part that isn't + // implemented: today `canMangle` is false here because we back off instead. + expect(ns.exportsInfo.ENUM_A.canMangle).toBe(true) + expect(ns.exportsInfo.ENUM_A.mangledName).not.toBe('ENUM_A') + expect(ns.exportsInfo.ENUM_B.mangledName).not.toBe( + ns.exportsInfo.ENUM_A.mangledName + ) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/input/provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/input/provider.js new file mode 100644 index 000000000000..0a87bfdc3b04 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/input/provider.js @@ -0,0 +1,4 @@ +import * as ns from './enums' + +// The namespace object escapes, so it cannot be statically tracked. +export const getEnums = () => ns diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/__skipped__/mangle-materialized-namespace/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/index.js new file mode 100644 index 000000000000..701fd8e23117 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/index.js @@ -0,0 +1,45 @@ +import { + a, + b, + foo, + reallyLongExportName, + anotherVeryLongExportName, + shortFn, + thisIsAVeryLongFunctionName, + exportsInfo, +} from './named-exports' + +it('should keep the values of mangled exports correct', () => { + expect(a).toBe('short-a') + expect(b).toBe('short-b') + expect(foo).toBe('short-foo') + expect(reallyLongExportName).toBe('long-name-1') + expect(anotherVeryLongExportName).toBe('long-name-2') + expect(shortFn()).toBe('short-fn') + expect(thisIsAVeryLongFunctionName()).toBe('long-fn') +}) + +it('should actually mangle the exported names', () => { + // `__webpack_exports_info__` is keyed by the original names and reports the key the export is + // emitted under, which is how a running test can observe that mangling happened at all. + expect(exportsInfo.reallyLongExportName.canMangle).toBe(true) + expect(exportsInfo.reallyLongExportName.mangledName).toEqual( + expect.any(String) + ) + expect( + exportsInfo.reallyLongExportName.mangledName.length + ).toBeLessThanOrEqual(2) + expect(exportsInfo.thisIsAVeryLongFunctionName.mangledName).not.toBe( + 'thisIsAVeryLongFunctionName' + ) + // Distinct exports never share a key. + expect(exportsInfo.reallyLongExportName.mangledName).not.toBe( + exportsInfo.thisIsAVeryLongFunctionName.mangledName + ) +}) + +it('should keep a name that is already short', () => { + // `a` is already a valid one-character identifier, so it reserves that bucket and keeps itself + // instead of being renamed. + expect(exportsInfo.a.mangledName).toBe('a') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/named-exports.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/named-exports.js new file mode 100644 index 000000000000..2d9a96e3cce4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/input/named-exports.js @@ -0,0 +1,23 @@ +// Every export here is imported by name, so the keys this module emits are only ever read by +// generated code and can be shortened. + +export const a = 'short-a' +export const b = 'short-b' +export const foo = 'short-foo' +export const reallyLongExportName = 'long-name-1' +export const anotherVeryLongExportName = 'long-name-2' + +export function shortFn() { + return 'short-fn' +} + +export function thisIsAVeryLongFunctionName() { + return 'long-fn' +} + +export const exportsInfo = { + a: __webpack_exports_info__.a, + reallyLongExportName: __webpack_exports_info__.reallyLongExportName, + thisIsAVeryLongFunctionName: + __webpack_exports_info__.thisIsAVeryLongFunctionName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-basic/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/cjs-consumer.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/cjs-consumer.js new file mode 100644 index 000000000000..691ab04bece7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/cjs-consumer.js @@ -0,0 +1,6 @@ +const esm = require('./esm') + +exports.readNamed = () => esm.someLongExportName +exports.readOther = () => esm.anotherLongExportName +exports.exportsInfo = esm.exportsInfo +exports.keys = () => Object.keys(esm) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/esm.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/esm.js new file mode 100644 index 000000000000..613c956d89d0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/esm.js @@ -0,0 +1,9 @@ +// Imported by a CommonJS module through `require()`. The property accesses on the required object +// live in user source and are not rewritten, so this module must keep its original names. + +export const someLongExportName = 'esm-1' +export const anotherLongExportName = 'esm-2' + +export const exportsInfo = { + someLongExportName: __webpack_exports_info__.someLongExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/index.js new file mode 100644 index 000000000000..e9f20c2b1d31 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/input/index.js @@ -0,0 +1,16 @@ +import { readNamed, readOther, exportsInfo, keys } from './cjs-consumer' +import { someLongExportName } from './esm' + +it('should keep a CommonJS consumer of an ESM module working', () => { + expect(readNamed()).toBe('esm-1') + expect(readOther()).toBe('esm-2') + // The ESM import of the same module resolves to the same binding. + expect(someLongExportName).toBe('esm-1') +}) + +it('should not mangle a module that a CommonJS module requires', () => { + // The `esm.someLongExportName` accesses in `cjs-consumer.js` are user source, so the names have + // to stay as written. + expect(exportsInfo.someLongExportName.canMangle).toBe(false) + expect(keys()).toContain('someLongExportName') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/options.json new file mode 100644 index 000000000000..65869c16c6b5 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-cjs-consumer/options.json @@ -0,0 +1,6 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false, + "cjsTreeShaking": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/es-module-name.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/es-module-name.js new file mode 100644 index 000000000000..74da3f294c7d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/es-module-name.js @@ -0,0 +1,10 @@ +// `__esModule` is how a CommonJS module signals interop *to* ESM; the runtime defines it on the +// exports object itself. An ESM module that happens to export that name is just an ordinary export, +// so its key is mangled like any other — and the runtime's own `__esModule` is untouched. + +export const __esModule = 'a-normal-export' +export const someLongExportName = 'named-value' + +export const exportsInfo = { + __esModule: __webpack_exports_info__.__esModule, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/index.js new file mode 100644 index 000000000000..95dd307dfed8 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/index.js @@ -0,0 +1,34 @@ +import theDefault, { someLongExportName, exportsInfo } from './lib' +import throughReexport, { renamedDefault } from './reexport' +import { + __esModule as esModuleExport, + exportsInfo as esModuleExportsInfo, +} from './es-module-name' + +it('should keep the default export working when it is mangled', () => { + expect(theDefault()).toBe('default-value') + expect(someLongExportName).toBe('named-value') +}) + +it('should mangle the default export key', () => { + expect(exportsInfo.default.canMangle).toBe(true) + expect(exportsInfo.default.mangledName).not.toBe('default') + expect(exportsInfo.default.mangledName.length).toBeLessThanOrEqual(2) + // `default` shares the table with the other exports, so it never collides with them. + expect(exportsInfo.default.mangledName).not.toBe( + exportsInfo.someLongExportName.mangledName + ) +}) + +it('should carry a mangled default through a re-export', () => { + expect(throughReexport()).toBe('default-value') + expect(renamedDefault()).toBe('default-value') +}) + +it('should mangle an export literally named __esModule', () => { + // The name is only an interop marker when a CommonJS module sets it; as an ESM export name it + // carries no meaning, so its key is shortened like any other. + expect(esModuleExport).toBe('a-normal-export') + expect(esModuleExportsInfo.__esModule.canMangle).toBe(true) + expect(esModuleExportsInfo.__esModule.mangledName).not.toBe('__esModule') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/lib.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/lib.js new file mode 100644 index 000000000000..2754db7418fc --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/lib.js @@ -0,0 +1,14 @@ +// `default` is mangled like any other export key: every read of it in an eligible module is +// generated by us. Modules whose names can be observed (CommonJS interop, an escaping namespace, +// a dynamic import) keep `default` along with everything else — see the other `mangle-*` fixtures. + +export default function theDefaultExport() { + return 'default-value' +} + +export const someLongExportName = 'named-value' + +export const exportsInfo = { + default: __webpack_exports_info__.default, + someLongExportName: __webpack_exports_info__.someLongExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/reexport.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/reexport.js new file mode 100644 index 000000000000..41cc13bc6c6d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/input/reexport.js @@ -0,0 +1,3 @@ +// A default export travelling through a re-export, in both spellings. +export { default } from './lib' +export { default as renamedDefault } from './lib' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-default/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/index.js new file mode 100644 index 000000000000..d1112f3aa09d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/index.js @@ -0,0 +1,32 @@ +import * as mod from './module' + +it('should keep values correct when destructuring the namespace', () => { + const { aVeryLongExportName, anotherVeryLongExportName } = mod + expect(aVeryLongExportName).toBe('a-value') + expect(anotherVeryLongExportName).toBe('b-value') +}) + +it('should keep values correct when destructuring a namespace property', () => { + const { a, b } = mod.objectValuedExportName + expect(a).toBe('a') + expect(b).toBe('b') +}) + +it('should keep values correct with member access on the namespace', () => { + expect(mod.aVeryLongExportName).toBe('a-value') +}) + +it('should handle `default` when destructuring a namespace', () => { + const { default: value } = mod + expect(value).toBe('default-value') +}) + +it('should back off for a module read through a namespace binding', () => { + // Reads through an `import * as ns` binding are reported as a *partial namespace object*: we + // know which names are used, but not whether every read was lowered to a direct named access, + // and a read that wasn't (a destructuring pattern, say) still uses the original name. So the + // module keeps its names. Distinguishing lowered reads from a materialized namespace object is + // a follow-up; it would unlock mangling for namespace-imported modules too. + expect(mod.exportsInfo.aVeryLongExportName.canMangle).toBe(false) + expect(mod.exportsInfo.aVeryLongExportName.mangledName).toBe(null) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/module.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/module.js new file mode 100644 index 000000000000..32e024ecd525 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/input/module.js @@ -0,0 +1,9 @@ +export const aVeryLongExportName = 'a-value' +export const anotherVeryLongExportName = 'b-value' +export const objectValuedExportName = { a: 'a', b: 'b' } +export default 'default-value' + +export const exportsInfo = { + aVeryLongExportName: __webpack_exports_info__.aVeryLongExportName, + objectValuedExportName: __webpack_exports_info__.objectValuedExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-destructuring/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js new file mode 100644 index 000000000000..c4c2e9774d20 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js @@ -0,0 +1,5 @@ +// A CommonJS module whose exports can't be determined statically, so a re-export of it has to be +// resolved at runtime by property access on the original names. +const key = 'dynamicallyNamedExport' +exports[key] = 'dynamic-value' +exports.staticallyNamedExport = 'static-value' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/index.js new file mode 100644 index 000000000000..89928dc5c918 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/index.js @@ -0,0 +1,16 @@ +import { ownLongExportName, staticallyNamedExport } from './reexport' +import * as ns from './reexport' + +it('should keep a re-export of dynamic exports working', () => { + expect(ownLongExportName).toBe('own-value') + expect(staticallyNamedExport).toBe('static-value') +}) + +it('should expose the original names of a module with dynamic re-exports', () => { + // The star re-export is resolved at runtime by property access on the original names, so the + // names this module exposes must be the ones written in the source. + expect(ns.ownLongExportName).toBe('own-value') + expect(ns.staticallyNamedExport).toBe('static-value') + expect(ns.dynamicallyNamedExport).toBe('dynamic-value') + expect(Object.keys(ns)).toContain('ownLongExportName') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/reexport.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/reexport.js new file mode 100644 index 000000000000..39647c57427d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/reexport.js @@ -0,0 +1,5 @@ +// `export *` from a module with dynamic exports: the re-exported names are only known at runtime, +// so this module keeps its original export names. +export * from './dynamic-cjs' + +export const ownLongExportName = 'own-value' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/issues/unexpected export __star__-aa3e7d.txt b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/issues/unexpected export __star__-aa3e7d.txt new file mode 100644 index 000000000000..cf7908c77ece --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/issues/unexpected export __star__-aa3e7d.txt @@ -0,0 +1,9 @@ +warning - [analysis] /turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js unexpected export * + + export * used with module [project]/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js [test] (ecmascript) which is a CommonJS module with exports only available at runtime + List all export names manually (`export { a, b, c } from "...") or rewrite the module to ESM, to avoid the additional runtime code.` + + Import trace: + test: + ./turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/dynamic-cjs.js + ./turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/input/index.js \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-dynamic-exports/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs-provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs-provider.js new file mode 100644 index 000000000000..3caec227a216 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs-provider.js @@ -0,0 +1,5 @@ +import * as ns from './cjs' + +// A CommonJS namespace escaping through the interop layer. CommonJS exports are never mangled, +// and the interop object must keep working unchanged. +export const getCjs = () => ns diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs.js new file mode 100644 index 000000000000..41dcaf493c42 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/cjs.js @@ -0,0 +1,2 @@ +exports.CJS_A = 'cjs-a' +exports.CJS_B = 'cjs-b' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/destr.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/destr.js new file mode 100644 index 000000000000..799e24d95eb7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/destr.js @@ -0,0 +1,8 @@ +import * as ns from './enums' + +// A statically tracked read of the same module whose namespace escapes elsewhere. It has to keep +// working, and it has to read the same (original) name the escaping namespace exposes. +export function read() { + const { ENUM_B } = ns + return ENUM_B +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/enums.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/enums.js new file mode 100644 index 000000000000..2498439743c4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/enums.js @@ -0,0 +1,8 @@ +// A namespace object of this module escapes into user code (see `provider.js`), so its export +// names are observable and must not be shortened. + +export const ENUM_A = 'a-value' +export const ENUM_B = 'b-value' +export const ENUM_C = 'c-value' +export const NUM = 42 +export default 'default-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 new file mode 100644 index 000000000000..63ca1d873642 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/index.js @@ -0,0 +1,57 @@ +import { getEnums } from './provider' +import { enumsNs } from './reexport' +import { getCjs } from './cjs-provider' +import { read } from './destr' +import { + someLongExportName, + anotherLongExportName, + exportsInfo, +} from './mangleable' + +it('should keep an escaped namespace object readable by its original names', () => { + const ns = getEnums() + expect(ns.ENUM_A).toBe('a-value') + expect(ns.ENUM_B).toBe('b-value') + expect(ns.ENUM_C).toBe('c-value') + expect(ns.NUM).toBe(42) + expect(ns.default).toBe('default-value') +}) + +it('should expose the original names when enumerating an escaped namespace', () => { + const keys = Object.keys(getEnums()).sort() + expect(keys).toEqual(['ENUM_A', 'ENUM_B', 'ENUM_C', 'NUM', 'default']) +}) + +it('should keep an escaped re-exported (export * as) namespace working', () => { + const get = () => enumsNs + const ns = get() + expect(ns.ENUM_A).toBe('a-value') + expect(ns.default).toBe('default-value') +}) + +it('should keep destructuring working for a module that also escapes', () => { + expect(read()).toBe('b-value') +}) + +it('should keep `delete ns.member` valid when the namespace escapes', () => { + // Deleting a member that does not exist returns true per spec, and must not be emitted as a + // bare `delete undefined`, which is a SyntaxError in strict mode. + const ns = getEnums() + expect(delete ns.doesNotExist).toBe(true) + expect(ns.ENUM_A).toBe('a-value') +}) + +it('should keep an escaped CommonJS namespace interop correct', () => { + const ns = getCjs() + expect(ns.CJS_A).toBe('cjs-a') + expect(ns.CJS_B).toBe('cjs-b') +}) + +it('should still mangle a sibling module that does not escape', () => { + expect(someLongExportName).toBe('mangled-1') + expect(anotherLongExportName).toBe('mangled-2') + expect(exportsInfo.someLongExportName.canMangle).toBe(true) + expect(exportsInfo.someLongExportName.mangledName).not.toBe( + 'someLongExportName' + ) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable.js new file mode 100644 index 000000000000..8a3ebddcf6e0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/mangleable.js @@ -0,0 +1,10 @@ +// Nothing escapes here: every export is read by name, so this module *is* mangled even though +// another module in the same graph had to back off. Back-off is per module. + +export const someLongExportName = 'mangled-1' +export const anotherLongExportName = 'mangled-2' + +export const exportsInfo = { + someLongExportName: __webpack_exports_info__.someLongExportName, + anotherLongExportName: __webpack_exports_info__.anotherLongExportName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/provider.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/provider.js new file mode 100644 index 000000000000..588b6a383222 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/provider.js @@ -0,0 +1,5 @@ +import * as ns from './enums' + +// The whole namespace object escapes through a function return, so no static analysis can tell +// which names the caller will read. +export const getEnums = () => ns diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/reexport.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/reexport.js new file mode 100644 index 000000000000..49d4209fb319 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/input/reexport.js @@ -0,0 +1,2 @@ +// A re-exported namespace object that later escapes. +export * as enumsNs from './enums' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-escaping-namespace/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/index.js new file mode 100644 index 000000000000..2e7c07d7e78a --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/index.js @@ -0,0 +1,20 @@ +import { + someLongExportName, + anotherLongExportName, + aLongFunctionName, +} from './shared' + +it('should be correct with scope hoisting enabled', () => { + expect(someLongExportName).toBe('shared-1') + expect(anotherLongExportName).toBe('shared-2') + expect(aLongFunctionName()).toBe('shared-fn') +}) + +it('should be correct across a chunk boundary with scope hoisting enabled', async () => { + // `lazy.js` lives in another chunk and reads the same module, so the binding has to be resolved + // through the emitted export object rather than a merged local. + const { fromLazy } = await import( + /* turbopackExports: ["fromLazy"] */ './lazy' + ) + expect(fromLazy()).toBe('shared-1/shared-fn') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/lazy.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/lazy.js new file mode 100644 index 000000000000..004cfad65da8 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/lazy.js @@ -0,0 +1,3 @@ +import { someLongExportName, aLongFunctionName } from './shared' + +export const fromLazy = () => `${someLongExportName}/${aLongFunctionName()}` diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/shared.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/shared.js new file mode 100644 index 000000000000..55ba316ee848 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/input/shared.js @@ -0,0 +1,9 @@ +// Statically imported by `index.js` and dynamically by `lazy.js`, so it is exposed across a chunk +// boundary and its export object is really emitted even with scope hoisting on. + +export const someLongExportName = 'shared-1' +export const anotherLongExportName = 'shared-2' + +export function aLongFunctionName() { + return 'shared-fn' +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/options.json new file mode 100644 index 000000000000..8e711cc5db1d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-hoisted/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/index.js new file mode 100644 index 000000000000..d107d595c6ca --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/index.js @@ -0,0 +1,24 @@ +it('should keep the names of a module imported with webpackExports', async () => { + const { usedName, exportsInfo } = await import( + /* webpackExports: ["usedName", "exportsInfo"] */ './lazy' + ) + expect(usedName).toBe('used') + // The magic comment narrows which exports are *used*, but the namespace object still exposes + // them under their original names, so the module has to back off. + expect(exportsInfo.usedName.canMangle).toBe(false) + expect(exportsInfo.usedName.mangledName).toBe(null) +}) + +it('should keep the names of a module imported with turbopackExports', async () => { + const ns = await import( + /* turbopackExports: ["otherUsedName", "exportsInfo"] */ './lazy' + ) + expect(ns.otherUsedName).toBe('other-used') + expect(ns.exportsInfo.otherUsedName.canMangle).toBe(false) +}) + +it('should keep a plain dynamic import working', async () => { + const ns = await import('./lazy') + expect(ns.usedName).toBe('used') + expect(Object.keys(ns)).toContain('usedName') +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/lazy.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/lazy.js new file mode 100644 index 000000000000..b60403e95d97 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/input/lazy.js @@ -0,0 +1,12 @@ +// Reached only through `import()`. Even with a `webpackExports` / `turbopackExports` comment +// telling us which exports are used, the namespace object that `import()` resolves to is handed +// to user code and destructured by the *original* names, so this module must not be mangled. + +export const usedName = 'used' +export const otherUsedName = 'other-used' +export const unusedName = 'unused' + +export const exportsInfo = { + usedName: __webpack_exports_info__.usedName, + otherUsedName: __webpack_exports_info__.otherUsedName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-magic-comments/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/consume.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/consume.js new file mode 100644 index 000000000000..beaf1f0d5583 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/consume.js @@ -0,0 +1,128 @@ +import { + exportNumber00, + exportNumber01, + exportNumber02, + exportNumber03, + exportNumber04, + exportNumber05, + exportNumber06, + exportNumber07, + exportNumber08, + exportNumber09, + exportNumber10, + exportNumber11, + exportNumber12, + exportNumber13, + exportNumber14, + exportNumber15, + exportNumber16, + exportNumber17, + exportNumber18, + exportNumber19, + exportNumber20, + exportNumber21, + exportNumber22, + exportNumber23, + exportNumber24, + exportNumber25, + exportNumber26, + exportNumber27, + exportNumber28, + exportNumber29, + exportNumber30, + exportNumber31, + exportNumber32, + exportNumber33, + exportNumber34, + exportNumber35, + exportNumber36, + exportNumber37, + exportNumber38, + exportNumber39, + exportNumber40, + exportNumber41, + exportNumber42, + exportNumber43, + exportNumber44, + exportNumber45, + exportNumber46, + exportNumber47, + exportNumber48, + exportNumber49, + exportNumber50, + exportNumber51, + exportNumber52, + exportNumber53, + exportNumber54, + exportNumber55, + exportNumber56, + exportNumber57, + exportNumber58, + exportNumber59, + exportsInfo, +} from './many-named' + +export const values = () => [ + exportNumber00, + exportNumber01, + exportNumber02, + exportNumber03, + exportNumber04, + exportNumber05, + exportNumber06, + exportNumber07, + exportNumber08, + exportNumber09, + exportNumber10, + exportNumber11, + exportNumber12, + exportNumber13, + exportNumber14, + exportNumber15, + exportNumber16, + exportNumber17, + exportNumber18, + exportNumber19, + exportNumber20, + exportNumber21, + exportNumber22, + exportNumber23, + exportNumber24, + exportNumber25, + exportNumber26, + exportNumber27, + exportNumber28, + exportNumber29, + exportNumber30, + exportNumber31, + exportNumber32, + exportNumber33, + exportNumber34, + exportNumber35, + exportNumber36, + exportNumber37, + exportNumber38, + exportNumber39, + exportNumber40, + exportNumber41, + exportNumber42, + exportNumber43, + exportNumber44, + exportNumber45, + exportNumber46, + exportNumber47, + exportNumber48, + exportNumber49, + exportNumber50, + exportNumber51, + exportNumber52, + exportNumber53, + exportNumber54, + exportNumber55, + exportNumber56, + exportNumber57, + exportNumber58, + exportNumber59, +] + +export { exportsInfo } diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/generate.mjs b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/generate.mjs new file mode 100644 index 000000000000..e5cf6a6f87ef --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/generate.mjs @@ -0,0 +1,45 @@ +// Not part of the test: regenerates the two 60-export modules and the consumer that imports each +// of them by name. Run from this directory: +// node generate.mjs +import { writeFileSync } from 'node:fs' + +const count = 60 +const names = Array.from( + { length: count }, + (_, i) => `exportNumber${String(i).padStart(2, '0')}` +) + +const moduleSource = (withInfo) => + [ + ...names.map((name, i) => { + const n = String(i).padStart(2, '0') + return `export const ${name} = 'value-${n}'` + }), + '', + 'export const exportsInfo = {', + ...names.map((name) => ` ${name}: __webpack_exports_info__.${name},`), + '}', + '', + ].join('\n') + +// Read through a namespace binding with a computed key, so this one has to keep its names. +writeFileSync('many.js', moduleSource()) +// Imported entirely by name, so this one is mangled with a two-character table. +writeFileSync('many-named.js', moduleSource()) + +writeFileSync( + 'consume.js', + [ + 'import {', + ...names.map((name) => ` ${name},`), + ' exportsInfo,', + "} from './many-named'", + '', + 'export const values = () => [', + ...names.map((name) => ` ${name},`), + ']', + '', + 'export { exportsInfo }', + '', + ].join('\n') +) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/index.js new file mode 100644 index 000000000000..d3b6cefea17b --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/index.js @@ -0,0 +1,40 @@ +import * as manyAsNamespace from './many' +import { values, exportsInfo as namedExportsInfo } from './consume' + +// 60 used exports don't fit the 54 single-character identifiers, so the table grows to two +// characters. + +it('should mangle a 60-export module into a two-character table', () => { + const entries = Object.entries(namedExportsInfo) + expect(entries.length).toBe(60) + + for (const [name, info] of entries) { + expect(info.canMangle).toBe(true) + expect(info.mangledName).not.toBe(name) + expect(info.mangledName.length).toBeLessThanOrEqual(2) + } + + // Every name gets a distinct key. + const keys = entries.map(([, info]) => info.mangledName) + expect(new Set(keys).size).toBe(60) + + // Some names still fit in one character: the table is only as long as it has to be, and open + // addressing never pushes a name past the chosen length. + expect(keys.some((key) => key.length === 1)).toBe(true) +}) + +it('should keep all 60 values correct through the mangled keys', () => { + const expected = Array.from( + { length: 60 }, + (_, i) => `value-${String(i).padStart(2, '0')}` + ) + expect(values()).toEqual(expected) +}) + +it('should keep the names of a module read with a computed key', () => { + for (let i = 0; i < 60; i++) { + const n = String(i).padStart(2, '0') + expect(manyAsNamespace[`exportNumber${n}`]).toBe(`value-${n}`) + } + expect(manyAsNamespace.exportsInfo.exportNumber00.canMangle).toBe(false) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many-named.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many-named.js new file mode 100644 index 000000000000..cb050669ef81 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many-named.js @@ -0,0 +1,123 @@ +export const exportNumber00 = 'value-00' +export const exportNumber01 = 'value-01' +export const exportNumber02 = 'value-02' +export const exportNumber03 = 'value-03' +export const exportNumber04 = 'value-04' +export const exportNumber05 = 'value-05' +export const exportNumber06 = 'value-06' +export const exportNumber07 = 'value-07' +export const exportNumber08 = 'value-08' +export const exportNumber09 = 'value-09' +export const exportNumber10 = 'value-10' +export const exportNumber11 = 'value-11' +export const exportNumber12 = 'value-12' +export const exportNumber13 = 'value-13' +export const exportNumber14 = 'value-14' +export const exportNumber15 = 'value-15' +export const exportNumber16 = 'value-16' +export const exportNumber17 = 'value-17' +export const exportNumber18 = 'value-18' +export const exportNumber19 = 'value-19' +export const exportNumber20 = 'value-20' +export const exportNumber21 = 'value-21' +export const exportNumber22 = 'value-22' +export const exportNumber23 = 'value-23' +export const exportNumber24 = 'value-24' +export const exportNumber25 = 'value-25' +export const exportNumber26 = 'value-26' +export const exportNumber27 = 'value-27' +export const exportNumber28 = 'value-28' +export const exportNumber29 = 'value-29' +export const exportNumber30 = 'value-30' +export const exportNumber31 = 'value-31' +export const exportNumber32 = 'value-32' +export const exportNumber33 = 'value-33' +export const exportNumber34 = 'value-34' +export const exportNumber35 = 'value-35' +export const exportNumber36 = 'value-36' +export const exportNumber37 = 'value-37' +export const exportNumber38 = 'value-38' +export const exportNumber39 = 'value-39' +export const exportNumber40 = 'value-40' +export const exportNumber41 = 'value-41' +export const exportNumber42 = 'value-42' +export const exportNumber43 = 'value-43' +export const exportNumber44 = 'value-44' +export const exportNumber45 = 'value-45' +export const exportNumber46 = 'value-46' +export const exportNumber47 = 'value-47' +export const exportNumber48 = 'value-48' +export const exportNumber49 = 'value-49' +export const exportNumber50 = 'value-50' +export const exportNumber51 = 'value-51' +export const exportNumber52 = 'value-52' +export const exportNumber53 = 'value-53' +export const exportNumber54 = 'value-54' +export const exportNumber55 = 'value-55' +export const exportNumber56 = 'value-56' +export const exportNumber57 = 'value-57' +export const exportNumber58 = 'value-58' +export const exportNumber59 = 'value-59' + +export const exportsInfo = { + exportNumber00: __webpack_exports_info__.exportNumber00, + exportNumber01: __webpack_exports_info__.exportNumber01, + exportNumber02: __webpack_exports_info__.exportNumber02, + exportNumber03: __webpack_exports_info__.exportNumber03, + exportNumber04: __webpack_exports_info__.exportNumber04, + exportNumber05: __webpack_exports_info__.exportNumber05, + exportNumber06: __webpack_exports_info__.exportNumber06, + exportNumber07: __webpack_exports_info__.exportNumber07, + exportNumber08: __webpack_exports_info__.exportNumber08, + exportNumber09: __webpack_exports_info__.exportNumber09, + exportNumber10: __webpack_exports_info__.exportNumber10, + exportNumber11: __webpack_exports_info__.exportNumber11, + exportNumber12: __webpack_exports_info__.exportNumber12, + exportNumber13: __webpack_exports_info__.exportNumber13, + exportNumber14: __webpack_exports_info__.exportNumber14, + exportNumber15: __webpack_exports_info__.exportNumber15, + exportNumber16: __webpack_exports_info__.exportNumber16, + exportNumber17: __webpack_exports_info__.exportNumber17, + exportNumber18: __webpack_exports_info__.exportNumber18, + exportNumber19: __webpack_exports_info__.exportNumber19, + exportNumber20: __webpack_exports_info__.exportNumber20, + exportNumber21: __webpack_exports_info__.exportNumber21, + exportNumber22: __webpack_exports_info__.exportNumber22, + exportNumber23: __webpack_exports_info__.exportNumber23, + exportNumber24: __webpack_exports_info__.exportNumber24, + exportNumber25: __webpack_exports_info__.exportNumber25, + exportNumber26: __webpack_exports_info__.exportNumber26, + exportNumber27: __webpack_exports_info__.exportNumber27, + exportNumber28: __webpack_exports_info__.exportNumber28, + exportNumber29: __webpack_exports_info__.exportNumber29, + exportNumber30: __webpack_exports_info__.exportNumber30, + exportNumber31: __webpack_exports_info__.exportNumber31, + exportNumber32: __webpack_exports_info__.exportNumber32, + exportNumber33: __webpack_exports_info__.exportNumber33, + exportNumber34: __webpack_exports_info__.exportNumber34, + exportNumber35: __webpack_exports_info__.exportNumber35, + exportNumber36: __webpack_exports_info__.exportNumber36, + exportNumber37: __webpack_exports_info__.exportNumber37, + exportNumber38: __webpack_exports_info__.exportNumber38, + exportNumber39: __webpack_exports_info__.exportNumber39, + exportNumber40: __webpack_exports_info__.exportNumber40, + exportNumber41: __webpack_exports_info__.exportNumber41, + exportNumber42: __webpack_exports_info__.exportNumber42, + exportNumber43: __webpack_exports_info__.exportNumber43, + exportNumber44: __webpack_exports_info__.exportNumber44, + exportNumber45: __webpack_exports_info__.exportNumber45, + exportNumber46: __webpack_exports_info__.exportNumber46, + exportNumber47: __webpack_exports_info__.exportNumber47, + exportNumber48: __webpack_exports_info__.exportNumber48, + exportNumber49: __webpack_exports_info__.exportNumber49, + exportNumber50: __webpack_exports_info__.exportNumber50, + exportNumber51: __webpack_exports_info__.exportNumber51, + exportNumber52: __webpack_exports_info__.exportNumber52, + exportNumber53: __webpack_exports_info__.exportNumber53, + exportNumber54: __webpack_exports_info__.exportNumber54, + exportNumber55: __webpack_exports_info__.exportNumber55, + exportNumber56: __webpack_exports_info__.exportNumber56, + exportNumber57: __webpack_exports_info__.exportNumber57, + exportNumber58: __webpack_exports_info__.exportNumber58, + exportNumber59: __webpack_exports_info__.exportNumber59, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many.js new file mode 100644 index 000000000000..cb050669ef81 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/input/many.js @@ -0,0 +1,123 @@ +export const exportNumber00 = 'value-00' +export const exportNumber01 = 'value-01' +export const exportNumber02 = 'value-02' +export const exportNumber03 = 'value-03' +export const exportNumber04 = 'value-04' +export const exportNumber05 = 'value-05' +export const exportNumber06 = 'value-06' +export const exportNumber07 = 'value-07' +export const exportNumber08 = 'value-08' +export const exportNumber09 = 'value-09' +export const exportNumber10 = 'value-10' +export const exportNumber11 = 'value-11' +export const exportNumber12 = 'value-12' +export const exportNumber13 = 'value-13' +export const exportNumber14 = 'value-14' +export const exportNumber15 = 'value-15' +export const exportNumber16 = 'value-16' +export const exportNumber17 = 'value-17' +export const exportNumber18 = 'value-18' +export const exportNumber19 = 'value-19' +export const exportNumber20 = 'value-20' +export const exportNumber21 = 'value-21' +export const exportNumber22 = 'value-22' +export const exportNumber23 = 'value-23' +export const exportNumber24 = 'value-24' +export const exportNumber25 = 'value-25' +export const exportNumber26 = 'value-26' +export const exportNumber27 = 'value-27' +export const exportNumber28 = 'value-28' +export const exportNumber29 = 'value-29' +export const exportNumber30 = 'value-30' +export const exportNumber31 = 'value-31' +export const exportNumber32 = 'value-32' +export const exportNumber33 = 'value-33' +export const exportNumber34 = 'value-34' +export const exportNumber35 = 'value-35' +export const exportNumber36 = 'value-36' +export const exportNumber37 = 'value-37' +export const exportNumber38 = 'value-38' +export const exportNumber39 = 'value-39' +export const exportNumber40 = 'value-40' +export const exportNumber41 = 'value-41' +export const exportNumber42 = 'value-42' +export const exportNumber43 = 'value-43' +export const exportNumber44 = 'value-44' +export const exportNumber45 = 'value-45' +export const exportNumber46 = 'value-46' +export const exportNumber47 = 'value-47' +export const exportNumber48 = 'value-48' +export const exportNumber49 = 'value-49' +export const exportNumber50 = 'value-50' +export const exportNumber51 = 'value-51' +export const exportNumber52 = 'value-52' +export const exportNumber53 = 'value-53' +export const exportNumber54 = 'value-54' +export const exportNumber55 = 'value-55' +export const exportNumber56 = 'value-56' +export const exportNumber57 = 'value-57' +export const exportNumber58 = 'value-58' +export const exportNumber59 = 'value-59' + +export const exportsInfo = { + exportNumber00: __webpack_exports_info__.exportNumber00, + exportNumber01: __webpack_exports_info__.exportNumber01, + exportNumber02: __webpack_exports_info__.exportNumber02, + exportNumber03: __webpack_exports_info__.exportNumber03, + exportNumber04: __webpack_exports_info__.exportNumber04, + exportNumber05: __webpack_exports_info__.exportNumber05, + exportNumber06: __webpack_exports_info__.exportNumber06, + exportNumber07: __webpack_exports_info__.exportNumber07, + exportNumber08: __webpack_exports_info__.exportNumber08, + exportNumber09: __webpack_exports_info__.exportNumber09, + exportNumber10: __webpack_exports_info__.exportNumber10, + exportNumber11: __webpack_exports_info__.exportNumber11, + exportNumber12: __webpack_exports_info__.exportNumber12, + exportNumber13: __webpack_exports_info__.exportNumber13, + exportNumber14: __webpack_exports_info__.exportNumber14, + exportNumber15: __webpack_exports_info__.exportNumber15, + exportNumber16: __webpack_exports_info__.exportNumber16, + exportNumber17: __webpack_exports_info__.exportNumber17, + exportNumber18: __webpack_exports_info__.exportNumber18, + exportNumber19: __webpack_exports_info__.exportNumber19, + exportNumber20: __webpack_exports_info__.exportNumber20, + exportNumber21: __webpack_exports_info__.exportNumber21, + exportNumber22: __webpack_exports_info__.exportNumber22, + exportNumber23: __webpack_exports_info__.exportNumber23, + exportNumber24: __webpack_exports_info__.exportNumber24, + exportNumber25: __webpack_exports_info__.exportNumber25, + exportNumber26: __webpack_exports_info__.exportNumber26, + exportNumber27: __webpack_exports_info__.exportNumber27, + exportNumber28: __webpack_exports_info__.exportNumber28, + exportNumber29: __webpack_exports_info__.exportNumber29, + exportNumber30: __webpack_exports_info__.exportNumber30, + exportNumber31: __webpack_exports_info__.exportNumber31, + exportNumber32: __webpack_exports_info__.exportNumber32, + exportNumber33: __webpack_exports_info__.exportNumber33, + exportNumber34: __webpack_exports_info__.exportNumber34, + exportNumber35: __webpack_exports_info__.exportNumber35, + exportNumber36: __webpack_exports_info__.exportNumber36, + exportNumber37: __webpack_exports_info__.exportNumber37, + exportNumber38: __webpack_exports_info__.exportNumber38, + exportNumber39: __webpack_exports_info__.exportNumber39, + exportNumber40: __webpack_exports_info__.exportNumber40, + exportNumber41: __webpack_exports_info__.exportNumber41, + exportNumber42: __webpack_exports_info__.exportNumber42, + exportNumber43: __webpack_exports_info__.exportNumber43, + exportNumber44: __webpack_exports_info__.exportNumber44, + exportNumber45: __webpack_exports_info__.exportNumber45, + exportNumber46: __webpack_exports_info__.exportNumber46, + exportNumber47: __webpack_exports_info__.exportNumber47, + exportNumber48: __webpack_exports_info__.exportNumber48, + exportNumber49: __webpack_exports_info__.exportNumber49, + exportNumber50: __webpack_exports_info__.exportNumber50, + exportNumber51: __webpack_exports_info__.exportNumber51, + exportNumber52: __webpack_exports_info__.exportNumber52, + exportNumber53: __webpack_exports_info__.exportNumber53, + exportNumber54: __webpack_exports_info__.exportNumber54, + exportNumber55: __webpack_exports_info__.exportNumber55, + exportNumber56: __webpack_exports_info__.exportNumber56, + exportNumber57: __webpack_exports_info__.exportNumber57, + exportNumber58: __webpack_exports_info__.exportNumber58, + exportNumber59: __webpack_exports_info__.exportNumber59, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/options.json new file mode 100644 index 000000000000..8e711cc5db1d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-many-exports/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": true +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/index.js new file mode 100644 index 000000000000..53e80b898f03 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/index.js @@ -0,0 +1,37 @@ +// `mangleExportNames` is on, but minification is off. Mangling and minification are independent +// options — this fixture proves that mangling still happens without minification, the same way it +// would with it (see `mangle-basic`). + +import { + a, + b, + foo, + reallyLongExportName, + anotherVeryLongExportName, + shortFn, + thisIsAVeryLongFunctionName, + exportsInfo, +} from './named-exports' + +it('should keep the values correct with minification disabled', () => { + expect(a).toBe('short-a') + expect(b).toBe('short-b') + expect(foo).toBe('short-foo') + expect(reallyLongExportName).toBe('long-name-1') + expect(anotherVeryLongExportName).toBe('long-name-2') + expect(shortFn()).toBe('short-fn') + expect(thisIsAVeryLongFunctionName()).toBe('long-fn') +}) + +it('should still mangle the exported names with minification disabled', () => { + expect(exportsInfo.reallyLongExportName.canMangle).toBe(true) + expect(exportsInfo.reallyLongExportName.mangledName).toEqual( + expect.any(String) + ) + expect( + exportsInfo.reallyLongExportName.mangledName.length + ).toBeLessThanOrEqual(2) + expect(exportsInfo.thisIsAVeryLongFunctionName.mangledName).not.toBe( + 'thisIsAVeryLongFunctionName' + ) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/named-exports.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/named-exports.js new file mode 100644 index 000000000000..2d9a96e3cce4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/input/named-exports.js @@ -0,0 +1,23 @@ +// Every export here is imported by name, so the keys this module emits are only ever read by +// generated code and can be shortened. + +export const a = 'short-a' +export const b = 'short-b' +export const foo = 'short-foo' +export const reallyLongExportName = 'long-name-1' +export const anotherVeryLongExportName = 'long-name-2' + +export function shortFn() { + return 'short-fn' +} + +export function thisIsAVeryLongFunctionName() { + return 'long-fn' +} + +export const exportsInfo = { + a: __webpack_exports_info__.a, + reallyLongExportName: __webpack_exports_info__.reallyLongExportName, + thisIsAVeryLongFunctionName: + __webpack_exports_info__.thisIsAVeryLongFunctionName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/options.json new file mode 100644 index 000000000000..84cb4a0f04b4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-minify-off/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": false, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/index.js new file mode 100644 index 000000000000..dd7696ab5d41 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/index.js @@ -0,0 +1,38 @@ +import { + toString, + valueOf, + constructor, + __proto__, + a, + $1, + __1, + aVeryLongExportNameIndeed, + exportsInfo, +} from './lib' + +it('should mangle exports whose names shadow Object.prototype members', () => { + expect(toString).toBe('toString-value') + expect(valueOf).toBe('valueOf-value') + expect(constructor).toBe('constructor-value') + expect(__proto__).toBe('proto-value') + expect(aVeryLongExportNameIndeed).toBe('long') +}) + +it('should keep short names and mangle the rest', () => { + expect(a).toBe('single char') + expect($1).toBe('double char') + expect(__1).toBe('3 chars') + + // `a` and `$` are single valid identifiers in the table's alphabet, so they keep themselves. + expect(exportsInfo.a.mangledName).toBe('a') + // `$1` and `__1` are longer than the table's identifier length, so they get hashed like any + // other name — including `__1`, which is not even representable in the encoding (trailing + // "zero" characters are rejected as degenerate). + expect(exportsInfo.$1.mangledName).not.toBe('$1') + expect(exportsInfo.__1.mangledName).not.toBe('__1') +}) + +it('should give every export a distinct key', () => { + const keys = Object.values(exportsInfo).map((e) => e.mangledName) + expect(new Set(keys).size).toBe(keys.length) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/lib.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/lib.js new file mode 100644 index 000000000000..41bf7f31ae6a --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/input/lib.js @@ -0,0 +1,20 @@ +// Export names that are awkward for a mangling table: names that collide with +// `Object.prototype` members, and names that are already short enough to be kept. + +export const toString = 'toString-value' +export const valueOf = 'valueOf-value' +export const constructor = 'constructor-value' +export const __proto__ = 'proto-value' +export const a = 'single char' +export const $1 = 'double char' +export const __1 = '3 chars' +export const aVeryLongExportNameIndeed = 'long' + +export const exportsInfo = { + toString: __webpack_exports_info__.toString, + valueOf: __webpack_exports_info__.valueOf, + a: __webpack_exports_info__.a, + $1: __webpack_exports_info__.$1, + __1: __webpack_exports_info__.__1, + aVeryLongExportNameIndeed: __webpack_exports_info__.aVeryLongExportNameIndeed, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-object-prop/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/index.js new file mode 100644 index 000000000000..2d26cb76be57 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/index.js @@ -0,0 +1,35 @@ +// The control for `mangle-basic`: identical input, `mangleExportNames` off. Same observable +// behaviour, and no export key is renamed. + +import { + a, + b, + foo, + reallyLongExportName, + anotherVeryLongExportName, + shortFn, + thisIsAVeryLongFunctionName, + exportsInfo, +} from './named-exports' + +it('should keep the values correct with mangling disabled', () => { + expect(a).toBe('short-a') + expect(b).toBe('short-b') + expect(foo).toBe('short-foo') + expect(reallyLongExportName).toBe('long-name-1') + expect(anotherVeryLongExportName).toBe('long-name-2') + expect(shortFn()).toBe('short-fn') + expect(thisIsAVeryLongFunctionName()).toBe('long-fn') +}) + +it('should report canMangle: false and mangledName: null when disabled', () => { + // With the option off, `canMangle`/`mangledName` are still present (the shape of + // `__webpack_exports_info__` no longer depends on whether mangling is enabled at all), but + // report that nothing was mangled: `canMangle` is always false, and `mangledName` is only ever + // a string when `canMangle` is true, so it's `null` here. + expect(exportsInfo.a.used).toBe(true) + expect(exportsInfo.a.canMangle).toBe(false) + expect(exportsInfo.a.mangledName).toBe(null) + expect(exportsInfo.reallyLongExportName.canMangle).toBe(false) + expect(exportsInfo.reallyLongExportName.mangledName).toBe(null) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/named-exports.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/named-exports.js new file mode 100644 index 000000000000..2d9a96e3cce4 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/input/named-exports.js @@ -0,0 +1,23 @@ +// Every export here is imported by name, so the keys this module emits are only ever read by +// generated code and can be shortened. + +export const a = 'short-a' +export const b = 'short-b' +export const foo = 'short-foo' +export const reallyLongExportName = 'long-name-1' +export const anotherVeryLongExportName = 'long-name-2' + +export function shortFn() { + return 'short-fn' +} + +export function thisIsAVeryLongFunctionName() { + return 'long-fn' +} + +export const exportsInfo = { + a: __webpack_exports_info__.a, + reallyLongExportName: __webpack_exports_info__.reallyLongExportName, + thisIsAVeryLongFunctionName: + __webpack_exports_info__.thisIsAVeryLongFunctionName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/options.json new file mode 100644 index 000000000000..693b36a63639 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-off/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": false, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/a.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/a.js new file mode 100644 index 000000000000..7ba2297fc0e1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/a.js @@ -0,0 +1,15 @@ +export const veryLongOriginalExportName = 'from-a' + +export function anotherLongFunctionName() { + return 'func-a' +} + +export default function () { + return 'default-from-a' +} + +export const exportsInfo = { + veryLongOriginalExportName: + __webpack_exports_info__.veryLongOriginalExportName, + anotherLongFunctionName: __webpack_exports_info__.anotherLongFunctionName, +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/b.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/b.js new file mode 100644 index 000000000000..69c4553e6b1c --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/b.js @@ -0,0 +1,6 @@ +export { + veryLongOriginalExportName as renamedInMiddleLayer, + anotherLongFunctionName as middleLayerFunction, + exportsInfo, +} from './a' +export { default as defaultFromA } from './a' diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/c.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/c.js new file mode 100644 index 000000000000..b3be560116f7 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/c.js @@ -0,0 +1,12 @@ +export { + renamedInMiddleLayer as finalExportName, + middleLayerFunction as finalFunctionName, + defaultFromA as finalDefaultExport, + exportsInfo, +} from './b' + +export const localInC = 'local-c' + +export default function () { + return 'default-from-c' +} diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/index.js new file mode 100644 index 000000000000..f0c654344731 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/input/index.js @@ -0,0 +1,40 @@ +import { + finalExportName, + finalFunctionName, + finalDefaultExport, + localInC, + exportsInfo, +} from './c' +import defaultFromC from './c' +import { renamedInMiddleLayer, middleLayerFunction } from './b' +import { veryLongOriginalExportName, anotherLongFunctionName } from './a' + +it('should carry values through a re-export chain', () => { + // Each module in the chain mangles its own keys independently; every hop has to resolve to the + // key of the module that actually produces the binding. + expect(finalExportName).toBe('from-a') + expect(finalFunctionName()).toBe('func-a') + expect(localInC).toBe('local-c') +}) + +it('should carry default exports through a re-export chain', () => { + expect(finalDefaultExport()).toBe('default-from-a') + expect(defaultFromC()).toBe('default-from-c') +}) + +it('should support importing from the middle and the source of the chain', () => { + expect(renamedInMiddleLayer).toBe('from-a') + expect(middleLayerFunction()).toBe('func-a') + expect(veryLongOriginalExportName).toBe('from-a') + expect(anotherLongFunctionName()).toBe('func-a') +}) + +it('should mangle the source module of the chain', () => { + expect(exportsInfo.veryLongOriginalExportName.canMangle).toBe(true) + expect(exportsInfo.veryLongOriginalExportName.mangledName).not.toBe( + 'veryLongOriginalExportName' + ) + expect(exportsInfo.anotherLongFunctionName.mangledName).not.toBe( + exportsInfo.veryLongOriginalExportName.mangledName + ) +}) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/options.json b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/options.json new file mode 100644 index 000000000000..c06a1097d5b0 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/exports/mangle-reexport-chain/options.json @@ -0,0 +1,5 @@ +{ + "mangleExportNames": true, + "minify": true, + "scopeHoisting": false +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot.rs b/turbopack/crates/turbopack-tests/tests/snapshot.rs index 9a6e3cfc1a8e..9f6a97342939 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot.rs +++ b/turbopack/crates/turbopack-tests/tests/snapshot.rs @@ -96,6 +96,8 @@ struct SnapshotOptions { #[serde(default)] cjs_tree_shaking: bool, #[serde(default)] + mangle_export_names: bool, + #[serde(default)] cjs_scope_hoisting: bool, #[serde(default)] cross_module_constants: bool, @@ -145,6 +147,7 @@ impl Default for SnapshotOptions { remove_unused_imports: false, remove_unused_exports: false, cjs_tree_shaking: false, + mangle_export_names: false, cjs_scope_hoisting: false, cross_module_constants: false, scope_hoisting: false, @@ -422,6 +425,7 @@ async fn run_test_operation(resource: RcStr) -> Result> { ignore_dynamic_requests: true, infer_module_side_effects: true, cjs_tree_shaking: options.cjs_tree_shaking, + mangle_export_names: options.mangle_export_names, cjs_scope_hoisting: options.cjs_scope_hoisting, cross_module_constants: options.cross_module_constants, enable_exports_info_inlining: true, diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js new file mode 100644 index 000000000000..82e98c0a1e99 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js @@ -0,0 +1,3 @@ +// A namespace object of this module escapes, so it keeps its original export names. +export const someLongExportName = 'long-1' +export const anotherLongExportName = 'long-2' diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js new file mode 100644 index 000000000000..0c71ff2925df --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js @@ -0,0 +1,6 @@ +import * as escaping from './escaping.js' +import { someLongExportName, anotherLongExportName } from './mangled.js' + +const leak = () => escaping + +console.log(leak(), someLongExportName, anotherLongExportName) diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js new file mode 100644 index 000000000000..9022b862b05f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js @@ -0,0 +1,3 @@ +// Nothing escapes here, so this module in the same graph is still mangled. +export const someLongExportName = 'mangled-1' +export const anotherLongExportName = 'mangled-2' diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/options.json b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/options.json new file mode 100644 index 000000000000..d71ee065972b --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/options.json @@ -0,0 +1,12 @@ +{ + "runtime": "NodeJs", + "mangleExportNames": true, + "removeUnusedExports": true, + "removeUnusedImports": true, + "followReexports": true, + "minifyType": { + "Minify": { + "mangle": "optimal-size" + } + } +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js new file mode 100644 index 000000000000..62a9bf3445ce --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js @@ -0,0 +1,3 @@ +module.exports=["[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js [test] (ecmascript)",t=>{"use strict";t.s(["anotherLongExportName",0,"long-2","someLongExportName",0,"long-1"])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js [test] (ecmascript)",t=>{"use strict";var s=t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js [test] (ecmascript)"),e=t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js [test] (ecmascript)");console.log(s,e.z,e.T),t.s([])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js [test] (ecmascript)",t=>{"use strict";t.s(["T",0,"mangled-2","z",0,"mangled-1"])}]; + +//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map new file mode 100644 index 000000000000..e6eceb194eb1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js","turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js","turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js"],"sourcesContent":["// A namespace object of this module escapes, so it keeps its original export names.\nexport const someLongExportName = 'long-1'\nexport const anotherLongExportName = 'long-2'\n","import * as escaping from './escaping.js'\nimport { someLongExportName, anotherLongExportName } from './mangled.js'\n\nconst leak = () => escaping\n\nconsole.log(leak(), someLongExportName, anotherLongExportName)\n","// Nothing escapes here, so this module in the same graph is still mangled.\nexport const someLongExportName = 'mangled-1'\nexport const anotherLongExportName = 'mangled-2'\n"],"names":["someLongExportName","anotherLongExportName","leak","imported module [project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js [test] (ecmascript)","console","log"],"mappings":"oMAEqC,gCADH,8JCDlC,IAAA,EAAA,EAAA,CAAA,CAAA,qIACA,EAAA,EAAA,CAAA,CAAA,oIAIAI,QAAQC,GAAG,CAFQF,AAEPD,EAAQ,EAAA,CAAkB,CAAE,EAAA,CAAqB,0KCHxB,kBADH"} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js new file mode 100644 index 000000000000..af1ff6b42072 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js @@ -0,0 +1,4 @@ +var RUNTIME_PUBLIC_PATH = "output/[turbopack]_runtime.js"; +var RELATIVE_ROOT_PATH = "../../../../../../.."; +var ASSET_PREFIX = "/"; +// Dummy runtime \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/[turbopack]_runtime.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js new file mode 100644 index 000000000000..1aa6f3e5e716 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js @@ -0,0 +1,4 @@ +var R=require("./[turbopack]_runtime.js")("output/index.entry.js") +R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js") +R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js [test] (ecmascript)") +module.exports=R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js [test] (ecmascript)").exports \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js new file mode 100644 index 000000000000..5909d8dcddd1 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js @@ -0,0 +1,15 @@ +import { + a, + bb, + someLongExportName, + anotherLongExportName, + aLongFunctionName, +} from './lib.js' + +console.log( + a, + bb, + someLongExportName, + anotherLongExportName, + aLongFunctionName() +) diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js new file mode 100644 index 000000000000..7b94ef2bf30e --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js @@ -0,0 +1,9 @@ +export const a = 'a' +export const bb = 'bb' +export const someLongExportName = 'long-1' +export const anotherLongExportName = 'long-2' +export const unusedLongExportName = 'unused' + +export function aLongFunctionName() { + return 'fn' +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/options.json b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/options.json new file mode 100644 index 000000000000..d71ee065972b --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/options.json @@ -0,0 +1,12 @@ +{ + "runtime": "NodeJs", + "mangleExportNames": true, + "removeUnusedExports": true, + "removeUnusedImports": true, + "followReexports": true, + "minifyType": { + "Minify": { + "mangle": "optimal-size" + } + } +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js new file mode 100644 index 000000000000..dd1cadf682a2 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js @@ -0,0 +1,3 @@ +module.exports=["[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js [test] (ecmascript)",t=>{"use strict";var s=t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js [test] (ecmascript)");console.log(s.a,s.$,s.z,s.T,(0,s.X)()),t.s([])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js [test] (ecmascript)",t=>{"use strict";t.s(["a",0,"a","X",0,function(){return"fn"},"T",0,"long-2","$",0,"bb","z",0,"long-1"])}]; + +//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map new file mode 100644 index 000000000000..eb20fdbc2b38 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js","turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js"],"sourcesContent":["import {\n a,\n bb,\n someLongExportName,\n anotherLongExportName,\n aLongFunctionName,\n} from './lib.js'\n\nconsole.log(\n a,\n bb,\n someLongExportName,\n anotherLongExportName,\n aLongFunctionName()\n)\n","export const a = 'a'\nexport const bb = 'bb'\nexport const someLongExportName = 'long-1'\nexport const anotherLongExportName = 'long-2'\nexport const unusedLongExportName = 'unused'\n\nexport function aLongFunctionName() {\n return 'fn'\n}\n"],"names":["console","log","a","bb","someLongExportName","anotherLongExportName","unusedLongExportName","aLongFunctionName"],"mappings":"mKAAA,IAAA,EAAA,EAAA,CAAA,CAAA,iIAQAA,QAAQC,GAAG,CACT,EAAA,CAAC,CACD,EAAA,CAAE,CACF,EAAA,CAAkB,CAClB,EAAA,CAAqB,CACrB,CAAA,EAAA,EAAA,CAAA,AAAiB,0KCbF,UAMV,SAASM,EACd,MAAO,IACT,QALqC,eAFnB,WACgB"} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js new file mode 100644 index 000000000000..af1ff6b42072 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js @@ -0,0 +1,4 @@ +var RUNTIME_PUBLIC_PATH = "output/[turbopack]_runtime.js"; +var RELATIVE_ROOT_PATH = "../../../../../../.."; +var ASSET_PREFIX = "/"; +// Dummy runtime \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/[turbopack]_runtime.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js new file mode 100644 index 000000000000..add990f5c959 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js @@ -0,0 +1,4 @@ +var R=require("./[turbopack]_runtime.js")("output/index.entry.js") +R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js") +R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js [test] (ecmascript)") +module.exports=R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js [test] (ecmascript)").exports \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js new file mode 100644 index 000000000000..580ba9a7cf99 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js @@ -0,0 +1,3 @@ +import { aLongFunctionNameNobodyWantsInTheBundle } from './lone' + +console.log(aLongFunctionNameNobodyWantsInTheBundle()) diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js new file mode 100644 index 000000000000..72ac87843d5f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js @@ -0,0 +1,6 @@ +// The only export of this module. It is emitted under the fixed single-export key rather than a +// hashed one, so that every single-export module in the graph produces the same `.f` byte sequence +// and gzip can share it. Visible in the committed output below. +export function aLongFunctionNameNobodyWantsInTheBundle() { + return 'lone' +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/options.json b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/options.json new file mode 100644 index 000000000000..d71ee065972b --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/options.json @@ -0,0 +1,12 @@ +{ + "runtime": "NodeJs", + "mangleExportNames": true, + "removeUnusedExports": true, + "removeUnusedImports": true, + "followReexports": true, + "minifyType": { + "Minify": { + "mangle": "optimal-size" + } + } +} diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js new file mode 100644 index 000000000000..a56a17553866 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js @@ -0,0 +1,3 @@ +module.exports=["[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js [test] (ecmascript)",t=>{"use strict";console.log((0,t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js [test] (ecmascript)").f)()),t.s([])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js [test] (ecmascript)",t=>{"use strict";t.s(["f",0,function(){return"lone"}])}]; + +//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map new file mode 100644 index 000000000000..bd58435855c8 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js","turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js"],"sourcesContent":["import { aLongFunctionNameNobodyWantsInTheBundle } from './lone'\n\nconsole.log(aLongFunctionNameNobodyWantsInTheBundle())\n","// The only export of this module. It is emitted under the fixed single-export key rather than a\n// hashed one, so that every single-export module in the graph produces the same `.f` byte sequence\n// and gzip can share it. Visible in the committed output below.\nexport function aLongFunctionNameNobodyWantsInTheBundle() {\n return 'lone'\n}\n"],"names":["console","log","aLongFunctionNameNobodyWantsInTheBundle"],"mappings":"6JAEAA,QAAQC,GAAG,CAAC,CAAA,EAFZ,AAEY,EAFZ,CAAA,CAAA,4HAEY,CAAA,AAAuC,qKCC5C,SAASC,EACd,MAAO,MACT"} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js new file mode 100644 index 000000000000..af1ff6b42072 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js @@ -0,0 +1,4 @@ +var RUNTIME_PUBLIC_PATH = "output/[turbopack]_runtime.js"; +var RELATIVE_ROOT_PATH = "../../../../../../.."; +var ASSET_PREFIX = "/"; +// Dummy runtime \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/[turbopack]_runtime.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js new file mode 100644 index 000000000000..515f23cd650d --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js @@ -0,0 +1,4 @@ +var R=require("./[turbopack]_runtime.js")("output/index.entry.js") +R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js") +R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js [test] (ecmascript)") +module.exports=R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js [test] (ecmascript)").exports \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js.map new file mode 100644 index 000000000000..c15d7ec00382 --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js.map @@ -0,0 +1,5 @@ +{ + "version": 3, + "sources": [], + "sections": [] +} \ No newline at end of file diff --git a/turbopack/crates/turbopack-wasm/src/module_asset.rs b/turbopack/crates/turbopack-wasm/src/module_asset.rs index 41a502c7393d..68dd2562dba0 100644 --- a/turbopack/crates/turbopack-wasm/src/module_asset.rs +++ b/turbopack/crates/turbopack-wasm/src/module_asset.rs @@ -212,7 +212,9 @@ impl ChunkableModule for WebAssemblyModuleAsset { impl EcmascriptChunkPlaceable for WebAssemblyModuleAsset { #[turbo_tasks::function] fn get_exports(self: Vc) -> Vc { - self.loader().get_exports() + // This module hands out the *loader* module's exports as its own, so they must not carry + // a mangling decision — see `EcmascriptExports::borrowed`. + self.loader().get_exports().borrowed() } #[turbo_tasks::function] diff --git a/turbopack/crates/turbopack/src/module_options/mod.rs b/turbopack/crates/turbopack/src/module_options/mod.rs index c1d235ba4399..1d1663bdc9ac 100644 --- a/turbopack/crates/turbopack/src/module_options/mod.rs +++ b/turbopack/crates/turbopack/src/module_options/mod.rs @@ -247,6 +247,7 @@ impl ModuleOptions { inline_helpers, infer_module_side_effects, cjs_tree_shaking, + mangle_export_names, cjs_scope_hoisting, cross_module_constants, ref preset_env_config, @@ -343,6 +344,7 @@ impl ModuleOptions { inline_helpers, infer_module_side_effects, cjs_tree_shaking, + mangle_export_names, cjs_scope_hoisting, cross_module_constants, ..Default::default() diff --git a/turbopack/crates/turbopack/src/module_options/module_options_context.rs b/turbopack/crates/turbopack/src/module_options/module_options_context.rs index 9aaf21f455bc..488c4b4161de 100644 --- a/turbopack/crates/turbopack/src/module_options/module_options_context.rs +++ b/turbopack/crates/turbopack/src/module_options/module_options_context.rs @@ -284,6 +284,12 @@ pub struct EcmascriptOptionsContext { /// Whether to tree shake unused exports from static CommonJS modules. Defaults to false. pub cjs_tree_shaking: bool, + + /// Whether to shorten ("mangle") the export names a module exposes to other modules, to + /// reduce output size. Only affects the keys used to link modules together, never a name that + /// is observable from user code — modules whose export names can escape keep their original + /// names. Defaults to false. + pub mangle_export_names: bool, /// Whether to scope-hoist static CommonJS modules. Defaults to false. pub cjs_scope_hoisting: bool, From 6f44fd34a1196369800cd8310c61baf6630ecaa2 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Fri, 28 Aug 2026 07:46:17 +0200 Subject: [PATCH 3/5] Turbopack: enable export mangling by default in production builds (#97676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #97672 — enable export mangling by default on canary releases. ### What? Two defaults change: - **Next.js builds.** `experimental.turbopackMangleExportNames` is pinned to `false` for stable releases and left *unset* on canary, where Turbopack then defaults it from the build mode: on for production builds, off in development. Explicitly setting the option wins in either direction, so setting it to `true` in development is honoured. - **Turbopack's own execution test suite.** `TestOptions::mangle_export_names` defaults to `true`, so all 279 fixtures exercise mangling instead of only the handful that opt in. ### Why? The feature is verified by turbopack's own fixtures, targeted e2e suites, and a couple of bundle-size measurements — a narrow slice of what Next.js's test suites actually cover. Defaulting it on for canary puts every production-mode e2e and integration test through the mangled code path for real users of the canary channel, without committing stable users to it yet. Turning it on for stable is a separate, later decision that can stack on top of this once canary has soaked it. Broad exposure has already earned its keep several times over. Turning mangling on in CI and in the fixture suite surfaced bugs that no hand-picked suite had found, including a module-fragments helper handing out another module's exports value (#97672's `EcmascriptExports::borrowed()`), a client-reference proxy memoized per wrapper instance rather than per content, and a code-elimination bug for export-less modules (fixed in the follow-up PR). ### How? `defaultConfig` cannot see the build mode, so it only expresses the stable/canary split (`isStableBuild() ? false : undefined`) and `NextConfig::turbopack_mangle_export_names(mode)` supplies the mode-dependent default on the Turbopack side. This is deliberately *not* the shape of the neighbouring `turbopackSharedRuntime: !isStableBuild()`: mangling should not apply in development, and hard-forcing `false` there in Rust — as an earlier revision of this PR did — would silently ignore a user who asked for it explicitly. Mangling does **not** depend on minification. An earlier version of this PR gated it on `minify(mode)`, which was wrong and has been removed; `--no-mangling` is a minifier flag and does not affect export mangling. Verified with a real `next build` A/B on a small app: the option left unset mangles (959,174 B total emitted JS, 293,407 B gzipped) while explicitly setting it to `false` does not (966,519 B / 294,963 B gzipped), confirming both the default and the override. All 279 execution fixtures pass with the suite default flipped, with no fixture needing an opt-out. Note this layer does not change the `turbopack-emit-collect` snapshots on its own: making a module without re-exports split additionally requires the split trigger added in the follow-up PR, so those snapshots move there. This is the layer to revert if canary turns up problems specific to running with the option on by default; #97672 remains useful as an opt-in feature either way. Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> --------- 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> --- crates/next-core/src/next_client/context.rs | 2 +- crates/next-core/src/next_config.rs | 19 +++++++++++++------ crates/next-core/src/next_server/context.rs | 2 +- packages/next/src/server/config-shared.ts | 4 ++++ .../crates/turbopack-tests/tests/execution.rs | 4 ++-- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/next-core/src/next_client/context.rs b/crates/next-core/src/next_client/context.rs index 937d61fac645..26c5078cdd1f 100644 --- a/crates/next-core/src/next_client/context.rs +++ b/crates/next-core/src/next_client/context.rs @@ -376,7 +376,7 @@ pub async fn get_client_module_options_context( source_maps, infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?, cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?, - mangle_export_names: *next_config.turbopack_mangle_export_names().await?, + mangle_export_names: *next_config.turbopack_mangle_export_names(mode).await?, cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?, cross_module_constants: *next_config.turbopack_cross_module_constants().await?, preset_env_config, diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index bc055e5f56dd..8267f47ec256 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -2589,13 +2589,20 @@ impl NextConfig { ) } + /// Whether Turbopack should shorten ("mangle") the export names modules expose to each other. + /// + /// An explicit value always wins, in either direction — setting this to `true` in development + /// is honoured. `mode` only supplies the default when the option is unset: on in production + /// builds, off in development, where the extra module splitting costs rebuild time and the + /// short names make debugging harder for no benefit. #[turbo_tasks::function] - pub fn turbopack_mangle_export_names(&self) -> Vc { - Vc::cell( - self.experimental - .turbopack_mangle_export_names - .unwrap_or(false), - ) + pub async fn turbopack_mangle_export_names(&self, mode: Vc) -> Result> { + Ok(Vc::cell( + match self.experimental.turbopack_mangle_export_names { + Some(explicit) => explicit, + None => !mode.await?.is_development(), + }, + )) } #[turbo_tasks::function] diff --git a/crates/next-core/src/next_server/context.rs b/crates/next-core/src/next_server/context.rs index 563351cb8a5a..7c56e5cc268f 100644 --- a/crates/next-core/src/next_server/context.rs +++ b/crates/next-core/src/next_server/context.rs @@ -551,7 +551,7 @@ pub async fn get_server_module_options_context( source_maps, infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?, cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?, - mangle_export_names: *next_config.turbopack_mangle_export_names().await?, + mangle_export_names: *next_config.turbopack_mangle_export_names(mode).await?, cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?, cross_module_constants: *next_config.turbopack_cross_module_constants().await?, ..Default::default() diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 7c69f5125e30..54b8e9448670 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -2381,6 +2381,10 @@ export const defaultConfig = Object.freeze({ turbopackInferModuleSideEffects: true, turbopackPluginRuntimeStrategy: 'childProcesses', turbopackSharedRuntime: !isStableBuild(), + // Pinned off for stable releases. Left unset on canary so the Turbopack side picks the + // default from the build mode (on for production builds, off in development) — see + // `NextConfig::turbopack_mangle_export_names`. An explicit value always wins either way. + turbopackMangleExportNames: isStableBuild() ? false : undefined, }, htmlLimitedBots: undefined, bundlePagesRouterDependencies: false, diff --git a/turbopack/crates/turbopack-tests/tests/execution.rs b/turbopack/crates/turbopack-tests/tests/execution.rs index 79d2c30c55af..981228c5f06d 100644 --- a/turbopack/crates/turbopack-tests/tests/execution.rs +++ b/turbopack/crates/turbopack-tests/tests/execution.rs @@ -280,7 +280,7 @@ struct TestOptions { infer_module_side_effects: bool, #[serde(default)] cjs_tree_shaking: bool, - #[serde(default)] + #[serde(default = "default_true")] mangle_export_names: bool, #[serde(default = "default_true")] cross_module_constants: bool, @@ -314,7 +314,7 @@ impl Default for TestOptions { remove_unused_imports: default_true(), scope_hoisting: default_true(), cjs_tree_shaking: false, - mangle_export_names: false, + mangle_export_names: default_true(), cjs_scope_hoisting: false, cross_module_constants: true, infer_module_side_effects: default_true(), From 17a901a74fc207af1a59842c92cb7746c4e7e8c4 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Fri, 28 Aug 2026 08:31:06 +0200 Subject: [PATCH 4/5] Guard filesystem reads against unresolved symlinks (#97902) ### What? Adds debug-only OS realpath validation to successful `DiskFileSystem` file and directory reads. When a successfully canonicalized path differs from the supplied path, the read returns a normal task error naming both paths. Fixes pattern/glob traversal and NFT tracing so physical filesystem access uses resolved paths while logical paths remain available for user-visible specifiers and complete symlink-chain recovery. ### Why? Reading through an unresolved symlink parent gives the same filesystem object multiple path identities. That can make Turbo Tasks dependency tracking and invalidation inconsistent and can produce invalid deployment ZIPs when NFT output contains files below unresolved links. The checks return errors rather than asserting because paths can disagree temporarily under eventual consistency. Propagating a task error avoids panicking a worker thread while still exposing invalid callers during development. ### How? The validation lives directly in `DiskFileSystem::read` and `DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API inline instead of the Turbo Tasks realpath task, keeping the diagnostic out of the task dependency graph. The guard runs only after the OS read succeeds, so missing/non-directory probes preserve their existing behavior. `read_matches` resolves each physical directory immediately before enumeration while retaining logical `PatternMatch` paths. `read_glob` and `track_glob` now resolve their initial directory before enumeration. Symlinks discovered later through wildcard segments are also traversed through resolved targets. `ReadGlobResult` deliberately retains logical paths rooted at the supplied base, allowing consumers to call `realpath_with_links` and recover the complete symlink chain. Consumers follow that contract explicitly: - NFT includes expand each logical match with `realpath_with_links`, emit resolved files and every traversed symlink, skip resolved directory targets, and deterministically deduplicate/sort output. - `import.meta.glob` uses recursive logical keys as the source of user-visible requests, while module resolution follows and tracks symlinks. - the hash-glob example resolves returned logical paths before reading. Webpack-loader context dependencies are covered for both `path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader fixture performs its directory read with Node `fs`, reports the directory using `addContextDependency`, and Turbopack tracks the resolved target. ### Verification - `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api -- --check` - `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --all-targets` - `cargo test -p turbo-tasks-fs` (128 passed) - `cargo test -p next-api` (7 passed) - `cargo check -p turbo-tasks-fs --examples` - `import.meta.glob` symlink execution fixture (1 passed) - Nine targeted node-file-trace CI cases with `release-with-assertions` (9 passed) - `pnpm build-all` - `webpack-loader-fs` Turbopack dev e2e (1 passed) - `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed) - twoslash Turbopack production, normal mode (4 passed) - twoslash Turbopack production, cache-components mode (4 passed) - `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200) ### Notes The disk guard is cross-platform, while its symlink-parent regression test is Unix-only, matching neighbouring symlink tests. On Windows, OS canonicalization can also normalize casing and 8.3 short names; a debug read using a non-canonical spelling will therefore return the same diagnostic error. --------- 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> --- crates/next-api/src/nft.rs | 99 +++++++- .../app/glob-target/inner/path/one.txt | 1 + .../webpack-loader-fs/app/path/to/symlink | 1 + .../webpack-loader-fs/test-file-loader.js | 11 +- .../webpack-loader-fs.test.ts | 2 +- .../build-trace-extra-entries-turbo.test.ts | 20 +- .../turbo-tasks-fs/examples/hash_glob.rs | 3 + turbopack/crates/turbo-tasks-fs/src/disk.rs | 112 ++++++++- .../crates/turbo-tasks-fs/src/read_glob.rs | 216 ++++++++++++++++-- .../turbopack-core/src/resolve/pattern.rs | 83 ++++++- .../src/references/import_meta_glob.rs | 12 +- .../resolving/import-meta-glob/input/index.js | 12 + .../resolving/import-meta-glob/input/linked | 1 + 13 files changed, 527 insertions(+), 46 deletions(-) create mode 100644 test/e2e/app-dir/webpack-loader-fs/app/glob-target/inner/path/one.txt create mode 120000 test/e2e/app-dir/webpack-loader-fs/app/path/to/symlink create mode 120000 turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/linked diff --git a/crates/next-api/src/nft.rs b/crates/next-api/src/nft.rs index e425afe16965..933e83c7bd94 100644 --- a/crates/next-api/src/nft.rs +++ b/crates/next-api/src/nft.rs @@ -10,7 +10,7 @@ use turbo_tasks::{ FxIndexMap, FxIndexSet, ReadRef, ResolvedVc, TraitRef, TryFlatJoinIterExt, TryJoinIterExt, Vc, }; use turbo_tasks_fs::{ - DirectoryEntry, FileSystemPath, + DirectoryEntry, FileSystemEntryType, FileSystemPath, glob::{Glob, GlobOptions}, }; use turbo_tasks_hash::HashAlgorithm; @@ -193,19 +193,28 @@ async fn get_glob_includes( let glob_result = project_root_path.read_glob(glob).await?; // Walk the full glob_result using an explicit stack to avoid async recursion overheads. - // Use a BTreeSet to get deterministic order (return value of `read_glob` has random order). - let mut result = vec![]; + // Deduplicate symlinks shared by many matches. The return value of `read_glob` has random + // order, so the result is sorted below. + let mut result = FxHashSet::default(); let mut stack = VecDeque::new(); stack.push_back(glob_result); while let Some(glob_result) = stack.pop_back() { - // Process direct results (files and directories at this level) + // Process direct results (files and directories at this level). for entry in glob_result.results.values() { let (DirectoryEntry::File(file_path) | DirectoryEntry::Symlink(file_path)) = entry else { continue; }; - result.push(file_path.clone()); + // ReadGlobResult paths are logical by contract. Resolve each match here so the NFT + // includes both the physical file and every symlink needed to reach it. + let realpath = file_path.realpath_with_links().await?; + result.extend(realpath.symlinks.iter().cloned()); + if let Ok(resolved_path) = &realpath.path_result + && matches!(*resolved_path.get_type().await?, FileSystemEntryType::File) + { + result.insert(resolved_path.clone()); + } } for nested_result in glob_result.inner.values() { @@ -216,8 +225,8 @@ async fn get_glob_includes( // All paths were matched from project_root_path, so they must all have the same `fs`. So it's // enough to sort by path. + let mut result: Vec<_> = result.into_iter().collect(); result.sort_by(|a, b| a.path.cmp(&b.path)); - Ok(result) } @@ -564,3 +573,81 @@ impl Issue for ForbiddenTracedFileIssue { Ok(Some(StyledString::Stack(stack))) } } + +#[cfg(all(test, unix))] +mod tests { + use std::{ + fs::{create_dir_all, write}, + os::unix::fs::symlink, + }; + + use turbo_rcstr::{RcStr, rcstr}; + use turbo_tasks::Vc; + use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; + use turbo_tasks_fs::{ + DiskFileSystem, FileSystem, + glob::{Glob, GlobOptions}, + }; + + use crate::nft::get_glob_includes; + + #[turbo_tasks::function(operation, root)] + async fn assert_glob_includes_operation(disk_root: RcStr) -> anyhow::Result<()> { + let root = DiskFileSystem::new(rcstr!("test"), Vc::cell(disk_root)) + .root() + .owned() + .await?; + let includes = get_glob_includes( + root, + Glob::new( + rcstr!("**"), + GlobOptions { + contains: true, + ..Default::default() + }, + ), + ) + .await?; + + assert_eq!( + includes + .iter() + .map(|path| path.path.as_str()) + .collect::>(), + [ + "alias", + "alias-chain", + "dangling", + "file-link", + "real/file.txt", + ] + ); + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn glob_includes_resolve_files_and_retain_symlinks() { + let scratch = tempfile::tempdir().unwrap(); + let root = scratch.path(); + create_dir_all(root.join("real")).unwrap(); + write(root.join("real/file.txt"), "content").unwrap(); + symlink("real", root.join("alias")).unwrap(); + symlink("alias", root.join("alias-chain")).unwrap(); + symlink("real/file.txt", root.join("file-link")).unwrap(); + symlink("missing", root.join("dangling")).unwrap(); + + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + let disk_root: RcStr = root.to_str().unwrap().into(); + tt.run_once(async move { + assert_glob_includes_operation(disk_root) + .read_strongly_consistent() + .await?; + anyhow::Ok(()) + }) + .await + .unwrap(); + } +} diff --git a/test/e2e/app-dir/webpack-loader-fs/app/glob-target/inner/path/one.txt b/test/e2e/app-dir/webpack-loader-fs/app/glob-target/inner/path/one.txt new file mode 100644 index 000000000000..5626abf0f72e --- /dev/null +++ b/test/e2e/app-dir/webpack-loader-fs/app/glob-target/inner/path/one.txt @@ -0,0 +1 @@ +one diff --git a/test/e2e/app-dir/webpack-loader-fs/app/path/to/symlink b/test/e2e/app-dir/webpack-loader-fs/app/path/to/symlink new file mode 120000 index 000000000000..f6748fcc8385 --- /dev/null +++ b/test/e2e/app-dir/webpack-loader-fs/app/path/to/symlink @@ -0,0 +1 @@ +../../glob-target \ No newline at end of file diff --git a/test/e2e/app-dir/webpack-loader-fs/test-file-loader.js b/test/e2e/app-dir/webpack-loader-fs/test-file-loader.js index 3f0cc52c1528..66c7e1c8ccd7 100644 --- a/test/e2e/app-dir/webpack-loader-fs/test-file-loader.js +++ b/test/e2e/app-dir/webpack-loader-fs/test-file-loader.js @@ -1,3 +1,4 @@ +const fs = require('fs') const path = require('path') module.exports = async function (content) { @@ -21,5 +22,13 @@ module.exports = async function (content) { res(data) }) ) - return `module.exports = "Buffer read: ${read1 instanceof Buffer ? read1.length : 0}, string read: '${read2.trim()}', binary read: ${read3.length}"` + const globDir = path.join(dir, 'path/to/symlink/inner/path') + const globEntries = await new Promise((res, rej) => + fs.readdir(globDir, (err, entries) => { + if (err) return rej(err) + res(entries) + }) + ) + this.addContextDependency(globDir) + return `module.exports = "Buffer read: ${read1 instanceof Buffer ? read1.length : 0}, string read: '${read2.trim()}', binary read: ${read3.length}, glob read: '${globEntries.sort().join(',')}'"` } diff --git a/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts b/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts index 080d00918e01..f247c44ade27 100644 --- a/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts +++ b/test/e2e/app-dir/webpack-loader-fs/webpack-loader-fs.test.ts @@ -11,7 +11,7 @@ describe('webpack-loader-fs', () => { it('should allow reading the input FS', async () => { const $ = await next.render$('/') expect($('#test').text()).toBe( - "Buffer read: 18, string read: 'this is some data', binary read: 6765" + "Buffer read: 18, string read: 'this is some data', binary read: 6765, glob read: 'one.txt'" ) }) }) diff --git a/test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts b/test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts index 5f5207ca340f..ecf85c2520ea 100644 --- a/test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts +++ b/test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts @@ -74,8 +74,8 @@ describe('build trace with extra entries', () => { ).toBe(true) if (isTurbopack) { // A symlink matched by outputFileTracingIncludes is traced as the symlink itself, even - // when it points at a directory (this used to fail the build with - // `reading file "..." Is a directory (os error 21)`). + // when it points at a directory. Files reached through it are traced at their resolved + // paths so deployment includes both the link and its target contents. // The webpack tracer globs with `nodir: true`, which drops directory symlinks, so this // only applies to Turbopack. expect( @@ -83,11 +83,27 @@ describe('build trace with extra entries', () => { (file: string) => file === '../../../include-me/link-to-dir' ) ).toBe(true) + expect( + tracedFiles.some( + (file: string) => file === '../../../content/hello.json' + ) + ).toBe(true) + expect( + tracedFiles.some( + (file: string) => + file === '../../../include-me/link-to-dir/hello.json' + ) + ).toBe(false) expect( appDirRoute1Trace.files.some( (file: string) => file === '../../../../include-me/link-to-dir' ) ).toBe(true) + expect( + appDirRoute1Trace.files.some( + (file: string) => file === '../../../../content/hello.json' + ) + ).toBe(true) } expect( indexTrace.files.some((file: string) => file.includes('exclude-me')) diff --git a/turbopack/crates/turbo-tasks-fs/examples/hash_glob.rs b/turbopack/crates/turbo-tasks-fs/examples/hash_glob.rs index 84cb1e0e38ba..fdbf861048d5 100644 --- a/turbopack/crates/turbo-tasks-fs/examples/hash_glob.rs +++ b/turbopack/crates/turbo-tasks-fs/examples/hash_glob.rs @@ -98,6 +98,9 @@ async fn hash_glob_result(result: Vc) -> Result> { #[turbo_tasks::function] async fn hash_file(file_path: FileSystemPath) -> Result> { + let Ok(file_path) = file_path.realpath().await? else { + return Ok(empty_string()); + }; let content = file_path.read().await?; Ok(match &*content { FileContent::Content(file) => hash_content(&mut file.read()), diff --git a/turbopack/crates/turbo-tasks-fs/src/disk.rs b/turbopack/crates/turbo-tasks-fs/src/disk.rs index c9b195f1d026..8dd768145de5 100644 --- a/turbopack/crates/turbo-tasks-fs/src/disk.rs +++ b/turbopack/crates/turbo-tasks-fs/src/disk.rs @@ -482,6 +482,22 @@ impl DiskFileSystem { &self.inner.root } + #[cfg(debug_assertions)] + async fn ensure_path_is_realpath(&self, operation: &str, path: &Path) -> Result<()> { + if let Ok(realpath) = retry_blocking(|| fs_err::canonicalize(path)) + .instrument(tracing::info_span!("realpath for filesystem read", name = ?path)) + .concurrency_limited(&self.inner.read_semaphore) + .await + && realpath != path + { + anyhow::bail!( + "{operation} called with unresolved path {path:?}; resolve it to {realpath:?} \ + first" + ); + } + Ok(()) + } + pub fn invalidate(&self) { self.inner.invalidate(); } @@ -797,7 +813,12 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.read_semaphore) .await { - Ok(file) => FileContent::new(file), + Ok(file) => { + #[cfg(debug_assertions)] + self.ensure_path_is_realpath("read_file", &full_path) + .await?; + FileContent::new(file) + } Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::InvalidFilename => { FileContent::NotFound } @@ -823,7 +844,11 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.read_semaphore) .await { - Ok(dir) => dir, + Ok(dir) => { + #[cfg(debug_assertions)] + self.ensure_path_is_realpath("read_dir", &full_path).await?; + dir + } Err(e) if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::NotADirectory @@ -1719,6 +1744,8 @@ mod tests { use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; use super::extract_effects_operation; + #[cfg(all(unix, debug_assertions))] + use crate::{DirectoryContent, FileContent, RawDirectoryContent}; use crate::{ DiskFileSystem, FileSystem, FileSystemEntryType, FileSystemPath, LinkContent, LinkTarget, RealPathErrorType, WriteLinkContent, WriteLinkTarget, WriteLinkTargetType, @@ -1865,6 +1892,87 @@ mod tests { Ok(()) } + #[cfg(all(unix, debug_assertions))] + #[turbo_tasks::function(operation, root)] + async fn assert_read_realpath_operation(root_path: FileSystemPath) -> anyhow::Result<()> { + let unresolved_dir = root_path.join("alias/child")?; + let resolved_dir = unresolved_dir + .realpath() + .await? + .expect("the linked directory should resolve"); + + assert_ne!(unresolved_dir, resolved_dir); + let error = unresolved_dir + .read_dir() + .await + .expect_err("a directory read through a symlinked parent must be rejected"); + let message = format!("{error:#}"); + assert!(message.contains("alias/child")); + assert!(message.contains("real/child")); + assert!(matches!( + &*resolved_dir.read_dir().await?, + DirectoryContent::Entries(entries) if entries.contains_key(&rcstr!("data.txt")) + )); + + assert!(matches!( + &*root_path.join("file-alias")?.raw_read_dir().await?, + RawDirectoryContent::NotFound + )); + + let unresolved_file = unresolved_dir.join("data.txt")?; + let resolved_file = unresolved_file + .realpath() + .await? + .expect("the linked file should resolve"); + assert_ne!(unresolved_file, resolved_file); + let error = unresolved_file + .read() + .await + .expect_err("a file read through a symlinked parent must be rejected"); + let message = format!("{error:#}"); + assert!(message.contains("alias/child/data.txt")); + assert!(message.contains("real/child/data.txt")); + assert!(matches!( + &*resolved_file.read().await?, + FileContent::Content(_) + )); + + Ok(()) + } + + #[cfg(all(unix, debug_assertions))] + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_reads_require_realpath() { + use std::os::unix::fs::symlink; + + let scratch = tempfile::tempdir().unwrap(); + let path = scratch.path().to_owned(); + create_dir_all(path.join("real/child")).unwrap(); + File::create_new(path.join("real/child/data.txt")).unwrap(); + symlink("real", path.join("alias")).unwrap(); + symlink("real/child/data.txt", path.join("file-alias")).unwrap(); + + let root = canonicalize_to_rcstr(&path).unwrap(); + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + + tt.run_once(async move { + let fs = disk_file_system_operation(root) + .resolve() + .strongly_consistent() + .await?; + let root_path = disk_file_system_root(fs); + assert_read_realpath_operation(root_path) + .read_strongly_consistent() + .await?; + anyhow::Ok(()) + }) + .await + .unwrap(); + } + /// `read_link` never looks at the target, so a dangling link still reads back as a valid /// [`LinkContent::Link`]. Resolving it reports the missing target. #[cfg(unix)] diff --git a/turbopack/crates/turbo-tasks-fs/src/read_glob.rs b/turbopack/crates/turbo-tasks-fs/src/read_glob.rs index ac3dc60d6973..d9c2c78dcb57 100644 --- a/turbopack/crates/turbo-tasks-fs/src/read_glob.rs +++ b/turbopack/crates/turbo-tasks-fs/src/read_glob.rs @@ -16,27 +16,42 @@ pub struct ReadGlobResult { pub inner: FxHashMap>, } -/// Reads matches of a glob pattern. Symlinks are not resolved (and returned as-is) +async fn resolve_glob_root(directory: FileSystemPath) -> Result { + Ok(directory + .realpath() + .await? + .unwrap_or_else(|_| directory.clone())) +} + +/// Reads matches of a glob pattern. +/// +/// Directories are resolved before physical enumeration, but [`DirectoryEntry`] paths in the +/// result remain logical paths rooted at the supplied `directory`. Consumers must resolve returned +/// paths before filesystem access when they need the physical path or its symlink chain. /// /// DETERMINISM: Result is in random order. Either sort result or do not depend /// on the order. #[turbo_tasks::function(fs)] pub async fn read_glob(directory: FileSystemPath, glob: Vc) -> Result> { - read_glob_internal("", directory, glob).await + let root = directory.clone(); + let directory = resolve_glob_root(directory).await?; + read_glob_internal("", &root, directory, glob).await } #[turbo_tasks::function(fs)] async fn read_glob_inner( prefix: RcStr, + root: FileSystemPath, directory: FileSystemPath, glob: Vc, ) -> Result> { - read_glob_internal(&prefix, directory, glob).await + read_glob_internal(&prefix, &root, directory, glob).await } -// The `prefix` represents the relative directory path where symlinks are not resolve. +// The `prefix` represents the relative directory path where symlinks are not resolved. async fn read_glob_internal( prefix: &str, + root: &FileSystemPath, directory: FileSystemPath, glob: Vc, ) -> Result> { @@ -58,7 +73,7 @@ async fn read_glob_internal( if glob_value.can_match_in_directory(&entry_path) { result.inner.insert( segment.clone(), - read_glob_inner(entry_path, path.clone(), glob) + read_glob_inner(entry_path, root.clone(), path.clone(), glob) .to_resolved() .await?, ); @@ -75,13 +90,22 @@ async fn read_glob_internal( format!("{prefix}/{segment}").into() }; + let output_path = root.join(&entry_path)?; + let output_entry = match entry { + DirectoryEntry::File(_) => DirectoryEntry::File(output_path), + DirectoryEntry::Directory(_) => DirectoryEntry::Directory(output_path), + DirectoryEntry::Symlink(_) => DirectoryEntry::Symlink(output_path), + DirectoryEntry::Other(_) => DirectoryEntry::Other(output_path), + DirectoryEntry::Error(error) => DirectoryEntry::Error(error.clone()), + }; + match entry { DirectoryEntry::File(_) => { - handle_file(&mut result, &entry_path, segment, entry); + handle_file(&mut result, &entry_path, segment, &output_entry); } DirectoryEntry::Directory(path) => { // Add the directory to `results` if it is a whole match of the glob - handle_file(&mut result, &entry_path, segment, entry); + handle_file(&mut result, &entry_path, segment, &output_entry); // Recursively handle the directory handle_dir(&mut result, entry_path, segment, path).await?; } @@ -91,7 +115,7 @@ async fn read_glob_internal( if let LinkContent::Link { target } = &*link_content { let Ok(realpath) = target.file_system_path().realpath().await? else { // Preserve unresolvable symlinks that match the glob. - handle_file(&mut result, &entry_path, segment, entry); + handle_file(&mut result, &entry_path, segment, &output_entry); continue; }; if matches!(*realpath.get_type().await?, FileSystemEntryType::Directory) @@ -100,11 +124,12 @@ async fn read_glob_internal( check_symlink_directory_recursion(path, &realpath)?; // Add the directory to `results` if it is a whole match of the glob - handle_file(&mut result, &entry_path, segment, entry); - // Recursively handle the directory - handle_dir(&mut result, entry_path, segment, path).await?; + handle_file(&mut result, &entry_path, segment, &output_entry); + // Enumerate the resolved target while preserving logical paths in + // the glob result. + handle_dir(&mut result, entry_path, segment, &realpath).await?; } else { - handle_file(&mut result, &entry_path, segment, entry); + handle_file(&mut result, &entry_path, segment, &output_entry); } } } @@ -157,15 +182,17 @@ fn check_symlink_directory_recursion( /// Traverses all directories that match the given `glob`. /// -/// This ensures that the calling task will be invalidated -/// whenever the directories or contents of the directories change, -/// but unlike read_glob doesn't accumulate data. +/// This ensures that the calling task will be invalidated whenever the directories or contents of +/// the directories change, but unlike [`read_glob`] doesn't accumulate data. Directories are +/// resolved before physical enumeration, including the initial `directory` and symlinks discovered +/// during traversal. #[turbo_tasks::function(fs)] pub async fn track_glob( directory: FileSystemPath, glob: Vc, include_dot_files: bool, ) -> Result> { + let directory = resolve_glob_root(directory).await?; track_glob_internal("", directory, glob, include_dot_files).await } @@ -266,7 +293,7 @@ pub mod tests { use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; use crate::{ - DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath, + DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath, ReadGlobResult, glob::{Glob, GlobOptions}, }; @@ -377,10 +404,16 @@ pub mod tests { let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?; assert_eq!( inner_sub_dir.results, - HashMap::from_iter([( - "index.js".into(), - DirectoryEntry::File(root.join("sub/dir/index.js")?), - )]) + HashMap::from_iter([ + ( + "index.js".into(), + DirectoryEntry::File(root.join("sub/dir/index.js")?), + ), + ( + "dead.js".into(), + DirectoryEntry::Symlink(root.join("sub/dir/dead.js")?), + ), + ]) ); assert_eq!(inner_sub_dir.inner.len(), 0); @@ -395,10 +428,16 @@ pub mod tests { let inner_sub_dir = &*inner_sub.inner.get("dir-chain").unwrap().await?; assert_eq!( inner_sub_dir.results, - HashMap::from_iter([( - "index.js".into(), - DirectoryEntry::File(root.join("sub/dir-chain/index.js")?), - )]) + HashMap::from_iter([ + ( + "index.js".into(), + DirectoryEntry::File(root.join("sub/dir-chain/index.js")?), + ), + ( + "dead.js".into(), + DirectoryEntry::Symlink(root.join("sub/dir-chain/dead.js")?), + ), + ]) ); assert_eq!(inner_sub_dir.inner.len(), 0); @@ -490,6 +529,7 @@ pub mod tests { .unwrap() .write_all(b"dir index") .unwrap(); + symlink(dir.join("missing.js"), dir.join("dead.js")).unwrap(); symlink(&dir, path.join("sub/dir")).unwrap(); let dir_link = path.join("dir-link"); symlink(&dir, &dir_link).unwrap(); @@ -564,6 +604,134 @@ pub mod tests { Ok(()) } + #[turbo_tasks::function(operation, root)] + async fn read_glob_from_operation( + path: RcStr, + directory: RcStr, + glob: RcStr, + ) -> anyhow::Result> { + let root = disk_file_system_root_operation(path) + .read_strongly_consistent() + .await?; + Ok(root + .join(&directory)? + .read_glob(Glob::new(glob, GlobOptions::default()))) + } + + #[turbo_tasks::function(operation, root)] + async fn track_glob_from_operation( + path: RcStr, + directory: RcStr, + glob: RcStr, + ) -> anyhow::Result> { + let root = disk_file_system_root_operation(path) + .read_strongly_consistent() + .await?; + Ok(root + .join(&directory)? + .track_glob(Glob::new(glob, GlobOptions::default()), false)) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn glob_roots_resolve_symlink_parents() { + let scratch = tempfile::tempdir().unwrap(); + let path = scratch.path(); + let target = path.join("target/inner/path"); + std::fs::create_dir_all(&target).unwrap(); + File::create_new(target.join("file.txt")) + .unwrap() + .write_all(b"initial") + .unwrap(); + std::fs::create_dir_all(path.join("path/to")).unwrap(); + symlink(path.join("target"), path.join("path/to/symlink")).unwrap(); + + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + let disk_root: RcStr = path.to_str().unwrap().into(); + tt.run_once(async move { + let root = disk_file_system_root_operation(disk_root.clone()) + .read_strongly_consistent() + .await?; + let logical_base = root.join("path/to/symlink/inner/path")?; + + let initial = read_glob_from_operation( + disk_root.clone(), + rcstr!("path/to/symlink/inner/path"), + rcstr!("*"), + ) + .read_strongly_consistent() + .await?; + assert_eq!( + initial.results.get("file.txt"), + Some(&DirectoryEntry::File(logical_base.join("file.txt")?)) + ); + + let wildcard = read_glob_from_operation( + disk_root.clone(), + rcstr!(""), + rcstr!("path/to/*/inner/path/*"), + ) + .read_strongly_consistent() + .await?; + let path_result = wildcard.inner.get("path").unwrap().await?; + let to_result = path_result.inner.get("to").unwrap().await?; + let symlink_result = to_result.inner.get("symlink").unwrap().await?; + let inner_result = symlink_result.inner.get("inner").unwrap().await?; + let final_result = inner_result.inner.get("path").unwrap().await?; + assert_eq!( + final_result.results.get("file.txt"), + Some(&DirectoryEntry::File(logical_base.join("file.txt")?)) + ); + + let initial_tracking = track_glob_from_operation( + disk_root.clone(), + rcstr!("path/to/symlink/inner/path"), + rcstr!("*"), + ) + .read_strongly_consistent() + .await?; + let wildcard_tracking = track_glob_from_operation( + disk_root.clone(), + rcstr!(""), + rcstr!("path/to/*/inner/path/*"), + ) + .read_strongly_consistent() + .await?; + + read_strongly_consistent_and_apply_effects( + extract_effects_operation(write( + root.join("target/inner/path/file.txt")?, + rcstr!("updated"), + )), + |e| e, + ) + .await?; + + let initial_tracking_after = track_glob_from_operation( + disk_root.clone(), + rcstr!("path/to/symlink/inner/path"), + rcstr!("*"), + ) + .read_strongly_consistent() + .await?; + let wildcard_tracking_after = + track_glob_from_operation(disk_root, rcstr!(""), rcstr!("path/to/*/inner/path/*")) + .read_strongly_consistent() + .await?; + + assert!(!ReadRef::ptr_eq(&initial_tracking, &initial_tracking_after)); + assert!(!ReadRef::ptr_eq( + &wildcard_tracking, + &wildcard_tracking_after + )); + anyhow::Ok(()) + }) + .await + .unwrap(); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn track_glob_invalidations() { let scratch = tempfile::tempdir().unwrap(); diff --git a/turbopack/crates/turbopack-core/src/resolve/pattern.rs b/turbopack/crates/turbopack-core/src/resolve/pattern.rs index 36311f3add4b..c5d0104f4a5a 100644 --- a/turbopack/crates/turbopack-core/src/resolve/pattern.rs +++ b/turbopack/crates/turbopack-core/src/resolve/pattern.rs @@ -11,7 +11,8 @@ use rustc_hash::{FxHashMap, FxHashSet}; use tracing::Instrument; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ - NonLocalValue, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, trace::TraceRawVcs, + NonLocalValue, ReadRef, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, + trace::TraceRawVcs, }; use turbo_tasks_fs::{ FileSystemEntryType, FileSystemPath, LinkContent, RawDirectoryContent, RawDirectoryEntry, @@ -1511,14 +1512,29 @@ impl PatternMatch { #[derive(Debug)] pub struct PatternMatches(Vec); +/// Reads the directory `path` points at, enumerating it through its realpath. +/// +/// Callers keep the original logical `path` when constructing [`PatternMatch`] values, so symlinks +/// stay visible in the results while the directory itself is never read through a symlinked +/// parent. Resolving also registers a dependency on the symlink chain, so replacing a link +/// invalidates the enumeration. +/// +/// A path that cannot be resolved (a dangling or cyclic link) is read as-is, which yields +/// [`RawDirectoryContent::NotFound`] just as it did before. +async fn raw_read_dir_resolved(path: &FileSystemPath) -> Result> { + let resolved = path.realpath().await?.unwrap_or_else(|_| path.clone()); + resolved.raw_read_dir().await +} + /// Find all files or directories that match the provided `pattern` with the /// specified `lookup_dir` directory. `prefix` is the already matched part of /// the pattern that leads to the `lookup_dir` directory. When /// `force_in_lookup_dir` is set, leaving the `lookup_dir` directory by /// matching `..` is not allowed. /// -/// Symlinks will not be resolved. It's expected that the caller resolves -/// symlinks when they are interested in that. +/// Symlinks in returned matches are not resolved. Lookup directories are resolved only for +/// physical enumeration; logical paths are retained in [`PatternMatch`] values so callers can +/// resolve and track the symlinks they are interested in. #[turbo_tasks::function] pub async fn read_matches( lookup_dir: FileSystemPath, @@ -1572,7 +1588,7 @@ pub async fn read_matches( lookup_dir.try_join(parent_path) }; if let Some(path) = path_option { - Some(e.insert((path.raw_read_dir().await?, path))) + Some(e.insert((raw_read_dir_resolved(&path).await?, path))) } else { None } @@ -1739,7 +1755,7 @@ pub async fn read_matches( prefix.pop(); prefix.pop(); } - match &*lookup_dir.raw_read_dir().await? { + match &*raw_read_dir_resolved(&lookup_dir).await? { RawDirectoryContent::Entries(map) => { for (key, entry) in map.iter() { match entry { @@ -1917,7 +1933,8 @@ mod tests { use turbo_tasks_fs::{DiskFileSystem, FileSystem}; use super::{ - Pattern, longest_common_prefix, longest_common_suffix, read_matches, split_last_segment, + Pattern, PatternMatch, longest_common_prefix, longest_common_suffix, read_matches, + split_last_segment, }; #[test] @@ -2660,6 +2677,60 @@ mod tests { assert_eq!(split_last_segment("../../a/"), ("../..", "a")); } + #[cfg(all(unix, debug_assertions))] + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_read_matches_resolves_lookup_dir_for_enumeration() { + use std::{fs::create_dir_all, os::unix::fs::symlink}; + + let scratch = tempfile::tempdir().unwrap(); + let root = scratch.path(); + create_dir_all(root.join("real")).unwrap(); + std::fs::write(root.join("real/file.js"), "content").unwrap(); + symlink("real", root.join("alias")).unwrap(); + symlink("real/file.js", root.join("file-alias")).unwrap(); + + #[turbo_tasks::function(operation, root)] + async fn operation(disk_root: RcStr) -> anyhow::Result<()> { + let root = DiskFileSystem::new(rcstr!("test"), Vc::cell(disk_root)) + .root() + .owned() + .await?; + let logical_dir = root.join("alias")?; + let matches = read_matches( + logical_dir.clone(), + rcstr!(""), + true, + Pattern::new(Pattern::Dynamic), + ) + .await?; + assert!(matches.iter().any(|m| m + == &PatternMatch::File(rcstr!("file.js"), logical_dir.join("file.js").unwrap(),))); + + let file_probe = read_matches( + root.join("file-alias")?, + rcstr!(""), + true, + Pattern::new(Pattern::Dynamic), + ) + .await?; + assert!(file_probe.is_empty()); + + Ok(()) + } + + let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( + BackendOptions::default(), + noop_backing_storage(), + )); + let disk_root: RcStr = root.to_str().unwrap().into(); + tt.run_once(async move { + operation(disk_root).read_strongly_consistent().await?; + anyhow::Ok(()) + }) + .await + .unwrap(); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_read_matches() { let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new( diff --git a/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs b/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs index 6e88c23849c7..5b8f992aa665 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs @@ -460,14 +460,18 @@ impl ImportMetaGlobMap { true } }) - .map(|(_base_relative, path)| { + .map(|(base_relative, _logical_path)| { let origin_path = &origin_path; + let base_dir = &base_dir; let query = &query; let reference_sub_type = &reference_sub_type; async move { - // Compute the origin-relative path for import resolution and as the - // user-visible key in the result object. - let Some(origin_relative) = origin_path.get_relative_path_to(path) else { + // ReadGlobResult paths are logical too, but reconstruct from its keys here so + // matching and user-visible specifiers have one explicit source of truth. The + // module resolver resolves this logical request and tracks its symlink chain. + let logical_path = base_dir.join(base_relative)?; + let Some(origin_relative) = origin_path.get_relative_path_to(&logical_path) + else { bail!( "import.meta.glob: failed to compute relative path from origin to \ matched file" diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js index 0e84042aa8ca..03aa27c4b921 100644 --- a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/index.js @@ -63,6 +63,16 @@ it('should support multiple patterns across directories', () => { expect(multiModules['./other/baz.js'].default).toBe('baz') }) +// The result keys stay logical while module resolution follows the directory symlink. +const symlinkModules = import.meta.glob('./linked/*.js', { eager: true }) + +it('should resolve modules through a symlink while preserving logical keys', () => { + const keys = Object.keys(symlinkModules).sort() + expect(keys).toEqual(['./linked/bar.js', './linked/foo.js']) + expect(symlinkModules['./linked/foo.js'].default).toBe('foo') + expect(symlinkModules['./linked/bar.js'].default).toBe('bar') +}) + // import: '*' (namespace import) — should return the whole module namespace // Uses ./other/*.js to avoid colliding with the eager test above (same pattern + eager + no import) const namespaceModules = import.meta.glob('./other/*.js', { @@ -116,6 +126,8 @@ it('should include dotfile directories with wildcard patterns', () => { './CaseDir/module-lower.js', './dir/bar.js', './dir/foo.js', + './linked/bar.js', + './linked/foo.js', './other/baz.js', ]) }) diff --git a/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/linked b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/linked new file mode 120000 index 000000000000..87245193225f --- /dev/null +++ b/turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/input/linked @@ -0,0 +1 @@ +dir \ No newline at end of file From cfc7da3049f91669a1b61a0f7a5edef503cddc53 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:18:26 +0200 Subject: [PATCH 5/5] Update outdated snapshots (#98011) I ran `UPDATE=1 cargo nextest run -p turbopack-tests` 1. https://github.com/vercel/next.js/pull/97672 was merged 2. But https://github.com/vercel/next.js/pull/97676 was merged without rebasing, leading to outdated snapshots --- ..._mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js} | 2 +- ...gle-exports_escaping-namespace_input_1ysta134r3dyz._.js.map} | 0 .../mangle-exports/escaping-namespace/output/index.entry.js | 2 +- ...mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js} | 2 +- ...le-exports_named-and-preserved_input_0pbreftkp_5pc._.js.map} | 0 .../mangle-exports/named-and-preserved/output/index.entry.js | 2 +- ...pshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js} | 2 +- ...t_mangle-exports_single-export_input_1mf78g4ogdhgv._.js.map} | 0 .../snapshot/mangle-exports/single-export/output/index.entry.js | 2 +- 9 files changed, 6 insertions(+), 6 deletions(-) rename turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/{0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js => 1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js} (88%) rename turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/{0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map => 1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js.map} (100%) rename turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/{0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js => 1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js} (83%) rename turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/{0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map => 1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js.map} (100%) rename turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/{0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js => 0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js} (91%) rename turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/{0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map => 0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js.map} (100%) diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js similarity index 88% rename from turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js rename to turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js index 62a9bf3445ce..5bb5aa3e8c38 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js @@ -1,3 +1,3 @@ module.exports=["[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js [test] (ecmascript)",t=>{"use strict";t.s(["anotherLongExportName",0,"long-2","someLongExportName",0,"long-1"])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js [test] (ecmascript)",t=>{"use strict";var s=t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/escaping.js [test] (ecmascript)"),e=t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js [test] (ecmascript)");console.log(s,e.z,e.T),t.s([])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/mangled.js [test] (ecmascript)",t=>{"use strict";t.s(["T",0,"mangled-2","z",0,"mangled-1"])}]; -//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map \ No newline at end of file +//# sourceMappingURL=1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js.map similarity index 100% rename from turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js.map rename to turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js.map diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js index 1aa6f3e5e716..8e2b0248e4a9 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/output/index.entry.js @@ -1,4 +1,4 @@ var R=require("./[turbopack]_runtime.js")("output/index.entry.js") -R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta13._.js") +R.c("output/1jsg_tests_snapshot_mangle-exports_escaping-namespace_input_1ysta134r3dyz._.js") R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js [test] (ecmascript)") module.exports=R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/escaping-namespace/input/index.js [test] (ecmascript)").exports \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js similarity index 83% rename from turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js rename to turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js index dd1cadf682a2..722ac517a65b 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js @@ -1,3 +1,3 @@ module.exports=["[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js [test] (ecmascript)",t=>{"use strict";var s=t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js [test] (ecmascript)");console.log(s.a,s.$,s.z,s.T,(0,s.X)()),t.s([])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/lib.js [test] (ecmascript)",t=>{"use strict";t.s(["a",0,"a","X",0,function(){return"fn"},"T",0,"long-2","$",0,"bb","z",0,"long-1"])}]; -//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map \ No newline at end of file +//# sourceMappingURL=1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js.map similarity index 100% rename from turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js.map rename to turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js.map diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js index add990f5c959..4e7478e4b86e 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/output/index.entry.js @@ -1,4 +1,4 @@ var R=require("./[turbopack]_runtime.js")("output/index.entry.js") -R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreft._.js") +R.c("output/1jsg_tests_snapshot_mangle-exports_named-and-preserved_input_0pbreftkp_5pc._.js") R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js [test] (ecmascript)") module.exports=R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/named-and-preserved/input/index.js [test] (ecmascript)").exports \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js similarity index 91% rename from turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js rename to turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js index a56a17553866..06ed5e88a924 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js @@ -1,3 +1,3 @@ module.exports=["[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js [test] (ecmascript)",t=>{"use strict";console.log((0,t.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js [test] (ecmascript)").f)()),t.s([])},"[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/lone.js [test] (ecmascript)",t=>{"use strict";t.s(["f",0,function(){return"lone"}])}]; -//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map \ No newline at end of file +//# sourceMappingURL=0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js.map similarity index 100% rename from turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js.map rename to turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js.map diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js index 515f23cd650d..62566e7d1530 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/output/index.entry.js @@ -1,4 +1,4 @@ var R=require("./[turbopack]_runtime.js")("output/index.entry.js") -R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4._.js") +R.c("output/0_9x_turbopack-tests_tests_snapshot_mangle-exports_single-export_input_1mf78g4ogdhgv._.js") R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js [test] (ecmascript)") module.exports=R.m("[project]/turbopack/crates/turbopack-tests/tests/snapshot/mangle-exports/single-export/input/index.js [test] (ecmascript)").exports \ No newline at end of file