@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.
Files changed (3) hide show
  1. package/README.md +46 -9
  2. package/index.js +79 -65
  3. 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`: `"effect"` or `false`
140
- - paired wrapperless mode: `wrapConditionals: false` with `memoWrapper: false`
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: false`,
163
- paired `wrapConditionals: false` / `memoWrapper: false`, `requireImportSource`,
164
- `staticMarker`, `validate`, `omitNestedClosingTags`, `omitLastClosingTag`,
165
- `builtIns`, and dynamic `renderers`
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 = requireNative();
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 === "requireImportSource") {
78
- if (value === false) continue;
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
- "@dom-expressions/jsx-compiler `requireImportSource` option must be false or a string"
67
+ `@dom-expressions/jsx-compiler \`${key}\` option must be a string import name or false`
82
68
  );
83
69
  }
84
- if (!hasJsxImportSource(code, value)) {
85
- return { skip: true };
86
- }
70
+ nativeOptions[key] = value;
87
71
  continue;
88
72
  }
89
- if (key === "effectWrapper") {
90
- if (value === "effect") continue;
91
- if (value === false) {
92
- nativeOptions.effectWrapper = false;
93
- continue;
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
- throw new Error(
96
- '@dom-expressions/jsx-compiler only supports `effectWrapper: false` or the default `"effect"`'
97
- );
79
+ if (value !== false) nativeOptions.requireImportSource = value;
80
+ continue;
98
81
  }
99
82
  if (key === "wrapConditionals") {
100
- if (value === true) continue;
101
- if (value === false) {
102
- nativeOptions.wrapConditionals = false;
103
- continue;
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
- throw new Error(
116
- '@dom-expressions/jsx-compiler only supports `memoWrapper: false` or the default `"memo"`'
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 requireNative() {
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)) return require(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(`@dom-expressions/jsx-compiler-${suffix}`);
190
+ return require(packageName);
209
191
  } catch (error) {
210
- if (error.code !== "MODULE_NOT_FOUND") throw 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 the native @dom-expressions/jsx-compiler binary for ${process.platform}-${process.arch}` +
207
+ `Could not find an @dom-expressions/jsx-compiler binding for ${process.platform}-${process.arch}` +
216
208
  (suffix
217
- ? ` (expected the @dom-expressions/jsx-compiler-${suffix} package or a local build)`
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
- ". Run `pnpm run build` in packages/jsx-compiler to build from source."
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.16",
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
- "@napi-rs/cli": "^3.6.2"
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.16",
42
- "@dom-expressions/jsx-compiler-darwin-arm64": "0.50.0-next.16",
43
- "@dom-expressions/jsx-compiler-linux-x64-gnu": "0.50.0-next.16",
44
- "@dom-expressions/jsx-compiler-linux-arm64-gnu": "0.50.0-next.16",
45
- "@dom-expressions/jsx-compiler-win32-x64-msvc": "0.50.0-next.16"
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",