turbopack_ecmascript/references/esm/mangle/mod.rs
1//! Shortening ("mangling") of the export names a module exposes to other modules.
2//!
3//! A module's export keys usually only exist to link modules together: the producing module emits
4//! `{ someVeryLongExportName: () => … }` and every consumer reads
5//! `ns["someVeryLongExportName"]`. Both sides are generated by us, so the key can be replaced by a
6//! much shorter one — as long as producer and consumer agree, and as long as the name is not
7//! observable from user code.
8//!
9//! [`mangled_export_names`] is the single source of truth for that mapping. Both the producer
10//! ([`super::export::EsmExports::code_generation`]) and the consumer
11//! ([`super::base::ReferencedAsset`]) ask it for the *target* module's map, so they cannot
12//! disagree. It returns an empty map whenever the module has to keep its original names — the
13//! same effect as mangling every name to itself, so callers never need to branch on "did mangling
14//! even apply here", only look a name up and fall back to itself.
15//!
16//! Code generation that spells an export access out **as a string** — rather than going through
17//! `ReferencedAssetIdent`, which handles this automatically — must resolve its key through
18//! [`generated_export_key`]. A hard-coded `["someExport"]` in generated source silently misses as
19//! soon as the target module's exports are mangled.
20
21mod table;
22
23use anyhow::Result;
24use turbo_frozenmap::FrozenMap;
25use turbo_rcstr::RcStr;
26use turbo_tasks::{ResolvedVc, Vc};
27use turbopack_core::{
28 chunk::ChunkingContext, module_graph::binding_usage_info::ModuleExportUsageInfo,
29};
30
31use self::table::shorten_to_unique_names;
32use crate::chunk::{EcmascriptChunkPlaceable, EcmascriptExports};
33
34/// A module's export name mapping: original export name -> the key actually used in the output.
35///
36/// `None` means the module is not eligible for mangling at all and keeps every original name (see
37/// [`mangled_export_names`] for the conditions). `Some` covers only the exports that were actually
38/// *renamed* — an export that kept its own name because it was already short enough, or because it
39/// lost every hash collision, is not a key in the map. So a missing key always means "use the
40/// original name", and callers that only want the correct output key can ignore the distinction
41/// entirely (see [`generated_export_key`]); the `canMangle` reporting in
42/// [`crate::references::exports_info`] is the one caller that needs to tell "not eligible" from
43/// "eligible but unchanged", which is exactly what the `Option` gives it.
44#[turbo_tasks::value(transparent)]
45pub struct MangledExportNames(pub Option<FrozenMap<RcStr, RcStr>>);
46
47/// Computes the mangled export names of `module`, or `None` when its exports must keep their
48/// original names.
49///
50/// Both the producing and the consuming side call this for the same `module`, which is what
51/// guarantees they agree. The mapping covers the module's *used* exports: unused exports are not
52/// emitted at all, so giving them names would only make the remaining names longer.
53///
54/// A module keeps its original names when any of these hold:
55///
56/// 1. mangling is disabled for the module that owns these exports,
57/// 2. its export usage is [`ModuleExportUsageInfo::All`] — a namespace import, a computed property
58/// access, an unresolvable `export *`, or a module referenced from outside the module graph
59/// (entries, which are seeded with `All`). This is what a plain `import * as ns` reaches in
60/// practice, whether or not the namespace object escapes: the reads are not enumerated, so the
61/// usage widens to `All`. Note that widening propagates, so splitting such a module into a
62/// facade and a locals module does not rescue the locals module,
63/// 3. it is read through a namespace value whose used names *are* known — in practice a dynamic
64/// `import()` with an enumerated export list (`import(/* webpackExports: [...] */ "…")`). We
65/// know which names are used, but not that every read of them was lowered to a direct named
66/// access, so an original name may still be read by user code,
67/// 4. its exports are not statically known ECMAScript exports, or contain dynamic re-exports.
68///
69/// None of these depend on the individual export, so "is `e` a candidate for mangling" is exactly
70/// "is this `Some`, and is `e` used" — the per-name step (hashing into a short identifier) never
71/// itself excludes a name.
72#[turbo_tasks::function]
73pub async fn mangled_export_names(
74 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
75 chunking_context: Vc<Box<dyn ChunkingContext>>,
76) -> Result<Vc<MangledExportNames>> {
77 // (4) Only statically known ESM exports can be renamed.
78 let EcmascriptExports::EsmExports(exports) = *module.get_exports().await? else {
79 return Ok(Vc::cell(None));
80 };
81
82 // (1) Disabled for the module these exports belong to. Deliberately independent of whether
83 // this build minifies at all: mangling and minification are two separate concerns, and a
84 // caller may want either without the other.
85 if !exports.await?.mangle_export_names {
86 return Ok(Vc::cell(None));
87 }
88
89 let usage = chunking_context
90 .module_export_usage(*ResolvedVc::upcast(module))
91 .await?;
92
93 // (3) An original name may still be read through a namespace value.
94 if usage.namespace_object_may_escape {
95 return Ok(Vc::cell(None));
96 }
97
98 // (2) We don't know which exports are used, so we don't know that all uses are ours.
99 let usage_info = usage.export_usage.await?;
100 let ModuleExportUsageInfo::Exports(used) = &*usage_info else {
101 return Ok(Vc::cell(None));
102 };
103 if used.is_empty() {
104 return Ok(Vc::cell(None));
105 }
106
107 // (4) `export * from "./some-dynamic-cjs"` is resolved at runtime by property access on the
108 // original names, and a used set that resolves to no actual export at all leaves nothing to
109 // mangle.
110 let expanded = exports.expand_exports(*usage.export_usage).await?;
111 if !expanded.dynamic_exports.is_empty() || expanded.exports.is_empty() {
112 return Ok(Vc::cell(None));
113 }
114
115 // Every emitted export is mangled, `default` and `__esModule` included.
116 //
117 // Neither is a runtime contract *as an export name*. The runtime paths that look `'default'` up
118 // by name — the CommonJS interop in `esmNamespaceObject` and the proxy traps behind
119 // `ensureDynamicExports` — only apply to CommonJS targets and to modules with dynamic
120 // re-exports, and both already keep their original names via the checks above. `__esModule` is
121 // how a CommonJS module signals interop *to* ESM, so an ESM module that happens to export that
122 // name is just an ordinary export. `__esModule` still cannot be *assigned* to some other
123 // export, because the runtime defines that property itself; see `RESERVED_KEYS` in `table`.
124 Ok(Vc::cell(Some(shorten_to_unique_names(
125 expanded.exports.keys(),
126 ))))
127}
128
129/// The key that generated code has to use to read `export` from `module` — the mangled key when
130/// the module's exports are mangled, and `export` itself otherwise.
131///
132/// **Every** piece of code generation that materializes an export access as a string has to get its
133/// key from here, or the access will miss when the module is mangled. That includes generated
134/// source text (`__turbopack_require__(id)["default"](…)` and friends) as well as AST built by
135/// hand. Code that goes through `ReferencedAssetIdent` (see `super::base`) is already covered.
136///
137/// `module` must be the module that actually *produces* the export. For a re-export, resolve to the
138/// producing module first (as `ReferencedAsset::get_ident_inner` does), because each module mangles
139/// its own keys independently.
140pub async fn generated_export_key(
141 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
142 chunking_context: Vc<Box<dyn ChunkingContext>>,
143 export: &RcStr,
144) -> Result<RcStr> {
145 let names = mangled_export_names(*module, chunking_context).await?;
146 Ok(names
147 .as_ref()
148 .and_then(|names| names.get(export))
149 .cloned()
150 .unwrap_or_else(|| export.clone()))
151}