@dom-expressions/jsx-compiler 0.50.0-next.16 → 0.50.0-next.18
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/README.md +46 -9
- package/index.js +79 -65
- package/package.json +23 -9
package/README.md
CHANGED
|
@@ -15,6 +15,29 @@ matching your platform automatically through `optionalDependencies`. On other
|
|
|
15
15
|
platforms, build from source with `pnpm run build` inside
|
|
16
16
|
`packages/jsx-compiler` (requires a Rust toolchain).
|
|
17
17
|
|
|
18
|
+
### WebAssembly and StackBlitz
|
|
19
|
+
|
|
20
|
+
The compiler also ships a WASI fallback for environments such as StackBlitz
|
|
21
|
+
WebContainers, where Node.js reports a native platform but cannot load `.node`
|
|
22
|
+
addons. Enable the `wasm32` optional dependency when installing:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install --cpu=wasm32 @dom-expressions/jsx-compiler
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With pnpm, add the installation architecture to `pnpm-workspace.yaml`:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
supportedArchitectures:
|
|
32
|
+
cpu:
|
|
33
|
+
- current
|
|
34
|
+
- wasm32
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The normal package entry point prefers a native binding and falls back to
|
|
38
|
+
`@dom-expressions/jsx-compiler-wasm32-wasi` when native addons are unavailable.
|
|
39
|
+
Set `NAPI_RS_FORCE_WASI=error` to require the WASI binding for testing.
|
|
40
|
+
|
|
18
41
|
## Usage
|
|
19
42
|
|
|
20
43
|
This package exposes a compiler backend API. It is not a Vite, Rollup, or Babel
|
|
@@ -136,8 +159,9 @@ Supported options track the Babel plugin where currently implemented:
|
|
|
136
159
|
- `omitQuotes`
|
|
137
160
|
- `omitAttributeSpacing`
|
|
138
161
|
- `inlineStyles`
|
|
139
|
-
- `effectWrapper`:
|
|
140
|
-
-
|
|
162
|
+
- `effectWrapper`: custom import name string, or `false` to disable
|
|
163
|
+
- `memoWrapper`: custom import name string, or `false` to disable
|
|
164
|
+
- `wrapConditionals`
|
|
141
165
|
- `staticMarker`
|
|
142
166
|
- `validate`
|
|
143
167
|
- `omitNestedClosingTags`
|
|
@@ -146,6 +170,22 @@ Supported options track the Babel plugin where currently implemented:
|
|
|
146
170
|
- `requireImportSource`
|
|
147
171
|
- `renderers`
|
|
148
172
|
|
|
173
|
+
## Performance
|
|
174
|
+
|
|
175
|
+
Compared against `@dom-expressions/babel-plugin-jsx` compiling identical
|
|
176
|
+
sources under identical options (Apple Silicon, release build, in-process,
|
|
177
|
+
median of 7 iterations after warmup — run `pnpm bench` in this package to
|
|
178
|
+
reproduce on your machine):
|
|
179
|
+
|
|
180
|
+
| Workload | babel-plugin-jsx | jsx-compiler | Speedup |
|
|
181
|
+
| ----------------------------------------------- | ---------------: | -----------: | ------: |
|
|
182
|
+
| Fixture corpus (88 files, 174 KB, all 10 modes) | 151 ms | 8.4 ms | 18x |
|
|
183
|
+
| 129 KB single module | 230 ms | 4.6 ms | 50x |
|
|
184
|
+
| 1 MB single module | 10,164 ms | 39 ms | 258x |
|
|
185
|
+
|
|
186
|
+
Native throughput stays flat at ~20–27 MB/s as input grows, while Babel's
|
|
187
|
+
per-file cost grows super-linearly — so the gap widens with file size.
|
|
188
|
+
|
|
149
189
|
## Current Scope
|
|
150
190
|
|
|
151
191
|
This package is the AST-native compiler backend. It currently has checked fixture coverage for
|
|
@@ -159,10 +199,10 @@ the DOM, hydratable DOM, dev hydratable DOM, SSR, hydratable SSR, universal, dyn
|
|
|
159
199
|
attribute handling covered by the checked fixture suites for those targets
|
|
160
200
|
- Solid-compatible defaults such as `contextToCustomElements: true`
|
|
161
201
|
- option coverage for `hydratable`, `dev`, `delegateEvents`, `delegatedEvents`,
|
|
162
|
-
`omitQuotes`, `omitAttributeSpacing`, `inlineStyles`, `effectWrapper
|
|
163
|
-
|
|
164
|
-
`
|
|
165
|
-
|
|
202
|
+
`omitQuotes`, `omitAttributeSpacing`, `inlineStyles`, `effectWrapper`,
|
|
203
|
+
`memoWrapper`, `wrapConditionals`, `requireImportSource`, `staticMarker`,
|
|
204
|
+
`validate`, `omitNestedClosingTags`, `omitLastClosingTag`, `builtIns`, and
|
|
205
|
+
dynamic `renderers`
|
|
166
206
|
- source maps for the implemented path
|
|
167
207
|
|
|
168
208
|
## Not Implemented Yet
|
|
@@ -172,8 +212,6 @@ The compiler intentionally rejects unsupported features instead of pretending to
|
|
|
172
212
|
- DOM `namespaceElements` sections that the current Oxc parser rejects before transform
|
|
173
213
|
(for example, hyphenated JSX member segments)
|
|
174
214
|
- arbitrary custom renderer names beyond dynamic DOM renderer override plus universal fallback
|
|
175
|
-
- custom `effectWrapper` / `memoWrapper` helper names
|
|
176
|
-
- unpaired `wrapConditionals: false` or `memoWrapper: false`
|
|
177
215
|
- unknown/custom namespaced DOM attributes outside known runtime namespaces such as `xlink`
|
|
178
216
|
|
|
179
217
|
## Architecture
|
|
@@ -198,4 +236,3 @@ The module layout mirrors the Babel plugin shape where possible:
|
|
|
198
236
|
- `src/ssr/transform.rs`
|
|
199
237
|
- `src/universal/mod.rs`
|
|
200
238
|
- `src/universal/transform.rs`
|
|
201
|
-
|
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
-
const native =
|
|
6
|
+
const native = requireBinding();
|
|
7
7
|
|
|
8
8
|
function transform(code, options) {
|
|
9
9
|
if (typeof code !== "string") {
|
|
@@ -13,12 +13,6 @@ function transform(code, options) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const nativeOptions = validateOptions(code, options);
|
|
16
|
-
if (nativeOptions?.skip) {
|
|
17
|
-
return {
|
|
18
|
-
code,
|
|
19
|
-
map: null
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
16
|
const result = native.transform(code, nativeOptions);
|
|
23
17
|
return {
|
|
24
18
|
code: result.code,
|
|
@@ -46,6 +40,8 @@ const nativeOptionKeys = new Set([
|
|
|
46
40
|
"effectWrapper",
|
|
47
41
|
"wrapConditionals",
|
|
48
42
|
"memoWrapper",
|
|
43
|
+
"requireImportSource",
|
|
44
|
+
"validate",
|
|
49
45
|
"staticMarker",
|
|
50
46
|
"omitNestedClosingTags",
|
|
51
47
|
"omitLastClosingTag",
|
|
@@ -63,63 +59,40 @@ function validateOptions(code, options) {
|
|
|
63
59
|
);
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
const wrapperless = options.wrapConditionals === false || options.memoWrapper === false;
|
|
67
|
-
if (wrapperless) {
|
|
68
|
-
if (options.wrapConditionals !== false || options.memoWrapper !== false) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
"@dom-expressions/jsx-compiler only supports wrapperless mode when `wrapConditionals: false` and `memoWrapper: false` are used together"
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
62
|
const nativeOptions = {};
|
|
76
63
|
for (const [key, value] of Object.entries(options)) {
|
|
77
|
-
if (key === "
|
|
78
|
-
if (value
|
|
79
|
-
if (typeof value !== "string") {
|
|
64
|
+
if (key === "effectWrapper" || key === "memoWrapper") {
|
|
65
|
+
if (typeof value !== "string" && typeof value !== "boolean") {
|
|
80
66
|
throw new TypeError(
|
|
81
|
-
|
|
67
|
+
`@dom-expressions/jsx-compiler \`${key}\` option must be a string import name or false`
|
|
82
68
|
);
|
|
83
69
|
}
|
|
84
|
-
|
|
85
|
-
return { skip: true };
|
|
86
|
-
}
|
|
70
|
+
nativeOptions[key] = value;
|
|
87
71
|
continue;
|
|
88
72
|
}
|
|
89
|
-
if (key === "
|
|
90
|
-
if (value
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
if (key === "requireImportSource") {
|
|
74
|
+
if (value !== false && typeof value !== "string") {
|
|
75
|
+
throw new TypeError(
|
|
76
|
+
"@dom-expressions/jsx-compiler `requireImportSource` option must be false or a string"
|
|
77
|
+
);
|
|
94
78
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
);
|
|
79
|
+
if (value !== false) nativeOptions.requireImportSource = value;
|
|
80
|
+
continue;
|
|
98
81
|
}
|
|
99
82
|
if (key === "wrapConditionals") {
|
|
100
|
-
if (value
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
throw new TypeError(
|
|
106
|
-
"@dom-expressions/jsx-compiler `wrapConditionals` option must be boolean"
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
if (key === "memoWrapper") {
|
|
110
|
-
if (value === "memo") continue;
|
|
111
|
-
if (value === false) {
|
|
112
|
-
nativeOptions.memoWrapper = false;
|
|
113
|
-
continue;
|
|
83
|
+
if (typeof value !== "boolean") {
|
|
84
|
+
throw new TypeError(
|
|
85
|
+
"@dom-expressions/jsx-compiler `wrapConditionals` option must be boolean"
|
|
86
|
+
);
|
|
114
87
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
88
|
+
nativeOptions.wrapConditionals = value;
|
|
89
|
+
continue;
|
|
118
90
|
}
|
|
119
91
|
if (key === "validate") {
|
|
120
92
|
if (typeof value !== "boolean") {
|
|
121
93
|
throw new TypeError("@dom-expressions/jsx-compiler `validate` option must be boolean");
|
|
122
94
|
}
|
|
95
|
+
nativeOptions.validate = value;
|
|
123
96
|
continue;
|
|
124
97
|
}
|
|
125
98
|
if (nativeOptionKeys.has(key)) {
|
|
@@ -139,15 +112,6 @@ function validateOptions(code, options) {
|
|
|
139
112
|
return nativeOptions;
|
|
140
113
|
}
|
|
141
114
|
|
|
142
|
-
function hasJsxImportSource(code, source) {
|
|
143
|
-
const pattern = /@jsxImportSource\s+([^\s*]+)/g;
|
|
144
|
-
let match;
|
|
145
|
-
while ((match = pattern.exec(code))) {
|
|
146
|
-
if (match[1] === source) return true;
|
|
147
|
-
}
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
115
|
function validateRenderers(renderers) {
|
|
152
116
|
if (renderers == null) return;
|
|
153
117
|
if (!Array.isArray(renderers)) {
|
|
@@ -188,10 +152,20 @@ function platformArchSuffix() {
|
|
|
188
152
|
return null;
|
|
189
153
|
}
|
|
190
154
|
|
|
191
|
-
function
|
|
155
|
+
function requireBinding() {
|
|
192
156
|
const explicit = process.env.JSX_DOM_EXPRESSIONS_COMPILER_NATIVE;
|
|
193
157
|
if (explicit) return require(explicit);
|
|
194
158
|
|
|
159
|
+
const forceWasi = process.env.NAPI_RS_FORCE_WASI;
|
|
160
|
+
if (forceWasi === "true" || forceWasi === "error") {
|
|
161
|
+
const wasi = requireWasi();
|
|
162
|
+
if (wasi) return wasi;
|
|
163
|
+
if (forceWasi === "error") {
|
|
164
|
+
throw new Error("WASI binding not found and NAPI_RS_FORCE_WASI is set to error");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let nativeError;
|
|
195
169
|
const suffix = platformArchSuffix();
|
|
196
170
|
|
|
197
171
|
// Local builds (napi build output) take precedence for development.
|
|
@@ -200,23 +174,63 @@ function requireNative() {
|
|
|
200
174
|
localCandidates.push("jsx-compiler.node");
|
|
201
175
|
for (const file of localCandidates) {
|
|
202
176
|
const full = path.join(__dirname, file);
|
|
203
|
-
if (fs.existsSync(full))
|
|
177
|
+
if (fs.existsSync(full)) {
|
|
178
|
+
try {
|
|
179
|
+
return require(full);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
nativeError = error;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
204
185
|
}
|
|
205
186
|
|
|
206
|
-
if (suffix) {
|
|
187
|
+
if (!nativeError && suffix) {
|
|
188
|
+
const packageName = `@dom-expressions/jsx-compiler-${suffix}`;
|
|
207
189
|
try {
|
|
208
|
-
return require(
|
|
190
|
+
return require(packageName);
|
|
209
191
|
} catch (error) {
|
|
210
|
-
if (error
|
|
192
|
+
if (!isMissingPackage(error, packageName)) nativeError = error;
|
|
211
193
|
}
|
|
212
194
|
}
|
|
213
195
|
|
|
196
|
+
const wasi = requireWasi();
|
|
197
|
+
if (wasi) return wasi;
|
|
198
|
+
|
|
199
|
+
if (nativeError) {
|
|
200
|
+
nativeError.message +=
|
|
201
|
+
"\nThe native binding could not be loaded and the optional " +
|
|
202
|
+
"@dom-expressions/jsx-compiler-wasm32-wasi fallback is not installed.";
|
|
203
|
+
throw nativeError;
|
|
204
|
+
}
|
|
205
|
+
|
|
214
206
|
throw new Error(
|
|
215
|
-
`Could not find
|
|
207
|
+
`Could not find an @dom-expressions/jsx-compiler binding for ${process.platform}-${process.arch}` +
|
|
216
208
|
(suffix
|
|
217
|
-
? ` (expected
|
|
209
|
+
? ` (expected @dom-expressions/jsx-compiler-${suffix}, @dom-expressions/jsx-compiler-wasm32-wasi, or a local build)`
|
|
218
210
|
: " (no prebuilt binary is published for this platform)") +
|
|
219
|
-
".
|
|
211
|
+
". Install with WASM support or run `pnpm run build` in packages/jsx-compiler."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function requireWasi() {
|
|
216
|
+
const localWasi = path.join(__dirname, "jsx-compiler.wasi.cjs");
|
|
217
|
+
if (fs.existsSync(localWasi)) return require(localWasi);
|
|
218
|
+
|
|
219
|
+
const packageName = "@dom-expressions/jsx-compiler-wasm32-wasi";
|
|
220
|
+
try {
|
|
221
|
+
return require(packageName);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (!isMissingPackage(error, packageName)) throw error;
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isMissingPackage(error, packageName) {
|
|
229
|
+
return (
|
|
230
|
+
error &&
|
|
231
|
+
error.code === "MODULE_NOT_FOUND" &&
|
|
232
|
+
typeof error.message === "string" &&
|
|
233
|
+
error.message.includes(`'${packageName}'`)
|
|
220
234
|
);
|
|
221
235
|
}
|
|
222
236
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dom-expressions/jsx-compiler",
|
|
3
3
|
"description": "A JSX to DOM Expressions compiler implemented with Oxc",
|
|
4
|
-
"version": "0.50.0-next.
|
|
4
|
+
"version": "0.50.0-next.18",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -31,22 +31,36 @@
|
|
|
31
31
|
"aarch64-apple-darwin",
|
|
32
32
|
"x86_64-unknown-linux-gnu",
|
|
33
33
|
"aarch64-unknown-linux-gnu",
|
|
34
|
-
"x86_64-pc-windows-msvc"
|
|
35
|
-
|
|
34
|
+
"x86_64-pc-windows-msvc",
|
|
35
|
+
"wasm32-wasip1-threads"
|
|
36
|
+
],
|
|
37
|
+
"wasm": {
|
|
38
|
+
"browser": {
|
|
39
|
+
"fs": false,
|
|
40
|
+
"asyncInit": false,
|
|
41
|
+
"buffer": false,
|
|
42
|
+
"errorEvent": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
},
|
|
37
46
|
"devDependencies": {
|
|
38
|
-
"@
|
|
47
|
+
"@emnapi/core": "^1.11.2",
|
|
48
|
+
"@emnapi/runtime": "^1.11.2",
|
|
49
|
+
"@napi-rs/cli": "^3.7.2",
|
|
50
|
+
"@napi-rs/wasm-runtime": "^1.1.6"
|
|
39
51
|
},
|
|
40
52
|
"optionalDependencies": {
|
|
41
|
-
"@dom-expressions/jsx-compiler-darwin-x64": "0.50.0-next.
|
|
42
|
-
"@dom-expressions/jsx-compiler-darwin-arm64": "0.50.0-next.
|
|
43
|
-
"@dom-expressions/jsx-compiler-linux-x64-gnu": "0.50.0-next.
|
|
44
|
-
"@dom-expressions/jsx-compiler-linux-arm64-gnu": "0.50.0-next.
|
|
45
|
-
"@dom-expressions/jsx-compiler-win32-x64-msvc": "0.50.0-next.
|
|
53
|
+
"@dom-expressions/jsx-compiler-darwin-x64": "0.50.0-next.18",
|
|
54
|
+
"@dom-expressions/jsx-compiler-darwin-arm64": "0.50.0-next.18",
|
|
55
|
+
"@dom-expressions/jsx-compiler-linux-x64-gnu": "0.50.0-next.18",
|
|
56
|
+
"@dom-expressions/jsx-compiler-linux-arm64-gnu": "0.50.0-next.18",
|
|
57
|
+
"@dom-expressions/jsx-compiler-win32-x64-msvc": "0.50.0-next.18",
|
|
58
|
+
"@dom-expressions/jsx-compiler-wasm32-wasi": "0.50.0-next.18"
|
|
46
59
|
},
|
|
47
60
|
"scripts": {
|
|
48
61
|
"build": "napi build --release --strip --manifest-path ./Cargo.toml",
|
|
49
62
|
"build:debug": "napi build --manifest-path ./Cargo.toml",
|
|
63
|
+
"bench": "pnpm run build && node scripts/bench.mjs",
|
|
50
64
|
"lint": "cargo clippy --manifest-path ./Cargo.toml -- -D warnings",
|
|
51
65
|
"test": "pnpm run build:debug && jest --no-cache",
|
|
52
66
|
"artifacts": "napi artifacts",
|