oxc 0.0.1 → 0.2.0

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +602 -0
  4. data/ext/oxc/extconf.rb +123 -0
  5. data/ext/oxc/include/oxc.h +40 -0
  6. data/ext/oxc/oxc.c +136 -0
  7. data/lib/oxc/backend.rb +41 -0
  8. data/lib/oxc/diagnosed.rb +33 -0
  9. data/lib/oxc/diagnostic.rb +86 -0
  10. data/lib/oxc/errors.rb +26 -0
  11. data/lib/oxc/minifier.rb +31 -0
  12. data/lib/oxc/minify_result.rb +25 -0
  13. data/lib/oxc/mutation_visitor.rb +128 -0
  14. data/lib/oxc/node.rb +186 -0
  15. data/lib/oxc/options.rb +113 -0
  16. data/lib/oxc/parse_result.rb +116 -0
  17. data/lib/oxc/result.rb +51 -0
  18. data/lib/oxc/transform_result.rb +47 -0
  19. data/lib/oxc/transformer.rb +31 -0
  20. data/lib/oxc/version.rb +1 -1
  21. data/lib/oxc/visitor.rb +25 -0
  22. data/lib/oxc.rb +49 -0
  23. data/licenses/README.md +12 -0
  24. data/licenses/oxc-MIT.txt +22 -0
  25. data/licenses/oxc-THIRD-PARTY.txt +763 -0
  26. data/oxc.gemspec +14 -2
  27. data/rust/Cargo.lock +1436 -0
  28. data/rust/Cargo.toml +32 -0
  29. data/rust/build.rs +52 -0
  30. data/rust/cbindgen.toml +24 -0
  31. data/rust/rustfmt.toml +3 -0
  32. data/rust/src/diagnostic.rs +75 -0
  33. data/rust/src/lib.rs +288 -0
  34. data/rust/src/module_record.rs +262 -0
  35. data/rust/src/options.rs +744 -0
  36. data/rust/src/parse.rs +93 -0
  37. data/rust/src/result.rs +55 -0
  38. data/rust/src/source_type.rs +26 -0
  39. data/rust/src/symbols.rs +101 -0
  40. data/rust/src/transform.rs +116 -0
  41. data/sig/oxc/backend.rbs +29 -0
  42. data/sig/oxc/diagnosed.rbs +23 -0
  43. data/sig/oxc/diagnostic.rbs +57 -0
  44. data/sig/oxc/errors.rbs +31 -0
  45. data/sig/oxc/minifier.rbs +21 -0
  46. data/sig/oxc/minify_result.rbs +11 -0
  47. data/sig/oxc/mutation_visitor.rbs +80 -0
  48. data/sig/oxc/node.rbs +94 -0
  49. data/sig/oxc/options.rbs +42 -0
  50. data/sig/oxc/parse_result.rbs +66 -0
  51. data/sig/oxc/result.rbs +32 -0
  52. data/sig/oxc/transform_result.rbs +22 -0
  53. data/sig/oxc/transformer.rbs +21 -0
  54. data/sig/oxc/types.rbs +96 -0
  55. data/sig/oxc/version.rbs +5 -0
  56. data/sig/oxc/visitor.rbs +11 -0
  57. data/sig/oxc.rbs +13 -2
  58. metadata +56 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f51f7c162aaba8c9c93db46fea55375e9470361e515a14b766d83cc4b5f65e14
4
- data.tar.gz: 9c24ffc7ec11336752abfa4e3c65cb128ca0c2dcdd4c660821079b8862b606cc
3
+ metadata.gz: fc92a71cfc1ef7eb85205dadf8ee6af6b2f2d640ba4daf26d1fbd42a7f00c338
4
+ data.tar.gz: 4d809332931716e027fe7c10b1025cb53014e71b0776ff4254747406b27cbd68
5
5
  SHA512:
