Skip to main content

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}