1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
use core::ptr::NonNull;
use crate::BundledAst as JSAst;
use bun_alloc::Arena as ThreadLocalArena;
use bun_alloc::{AstAlloc, AstVec};
use bun_ast::server_component_boundary;
use bun_collections::MultiArrayList;
use enum_map::EnumMap;
use crate::IndexStringMap::IndexStringMap;
use crate::PathToSourceIndexMap::PathToSourceIndexMap;
use crate::options;
use crate::{AdditionalFile, BundleV2, ThreadPool};
use bun_ast::Index;
// `bun.ast.Index.Int` — the underlying integer repr of `Index`.
pub(crate) use crate::IndexInt;
pub struct Graph<'a> {
// TODO(port): lifetime — no direct LIFETIMES.tsv row for Graph.pool, but row 170
// (ThreadPool.v2, BACKREF) evidence states "BundleV2.graph.pool owns ThreadPool".
// bundle_v2.zig:992 allocates it from `this.arena()` (the `self.heap` arena) and
// bundle_v2.zig:2248 calls `pool.deinit()`, so this is arena-owned but self-referential
// (sibling field). `BackRef` (not raw `NonNull`) so the read accessor `pool()` is
// safe — the BACKREF invariant (pointee outlives holder) holds for the entire
// bundle pass.
pub pool: bun_ptr::BackRef<ThreadPool>,
pub heap: &'a ThreadLocalArena,
/// Mapping user-specified entry points to their Source Index
// PERF(port): Zig fed this ArrayList from `self.heap` (self-referential arena).
pub entry_points: Vec<Index>,
/// Maps entry point source indices to their original specifiers (for virtual entries resolved by plugins)
pub entry_point_original_names: IndexStringMap,
/// Every source index has an associated InputFile
pub input_files: MultiArrayList<InputFile>,
/// Every source index has an associated Ast
/// When a parse is in progress / queued, it is `Ast.empty`
// PORT NOTE: BundledAst<'arena> borrows from self.heap (sibling-field self-ref);
// 'static here is a placeholder. TODO(refactor): thread the lifetime via raw ptr or Ouroboros.
pub ast: MultiArrayList<JSAst<'a>>,
/// During the scan + parse phase, this value keeps a count of the remaining
/// tasks. Once it hits zero, the scan phase ends and linking begins. Note
/// that if `deferred_pending > 0`, it means there are plugin callbacks
/// to invoke before linking, which can initiate another scan phase.
///
/// Increment and decrement this via `incrementScanCounter` and
/// `decrementScanCounter`, as asynchronous bundles check for `0` in the
/// decrement function, instead of at the top of the event loop.
///
/// - Parsing a file (ParseTask and ServerComponentParseTask)
/// - onResolve and onLoad functions
/// - Resolving an onDefer promise
pub pending_items: u32,
/// When an `onLoad` plugin calls `.defer()`, the count from `pending_items`
/// is "moved" into this counter (pending_items -= 1; deferred_pending += 1)
///
/// When `pending_items` hits zero and there are deferred pending tasks, those
/// tasks will be run, and the count is "moved" back to `pending_items`
pub deferred_pending: u32,
/// A map of build targets to their corresponding module graphs.
pub build_graphs: EnumMap<options::Target, PathToSourceIndexMap>,
/// When Server Components is enabled, this holds a list of all boundary
/// files. This happens for all files with a "use <side>" directive.
pub server_component_boundaries: server_component_boundary::List,
/// Track HTML imports from server-side code
/// Each entry represents a server file importing an HTML file that needs a client build
///
/// OutputPiece.Kind.HTMLManifest corresponds to indices into the array.
pub html_imports: HtmlImports,
pub estimated_file_loader_count: usize,
/// For Bake, a count of the CSS asts is used to make precise
/// pre-allocations without re-iterating the file listing.
pub css_file_count: usize,
// PERF(port): Zig fed this ArrayList from `self.heap` (self-referential arena).
pub additional_output_files: Vec<options::OutputFile>,
pub kit_referenced_server_data: bool,
pub kit_referenced_client_data: bool,
/// Do any input_files have a secondary_path.len > 0?
///
/// Helps skip a loop.
pub has_any_secondary_paths: bool,
}
#[derive(Default)]
pub struct HtmlImports {
/// Source index of the server file doing the import
pub server_source_indices: Vec<IndexInt>,
/// Source index of the HTML file being imported
pub html_source_indices: Vec<IndexInt>,
}
pub struct InputFile {
pub source: bun_ast::Source,
pub secondary_path: AstVec<u8>,
pub loader: options::Loader,
pub side_effects: SideEffects,
// PORT NOTE: Zig stored `arena: std.mem.Allocator = bun.default_allocator`
// here so deinit could free `source`/`secondary_path` with the right alloc.
// In Rust the owned fields (Box/Vec) carry their arena; field dropped.
pub additional_files: AstVec<AdditionalFile>,
pub unique_key_for_additional_file: Box<[u8], AstAlloc>,
pub content_hash_for_additional_file: u64,
pub flags: InputFileFlags,
}
impl Default for InputFile {
fn default() -> Self {
Self {
source: bun_ast::Source::default(),
secondary_path: AstAlloc::vec(),
loader: options::Loader::default(),
side_effects: SideEffects::default(),
additional_files: AstAlloc::vec(),
unique_key_for_additional_file: AstAlloc::vec().into_boxed_slice(),
content_hash_for_additional_file: 0,
flags: InputFileFlags::default(),
}
}
}
// SoA column accessors on `MultiArrayList<InputFile>` and `Slice<InputFile>`.
// Field name + type are checked against `InputFile`'s reflected layout at
// compile time by the underlying `items::<"name", T>()`.
bun_collections::multi_array_columns! {
pub trait InputFileColumns for InputFile {
source: bun_ast::Source,
secondary_path: AstVec<u8>,
loader: options::Loader,
side_effects: SideEffects,
additional_files: AstVec<AdditionalFile>,
unique_key_for_additional_file: Box<[u8], AstAlloc>,
content_hash_for_additional_file: u64,
flags: InputFileFlags,
}
}
bitflags::bitflags! {
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct InputFileFlags: u8 {
const IS_PLUGIN_FILE = 1 << 0;
/// Set when a barrel-eligible file has `export * from` this file.
const IS_EXPORT_STAR_TARGET = 1 << 1;
}
}
impl<'a> Graph<'a> {
pub fn new(heap: &'a ThreadLocalArena) -> Self {
Self {
// Self-referential arena pointer; real value wired in
// `BundleV2::init` before any use (Graph.zig has `= undefined`).
pool: bun_ptr::BackRef::from(NonNull::<ThreadPool>::dangling()),
heap,
entry_points: Vec::new(),
entry_point_original_names: IndexStringMap::default(),
input_files: MultiArrayList::default(),
ast: MultiArrayList::default(),
pending_items: 0,
deferred_pending: 0,
build_graphs: EnumMap::default(),
server_component_boundaries: server_component_boundary::List::default(),
html_imports: HtmlImports::default(),
estimated_file_loader_count: 0,
css_file_count: 0,
additional_output_files: Vec::new(),
kit_referenced_server_data: false,
kit_referenced_client_data: false,
has_any_secondary_paths: false,
}
}
}
impl<'a> Graph<'a> {
/// Shared borrow of the bundler `ThreadPool`.
///
/// `pool` is arena-allocated in `BundleV2::init` (bundle_v2.zig:992) and
/// torn down in `BundleV2::deinit` (bundle_v2.zig:2248). It is non-null
/// and valid for the entire bundle pass; see LIFETIMES.tsv row 170
/// (BACKREF). All `ThreadPool` driver methods (`schedule`, `start`,
/// `worker_pool`, `schedule_inside_thread_pool`) take `&self`, so callers
/// can use this in place of the prior open-coded
/// `unsafe { self.pool.as_ref() }` / `as_mut()`.
#[inline]
pub fn pool(&self) -> &ThreadPool {
// BackRef invariant: `pool` is set in `BundleV2::init` to an
// arena-owned `ThreadPool` and remains valid until `BundleV2::deinit`;
// no `&mut ThreadPool` is live across any `pool()` borrow (the only
// `&mut` site is `deinit`, called after all schedule/worker activity
// has drained).
self.pool.get()
}
/// Exclusive borrow of the bundler `ThreadPool`. Only needed for
/// `ThreadPool::deinit` during teardown; prefer [`Self::pool`] for
/// scheduling.
#[inline]
pub fn pool_mut(&mut self) -> &mut ThreadPool {
// SAFETY: see `pool()`. `&mut self` excludes other safe borrows of
// `Graph`, so no aliasing `&ThreadPool` is live.
unsafe { self.pool.get_mut() }
}
#[inline]
pub fn path_to_source_index_map(
&mut self,
target: options::Target,
) -> &mut PathToSourceIndexMap {
&mut self.build_graphs[target]
}
/// Schedule a task to be run on the JS thread which resolves the promise of
/// each `.defer()` called in an onLoad plugin.
///
/// Returns true if there were more tasks queued.
pub fn drain_deferred_tasks(&mut self, transpiler: &mut BundleV2) -> bool {
transpiler.thread_lock.assert_locked();
if self.deferred_pending > 0 {
self.pending_items += self.deferred_pending;
self.deferred_pending = 0;
transpiler.drain_defer_task.init();
transpiler.drain_defer_task.schedule();
return true;
}
false
}
}
// Spec: `side_effects: _resolver.SideEffects` (Graph.zig:74). The resolver
// crate re-exports the canonical enum from `bun_options_types`; re-export it
// here so `InputFile` and the derived `items_side_effects()` SoA accessor share
// the same type that `LinkerContext::mark_file_live_for_tree_shaking` expects.
use bun_ast::SideEffects;
// ported from: src/bundler/Graph.zig