@gjsify/oxfmt-native 0.4.44

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.
@@ -0,0 +1,274 @@
1
+ //! gjsify_oxfmt — Rust→C FFI for oxc's formatter (the `oxfmt` app crate).
2
+ //!
3
+ //! Reference: refs/oxc/apps/oxfmt/src/main.rs (oxc-project/oxc, MIT) — the
4
+ //! pure-Rust (non-napi) oxfmt CLI entry point. Reimplemented for GJS as two
5
+ //! FFI surfaces:
6
+ //!
7
+ //! * `gjsify_oxfmt_run(argv, argc) -> exit_code` — the full oxfmt CLI
8
+ //! in-process: bpaf arg parsing, `.oxfmtrc(.json)` + `.editorconfig`
9
+ //! resolution, ignore handling, parallel file walking (rayon),
10
+ //! `--write` / `--check` / `--list-different` modes, stdout/stderr
11
+ //! reporting. The same engine as the npm `oxfmt` launcher for
12
+ //! JS/TS/JSX (+ JSON/TOML), minus the Node-hosted pieces: the
13
+ //! `ExternalFormatter` (CSS/HTML/Vue/Markdown via Prettier-NAPI
14
+ //! callbacks), JS/TS config files (`.oxfmtrc.ts`), `--init`/`--migrate`
15
+ //! and the LSP — those all require a Node host by design (napi feature).
16
+ //!
17
+ //! * `gjsify_oxfmt_format(opts) -> GjsifyResult` — single-shot in-memory
18
+ //! format of one source string via `oxc_formatter::format` with default
19
+ //! (Prettier-compatible) options. No config resolution — callers that
20
+ //! need `.oxfmtrc` semantics use `gjsify_oxfmt_run`.
21
+ //!
22
+ //! The result struct owns its buffers until `gjsify_oxfmt_result_free()` is
23
+ //! called.
24
+
25
+ #![allow(clippy::not_unsafe_ptr_arg_deref)]
26
+
27
+ use std::ffi::{CStr, CString, OsString};
28
+ use std::os::raw::c_char;
29
+ use std::panic::{catch_unwind, AssertUnwindSafe};
30
+ use std::ptr;
31
+ use std::sync::Once;
32
+
33
+ use oxc_allocator::Allocator;
34
+ use oxc_formatter::JsFormatOptions;
35
+ use oxc_span::SourceType;
36
+ use oxfmt::cli::{format_command, init_miette, init_rayon, init_tracing, WalkRunner};
37
+
38
+ /// Input to `gjsify_oxfmt_format`.
39
+ #[repr(C)]
40
+ pub struct GjsifyFormatOpts {
41
+ /// Logical filename — its extension selects JS/TS/JSX. NULL → `input.ts`.
42
+ pub filename: *const c_char,
43
+ pub code: *const u8,
44
+ pub code_len: usize,
45
+ }
46
+
47
+ /// Result of a format. On success `error` is NULL and `code` holds the
48
+ /// formatted source; on failure `code` is NULL and `error` is a
49
+ /// NUL-terminated UTF-8 message. All non-NULL pointers were allocated on the
50
+ /// Rust side and MUST be freed via `gjsify_oxfmt_result_free` — never libc free.
51
+ #[repr(C)]
52
+ pub struct GjsifyResult {
53
+ pub code: *mut u8,
54
+ pub code_len: usize,
55
+ pub code_cap: usize,
56
+ pub error: *mut c_char,
57
+ }
58
+
59
+ impl GjsifyResult {
60
+ fn ok(s: String) -> Self {
61
+ let mut v = s.into_bytes();
62
+ let code = v.as_mut_ptr();
63
+ let code_len = v.len();
64
+ let code_cap = v.capacity();
65
+ std::mem::forget(v);
66
+ GjsifyResult {
67
+ code,
68
+ code_len,
69
+ code_cap,
70
+ error: ptr::null_mut(),
71
+ }
72
+ }
73
+
74
+ fn err(msg: impl Into<String>) -> Self {
75
+ let cstr = CString::new(msg.into()).unwrap_or_else(|_| {
76
+ CString::new("oxfmt: error message contained interior NUL byte").unwrap()
77
+ });
78
+ GjsifyResult {
79
+ code: ptr::null_mut(),
80
+ code_len: 0,
81
+ code_cap: 0,
82
+ error: cstr.into_raw(),
83
+ }
84
+ }
85
+ }
86
+
87
+ fn cstr_to_str<'a>(p: *const c_char) -> Option<&'a str> {
88
+ if p.is_null() {
89
+ None
90
+ } else {
91
+ unsafe { CStr::from_ptr(p) }.to_str().ok()
92
+ }
93
+ }
94
+
95
+ /// Core: parse + format `source` (typed from `filename`'s extension, TS by
96
+ /// default) via `oxc_formatter::format` with default options. Pure Rust,
97
+ /// no FFI — unit-tested directly.
98
+ fn format_impl(source: &str, filename: &str) -> Result<String, String> {
99
+ let source_type = SourceType::from_path(filename).unwrap_or_else(|_| SourceType::ts());
100
+ let allocator = Allocator::new();
101
+ let formatted = oxc_formatter::format(
102
+ &allocator,
103
+ source,
104
+ source_type,
105
+ JsFormatOptions::default(),
106
+ None,
107
+ )
108
+ .map_err(|e| format!("oxfmt: parse error in {filename}: {e}"))?;
109
+ formatted
110
+ .print()
111
+ .map(|printed| printed.into_code())
112
+ .map_err(|e| format!("oxfmt: print failed: {e:?}"))
113
+ }
114
+
115
+ /// One-shot format. Caller MUST pass the result to
116
+ /// `gjsify_oxfmt_result_free` exactly once to release buffers.
117
+ #[no_mangle]
118
+ pub extern "C" fn gjsify_oxfmt_format(opts: GjsifyFormatOpts) -> GjsifyResult {
119
+ if opts.code.is_null() {
120
+ return GjsifyResult::err("oxfmt: NULL input code");
121
+ }
122
+ let source = {
123
+ let slice = unsafe { std::slice::from_raw_parts(opts.code, opts.code_len) };
124
+ match std::str::from_utf8(slice) {
125
+ Ok(s) => s,
126
+ Err(_) => return GjsifyResult::err("oxfmt: invalid UTF-8 in input"),
127
+ }
128
+ };
129
+ let filename = cstr_to_str(opts.filename).unwrap_or("input.ts");
130
+ match format_impl(source, filename) {
131
+ Ok(code) => GjsifyResult::ok(code),
132
+ Err(e) => GjsifyResult::err(e),
133
+ }
134
+ }
135
+
136
+ /// Free a result returned by `gjsify_oxfmt_format`. Call exactly once.
137
+ #[no_mangle]
138
+ pub extern "C" fn gjsify_oxfmt_result_free(result: GjsifyResult) {
139
+ if !result.code.is_null() {
140
+ unsafe {
141
+ drop(Vec::from_raw_parts(
142
+ result.code,
143
+ result.code_len,
144
+ result.code_cap,
145
+ ))
146
+ };
147
+ }
148
+ if !result.error.is_null() {
149
+ unsafe { drop(CString::from_raw(result.error)) };
150
+ }
151
+ }
152
+
153
+ /// Process-global one-time init (tracing, miette hook, rayon pool). Mirrors
154
+ /// main.rs — these all panic or error when initialized twice, and a CLI
155
+ /// process only ever needs them once. The rayon thread count is taken from
156
+ /// the FIRST run's `--threads` option (subsequent in-process runs reuse the
157
+ /// pool — matches the one-run-per-process CLI model).
158
+ static INIT: Once = Once::new();
159
+
160
+ /// Run the oxfmt CLI in-process: `argv`/`argc` are the CLI arguments
161
+ /// (WITHOUT the program name — i.e. `process.argv.slice(2)` shaped, same as
162
+ /// the napi `run_cli`). Prints diagnostics/reports to stdout/stderr exactly
163
+ /// like the `oxfmt` binary. Returns the process exit code
164
+ /// (0 = success, 1 = config error/`--check` mismatch, 2 = no files/failed).
165
+ #[no_mangle]
166
+ pub extern "C" fn gjsify_oxfmt_run(argv: *const *const c_char, argc: usize) -> i32 {
167
+ // Never unwind across the FFI boundary — a panic would abort the host
168
+ // (GJS) process. Surface it as a non-zero exit code instead.
169
+ match catch_unwind(AssertUnwindSafe(|| run_impl(argv, argc))) {
170
+ Ok(code) => code,
171
+ Err(_) => {
172
+ eprintln!("oxfmt: internal panic during run (this is a bug in gjsify_oxfmt)");
173
+ 101
174
+ }
175
+ }
176
+ }
177
+
178
+ fn run_impl(argv: *const *const c_char, argc: usize) -> i32 {
179
+ let mut args: Vec<OsString> = Vec::with_capacity(argc);
180
+ if argc > 0 {
181
+ if argv.is_null() {
182
+ eprintln!("oxfmt: NULL argv with non-zero argc");
183
+ return 2;
184
+ }
185
+ for i in 0..argc {
186
+ let p = unsafe { *argv.add(i) };
187
+ match cstr_to_str(p) {
188
+ Some(s) => args.push(OsString::from(s)),
189
+ None => {
190
+ eprintln!("oxfmt: argument {i} is NULL or not valid UTF-8");
191
+ return 2;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // Use `run_inner()` to report errors instead of panicking (same as the
198
+ // napi `run_cli`). bpaf returns exit code 0 for --help/--version and
199
+ // non-0 for actual parse errors.
200
+ let command = match format_command().run_inner(&*args) {
201
+ Ok(cmd) => cmd,
202
+ Err(e) => {
203
+ e.print_message(100);
204
+ return i32::from(e.exit_code() != 0);
205
+ }
206
+ };
207
+
208
+ INIT.call_once(|| {
209
+ init_tracing();
210
+ init_miette();
211
+ init_rayon(command.runtime_options.threads);
212
+ });
213
+
214
+ i32::from(WalkRunner::new(command).run().exit_code())
215
+ }
216
+
217
+ #[cfg(test)]
218
+ mod tests {
219
+ use std::ffi::CString;
220
+ use std::os::raw::c_char;
221
+
222
+ use super::format_impl;
223
+
224
+ #[test]
225
+ fn formats_typescript() {
226
+ let out = format_impl("const x:number=1", "f.ts").unwrap();
227
+ // oxc_formatter is Prettier-compatible: collapse whitespace, add the
228
+ // type-annotation spacing + a trailing newline.
229
+ assert!(out.contains("const x"), "expected reflow, got: {out:?}");
230
+ assert!(
231
+ out.ends_with('\n'),
232
+ "expected trailing newline, got: {out:?}"
233
+ );
234
+ }
235
+
236
+ #[test]
237
+ fn formats_tsx() {
238
+ let out = format_impl("const el=<div className=\"x\">hi</div>;", "f.tsx").unwrap();
239
+ assert!(out.contains("<div"), "got: {out:?}");
240
+ }
241
+
242
+ fn run(args: &[&str]) -> i32 {
243
+ let cstrings: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
244
+ let ptrs: Vec<*const c_char> = cstrings.iter().map(|c| c.as_ptr()).collect();
245
+ super::gjsify_oxfmt_run(ptrs.as_ptr(), ptrs.len())
246
+ }
247
+
248
+ #[test]
249
+ fn run_rejects_unknown_flags() {
250
+ assert_eq!(run(&["--definitely-not-a-real-flag"]), 1);
251
+ }
252
+
253
+ #[test]
254
+ fn run_help_exits_zero() {
255
+ assert_eq!(run(&["--help"]), 0);
256
+ }
257
+
258
+ #[test]
259
+ fn run_formats_a_file_in_place() {
260
+ let dir =
261
+ std::env::temp_dir().join(format!("gjsify-oxfmt-run-test-{}", std::process::id()));
262
+ std::fs::create_dir_all(&dir).unwrap();
263
+ let file = dir.join("f.ts");
264
+ std::fs::write(&file, "const x:number=1").unwrap();
265
+ let code = run(&["--write", file.to_str().unwrap()]);
266
+ let out = std::fs::read_to_string(&file).unwrap();
267
+ std::fs::remove_dir_all(&dir).ok();
268
+ assert_eq!(code, 0, "expected exit 0, got {code}");
269
+ assert!(
270
+ out.contains("const x: number = 1;"),
271
+ "expected formatted output, got: {out:?}"
272
+ );
273
+ }
274
+ }
@@ -0,0 +1,63 @@
1
+ /*
2
+ * GLib-friendly glue for gjsify_oxfmt. See gjsify-oxfmt-glue.h.
3
+ */
4
+
5
+ #include "gjsify-oxfmt-glue.h"
6
+
7
+ GQuark
8
+ gjsify_oxfmt_error_quark (void)
9
+ {
10
+ return g_quark_from_static_string ("gjsify-oxfmt-error-quark");
11
+ }
12
+
13
+ GBytes *
14
+ gjsify_oxfmt_glue_format (const char *filename,
15
+ GBytes *code,
16
+ GError **error)
17
+ {
18
+ if (code == NULL)
19
+ {
20
+ g_set_error_literal (error,
21
+ GJSIFY_OXFMT_ERROR,
22
+ GJSIFY_OXFMT_ERROR_FAILED,
23
+ "oxfmt: NULL input bytes");
24
+ return NULL;
25
+ }
26
+
27
+ gsize code_len = 0;
28
+ const guint8 *code_data = g_bytes_get_data (code, &code_len);
29
+
30
+ GjsifyFormatOpts opts;
31
+ opts.filename = filename;
32
+ opts.code = code_data;
33
+ opts.code_len = code_len;
34
+
35
+ GjsifyResult res = gjsify_oxfmt_format (opts);
36
+
37
+ if (res.error != NULL)
38
+ {
39
+ g_set_error_literal (error,
40
+ GJSIFY_OXFMT_ERROR,
41
+ GJSIFY_OXFMT_ERROR_FAILED,
42
+ res.error);
43
+ gjsify_oxfmt_result_free (res);
44
+ return NULL;
45
+ }
46
+
47
+ /* Copy the Rust-allocated buffer into GBytes (GLib heap), then free the
48
+ * Rust result — keeps the ownership boundary clean; formatted source is
49
+ * typically small. */
50
+ GBytes *out = g_bytes_new (res.code, res.code_len);
51
+ gjsify_oxfmt_result_free (res);
52
+ return out;
53
+ }
54
+
55
+ int
56
+ gjsify_oxfmt_glue_run (char **args,
57
+ int n_args)
58
+ {
59
+ if (n_args < 0)
60
+ n_args = 0;
61
+
62
+ return gjsify_oxfmt_run ((const char * const *) args, (size_t) n_args);
63
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * GLib-friendly wrapper around the Rust format shim.
3
+ *
4
+ * The Rust side returns its own struct with malloc'd buffers. This glue
5
+ * layer copies the formatted code into GBytes (so GLib's refcount controls
6
+ * lifetime — friendly to SpiderMonkey GC) and frees the Rust result
7
+ * immediately. Errors become GError on the GJSIFY_OXFMT quark.
8
+ */
9
+
10
+ #ifndef GJSIFY_OXFMT_GLUE_H
11
+ #define GJSIFY_OXFMT_GLUE_H
12
+
13
+ #include <glib.h>
14
+ #include <stdbool.h>
15
+ #include "gjsify-oxfmt.h"
16
+
17
+ G_BEGIN_DECLS
18
+
19
+ #define GJSIFY_OXFMT_ERROR (gjsify_oxfmt_error_quark ())
20
+
21
+ typedef enum {
22
+ GJSIFY_OXFMT_ERROR_FAILED = 0,
23
+ } GjsifyOxfmtError;
24
+
25
+ GQuark gjsify_oxfmt_error_quark (void);
26
+
27
+ /**
28
+ * gjsify_oxfmt_glue_format:
29
+ * @filename: logical filename — its extension selects JS/TS/JSX (NULL OK)
30
+ * @code: input source bytes (UTF-8)
31
+ * @error: (out)(optional): GError on failure
32
+ *
33
+ * Returns: (transfer full): formatted source as GBytes, or NULL on error.
34
+ */
35
+ GBytes *gjsify_oxfmt_glue_format (const char *filename,
36
+ GBytes *code,
37
+ GError **error);
38
+
39
+ /**
40
+ * gjsify_oxfmt_glue_run:
41
+ * @args: (array length=n_args): CLI arguments (without the program name)
42
+ * @n_args: number of arguments
43
+ *
44
+ * Runs the oxfmt CLI in-process (config resolution, file walking,
45
+ * write/check/list-different). Prints to stdout/stderr like the `oxfmt`
46
+ * binary.
47
+ *
48
+ * Returns: the process exit code (0 success, 1 warning, 2 fatal).
49
+ */
50
+ int gjsify_oxfmt_glue_run (char **args,
51
+ int n_args);
52
+
53
+ G_END_DECLS
54
+
55
+ #endif /* GJSIFY_OXFMT_GLUE_H */
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Hand-written C header matching the Rust extern "C" surface of the
3
+ * gjsify_oxfmt cdylib (src/rust/src/lib.rs).
4
+ *
5
+ * Vala includes this via [CCode (cheader_filename = "gjsify-oxfmt.h")] to
6
+ * call into the Rust shim. The surface is tiny (one format call + a free),
7
+ * so a hand-maintained header is simpler than pulling cbindgen into meson.
8
+ */
9
+
10
+ #ifndef GJSIFY_OXFMT_H
11
+ #define GJSIFY_OXFMT_H
12
+
13
+ #include <glib.h>
14
+ #include <stdbool.h>
15
+ #include <stddef.h>
16
+ #include <stdint.h>
17
+
18
+ G_BEGIN_DECLS
19
+
20
+ typedef struct {
21
+ const char *filename; /* extension selects JS/TS/JSX; NULL → "input.ts" */
22
+ const uint8_t *code;
23
+ size_t code_len;
24
+ } GjsifyFormatOpts;
25
+
26
+ typedef struct {
27
+ uint8_t *code; /* formatted source; NULL on error */
28
+ size_t code_len;
29
+ size_t code_cap;
30
+ char *error; /* NUL-terminated message; NULL on success */
31
+ } GjsifyResult;
32
+
33
+ /* One-shot format. The returned struct owns its buffers — pass it to
34
+ * gjsify_oxfmt_result_free() exactly once when done. */
35
+ GjsifyResult gjsify_oxfmt_format (GjsifyFormatOpts opts);
36
+
37
+ void gjsify_oxfmt_result_free (GjsifyResult result);
38
+
39
+ /* Run the oxfmt CLI in-process. argv = the CLI arguments WITHOUT the
40
+ * program name (process.argv.slice(2) shaped). Prints to stdout/stderr
41
+ * like the oxfmt binary; returns the process exit code. */
42
+ int gjsify_oxfmt_run (const char * const *argv, size_t argc);
43
+
44
+ G_END_DECLS
45
+
46
+ #endif /* GJSIFY_OXFMT_H */
@@ -0,0 +1,79 @@
1
+ /*
2
+ * GjsifyOxfmt — Vala wrapper around the Rust oxc-formatter cdylib.
3
+ *
4
+ * The real formatting pipeline lives in src/rust/ (compiled by meson via
5
+ * cargo to libgjsify_oxfmt.so) and a tiny C glue file
6
+ * (gjsify-oxfmt-glue.{h,c}) translates the malloc'd Rust struct into
7
+ * GBytes + GError, which GJS handles natively.
8
+ *
9
+ * This Vala layer exposes the glue function as a GObject method that emits
10
+ * to GIR/typelib, so JS can do:
11
+ *
12
+ * import GjsifyOxfmt from "gi://GjsifyOxfmt?version=1.0";
13
+ * const fmt = new GjsifyOxfmt.Formatter();
14
+ * const out = fmt.format("file.ts", input); // GLib.Bytes
15
+ *
16
+ * Pattern: same as @gjsify/lightningcss-native — Rust/C ownership stays in
17
+ * native land via GLib.Bytes; SpiderMonkey only sees refcounted handles.
18
+ */
19
+
20
+ namespace GjsifyOxfmt {
21
+
22
+ [CCode (cname = "gjsify_oxfmt_glue_format",
23
+ cheader_filename = "gjsify-oxfmt-glue.h")]
24
+ private extern GLib.Bytes? _glue_format (string? filename,
25
+ GLib.Bytes code) throws GLib.Error;
26
+
27
+ [CCode (cname = "gjsify_oxfmt_glue_run",
28
+ cheader_filename = "gjsify-oxfmt-glue.h")]
29
+ private extern int _glue_run (string[] args);
30
+
31
+ /**
32
+ * Formatter — stateless one-shot oxc formatter.
33
+ *
34
+ * Each call parses the source on the Rust side (typed from the
35
+ * filename's extension, TypeScript by default) and prints it back
36
+ * through oxc_formatter (Prettier-compatible). Only the native
37
+ * JS/TS/JSX path is wrapped — embedded CSS/HTML/Markdown formatting
38
+ * (oxfmt's Prettier-NAPI ExternalFormatter) is out of scope.
39
+ */
40
+ public class Formatter : GLib.Object {
41
+
42
+ /**
43
+ * format:
44
+ * @filename: logical filename; its extension selects JS/TS/JSX
45
+ * (may be null → treated as TypeScript)
46
+ * @code: input source bytes (UTF-8)
47
+ *
48
+ * Returns: (transfer full): formatted source as GLib.Bytes.
49
+ * Throws GjsifyOxfmtError.FAILED on a fatal parse/print error.
50
+ */
51
+ public GLib.Bytes format (string? filename,
52
+ GLib.Bytes code) throws GLib.Error {
53
+ var bytes = _glue_format (filename, code);
54
+ if (bytes == null)
55
+ throw new GLib.Error (GLib.Quark.from_string ("gjsify-oxfmt-error-quark"),
56
+ 0, "oxfmt: unknown error (NULL result without GError)");
57
+ return bytes;
58
+ }
59
+
60
+ /**
61
+ * run:
62
+ * @args: CLI arguments WITHOUT the program name
63
+ * (`process.argv.slice(2)` shaped) — e.g.
64
+ * `["--check", "--config", "/abs/.oxfmtrc.json", "src"]`
65
+ *
66
+ * Runs the full oxfmt CLI in-process: `.oxfmtrc(.json)` +
67
+ * `.editorconfig` resolution, ignore handling, parallel file
68
+ * walking, `--write` / `--check` / `--list-different` modes.
69
+ * Reports to stdout/stderr exactly like the `oxfmt` binary.
70
+ * Working directory = the current process working directory.
71
+ *
72
+ * Returns: the process exit code (0 = success, 1 = config error or
73
+ * `--check` mismatch, 2 = no files found / format failed).
74
+ */
75
+ public int run (string[] args) {
76
+ return _glue_run (args);
77
+ }
78
+ }
79
+ }