@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
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
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
|
-
}
|
|
2
|
+
"name": "@gjsify/rolldown-native",
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "Phase D-2 POC: Vala/GObject + Rust cdylib bridge to rolldown for GJS. Wraps `rolldown::Bundler::generate()` (async, tokio) in a per-call current-thread runtime so JS sees a sync `bundle()` call. JSON-encoded BundlerOptions in, JSON-encoded BundleOutput out — the boundary stays small even though rolldown's option surface is enormous. POC scope: no JS plugins (Phase B), no watch/HMR, no incremental builds. Companion to `@gjsify/lightningcss-native`; together they remove the last two Rust-crate runtime blockers from the gjsify build pipeline (Phase D-3 unblock).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/esm/index.js",
|
|
7
|
+
"module": "lib/esm/index.js",
|
|
8
|
+
"types": "lib/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/types/index.d.ts",
|
|
12
|
+
"default": "./lib/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"prebuilds",
|
|
18
|
+
"meson.build",
|
|
19
|
+
"src/rust",
|
|
20
|
+
"src/vala"
|
|
21
|
+
],
|
|
22
|
+
"gjsify": {
|
|
23
|
+
"prebuilds": "prebuilds"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clear": "rm -rf lib build tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo || exit 0",
|
|
27
|
+
"check": "tsc --noEmit",
|
|
28
|
+
"init:meson": "meson setup build .",
|
|
29
|
+
"init:meson:wipe": "gjsify run init:meson --wipe",
|
|
30
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
31
|
+
"build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}'",
|
|
32
|
+
"build:meson": "gjsify run init:meson && meson compile -C build",
|
|
33
|
+
"build:types": "tsc",
|
|
34
|
+
"build:prebuilds": "gjsify run build:meson && mkdir -p prebuilds/linux-x86_64 && cp build/libgjsifyrolldown.so build/libgjsify_rolldown.so build/GjsifyRolldown-1.0.gir build/GjsifyRolldown-1.0.typelib prebuilds/linux-x86_64/"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"gjs",
|
|
38
|
+
"rolldown",
|
|
39
|
+
"bundler",
|
|
40
|
+
"vala",
|
|
41
|
+
"rust",
|
|
42
|
+
"native"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@girs/gjs": "4.0.0-rc.15",
|
|
46
|
+
"@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
|
|
47
|
+
"@girs/gobject-2.0": "2.88.0-4.0.0-rc.15"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@gjsify/cli": "^0.4.4",
|
|
51
|
+
"@types/node": "^25.6.2",
|
|
52
|
+
"typescript": "^6.0.3"
|
|
53
|
+
},
|
|
54
|
+
"author": "Pascal Garber <pascal@artandcode.studio>",
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "gjsify_rolldown"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
# NOTE on dependency resolution
|
|
11
|
+
# ─────────────────────────────
|
|
12
|
+
# rolldown 0.1.0 on crates.io ships with transitive workspace deps that
|
|
13
|
+
# don't compile cleanly together (rolldown_fs depends on a newer
|
|
14
|
+
# OxcResolverFileSystem trait surface than the published rolldown_resolver
|
|
15
|
+
# can satisfy — `error E0046: missing read, canonicalize`). Until that
|
|
16
|
+
# coherence problem is fixed upstream, we build against the in-tree
|
|
17
|
+
# refs/rolldown submodule via path deps. This means contributors need
|
|
18
|
+
# `git submodule update --init refs/rolldown` to build the cdylib from
|
|
19
|
+
# source — but downstream npm consumers only ever consume the prebuilt
|
|
20
|
+
# .so + .typelib in `prebuilds/`, so they never see the submodule.
|
|
21
|
+
[dependencies]
|
|
22
|
+
rolldown = { path = "../../../../../refs/rolldown/crates/rolldown", features = ["serde"] }
|
|
23
|
+
rolldown_common = { path = "../../../../../refs/rolldown/crates/rolldown_common", features = ["deserialize_bundler_options"] }
|
|
24
|
+
rolldown_plugin = { path = "../../../../../refs/rolldown/crates/rolldown_plugin" }
|
|
25
|
+
anyhow = "1"
|
|
26
|
+
async-trait = "0.1"
|
|
27
|
+
crossbeam-channel = "0.5"
|
|
28
|
+
libc = "0.2"
|
|
29
|
+
regex = "1"
|
|
30
|
+
serde = { version = "1", features = ["derive"] }
|
|
31
|
+
serde_json = "1"
|
|
32
|
+
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "time"] }
|
|
33
|
+
|
|
34
|
+
[profile.release]
|
|
35
|
+
lto = "thin"
|
|
36
|
+
codegen-units = 1
|
|
37
|
+
strip = true
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
//! gjsify_rolldown — single-call Rust→C FFI for the rolldown bundler.
|
|
2
|
+
//!
|
|
3
|
+
//! Phase D-2 POC. Mirrors the @gjsify/lightningcss-native pattern:
|
|
4
|
+
//! one `extern "C"` entry point that takes a JSON-encoded options blob
|
|
5
|
+
//! and returns either a JSON-encoded BundleOutput or a heap-allocated
|
|
6
|
+
//! error message. The C glue layer (src/vala/gjsify-rolldown-glue.c)
|
|
7
|
+
//! turns those into GBytes + GError; the Vala wrapper (src/vala/rolldown.vala)
|
|
8
|
+
//! exposes a GObject method.
|
|
9
|
+
//!
|
|
10
|
+
//! Why JSON for both directions?
|
|
11
|
+
//! - rolldown's BundlerOptions has dozens of nested types with custom
|
|
12
|
+
//! serde deserializers (browserslist, treeshake, code-splitting…);
|
|
13
|
+
//! a flat C struct hand-binding would be many hundreds of lines and
|
|
14
|
+
//! would drift each rolldown release. JSON keeps the boundary tiny.
|
|
15
|
+
//! - The output side has the same problem (warnings, source maps,
|
|
16
|
+
//! module dependency graph). Re-using rolldown's serde shape means
|
|
17
|
+
//! gjsify-side TS code can lean on the exact same JSON the
|
|
18
|
+
//! @rolldown/binding-wasm32-wasi loader emits for Node consumers.
|
|
19
|
+
//!
|
|
20
|
+
//! POC scope: no JS plugins, no watch mode, no incremental builds. Each
|
|
21
|
+
//! call constructs a fresh Bundler, runs `generate()` on a per-call
|
|
22
|
+
//! current-thread tokio runtime, and returns. This keeps the boundary
|
|
23
|
+
//! sync from the JS view (what GJS expects) at the cost of losing
|
|
24
|
+
//! parallel rolldown plugin workers — fine for the POC, addressed in
|
|
25
|
+
//! Phase B alongside JS plugin support.
|
|
26
|
+
|
|
27
|
+
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
|
28
|
+
|
|
29
|
+
use std::ffi::CString;
|
|
30
|
+
use std::os::raw::c_char;
|
|
31
|
+
use std::ptr;
|
|
32
|
+
|
|
33
|
+
use rolldown::Bundler;
|
|
34
|
+
use rolldown_common::BundlerOptions;
|
|
35
|
+
use serde::Serialize;
|
|
36
|
+
use tokio::runtime::Builder;
|
|
37
|
+
|
|
38
|
+
// Plugin-callback machinery (Phase B.1). The session module owns the
|
|
39
|
+
// stateful Bundler instance + tokio runtime + Vala-side eventfd
|
|
40
|
+
// pumps; plugin_proxy holds the JsPluginProxy that bridges
|
|
41
|
+
// rolldown's Pluginable trait to the channel.
|
|
42
|
+
pub mod plugin_proxy;
|
|
43
|
+
pub mod session;
|
|
44
|
+
|
|
45
|
+
/// Serializable mirror of the parts of `rolldown_common::Output` we expose to JS.
|
|
46
|
+
///
|
|
47
|
+
/// Both the variant tag (`Chunk`/`Asset` → `chunk`/`asset`) and the per-variant
|
|
48
|
+
/// field names are camelCased so JS sees `{type:"chunk", fileName:"…"}`.
|
|
49
|
+
#[derive(Serialize)]
|
|
50
|
+
#[serde(tag = "type", rename_all = "camelCase")]
|
|
51
|
+
pub enum OutputJson {
|
|
52
|
+
#[serde(rename_all = "camelCase")]
|
|
53
|
+
Chunk {
|
|
54
|
+
file_name: String,
|
|
55
|
+
name: String,
|
|
56
|
+
is_entry: bool,
|
|
57
|
+
is_dynamic_entry: bool,
|
|
58
|
+
code: String,
|
|
59
|
+
map: Option<String>,
|
|
60
|
+
sourcemap_filename: Option<String>,
|
|
61
|
+
imports: Vec<String>,
|
|
62
|
+
dynamic_imports: Vec<String>,
|
|
63
|
+
},
|
|
64
|
+
#[serde(rename_all = "camelCase")]
|
|
65
|
+
Asset {
|
|
66
|
+
file_name: String,
|
|
67
|
+
names: Vec<String>,
|
|
68
|
+
original_file_names: Vec<String>,
|
|
69
|
+
source_text: Option<String>,
|
|
70
|
+
source_bytes_len: usize,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[derive(Serialize)]
|
|
75
|
+
#[serde(rename_all = "camelCase")]
|
|
76
|
+
pub struct BundleOutputJson {
|
|
77
|
+
pub warnings: Vec<String>,
|
|
78
|
+
pub output: Vec<OutputJson>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Result of `gjsify_rolldown_bundle`.
|
|
82
|
+
///
|
|
83
|
+
/// On success: `output` points at a heap-allocated NUL-terminated UTF-8
|
|
84
|
+
/// JSON document of `BundleOutputJson`; `error` is NULL.
|
|
85
|
+
/// On error: `output` is NULL; `error` points at a heap-allocated
|
|
86
|
+
/// NUL-terminated UTF-8 message.
|
|
87
|
+
///
|
|
88
|
+
/// Caller MUST pass the returned struct to
|
|
89
|
+
/// `gjsify_rolldown_result_free` exactly once.
|
|
90
|
+
#[repr(C)]
|
|
91
|
+
pub struct GjsifyRolldownResult {
|
|
92
|
+
pub output: *mut c_char,
|
|
93
|
+
pub output_len: usize,
|
|
94
|
+
pub error: *mut c_char,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl GjsifyRolldownResult {
|
|
98
|
+
fn empty() -> Self {
|
|
99
|
+
Self { output: ptr::null_mut(), output_len: 0, error: ptr::null_mut() }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn err(msg: impl Into<String>) -> Self {
|
|
103
|
+
let mut r = Self::empty();
|
|
104
|
+
let cstr = CString::new(msg.into()).unwrap_or_else(|_| {
|
|
105
|
+
CString::new("rolldown: error message contained interior NUL").unwrap()
|
|
106
|
+
});
|
|
107
|
+
r.error = cstr.into_raw();
|
|
108
|
+
r
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn ok(json: String) -> Self {
|
|
112
|
+
let len = json.len();
|
|
113
|
+
let cstr = match CString::new(json) {
|
|
114
|
+
Ok(c) => c,
|
|
115
|
+
Err(_) => return Self::err("rolldown: output JSON contained interior NUL"),
|
|
116
|
+
};
|
|
117
|
+
Self { output: cstr.into_raw(), output_len: len, error: ptr::null_mut() }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn convert_output(out: rolldown_common::Output) -> OutputJson {
|
|
122
|
+
use rolldown_common::Output;
|
|
123
|
+
match out {
|
|
124
|
+
Output::Chunk(chunk) => {
|
|
125
|
+
let map_str = chunk.map.as_ref().map(|m| m.to_json_string());
|
|
126
|
+
OutputJson::Chunk {
|
|
127
|
+
file_name: chunk.filename.to_string(),
|
|
128
|
+
name: chunk.name.to_string(),
|
|
129
|
+
is_entry: chunk.is_entry,
|
|
130
|
+
is_dynamic_entry: chunk.is_dynamic_entry,
|
|
131
|
+
code: chunk.code.clone(),
|
|
132
|
+
map: map_str,
|
|
133
|
+
sourcemap_filename: chunk.sourcemap_filename.clone(),
|
|
134
|
+
imports: chunk.imports.iter().map(|s| s.to_string()).collect(),
|
|
135
|
+
dynamic_imports: chunk.dynamic_imports.iter().map(|s| s.to_string()).collect(),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
Output::Asset(asset) => {
|
|
139
|
+
let bytes = asset.source.as_bytes();
|
|
140
|
+
// Try to expose UTF-8 assets as text; fall back to byte-length only
|
|
141
|
+
// for binary assets (PNG etc). Binary content can be read via the
|
|
142
|
+
// emitted file when output.dir is set.
|
|
143
|
+
let source_text = std::str::from_utf8(bytes).ok().map(str::to_owned);
|
|
144
|
+
OutputJson::Asset {
|
|
145
|
+
file_name: asset.filename.to_string(),
|
|
146
|
+
names: asset.names.clone(),
|
|
147
|
+
original_file_names: asset.original_file_names.clone(),
|
|
148
|
+
source_text,
|
|
149
|
+
source_bytes_len: bytes.len(),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Bundle a JS project. `options_json` is a NUL-terminated UTF-8 JSON
|
|
156
|
+
/// document matching rolldown's `BundlerOptions` serde shape (camelCase).
|
|
157
|
+
///
|
|
158
|
+
/// Memory: the input pointer is borrowed; the returned struct owns its
|
|
159
|
+
/// allocations and must be released via `gjsify_rolldown_result_free`.
|
|
160
|
+
#[unsafe(no_mangle)]
|
|
161
|
+
pub extern "C" fn gjsify_rolldown_bundle(
|
|
162
|
+
options_json: *const c_char,
|
|
163
|
+
options_json_len: usize,
|
|
164
|
+
) -> GjsifyRolldownResult {
|
|
165
|
+
if options_json.is_null() || options_json_len == 0 {
|
|
166
|
+
return GjsifyRolldownResult::err("rolldown: empty options JSON");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let json_slice = unsafe { std::slice::from_raw_parts(options_json as *const u8, options_json_len) };
|
|
170
|
+
let json_str = match std::str::from_utf8(json_slice) {
|
|
171
|
+
Ok(s) => s,
|
|
172
|
+
Err(_) => return GjsifyRolldownResult::err("rolldown: invalid UTF-8 in options JSON"),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let opts: BundlerOptions = match serde_json::from_str(json_str) {
|
|
176
|
+
Ok(o) => o,
|
|
177
|
+
Err(e) => return GjsifyRolldownResult::err(format!("rolldown: options parse: {e}")),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Per-call current-thread tokio runtime: lets GJS exit cleanly, no
|
|
181
|
+
// background workers leak after bundle() returns. Phase B can move
|
|
182
|
+
// to a long-lived runtime for plugin-callback support.
|
|
183
|
+
let rt = match Builder::new_current_thread().enable_all().build() {
|
|
184
|
+
Ok(r) => r,
|
|
185
|
+
Err(e) => return GjsifyRolldownResult::err(format!("rolldown: tokio init: {e}")),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
let bundle_result = rt.block_on(async {
|
|
189
|
+
let mut bundler = Bundler::new(opts).map_err(|e| format!("Bundler::new: {e:?}"))?;
|
|
190
|
+
bundler.generate().await.map_err(|e| format!("Bundler::generate: {e:?}"))
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let bundle = match bundle_result {
|
|
194
|
+
Ok(b) => b,
|
|
195
|
+
Err(msg) => return GjsifyRolldownResult::err(format!("rolldown: {msg}")),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let warnings: Vec<String> = bundle.warnings.iter().map(|w| format!("{w:?}")).collect();
|
|
199
|
+
let output: Vec<OutputJson> = bundle
|
|
200
|
+
.assets
|
|
201
|
+
.into_iter()
|
|
202
|
+
.map(convert_output)
|
|
203
|
+
.collect();
|
|
204
|
+
|
|
205
|
+
let json = match serde_json::to_string(&BundleOutputJson { warnings, output }) {
|
|
206
|
+
Ok(s) => s,
|
|
207
|
+
Err(e) => return GjsifyRolldownResult::err(format!("rolldown: serialize: {e}")),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
GjsifyRolldownResult::ok(json)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Free the buffers owned by a `GjsifyRolldownResult`. Safe to call on
|
|
214
|
+
/// either an OK result (frees `output`) or an error result (frees
|
|
215
|
+
/// `error`). Must NOT be called twice on the same struct.
|
|
216
|
+
#[unsafe(no_mangle)]
|
|
217
|
+
pub extern "C" fn gjsify_rolldown_result_free(result: GjsifyRolldownResult) {
|
|
218
|
+
if !result.output.is_null() {
|
|
219
|
+
unsafe { drop(CString::from_raw(result.output)) };
|
|
220
|
+
}
|
|
221
|
+
if !result.error.is_null() {
|
|
222
|
+
unsafe { drop(CString::from_raw(result.error)) };
|
|
223
|
+
}
|
|
224
|
+
}
|