@gjsify/rolldown-native 0.4.0 → 0.4.3
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-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,779 @@
|
|
|
1
|
+
//! JsPluginProxy — a `Pluginable` implementation that ships hook
|
|
2
|
+
//! invocations through a crossbeam channel for a JavaScript-side
|
|
3
|
+
//! handler (running on the GJS main loop) to fulfill.
|
|
4
|
+
//!
|
|
5
|
+
//! Phase B.2 scope: all 12 hooks routed through the channel. JS-side
|
|
6
|
+
//! filter handling (a plugin declared as `{filter, handler}` short-
|
|
7
|
+
//! circuits before reaching us by virtue of register_hook_usage). The
|
|
8
|
+
//! actual per-call regex-against-id matching for `transform({filter})`
|
|
9
|
+
//! style plugins is performed JS-side for now; pushing it into Rust to
|
|
10
|
+
//! avoid the JSON roundtrip on no-match is the Phase B.4 optimization.
|
|
11
|
+
//!
|
|
12
|
+
//! Architecture mirrors `@gjsify/webrtc-native`'s signal-bridge
|
|
13
|
+
//! pattern (refs `packages/web/webrtc-native/src/vala/promise-bridge.vala`):
|
|
14
|
+
//! - tokio-task running `Bundler::generate()` on the worker pool
|
|
15
|
+
//! - calls into JsPluginProxy from various worker threads
|
|
16
|
+
//! - JsPluginProxy serializes a `HookRequest`, pushes to channel
|
|
17
|
+
//! - eventfd-write wakes the GLib main loop
|
|
18
|
+
//! - Vala source pulls requests, emits GObject signal on main thread
|
|
19
|
+
//! - JS handler runs, calls `session.respond(req_id, json)`
|
|
20
|
+
//! - Vala forwards to `gjsify_rolldown_session_respond` extern
|
|
21
|
+
//! - Rust pulls the matching oneshot::Sender from `pending`, sends response
|
|
22
|
+
//! - tokio-task on the original thread wakes up from `oneshot::recv()`
|
|
23
|
+
//! and continues with the parsed result.
|
|
24
|
+
|
|
25
|
+
use std::borrow::Cow;
|
|
26
|
+
use std::fmt;
|
|
27
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
28
|
+
use std::time::Duration;
|
|
29
|
+
|
|
30
|
+
use anyhow::anyhow;
|
|
31
|
+
use crossbeam_channel::Sender;
|
|
32
|
+
use regex::Regex;
|
|
33
|
+
use rolldown_plugin::{
|
|
34
|
+
HookAddonArgs, HookBuildEndArgs, HookBuildStartArgs, HookCloseBundleArgs,
|
|
35
|
+
HookGenerateBundleArgs, HookInjectionOutputReturn, HookLoadArgs, HookLoadOutput,
|
|
36
|
+
HookLoadReturn, HookNoopReturn, HookRenderChunkArgs, HookRenderChunkOutput,
|
|
37
|
+
HookRenderChunkReturn, HookResolveIdArgs, HookResolveIdOutput, HookResolveIdReturn,
|
|
38
|
+
HookTransformArgs, HookTransformOutput, HookTransformReturn, HookUsage, HookWriteBundleArgs,
|
|
39
|
+
Plugin, PluginContext, SharedLoadPluginContext, SharedTransformPluginContext,
|
|
40
|
+
};
|
|
41
|
+
use serde::{Deserialize, Serialize};
|
|
42
|
+
use tokio::sync::oneshot;
|
|
43
|
+
|
|
44
|
+
/// Per-plugin metadata we send with each request so the JS adapter
|
|
45
|
+
/// knows which user plugin to dispatch to.
|
|
46
|
+
///
|
|
47
|
+
/// `id_filter` lets a plugin pre-declare per-hook regex filters
|
|
48
|
+
/// (load/transform/resolveId only — the only hooks where a single
|
|
49
|
+
/// `id` string is the matchable input). When set, the proxy compiles
|
|
50
|
+
/// the regex once and skips the JS round-trip whenever the id doesn't
|
|
51
|
+
/// match. Mirrors rolldown's own `HookFilter.value` short-circuit
|
|
52
|
+
/// path; full token-tree boolean expressions stay JS-side for now.
|
|
53
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
54
|
+
#[serde(rename_all = "camelCase")]
|
|
55
|
+
pub struct PluginMeta {
|
|
56
|
+
pub name: String,
|
|
57
|
+
pub hooks: Vec<String>,
|
|
58
|
+
#[serde(default)]
|
|
59
|
+
pub id_filter: PluginIdFilter,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
63
|
+
#[serde(rename_all = "camelCase")]
|
|
64
|
+
pub struct PluginIdFilter {
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
pub load: Option<String>,
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub transform: Option<String>,
|
|
69
|
+
#[serde(default)]
|
|
70
|
+
pub resolve_id: Option<String>,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Hook invocation flowing Rust → JS. Tagged so the Vala bridge can
|
|
74
|
+
/// route to the matching GObject signal.
|
|
75
|
+
///
|
|
76
|
+
/// Per-hook payloads are intentionally small. Phase B.4 split heavy
|
|
77
|
+
/// data (`transform.code`) out of the JSON envelope and into a
|
|
78
|
+
/// parallel bytes-payload slot keyed by req_id, so the JS adapter
|
|
79
|
+
/// can fetch it as a `GLib.Bytes` (zero-copy Uint8Array view) via
|
|
80
|
+
/// `take_request_payload(reqId)`. The Transform variant therefore
|
|
81
|
+
/// no longer carries the code string — only `payloadKind:'code'`
|
|
82
|
+
/// telling JS to go fetch the bytes.
|
|
83
|
+
#[derive(Debug, Serialize)]
|
|
84
|
+
#[serde(tag = "hook", rename_all = "camelCase")]
|
|
85
|
+
pub enum HookRequestPayload {
|
|
86
|
+
#[serde(rename_all = "camelCase")]
|
|
87
|
+
Load { id: String },
|
|
88
|
+
#[serde(rename_all = "camelCase")]
|
|
89
|
+
ResolveId { specifier: String, importer: Option<String>, is_entry: bool },
|
|
90
|
+
/// `code` is delivered as bytes via the request-payload side-channel
|
|
91
|
+
/// (`take_request_payload(reqId)`). The `payload_kind` marker
|
|
92
|
+
/// flags this for the JS adapter.
|
|
93
|
+
#[serde(rename_all = "camelCase")]
|
|
94
|
+
Transform { id: String, module_type: String, payload_kind: &'static str },
|
|
95
|
+
#[serde(rename_all = "camelCase")]
|
|
96
|
+
RenderChunk { code: String, file_name: String, name: String, is_entry: bool },
|
|
97
|
+
#[serde(rename_all = "camelCase")]
|
|
98
|
+
Banner { file_name: String, name: String, is_entry: bool },
|
|
99
|
+
#[serde(rename_all = "camelCase")]
|
|
100
|
+
Footer { file_name: String, name: String, is_entry: bool },
|
|
101
|
+
#[serde(rename_all = "camelCase")]
|
|
102
|
+
Intro { file_name: String, name: String, is_entry: bool },
|
|
103
|
+
#[serde(rename_all = "camelCase")]
|
|
104
|
+
Outro { file_name: String, name: String, is_entry: bool },
|
|
105
|
+
BuildStart {},
|
|
106
|
+
#[serde(rename_all = "camelCase")]
|
|
107
|
+
BuildEnd { error: Option<String> },
|
|
108
|
+
GenerateBundle {},
|
|
109
|
+
WriteBundle {},
|
|
110
|
+
CloseBundle {},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Wire-format envelope for one hook invocation. Sent over
|
|
114
|
+
/// crossbeam channel; eventfd written after each push.
|
|
115
|
+
pub struct HookRequest {
|
|
116
|
+
pub req_id: u64,
|
|
117
|
+
pub plugin_index: usize,
|
|
118
|
+
pub payload: HookRequestPayload,
|
|
119
|
+
/// Filled in by JS via `session.respond()`.
|
|
120
|
+
pub reply: oneshot::Sender<HookResponse>,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
impl fmt::Debug for HookRequest {
|
|
124
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
125
|
+
f.debug_struct("HookRequest")
|
|
126
|
+
.field("req_id", &self.req_id)
|
|
127
|
+
.field("plugin_index", &self.plugin_index)
|
|
128
|
+
.field("payload", &self.payload)
|
|
129
|
+
.finish()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Tagged response from JS handler. Three states match rolldown's
|
|
134
|
+
/// `Result<Option<T>>` convention:
|
|
135
|
+
/// - `Skip` → `Ok(None)` → next plugin in chain runs
|
|
136
|
+
/// - `Ok` → `Ok(Some(value))` → this plugin's result wins
|
|
137
|
+
/// - `Error` → `Err(BuildDiagnostic)` → fail the build
|
|
138
|
+
#[derive(Debug, Deserialize)]
|
|
139
|
+
#[serde(tag = "kind", rename_all = "camelCase")]
|
|
140
|
+
pub enum HookResponse {
|
|
141
|
+
Skip,
|
|
142
|
+
Ok { value: serde_json::Value },
|
|
143
|
+
Error { message: String, stack: Option<String> },
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Wire shape for a load-hook response. Mirrors the small subset of
|
|
147
|
+
/// `HookLoadOutput` we expose to JS plugins (no source-map yet — that
|
|
148
|
+
/// comes in Phase B.4 alongside the zero-copy GBytes path).
|
|
149
|
+
#[derive(Debug, Deserialize)]
|
|
150
|
+
#[serde(rename_all = "camelCase")]
|
|
151
|
+
pub struct LoadHookValue {
|
|
152
|
+
pub code: String,
|
|
153
|
+
#[serde(default)]
|
|
154
|
+
pub module_type: Option<String>,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[derive(Debug, Deserialize)]
|
|
158
|
+
#[serde(rename_all = "camelCase")]
|
|
159
|
+
pub struct ResolveIdHookValue {
|
|
160
|
+
pub id: String,
|
|
161
|
+
#[serde(default)]
|
|
162
|
+
pub external: Option<bool>,
|
|
163
|
+
#[serde(default)]
|
|
164
|
+
pub side_effects: Option<bool>,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[derive(Debug, Deserialize)]
|
|
168
|
+
#[serde(rename_all = "camelCase")]
|
|
169
|
+
pub struct TransformHookValue {
|
|
170
|
+
#[serde(default)]
|
|
171
|
+
pub code: Option<String>,
|
|
172
|
+
#[serde(default)]
|
|
173
|
+
pub module_type: Option<String>,
|
|
174
|
+
/// Phase B.4 — when true, the new code bytes were stashed via
|
|
175
|
+
/// `set_response_payload(reqId, bytes)` before `respond()`.
|
|
176
|
+
/// `code` is then expected to be absent and the Rust side picks
|
|
177
|
+
/// the bytes up from `SessionShared.response_payloads`.
|
|
178
|
+
#[serde(default)]
|
|
179
|
+
pub has_code_bytes: bool,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[derive(Debug, Deserialize)]
|
|
183
|
+
#[serde(rename_all = "camelCase")]
|
|
184
|
+
pub struct RenderChunkHookValue {
|
|
185
|
+
pub code: String,
|
|
186
|
+
// Source map deferred to Phase B.4 (zero-copy GBytes path).
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[derive(Debug, Deserialize)]
|
|
190
|
+
#[serde(rename_all = "camelCase")]
|
|
191
|
+
pub struct StringHookValue {
|
|
192
|
+
pub text: String,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn parse_module_type(s: Option<&str>) -> anyhow::Result<Option<rolldown_common::ModuleType>> {
|
|
196
|
+
match s {
|
|
197
|
+
None => Ok(None),
|
|
198
|
+
Some("js") | Some("ecmascript") => Ok(Some(rolldown_common::ModuleType::Js)),
|
|
199
|
+
Some("json") => Ok(Some(rolldown_common::ModuleType::Json)),
|
|
200
|
+
Some("text") => Ok(Some(rolldown_common::ModuleType::Text)),
|
|
201
|
+
Some(other) => Err(anyhow!("rolldown: unsupported moduleType '{}'", other)),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl HookResponse {
|
|
206
|
+
pub fn into_load_return(self) -> HookLoadReturn {
|
|
207
|
+
match self {
|
|
208
|
+
HookResponse::Skip => Ok(None),
|
|
209
|
+
HookResponse::Ok { value } => {
|
|
210
|
+
let v: LoadHookValue = serde_json::from_value(value)
|
|
211
|
+
.map_err(|e| anyhow!("rolldown: malformed load response: {e}"))?;
|
|
212
|
+
let module_type = parse_module_type(v.module_type.as_deref())?;
|
|
213
|
+
Ok(Some(HookLoadOutput {
|
|
214
|
+
code: v.code.into(),
|
|
215
|
+
map: None,
|
|
216
|
+
side_effects: None,
|
|
217
|
+
module_type,
|
|
218
|
+
}))
|
|
219
|
+
}
|
|
220
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
pub fn into_resolve_id_return(self) -> HookResolveIdReturn {
|
|
225
|
+
match self {
|
|
226
|
+
HookResponse::Skip => Ok(None),
|
|
227
|
+
HookResponse::Ok { value } => {
|
|
228
|
+
let v: ResolveIdHookValue = serde_json::from_value(value)
|
|
229
|
+
.map_err(|e| anyhow!("rolldown: malformed resolveId response: {e}"))?;
|
|
230
|
+
let external = match v.external {
|
|
231
|
+
Some(true) => Some(rolldown_common::ResolvedExternal::Absolute),
|
|
232
|
+
_ => None,
|
|
233
|
+
};
|
|
234
|
+
let side_effects = match v.side_effects {
|
|
235
|
+
Some(true) => Some(rolldown_common::side_effects::HookSideEffects::True),
|
|
236
|
+
Some(false) => Some(rolldown_common::side_effects::HookSideEffects::False),
|
|
237
|
+
None => None,
|
|
238
|
+
};
|
|
239
|
+
Ok(Some(HookResolveIdOutput {
|
|
240
|
+
id: v.id.into(),
|
|
241
|
+
external,
|
|
242
|
+
normalize_external_id: None,
|
|
243
|
+
side_effects,
|
|
244
|
+
package_json_path: None,
|
|
245
|
+
}))
|
|
246
|
+
}
|
|
247
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Phase B.4 variant — like `into_transform_return` but consumes
|
|
252
|
+
/// pre-fetched `response_bytes` (taken from the response-payload
|
|
253
|
+
/// slot at the dispatch site) when the response envelope
|
|
254
|
+
/// advertises `hasCodeBytes: true`. Falls back to `value.code`
|
|
255
|
+
/// (JSON-embedded string) otherwise so callers not yet using the
|
|
256
|
+
/// zero-copy path keep working.
|
|
257
|
+
pub fn into_transform_return_with_bytes(
|
|
258
|
+
self,
|
|
259
|
+
response_bytes: Option<Vec<u8>>,
|
|
260
|
+
) -> HookTransformReturn {
|
|
261
|
+
match self {
|
|
262
|
+
HookResponse::Skip => Ok(None),
|
|
263
|
+
HookResponse::Ok { value } => {
|
|
264
|
+
let v: TransformHookValue = serde_json::from_value(value)
|
|
265
|
+
.map_err(|e| anyhow!("rolldown: malformed transform response: {e}"))?;
|
|
266
|
+
let module_type = parse_module_type(v.module_type.as_deref())?;
|
|
267
|
+
let code = if v.has_code_bytes {
|
|
268
|
+
let bytes = response_bytes.ok_or_else(|| {
|
|
269
|
+
anyhow!("rolldown: transform response advertised hasCodeBytes but no payload was stashed")
|
|
270
|
+
})?;
|
|
271
|
+
match String::from_utf8(bytes) {
|
|
272
|
+
Ok(s) => Some(s),
|
|
273
|
+
Err(e) => return Err(anyhow!("rolldown: transform response payload was not valid UTF-8: {e}")),
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
v.code
|
|
277
|
+
};
|
|
278
|
+
Ok(Some(HookTransformOutput {
|
|
279
|
+
code,
|
|
280
|
+
map: None,
|
|
281
|
+
side_effects: None,
|
|
282
|
+
module_type,
|
|
283
|
+
}))
|
|
284
|
+
}
|
|
285
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pub fn into_transform_return(self) -> HookTransformReturn {
|
|
290
|
+
match self {
|
|
291
|
+
HookResponse::Skip => Ok(None),
|
|
292
|
+
HookResponse::Ok { value } => {
|
|
293
|
+
let v: TransformHookValue = serde_json::from_value(value)
|
|
294
|
+
.map_err(|e| anyhow!("rolldown: malformed transform response: {e}"))?;
|
|
295
|
+
let module_type = parse_module_type(v.module_type.as_deref())?;
|
|
296
|
+
Ok(Some(HookTransformOutput {
|
|
297
|
+
code: v.code,
|
|
298
|
+
map: None,
|
|
299
|
+
side_effects: None,
|
|
300
|
+
module_type,
|
|
301
|
+
}))
|
|
302
|
+
}
|
|
303
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pub fn into_render_chunk_return(self) -> HookRenderChunkReturn {
|
|
308
|
+
match self {
|
|
309
|
+
HookResponse::Skip => Ok(None),
|
|
310
|
+
HookResponse::Ok { value } => {
|
|
311
|
+
let v: RenderChunkHookValue = serde_json::from_value(value)
|
|
312
|
+
.map_err(|e| anyhow!("rolldown: malformed renderChunk response: {e}"))?;
|
|
313
|
+
Ok(Some(HookRenderChunkOutput {
|
|
314
|
+
code: v.code,
|
|
315
|
+
map: None,
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// banner/footer/intro/outro all return `Result<Option<String>>`.
|
|
323
|
+
pub fn into_injection_return(self) -> HookInjectionOutputReturn {
|
|
324
|
+
match self {
|
|
325
|
+
HookResponse::Skip => Ok(None),
|
|
326
|
+
HookResponse::Ok { value } => {
|
|
327
|
+
// Accept either {"text": "..."} or a raw string.
|
|
328
|
+
if let serde_json::Value::String(s) = &value {
|
|
329
|
+
return Ok(Some(s.clone()));
|
|
330
|
+
}
|
|
331
|
+
let v: StringHookValue = serde_json::from_value(value)
|
|
332
|
+
.map_err(|e| anyhow!("rolldown: malformed injection response: {e}"))?;
|
|
333
|
+
Ok(Some(v.text))
|
|
334
|
+
}
|
|
335
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// build_start/build_end/generate_bundle/write_bundle/close_bundle
|
|
340
|
+
/// all return `Result<()>`.
|
|
341
|
+
pub fn into_noop_return(self) -> HookNoopReturn {
|
|
342
|
+
match self {
|
|
343
|
+
HookResponse::Skip | HookResponse::Ok { .. } => Ok(()),
|
|
344
|
+
HookResponse::Error { message, stack } => Err(combine(message, stack)),
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn combine(message: String, stack: Option<String>) -> anyhow::Error {
|
|
350
|
+
let mut msg = message;
|
|
351
|
+
if let Some(s) = stack {
|
|
352
|
+
msg.push('\n');
|
|
353
|
+
msg.push_str(&s);
|
|
354
|
+
}
|
|
355
|
+
anyhow!(msg)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// `Pluginable` implementation that proxies a single user plugin.
|
|
359
|
+
/// Each plugin in the user's `plugins[]` array gets its own
|
|
360
|
+
/// `JsPluginProxy` so rolldown's per-hook ordering / first-non-null
|
|
361
|
+
/// chaining works unmodified.
|
|
362
|
+
pub struct JsPluginProxy {
|
|
363
|
+
pub name: String,
|
|
364
|
+
pub plugin_index: usize,
|
|
365
|
+
pub hooks: Vec<String>,
|
|
366
|
+
pub request_tx: Sender<HookRequest>,
|
|
367
|
+
/// Atomic monotonic ID dispenser; shared across all proxies in
|
|
368
|
+
/// the same session via Arc clone in the session constructor.
|
|
369
|
+
pub next_request_id: std::sync::Arc<AtomicU64>,
|
|
370
|
+
/// Wakeup pipe written after each request.send() so the Vala
|
|
371
|
+
/// source on the main loop can react.
|
|
372
|
+
pub request_eventfd: i32,
|
|
373
|
+
/// Maximum time we wait for a JS response before failing the
|
|
374
|
+
/// build with a timeout error. Defaults to 60s.
|
|
375
|
+
pub response_timeout: Duration,
|
|
376
|
+
/// Compiled per-hook id regex. None means "always dispatch".
|
|
377
|
+
pub load_id_filter: Option<Regex>,
|
|
378
|
+
pub transform_id_filter: Option<Regex>,
|
|
379
|
+
pub resolve_id_filter: Option<Regex>,
|
|
380
|
+
/// Shared session state for nested-protocol callbacks
|
|
381
|
+
/// (`this.resolve()` / `this.warn()`). The proxy registers each
|
|
382
|
+
/// load/transform hook's `PluginContext` here so the JS handler
|
|
383
|
+
/// can re-enter Rust during its `await`.
|
|
384
|
+
pub shared: std::sync::Arc<crate::session::SessionShared>,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
impl fmt::Debug for JsPluginProxy {
|
|
388
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
389
|
+
f.debug_struct("JsPluginProxy")
|
|
390
|
+
.field("name", &self.name)
|
|
391
|
+
.field("plugin_index", &self.plugin_index)
|
|
392
|
+
.field("hooks", &self.hooks)
|
|
393
|
+
.finish()
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
impl JsPluginProxy {
|
|
398
|
+
fn new_request_id(&self) -> u64 {
|
|
399
|
+
self.next_request_id.fetch_add(1, Ordering::SeqCst)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fn proxies(&self, hook: &str) -> bool {
|
|
403
|
+
self.hooks.iter().any(|h| h == hook)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Push a hook request onto the channel + wake the main loop via
|
|
407
|
+
/// eventfd-write, then await the JS-side response.
|
|
408
|
+
async fn dispatch(&self, payload: HookRequestPayload) -> anyhow::Result<HookResponse> {
|
|
409
|
+
let (resp, _bytes) = self.dispatch_inner(payload, None, None).await?;
|
|
410
|
+
Ok(resp)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/// Variant of `dispatch` that registers `ctx` for the duration of
|
|
414
|
+
/// the JS handler's await. Used by load/transform hooks so the
|
|
415
|
+
/// JS handler may call `this.resolve()` / `this.warn()` mid-await
|
|
416
|
+
/// via `gjsify_rolldown_session_context_resolve`.
|
|
417
|
+
async fn dispatch_with_ctx(
|
|
418
|
+
&self,
|
|
419
|
+
payload: HookRequestPayload,
|
|
420
|
+
ctx: rolldown_plugin::PluginContext,
|
|
421
|
+
) -> anyhow::Result<HookResponse> {
|
|
422
|
+
let (resp, _bytes) = self.dispatch_inner(payload, Some(ctx), None).await?;
|
|
423
|
+
Ok(resp)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// Variant that pre-stashes a bytes payload alongside the JSON
|
|
427
|
+
/// envelope. The JS side fetches it via
|
|
428
|
+
/// `take_request_payload(reqId)` instead of seeing it embedded in
|
|
429
|
+
/// the envelope. Used by `transform` to avoid JSON-escaping
|
|
430
|
+
/// arbitrarily large source code. Returns the response paired
|
|
431
|
+
/// with any response bytes the JS side stashed before replying.
|
|
432
|
+
async fn dispatch_with_payload(
|
|
433
|
+
&self,
|
|
434
|
+
payload: HookRequestPayload,
|
|
435
|
+
ctx: rolldown_plugin::PluginContext,
|
|
436
|
+
request_bytes: Vec<u8>,
|
|
437
|
+
) -> anyhow::Result<(HookResponse, Option<Vec<u8>>)> {
|
|
438
|
+
self.dispatch_inner(payload, Some(ctx), Some(request_bytes)).await
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async fn dispatch_inner(
|
|
442
|
+
&self,
|
|
443
|
+
payload: HookRequestPayload,
|
|
444
|
+
ctx: Option<rolldown_plugin::PluginContext>,
|
|
445
|
+
request_bytes: Option<Vec<u8>>,
|
|
446
|
+
) -> anyhow::Result<(HookResponse, Option<Vec<u8>>)> {
|
|
447
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
448
|
+
let req_id = self.new_request_id();
|
|
449
|
+
|
|
450
|
+
if let Some(c) = ctx {
|
|
451
|
+
self.shared.contexts.lock().unwrap().insert(req_id, c);
|
|
452
|
+
}
|
|
453
|
+
if let Some(bytes) = request_bytes {
|
|
454
|
+
self.shared.request_payloads.lock().unwrap().insert(req_id, bytes);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let req = HookRequest {
|
|
458
|
+
req_id,
|
|
459
|
+
plugin_index: self.plugin_index,
|
|
460
|
+
payload,
|
|
461
|
+
reply: reply_tx,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
if self.request_tx.send(req).is_err() {
|
|
465
|
+
self.shared.contexts.lock().unwrap().remove(&req_id);
|
|
466
|
+
self.shared.request_payloads.lock().unwrap().remove(&req_id);
|
|
467
|
+
return Err(anyhow!("rolldown: plugin channel closed (session aborted)"));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let one: u64 = 1;
|
|
471
|
+
unsafe {
|
|
472
|
+
libc::write(
|
|
473
|
+
self.request_eventfd,
|
|
474
|
+
&one as *const u64 as *const libc::c_void,
|
|
475
|
+
8,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let result = tokio::time::timeout(self.response_timeout, reply_rx).await;
|
|
480
|
+
// Clear the context registration regardless of outcome; the
|
|
481
|
+
// JS handler is finished (or timed out) and any further
|
|
482
|
+
// `this.resolve()` calls keyed on this req_id would race.
|
|
483
|
+
self.shared.contexts.lock().unwrap().remove(&req_id);
|
|
484
|
+
// Clean up any unconsumed request payload bytes. If the JS
|
|
485
|
+
// adapter took ownership via `take_request_payload`, this is
|
|
486
|
+
// a no-op; if it never did, we don't want the bytes to leak.
|
|
487
|
+
self.shared.request_payloads.lock().unwrap().remove(&req_id);
|
|
488
|
+
// Pop any response bytes the JS side stashed via
|
|
489
|
+
// `set_response_payload(req_id, bytes)`. Returned to the
|
|
490
|
+
// caller so it can decode + use without going through JSON.
|
|
491
|
+
let response_bytes = self.shared.response_payloads.lock().unwrap().remove(&req_id);
|
|
492
|
+
|
|
493
|
+
match result {
|
|
494
|
+
Ok(Ok(resp)) => Ok((resp, response_bytes)),
|
|
495
|
+
Ok(Err(_)) => Err(anyhow!(
|
|
496
|
+
"rolldown: plugin {} dropped reply for request",
|
|
497
|
+
self.name
|
|
498
|
+
)),
|
|
499
|
+
Err(_) => Err(anyhow!(
|
|
500
|
+
"rolldown: plugin {} timed out after {}s waiting for JS response",
|
|
501
|
+
self.name,
|
|
502
|
+
self.response_timeout.as_secs()
|
|
503
|
+
)),
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
impl Plugin for JsPluginProxy {
|
|
509
|
+
fn name(&self) -> Cow<'static, str> {
|
|
510
|
+
Cow::Owned(self.name.clone())
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
fn register_hook_usage(&self) -> HookUsage {
|
|
514
|
+
let mut usage = HookUsage::empty();
|
|
515
|
+
for h in &self.hooks {
|
|
516
|
+
match h.as_str() {
|
|
517
|
+
"load" => usage |= HookUsage::Load,
|
|
518
|
+
"transform" => usage |= HookUsage::Transform,
|
|
519
|
+
"resolveId" => usage |= HookUsage::ResolveId,
|
|
520
|
+
"renderChunk" => usage |= HookUsage::RenderChunk,
|
|
521
|
+
"buildStart" => usage |= HookUsage::BuildStart,
|
|
522
|
+
"buildEnd" => usage |= HookUsage::BuildEnd,
|
|
523
|
+
"generateBundle" => usage |= HookUsage::GenerateBundle,
|
|
524
|
+
"writeBundle" => usage |= HookUsage::WriteBundle,
|
|
525
|
+
"closeBundle" => usage |= HookUsage::CloseBundle,
|
|
526
|
+
"banner" => usage |= HookUsage::Banner,
|
|
527
|
+
"footer" => usage |= HookUsage::Footer,
|
|
528
|
+
"intro" => usage |= HookUsage::Intro,
|
|
529
|
+
"outro" => usage |= HookUsage::Outro,
|
|
530
|
+
_ => {}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
usage
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
fn load(
|
|
537
|
+
&self,
|
|
538
|
+
ctx: SharedLoadPluginContext,
|
|
539
|
+
args: &HookLoadArgs<'_>,
|
|
540
|
+
) -> impl std::future::Future<Output = HookLoadReturn> + Send {
|
|
541
|
+
let proxies = self.proxies("load");
|
|
542
|
+
let id = args.id.to_string();
|
|
543
|
+
let filtered = self
|
|
544
|
+
.load_id_filter
|
|
545
|
+
.as_ref()
|
|
546
|
+
.map(|re| !re.is_match(&id))
|
|
547
|
+
.unwrap_or(false);
|
|
548
|
+
let ctx_clone = ctx.inner.clone();
|
|
549
|
+
async move {
|
|
550
|
+
if !proxies || filtered { return Ok(None); }
|
|
551
|
+
let resp = self
|
|
552
|
+
.dispatch_with_ctx(HookRequestPayload::Load { id }, ctx_clone)
|
|
553
|
+
.await?;
|
|
554
|
+
resp.into_load_return()
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
fn resolve_id(
|
|
559
|
+
&self,
|
|
560
|
+
ctx: &PluginContext,
|
|
561
|
+
args: &HookResolveIdArgs<'_>,
|
|
562
|
+
) -> impl std::future::Future<Output = HookResolveIdReturn> + Send {
|
|
563
|
+
let proxies = self.proxies("resolveId");
|
|
564
|
+
let specifier = args.specifier.to_string();
|
|
565
|
+
let importer = args.importer.map(|s| s.to_string());
|
|
566
|
+
let is_entry = args.is_entry;
|
|
567
|
+
let filtered = self
|
|
568
|
+
.resolve_id_filter
|
|
569
|
+
.as_ref()
|
|
570
|
+
.map(|re| !re.is_match(&specifier))
|
|
571
|
+
.unwrap_or(false);
|
|
572
|
+
let ctx_clone = ctx.clone();
|
|
573
|
+
async move {
|
|
574
|
+
if !proxies || filtered { return Ok(None); }
|
|
575
|
+
let resp = self
|
|
576
|
+
.dispatch_with_ctx(
|
|
577
|
+
HookRequestPayload::ResolveId { specifier, importer, is_entry },
|
|
578
|
+
ctx_clone,
|
|
579
|
+
)
|
|
580
|
+
.await?;
|
|
581
|
+
resp.into_resolve_id_return()
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn transform(
|
|
586
|
+
&self,
|
|
587
|
+
ctx: SharedTransformPluginContext,
|
|
588
|
+
args: &HookTransformArgs<'_>,
|
|
589
|
+
) -> impl std::future::Future<Output = HookTransformReturn> + Send {
|
|
590
|
+
let proxies = self.proxies("transform");
|
|
591
|
+
let id = args.id.to_string();
|
|
592
|
+
let code_bytes = args.code.as_bytes().to_vec();
|
|
593
|
+
let module_type = format!("{:?}", args.module_type).to_lowercase();
|
|
594
|
+
let filtered = self
|
|
595
|
+
.transform_id_filter
|
|
596
|
+
.as_ref()
|
|
597
|
+
.map(|re| !re.is_match(&id))
|
|
598
|
+
.unwrap_or(false);
|
|
599
|
+
let ctx_clone = ctx.inner.clone();
|
|
600
|
+
async move {
|
|
601
|
+
if !proxies || filtered { return Ok(None); }
|
|
602
|
+
// Phase B.4 — the JS adapter fetches `code` bytes via
|
|
603
|
+
// `take_request_payload(reqId)`; the JSON envelope only
|
|
604
|
+
// carries metadata + the `payloadKind:'code'` marker.
|
|
605
|
+
// Response side: if JS stashed new code as response
|
|
606
|
+
// payload bytes (and set `hasCodeBytes: true` on the
|
|
607
|
+
// value), `dispatch_with_payload` returns them too.
|
|
608
|
+
let (resp, response_bytes) = self
|
|
609
|
+
.dispatch_with_payload(
|
|
610
|
+
HookRequestPayload::Transform {
|
|
611
|
+
id,
|
|
612
|
+
module_type,
|
|
613
|
+
payload_kind: "code",
|
|
614
|
+
},
|
|
615
|
+
ctx_clone,
|
|
616
|
+
code_bytes,
|
|
617
|
+
)
|
|
618
|
+
.await?;
|
|
619
|
+
resp.into_transform_return_with_bytes(response_bytes)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
fn render_chunk(
|
|
624
|
+
&self,
|
|
625
|
+
_ctx: &PluginContext,
|
|
626
|
+
args: &HookRenderChunkArgs<'_>,
|
|
627
|
+
) -> impl std::future::Future<Output = HookRenderChunkReturn> + Send {
|
|
628
|
+
let proxies = self.proxies("renderChunk");
|
|
629
|
+
let code = args.code.clone();
|
|
630
|
+
let file_name = args.chunk.filename.to_string();
|
|
631
|
+
let name = args.chunk.name.to_string();
|
|
632
|
+
let is_entry = args.chunk.is_entry;
|
|
633
|
+
async move {
|
|
634
|
+
if !proxies { return Ok(None); }
|
|
635
|
+
let resp = self
|
|
636
|
+
.dispatch(HookRequestPayload::RenderChunk { code, file_name, name, is_entry })
|
|
637
|
+
.await?;
|
|
638
|
+
resp.into_render_chunk_return()
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
fn banner(
|
|
643
|
+
&self,
|
|
644
|
+
_ctx: &PluginContext,
|
|
645
|
+
args: &HookAddonArgs,
|
|
646
|
+
) -> impl std::future::Future<Output = HookInjectionOutputReturn> + Send {
|
|
647
|
+
let proxies = self.proxies("banner");
|
|
648
|
+
let file_name = args.chunk.filename.to_string();
|
|
649
|
+
let name = args.chunk.name.to_string();
|
|
650
|
+
let is_entry = args.chunk.is_entry;
|
|
651
|
+
async move {
|
|
652
|
+
if !proxies { return Ok(None); }
|
|
653
|
+
let resp = self
|
|
654
|
+
.dispatch(HookRequestPayload::Banner { file_name, name, is_entry })
|
|
655
|
+
.await?;
|
|
656
|
+
resp.into_injection_return()
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
fn footer(
|
|
661
|
+
&self,
|
|
662
|
+
_ctx: &PluginContext,
|
|
663
|
+
args: &HookAddonArgs,
|
|
664
|
+
) -> impl std::future::Future<Output = HookInjectionOutputReturn> + Send {
|
|
665
|
+
let proxies = self.proxies("footer");
|
|
666
|
+
let file_name = args.chunk.filename.to_string();
|
|
667
|
+
let name = args.chunk.name.to_string();
|
|
668
|
+
let is_entry = args.chunk.is_entry;
|
|
669
|
+
async move {
|
|
670
|
+
if !proxies { return Ok(None); }
|
|
671
|
+
let resp = self
|
|
672
|
+
.dispatch(HookRequestPayload::Footer { file_name, name, is_entry })
|
|
673
|
+
.await?;
|
|
674
|
+
resp.into_injection_return()
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fn intro(
|
|
679
|
+
&self,
|
|
680
|
+
_ctx: &PluginContext,
|
|
681
|
+
args: &HookAddonArgs,
|
|
682
|
+
) -> impl std::future::Future<Output = HookInjectionOutputReturn> + Send {
|
|
683
|
+
let proxies = self.proxies("intro");
|
|
684
|
+
let file_name = args.chunk.filename.to_string();
|
|
685
|
+
let name = args.chunk.name.to_string();
|
|
686
|
+
let is_entry = args.chunk.is_entry;
|
|
687
|
+
async move {
|
|
688
|
+
if !proxies { return Ok(None); }
|
|
689
|
+
let resp = self
|
|
690
|
+
.dispatch(HookRequestPayload::Intro { file_name, name, is_entry })
|
|
691
|
+
.await?;
|
|
692
|
+
resp.into_injection_return()
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
fn outro(
|
|
697
|
+
&self,
|
|
698
|
+
_ctx: &PluginContext,
|
|
699
|
+
args: &HookAddonArgs,
|
|
700
|
+
) -> impl std::future::Future<Output = HookInjectionOutputReturn> + Send {
|
|
701
|
+
let proxies = self.proxies("outro");
|
|
702
|
+
let file_name = args.chunk.filename.to_string();
|
|
703
|
+
let name = args.chunk.name.to_string();
|
|
704
|
+
let is_entry = args.chunk.is_entry;
|
|
705
|
+
async move {
|
|
706
|
+
if !proxies { return Ok(None); }
|
|
707
|
+
let resp = self
|
|
708
|
+
.dispatch(HookRequestPayload::Outro { file_name, name, is_entry })
|
|
709
|
+
.await?;
|
|
710
|
+
resp.into_injection_return()
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
fn build_start(
|
|
715
|
+
&self,
|
|
716
|
+
_ctx: &PluginContext,
|
|
717
|
+
_args: &HookBuildStartArgs<'_>,
|
|
718
|
+
) -> impl std::future::Future<Output = HookNoopReturn> + Send {
|
|
719
|
+
let proxies = self.proxies("buildStart");
|
|
720
|
+
async move {
|
|
721
|
+
if !proxies { return Ok(()); }
|
|
722
|
+
let resp = self.dispatch(HookRequestPayload::BuildStart {}).await?;
|
|
723
|
+
resp.into_noop_return()
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
fn build_end(
|
|
728
|
+
&self,
|
|
729
|
+
_ctx: &PluginContext,
|
|
730
|
+
args: Option<&HookBuildEndArgs>,
|
|
731
|
+
) -> impl std::future::Future<Output = HookNoopReturn> + Send {
|
|
732
|
+
let proxies = self.proxies("buildEnd");
|
|
733
|
+
let error = args.map(|a| format!("{:?}", a.errors));
|
|
734
|
+
async move {
|
|
735
|
+
if !proxies { return Ok(()); }
|
|
736
|
+
let resp = self.dispatch(HookRequestPayload::BuildEnd { error }).await?;
|
|
737
|
+
resp.into_noop_return()
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
fn generate_bundle(
|
|
742
|
+
&self,
|
|
743
|
+
_ctx: &PluginContext,
|
|
744
|
+
_args: &mut HookGenerateBundleArgs<'_>,
|
|
745
|
+
) -> impl std::future::Future<Output = HookNoopReturn> + Send {
|
|
746
|
+
let proxies = self.proxies("generateBundle");
|
|
747
|
+
async move {
|
|
748
|
+
if !proxies { return Ok(()); }
|
|
749
|
+
let resp = self.dispatch(HookRequestPayload::GenerateBundle {}).await?;
|
|
750
|
+
resp.into_noop_return()
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
fn write_bundle(
|
|
755
|
+
&self,
|
|
756
|
+
_ctx: &PluginContext,
|
|
757
|
+
_args: &mut HookWriteBundleArgs,
|
|
758
|
+
) -> impl std::future::Future<Output = HookNoopReturn> + Send {
|
|
759
|
+
let proxies = self.proxies("writeBundle");
|
|
760
|
+
async move {
|
|
761
|
+
if !proxies { return Ok(()); }
|
|
762
|
+
let resp = self.dispatch(HookRequestPayload::WriteBundle {}).await?;
|
|
763
|
+
resp.into_noop_return()
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
fn close_bundle(
|
|
768
|
+
&self,
|
|
769
|
+
_ctx: &PluginContext,
|
|
770
|
+
_args: Option<&HookCloseBundleArgs>,
|
|
771
|
+
) -> impl std::future::Future<Output = HookNoopReturn> + Send {
|
|
772
|
+
let proxies = self.proxies("closeBundle");
|
|
773
|
+
async move {
|
|
774
|
+
if !proxies { return Ok(()); }
|
|
775
|
+
let resp = self.dispatch(HookRequestPayload::CloseBundle {}).await?;
|
|
776
|
+
resp.into_noop_return()
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|