oxc 0.1.0-aarch64-linux-gnu
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +437 -0
- data/ext/oxc/extconf.rb +123 -0
- data/ext/oxc/include/oxc.h +40 -0
- data/ext/oxc/oxc.c +136 -0
- data/lib/oxc/3.2/oxc.so +0 -0
- data/lib/oxc/3.3/oxc.so +0 -0
- data/lib/oxc/3.4/oxc.so +0 -0
- data/lib/oxc/4.0/oxc.so +0 -0
- data/lib/oxc/backend.rb +41 -0
- data/lib/oxc/diagnosed.rb +33 -0
- data/lib/oxc/diagnostic.rb +86 -0
- data/lib/oxc/errors.rb +26 -0
- data/lib/oxc/minifier.rb +31 -0
- data/lib/oxc/minify_result.rb +25 -0
- data/lib/oxc/options.rb +113 -0
- data/lib/oxc/parse_result.rb +106 -0
- data/lib/oxc/result.rb +51 -0
- data/lib/oxc/transform_result.rb +47 -0
- data/lib/oxc/transformer.rb +31 -0
- data/lib/oxc/version.rb +5 -0
- data/lib/oxc.rb +52 -0
- data/licenses/README.md +12 -0
- data/licenses/oxc-MIT.txt +22 -0
- data/licenses/oxc-THIRD-PARTY.txt +763 -0
- data/oxc.gemspec +43 -0
- data/rust/Cargo.lock +1436 -0
- data/rust/Cargo.toml +32 -0
- data/rust/build.rs +52 -0
- data/rust/cbindgen.toml +24 -0
- data/rust/rustfmt.toml +3 -0
- data/rust/src/diagnostic.rs +75 -0
- data/rust/src/lib.rs +288 -0
- data/rust/src/module_record.rs +262 -0
- data/rust/src/options.rs +744 -0
- data/rust/src/parse.rs +93 -0
- data/rust/src/result.rs +55 -0
- data/rust/src/source_type.rs +26 -0
- data/rust/src/symbols.rs +101 -0
- data/rust/src/transform.rs +116 -0
- data/sig/oxc/backend.rbs +29 -0
- data/sig/oxc/diagnosed.rbs +23 -0
- data/sig/oxc/diagnostic.rbs +57 -0
- data/sig/oxc/errors.rbs +31 -0
- data/sig/oxc/minifier.rbs +21 -0
- data/sig/oxc/minify_result.rbs +11 -0
- data/sig/oxc/options.rbs +42 -0
- data/sig/oxc/parse_result.rbs +61 -0
- data/sig/oxc/result.rbs +32 -0
- data/sig/oxc/transform_result.rbs +22 -0
- data/sig/oxc/transformer.rbs +21 -0
- data/sig/oxc/types.rbs +96 -0
- data/sig/oxc/version.rbs +5 -0
- data/sig/oxc.rbs +15 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9fa077d820131502c696fa997503249eab68ff17c9b5141fcf21bb58960a18f3
|
|
4
|
+
data.tar.gz: '0943773966316220daaa10d8a6c4db27fe904cd4ef298f0199d3d50d97995e16'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 412b3a718be49dede29e4c333029fc817234afec37edf7eda372eb172c833871e0b69bea2a783f4c70f3f793ed81e079094306363f25f1fd5261efa168faf22f
|
|
7
|
+
data.tar.gz: a5eab5ac59da90a729a5ec53dd64f295aeb01d840f959631d8881732d2efb4a289f65016a42ecc5b4118989b0c2208454091c63fca4267103f57b6eac2454530
|
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,437 @@
|
|
|
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
|
+
#### What a file declared, and what it only used
|
|
209
|
+
|
|
210
|
+
`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.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
symbols = Oxc.parse(source, symbols: true).symbols
|
|
214
|
+
|
|
215
|
+
symbols["declared"]
|
|
216
|
+
#=> [{"name" => "count", "root" => true, "declaration" => {...}, "references" => [{...}]}]
|
|
217
|
+
|
|
218
|
+
symbols["unresolved"]
|
|
219
|
+
#=> [{"name" => "fetch", "references" => [{...}]}]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# let count = 0; function bump() { count += 1; render(count) }; count = 5
|
|
226
|
+
references.map { |reference| [reference["read"], reference["write"]] }
|
|
227
|
+
#=> [[true, true], [true, false], [false, true]]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`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.
|
|
231
|
+
|
|
232
|
+
#### What a file imports and exports
|
|
233
|
+
|
|
234
|
+
`module_record: true` answers the module's imports and exports without walking the AST for them.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
record = Oxc.parse(source, source_type: "module", module_record: true).module_record
|
|
238
|
+
|
|
239
|
+
record["has_module_syntax"]
|
|
240
|
+
record["static_imports"].map { |import| import["module_request"]["value"] }
|
|
241
|
+
#=> ["./a", "./b"]
|
|
242
|
+
|
|
243
|
+
record["static_exports"]
|
|
244
|
+
record["dynamic_imports"]
|
|
245
|
+
record["import_metas"]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
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.
|
|
249
|
+
|
|
250
|
+
#### Reusing options
|
|
251
|
+
|
|
252
|
+
`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.
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
transformer = Oxc::Transformer.new(target: "es2020", jsx: { runtime: "automatic" })
|
|
256
|
+
|
|
257
|
+
transformer.transform(source, filename: "app.tsx").code
|
|
258
|
+
transformer.with(minify: true).transform(source, filename: "app.ts").code
|
|
259
|
+
|
|
260
|
+
minifier = Oxc::Minifier.new(compress: { drop_console: true })
|
|
261
|
+
|
|
262
|
+
minifier.minify(source).code
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Both answer `call` as well, so either can be handed to anything expecting something callable.
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
minifier.call(source).to_s
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
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.
|
|
272
|
+
|
|
273
|
+
### Options
|
|
274
|
+
|
|
275
|
+
| Option | Type | Description |
|
|
276
|
+
|---------------|-----------------|----------------------------------------------------------------------------------|
|
|
277
|
+
| `filename` | `String` | The name to use in diagnostics, in the source map, and to read the grammar from. |
|
|
278
|
+
| `lang` | `String` | `js`, `jsx`, `ts`, `tsx` or `dts`, when the filename does not say. |
|
|
279
|
+
| `source_type` | `String` | `script`, `module`, `commonjs` or `unambiguous`. |
|
|
280
|
+
| `compress` | `bool`, `Hash` | Whether to compress, and how. |
|
|
281
|
+
| `mangle` | `bool`, `Hash` | Whether to rename what nothing outside can see, and how. |
|
|
282
|
+
| `codegen` | `bool`, `Hash` | How to print the result. |
|
|
283
|
+
| `sourcemap` | `bool` | Whether to answer a source map alongside the code. |
|
|
284
|
+
| `strict` | `bool` | Whether to raise on any diagnostic. Off, only unusable output raises. |
|
|
285
|
+
|
|
286
|
+
`parse` reads these instead:
|
|
287
|
+
|
|
288
|
+
| Option | Type | Description |
|
|
289
|
+
|-------------------|----------|----------------------------------------------------------------------------|
|
|
290
|
+
| `ast` | `bool` | Whether to build the AST at all. On by default. |
|
|
291
|
+
| `ast_type` | `String` | `js` or `ts`, to include or leave out the TypeScript properties. |
|
|
292
|
+
| `ranges` | `bool` | Whether every node carries a `range` pair. |
|
|
293
|
+
| `preserve_parens` | `bool` | Whether parentheses become `ParenthesizedExpression` nodes. On by default. |
|
|
294
|
+
| `comments` | `bool` | Whether to collect the comments. On by default. |
|
|
295
|
+
| `semantic_errors` | `bool` | Whether to also report what semantic analysis finds. |
|
|
296
|
+
|
|
297
|
+
`transform` reads these instead of `compress` and `mangle`:
|
|
298
|
+
|
|
299
|
+
| Option | Type | Description |
|
|
300
|
+
|--------------|-------------------|--------------------------------------------------------------------|
|
|
301
|
+
| `target` | `String`, `Array` | The ECMAScript version or browsers to lower for, such as `es2015`. |
|
|
302
|
+
| `jsx` | `bool`, `Hash` | Whether to compile JSX, and how. `false` leaves it as written. |
|
|
303
|
+
| `typescript` | `Hash` | How to compile TypeScript. |
|
|
304
|
+
| `helpers` | `Hash` | Where the runtime helpers come from, `runtime` or `external`. |
|
|
305
|
+
| `define` | `Hash` | Names to replace wherever they appear. |
|
|
306
|
+
| `inject` | `Hash` | Names to import where the source used them without importing. |
|
|
307
|
+
| `minify` | `bool`, `Hash` | Whether to minify in the same pass, and how. |
|
|
308
|
+
| `cwd` | `String` | What relative paths in other options are relative to. |
|
|
309
|
+
|
|
310
|
+
An option nobody reads is refused, and so is one inside a nested hash:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
Oxc.minify(source, nonsense: true)
|
|
314
|
+
#=> Oxc::OptionError: Unknown option: nonsense
|
|
315
|
+
|
|
316
|
+
Oxc.minify(source, compress: { nonsense: true })
|
|
317
|
+
#=> Oxc::OptionError: Invalid options: unknown field `nonsense`, expected one of `target`, ...
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Results
|
|
321
|
+
|
|
322
|
+
Each call answers its own result, so no result carries a field the call that produced it can never fill.
|
|
323
|
+
|
|
324
|
+
* `Oxc.minify` answers an `Oxc::MinifyResult`
|
|
325
|
+
* `Oxc.transform` answers an `Oxc::TransformResult`
|
|
326
|
+
* `Oxc.parse` answers an `Oxc::ParseResult`
|
|
327
|
+
|
|
328
|
+
`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.
|
|
329
|
+
|
|
330
|
+
#### What every result answers
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
result = Oxc.minify("const x = 1; console.log(x)")
|
|
334
|
+
|
|
335
|
+
result.diagnostics #=> everything oxc had to say
|
|
336
|
+
result.errors #=> the error-severity half of it
|
|
337
|
+
result.warnings #=> the warning-severity half
|
|
338
|
+
result.errors?
|
|
339
|
+
result.warnings?
|
|
340
|
+
result.panicked? #=> whether oxc gave up on the source
|
|
341
|
+
result.validate! #=> itself, or raises Oxc::SyntaxError
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`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.
|
|
345
|
+
|
|
346
|
+
#### Minify and transform
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
result = Oxc.minify("const x = 1; console.log(x)")
|
|
350
|
+
|
|
351
|
+
result.code #=> "console.log(1);"
|
|
352
|
+
result.to_s #=> "console.log(1);"
|
|
353
|
+
result.map #=> nil, or the source map as JSON text
|
|
354
|
+
result.legal_comments #=> []
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
A transform adds what only a transform can answer.
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
result = Oxc.transform(source, filename: "app.ts", sourcemap: true, typescript: { declaration: true })
|
|
361
|
+
|
|
362
|
+
result.declaration #=> the .d.ts it wrote
|
|
363
|
+
result.declaration_map #=> its source map, as JSON text
|
|
364
|
+
result.helpers_used #=> the runtime helpers its lowering reached for
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Parse
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
parsed = Oxc.parse(source, source_type: "module", module_record: true)
|
|
371
|
+
|
|
372
|
+
parsed.program #=> the ESTree AST, or nil when ast: false
|
|
373
|
+
parsed.module_record #=> the imports and exports, when asked for
|
|
374
|
+
parsed.symbols #=> the bindings and their references, when asked for
|
|
375
|
+
parsed.comments #=> Array[Oxc::Comment]
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Diagnostics
|
|
379
|
+
|
|
380
|
+
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.
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
begin
|
|
384
|
+
Oxc.minify("const x = ;", filename: "broken.js")
|
|
385
|
+
rescue Oxc::SyntaxError => e
|
|
386
|
+
e.message #=> "Unexpected token"
|
|
387
|
+
e.diagnostics.first.codeframe #=> the frame below
|
|
388
|
+
e.result.panicked? #=> true
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
x Unexpected token
|
|
394
|
+
,-[broken.js:1:11]
|
|
395
|
+
1 | const x = ;
|
|
396
|
+
: ^
|
|
397
|
+
`----
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
A diagnostic's labels count in **UTF-8 bytes**, which is what oxc counts in and what Ruby slices by:
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
label = e.diagnostics.first.labels.first
|
|
404
|
+
|
|
405
|
+
label.start #=> 10
|
|
406
|
+
label.finish #=> 11
|
|
407
|
+
label.slice(source) #=> ";"
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Development
|
|
411
|
+
|
|
412
|
+
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.
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
bin/setup
|
|
416
|
+
bundle exec rake
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`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.
|
|
420
|
+
|
|
421
|
+
### Acknowledgements
|
|
422
|
+
|
|
423
|
+
[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.
|
|
424
|
+
|
|
425
|
+
Thank you to all of them.
|
|
426
|
+
|
|
427
|
+
### Contributing
|
|
428
|
+
|
|
429
|
+
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).
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
### License
|
|
434
|
+
|
|
435
|
+
The Ruby, C, and Rust code in this gem is available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
436
|
+
|
|
437
|
+
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.
|
data/ext/oxc/extconf.rb
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
ext_dir = __dir__
|
|
7
|
+
root_dir = File.expand_path("../..", ext_dir)
|
|
8
|
+
|
|
9
|
+
rust_dir = File.join(root_dir, "rust")
|
|
10
|
+
|
|
11
|
+
unless File.exist?(File.join(rust_dir, "Cargo.toml"))
|
|
12
|
+
abort <<~MESSAGE
|
|
13
|
+
|
|
14
|
+
ERROR: Rust sources not found at #{rust_dir}.
|
|
15
|
+
|
|
16
|
+
MESSAGE
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless system("cargo --version > /dev/null 2>&1")
|
|
20
|
+
abort <<~MESSAGE
|
|
21
|
+
|
|
22
|
+
ERROR: Rust toolchain not found.
|
|
23
|
+
|
|
24
|
+
oxc requires the Rust toolchain to compile from source.
|
|
25
|
+
|
|
26
|
+
Install Rust: https://rustup.rs
|
|
27
|
+
|
|
28
|
+
MESSAGE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
RUST_TARGETS = {
|
|
32
|
+
"aarch64-linux-gnu" => "aarch64-unknown-linux-gnu",
|
|
33
|
+
"aarch64-linux-musl" => "aarch64-unknown-linux-musl",
|
|
34
|
+
"arm-linux-gnu" => "armv7-unknown-linux-gnueabihf",
|
|
35
|
+
"arm-linux-musl" => "armv7-unknown-linux-musleabihf",
|
|
36
|
+
"arm64-darwin" => "aarch64-apple-darwin",
|
|
37
|
+
"x86_64-darwin" => "x86_64-apple-darwin",
|
|
38
|
+
"x86_64-linux-gnu" => "x86_64-unknown-linux-gnu",
|
|
39
|
+
"x86_64-linux-musl" => "x86_64-unknown-linux-musl",
|
|
40
|
+
"x86-linux-gnu" => "i686-unknown-linux-gnu",
|
|
41
|
+
"x86-linux-musl" => "i686-unknown-linux-musl",
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
cross_compiling = ENV.key?("RUBY_CC_VERSION")
|
|
45
|
+
target_platform = ENV.fetch("CARGO_BUILD_TARGET", nil)
|
|
46
|
+
|
|
47
|
+
if cross_compiling && target_platform.nil?
|
|
48
|
+
rcd_platform = ENV.fetch("RCD_PLATFORM", "")
|
|
49
|
+
target_platform = RUST_TARGETS[rcd_platform]
|
|
50
|
+
|
|
51
|
+
if target_platform.nil?
|
|
52
|
+
ruby_platform = RbConfig::CONFIG["arch"]
|
|
53
|
+
target_platform = RUST_TARGETS.values.find { |target| ruby_platform.include?(target.split("-").first) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
header_path = File.join(ext_dir, "include", "oxc.h")
|
|
58
|
+
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(header_path))
|
|
60
|
+
|
|
61
|
+
target_dir = File.join(rust_dir, "target")
|
|
62
|
+
|
|
63
|
+
if target_platform
|
|
64
|
+
puts "oxc: Cross-compiling Rust for target: #{target_platform}"
|
|
65
|
+
|
|
66
|
+
system("rustup target add #{target_platform}") || warn("oxc: Failed to add Rust target #{target_platform}")
|
|
67
|
+
|
|
68
|
+
cargo_args = "--release --target #{target_platform}"
|
|
69
|
+
lib_dir = File.join(target_dir, target_platform, "release")
|
|
70
|
+
else
|
|
71
|
+
puts "oxc: Compiling Rust library for native platform..."
|
|
72
|
+
|
|
73
|
+
cargo_args = "--release"
|
|
74
|
+
lib_dir = File.join(target_dir, "release")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
unless system("cd #{rust_dir} && cargo build #{cargo_args}")
|
|
78
|
+
abort "ERROR: Failed to compile oxc from Rust source."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless File.exist?(header_path)
|
|
82
|
+
abort "ERROR: cbindgen did not generate #{header_path}. Try `cargo clean` in #{rust_dir} and reinstall."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
static_lib = File.join(lib_dir, "liboxc_ffi.a")
|
|
86
|
+
|
|
87
|
+
developing = File.exist?(File.join(root_dir, ".git"))
|
|
88
|
+
|
|
89
|
+
if File.exist?(static_lib) && !developing
|
|
90
|
+
vendored = File.join(ext_dir, "liboxc_ffi.a")
|
|
91
|
+
|
|
92
|
+
FileUtils.cp(static_lib, vendored)
|
|
93
|
+
FileUtils.rm_rf(target_dir)
|
|
94
|
+
|
|
95
|
+
puts "oxc: Static library vendored at #{vendored}, Rust build directory removed"
|
|
96
|
+
|
|
97
|
+
$LDFLAGS << " #{vendored}"
|
|
98
|
+
elsif File.exist?(static_lib)
|
|
99
|
+
puts "oxc: Static library found at #{static_lib}"
|
|
100
|
+
|
|
101
|
+
$LDFLAGS << " #{static_lib}"
|
|
102
|
+
else
|
|
103
|
+
host_os = target_platform || RbConfig::CONFIG["host_os"]
|
|
104
|
+
|
|
105
|
+
lib_name = case host_os
|
|
106
|
+
when /darwin/ then "liboxc_ffi.dylib"
|
|
107
|
+
when /mingw|mswin|windows/ then "oxc_ffi.dll"
|
|
108
|
+
else "liboxc_ffi.so"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
lib_path = File.join(lib_dir, lib_name)
|
|
112
|
+
|
|
113
|
+
abort "ERROR: Shared library not found at #{lib_path}" unless File.exist?(lib_path)
|
|
114
|
+
|
|
115
|
+
puts "oxc: Shared library found at #{lib_path} (dynamic)"
|
|
116
|
+
|
|
117
|
+
$LDFLAGS << " -L#{lib_dir} -loxc_ffi"
|
|
118
|
+
$LDFLAGS << " -Wl,-rpath,#{lib_dir}" if RbConfig::CONFIG["host_os"].match?(/darwin|linux/)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
$CFLAGS << " -I#{ext_dir}"
|
|
122
|
+
|
|
123
|
+
create_makefile("oxc/oxc")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Generated by cbindgen — do not edit manually */
|
|
2
|
+
|
|
3
|
+
#include <stdbool.h>
|
|
4
|
+
#include <stdint.h>
|
|
5
|
+
#include <stddef.h>
|
|
6
|
+
|
|
7
|
+
#ifndef OXC_H
|
|
8
|
+
#define OXC_H
|
|
9
|
+
|
|
10
|
+
typedef enum OxcErrorCode {
|
|
11
|
+
OXC_ERROR_CODE_NONE = 0,
|
|
12
|
+
OXC_ERROR_CODE_OPTION,
|
|
13
|
+
OXC_ERROR_CODE_ENCODING,
|
|
14
|
+
OXC_ERROR_CODE_TRANSFORM,
|
|
15
|
+
OXC_ERROR_CODE_INTERNAL,
|
|
16
|
+
OXC_ERROR_CODE_PANIC,
|
|
17
|
+
} OxcErrorCode;
|
|
18
|
+
|
|
19
|
+
typedef struct OxcResult {
|
|
20
|
+
char *value;
|
|
21
|
+
uintptr_t value_len;
|
|
22
|
+
char *error;
|
|
23
|
+
enum OxcErrorCode code;
|
|
24
|
+
} OxcResult;
|
|
25
|
+
|
|
26
|
+
struct OxcResult oxc_parse(const char *source, const char *options_json);
|
|
27
|
+
|
|
28
|
+
struct OxcResult oxc_transform(const char *source, const char *options_json);
|
|
29
|
+
|
|
30
|
+
struct OxcResult oxc_minify(const char *source, const char *options_json);
|
|
31
|
+
|
|
32
|
+
char *oxc_version(void);
|
|
33
|
+
|
|
34
|
+
char *oxc_oxc_version(void);
|
|
35
|
+
|
|
36
|
+
void oxc_string_free(char *value);
|
|
37
|
+
|
|
38
|
+
void oxc_result_free(struct OxcResult result);
|
|
39
|
+
|
|
40
|
+
#endif /* OXC_H */
|