rolldown_common/ecmascript/ecma_view.rs
1use crate::{ConstExportMeta, ImportAttribute, SourcemapChainElement};
2use arcstr::ArcStr;
3use bitflags::bitflags;
4use oxc::{
5 semantic::{NodeId, SymbolId},
6 span::Span,
7};
8use oxc_index::IndexVec;
9use oxc_str::CompactStr;
10use rolldown_utils::indexmap::{FxIndexMap, FxIndexSet};
11use rustc_hash::{FxHashMap, FxHashSet};
12
13use crate::{
14 ExportsKind, HmrInfo, ImportRecordIdx, ImporterRecord, LocalExport, ModuleDefFormat, ModuleId,
15 ModuleIdx, NamedImport, ResolvedImportRecord, SourceMutation, StmtInfoIdx, SymbolRef,
16 side_effects::DeterminedSideEffects, types::source_mutation::ArcSourceMutation,
17};
18
19bitflags! {
20 #[derive(Debug, Default, Clone, Copy)]
21 pub struct EcmaViewMeta: u8 {
22 const Eval = 1;
23 const HasLazyExport = 1 << 1;
24 const HasStarExport = 1 << 2;
25 const SafelyTreeshakeCommonjs = 1 << 3;
26 /// If a module has side effects or has top-level global variable access
27 const ExecutionOrderSensitive = 1 << 4;
28 /// If the module has top-level empty function, if any module has top level empty function, we need
29 /// to apply cross module optimization.
30 const TopExportedSideEffectsFreeFunction = 1 << 5;
31 /// Module evaluation reads at least one imported binding.
32 const TopLevelImportRead = 1 << 6;
33 /// Local initialization creates data and functions without observing outside state.
34 const IndependentInitialization = 1 << 7;
35 }
36}
37
38/// One occurrence of `import.meta.ROLLDOWN_FILE_URL_<referenceId>`.
39#[derive(Debug, Clone)]
40pub struct RolldownFileUrlReference {
41 /// Cross-pass identity of the member expression; the key the module finalizer
42 /// looks the replacement up by (see internal-docs/ast-mutation/implementation.md).
43 pub node_id: NodeId,
44 /// The enclosing top-level statement, so occurrences whose statement is
45 /// tree-shaken away can be skipped when invoking the `resolveFileUrl` hook.
46 pub stmt_info_idx: StmtInfoIdx,
47 /// The `<referenceId>` suffix, as handed out by `emitFile`.
48 pub reference_id: CompactStr,
49 /// Optional `urlId`
50 pub url_id: Option<CompactStr>,
51}
52
53#[derive(Debug, Copy, Clone, PartialEq, Eq)]
54pub enum ThisExprReplaceKind {
55 /// It depends on `context` set by the user. If it's unset, replace it with `undefined`.
56 Context,
57 /// The top-level `this` of a CommonJS module, which is the initial exports object of the module.
58 /// If the finalizer prints the module inside a CJS closure, it prints `this` as
59 /// `EcmaView::cjs_this_ref`. If the module is an unwrapped CommonJS entry, the finalizer keeps
60 /// `this`, which is the module `this` of Node. The HMR finalizer prints its own exports reference
61 /// instead.
62 Exports,
63}
64
65#[inline]
66#[expect(clippy::implicit_hasher)]
67pub fn generate_replace_this_expr_map(
68 set: &FxHashSet<NodeId>,
69 kind: ThisExprReplaceKind,
70) -> FxHashMap<NodeId, ThisExprReplaceKind> {
71 set.iter().map(|node_id| (*node_id, kind)).collect()
72}
73
74impl EcmaViewMeta {
75 #[inline]
76 pub fn has_eval(&self) -> bool {
77 self.contains(Self::Eval)
78 }
79 #[inline]
80 pub fn has_lazy_export(&self) -> bool {
81 self.contains(Self::HasLazyExport)
82 }
83 #[inline]
84 pub fn has_star_export(&self) -> bool {
85 self.contains(Self::HasStarExport)
86 }
87}
88
89/// Where a named export's value comes from: the module's own declaration, or a re-export of an
90/// import.
91///
92/// This classification is the contract between the lazy-barrel loader and tree shaking's
93/// body-demand gating, and the two sides MUST agree: the loader loads every plain import record
94/// of a barrel as soon as one of its *own* exports is requested (`BarrelInfo::local` in
95/// `take_needed_records`), and tree shaking includes the module's gated side-effect statements as
96/// soon as one of its *own* exports is used (`compute_body_demand_keys`). If they classified an
97/// export differently, a retained statement could reference an import record that was never
98/// loaded — a free identifier at runtime (the #9806 bug family). Keeping the classification in
99/// one place makes that agreement hold by construction.
100pub enum ExportOrigin<'a> {
101 /// Declared by the module itself: `export const a = ...`, `export function f() {}`, or a plain
102 /// `export { local }` of a local binding.
103 Own,
104 /// Re-exports an import: `export { a } from './x'`, `export * as ns from './x'`, or
105 /// `import { a } from './x'; export { a }`.
106 ReExport(&'a NamedImport),
107}
108
109impl EcmaView {
110 /// Classify a named export as the module's own declaration or a re-export of an import.
111 /// See [`ExportOrigin`] for why this must stay the single source of truth.
112 pub fn classify_export(&self, local_export: &LocalExport) -> ExportOrigin<'_> {
113 match self.named_imports.get(&local_export.referenced) {
114 Some(named_import) => ExportOrigin::ReExport(named_import),
115 None => ExportOrigin::Own,
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
121pub struct EcmaView {
122 pub dummy_record_set: FxHashSet<NodeId>,
123 pub source: ArcStr,
124 pub def_format: ModuleDefFormat,
125 /// Represents [Module Namespace Object](https://tc39.es/ecma262/#sec-module-namespace-exotic-objects)
126 pub namespace_object_ref: SymbolRef,
127 pub named_imports: FxIndexMap<SymbolRef, NamedImport>,
128 pub named_exports: FxHashMap<CompactStr, LocalExport>,
129 pub import_records: IndexVec<ImportRecordIdx, ResolvedImportRecord>,
130 /// Cross-pass AST-node side tables use post-semantic `NodeId`. See internal-docs/ast-mutation/implementation.md.
131 ///
132 /// The key is the `NodeId` of `ImportDeclaration`, `ImportExpression`,
133 /// `ExportFromDeclaration`, or `ExportAllDeclaration`.
134 /// and `CallExpression`(only when the callee is `require`).
135 pub imports: FxHashMap<NodeId, ImportRecordIdx>,
136 pub exports_kind: ExportsKind,
137 pub default_export_ref: SymbolRef,
138 pub sourcemap_chain: Vec<SourcemapChainElement>,
139 // the ids of all modules that statically import this module
140 pub importers: FxIndexSet<ModuleId>,
141 pub importers_idx: FxIndexSet<ModuleIdx>,
142 // the ids of all modules that import this module via dynamic import()
143 pub dynamic_importers: FxIndexSet<ModuleId>,
144 // the idx of all modules that import this module via dynamic import()
145 pub dynamic_importers_idx: FxIndexSet<ModuleIdx>,
146 // the module ids statically imported by this module
147 pub imported_ids: FxIndexSet<ModuleId>,
148 // the module ids imported by this module via dynamic import()
149 pub dynamically_imported_ids: FxIndexSet<ModuleId>,
150 pub side_effects: DeterminedSideEffects,
151 pub ast_usage: EcmaModuleAstUsage,
152 pub self_referenced_class_decl_symbol_ids: FxHashSet<SymbolId>,
153 // the range of hashbang in source
154 pub hashbang_range: Option<Span>,
155 pub directive_range: Vec<Span>,
156 pub meta: EcmaViewMeta,
157 pub mutations: Vec<ArcSourceMutation>,
158 /// `NodeId` of `new URL('path', import.meta.url)` -> `ImportRecordIdx`
159 pub new_url_references: FxHashMap<NodeId, ImportRecordIdx>,
160 /// `NodeId` of `'key' in ns`, where `ns` is a namespace import -> `key`.
161 pub namespace_in_checks: FxHashMap<NodeId, CompactStr>,
162 /// Occurrences of `import.meta.ROLLDOWN_FILE_URL_<referenceId>[_<urlId>]`, in source order.
163 /// One entry per occurrence: the `resolveFileUrl` hook is called per occurrence,
164 /// matching Rollup, so duplicates are meaningful.
165 pub rolldown_file_url_references: Vec<RolldownFileUrlReference>,
166 pub this_expr_replace_map: FxHashMap<NodeId, ThisExprReplaceKind>,
167 /// The facade symbol for the top-level `this` of a CommonJS module. The scanner makes it for each
168 /// CommonJS module that reads its top-level `this`. If the finalizer prints the module inside a
169 /// CJS closure, the renamer gives the symbol a name, and the finalizer prints it as the third
170 /// parameter of the closure. The runtime passes the initial exports object of the module to that
171 /// parameter. The name is synthesized, so no binding of the module can capture it. An unwrapped
172 /// CommonJS entry does not use this symbol. See internal-docs/renaming/design.md.
173 pub cjs_this_ref: Option<SymbolRef>,
174
175 pub hmr_hot_ref: Option<SymbolRef>,
176 pub hmr_info: HmrInfo,
177 pub constant_export_map: FxHashMap<SymbolId, ConstExportMeta>,
178 /// Enum member constant values, keyed by enum name → member name → value.
179 /// Used by the finalizer to inline `Direction.Up` style accesses across modules.
180 /// Contains both const and regular enums.
181 pub enum_member_value_map: FxHashMap<CompactStr, FxHashMap<CompactStr, ConstExportMeta>>,
182 pub import_attribute_map: FxHashMap<ImportRecordIdx, ImportAttribute>,
183 /// Use `Box` since it is rarely used also it could reduce the size of `EcmaView`, .
184 pub json_module_none_self_reference_included_symbol: Option<Box<FxHashSet<SymbolRef>>>,
185 /// Import record indices for `module.exports = require(...)` patterns.
186 pub cjs_reexport_import_record_ids: Vec<ImportRecordIdx>,
187 /// Members called on a dynamic import's namespace, e.g. `foo` in
188 /// `import('mod').then(ns => ns.foo())`.
189 pub dynamic_import_called_names: FxHashMap<ImportRecordIdx, FxHashSet<CompactStr>>,
190}
191
192impl EcmaView {
193 /// Re-derives the importer sets from this module's `ImporterRecord`s.
194 pub fn rebuild_importer_sets(&mut self, records: &[ImporterRecord]) {
195 self.importers.clear();
196 self.importers_idx.clear();
197 self.dynamic_importers.clear();
198 self.dynamic_importers_idx.clear();
199 for record in records {
200 if record.kind.is_static() {
201 self.importers.insert(record.importer_path.clone());
202 self.importers_idx.insert(record.importer_idx);
203 } else {
204 self.dynamic_importers.insert(record.importer_path.clone());
205 // Only real `import()` edges join the walkable dynamic-importer set; `HotAccept`
206 // and other non-static records are not import edges. Lazy-compilation proxy
207 // importers (`?rolldown-lazy=1`) are an internal artifact — excluding them keeps
208 // the server's superset walk from bubbling through the proxy chain
209 // (`foo -> proxy -> app`), which patch generation is not set up for. Lazy
210 // dynamic-import HMR is a separate follow-up; until then the walk stops at the
211 // lazy entry, and a tab whose own walk crosses the proxy chain reloads itself
212 // via its missing-factory / no-boundary paths, exactly as before.
213 if record.kind.is_dynamic() && !record.importer_path.as_str().contains("?rolldown-lazy=1") {
214 self.dynamic_importers_idx.insert(record.importer_idx);
215 }
216 }
217 }
218 }
219}
220
221bitflags! {
222 #[derive(Debug, Clone, Copy)]
223 pub struct EcmaModuleAstUsage: u16 {
224 const ModuleRef = 1;
225 const ExportsRef = 1 << 1;
226 /// If the module has `Object.defineProperty(module.exports, "__esModule", { value: true })` or it's variant
227 const EsModuleFlag = 1 << 2;
228 const AllStaticExportPropertyAccess = 1 << 3;
229 /// module.exports = require('mod');
230 const IsCjsReexport = 1 << 4;
231 const TopLevelAwait = 1 << 5;
232 const HmrSelfAccept = 1 << 6;
233 const UnknownExportsRead = 1 << 7;
234 /// Top-level return statement (only valid in CommonJS)
235 const TopLevelReturn = 1 << 8;
236 /// `import.meta.hot.acceptExports(...)` is called somewhere in the module
237 const HmrAcceptExports = 1 << 9;
238 const ModuleOrExports = Self::ModuleRef.bits() | Self::ExportsRef.bits();
239 }
240}
241
242#[derive(Debug, Default)]
243pub struct PrependRenderedImport {
244 pub intro: String,
245}
246
247impl SourceMutation for PrependRenderedImport {
248 fn apply(&self, magic_string: &mut string_wizard::MagicString<'_>) {
249 magic_string.append_intro(self.intro.clone());
250 }
251}