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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
//! Files for Server Components are generated using `AstBuilder`, instead of
//! running through the js_parser. It emits a ParseTask.Result and joins
//! with the same logic that it runs though.
use core::mem::offset_of;
use std::fmt::Write as _;
use bun_alloc::{AllocError as OOM, Arena}; // bumpalo::Bump re-export
use bun_collections::VecExt;
use bun_ast::{Loc, Log, Source};
use bun_threading::thread_pool::Task as ThreadPoolTask;
use bun_ast::ast_result::NamedExports;
use bun_ast::{B, Binding, E, G, S, Stmt, symbol};
use bun_ast::{ExprNodeList, LocRef, StmtOrExpr, UseDirective};
use bun_ast::{ImportKind, ImportRecordFlags};
use crate::AstBuilder::AstBuilder;
use crate::JSAst;
use crate::Worker;
use crate::bundle_v2::BundleV2;
use crate::cache::ExternalFreeFunction;
use crate::options::{Loader, Target};
use crate::parse_task::{self, ResultValue, Success, WatcherData, on_complete};
pub use crate::ThreadPool;
pub use crate::DeferredBatchTask::DeferredBatchTask;
pub use crate::parse_task::ParseTask;
pub(crate) struct ServerComponentParseTask {
pub task: ThreadPoolTask,
pub data: Data,
// BACKREF (LIFETIMES.tsv) — Zig `*BundleV2` is mutable; written through in `on_complete`.
// `ParentRef` (write-provenance via `NonNull::from(&mut self)` at construction)
// so deref sites are safe; `None` only for the FRU `Default` placeholder.
pub ctx: Option<bun_ptr::ParentRef<BundleV2<'static>>>,
pub source: Source,
}
// `ServerComponentParseTask` is bump-arena-allocated; boxing the large arm
// would leak. The size diff is acceptable.
#[allow(clippy::large_enum_variant)]
pub enum Data {
/// Generate server-side code for a "use client" module. Given the
/// client ast, a "reference proxy" is created with identical exports.
ClientReferenceProxy(ReferenceProxy),
ClientEntryWrapper(ClientEntryWrapper),
}
pub struct ReferenceProxy {
pub other_source: Source,
pub named_exports: NamedExports,
}
pub struct ClientEntryWrapper {
// TODO(port): lifetime — Zig `[]const u8` borrowed from caller; never freed in this file.
pub path: Box<[u8]>,
}
/// Raw thread-pool callback. Recovers `&mut ServerComponentParseTask` from the
/// intrusive `task` field and dispatches the parse, then posts the result back
/// to the owning event loop.
// CONCURRENCY: thread-pool callback — runs on worker threads, one task per
// `ServerComponentParseTask` (heap-allocated, scheduled exactly once). Writes:
// own fields + `Log` (local) + result is posted via
// `ctx.loop_.enqueue_task_concurrent` (MPSC). Reads `ctx: &BundleV2` shared.
// `ServerComponentParseTask` is `Send` because `ctx: *mut BundleV2` is a
// backref to a `Send` type and `Source`/`Data` payloads are bundle-arena
// slices.
fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) {
// SAFETY: `thread_pool_task` points to the `task` field of a heap-allocated
// `ServerComponentParseTask` enqueued by BundleV2; offset_of recovers the parent.
let task: &mut ServerComponentParseTask = unsafe {
&mut *(bun_core::from_field_ptr!(ServerComponentParseTask, task, thread_pool_task))
};
// `ctx` is a `ParentRef` BACKREF to the owning BundleV2 (set at enqueue).
let ctx = task
.ctx
.expect("ServerComponentParseTask.ctx set at enqueue");
let worker = Worker::get(ctx.get());
// PORT NOTE: `defer worker.unget()` — handled at end of fn (no early returns).
let mut log = Log::new();
// SAFETY: `worker.arena` is set in `Worker::create` to point at the
// worker-owned bump arena; lives for the worker's lifetime.
let arena: &Arena = worker.arena();
let value = match task_callback(task, &mut log, arena) {
Ok(success) => ResultValue::Success(success),
// Only possible error is OOM; abort like `bun.outOfMemory()`.
Err(_oom) => bun_core::out_of_memory(),
};
let result = Box::new(parse_task::Result {
// `ctx` already a `ParentRef<BundleV2>` with write provenance
// (constructed from `NonNull::from(&mut self)` in `bundle_v2.rs`).
ctx,
// SAFETY: Zig leaves `.task = undefined`; consumer overwrites before read.
task: Default::default(),
value,
external: ExternalFreeFunction::NONE,
watcher_data: WatcherData::NONE,
});
let result = bun_core::heap::into_raw(result);
// Zig matched `worker.ctx.loop().*` on `AnyEventLoop::{js, mini}`.
// `worker.ctx` is a `BackRef<BundleV2>` (safe `Deref`); the BACKREF deref
// of `linker.r#loop` is centralised in `LinkerContext::any_loop_mut`.
//
// Zig `worker.ctx.loop().*` is non-optional (.zig:52) — `BundleV2::init`
// always sets `linker.r#loop` before scheduling any ServerComponentParseTask.
// Running `on_complete` inline on the worker thread would violate
// `BundleV2::on_parse_task_complete`'s threading contract (it mutates the
// bundler graph, which is owned by the main/bundler thread).
match worker
.ctx
.linker
.any_loop_mut()
.expect("BundleV2.linker.loop must be set before scheduling ServerComponentParseTask")
{
bun_event_loop::AnyEventLoop::Js { owner } => {
owner.enqueue_task_concurrent(
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(result, |p| {
// SAFETY: `p` is the `result` Box leaked above; ownership
// transfers to `on_complete`, which deallocates it.
unsafe { on_complete(p) };
Ok(())
}),
);
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
// SAFETY: `result` is a freshly Box-leaked `parse_task::Result` (above) and
// `offset_of!(parse_task::Result, task)` is the intrusive task field within it.
unsafe {
mini.enqueue_task_concurrent_with_extra_ctx::<parse_task::Result, BundleV2<'static>>(
result,
on_complete_mini,
offset_of!(parse_task::Result, task),
);
}
}
}
// Zig: `defer worker.unget()` — runs at function exit, i.e. after enqueue.
worker.unget();
}
fn on_complete_mini(result: *mut parse_task::Result, _ctx: *mut BundleV2<'static>) {
// `on_complete` already recovers `ctx` from `result.ctx`.
// SAFETY: callback contract — `result` is the uniquely-owned Box leaked in
// `run_from_thread_pool`; ownership transfers to `on_complete`.
unsafe { on_complete(result) };
}
fn task_callback(
task: &mut ServerComponentParseTask,
log: &mut Log,
bump: &'static Arena,
) -> Result<Success, OOM> {
// `ctx` is a `ParentRef` BACKREF to the owning BundleV2; safe `Deref`.
let ctx: &BundleV2 = task
.ctx
.as_deref()
.expect("ServerComponentParseTask.ctx set at enqueue");
// PORT NOTE: `Source` is not `Clone`; the original is consumed here
// (Zig copied by value). Take it up-front so `ab`'s borrow of it ends
// (via NLL) before we move it into `Success`.
let source = core::mem::take(&mut task.source);
let mut ab = AstBuilder::init(bump, &source, ctx.transpiler().options.hot_module_reloading)?;
match &task.data {
Data::ClientReferenceProxy(data) => generate_client_reference_proxy(ctx, data, &mut ab)?,
Data::ClientEntryWrapper(data) => generate_client_entry_wrapper(data, &mut ab)?,
}
let target = match &task.data {
// Server-side
Data::ClientReferenceProxy(_) => ctx.transpiler().options.target,
// Client-side,
Data::ClientEntryWrapper(_) => Target::Browser,
};
let hmr_api_ref = ab.hmr_api_ref;
let mut bundled_ast: JSAst = ab.to_bundled_ast(target)?;
// `wrapper_ref` is used to hold the HMR api ref (see comment in
// `src/ast/Ast.zig`)
bundled_ast.wrapper_ref = hmr_api_ref;
Ok(Success {
ast: bundled_ast,
source,
loader: Loader::Js,
log: core::mem::take(log),
use_directive: UseDirective::None,
side_effects: bun_ast::SideEffects::NoSideEffectsPureData,
unique_key_for_additional_file: bun_ast::StoreStr::EMPTY,
content_hash_for_additional_file: 0,
package_name: bun_ast::StoreStr::EMPTY,
})
}
impl Default for ServerComponentParseTask {
/// Mirrors Zig's `task: ThreadPoolLib.Task = .{ .callback = &taskCallbackWrap }`
/// default-field-value. Callers (`bundle_v2.rs`) supply `data`/`ctx`/`source`
/// via FRU and rely on this for the intrusive `task` link.
fn default() -> Self {
Self {
task: ThreadPoolTask {
node: Default::default(),
callback: task_callback_wrap,
},
data: Data::ClientEntryWrapper(ClientEntryWrapper {
path: Box::default(),
}),
ctx: None,
source: Source::default(),
}
}
}
fn generate_client_entry_wrapper(data: &ClientEntryWrapper, b: &mut AstBuilder) -> Result<(), OOM> {
// `add_import_record` stores the slice raw in the `ImportRecord`; `data.path`
// outlives the bundle pass (owned by the heap-allocated task). Route through
// `StoreStr` so the lifetime erasure goes through one audited unsafe.
let path = bun_ast::StoreStr::new(&data.path[..]);
let record = b.add_import_record(path.slice(), ImportKind::Stmt)?;
let namespace_ref = b.new_symbol(symbol::Kind::Other, b"main")?;
b.append_stmt(S::Import {
namespace_ref,
import_record_index: record,
is_single_line: true,
..Default::default()
})?;
b.import_records[record as usize]
.flags
.insert(ImportRecordFlags::WAS_ORIGINALLY_BARE_IMPORT);
Ok(())
}
fn generate_client_reference_proxy(
ctx: &BundleV2,
data: &ReferenceProxy,
b: &mut AstBuilder,
) -> Result<(), OOM> {
let server_components = ctx
.framework
.as_ref()
.unwrap()
.server_components
.as_ref()
// config must be non-null to enter this function
.unwrap_or_else(|| unreachable!());
let client_named_exports = &data.named_exports;
// `add_import_stmt` stores the slices raw in `ImportRecord`/`ClauseItem`s;
// the framework config outlives the bundle pass. Route through `StoreStr`
// so the lifetime erasure goes through one audited unsafe.
let runtime_import = bun_ast::StoreStr::new(&server_components.server_runtime_import[..]);
let register_ref =
bun_ast::StoreStr::new(&server_components.server_register_client_reference[..]);
let register_client_reference =
b.add_import_stmt(runtime_import.slice(), [register_ref.slice()])?[0];
let module_path = b.new_expr(E::String::init(
// In development, the path loaded is the source file: Easy!
//
// In production, the path here must be the final chunk path, but
// that information is not yet available since chunks are not
// computed. The unique_key replacement system is used here.
if ctx.transpiler().options.has_dev_server() {
b.bump.alloc_slice_copy(data.other_source.path.pretty)
} else {
// PERF(port): was arena allocPrint — profile if it shows up on a hot path
let mut buf = bun_alloc::ArenaString::new_in(b.bump);
write!(
&mut buf,
"{}",
crate::chunk::UniqueKey {
prefix: ctx.unique_key,
kind: crate::chunk::QueryKind::Scb,
index: data.other_source.index.0,
},
)
.map_err(|_| OOM)?;
buf.into_bump_str().as_bytes()
},
));
for key in client_named_exports.keys() {
let key: &[u8] = key.as_ref();
let is_default = key == b"default";
// This error message is taken from
// https://github.com/facebook/react/blob/c5b9375767e2c4102d7e5559d383523736f1c902/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js#L323-L354
let err_msg_string: &[u8] = {
// PERF(port): was arena allocPrint — profile if it shows up on a hot path
let mut buf = bun_alloc::ArenaString::new_in(b.bump);
if is_default {
write!(
&mut buf,
concat!(
"Attempted to call the default export of {module_path} from ",
"the server, but it's on the client. It's not possible to invoke a ",
"client function from the server, it can only be rendered as a ",
"Component or passed to props of a Client Component.",
),
module_path = bstr::BStr::new(data.other_source.path.pretty),
)
} else {
write!(
&mut buf,
concat!(
"Attempted to call {key}() from the server but {key} ",
"is on the client. It's not possible to invoke a client function from ",
"the server, it can only be rendered as a Component or passed to ",
"props of a Client Component.",
),
key = bstr::BStr::new(key),
)
}
.map_err(|_| OOM)?;
buf.into_bump_str().as_bytes()
};
// throw new Error(...)
// Hoist the `&mut self` symbol allocation out of the nested `&self`
// `new_expr` calls to satisfy the borrow checker.
let error_ref = b.new_external_symbol(b"Error")?;
let err_msg = b.new_expr(E::New {
target: b.new_expr(E::Identifier {
ref_: error_ref,
..Default::default()
}),
args: bun_ast::ExprNodeList::from_slice(&[b.new_expr(E::String::init(err_msg_string))]),
close_parens_loc: Loc::EMPTY,
..Default::default()
});
// registerClientReference(
// () => { throw new Error(...) },
// "src/filepath.tsx",
// "Comp"
// );
let throw_stmt = b.new_stmt(S::Throw { value: err_msg });
let arrow_body_stmts: &mut [Stmt] = b.bump.alloc_slice_copy(&[throw_stmt]);
let value = b.new_expr(E::Call {
target: register_client_reference,
args: ExprNodeList::from_slice(&[
b.new_expr(E::Arrow {
body: G::FnBody {
stmts: bun_ast::StoreSlice::new_mut(arrow_body_stmts),
loc: Loc::EMPTY,
},
..Default::default()
}),
module_path,
b.new_expr(E::String::init(b.bump.alloc_slice_copy(key))),
]),
..Default::default()
});
if is_default {
let ref_ = b.new_symbol(symbol::Kind::Other, b"default")?;
// export default registerClientReference(...);
b.append_stmt(S::ExportDefault {
value: StmtOrExpr::Expr(value),
default_name: LocRef {
loc: Loc::EMPTY,
ref_: Some(ref_),
},
})?;
} else {
// export const Component = registerClientReference(...);
let export_ref = b.new_symbol(symbol::Kind::Other, key)?;
b.append_stmt(S::Local {
decls: G::DeclList::from_slice(&[G::Decl {
binding: Binding::alloc(
b.bump,
B::Identifier { r#ref: export_ref },
Loc::EMPTY,
),
value: Some(value),
}]),
is_export: true,
kind: S::Kind::KConst,
..Default::default()
})?;
}
}
Ok(())
}
// ported from: src/bundler/ServerComponentParseTask.zig