@knighted/jsx 1.3.1 → 1.3.2

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 CHANGED
@@ -18,10 +18,9 @@ A runtime JSX template tag backed by the [`oxc-parser`](https://github.com/oxc-p
18
18
  - [React runtime](#react-runtime-reactjsx)
19
19
  - [Loader integration](#loader-integration)
20
20
  - [Node / SSR usage](#node--ssr-usage)
21
- - [Next.js integration](#nextjs-integration)
22
21
  - [Browser usage](#browser-usage)
22
+ - [TypeScript plugin](docs/ts-plugin.md)
23
23
  - [Component testing](docs/testing.md)
24
- - [Testing & demos](#testing)
25
24
  - [CLI setup](docs/cli.md)
26
25
 
27
26
  ## Installation
@@ -31,7 +30,7 @@ npm install @knighted/jsx
31
30
  ```
32
31
 
33
32
  > [!IMPORTANT]
34
- > This package is ESM-only and targets browsers or ESM-aware bundlers. `require()` is not supported; use native `import`/`<script type="module">` and a DOM-like environment.
33
+ > `@knighted/jsx` ships as ESM-only. The dual-mode `.cjs` artifacts we build internally are not published.
35
34
 
36
35
  > [!NOTE]
37
36
  > Planning to use the React runtime (`@knighted/jsx/react`)? Install `react@>=18` and `react-dom@>=18` alongside this package so the helper can create elements and render them through ReactDOM.
@@ -95,6 +94,51 @@ createRoot(document.getElementById('root')!).render(reactJsx`<${App} />`)
95
94
 
96
95
  The React runtime shares the same template semantics as `jsx`, except it returns React elements (via `React.createElement`) so you can embed other React components with `<${MyComponent} />` and use hooks/state as usual. The helper lives in a separate subpath so DOM-only consumers never pay the React dependency cost.
97
96
 
97
+ ### DOM-specific props
98
+
99
+ - `style` accepts either a string or an object. Object values handle CSS custom properties (`--token`) automatically.
100
+ - `class` and `className` both work and can be strings or arrays.
101
+ - Event handlers use the `on<Event>` naming convention (e.g. `onClick`).
102
+ - `ref` supports callback refs as well as mutable `{ current }` objects.
103
+ - `dangerouslySetInnerHTML` expects an object with an `__html` field, mirroring React.
104
+
105
+ ### Fragments & SVG
106
+
107
+ Use JSX fragments (`<>...</>`) for multi-root templates. SVG trees automatically switch to the `http://www.w3.org/2000/svg` namespace once they enter an `<svg>` tag, and fall back inside `<foreignObject>`.
108
+
109
+ ### Interpolations and components
110
+
111
+ - `${...}` works exactly like JSX braces: drop expressions anywhere (text, attributes, spreads, conditionals) and the runtime keeps the original syntax. Text nodes do not need extra wrapping—`Count is ${value}` already works.
112
+ - Interpolated values can be primitives, DOM nodes, arrays/iterables, other `jsx` trees, or component functions. Resolve Promises before passing them in.
113
+ - Inline components are just functions/classes you interpolate as the tag name; they receive props plus optional `children` and can return anything `jsx` accepts.
114
+
115
+ ```ts
116
+ const Button = ({ variant = 'primary' }) => {
117
+ let count = 3
118
+
119
+ return jsx`
120
+ <button
121
+ data-variant=${variant}
122
+ onClick=${() => {
123
+ count += 1
124
+ console.log(`Count is now ${count}`)
125
+ }}
126
+ >
127
+ Count is ${count}
128
+ </button>
129
+ `
130
+ }
131
+
132
+ const view = jsx`
133
+ <section>
134
+ <p>Inline components can manage their own state.</p>
135
+ <${Button} variant="ghost" />
136
+ </section>
137
+ `
138
+
139
+ document.body.append(view)
140
+ ```
141
+
98
142
  ## Loader integration
99
143
 
100
144
  Use the published loader entry (`@knighted/jsx/loader`) when you want your bundler to rewrite tagged template literals at build time. The loader finds every ` jsx`` ` (and, by default, ` reactJsx`` ` ) invocation, rebuilds the template with real JSX semantics, and hands back transformed source that can run in any environment.
@@ -122,24 +166,9 @@ export default {
122
166
  }
123
167
  ```
124
168
 
125
- Pair the loader with your existing TypeScript/JSX transpiler (SWC, Babel, Rspack’s builtin loader, etc.) so regular React components and the tagged templates can live side by side. The demo fixture under `test/fixtures/rspack-app` shows a full setup that mixes Lit and React, and there is also a standalone walkthrough at [morganney/jsx-loader-demo](https://github.com/morganney/jsx-loader-demo):
126
-
127
- ```sh
128
- npm run build
129
- npm run setup:wasm
130
- npm run build:fixture
131
- ```
132
-
133
- Then point a static server at the fixture root (which serves `index.html` plus the bundled `dist/hybrid.js` and `dist/reactMode.js`) to see it in a browser:
134
-
135
- ```sh
136
- # Serve the rspack fixture from the repo root
137
- npx http-server test/fixtures/rspack-app -p 4173
138
- ```
139
-
140
- Visit `http://localhost:4173` (or whichever port you pick) to interact with both the Lit + React hybrid demo and the React-mode bundle.
169
+ Pair the loader with your existing TypeScript/JSX transpiler (SWC, Babel, Rspack’s builtin loader, etc.) so regular React components and the tagged templates can live side by side.
141
170
 
142
- Need a deeper dive into loader behavior and options? Check out [`src/loader/README.md`](src/loader/README.md) for a full walkthrough.
171
+ Need a deeper dive into loader behavior and options? Check out [`src/loader/README.md`](src/loader/README.md). There is also a standalone walkthrough at [morganney/jsx-loader-demo](https://github.com/morganney/jsx-loader-demo).
143
172
 
144
173
  ## Node / SSR usage
145
174
 
@@ -175,126 +204,7 @@ console.log(shell.outerHTML)
175
204
 
176
205
  This repository ships a ready-to-run fixture under `test/fixtures/node-ssr` that uses the Node entry to render a Lit shell plus a React subtree through `ReactDOMServer.renderToString`. Run `npm run build` once to emit `dist/`, then execute `npm run demo:node-ssr` to log the generated markup.
177
206
 
178
- ## Next.js integration
179
-
180
- > [!IMPORTANT]
181
- > Next already compiles `.tsx/.jsx` files, so you do not need this helper to author regular components. The loader only adds value when you want to reuse the tagged template runtime during SSR—mixing DOM nodes built by `jsx` with React markup, rendering shared utilities on the server, or processing tagged templates outside the usual component pipeline.
182
-
183
- Next (and Remix/other Webpack-based SSR stacks) can run the loader by adding a post-loader to the framework config so the template tags are rewritten after SWC/Babel transpilation. The fixture under `test/fixtures/next-app` ships a complete example that mixes DOM and React helpers during SSR so you can pre-render DOM snippets (for emails, HTML streams, CMS content, etc.) while still returning React components from your pages. The important bits live in `next.config.mjs`:
184
-
185
- ```js
186
- import path from 'node:path'
187
- import { fileURLToPath } from 'node:url'
188
-
189
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
190
- const repoRoot = path.resolve(__dirname, '../../..')
191
- const distDir = path.join(repoRoot, 'dist')
192
-
193
- export default {
194
- output: 'export',
195
- webpack(config) {
196
- config.resolve.alias = {
197
- ...(config.resolve.alias ?? {}),
198
- '@knighted/jsx': path.join(distDir, 'index.js'),
199
- '@knighted/jsx/react': path.join(distDir, 'react/index.js'),
200
- }
201
-
202
- config.module.rules.push({
203
- test: /\.[jt]sx?$/,
204
- include: path.join(__dirname, 'pages'),
205
- enforce: 'post',
206
- use: [{ loader: path.join(distDir, 'loader/jsx.js') }],
207
- })
208
-
209
- return config
210
- },
211
- }
212
- ```
213
-
214
- Inside `pages/index.tsx` you can freely mix the helpers. The snippet below uses `jsx` on the server to prebuild a DOM fragment and then injects that HTML alongside a normal React component on the client:
215
-
216
- ```ts
217
- import type { GetServerSideProps } from 'next'
218
- import { jsx } from '@knighted/jsx'
219
- import { reactJsx } from '@knighted/jsx/react'
220
-
221
- const buildDomShell = () =>
222
- jsx`
223
- <section data-kind="dom-runtime">
224
- <h2>DOM runtime</h2>
225
- <p>Rendered as static HTML on the server</p>
226
- </section>
227
- `
228
-
229
- export const getServerSideProps: GetServerSideProps = async () => {
230
- return {
231
- props: {
232
- domShell: buildDomShell().outerHTML,
233
- },
234
- }
235
- }
236
-
237
- const ReactBadge = () =>
238
- reactJsx`
239
- <button type="button">React badge</button>
240
- `
241
-
242
- type PageProps = { domShell: string }
243
-
244
- export default function Page({ domShell }: PageProps) {
245
- return reactJsx`
246
- <main>
247
- <${ReactBadge} />
248
- <div dangerouslySetInnerHTML={${{ __html: domShell }}}></div>
249
- </main>
250
- `
251
- }
252
- ```
253
-
254
- Build the fixture locally with `npx next build test/fixtures/next-app` (or run `npx vitest run test/next-fixture.test.ts`) to verify the integration end to end. You can adapt the same pattern in `app/` routes, API handlers, or server actions whenever you need DOM output generated by the tagged template runtime.
255
-
256
- ### Interpolations
257
-
258
- - All dynamic values are provided through standard template literal expressions (`${...}`) and map to JSX exactly where they appear. Interpolations used as text children no longer need an extra `{...}` wrapper—the runtime automatically recognizes placeholders inside text segments (so `Count is ${value}` just works). Use the usual JSX braces when the syntax requires them (`className={${value}}`, `{...props}`, conditionals, etc.).
259
- - Every expression can be any JavaScript value: primitives, arrays/iterables, DOM nodes, functions, other `jsx` results, or custom component references.
260
- - Async values (Promises) are not supported. Resolve them before passing into the template.
261
-
262
- ### Components
263
-
264
- You can inline components by interpolating the function used for the tag name. The component receives a props object plus the optional `children` prop and can return anything that `jsx` can render (DOM nodes, strings, fragments, other arrays, ...).
265
-
266
- ```ts
267
- const Button = ({ children, variant = 'primary' }) => {
268
- const el = document.createElement('button')
269
- el.dataset.variant = variant
270
- el.append(children ?? '')
271
- return el
272
- }
273
-
274
- const label = 'Tap me'
275
-
276
- const view = jsx`
277
- <section>
278
- <${Button} variant="ghost">
279
- ${label}
280
- </${Button}>
281
- </section>
282
- `
283
-
284
- document.body.append(view)
285
- ```
286
-
287
- ### Fragments & SVG
288
-
289
- Use JSX fragments (`<>...</>`) for multi-root templates. SVG trees automatically switch to the `http://www.w3.org/2000/svg` namespace once they enter an `<svg>` tag, and fall back inside `<foreignObject>`.
290
-
291
- ### DOM-specific props
292
-
293
- - `style` accepts either a string or an object. Object values handle CSS custom properties (`--token`) automatically.
294
- - `class` and `className` both work and can be strings or arrays.
295
- - Event handlers use the `on<Event>` naming convention (e.g. `onClick`).
296
- - `ref` supports callback refs as well as mutable `{ current }` objects.
297
- - `dangerouslySetInnerHTML` expects an object with an `__html` field, mirroring React.
207
+ See how to [integrate with Next.js](./docs/nextjs-integration.md).
298
208
 
299
209
  ## Browser usage
300
210
 
@@ -303,26 +213,26 @@ When you are not using a bundler, load the module directly from a CDN that under
303
213
  ```html
304
214
  <script type="module">
305
215
  import { jsx } from 'https://esm.sh/@knighted/jsx'
216
+ import { reactJsx } from 'https://esm.sh/@knighted/jsx/react'
217
+ import { useState } from 'https://esm.sh/react@19'
218
+ import { createRoot } from 'https://esm.sh/react-dom@19/client'
306
219
 
307
- const message = jsx`<p>Hello from the browser</p>`
308
- document.body.append(message)
309
- </script>
310
- ```
311
-
312
- If you are building locally with Vite/Rollup/Webpack make sure the WASM binding is installable so the bundler can resolve `@oxc-parser/binding-wasm32-wasi` (details below).
220
+ const reactMount = jsx`<div data-kind="react-mount" />`
313
221
 
314
- ### Installing the WASM binding locally
315
-
316
- `@oxc-parser/binding-wasm32-wasi` publishes with `"cpu": ["wasm32"]`, so npm/yarn/pnpm skip it on macOS and Linux unless you override the platform guard. Run the helper script after cloning (or whenever you clean `node_modules`) to pull the binding into place for the Vite demo and any other local bundler builds:
222
+ const CounterButton = () => {
223
+ const [count, setCount] = useState(0)
224
+ return reactJsx`
225
+ <button type="button" onClick={${() => setCount(value => value + 1)}}>
226
+ Count is ${count}
227
+ </button>
228
+ `
229
+ }
317
230
 
318
- ```sh
319
- npm run setup:wasm
231
+ document.body.append(reactMount)
232
+ createRoot(reactMount).render(reactJsx`<${CounterButton} />`)
233
+ </script>
320
234
  ```
321
235
 
322
- The script downloads the published tarball via `npm pack`, extracts it into `node_modules/@oxc-parser/binding-wasm32-wasi`, and removes the temporary archive so your lockfile stays untouched. If you need to test a different binding build, set `WASM_BINDING_PACKAGE` before running the script (for example, `WASM_BINDING_PACKAGE=@oxc-parser/binding-wasm32-wasi@0.100.0 npm run setup:wasm`).
323
-
324
- Prefer the manual route? You can still run `npm_config_ignore_platform=true npm install --no-save @oxc-parser/binding-wasm32-wasi@^0.99.0`, but the script above replicates the vendored behavior with less ceremony.
325
-
326
236
  ### Lite bundle entry
327
237
 
328
238
  If you already run this package through your own bundler you can trim a few extra kilobytes by importing the minified entries:
@@ -336,44 +246,6 @@ import { reactJsx as nodeReactJsx } from '@knighted/jsx/node/react/lite'
336
246
 
337
247
  Each lite subpath ships the same API as its standard counterpart but is pre-minified and scoped to just that runtime (DOM, React, Node DOM, or Node React). Swap them in when you want the smallest possible bundles; otherwise the default exports keep working as-is.
338
248
 
339
- ## Testing
340
-
341
- Looking for guidance on testing your own components with `jsx` or `reactJsx`? See
342
- [docs/testing.md](docs/testing.md) for DOM and React runtime examples. The commands
343
- below cover the library's internal test suites.
344
-
345
- Run the Vitest suite (powered by jsdom) to exercise the DOM runtime and component support:
346
-
347
- ```sh
348
- npm run test
349
- ```
350
-
351
- Tests live in `test/jsx.test.ts` and cover DOM props/events, custom components, fragments, and iterable children so you can see exactly how the template tag is meant to be used.
352
-
353
- Need full end-to-end coverage? The Playwright suite boots the CDN demo (`examples/esm-demo.html`) and the loader-backed Rspack fixture to verify nested trees, sibling structures, and interop with Lit/React:
354
-
355
- ```sh
356
- npm run test:e2e
357
- ```
358
-
359
- > [!NOTE]
360
- > The e2e script builds the library, installs the WASM parser binding, bundles the loader fixture, and then runs `playwright test`. Make sure Playwright browsers are installed locally (`npx playwright install --with-deps chromium`).
361
-
362
- ## Browser demo / Vite build
363
-
364
- This repo ships with a ready-to-run Vite demo under `examples/browser` that bundles the library (make sure you have installed the WASM binding via the command above first). Use it for a full end-to-end verification in a real browser (the demo imports `@knighted/jsx/lite` so you can confirm the lighter entry behaves identically):
365
-
366
- ```sh
367
- # Start a dev server at http://localhost:5173
368
- npm run dev
369
-
370
- # Produce a production Rollup build and preview it
371
- npm run build:demo
372
- npm run preview
373
- ```
374
-
375
- For a zero-build verification of the lite bundle, open `examples/esm-demo-lite.html` locally (double-click or run `open examples/esm-demo-lite.html`) or visit the deployed GitHub Pages build produced by `.github/workflows/deploy-demo.yml` (it serves that same lite HTML demo).
376
-
377
249
  ## Limitations
378
250
 
379
251
  - Requires a DOM-like environment (it throws when `document` is missing).
@@ -26,6 +26,21 @@ const tar_1 = require("tar");
26
26
  const DEFAULT_BINDING_SPEC = process.env.WASM_BINDING_PACKAGE ?? '@oxc-parser/binding-wasm32-wasi@^0.99.0';
27
27
  const RUNTIME_DEPS = ['@napi-rs/wasm-runtime', '@emnapi/runtime', '@emnapi/core'];
28
28
  const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
29
+ // Node emits a noisy ExperimentalWarning whenever the WASI shim loads; silence just that message.
30
+ const WASI_WARNING_SNIPPET = 'WASI is an experimental feature';
31
+ const LOADER_CONFIG_EXAMPLE = [
32
+ '// Example loader entry to drop into your bundler rules array:',
33
+ '{',
34
+ " loader: '@knighted/jsx/loader',",
35
+ ' options: {',
36
+ " tags: ['jsx', 'reactJsx'],",
37
+ ' tagModes: {',
38
+ " reactJsx: 'react',",
39
+ ' },',
40
+ ' },',
41
+ '}',
42
+ ].join('\n');
43
+ suppressExperimentalWasiWarning();
29
44
  function parseArgs(argv) {
30
45
  const options = {
31
46
  cwd: process.cwd(),
@@ -114,11 +129,11 @@ function ensurePackageJson(cwd) {
114
129
  throw new Error('No package.json found. Run this inside a project with package.json.');
115
130
  }
116
131
  }
117
- function runNpmPack(spec, cwd, dryRun, verbose) {
132
+ function runNpmPack(spec, cwd, dryRun, verbose, execFn = node_child_process_1.execFileSync) {
118
133
  logVerbose(`> npm pack ${spec}`, verbose);
119
134
  if (dryRun)
120
135
  return `${spec.replace(/\W+/g, '_')}.tgz`;
121
- const output = (0, node_child_process_1.execFileSync)('npm', ['pack', spec], {
136
+ const output = execFn('npm', ['pack', spec], {
122
137
  cwd,
123
138
  encoding: 'utf8',
124
139
  stdio: ['ignore', 'pipe', 'inherit'],
@@ -133,9 +148,9 @@ function parsePackageName(spec) {
133
148
  const [, name, version] = match;
134
149
  return { name, version };
135
150
  }
136
- async function installBinding(spec, cwd, dryRun, verbose) {
151
+ async function installBinding(spec, cwd, dryRun, verbose, pack = runNpmPack) {
137
152
  const { name, version } = parsePackageName(spec);
138
- const tarballName = runNpmPack(spec, cwd, dryRun, verbose);
153
+ const tarballName = pack(spec, cwd, dryRun, verbose);
139
154
  const tarballPath = node_path_1.default.resolve(cwd, tarballName);
140
155
  const targetDir = node_path_1.default.resolve(cwd, 'node_modules', ...name.split('/'));
141
156
  log(`> Installing ${spec} into ${targetDir}`);
@@ -147,7 +162,7 @@ async function installBinding(spec, cwd, dryRun, verbose) {
147
162
  }
148
163
  return { targetDir, tarballPath: dryRun ? undefined : tarballPath, name, version };
149
164
  }
150
- function installRuntimeDeps(pm, deps, cwd, dryRun, verbose) {
165
+ function installRuntimeDeps(pm, deps, cwd, dryRun, verbose, spawner = node_child_process_1.spawnSync) {
151
166
  const missing = deps.filter(dep => !isDependencyInstalled(dep, cwd));
152
167
  if (missing.length === 0) {
153
168
  log('> Runtime dependencies already present; skipping install');
@@ -162,7 +177,7 @@ function installRuntimeDeps(pm, deps, cwd, dryRun, verbose) {
162
177
  const [command, args] = commands[pm];
163
178
  logVerbose(`> ${command} ${args.join(' ')}`, verbose);
164
179
  if (!dryRun) {
165
- const result = (0, node_child_process_1.spawnSync)(command, args, { cwd, stdio: 'inherit' });
180
+ const result = spawner(command, args, { cwd, stdio: 'inherit' });
166
181
  if (result.status !== 0) {
167
182
  throw new Error(`Failed to install runtime dependencies with ${pm}`);
168
183
  }
@@ -194,11 +209,11 @@ function persistBindingSpec(cwd, name, version, dryRun, verbose) {
194
209
  node_fs_1.default.writeFileSync(pkgPath, `${JSON.stringify(pkgJson, null, 2)}\n`, 'utf8');
195
210
  }
196
211
  }
197
- async function verifyBinding(name, cwd, verbose) {
212
+ async function verifyBinding(name, cwd, verbose, importer = specifier => import(specifier)) {
198
213
  const requireFromCwd = (0, node_module_1.createRequire)(node_path_1.default.join(cwd, 'package.json'));
199
214
  const resolved = requireFromCwd.resolve(name);
200
215
  logVerbose(`> Resolved ${name} to ${resolved}`, verbose);
201
- const imported = await import((0, node_url_1.pathToFileURL)(resolved).href);
216
+ const imported = await importer((0, node_url_1.pathToFileURL)(resolved).href);
202
217
  if (!imported) {
203
218
  throw new Error(`Imported ${name} is empty; verification failed`);
204
219
  }
@@ -229,27 +244,30 @@ async function maybeHandleConfigPrompt(skipConfig, force) {
229
244
  return;
230
245
  }
231
246
  log('> Loader assistance is interactive and not applied automatically yet. See docs at docs/cli.md for next steps.');
247
+ log('> Example loader config (webpack / rspack):');
248
+ console.log(LOADER_CONFIG_EXAMPLE);
232
249
  }
233
- async function main() {
234
- const options = parseArgs(process.argv.slice(2));
235
- ensurePackageJson(options.cwd);
236
- const packageManager = detectPackageManager(options.cwd, options.packageManager);
237
- log(`> Using package manager: ${packageManager}`);
238
- const installedRuntimeDeps = installRuntimeDeps(packageManager, RUNTIME_DEPS, options.cwd, options.dryRun, options.verbose);
239
- const binding = await installBinding(options.wasmPackage, options.cwd, options.dryRun, options.verbose);
240
- persistBindingSpec(options.cwd, binding.name, binding.version, options.dryRun, options.verbose);
250
+ async function main(overrides = {}) {
251
+ const { parseArgs: parseArgsImpl = parseArgs, ensurePackageJson: ensurePackageJsonImpl = ensurePackageJson, detectPackageManager: detectPackageManagerImpl = detectPackageManager, installRuntimeDeps: installRuntimeDepsImpl = installRuntimeDeps, installBinding: installBindingImpl = installBinding, persistBindingSpec: persistBindingSpecImpl = persistBindingSpec, verifyBinding: verifyBindingImpl = verifyBinding, maybeHandleConfigPrompt: maybeHandleConfigPromptImpl = maybeHandleConfigPrompt, log: logImpl = log, } = overrides;
252
+ const options = parseArgsImpl(process.argv.slice(2));
253
+ ensurePackageJsonImpl(options.cwd);
254
+ const packageManager = detectPackageManagerImpl(options.cwd, options.packageManager);
255
+ logImpl(`> Using package manager: ${packageManager}`);
256
+ const installedRuntimeDeps = installRuntimeDepsImpl(packageManager, RUNTIME_DEPS, options.cwd, options.dryRun, options.verbose);
257
+ const binding = await installBindingImpl(options.wasmPackage, options.cwd, options.dryRun, options.verbose);
258
+ persistBindingSpecImpl(options.cwd, binding.name, binding.version, options.dryRun, options.verbose);
241
259
  let resolvedPath;
242
260
  if (!options.dryRun) {
243
- resolvedPath = await verifyBinding(binding.name, options.cwd, options.verbose);
244
- log(`> Verified ${binding.name} at ${resolvedPath}`);
261
+ resolvedPath = await verifyBindingImpl(binding.name, options.cwd, options.verbose);
262
+ logImpl(`> Verified ${binding.name} at ${resolvedPath}`);
245
263
  }
246
- await maybeHandleConfigPrompt(options.skipConfig, options.force);
247
- log('\nDone!');
248
- log(`- Binding: ${binding.name}${binding.version ? `@${binding.version}` : ''}`);
249
- log(`- Target: ${binding.targetDir}`);
250
- log(`- Runtime deps installed: ${installedRuntimeDeps.join(', ') || 'none (already present)'}`);
264
+ await maybeHandleConfigPromptImpl(options.skipConfig, options.force);
265
+ logImpl('\nDone!');
266
+ logImpl(`- Binding: ${binding.name}${binding.version ? `@${binding.version}` : ''}`);
267
+ logImpl(`- Target: ${binding.targetDir}`);
268
+ logImpl(`- Runtime deps installed: ${installedRuntimeDeps.join(', ') || 'none (already present)'}`);
251
269
  if (resolvedPath)
252
- log(`- Verified import: ${resolvedPath}`);
270
+ logImpl(`- Verified import: ${resolvedPath}`);
253
271
  }
254
272
  if (process.env.KNIGHTED_JSX_CLI_TEST !== '1') {
255
273
  main().catch(error => {
@@ -257,3 +275,17 @@ if (process.env.KNIGHTED_JSX_CLI_TEST !== '1') {
257
275
  process.exitCode = 1;
258
276
  });
259
277
  }
278
+ function suppressExperimentalWasiWarning() {
279
+ const originalEmitWarning = process.emitWarning.bind(process);
280
+ process.emitWarning = ((warning, ...args) => {
281
+ const [typeMaybe] = args;
282
+ const message = typeof warning === 'string' ? warning : warning.message;
283
+ const name = typeof warning === 'string' ? undefined : warning.name;
284
+ const type = typeof typeMaybe === 'string' ? typeMaybe : undefined;
285
+ if (message.includes(WASI_WARNING_SNIPPET) &&
286
+ (name === 'ExperimentalWarning' || type === 'ExperimentalWarning')) {
287
+ return;
288
+ }
289
+ return originalEmitWarning(warning, ...args);
290
+ });
291
+ }
@@ -1,3 +1,4 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
1
2
  declare const SUPPORTED_PACKAGE_MANAGERS: readonly ["npm", "pnpm", "yarn", "bun"];
2
3
  type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number];
3
4
  type CliOptions = {
@@ -10,9 +11,10 @@ type CliOptions = {
10
11
  wasmPackage: string;
11
12
  };
12
13
  declare function parseArgs(argv: string[]): CliOptions;
14
+ declare function log(message: string): void;
13
15
  declare function detectPackageManager(cwd: string, explicit?: PackageManager): PackageManager;
14
16
  declare function ensurePackageJson(cwd: string): void;
15
- declare function runNpmPack(spec: string, cwd: string, dryRun: boolean, verbose: boolean): string;
17
+ declare function runNpmPack(spec: string, cwd: string, dryRun: boolean, verbose: boolean, execFn?: typeof execFileSync): string;
16
18
  declare function parsePackageName(spec: string): {
17
19
  name: string;
18
20
  version: undefined;
@@ -20,17 +22,30 @@ declare function parsePackageName(spec: string): {
20
22
  name: string;
21
23
  version: string;
22
24
  };
23
- declare function installBinding(spec: string, cwd: string, dryRun: boolean, verbose: boolean): Promise<{
25
+ type PackFunction = (spec: string, cwd: string, dryRun: boolean, verbose: boolean) => string;
26
+ declare function installBinding(spec: string, cwd: string, dryRun: boolean, verbose: boolean, pack?: PackFunction): Promise<{
24
27
  targetDir: string;
25
28
  tarballPath?: string;
26
29
  name: string;
27
30
  version?: string;
28
31
  }>;
29
- declare function installRuntimeDeps(pm: PackageManager, deps: string[], cwd: string, dryRun: boolean, verbose: boolean): string[];
32
+ declare function installRuntimeDeps(pm: PackageManager, deps: string[], cwd: string, dryRun: boolean, verbose: boolean, spawner?: typeof spawnSync): string[];
30
33
  declare function isDependencyInstalled(dep: string, cwd: string): boolean;
31
34
  declare function persistBindingSpec(cwd: string, name: string, version: string | undefined, dryRun: boolean, verbose: boolean): void;
32
- declare function verifyBinding(name: string, cwd: string, verbose: boolean): Promise<string>;
35
+ type BindingImporter = (specifier: string) => Promise<unknown>;
36
+ declare function verifyBinding(name: string, cwd: string, verbose: boolean, importer?: BindingImporter): Promise<string>;
33
37
  declare function promptYesNo(prompt: string, defaultValue: boolean, force: boolean): Promise<boolean>;
34
38
  declare function maybeHandleConfigPrompt(skipConfig: boolean, force: boolean): Promise<void>;
35
- declare function main(): Promise<void>;
39
+ type MainDeps = {
40
+ parseArgs: typeof parseArgs;
41
+ ensurePackageJson: typeof ensurePackageJson;
42
+ detectPackageManager: typeof detectPackageManager;
43
+ installRuntimeDeps: typeof installRuntimeDeps;
44
+ installBinding: typeof installBinding;
45
+ persistBindingSpec: typeof persistBindingSpec;
46
+ verifyBinding: typeof verifyBinding;
47
+ maybeHandleConfigPrompt: typeof maybeHandleConfigPrompt;
48
+ log: typeof log;
49
+ };
50
+ declare function main(overrides?: Partial<MainDeps>): Promise<void>;
36
51
  export { parseArgs, detectPackageManager, ensurePackageJson, runNpmPack, parsePackageName, installBinding, installRuntimeDeps, isDependencyInstalled, persistBindingSpec, verifyBinding, promptYesNo, maybeHandleConfigPrompt, main, };
@@ -1,3 +1,4 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
1
2
  declare const SUPPORTED_PACKAGE_MANAGERS: readonly ["npm", "pnpm", "yarn", "bun"];
2
3
  type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number];
3
4
  type CliOptions = {
@@ -10,9 +11,10 @@ type CliOptions = {
10
11
  wasmPackage: string;
11
12
  };
12
13
  declare function parseArgs(argv: string[]): CliOptions;
14
+ declare function log(message: string): void;
13
15
  declare function detectPackageManager(cwd: string, explicit?: PackageManager): PackageManager;
14
16
  declare function ensurePackageJson(cwd: string): void;
15
- declare function runNpmPack(spec: string, cwd: string, dryRun: boolean, verbose: boolean): string;
17
+ declare function runNpmPack(spec: string, cwd: string, dryRun: boolean, verbose: boolean, execFn?: typeof execFileSync): string;
16
18
  declare function parsePackageName(spec: string): {
17
19
  name: string;
18
20
  version: undefined;
@@ -20,17 +22,30 @@ declare function parsePackageName(spec: string): {
20
22
  name: string;
21
23
  version: string;
22
24
  };
23
- declare function installBinding(spec: string, cwd: string, dryRun: boolean, verbose: boolean): Promise<{
25
+ type PackFunction = (spec: string, cwd: string, dryRun: boolean, verbose: boolean) => string;
26
+ declare function installBinding(spec: string, cwd: string, dryRun: boolean, verbose: boolean, pack?: PackFunction): Promise<{
24
27
  targetDir: string;
25
28
  tarballPath?: string;
26
29
  name: string;
27
30
  version?: string;
28
31
  }>;
29
- declare function installRuntimeDeps(pm: PackageManager, deps: string[], cwd: string, dryRun: boolean, verbose: boolean): string[];
32
+ declare function installRuntimeDeps(pm: PackageManager, deps: string[], cwd: string, dryRun: boolean, verbose: boolean, spawner?: typeof spawnSync): string[];
30
33
  declare function isDependencyInstalled(dep: string, cwd: string): boolean;
31
34
  declare function persistBindingSpec(cwd: string, name: string, version: string | undefined, dryRun: boolean, verbose: boolean): void;
32
- declare function verifyBinding(name: string, cwd: string, verbose: boolean): Promise<string>;
35
+ type BindingImporter = (specifier: string) => Promise<unknown>;
36
+ declare function verifyBinding(name: string, cwd: string, verbose: boolean, importer?: BindingImporter): Promise<string>;
33
37
  declare function promptYesNo(prompt: string, defaultValue: boolean, force: boolean): Promise<boolean>;
34
38
  declare function maybeHandleConfigPrompt(skipConfig: boolean, force: boolean): Promise<void>;
35
- declare function main(): Promise<void>;
39
+ type MainDeps = {
40
+ parseArgs: typeof parseArgs;
41
+ ensurePackageJson: typeof ensurePackageJson;
42
+ detectPackageManager: typeof detectPackageManager;
43
+ installRuntimeDeps: typeof installRuntimeDeps;
44
+ installBinding: typeof installBinding;
45
+ persistBindingSpec: typeof persistBindingSpec;
46
+ verifyBinding: typeof verifyBinding;
47
+ maybeHandleConfigPrompt: typeof maybeHandleConfigPrompt;
48
+ log: typeof log;
49
+ };
50
+ declare function main(overrides?: Partial<MainDeps>): Promise<void>;
36
51
  export { parseArgs, detectPackageManager, ensurePackageJson, runNpmPack, parsePackageName, installBinding, installRuntimeDeps, isDependencyInstalled, persistBindingSpec, verifyBinding, promptYesNo, maybeHandleConfigPrompt, main, };
package/dist/cli/init.js CHANGED
@@ -10,6 +10,20 @@ import { extract } from 'tar';
10
10
  var DEFAULT_BINDING_SPEC = process.env.WASM_BINDING_PACKAGE ?? "@oxc-parser/binding-wasm32-wasi@^0.99.0";
11
11
  var RUNTIME_DEPS = ["@napi-rs/wasm-runtime", "@emnapi/runtime", "@emnapi/core"];
12
12
  var SUPPORTED_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
13
+ var WASI_WARNING_SNIPPET = "WASI is an experimental feature";
14
+ var LOADER_CONFIG_EXAMPLE = [
15
+ "// Example loader entry to drop into your bundler rules array:",
16
+ "{",
17
+ " loader: '@knighted/jsx/loader',",
18
+ " options: {",
19
+ " tags: ['jsx', 'reactJsx'],",
20
+ " tagModes: {",
21
+ " reactJsx: 'react',",
22
+ " },",
23
+ " },",
24
+ "}"
25
+ ].join("\n");
26
+ suppressExperimentalWasiWarning();
13
27
  function parseArgs(argv) {
14
28
  const options = {
15
29
  cwd: process.cwd(),
@@ -97,10 +111,10 @@ function ensurePackageJson(cwd) {
97
111
  throw new Error("No package.json found. Run this inside a project with package.json.");
98
112
  }
99
113
  }
100
- function runNpmPack(spec, cwd, dryRun, verbose) {
114
+ function runNpmPack(spec, cwd, dryRun, verbose, execFn = execFileSync) {
101
115
  logVerbose(`> npm pack ${spec}`, verbose);
102
116
  if (dryRun) return `${spec.replace(/\W+/g, "_")}.tgz`;
103
- const output = execFileSync("npm", ["pack", spec], {
117
+ const output = execFn("npm", ["pack", spec], {
104
118
  cwd,
105
119
  encoding: "utf8",
106
120
  stdio: ["ignore", "pipe", "inherit"]
@@ -114,9 +128,9 @@ function parsePackageName(spec) {
114
128
  const [, name, version] = match;
115
129
  return { name, version };
116
130
  }
117
- async function installBinding(spec, cwd, dryRun, verbose) {
131
+ async function installBinding(spec, cwd, dryRun, verbose, pack = runNpmPack) {
118
132
  const { name, version } = parsePackageName(spec);
119
- const tarballName = runNpmPack(spec, cwd, dryRun, verbose);
133
+ const tarballName = pack(spec, cwd, dryRun, verbose);
120
134
  const tarballPath = path.resolve(cwd, tarballName);
121
135
  const targetDir = path.resolve(cwd, "node_modules", ...name.split("/"));
122
136
  log(`> Installing ${spec} into ${targetDir}`);
@@ -128,7 +142,7 @@ async function installBinding(spec, cwd, dryRun, verbose) {
128
142
  }
129
143
  return { targetDir, tarballPath: dryRun ? void 0 : tarballPath, name, version };
130
144
  }
131
- function installRuntimeDeps(pm, deps, cwd, dryRun, verbose) {
145
+ function installRuntimeDeps(pm, deps, cwd, dryRun, verbose, spawner = spawnSync) {
132
146
  const missing = deps.filter((dep) => !isDependencyInstalled(dep, cwd));
133
147
  if (missing.length === 0) {
134
148
  log("> Runtime dependencies already present; skipping install");
@@ -143,7 +157,7 @@ function installRuntimeDeps(pm, deps, cwd, dryRun, verbose) {
143
157
  const [command, args] = commands[pm];
144
158
  logVerbose(`> ${command} ${args.join(" ")}`, verbose);
145
159
  if (!dryRun) {
146
- const result = spawnSync(command, args, { cwd, stdio: "inherit" });
160
+ const result = spawner(command, args, { cwd, stdio: "inherit" });
147
161
  if (result.status !== 0) {
148
162
  throw new Error(`Failed to install runtime dependencies with ${pm}`);
149
163
  }
@@ -175,11 +189,11 @@ function persistBindingSpec(cwd, name, version, dryRun, verbose) {
175
189
  `, "utf8");
176
190
  }
177
191
  }
178
- async function verifyBinding(name, cwd, verbose) {
192
+ async function verifyBinding(name, cwd, verbose, importer = (specifier) => import(specifier)) {
179
193
  const requireFromCwd = createRequire(path.join(cwd, "package.json"));
180
194
  const resolved = requireFromCwd.resolve(name);
181
195
  logVerbose(`> Resolved ${name} to ${resolved}`, verbose);
182
- const imported = await import(pathToFileURL(resolved).href);
196
+ const imported = await importer(pathToFileURL(resolved).href);
183
197
  if (!imported) {
184
198
  throw new Error(`Imported ${name} is empty; verification failed`);
185
199
  }
@@ -213,26 +227,39 @@ async function maybeHandleConfigPrompt(skipConfig, force) {
213
227
  log(
214
228
  "> Loader assistance is interactive and not applied automatically yet. See docs at docs/cli.md for next steps."
215
229
  );
230
+ log("> Example loader config (webpack / rspack):");
231
+ console.log(LOADER_CONFIG_EXAMPLE);
216
232
  }
217
- async function main() {
218
- const options = parseArgs(process.argv.slice(2));
219
- ensurePackageJson(options.cwd);
220
- const packageManager = detectPackageManager(options.cwd, options.packageManager);
221
- log(`> Using package manager: ${packageManager}`);
222
- const installedRuntimeDeps = installRuntimeDeps(
233
+ async function main(overrides = {}) {
234
+ const {
235
+ parseArgs: parseArgsImpl = parseArgs,
236
+ ensurePackageJson: ensurePackageJsonImpl = ensurePackageJson,
237
+ detectPackageManager: detectPackageManagerImpl = detectPackageManager,
238
+ installRuntimeDeps: installRuntimeDepsImpl = installRuntimeDeps,
239
+ installBinding: installBindingImpl = installBinding,
240
+ persistBindingSpec: persistBindingSpecImpl = persistBindingSpec,
241
+ verifyBinding: verifyBindingImpl = verifyBinding,
242
+ maybeHandleConfigPrompt: maybeHandleConfigPromptImpl = maybeHandleConfigPrompt,
243
+ log: logImpl = log
244
+ } = overrides;
245
+ const options = parseArgsImpl(process.argv.slice(2));
246
+ ensurePackageJsonImpl(options.cwd);
247
+ const packageManager = detectPackageManagerImpl(options.cwd, options.packageManager);
248
+ logImpl(`> Using package manager: ${packageManager}`);
249
+ const installedRuntimeDeps = installRuntimeDepsImpl(
223
250
  packageManager,
224
251
  RUNTIME_DEPS,
225
252
  options.cwd,
226
253
  options.dryRun,
227
254
  options.verbose
228
255
  );
229
- const binding = await installBinding(
256
+ const binding = await installBindingImpl(
230
257
  options.wasmPackage,
231
258
  options.cwd,
232
259
  options.dryRun,
233
260
  options.verbose
234
261
  );
235
- persistBindingSpec(
262
+ persistBindingSpecImpl(
236
263
  options.cwd,
237
264
  binding.name,
238
265
  binding.version,
@@ -241,17 +268,17 @@ async function main() {
241
268
  );
242
269
  let resolvedPath;
243
270
  if (!options.dryRun) {
244
- resolvedPath = await verifyBinding(binding.name, options.cwd, options.verbose);
245
- log(`> Verified ${binding.name} at ${resolvedPath}`);
271
+ resolvedPath = await verifyBindingImpl(binding.name, options.cwd, options.verbose);
272
+ logImpl(`> Verified ${binding.name} at ${resolvedPath}`);
246
273
  }
247
- await maybeHandleConfigPrompt(options.skipConfig, options.force);
248
- log("\nDone!");
249
- log(`- Binding: ${binding.name}${binding.version ? `@${binding.version}` : ""}`);
250
- log(`- Target: ${binding.targetDir}`);
251
- log(
274
+ await maybeHandleConfigPromptImpl(options.skipConfig, options.force);
275
+ logImpl("\nDone!");
276
+ logImpl(`- Binding: ${binding.name}${binding.version ? `@${binding.version}` : ""}`);
277
+ logImpl(`- Target: ${binding.targetDir}`);
278
+ logImpl(
252
279
  `- Runtime deps installed: ${installedRuntimeDeps.join(", ") || "none (already present)"}`
253
280
  );
254
- if (resolvedPath) log(`- Verified import: ${resolvedPath}`);
281
+ if (resolvedPath) logImpl(`- Verified import: ${resolvedPath}`);
255
282
  }
256
283
  if (process.env.KNIGHTED_JSX_CLI_TEST !== "1") {
257
284
  main().catch((error) => {
@@ -262,5 +289,18 @@ if (process.env.KNIGHTED_JSX_CLI_TEST !== "1") {
262
289
  process.exitCode = 1;
263
290
  });
264
291
  }
292
+ function suppressExperimentalWasiWarning() {
293
+ const originalEmitWarning = process.emitWarning.bind(process);
294
+ process.emitWarning = ((warning, ...args) => {
295
+ const [typeMaybe] = args;
296
+ const message = typeof warning === "string" ? warning : warning.message;
297
+ const name = typeof warning === "string" ? void 0 : warning.name;
298
+ const type = typeof typeMaybe === "string" ? typeMaybe : void 0;
299
+ if (message.includes(WASI_WARNING_SNIPPET) && (name === "ExperimentalWarning" || type === "ExperimentalWarning")) {
300
+ return;
301
+ }
302
+ return originalEmitWarning(warning, ...args);
303
+ });
304
+ }
265
305
 
266
306
  export { detectPackageManager, ensurePackageJson, installBinding, installRuntimeDeps, isDependencyInstalled, main, maybeHandleConfigPrompt, parseArgs, parsePackageName, persistBindingSpec, promptYesNo, runNpmPack, verifyBinding };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/jsx",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Runtime JSX tagged template that renders DOM or React trees anywhere without a build step.",
5
5
  "keywords": [
6
6
  "jsx runtime",