Skip to main content

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}