6
- metadata.gz: 623b310be4738aa34be58fc9352b588874b36ee0ccf34f083a081244df59130e6d7e4a6f176ae17d3f176234b91f5b883e4afd2c7435a1cc0d6a5749d07656d1
7
- data.tar.gz: 0fa21e6e9f2eac40277922b534d0f93e422e9b977014543009d92d136ee6a640cb1ae6d70c2ccb522d964161b59b913a0a584850335d9173e31454cc67fb6b01
6
+ metadata.gz: 890ad9e1ed71d762bec40e2dc091716a59db07901397dfe17fb747bf4783cee8b9aab40833e8fc1c5b846d9c2772e49b709a2008064287c286e73705f8a744f0
7
+ data.tar.gz: b0630720f739ca928444efd248004f0430bc30d403aa846668a9ec64e2b9b960ee53673ac88193bdbc3eaaae86f72db0a691c4bd5929aedfb15969f904368de5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Marco Roth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,602 @@
1
+ <h2 align="center">⚓ Oxc for Ruby</h2>
2
+
3
+ <h4 align="center">A collection of high-performance tools for JavaScript and TypeScript written in Rust.</h4>
4
+
5
+ <div align="center">Ruby bindings for <a href="https://oxc.rs">Oxc</a>, the JavaScript Oxidation Compiler.</div><br/>
6
+
7
+ <p align="center">
8
+ <a href="https://rubygems.org/gems/oxc"><img alt="Gem Version" src="https://img.shields.io/gem/v/oxc"></a>
9
+ <a href="https://oxc.rs"><img alt="Documentation" src="https://img.shields.io/badge/oxc.rs-documentation-green"></a>
10
+ <a href="https://github.com/marcoroth/oxc-ruby/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/github/license/marcoroth/oxc-ruby"></a>
11
+ <a href="https://github.com/marcoroth/oxc-ruby/issues"><img alt="Issues" src="https://img.shields.io/github/issues/marcoroth/oxc-ruby"></a>
12
+ </p>
13
+
14
+ <br/>
15
+
16
+ ### What is Oxc for Ruby?
17
+
18
+ Ruby bindings for [Oxc](https://oxc.rs), a collection of high-performance tools for JavaScript and TypeScript written in Rust. Parse, transform and minify JavaScript from Ruby, without the need for a JavaScript runtime.
19
+
20
+ Everything here is Oxc doing the work. For what the options mean and what it can do, [oxc.rs](https://oxc.rs) is the reference.
21
+
22
+ ### Installation
23
+
24
+ ```bash
25
+ bundle add oxc
26
+ ```
27
+
28
+ Anywhere a precompiled gem is not published, the gem builds from source and needs the [Rust toolchain](https://rustup.rs) at 1.96 or newer.
29
+
30
+ ### Usage
31
+
32
+ #### Minifying
33
+
34
+ ```ruby
35
+ Oxc.minify("const x = 1; console.log(x)").code
36
+ #=> "console.log(1);"
37
+ ```
38
+
39
+ Compressing and mangling are both on. Either can be switched off, or given settings of its own.
40
+
41
+ ```ruby
42
+ Oxc.minify(source, compress: false).code
43
+ Oxc.minify(source, mangle: { top_level: false, reserved: ["exports"] }).code
44
+ Oxc.minify(source, compress: { drop_console: true, drop_debugger: false }).code
45
+ ```
46
+
47
+ #### Transforming
48
+
49
+ `transform` compiles TypeScript and JSX away, and lowers what a browser you support cannot read. It leaves the output readable unless it is asked for otherwise.
50
+
51
+ ```ruby
52
+ Oxc.transform("const x: number = 1; console.log(x)", filename: "app.ts").code
53
+ #=> "const x = 1;\nconsole.log(x);\n"
54
+
55
+ Oxc.transform("const f = (a) => a ** 2; foo(f)", target: "es2015").code
56
+ #=> "const f = (a) => Math.pow(a, 2);\nfoo(f);\n"
57
+
58
+ Oxc.transform(source, filename: "app.jsx", source_type: "module").code
59
+ #=> "import { jsx as _jsx } from \"react/jsx-runtime\";\n..."
60
+ ```
61
+
62
+ Minifying in the same call reads the source once instead of twice. The minifier lowers to the same target, so it never undoes the lowering the transform just did.
63
+
64
+ ```ruby
65
+ Oxc.transform(source, filename: "app.ts", target: "es2020", minify: true).code
66
+ ```
67
+
68
+ `define` replaces a name wherever it appears, and whatever that makes unreachable is dropped with it. `inject` adds an import for a name the source used without importing.
69
+
70
+ ```ruby
71
+ Oxc.transform("if (DEBUG) { log() }", define: { "DEBUG" => "false" }).code
72
+ #=> ""
73
+
74
+ Oxc.transform("foo(process)", inject: { "process" => "node:process" }, source_type: "module").code
75
+ #=> "import process from \"node:process\";\nfoo(process);\n"
76
+ ```
77
+
78
+ #### Declaration files
79
+
80
+ Asking for a declaration writes the `.d.ts` beside the code, from the types it just stripped.
81
+
82
+ ```ruby
83
+ result = Oxc.transform(source, filename: "add.ts", source_type: "module", typescript: { declaration: true })
84
+
85
+ result.code #=> "export const add = (a, b) => a + b;\n"
86
+ result.declaration #=> "export declare const add: (a: number, b: number) => number;\n"
87
+ ```
88
+
89
+ `declaration_map` comes with it when `sourcemap: true` is set.
90
+
91
+ #### Decorators
92
+
93
+ ```ruby
94
+ Oxc.transform(source, filename: "a.ts", decorator: { legacy: true, emit_decorator_metadata: true }).code
95
+ ```
96
+
97
+ `legacy` is the version of decorators TypeScript shipped before the standard, matching `experimentalDecorators`.
98
+
99
+ #### Runtime helpers
100
+
101
+ Lowering sometimes needs a helper function, and by default oxc imports it from the `@oxc-project/runtime` npm package. In an application with no npm packages that import resolves to nothing, so `helpers_used` says what a transform reached for.
102
+
103
+ ```ruby
104
+ result = Oxc.transform(source, target: "es2015")
105
+
106
+ result.helpers_used
107
+ #=> {"classPrivateFieldGet2" => "@oxc-project/runtime/helpers/classPrivateFieldGet2"}
108
+ ```
109
+
110
+ Leaving `target` unset asks for no lowering, and needs no helpers. The other way out is `external`, which reads the helpers off a global `babelHelpers` object you provide.
111
+
112
+ ```ruby
113
+ Oxc.transform(source, target: "es2015", helpers: { mode: "external" }).code
114
+ ```
115
+
116
+ An assumption can remove the need for a helper altogether. Telling oxc that public class fields shadow nothing lets it assign them directly, and the helper import goes away.
117
+
118
+ ```ruby
119
+ Oxc.transform("class A { x = 1 }", target: "es2015").helpers_used
120
+ #=> {"defineProperty" => "@oxc-project/runtime/helpers/defineProperty"}
121
+
122
+ Oxc.transform("class A { x = 1 }", target: "es2015", assumptions: { set_public_class_fields: true }).helpers_used
123
+ #=> {}
124
+ ```
125
+
126
+ The assumptions are `ignore_function_length`, `no_document_all`, `object_rest_no_symbols`, `pure_getters` and `set_public_class_fields`. oxc says so when one of them is not implemented for the transform it would apply to.
127
+
128
+ #### Reading TypeScript
129
+
130
+ The grammar comes from the filename, and `lang` says so where the filename cannot.
131
+
132
+ ```ruby
133
+ Oxc.minify(source, filename: "app.ts").code
134
+ Oxc.minify(source, lang: "tsx").code
135
+ ```
136
+
137
+ #### Source maps
138
+
139
+ `map` is the source map as JSON text, so a caller who only writes it out never pays to parse it.
140
+
141
+ ```ruby
142
+ result = Oxc.minify(source, filename: "app.js", sourcemap: true)
143
+
144
+ result.code
145
+ JSON.parse(result.map)
146
+ ```
147
+
148
+ #### Keeping the output readable
149
+
150
+ ```ruby
151
+ Oxc.minify("const x = 1; foo(x)", codegen: { remove_whitespace: false }).code
152
+ #=> "foo(1);\n"
153
+ ```
154
+
155
+ #### Legal comments
156
+
157
+ A legal comment is one carrying `@license` or `@preserve`, or starting with `//!` or `/*!`. They can stay inline, move to the end, or come back separately.
158
+
159
+ ```ruby
160
+ result = Oxc.minify("/*! (c) me */ foo()", codegen: { legal_comments: "external" })
161
+
162
+ result.code #=> "foo();"
163
+ result.legal_comments #=> ["/*! (c) me */"]
164
+ ```
165
+
166
+ #### Parsing
167
+
168
+ `parse` answers the [ESTree](https://github.com/estree/estree) AST oxc read, as plain Ruby hashes and arrays.
169
+
170
+ ```ruby
171
+ program = Oxc.parse("let a = 1").program
172
+
173
+ program["type"] #=> "Program"
174
+ program["body"].first["kind"] #=> "let"
175
+ program["body"].first["declarations"].first["id"]["name"] #=> "a"
176
+ ```
177
+
178
+ Parsing never raises for source it could not read. The parser recovers, so what it could not read comes back in `errors`, and `panicked?` says whether it gave up. `validate!` raises on demand.
179
+
180
+ ```ruby
181
+ parsed = Oxc.parse("const x = ;")
182
+
183
+ parsed.errors.map(&:message) #=> ["Unexpected token"]
184
+ parsed.panicked? #=> true
185
+ parsed.validate! #=> raises Oxc::SyntaxError
186
+ ```
187
+
188
+ The AST is by far the largest thing crossing the boundary, so `ast: false` skips building it. Use it when only the diagnostics matter.
189
+
190
+ ```ruby
191
+ Oxc.parse(source, ast: false).errors?
192
+ ```
193
+
194
+ Comments come back beside the AST, and a hashbang reads as the line comment it looks like.
195
+
196
+ ```ruby
197
+ Oxc.parse("// hi\nfoo() /* there */").comments.map { |comment| [comment.type, comment.value] }
198
+ #=> [["Line", " hi"], ["Block", " there "]]
199
+ ```
200
+
201
+ A few more knobs: `ranges: true` adds a `range` pair to every node, `preserve_parens: false` drops the `ParenthesizedExpression` wrappers, `ast_type: "js"` leaves the TypeScript properties off a TypeScript AST, and `semantic_errors: true` reports what only semantic analysis can see.
202
+
203
+ ```ruby
204
+ Oxc.parse("let a; let a;", semantic_errors: true).errors.map(&:message)
205
+ #=> ["Identifier `a` has already been declared"]
206
+ ```
207
+
208
+ #### Walking the AST
209
+
210
+ `root` answers the program as an `Oxc::Node`, which walks, reads its fields by name, and knows what it sits inside.
211
+
212
+ ```ruby
213
+ root = Oxc.parse(source).root
214
+
215
+ root.type #=> "Program"
216
+ root.keys #=> the ESTree fields this node carries
217
+ root.fields #=> those fields and their values, without the span
218
+ root.child_nodes #=> the nodes directly under it
219
+ root.every("Identifier") #=> every identifier in the file
220
+ root.at(offset) #=> the innermost node covering a byte offset
221
+ root.each #=> an Enumerator over every node
222
+ ```
223
+
224
+ Inspecting a node shows every field it carries, so there is always something to reach for next.
225
+
226
+ ```
227
+ #<Oxc::Node VariableDeclaration range=[0, 13] kind="let" declarations=[... 1 item]>
228
+ #<Oxc::Node VariableDeclarator range=[4, 13] id=#<Oxc::Node Identifier> init=#<Oxc::Node Literal>>
229
+ #<Oxc::Node Identifier range=[4, 9] name="count">
230
+ ```
231
+
232
+ Every field is there, so what `inspect` prints and what `keys` answers never disagree. A field holding a node prints as that node's type, one holding a list prints how many it holds, and one holding nothing prints the `nil`, the `false` or the `[]` it holds.
233
+
234
+ An ESTree field always wins over a method of the gem's own, since `name`, `attributes` and `children` are all real fields. `Identifier#name` is the identifier's name, `JSXElement#children` is what the element wraps, and `ImportDeclaration#attributes` is the import's `with` clause. The walker spells its own versions `underscored_type`, `to_h` and `child_nodes`, which no ESTree field can be called.
235
+
236
+ A field comes back as a node when it holds one, so reads chain.
237
+
238
+ ```ruby
239
+ declaration = root.child_nodes.first
240
+
241
+ declaration.kind
242
+ #=> "let"
243
+
244
+ declaration.declarations.first.id.name
245
+ #=> "count"
246
+ ```
247
+
248
+ ESTree names its fields in camelCase, and a field answers to its snake_case name too, so reading an AST does not mean writing JavaScript casing in Ruby.
249
+
250
+ ```ruby
251
+ node.type_annotation # the same field as node.typeAnnotation
252
+ node.super_class # superClass
253
+ root.source_type # sourceType
254
+ ```
255
+
256
+ Patterns take either name as well, binding what you asked for.
257
+
258
+ ```ruby
259
+ node => { type_annotation: { type: }, readonly: }
260
+ ```
261
+
262
+ `ancestors` is what a rewrite needs, since a reference sits inside the expression that has to be replaced.
263
+
264
+ ```ruby
265
+ reference = root.at(source.index("count +="))
266
+ reference.ancestors.find { |node| node.type == "AssignmentExpression" }.slice
267
+ #=> "count += 1"
268
+ ```
269
+
270
+ A parse result keeps the source it read and hands it down to every node it builds, so `slice` answers with no argument at all.
271
+
272
+ ```ruby
273
+ parsed = Oxc.parse(source)
274
+
275
+ parsed.source
276
+ #=> "let count = 0\nfunction bump() { count += 1; render(count) }"
277
+
278
+ parsed.root.every("FunctionDeclaration").first.slice
279
+ #=> "function bump() { count += 1; render(count) }"
280
+ ```
281
+
282
+ It still takes one, for a node assembled by hand or read against a different string.
283
+
284
+ ```ruby
285
+ node.slice(other_source)
286
+ ```
287
+
288
+ Nodes pattern match, and nest, since a field holding a node comes back as one.
289
+
290
+ ```ruby
291
+ node => { type: "VariableDeclarator", id: { name: }, init: { value: } }
292
+ name #=> "count"
293
+ value #=> 0
294
+
295
+ root.select { |node| node in { type: "FunctionDeclaration", id: { name: /^handle/ } } }
296
+ ```
297
+
298
+ `deconstruct_keys` is the whole protocol here. There is no `deconstruct`, since `each` yields every descendant and an array pattern over direct children would disagree with `to_a`.
299
+
300
+ `to_h` and `to_json` answer the ESTree the node wraps, which is what a snapshot test or a dump to another tool wants.
301
+
302
+ ```ruby
303
+ node.to_h
304
+ #=> { "type" => "Identifier", "name" => "count", "start" => 4, "end" => 9 }
305
+
306
+ node.to_json
307
+ #=> "{\"type\":\"Identifier\",\"name\":\"count\",\"start\":4,\"end\":9}"
308
+ ```
309
+
310
+ `Oxc::Visitor` answers a node with the method named after its type, and walks through anything nothing answers.
311
+
312
+ ```ruby
313
+ class Reads < Oxc::Visitor
314
+ def visit_assignment_expression(node)
315
+ puts "#{node.left["name"]} #{node.operator}"
316
+
317
+ visit_children(node)
318
+ end
319
+ end
320
+
321
+ Reads.new.visit(Oxc.parse(source))
322
+ ```
323
+
324
+ It takes a parse result or a node, so the common case needs no `root`. A result with no AST is nothing to walk and visits nothing.
325
+
326
+ There is one node class, not one per type, so a type the gem has never seen still walks and still answers. The types and their fields are [ESTree](https://github.com/estree/estree). For the TypeScript and JSX nodes, which ESTree does not cover, oxc publishes the exact shapes it emits as [`@oxc-project/types`](https://www.npmjs.com/package/@oxc-project/types).
327
+
328
+ #### Rewriting
329
+
330
+ `Oxc::MutationVisitor` records what to do to a node and splices the original text at the end, so everything it did not touch survives byte for byte, comments and indentation included.
331
+
332
+ ```ruby
333
+ class Renamer < Oxc::MutationVisitor
334
+ def visit_identifier(node)
335
+ replace(node, "renamed") if node["name"] == "count"
336
+ end
337
+ end
338
+
339
+ Renamer.new.rewrite("let count = 1 // keep me")
340
+ #=> "let renamed = 1 // keep me"
341
+ ```
342
+
343
+ `replace`, `remove`, `insert_before`, `insert_after` and `wrap` are the operations, and each takes a node. Spans are exact, so removing `debugger;` removes what the node covered and leaves the newline after it alone.
344
+
345
+ Walking into a node that was replaced would edit text that is no longer there, so it stops. Two edits over the same span raise `Oxc::MutationVisitor::Overlap` instead of quietly producing something broken.
346
+
347
+ What goes in is text, so a node can become anything, including several statements or nothing at all. Nothing checks it on the way, so the result is read back afterwards and refused if it stopped being JavaScript.
348
+
349
+ ```ruby
350
+ Breaker.new.rewrite("foo(data)")
351
+ #=> Oxc::MutationVisitor::Invalid: what was rewritten no longer reads as JavaScript: Unexpected token
352
+ ```
353
+
354
+ Pass `verify: false` for a fragment that was never going to parse on its own.
355
+
356
+ `parsed` reaches what the source parsed to, so a rewrite can ask for symbols and drive from them. That is the difference between rewriting a name and rewriting the right one.
357
+
358
+ ```ruby
359
+ class ToState < Oxc::MutationVisitor
360
+ def rewrite(source) = super(source, symbols: true)
361
+
362
+ def visit_identifier(node)
363
+ reference = parsed.symbols.fetch("declared").flat_map { |symbol| symbol["references"] }
364
+ .find { |found| found["start"] == node.start }
365
+
366
+ replace(node, %(state.get("#{node["name"]}"))) if reference && !reference["write"]
367
+ end
368
+ end
369
+ ```
370
+
371
+ Drive from references, not from every `Identifier`. That is what keeps a declaration, a shadowed local, and a same-named property out of the rewrite.
372
+
373
+ #### What a file declared, and what it only used
374
+
375
+ `symbols: true` answers every binding with the span it was declared at and the spans of every reference to it, plus the names the file used without declaring.
376
+
377
+ ```ruby
378
+ symbols = Oxc.parse(source, symbols: true).symbols
379
+
380
+ symbols["declared"]
381
+ #=> [{"name" => "count", "root" => true, "declaration" => {...}, "references" => [{...}]}]
382
+
383
+ symbols["unresolved"]
384
+ #=> [{"name" => "fetch", "references" => [{...}]}]
385
+ ```
386
+
387
+ Every reference says whether it read the name, wrote it, or both, which comes from oxc's scope analysis and not from the shape of the tree.
388
+
389
+ ```ruby
390
+ # let count = 0; function bump() { count += 1; render(count) }; count = 5
391
+ references.map { |reference| [reference["read"], reference["write"]] }
392
+ #=> [[true, true], [true, false], [false, true]]
393
+ ```
394
+
395
+ `root` says whether the file declared it at the top level. Every span counts in UTF-8 bytes, so a rewrite can splice the source directly with `String#byteslice`, which is how the JavaScript ecosystem edits code without reprinting it.
396
+
397
+ #### What a file imports and exports
398
+
399
+ `module_record: true` answers the module's imports and exports without walking the AST for them.
400
+
401
+ ```ruby
402
+ record = Oxc.parse(source, source_type: "module", module_record: true).module_record
403
+
404
+ record["has_module_syntax"]
405
+ record["static_imports"].map { |import| import["module_request"]["value"] }
406
+ #=> ["./a", "./b"]
407
+
408
+ record["static_exports"]
409
+ record["dynamic_imports"]
410
+ record["import_metas"]
411
+ ```
412
+
413
+ Every entry carries the span it was written at, so it maps back onto the source. An import entry says whether it was a TypeScript `import type`, and an export says which module it came from.
414
+
415
+ #### Reusing options
416
+
417
+ `Oxc::Transformer` and `Oxc::Minifier` each hold a set of options to use across many files. Options given to a call are merged over the ones the object was built with, so the ones that belong to the project are written once and the ones that belong to a single file travel with it.
418
+
419
+ ```ruby
420
+ transformer = Oxc::Transformer.new(target: "es2020", jsx: { runtime: "automatic" })
421
+
422
+ transformer.transform(source, filename: "app.tsx").code
423
+ transformer.with(minify: true).transform(source, filename: "app.ts").code
424
+
425
+ minifier = Oxc::Minifier.new(compress: { drop_console: true })
426
+
427
+ minifier.minify(source).code
428
+ ```
429
+
430
+ Both answer `call` as well, so either can be handed to anything expecting something callable.
431
+
432
+ ```ruby
433
+ minifier.call(source).to_s
434
+ ```
435
+
436
+ They are separate objects because they read separate options. `minify` reads `compress` and `mangle`, while `transform` reads `target`, `jsx` and the rest, which is the same split upstream draws between the `oxc-minify` and `oxc-transform` packages.
437
+
438
+ ### Options
439
+
440
+ | Option | Type | Description |
441
+ |---------------|-----------------|----------------------------------------------------------------------------------|
442
+ | `filename` | `String` | The name to use in diagnostics, in the source map, and to read the grammar from. |
443
+ | `lang` | `String` | `js`, `jsx`, `ts`, `tsx` or `dts`, when the filename does not say. |
444
+ | `source_type` | `String` | `script`, `module`, `commonjs` or `unambiguous`. |
445
+ | `compress` | `bool`, `Hash` | Whether to compress, and how. |
446
+ | `mangle` | `bool`, `Hash` | Whether to rename what nothing outside can see, and how. |
447
+ | `codegen` | `bool`, `Hash` | How to print the result. |
448
+ | `sourcemap` | `bool` | Whether to answer a source map alongside the code. |
449
+ | `strict` | `bool` | Whether to raise on any diagnostic. Off, only unusable output raises. |
450
+
451
+ `parse` reads these instead:
452
+
453
+ | Option | Type | Description |
454
+ |-------------------|----------|----------------------------------------------------------------------------|
455
+ | `ast` | `bool` | Whether to build the AST at all. On by default. |
456
+ | `ast_type` | `String` | `js` or `ts`, to include or leave out the TypeScript properties. |
457
+ | `ranges` | `bool` | Whether every node carries a `range` pair. |
458
+ | `preserve_parens` | `bool` | Whether parentheses become `ParenthesizedExpression` nodes. On by default. |
459
+ | `comments` | `bool` | Whether to collect the comments. On by default. |
460
+ | `semantic_errors` | `bool` | Whether to also report what semantic analysis finds. |
461
+
462
+ `transform` reads these instead of `compress` and `mangle`:
463
+
464
+ | Option | Type | Description |
465
+ |--------------|-------------------|--------------------------------------------------------------------|
466
+ | `target` | `String`, `Array` | The ECMAScript version or browsers to lower for, such as `es2015`. |
467
+ | `jsx` | `bool`, `Hash` | Whether to compile JSX, and how. `false` leaves it as written. |
468
+ | `typescript` | `Hash` | How to compile TypeScript. |
469
+ | `helpers` | `Hash` | Where the runtime helpers come from, `runtime` or `external`. |
470
+ | `define` | `Hash` | Names to replace wherever they appear. |
471
+ | `inject` | `Hash` | Names to import where the source used them without importing. |
472
+ | `minify` | `bool`, `Hash` | Whether to minify in the same pass, and how. |
473
+ | `cwd` | `String` | What relative paths in other options are relative to. |
474
+
475
+ An option nobody reads is refused, and so is one inside a nested hash:
476
+
477
+ ```ruby
478
+ Oxc.minify(source, nonsense: true)
479
+ #=> Oxc::OptionError: Unknown option: nonsense
480
+
481
+ Oxc.minify(source, compress: { nonsense: true })
482
+ #=> Oxc::OptionError: Invalid options: unknown field `nonsense`, expected one of `target`, ...
483
+ ```
484
+
485
+ ### Results
486
+
487
+ Each call answers its own result, so no result carries a field the call that produced it can never fill.
488
+
489
+ * `Oxc.minify` answers an `Oxc::MinifyResult`
490
+ * `Oxc.transform` answers an `Oxc::TransformResult`
491
+ * `Oxc.parse` answers an `Oxc::ParseResult`
492
+
493
+ `Oxc::MinifyResult` and `Oxc::TransformResult` are both an `Oxc::Result`, so anything reading `code` or `to_s` takes either one. `Oxc::ParseResult` stands on its own, because a parse answers a tree and has no code to print.
494
+
495
+ #### What every result answers
496
+
497
+ ```ruby
498
+ result = Oxc.minify("const x = 1; console.log(x)")
499
+
500
+ result.diagnostics #=> everything oxc had to say
501
+ result.errors #=> the error-severity half of it
502
+ result.warnings #=> the warning-severity half
503
+ result.errors?
504
+ result.warnings?
505
+ result.panicked? #=> whether oxc gave up on the source
506
+ result.validate! #=> itself, or raises Oxc::SyntaxError
507
+ ```
508
+
509
+ `validate!` means something slightly different for each. `Oxc::MinifyResult` and `Oxc::TransformResult` raise only when there is nothing usable to answer with, and `strict: true` widens that to any error at all. `Oxc::ParseResult` raises on any error, because a parse routinely answers a usable tree alongside them.
510
+
511
+ #### Minify and transform
512
+
513
+ ```ruby
514
+ result = Oxc.minify("const x = 1; console.log(x)")
515
+
516
+ result.code #=> "console.log(1);"
517
+ result.to_s #=> "console.log(1);"
518
+ result.map #=> nil, or the source map as JSON text
519
+ result.legal_comments #=> []
520
+ ```
521
+
522
+ A transform adds what only a transform can answer.
523
+
524
+ ```ruby
525
+ result = Oxc.transform(source, filename: "app.ts", sourcemap: true, typescript: { declaration: true })
526
+
527
+ result.declaration #=> the .d.ts it wrote
528
+ result.declaration_map #=> its source map, as JSON text
529
+ result.helpers_used #=> the runtime helpers its lowering reached for
530
+ ```
531
+
532
+ #### Parse
533
+
534
+ ```ruby
535
+ parsed = Oxc.parse(source, source_type: "module", module_record: true)
536
+
537
+ parsed.program #=> the ESTree AST, or nil when ast: false
538
+ parsed.module_record #=> the imports and exports, when asked for
539
+ parsed.symbols #=> the bindings and their references, when asked for
540
+ parsed.comments #=> Array[Oxc::Comment]
541
+ ```
542
+
543
+ ### Diagnostics
544
+
545
+ oxc's parser recovers, so source it could not fully read still produces a result, and what it could not read comes back as diagnostics. When a call does raise, `Oxc::SyntaxError` carries the result it came from, whichever of the three that was.
546
+
547
+ ```ruby
548
+ begin
549
+ Oxc.minify("const x = ;", filename: "broken.js")
550
+ rescue Oxc::SyntaxError => e
551
+ e.message #=> "Unexpected token"
552
+ e.diagnostics.first.codeframe #=> the frame below
553
+ e.result.panicked? #=> true
554
+ end
555
+ ```
556
+
557
+ ```
558
+ x Unexpected token
559
+ ,-[broken.js:1:11]
560
+ 1 | const x = ;
561
+ : ^
562
+ `----
563
+ ```
564
+
565
+ A diagnostic's labels count in **UTF-8 bytes**, which is what oxc counts in and what Ruby slices by:
566
+
567
+ ```ruby
568
+ label = e.diagnostics.first.labels.first
569
+
570
+ label.start #=> 10
571
+ label.finish #=> 11
572
+ label.slice(source) #=> ";"
573
+ ```
574
+
575
+ ### Development
576
+
577
+ The gem is a C extension over a Rust crate. `rust/` builds a static library and generates the C header with [cbindgen](https://github.com/mozilla/cbindgen), `ext/oxc/` wraps it, and `lib/` is the Ruby API over that.
578
+
579
+ ```bash
580
+ bin/setup
581
+ bundle exec rake
582
+ ```
583
+
584
+ `sig/` is generated from the `#:` annotations next to the code. Regenerate it with `rake rbs` after changing a signature, and CI checks that it matches.
585
+
586
+ ### Acknowledgements
587
+
588
+ [Oxc](https://oxc.rs) is maintained at [oxc-project/oxc](https://github.com/oxc-project/oxc) and is part of [VoidZero](https://voidzero.dev)'s toolchain for JavaScript. This gem only calls into it. Every parser, transformer and minifier feature comes from there.
589
+
590
+ Thank you to all of them.
591
+
592
+ ### Contributing
593
+
594
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/oxc-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/marcoroth/oxc-ruby/blob/main/CODE_OF_CONDUCT.md).
595
+
596
+ Issues with parsing, transforming or minifying itself belong [upstream](https://github.com/oxc-project/oxc/issues), since this gem does none of that. Issues with the Ruby API, the build, or the bindings belong here.
597
+
598
+ ### License
599
+
600
+ The Ruby, C, and Rust code in this gem is available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
601
+
602
+ It builds against [Oxc](https://github.com/oxc-project/oxc), which is MIT licensed and carries some Apache-2.0 code of its own. A copy of both travels with the gem in [`licenses/`](licenses) so that whoever received it has the terms in hand.