@gjsify/rolldown-native 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +55 -55
- package/prebuilds/linux-aarch64/libgjsify_rolldown.so +0 -0
- package/prebuilds/linux-x86_64/libgjsify_rolldown.so +0 -0
- package/prebuilds/linux-x86_64/libgjsifyrolldown.so +0 -0
- package/src/rust/Cargo.toml +37 -0
- package/src/rust/src/lib.rs +224 -0
- package/src/rust/src/plugin_proxy.rs +779 -0
- package/src/rust/src/session.rs +852 -0
- package/src/vala/gjsify-rolldown-glue.c +195 -0
- package/src/vala/gjsify-rolldown-glue.h +100 -0
- package/src/vala/gjsify-rolldown.h +136 -0
- package/src/vala/rolldown.vala +437 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
//! BundleSession — owns the tokio runtime, the plugin-channel pump,
|
|
2
|
+
//! the eventfd wakeup pair, and the BundleHandle the JS layer uses
|
|
3
|
+
//! to drive the build.
|
|
4
|
+
//!
|
|
5
|
+
//! Lifecycle:
|
|
6
|
+
//! 1. `start()` — caller passes options + plugin metadata. We
|
|
7
|
+
//! construct N JsPluginProxy instances, hand them to
|
|
8
|
+
//! Bundler::with_plugins, spawn a tokio task that drives
|
|
9
|
+
//! `Bundler::generate()` to completion, return the session.
|
|
10
|
+
//! 2. While the build runs, the worker pool calls into the proxies,
|
|
11
|
+
//! pushing HookRequests to the channel + writing the request
|
|
12
|
+
//! eventfd. JS drains via `next_request()` and replies via
|
|
13
|
+
//! `respond()`.
|
|
14
|
+
//! 3. When the bundle task completes, the result is stored on the
|
|
15
|
+
//! session. The Vala side polls `try_take_result()` after
|
|
16
|
+
//! seeing the request_eventfd close (or its own completion
|
|
17
|
+
//! eventfd, see Phase B.2 — for B.1 we expose `wait()` only).
|
|
18
|
+
|
|
19
|
+
use std::ffi::CString;
|
|
20
|
+
use std::os::raw::{c_char, c_int};
|
|
21
|
+
use std::ptr;
|
|
22
|
+
use std::sync::Arc;
|
|
23
|
+
use std::sync::Mutex;
|
|
24
|
+
use std::sync::atomic::AtomicU64;
|
|
25
|
+
use std::time::Duration;
|
|
26
|
+
|
|
27
|
+
use crossbeam_channel::{Receiver, Sender, unbounded};
|
|
28
|
+
use rolldown::Bundler;
|
|
29
|
+
use rolldown_common::BundlerOptions;
|
|
30
|
+
use rolldown_plugin::__inner::SharedPluginable;
|
|
31
|
+
use rolldown_plugin::{PluginContext, PluginContextResolveOptions};
|
|
32
|
+
use serde::{Deserialize, Serialize};
|
|
33
|
+
use tokio::runtime::{Builder, Runtime};
|
|
34
|
+
use tokio::sync::oneshot;
|
|
35
|
+
|
|
36
|
+
use crate::plugin_proxy::{HookRequest, HookResponse, JsPluginProxy, PluginMeta};
|
|
37
|
+
use crate::{BundleOutputJson, OutputJson, convert_output};
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Deserialize)]
|
|
40
|
+
pub struct StartArgs {
|
|
41
|
+
pub options: BundlerOptions,
|
|
42
|
+
pub plugins: Vec<PluginMeta>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// One context-resolve sub-result flowing Rust → JS. Surfaces through
|
|
46
|
+
/// the `context_response_eventfd` queue.
|
|
47
|
+
#[derive(Debug, Serialize)]
|
|
48
|
+
#[serde(rename_all = "camelCase")]
|
|
49
|
+
pub struct ContextResolveResponse {
|
|
50
|
+
pub child_id: u64,
|
|
51
|
+
/// Resolved id, when the call succeeded and produced one.
|
|
52
|
+
pub id: Option<String>,
|
|
53
|
+
/// Whether the resolved module is external. Mirrors rolldown's
|
|
54
|
+
/// `ResolvedId.is_external`.
|
|
55
|
+
pub external: Option<bool>,
|
|
56
|
+
/// Set when `ctx.resolve()` failed. Maps to rolldown's
|
|
57
|
+
/// `ResolveError` variants serialized as `Display`. JS handler
|
|
58
|
+
/// rejects its `await` Promise with this message.
|
|
59
|
+
pub error: Option<String>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Slice of session state shared with `JsPluginProxy`. Wrapped in
|
|
63
|
+
/// `Arc` and cloned into each proxy. Holds everything the proxies
|
|
64
|
+
/// need to (a) register/unregister parent `PluginContext` snapshots
|
|
65
|
+
/// for nested-protocol callbacks, (b) deliver context-resolve results
|
|
66
|
+
/// to JS via the dedicated eventfd, and (c) accumulate `this.warn()`
|
|
67
|
+
/// strings into the final BundleOutput.
|
|
68
|
+
pub struct SessionShared {
|
|
69
|
+
pub contexts: Mutex<std::collections::HashMap<u64, PluginContext>>,
|
|
70
|
+
pub next_child_id: AtomicU64,
|
|
71
|
+
pub context_response_tx: Sender<ContextResolveResponse>,
|
|
72
|
+
pub context_response_eventfd: c_int,
|
|
73
|
+
pub context_warnings: Mutex<Vec<String>>,
|
|
74
|
+
/// Phase B.4 — parallel bytes-payload slots keyed by req_id.
|
|
75
|
+
/// `request_payloads` holds Rust → JS payloads (e.g. transform
|
|
76
|
+
/// `code` source bytes), drained by the JS adapter via
|
|
77
|
+
/// `take_request_payload(reqId)`. `response_payloads` holds JS →
|
|
78
|
+
/// Rust payloads (e.g. transform output bytes), stashed by JS
|
|
79
|
+
/// before `respond()` and consumed by the Rust dispatch site.
|
|
80
|
+
pub request_payloads: Mutex<std::collections::HashMap<u64, Vec<u8>>>,
|
|
81
|
+
pub response_payloads: Mutex<std::collections::HashMap<u64, Vec<u8>>>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Public session handle. Owned via raw pointer on the C side.
|
|
85
|
+
pub struct BundleSession {
|
|
86
|
+
/// Runtime kept alive for the lifetime of the session. multi_thread
|
|
87
|
+
/// because rolldown internally spawns parallel module-graph tasks
|
|
88
|
+
/// that would otherwise serialize behind plugin-callback awaits.
|
|
89
|
+
pub runtime: Runtime,
|
|
90
|
+
/// Channel: many proxies → single drain. Sender side is cloned
|
|
91
|
+
/// into each JsPluginProxy.
|
|
92
|
+
pub request_rx: Receiver<HookRequest>,
|
|
93
|
+
/// Pending requests waiting on JS reply. Keyed by req_id.
|
|
94
|
+
pub pending: Mutex<std::collections::HashMap<u64, oneshot::Sender<HookResponse>>>,
|
|
95
|
+
/// eventfd for "request available" wakeup → GLib main loop watches.
|
|
96
|
+
pub request_eventfd: c_int,
|
|
97
|
+
/// Result of the bundle task once complete. None while still running.
|
|
98
|
+
pub result: Mutex<Option<Result<String, String>>>,
|
|
99
|
+
/// Completion eventfd: written exactly once when `result` is set.
|
|
100
|
+
pub complete_eventfd: c_int,
|
|
101
|
+
/// Cancel flag — set by the C side via `cancel()`. tokio task
|
|
102
|
+
/// observes via shared atomic and aborts.
|
|
103
|
+
pub cancelled: Arc<std::sync::atomic::AtomicBool>,
|
|
104
|
+
/// Phase B.3 — shared with each `JsPluginProxy` for nested
|
|
105
|
+
/// plugin-context callbacks.
|
|
106
|
+
pub shared: Arc<SessionShared>,
|
|
107
|
+
/// Receiver side of the context-response channel. Drained by
|
|
108
|
+
/// `gjsify_rolldown_session_next_context_response` from the GLib
|
|
109
|
+
/// main loop after waking on `context_response_eventfd`.
|
|
110
|
+
pub context_response_rx: Receiver<ContextResolveResponse>,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
impl BundleSession {
|
|
114
|
+
/// Drain at most one pending HookRequest non-blockingly. Returns
|
|
115
|
+
/// None if the channel is empty (or if the receiver was dropped).
|
|
116
|
+
pub fn try_next_request(&self) -> Option<HookRequest> {
|
|
117
|
+
self.request_rx.try_recv().ok()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Park the JS-supplied response into the matching pending slot.
|
|
121
|
+
/// Returns true if a waiter was found (and woken).
|
|
122
|
+
pub fn deliver_response(&self, req_id: u64, response: HookResponse) -> bool {
|
|
123
|
+
let mut pending = self.pending.lock().unwrap();
|
|
124
|
+
if let Some(tx) = pending.remove(&req_id) {
|
|
125
|
+
let _ = tx.send(response);
|
|
126
|
+
true
|
|
127
|
+
} else {
|
|
128
|
+
false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Snapshot the bundle result. Returns None if the build is
|
|
133
|
+
/// still running. On completion: Some(Ok(json)) or
|
|
134
|
+
/// Some(Err(message)).
|
|
135
|
+
pub fn try_take_result(&self) -> Option<Result<String, String>> {
|
|
136
|
+
self.result.lock().unwrap().take()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Mark cancelled. The tokio task observes this on its next
|
|
140
|
+
/// poll and aborts. Pending JS-replies will fail with timeout
|
|
141
|
+
/// or "channel closed".
|
|
142
|
+
pub fn cancel(&self) {
|
|
143
|
+
self.cancelled
|
|
144
|
+
.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
145
|
+
// Drop the channel sender by dropping the proxies on shutdown.
|
|
146
|
+
// The tokio runtime drop will abort all spawned tasks.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// FFI: start a new bundle session. Returns NULL on error +
|
|
151
|
+
/// `*err_out` set to a heap-allocated message (caller frees via
|
|
152
|
+
/// `gjsify_rolldown_session_free_error`).
|
|
153
|
+
#[unsafe(no_mangle)]
|
|
154
|
+
pub extern "C" fn gjsify_rolldown_session_start(
|
|
155
|
+
args_json: *const c_char,
|
|
156
|
+
args_json_len: usize,
|
|
157
|
+
err_out: *mut *mut c_char,
|
|
158
|
+
) -> *mut BundleSession {
|
|
159
|
+
let args: StartArgs = match parse_json_args(args_json, args_json_len) {
|
|
160
|
+
Ok(a) => a,
|
|
161
|
+
Err(msg) => {
|
|
162
|
+
unsafe { *err_out = err_to_cstr(msg) };
|
|
163
|
+
return ptr::null_mut();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Create the eventfd pair. EFD_NONBLOCK so writes never block;
|
|
168
|
+
// EFD_SEMAPHORE off (we use counter-mode so JS can read once
|
|
169
|
+
// per drain cycle and process all pending requests in a loop).
|
|
170
|
+
let request_eventfd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
171
|
+
if request_eventfd < 0 {
|
|
172
|
+
unsafe { *err_out = err_to_cstr("rolldown: eventfd(request) failed".to_string()) };
|
|
173
|
+
return ptr::null_mut();
|
|
174
|
+
}
|
|
175
|
+
let complete_eventfd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
176
|
+
if complete_eventfd < 0 {
|
|
177
|
+
unsafe { libc::close(request_eventfd) };
|
|
178
|
+
unsafe { *err_out = err_to_cstr("rolldown: eventfd(complete) failed".to_string()) };
|
|
179
|
+
return ptr::null_mut();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let context_response_eventfd =
|
|
183
|
+
unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
184
|
+
if context_response_eventfd < 0 {
|
|
185
|
+
unsafe { libc::close(request_eventfd) };
|
|
186
|
+
unsafe { libc::close(complete_eventfd) };
|
|
187
|
+
unsafe { *err_out = err_to_cstr("rolldown: eventfd(context_response) failed".to_string()) };
|
|
188
|
+
return ptr::null_mut();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
192
|
+
let (request_tx, request_rx) = unbounded::<HookRequest>();
|
|
193
|
+
let (context_response_tx, context_response_rx) = unbounded::<ContextResolveResponse>();
|
|
194
|
+
let next_request_id = Arc::new(AtomicU64::new(1));
|
|
195
|
+
|
|
196
|
+
let shared = Arc::new(SessionShared {
|
|
197
|
+
contexts: Mutex::new(std::collections::HashMap::new()),
|
|
198
|
+
next_child_id: AtomicU64::new(1),
|
|
199
|
+
context_response_tx,
|
|
200
|
+
context_response_eventfd,
|
|
201
|
+
context_warnings: Mutex::new(Vec::new()),
|
|
202
|
+
request_payloads: Mutex::new(std::collections::HashMap::new()),
|
|
203
|
+
response_payloads: Mutex::new(std::collections::HashMap::new()),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Build N proxies, one per user plugin. Order in this Vec is the
|
|
207
|
+
// plugin order rolldown sees — matches plugin_index in the
|
|
208
|
+
// request payload so JS dispatches to the right plugin.
|
|
209
|
+
let proxies: Vec<SharedPluginable> = match args
|
|
210
|
+
.plugins
|
|
211
|
+
.iter()
|
|
212
|
+
.enumerate()
|
|
213
|
+
.map(|(idx, meta)| {
|
|
214
|
+
let load_id_filter = compile_filter(&meta.id_filter.load, &meta.name, "load")?;
|
|
215
|
+
let transform_id_filter =
|
|
216
|
+
compile_filter(&meta.id_filter.transform, &meta.name, "transform")?;
|
|
217
|
+
let resolve_id_filter =
|
|
218
|
+
compile_filter(&meta.id_filter.resolve_id, &meta.name, "resolveId")?;
|
|
219
|
+
Ok::<_, String>(Arc::new(JsPluginProxy {
|
|
220
|
+
name: meta.name.clone(),
|
|
221
|
+
plugin_index: idx,
|
|
222
|
+
hooks: meta.hooks.clone(),
|
|
223
|
+
request_tx: request_tx.clone(),
|
|
224
|
+
next_request_id: next_request_id.clone(),
|
|
225
|
+
request_eventfd,
|
|
226
|
+
response_timeout: Duration::from_secs(60),
|
|
227
|
+
load_id_filter,
|
|
228
|
+
transform_id_filter,
|
|
229
|
+
resolve_id_filter,
|
|
230
|
+
shared: shared.clone(),
|
|
231
|
+
}) as SharedPluginable)
|
|
232
|
+
})
|
|
233
|
+
.collect::<Result<Vec<_>, _>>()
|
|
234
|
+
{
|
|
235
|
+
Ok(v) => v,
|
|
236
|
+
Err(msg) => {
|
|
237
|
+
unsafe { libc::close(request_eventfd) };
|
|
238
|
+
unsafe { libc::close(complete_eventfd) };
|
|
239
|
+
unsafe { libc::close(context_response_eventfd) };
|
|
240
|
+
unsafe { *err_out = err_to_cstr(msg) };
|
|
241
|
+
return ptr::null_mut();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Drop the original sender so the channel naturally closes when
|
|
246
|
+
// all proxies are dropped (which happens after the bundle task
|
|
247
|
+
// exits).
|
|
248
|
+
drop(request_tx);
|
|
249
|
+
|
|
250
|
+
let runtime = match Builder::new_multi_thread()
|
|
251
|
+
.worker_threads(num_cpus_capped())
|
|
252
|
+
.enable_all()
|
|
253
|
+
.build()
|
|
254
|
+
{
|
|
255
|
+
Ok(r) => r,
|
|
256
|
+
Err(e) => {
|
|
257
|
+
unsafe { libc::close(request_eventfd) };
|
|
258
|
+
unsafe { libc::close(complete_eventfd) };
|
|
259
|
+
unsafe { libc::close(context_response_eventfd) };
|
|
260
|
+
unsafe { *err_out = err_to_cstr(format!("rolldown: tokio init: {e}")) };
|
|
261
|
+
return ptr::null_mut();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
let result_slot: Arc<Mutex<Option<Result<String, String>>>> = Arc::new(Mutex::new(None));
|
|
266
|
+
let result_slot_clone = result_slot.clone();
|
|
267
|
+
let complete_eventfd_for_task = complete_eventfd;
|
|
268
|
+
let cancelled_for_task = cancelled.clone();
|
|
269
|
+
let shared_for_task = shared.clone();
|
|
270
|
+
|
|
271
|
+
runtime.spawn(async move {
|
|
272
|
+
let res = run_bundle(args.options, proxies, cancelled_for_task, shared_for_task).await;
|
|
273
|
+
let final_json = match res {
|
|
274
|
+
Ok(json) => Ok(json),
|
|
275
|
+
Err(msg) => Err(msg),
|
|
276
|
+
};
|
|
277
|
+
*result_slot_clone.lock().unwrap() = Some(final_json);
|
|
278
|
+
// Signal completion to the GLib main loop.
|
|
279
|
+
let one: u64 = 1;
|
|
280
|
+
unsafe {
|
|
281
|
+
libc::write(
|
|
282
|
+
complete_eventfd_for_task,
|
|
283
|
+
&one as *const u64 as *const libc::c_void,
|
|
284
|
+
8,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
let session = Box::new(BundleSession {
|
|
290
|
+
runtime,
|
|
291
|
+
request_rx,
|
|
292
|
+
pending: Mutex::new(std::collections::HashMap::new()),
|
|
293
|
+
request_eventfd,
|
|
294
|
+
result: Mutex::new(None),
|
|
295
|
+
complete_eventfd,
|
|
296
|
+
cancelled,
|
|
297
|
+
shared,
|
|
298
|
+
context_response_rx,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Move the result slot into the session AFTER spawn (the spawn
|
|
302
|
+
// closure has its own clone; the session reads from the same
|
|
303
|
+
// shared slot).
|
|
304
|
+
let session_ptr = Box::into_raw(session);
|
|
305
|
+
// Patch: the session owns its own Mutex<Option<Result>> — but
|
|
306
|
+
// the spawn closure was given a clone via result_slot_clone.
|
|
307
|
+
// To make `try_take_result` see the same slot, we re-point.
|
|
308
|
+
// Simplest fix: stash result_slot Arc inside the session via
|
|
309
|
+
// a side-table. For B.1 we route through an interior-mutability
|
|
310
|
+
// helper — the result Mutex inside the session IS the slot;
|
|
311
|
+
// the closure already has a separate Arc. To unify, we drop
|
|
312
|
+
// the session-internal slot and instead use the Arc directly.
|
|
313
|
+
//
|
|
314
|
+
// Implementation: replace session.result with a method that
|
|
315
|
+
// pulls from result_slot. Done below via `ResultSlotRegistry`.
|
|
316
|
+
register_result_slot(session_ptr, result_slot);
|
|
317
|
+
|
|
318
|
+
unsafe { *err_out = ptr::null_mut() };
|
|
319
|
+
session_ptr
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- result-slot registry ----------------------------------------------
|
|
323
|
+
//
|
|
324
|
+
// The clean way would be to put the Arc<Mutex<...>> straight on the
|
|
325
|
+
// session. We do that here via an interior swap: the session's own
|
|
326
|
+
// `result` Mutex<Option<...>> is unused; `register_result_slot`
|
|
327
|
+
// stores the *real* slot in a static map keyed by session pointer.
|
|
328
|
+
// `try_take_result` is rewritten to consult the map.
|
|
329
|
+
//
|
|
330
|
+
// Reason for this indirection: we needed to spawn the task before
|
|
331
|
+
// constructing the Box<Session> (because moving the proxies into
|
|
332
|
+
// the spawn future requires them by value), and we couldn't
|
|
333
|
+
// reference `session.result` from inside the spawn closure
|
|
334
|
+
// without &mut Box ownership shenanigans.
|
|
335
|
+
|
|
336
|
+
use std::collections::HashMap as StdHashMap;
|
|
337
|
+
use std::sync::OnceLock;
|
|
338
|
+
|
|
339
|
+
type ResultSlot = Arc<Mutex<Option<Result<String, String>>>>;
|
|
340
|
+
|
|
341
|
+
static RESULT_SLOTS: OnceLock<Mutex<StdHashMap<usize, ResultSlot>>> = OnceLock::new();
|
|
342
|
+
|
|
343
|
+
fn registry() -> &'static Mutex<StdHashMap<usize, ResultSlot>> {
|
|
344
|
+
RESULT_SLOTS.get_or_init(|| Mutex::new(StdHashMap::new()))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn register_result_slot(session_ptr: *mut BundleSession, slot: ResultSlot) {
|
|
348
|
+
registry().lock().unwrap().insert(session_ptr as usize, slot);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fn lookup_result_slot(session_ptr: *mut BundleSession) -> Option<ResultSlot> {
|
|
352
|
+
registry().lock().unwrap().get(&(session_ptr as usize)).cloned()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fn unregister_result_slot(session_ptr: *mut BundleSession) {
|
|
356
|
+
registry().lock().unwrap().remove(&(session_ptr as usize));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- bundle driver -----------------------------------------------------
|
|
360
|
+
|
|
361
|
+
async fn run_bundle(
|
|
362
|
+
options: BundlerOptions,
|
|
363
|
+
plugins: Vec<SharedPluginable>,
|
|
364
|
+
cancelled: Arc<std::sync::atomic::AtomicBool>,
|
|
365
|
+
shared: Arc<SessionShared>,
|
|
366
|
+
) -> Result<String, String> {
|
|
367
|
+
if cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
|
368
|
+
return Err("rolldown: cancelled before start".to_string());
|
|
369
|
+
}
|
|
370
|
+
let mut bundler = Bundler::with_plugins(options, plugins)
|
|
371
|
+
.map_err(|e| format!("rolldown: Bundler::new: {e:?}"))?;
|
|
372
|
+
let bundle = bundler
|
|
373
|
+
.generate()
|
|
374
|
+
.await
|
|
375
|
+
.map_err(|e| format!("rolldown: Bundler::generate: {e:?}"))?;
|
|
376
|
+
|
|
377
|
+
let mut warnings: Vec<String> = bundle.warnings.iter().map(|w| format!("{w:?}")).collect();
|
|
378
|
+
// Append `this.warn(...)` strings collected during the build so the
|
|
379
|
+
// JS caller sees them alongside rolldown-internal warnings.
|
|
380
|
+
warnings.extend(shared.context_warnings.lock().unwrap().drain(..));
|
|
381
|
+
let output: Vec<OutputJson> = bundle.assets.into_iter().map(convert_output).collect();
|
|
382
|
+
serde_json::to_string(&BundleOutputJson { warnings, output })
|
|
383
|
+
.map_err(|e| format!("rolldown: serialize: {e}"))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --- FFI surface -------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
#[unsafe(no_mangle)]
|
|
389
|
+
pub extern "C" fn gjsify_rolldown_session_request_fd(session: *mut BundleSession) -> c_int {
|
|
390
|
+
if session.is_null() {
|
|
391
|
+
return -1;
|
|
392
|
+
}
|
|
393
|
+
unsafe { (*session).request_eventfd }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[unsafe(no_mangle)]
|
|
397
|
+
pub extern "C" fn gjsify_rolldown_session_complete_fd(session: *mut BundleSession) -> c_int {
|
|
398
|
+
if session.is_null() {
|
|
399
|
+
return -1;
|
|
400
|
+
}
|
|
401
|
+
unsafe { (*session).complete_eventfd }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// Drain one pending request. Returns NULL when the channel is
|
|
405
|
+
/// momentarily empty (caller should retry on next eventfd wake).
|
|
406
|
+
/// Caller frees the returned string via `gjsify_rolldown_session_free_string`.
|
|
407
|
+
#[unsafe(no_mangle)]
|
|
408
|
+
pub extern "C" fn gjsify_rolldown_session_next_request(
|
|
409
|
+
session: *mut BundleSession,
|
|
410
|
+
out_len: *mut usize,
|
|
411
|
+
) -> *mut c_char {
|
|
412
|
+
if session.is_null() {
|
|
413
|
+
unsafe { *out_len = 0 };
|
|
414
|
+
return ptr::null_mut();
|
|
415
|
+
}
|
|
416
|
+
let session = unsafe { &*session };
|
|
417
|
+
|
|
418
|
+
// Pull one request. Move its `reply` Sender into pending map
|
|
419
|
+
// before we serialize the payload (so JS can respond as soon
|
|
420
|
+
// as it sees the request_id).
|
|
421
|
+
let req = match session.try_next_request() {
|
|
422
|
+
Some(r) => r,
|
|
423
|
+
None => {
|
|
424
|
+
unsafe { *out_len = 0 };
|
|
425
|
+
return ptr::null_mut();
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
let req_id = req.req_id;
|
|
430
|
+
let plugin_index = req.plugin_index;
|
|
431
|
+
let payload = req.payload;
|
|
432
|
+
session.pending.lock().unwrap().insert(req_id, req.reply);
|
|
433
|
+
|
|
434
|
+
// Serialize the payload standalone so the per-variant
|
|
435
|
+
// `#[serde(rename_all = "camelCase")]` inside HookRequestPayload
|
|
436
|
+
// is honored. `#[serde(flatten)]` would silently drop those rules
|
|
437
|
+
// (serde issue #1346) and re-emit fields like `module_type` /
|
|
438
|
+
// `is_entry` in their original snake_case.
|
|
439
|
+
let json = match serde_json::to_value(&payload) {
|
|
440
|
+
Ok(mut value) => {
|
|
441
|
+
if let Some(obj) = value.as_object_mut() {
|
|
442
|
+
obj.insert("reqId".to_string(), serde_json::json!(req_id));
|
|
443
|
+
obj.insert("pluginIndex".to_string(), serde_json::json!(plugin_index));
|
|
444
|
+
}
|
|
445
|
+
value.to_string()
|
|
446
|
+
}
|
|
447
|
+
Err(e) => format!("{{\"reqId\":{req_id},\"error\":\"serialize: {e}\"}}"),
|
|
448
|
+
};
|
|
449
|
+
let len = json.len();
|
|
450
|
+
unsafe { *out_len = len };
|
|
451
|
+
let cstr = match CString::new(json) {
|
|
452
|
+
Ok(c) => c,
|
|
453
|
+
Err(_) => return ptr::null_mut(),
|
|
454
|
+
};
|
|
455
|
+
cstr.into_raw()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[unsafe(no_mangle)]
|
|
459
|
+
pub extern "C" fn gjsify_rolldown_session_respond(
|
|
460
|
+
session: *mut BundleSession,
|
|
461
|
+
req_id: u64,
|
|
462
|
+
response_json: *const c_char,
|
|
463
|
+
response_json_len: usize,
|
|
464
|
+
) -> bool {
|
|
465
|
+
if session.is_null() || response_json.is_null() {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
let slice =
|
|
469
|
+
unsafe { std::slice::from_raw_parts(response_json as *const u8, response_json_len) };
|
|
470
|
+
let resp: HookResponse = match serde_json::from_slice(slice) {
|
|
471
|
+
Ok(r) => r,
|
|
472
|
+
Err(e) => HookResponse::Error {
|
|
473
|
+
message: format!("rolldown: malformed response JSON: {e}"),
|
|
474
|
+
stack: None,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
let session = unsafe { &*session };
|
|
478
|
+
session.deliver_response(req_id, resp)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// Returns NULL while still building. On completion: returns a
|
|
482
|
+
/// JSON document (caller frees via `gjsify_rolldown_session_free_string`).
|
|
483
|
+
/// `*is_error` is set to true when the response carries an error
|
|
484
|
+
/// message; in that case the JSON is the error text wrapped as
|
|
485
|
+
/// `{"error": "..."}`.
|
|
486
|
+
#[unsafe(no_mangle)]
|
|
487
|
+
pub extern "C" fn gjsify_rolldown_session_try_result(
|
|
488
|
+
session: *mut BundleSession,
|
|
489
|
+
out_len: *mut usize,
|
|
490
|
+
is_error: *mut bool,
|
|
491
|
+
) -> *mut c_char {
|
|
492
|
+
if session.is_null() {
|
|
493
|
+
unsafe { *out_len = 0 };
|
|
494
|
+
unsafe { *is_error = true };
|
|
495
|
+
return ptr::null_mut();
|
|
496
|
+
}
|
|
497
|
+
let slot = match lookup_result_slot(session) {
|
|
498
|
+
Some(s) => s,
|
|
499
|
+
None => {
|
|
500
|
+
unsafe { *out_len = 0 };
|
|
501
|
+
unsafe { *is_error = false };
|
|
502
|
+
return ptr::null_mut();
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
let result = match slot.lock().unwrap().take() {
|
|
506
|
+
Some(r) => r,
|
|
507
|
+
None => {
|
|
508
|
+
unsafe { *out_len = 0 };
|
|
509
|
+
unsafe { *is_error = false };
|
|
510
|
+
return ptr::null_mut();
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
match result {
|
|
514
|
+
Ok(json) => {
|
|
515
|
+
unsafe { *is_error = false };
|
|
516
|
+
unsafe { *out_len = json.len() };
|
|
517
|
+
CString::new(json).ok().map(|c| c.into_raw()).unwrap_or(ptr::null_mut())
|
|
518
|
+
}
|
|
519
|
+
Err(msg) => {
|
|
520
|
+
unsafe { *is_error = true };
|
|
521
|
+
let wrapped = format!("{{\"error\":{}}}", serde_json::to_string(&msg).unwrap_or_else(|_| "\"<unprintable>\"".to_string()));
|
|
522
|
+
unsafe { *out_len = wrapped.len() };
|
|
523
|
+
CString::new(wrapped).ok().map(|c| c.into_raw()).unwrap_or(ptr::null_mut())
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[unsafe(no_mangle)]
|
|
529
|
+
pub extern "C" fn gjsify_rolldown_session_cancel(session: *mut BundleSession) {
|
|
530
|
+
if session.is_null() {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
let session = unsafe { &*session };
|
|
534
|
+
session.cancel();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#[unsafe(no_mangle)]
|
|
538
|
+
pub extern "C" fn gjsify_rolldown_session_free(session: *mut BundleSession) {
|
|
539
|
+
if session.is_null() {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
unregister_result_slot(session);
|
|
543
|
+
let boxed = unsafe { Box::from_raw(session) };
|
|
544
|
+
unsafe {
|
|
545
|
+
libc::close(boxed.request_eventfd);
|
|
546
|
+
libc::close(boxed.complete_eventfd);
|
|
547
|
+
libc::close(boxed.shared.context_response_eventfd);
|
|
548
|
+
}
|
|
549
|
+
// Dropping the runtime aborts all in-flight tasks. shutdown_timeout
|
|
550
|
+
// gives them up to 500ms to drain; any remaining workers are
|
|
551
|
+
// terminated. Prevents the GJS process exit from hanging.
|
|
552
|
+
boxed.runtime.shutdown_timeout(Duration::from_millis(500));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------------------------------------------------------------------
|
|
556
|
+
// Phase B.3 — nested-protocol FFI surface
|
|
557
|
+
// ---------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
/// JS-side request shape for `this.resolve()`. `args_json` is the
|
|
560
|
+
/// payload of `gjsify_rolldown_session_context_resolve`. We deserialize
|
|
561
|
+
/// it once, hand the values to `PluginContext.resolve()`, and surface
|
|
562
|
+
/// the result on the context-response channel.
|
|
563
|
+
#[derive(Debug, Deserialize)]
|
|
564
|
+
#[serde(rename_all = "camelCase")]
|
|
565
|
+
pub struct ContextResolveRequest {
|
|
566
|
+
pub specifier: String,
|
|
567
|
+
#[serde(default)]
|
|
568
|
+
pub importer: Option<String>,
|
|
569
|
+
#[serde(default)]
|
|
570
|
+
pub skip_self: Option<bool>,
|
|
571
|
+
#[serde(default)]
|
|
572
|
+
pub is_entry: Option<bool>,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/// FFI: trigger a `ctx.resolve()` callback on behalf of a JS plugin
|
|
576
|
+
/// hook handler. Returns a child request ID immediately; the actual
|
|
577
|
+
/// result lands on the context-response queue (drained via
|
|
578
|
+
/// `next_context_response()`).
|
|
579
|
+
///
|
|
580
|
+
/// Returns 0 on error (parent_req_id unknown / args malformed).
|
|
581
|
+
#[unsafe(no_mangle)]
|
|
582
|
+
pub extern "C" fn gjsify_rolldown_session_context_resolve(
|
|
583
|
+
session: *mut BundleSession,
|
|
584
|
+
parent_req_id: u64,
|
|
585
|
+
args_json: *const c_char,
|
|
586
|
+
args_json_len: usize,
|
|
587
|
+
) -> u64 {
|
|
588
|
+
if session.is_null() || args_json.is_null() {
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
let session = unsafe { &*session };
|
|
592
|
+
|
|
593
|
+
// Snapshot the parent context. Both load and transform hooks
|
|
594
|
+
// register a `PluginContext::clone()` keyed by their req_id
|
|
595
|
+
// before sending the JS request.
|
|
596
|
+
let ctx = match session.shared.contexts.lock().unwrap().get(&parent_req_id).cloned() {
|
|
597
|
+
Some(c) => c,
|
|
598
|
+
None => return 0,
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
let slice = unsafe { std::slice::from_raw_parts(args_json as *const u8, args_json_len) };
|
|
602
|
+
let req: ContextResolveRequest = match serde_json::from_slice(slice) {
|
|
603
|
+
Ok(r) => r,
|
|
604
|
+
Err(e) => {
|
|
605
|
+
// Best effort: drop a synthetic child reply so JS doesn't
|
|
606
|
+
// hang waiting on a Promise that will never resolve.
|
|
607
|
+
let child_id = session.shared.next_child_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
608
|
+
let resp = ContextResolveResponse {
|
|
609
|
+
child_id,
|
|
610
|
+
id: None,
|
|
611
|
+
external: None,
|
|
612
|
+
error: Some(format!("rolldown: malformed context_resolve args JSON: {e}")),
|
|
613
|
+
};
|
|
614
|
+
let _ = session.shared.context_response_tx.send(resp);
|
|
615
|
+
wake_eventfd(session.shared.context_response_eventfd);
|
|
616
|
+
return child_id;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
let child_id = session
|
|
621
|
+
.shared
|
|
622
|
+
.next_child_id
|
|
623
|
+
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
624
|
+
let shared = session.shared.clone();
|
|
625
|
+
|
|
626
|
+
session.runtime.spawn(async move {
|
|
627
|
+
let opts = PluginContextResolveOptions {
|
|
628
|
+
skip_self: req.skip_self.unwrap_or(true),
|
|
629
|
+
is_entry: req.is_entry.unwrap_or(false),
|
|
630
|
+
..Default::default()
|
|
631
|
+
};
|
|
632
|
+
let resp = match ctx
|
|
633
|
+
.resolve(&req.specifier, req.importer.as_deref(), Some(opts))
|
|
634
|
+
.await
|
|
635
|
+
{
|
|
636
|
+
Ok(Ok(resolved)) => ContextResolveResponse {
|
|
637
|
+
child_id,
|
|
638
|
+
id: Some(resolved.id.to_string()),
|
|
639
|
+
external: Some(matches!(
|
|
640
|
+
resolved.external,
|
|
641
|
+
rolldown_common::ResolvedExternal::Absolute
|
|
642
|
+
| rolldown_common::ResolvedExternal::Relative
|
|
643
|
+
)),
|
|
644
|
+
error: None,
|
|
645
|
+
},
|
|
646
|
+
Ok(Err(resolve_err)) => ContextResolveResponse {
|
|
647
|
+
child_id,
|
|
648
|
+
id: None,
|
|
649
|
+
external: None,
|
|
650
|
+
error: Some(format!("{resolve_err:?}")),
|
|
651
|
+
},
|
|
652
|
+
Err(e) => ContextResolveResponse {
|
|
653
|
+
child_id,
|
|
654
|
+
id: None,
|
|
655
|
+
external: None,
|
|
656
|
+
error: Some(format!("{e}")),
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
let _ = shared.context_response_tx.send(resp);
|
|
660
|
+
wake_eventfd(shared.context_response_eventfd);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
child_id
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/// JS-side `this.warn(msg)` — accumulate the message; it surfaces in
|
|
667
|
+
/// the final `BundleOutputJson.warnings` array.
|
|
668
|
+
#[unsafe(no_mangle)]
|
|
669
|
+
pub extern "C" fn gjsify_rolldown_session_context_warn(
|
|
670
|
+
session: *mut BundleSession,
|
|
671
|
+
message: *const c_char,
|
|
672
|
+
message_len: usize,
|
|
673
|
+
) {
|
|
674
|
+
if session.is_null() || message.is_null() {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
let slice = unsafe { std::slice::from_raw_parts(message as *const u8, message_len) };
|
|
678
|
+
let msg = match std::str::from_utf8(slice) {
|
|
679
|
+
Ok(s) => s.to_string(),
|
|
680
|
+
Err(_) => "rolldown: this.warn() called with non-UTF-8 message".to_string(),
|
|
681
|
+
};
|
|
682
|
+
let session = unsafe { &*session };
|
|
683
|
+
session.shared.context_warnings.lock().unwrap().push(msg);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/// FFI: get the eventfd that signals "a context-resolve sub-result is
|
|
687
|
+
/// available". GLib main loop watches this with `G_IO_IN`.
|
|
688
|
+
#[unsafe(no_mangle)]
|
|
689
|
+
pub extern "C" fn gjsify_rolldown_session_context_response_fd(
|
|
690
|
+
session: *mut BundleSession,
|
|
691
|
+
) -> c_int {
|
|
692
|
+
if session.is_null() {
|
|
693
|
+
return -1;
|
|
694
|
+
}
|
|
695
|
+
let session = unsafe { &*session };
|
|
696
|
+
session.shared.context_response_eventfd
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/// FFI: drain one pending context-resolve response. Returns NULL when
|
|
700
|
+
/// the channel is empty.
|
|
701
|
+
#[unsafe(no_mangle)]
|
|
702
|
+
pub extern "C" fn gjsify_rolldown_session_next_context_response(
|
|
703
|
+
session: *mut BundleSession,
|
|
704
|
+
out_len: *mut usize,
|
|
705
|
+
) -> *mut c_char {
|
|
706
|
+
if session.is_null() {
|
|
707
|
+
unsafe { *out_len = 0 };
|
|
708
|
+
return ptr::null_mut();
|
|
709
|
+
}
|
|
710
|
+
let session = unsafe { &*session };
|
|
711
|
+
let resp = match session.context_response_rx.try_recv() {
|
|
712
|
+
Ok(r) => r,
|
|
713
|
+
Err(_) => {
|
|
714
|
+
unsafe { *out_len = 0 };
|
|
715
|
+
return ptr::null_mut();
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
let json = match serde_json::to_string(&resp) {
|
|
719
|
+
Ok(s) => s,
|
|
720
|
+
Err(e) => format!("{{\"childId\":{},\"error\":\"serialize: {e}\"}}", resp.child_id),
|
|
721
|
+
};
|
|
722
|
+
unsafe { *out_len = json.len() };
|
|
723
|
+
CString::new(json).ok().map(|c| c.into_raw()).unwrap_or(ptr::null_mut())
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
fn wake_eventfd(fd: c_int) {
|
|
727
|
+
let one: u64 = 1;
|
|
728
|
+
unsafe {
|
|
729
|
+
libc::write(fd, &one as *const u64 as *const libc::c_void, 8);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------
|
|
734
|
+
// Phase B.4 — bytes-payload side-channel for the transform hook.
|
|
735
|
+
// ---------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
/// FFI: drain the request-payload bytes the Rust side stashed for
|
|
738
|
+
/// `req_id` (transform's source code). Returns NULL if the slot is
|
|
739
|
+
/// empty or already consumed. Caller frees the returned buffer via
|
|
740
|
+
/// `gjsify_rolldown_session_free_payload(buf, len)`.
|
|
741
|
+
#[unsafe(no_mangle)]
|
|
742
|
+
pub extern "C" fn gjsify_rolldown_session_take_request_payload(
|
|
743
|
+
session: *mut BundleSession,
|
|
744
|
+
req_id: u64,
|
|
745
|
+
out_len: *mut usize,
|
|
746
|
+
) -> *mut u8 {
|
|
747
|
+
if session.is_null() {
|
|
748
|
+
unsafe { *out_len = 0 };
|
|
749
|
+
return ptr::null_mut();
|
|
750
|
+
}
|
|
751
|
+
let session = unsafe { &*session };
|
|
752
|
+
let bytes = match session.shared.request_payloads.lock().unwrap().remove(&req_id) {
|
|
753
|
+
Some(b) => b,
|
|
754
|
+
None => {
|
|
755
|
+
unsafe { *out_len = 0 };
|
|
756
|
+
return ptr::null_mut();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
let mut boxed = bytes.into_boxed_slice();
|
|
760
|
+
let ptr = boxed.as_mut_ptr();
|
|
761
|
+
let len = boxed.len();
|
|
762
|
+
std::mem::forget(boxed);
|
|
763
|
+
unsafe { *out_len = len };
|
|
764
|
+
ptr
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/// FFI: JS-side stashes response payload bytes (the transform hook's
|
|
768
|
+
/// output code) for the Rust dispatch site to pick up after
|
|
769
|
+
/// `respond()`. Returns true on success.
|
|
770
|
+
#[unsafe(no_mangle)]
|
|
771
|
+
pub extern "C" fn gjsify_rolldown_session_set_response_payload(
|
|
772
|
+
session: *mut BundleSession,
|
|
773
|
+
req_id: u64,
|
|
774
|
+
bytes: *const u8,
|
|
775
|
+
bytes_len: usize,
|
|
776
|
+
) -> bool {
|
|
777
|
+
if session.is_null() || bytes.is_null() {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
let slice = unsafe { std::slice::from_raw_parts(bytes, bytes_len) };
|
|
781
|
+
let owned = slice.to_vec();
|
|
782
|
+
let session = unsafe { &*session };
|
|
783
|
+
session
|
|
784
|
+
.shared
|
|
785
|
+
.response_payloads
|
|
786
|
+
.lock()
|
|
787
|
+
.unwrap()
|
|
788
|
+
.insert(req_id, owned);
|
|
789
|
+
true
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/// Free a payload buffer returned by
|
|
793
|
+
/// `gjsify_rolldown_session_take_request_payload`. The caller MUST
|
|
794
|
+
/// pass back the same length they received.
|
|
795
|
+
#[unsafe(no_mangle)]
|
|
796
|
+
pub extern "C" fn gjsify_rolldown_session_free_payload(buf: *mut u8, len: usize) {
|
|
797
|
+
if buf.is_null() {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
let slice = unsafe { std::slice::from_raw_parts_mut(buf, len) };
|
|
801
|
+
let _boxed = unsafe { Box::from_raw(slice as *mut [u8]) };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
#[unsafe(no_mangle)]
|
|
805
|
+
pub extern "C" fn gjsify_rolldown_session_free_string(s: *mut c_char) {
|
|
806
|
+
if !s.is_null() {
|
|
807
|
+
unsafe { drop(CString::from_raw(s)) };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
#[unsafe(no_mangle)]
|
|
812
|
+
pub extern "C" fn gjsify_rolldown_session_free_error(s: *mut c_char) {
|
|
813
|
+
if !s.is_null() {
|
|
814
|
+
unsafe { drop(CString::from_raw(s)) };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// --- helpers -----------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
fn parse_json_args(p: *const c_char, len: usize) -> Result<StartArgs, String> {
|
|
821
|
+
if p.is_null() || len == 0 {
|
|
822
|
+
return Err("rolldown: empty args JSON".to_string());
|
|
823
|
+
}
|
|
824
|
+
let slice = unsafe { std::slice::from_raw_parts(p as *const u8, len) };
|
|
825
|
+
let s = std::str::from_utf8(slice).map_err(|_| "rolldown: invalid UTF-8 in args".to_string())?;
|
|
826
|
+
serde_json::from_str(s).map_err(|e| format!("rolldown: args parse: {e}"))
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
fn err_to_cstr(s: String) -> *mut c_char {
|
|
830
|
+
CString::new(s)
|
|
831
|
+
.unwrap_or_else(|_| CString::new("rolldown: <unprintable error>").unwrap())
|
|
832
|
+
.into_raw()
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
fn num_cpus_capped() -> usize {
|
|
836
|
+
std::thread::available_parallelism()
|
|
837
|
+
.map(|n| n.get().min(4))
|
|
838
|
+
.unwrap_or(2)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
fn compile_filter(
|
|
842
|
+
src: &Option<String>,
|
|
843
|
+
plugin_name: &str,
|
|
844
|
+
hook_name: &str,
|
|
845
|
+
) -> Result<Option<regex::Regex>, String> {
|
|
846
|
+
match src {
|
|
847
|
+
None => Ok(None),
|
|
848
|
+
Some(pat) => regex::Regex::new(pat).map(Some).map_err(|e| {
|
|
849
|
+
format!("rolldown: plugin '{plugin_name}' {hook_name} filter '{pat}' invalid: {e}")
|
|
850
|
+
}),
|
|
851
|
+
}
|
|
852
|
+
}
|