@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 CHANGED
@@ -1,56 +1,56 @@
1
1
  {
2
- "name": "@gjsify/rolldown-native",
3
- "version": "0.4.0",
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": "yarn init:meson --wipe",
30
- "build": "yarn build:gjsify && yarn build:types",
31
- "build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}'",
32
- "build:meson": "yarn init:meson && meson compile -C build",
33
- "build:types": "tsc",
34
- "build:prebuilds": "yarn 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.0",
51
- "@types/node": "^25.6.2",
52
- "typescript": "^6.0.3"
53
- },
54
- "author": "Pascal Garber <pascal@artandcode.studio>",
55
- "license": "MIT"
56
- }
2
+ "name": "@gjsify/rolldown-native",
3
+ "version": "0.4.3",
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": "workspace:^",
51
+ "@types/node": "^25.6.2",
52
+ "typescript": "^6.0.3"
53
+ },
54
+ "author": "Pascal Garber <pascal@artandcode.studio>",
55
+ "license": "MIT"
56
+ }
File without changes
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
+ }