@knighted/jsx 1.1.0 → 1.2.0-rc.1
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 +186 -8
- package/dist/cjs/jsx.cjs +18 -172
- package/dist/cjs/loader/jsx.cjs +23 -12
- package/dist/cjs/loader/jsx.d.cts +5 -0
- package/dist/cjs/node/bootstrap.cjs +83 -0
- package/dist/cjs/node/bootstrap.d.cts +1 -0
- package/dist/cjs/node/index.cjs +4 -0
- package/dist/cjs/node/index.d.cts +2 -0
- package/dist/cjs/node/react/index.cjs +1 -0
- package/dist/cjs/node/react/index.d.cts +2 -0
- package/dist/cjs/react/index.cjs +5 -0
- package/dist/cjs/react/index.d.cts +2 -0
- package/dist/cjs/react/react-jsx.cjs +142 -0
- package/dist/cjs/react/react-jsx.d.cts +5 -0
- package/dist/cjs/runtime/shared.cjs +169 -0
- package/dist/cjs/runtime/shared.d.cts +38 -0
- package/dist/jsx.js +11 -165
- package/dist/lite/index.js +3 -3
- package/dist/loader/jsx.d.ts +5 -0
- package/dist/loader/jsx.js +23 -12
- package/dist/node/bootstrap.d.ts +1 -0
- package/dist/node/bootstrap.js +83 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +4 -0
- package/dist/node/react/index.d.ts +2 -0
- package/dist/node/react/index.js +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +1 -0
- package/dist/react/react-jsx.d.ts +5 -0
- package/dist/react/react-jsx.js +138 -0
- package/dist/runtime/shared.d.ts +38 -0
- package/dist/runtime/shared.js +156 -0
- package/package.json +47 -8
package/README.md
CHANGED
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
[](https://codecov.io/gh/knightedcodemonkey/jsx)
|
|
5
5
|
[](https://www.npmjs.com/package/@knighted/jsx)
|
|
6
6
|
|
|
7
|
-
A runtime JSX template tag backed by the [`oxc-parser`](https://github.com/oxc-project/oxc) WebAssembly build. Use real JSX syntax directly inside template literals and turn the result into live DOM nodes (or values returned from your own components) without running a bundler.
|
|
7
|
+
A runtime JSX template tag backed by the [`oxc-parser`](https://github.com/oxc-project/oxc) WebAssembly build. Use real JSX syntax directly inside template literals and turn the result into live DOM nodes (or values returned from your own components) without running a bundler. One syntax works everywhere—browser scripts, SSR utilities, and bundler pipelines—no separate transpilation step required.
|
|
8
|
+
|
|
9
|
+
## Key features
|
|
10
|
+
|
|
11
|
+
- **Parse true JSX with no build step** – template literals go through `oxc-parser`, so fragments, spreads, and SVG namespaces all work as expected.
|
|
12
|
+
- **DOM + React runtimes** – choose `jsx` for DOM nodes or `reactJsx` for React elements, and mix them freely (even on the server).
|
|
13
|
+
- **Loader + SSR support** – ship tagged templates through Webpack/Rspack, Next.js, or plain Node by using the loader and the `@knighted/jsx/node` entry.
|
|
14
|
+
|
|
15
|
+
## Quick links
|
|
16
|
+
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [React runtime](#react-runtime-reactjsx)
|
|
19
|
+
- [Loader integration](#loader-integration)
|
|
20
|
+
- [Node / SSR usage](#node--ssr-usage)
|
|
21
|
+
- [Next.js integration](#nextjs-integration)
|
|
22
|
+
- [Browser usage](#browser-usage)
|
|
23
|
+
- [Testing & demos](#testing)
|
|
8
24
|
|
|
9
25
|
## Installation
|
|
10
26
|
|
|
@@ -15,6 +31,9 @@ npm install @knighted/jsx
|
|
|
15
31
|
> [!IMPORTANT]
|
|
16
32
|
> 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.
|
|
17
33
|
|
|
34
|
+
> [!NOTE]
|
|
35
|
+
> 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.
|
|
36
|
+
|
|
18
37
|
The parser automatically uses native bindings when it runs in Node.js. To enable the WASM binding for browser builds you also need the `@oxc-parser/binding-wasm32-wasi` package. Because npm enforces the `cpu: ["wasm32"]` flag you must opt into the install explicitly:
|
|
19
38
|
|
|
20
39
|
```sh
|
|
@@ -29,8 +48,11 @@ npm_config_ignore_platform=true npm install @oxc-parser/binding-wasm32-wasi
|
|
|
29
48
|
```ts
|
|
30
49
|
import { jsx } from '@knighted/jsx'
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
const handleClick = () =>
|
|
51
|
+
let count = 3
|
|
52
|
+
const handleClick = () => {
|
|
53
|
+
count += 1
|
|
54
|
+
console.log(`Count is now ${count}`)
|
|
55
|
+
}
|
|
34
56
|
|
|
35
57
|
const button = jsx`
|
|
36
58
|
<button className={${`counter-${count}`}} onClick={${handleClick}}>
|
|
@@ -41,9 +63,37 @@ const button = jsx`
|
|
|
41
63
|
document.body.append(button)
|
|
42
64
|
```
|
|
43
65
|
|
|
66
|
+
### React runtime (`reactJsx`)
|
|
67
|
+
|
|
68
|
+
Need to compose React elements instead of DOM nodes? Import the dedicated helper from the `@knighted/jsx/react` subpath (React 18+ and `react-dom` are still required to mount the tree):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { useState } from 'react'
|
|
72
|
+
import { reactJsx } from '@knighted/jsx/react'
|
|
73
|
+
import { createRoot } from 'react-dom/client'
|
|
74
|
+
|
|
75
|
+
const App = () => {
|
|
76
|
+
const [count, setCount] = useState(0)
|
|
77
|
+
|
|
78
|
+
return reactJsx`
|
|
79
|
+
<section className="react-demo">
|
|
80
|
+
<h2>Hello from React</h2>
|
|
81
|
+
<p>Count is {${count}}</p>
|
|
82
|
+
<button onClick={${() => setCount(value => value + 1)}}>
|
|
83
|
+
Increment
|
|
84
|
+
</button>
|
|
85
|
+
</section>
|
|
86
|
+
`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
createRoot(document.getElementById('root')!).render(reactJsx`<${App} />`)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
44
94
|
## Loader integration
|
|
45
95
|
|
|
46
|
-
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
|
|
96
|
+
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.
|
|
47
97
|
|
|
48
98
|
```js
|
|
49
99
|
// rspack.config.js / webpack.config.js
|
|
@@ -57,8 +107,9 @@ export default {
|
|
|
57
107
|
{
|
|
58
108
|
loader: '@knighted/jsx/loader',
|
|
59
109
|
options: {
|
|
60
|
-
//
|
|
61
|
-
tag: 'jsx',
|
|
110
|
+
// Both optional: restrict or rename the tagged templates.
|
|
111
|
+
// tag: 'jsx', // single-tag option
|
|
112
|
+
// tags: ['jsx', 'reactJsx'],
|
|
62
113
|
},
|
|
63
114
|
},
|
|
64
115
|
],
|
|
@@ -68,7 +119,134 @@ export default {
|
|
|
68
119
|
}
|
|
69
120
|
```
|
|
70
121
|
|
|
71
|
-
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
|
|
122
|
+
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:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
npm run build
|
|
126
|
+
npm run setup:wasm
|
|
127
|
+
npm run build:fixture
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then point a static server at the fixture root (which serves `index.html` and the bundled `dist/bundle.js`) to see it in a browser:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
# Serve the rspack fixture from the repo root
|
|
134
|
+
npx http-server test/fixtures/rspack-app -p 4173
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Visit `http://localhost:4173` (or whichever port you pick) to interact with the Lit + React demo.
|
|
138
|
+
|
|
139
|
+
## Node / SSR usage
|
|
140
|
+
|
|
141
|
+
Import the dedicated Node entry (`@knighted/jsx/node`) when you want to run the template tag inside bare Node.js. It automatically bootstraps a DOM shim by loading either `linkedom` or `jsdom` (install one of them to opt in) and then re-exports the usual helpers so you can keep authoring JSX in the same way:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { jsx } from '@knighted/jsx/node'
|
|
145
|
+
import { reactJsx } from '@knighted/jsx/node/react'
|
|
146
|
+
import { renderToString } from 'react-dom/server'
|
|
147
|
+
|
|
148
|
+
const Badge = ({ label }: { label: string }) =>
|
|
149
|
+
reactJsx`
|
|
150
|
+
<button type="button">React says: {${label}}</button>
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
const reactMarkup = renderToString(
|
|
154
|
+
reactJsx`
|
|
155
|
+
<${Badge} label={${'Server-only'}} />
|
|
156
|
+
`,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const shell = jsx`
|
|
160
|
+
<main>
|
|
161
|
+
<section dangerouslySetInnerHTML={${{ __html: reactMarkup }}}></section>
|
|
162
|
+
</main>
|
|
163
|
+
`
|
|
164
|
+
|
|
165
|
+
console.log(shell.outerHTML)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> [!NOTE]
|
|
169
|
+
> The Node entry tries `linkedom` first and falls back to `jsdom`. Install whichever shim you prefer (both are optional peer dependencies) and, if needed, set `KNIGHTED_JSX_NODE_SHIM=jsdom` or `linkedom` to force a specific one.
|
|
170
|
+
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
## Next.js integration
|
|
174
|
+
|
|
175
|
+
> [!IMPORTANT]
|
|
176
|
+
> 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.
|
|
177
|
+
|
|
178
|
+
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`:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import path from 'node:path'
|
|
182
|
+
import { fileURLToPath } from 'node:url'
|
|
183
|
+
|
|
184
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
185
|
+
const repoRoot = path.resolve(__dirname, '../../..')
|
|
186
|
+
const distDir = path.join(repoRoot, 'dist')
|
|
187
|
+
|
|
188
|
+
export default {
|
|
189
|
+
output: 'export',
|
|
190
|
+
webpack(config) {
|
|
191
|
+
config.resolve.alias = {
|
|
192
|
+
...(config.resolve.alias ?? {}),
|
|
193
|
+
'@knighted/jsx': path.join(distDir, 'index.js'),
|
|
194
|
+
'@knighted/jsx/react': path.join(distDir, 'react/index.js'),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
config.module.rules.push({
|
|
198
|
+
test: /\.[jt]sx?$/,
|
|
199
|
+
include: path.join(__dirname, 'pages'),
|
|
200
|
+
enforce: 'post',
|
|
201
|
+
use: [{ loader: path.join(distDir, 'loader/jsx.js') }],
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return config
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
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:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import type { GetServerSideProps } from 'next'
|
|
213
|
+
import { jsx } from '@knighted/jsx'
|
|
214
|
+
import { reactJsx } from '@knighted/jsx/react'
|
|
215
|
+
|
|
216
|
+
const buildDomShell = () =>
|
|
217
|
+
jsx`
|
|
218
|
+
<section data-kind="dom-runtime">
|
|
219
|
+
<h2>DOM runtime</h2>
|
|
220
|
+
<p>Rendered as static HTML on the server</p>
|
|
221
|
+
</section>
|
|
222
|
+
`
|
|
223
|
+
|
|
224
|
+
export const getServerSideProps: GetServerSideProps = async () => {
|
|
225
|
+
return {
|
|
226
|
+
props: {
|
|
227
|
+
domShell: buildDomShell().outerHTML,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const ReactBadge = () =>
|
|
233
|
+
reactJsx`
|
|
234
|
+
<button type="button">React badge</button>
|
|
235
|
+
`
|
|
236
|
+
|
|
237
|
+
type PageProps = { domShell: string }
|
|
238
|
+
|
|
239
|
+
export default function Page({ domShell }: PageProps) {
|
|
240
|
+
return reactJsx`
|
|
241
|
+
<main>
|
|
242
|
+
<${ReactBadge} />
|
|
243
|
+
<div dangerouslySetInnerHTML={${{ __html: domShell }}}></div>
|
|
244
|
+
</main>
|
|
245
|
+
`
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
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.
|
|
72
250
|
|
|
73
251
|
### Interpolations
|
|
74
252
|
|
|
@@ -188,7 +366,7 @@ Tradeoffs to keep in mind:
|
|
|
188
366
|
- **Parser vs tokenizer** – `htm` performs lightweight string tokenization, while `@knighted/jsx` pays a higher one-time parse cost but gains the full JSX grammar (fragments, spread children, nested namespaces) without heuristics. For large or deeply nested templates the WASM-backed parser is typically faster and more accurate than string slicing.
|
|
189
367
|
- **DOM-first rendering** – this runtime builds DOM nodes directly, so the cost after parsing is mostly attribute assignment and child insertion. `htm` usually feeds a virtual DOM/hyperscript factory (e.g., Preact’s `h`), which may add an extra abstraction layer before hitting the DOM.
|
|
190
368
|
- **Bundle size** – including the parser and WASM binding is heavier than `htm`’s ~1 kB tokenizer. If you just need hyperscript sugar, `htm` stays leaner; if you value real JSX semantics without a build step, the extra kilobytes buy you correctness and speed on complex trees.
|
|
191
|
-
- **Actual size** – the default `dist/jsx.js` bundle is ~
|
|
369
|
+
- **Actual size** – as of `v1.2.0-rc.1` the default `dist/jsx.js` bundle is ~9.0 kB raw / ~2.3 kB min+gzip, while the `@knighted/jsx/lite` entry stays ~5.7 kB raw / ~2.5 kB min+gzip. `htm` weighs in at roughly 0.7 kB min+gzip, so the lite entry narrows the gap to ~1.8 kB for production payloads.
|
|
192
370
|
|
|
193
371
|
In short, `@knighted/jsx` trades a slightly larger runtime for the ability to parse genuine JSX with native performance, whereas `htm` favors minimal footprint and hyperscript integration. Pick the tool that aligns with your rendering stack and performance envelope.
|
|
194
372
|
|
package/dist/cjs/jsx.cjs
CHANGED
|
@@ -2,57 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsx = void 0;
|
|
4
4
|
const oxc_parser_1 = require("oxc-parser");
|
|
5
|
-
const
|
|
6
|
-
const CLOSE_TAG_RE = /<\/\s*$/;
|
|
7
|
-
const PLACEHOLDER_PREFIX = '__KX_EXPR__';
|
|
8
|
-
let invocationCounter = 0;
|
|
9
|
-
const parserOptions = {
|
|
10
|
-
lang: 'jsx',
|
|
11
|
-
sourceType: 'module',
|
|
12
|
-
range: true,
|
|
13
|
-
preserveParens: true,
|
|
14
|
-
};
|
|
5
|
+
const shared_js_1 = require("./runtime/shared.cjs");
|
|
15
6
|
const ensureDomAvailable = () => {
|
|
16
7
|
if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
|
|
17
8
|
throw new Error('The jsx template tag requires a DOM-like environment (document missing).');
|
|
18
9
|
}
|
|
19
10
|
};
|
|
20
|
-
const formatParserError = (error) => {
|
|
21
|
-
let message = `[oxc-parser] ${error.message}`;
|
|
22
|
-
if (error.labels?.length) {
|
|
23
|
-
const label = error.labels[0];
|
|
24
|
-
if (label.message) {
|
|
25
|
-
message += `\n${label.message}`;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (error.codeframe) {
|
|
29
|
-
message += `\n${error.codeframe}`;
|
|
30
|
-
}
|
|
31
|
-
return message;
|
|
32
|
-
};
|
|
33
|
-
const extractRootNode = (program) => {
|
|
34
|
-
for (const statement of program.body) {
|
|
35
|
-
if (statement.type === 'ExpressionStatement') {
|
|
36
|
-
const expression = statement.expression;
|
|
37
|
-
if (expression.type === 'JSXElement' || expression.type === 'JSXFragment') {
|
|
38
|
-
return expression;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
throw new Error('The jsx template must contain a single JSX element or fragment.');
|
|
43
|
-
};
|
|
44
|
-
const getIdentifierName = (identifier) => {
|
|
45
|
-
switch (identifier.type) {
|
|
46
|
-
case 'JSXIdentifier':
|
|
47
|
-
return identifier.name;
|
|
48
|
-
case 'JSXNamespacedName':
|
|
49
|
-
return `${identifier.namespace.name}:${identifier.name.name}`;
|
|
50
|
-
case 'JSXMemberExpression':
|
|
51
|
-
return `${getIdentifierName(identifier.object)}.${identifier.property.name}`;
|
|
52
|
-
default:
|
|
53
|
-
return '';
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
11
|
const isNodeLike = (value) => {
|
|
57
12
|
if (typeof Node === 'undefined') {
|
|
58
13
|
return false;
|
|
@@ -71,11 +26,6 @@ const isPromiseLike = (value) => {
|
|
|
71
26
|
}
|
|
72
27
|
return typeof value.then === 'function';
|
|
73
28
|
};
|
|
74
|
-
const normalizeJsxText = (value) => {
|
|
75
|
-
const collapsed = value.replace(/\r/g, '').replace(/\n\s+/g, ' ');
|
|
76
|
-
const trimmed = collapsed.trim();
|
|
77
|
-
return trimmed.length > 0 ? trimmed : '';
|
|
78
|
-
};
|
|
79
29
|
const setDomProp = (element, name, value) => {
|
|
80
30
|
if (value === false || value === null || value === undefined) {
|
|
81
31
|
return;
|
|
@@ -165,17 +115,18 @@ const appendChildValue = (parent, value) => {
|
|
|
165
115
|
}
|
|
166
116
|
parent.appendChild(document.createTextNode(String(value)));
|
|
167
117
|
};
|
|
168
|
-
const
|
|
118
|
+
const evaluateExpressionWithNamespace = (expression, ctx, namespace) => (0, shared_js_1.evaluateExpression)(expression, ctx, node => evaluateJsxNode(node, ctx, namespace));
|
|
119
|
+
const resolveAttributes = (attributes, ctx, namespace) => {
|
|
169
120
|
const props = {};
|
|
170
121
|
attributes.forEach(attribute => {
|
|
171
122
|
if (attribute.type === 'JSXSpreadAttribute') {
|
|
172
|
-
const spreadValue =
|
|
123
|
+
const spreadValue = evaluateExpressionWithNamespace(attribute.argument, ctx, namespace);
|
|
173
124
|
if (spreadValue && typeof spreadValue === 'object' && !Array.isArray(spreadValue)) {
|
|
174
125
|
Object.assign(props, spreadValue);
|
|
175
126
|
}
|
|
176
127
|
return;
|
|
177
128
|
}
|
|
178
|
-
const name = getIdentifierName(attribute.name);
|
|
129
|
+
const name = (0, shared_js_1.getIdentifierName)(attribute.name);
|
|
179
130
|
if (!attribute.value) {
|
|
180
131
|
props[name] = true;
|
|
181
132
|
return;
|
|
@@ -188,13 +139,13 @@ const resolveAttributes = (attributes, ctx) => {
|
|
|
188
139
|
if (attribute.value.expression.type === 'JSXEmptyExpression') {
|
|
189
140
|
return;
|
|
190
141
|
}
|
|
191
|
-
props[name] =
|
|
142
|
+
props[name] = evaluateExpressionWithNamespace(attribute.value.expression, ctx, namespace);
|
|
192
143
|
}
|
|
193
144
|
});
|
|
194
145
|
return props;
|
|
195
146
|
};
|
|
196
|
-
const applyDomAttributes = (element, attributes, ctx) => {
|
|
197
|
-
const props = resolveAttributes(attributes, ctx);
|
|
147
|
+
const applyDomAttributes = (element, attributes, ctx, namespace) => {
|
|
148
|
+
const props = resolveAttributes(attributes, ctx, namespace);
|
|
198
149
|
Object.entries(props).forEach(([name, value]) => {
|
|
199
150
|
if (name === 'key') {
|
|
200
151
|
return;
|
|
@@ -211,7 +162,7 @@ const evaluateJsxChildren = (children, ctx, namespace) => {
|
|
|
211
162
|
children.forEach(child => {
|
|
212
163
|
switch (child.type) {
|
|
213
164
|
case 'JSXText': {
|
|
214
|
-
const text = normalizeJsxText(child.value);
|
|
165
|
+
const text = (0, shared_js_1.normalizeJsxText)(child.value);
|
|
215
166
|
if (text) {
|
|
216
167
|
resolved.push(text);
|
|
217
168
|
}
|
|
@@ -221,11 +172,11 @@ const evaluateJsxChildren = (children, ctx, namespace) => {
|
|
|
221
172
|
if (child.expression.type === 'JSXEmptyExpression') {
|
|
222
173
|
break;
|
|
223
174
|
}
|
|
224
|
-
resolved.push(
|
|
175
|
+
resolved.push(evaluateExpressionWithNamespace(child.expression, ctx, namespace));
|
|
225
176
|
break;
|
|
226
177
|
}
|
|
227
178
|
case 'JSXSpreadChild': {
|
|
228
|
-
const spreadValue =
|
|
179
|
+
const spreadValue = evaluateExpressionWithNamespace(child.expression, ctx, namespace);
|
|
229
180
|
if (spreadValue !== undefined && spreadValue !== null) {
|
|
230
181
|
resolved.push(spreadValue);
|
|
231
182
|
}
|
|
@@ -241,7 +192,7 @@ const evaluateJsxChildren = (children, ctx, namespace) => {
|
|
|
241
192
|
return resolved;
|
|
242
193
|
};
|
|
243
194
|
const evaluateComponent = (element, ctx, component, namespace) => {
|
|
244
|
-
const props = resolveAttributes(element.openingElement.attributes, ctx);
|
|
195
|
+
const props = resolveAttributes(element.openingElement.attributes, ctx, namespace);
|
|
245
196
|
const childValues = evaluateJsxChildren(element.children, ctx, namespace);
|
|
246
197
|
if (childValues.length === 1) {
|
|
247
198
|
props.children = childValues[0];
|
|
@@ -257,7 +208,7 @@ const evaluateComponent = (element, ctx, component, namespace) => {
|
|
|
257
208
|
};
|
|
258
209
|
const evaluateJsxElement = (element, ctx, namespace) => {
|
|
259
210
|
const opening = element.openingElement;
|
|
260
|
-
const tagName = getIdentifierName(opening.name);
|
|
211
|
+
const tagName = (0, shared_js_1.getIdentifierName)(opening.name);
|
|
261
212
|
const component = ctx.components.get(tagName);
|
|
262
213
|
if (component) {
|
|
263
214
|
return evaluateComponent(element, ctx, component, namespace);
|
|
@@ -270,7 +221,7 @@ const evaluateJsxElement = (element, ctx, namespace) => {
|
|
|
270
221
|
const domElement = nextNamespace === 'svg'
|
|
271
222
|
? document.createElementNS('http://www.w3.org/2000/svg', tagName)
|
|
272
223
|
: document.createElement(tagName);
|
|
273
|
-
applyDomAttributes(domElement, opening.attributes, ctx);
|
|
224
|
+
applyDomAttributes(domElement, opening.attributes, ctx, nextNamespace);
|
|
274
225
|
const childValues = evaluateJsxChildren(element.children, ctx, childNamespace);
|
|
275
226
|
childValues.forEach(value => appendChildValue(domElement, value));
|
|
276
227
|
return domElement;
|
|
@@ -284,119 +235,14 @@ const evaluateJsxNode = (node, ctx, namespace) => {
|
|
|
284
235
|
}
|
|
285
236
|
return evaluateJsxElement(node, ctx, namespace);
|
|
286
237
|
};
|
|
287
|
-
const walkAst = (node, visitor) => {
|
|
288
|
-
if (!node || typeof node !== 'object') {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const candidate = node;
|
|
292
|
-
if (typeof candidate.type !== 'string') {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
visitor(candidate);
|
|
296
|
-
Object.values(candidate).forEach(value => {
|
|
297
|
-
if (!value) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (Array.isArray(value)) {
|
|
301
|
-
value.forEach(child => walkAst(child, visitor));
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (typeof value === 'object') {
|
|
305
|
-
walkAst(value, visitor);
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
const collectPlaceholderNames = (expression, ctx) => {
|
|
310
|
-
const placeholders = new Set();
|
|
311
|
-
walkAst(expression, node => {
|
|
312
|
-
if (node.type === 'Identifier' && ctx.placeholders.has(node.name)) {
|
|
313
|
-
placeholders.add(node.name);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
return Array.from(placeholders);
|
|
317
|
-
};
|
|
318
|
-
const evaluateExpression = (expression, ctx) => {
|
|
319
|
-
if (expression.type === 'JSXElement' || expression.type === 'JSXFragment') {
|
|
320
|
-
return evaluateJsxNode(expression, ctx, null);
|
|
321
|
-
}
|
|
322
|
-
if (!('range' in expression) || !expression.range) {
|
|
323
|
-
throw new Error('Unable to evaluate expression: missing source range information.');
|
|
324
|
-
}
|
|
325
|
-
const [start, end] = expression.range;
|
|
326
|
-
const source = ctx.source.slice(start, end);
|
|
327
|
-
const placeholders = collectPlaceholderNames(expression, ctx);
|
|
328
|
-
try {
|
|
329
|
-
const evaluator = new Function(...placeholders, `"use strict"; return (${source});`);
|
|
330
|
-
const args = placeholders.map(name => ctx.placeholders.get(name));
|
|
331
|
-
return evaluator(...args);
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
throw new Error(`Failed to evaluate expression ${source}: ${error.message}`);
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
const sanitizeIdentifier = (value) => {
|
|
338
|
-
const cleaned = value.replace(/[^a-zA-Z0-9_$]/g, '');
|
|
339
|
-
if (!cleaned) {
|
|
340
|
-
return 'Component';
|
|
341
|
-
}
|
|
342
|
-
if (!/[A-Za-z_$]/.test(cleaned[0])) {
|
|
343
|
-
return `Component${cleaned}`;
|
|
344
|
-
}
|
|
345
|
-
return cleaned;
|
|
346
|
-
};
|
|
347
|
-
const ensureBinding = (value, bindings, bindingLookup) => {
|
|
348
|
-
const existing = bindingLookup.get(value);
|
|
349
|
-
if (existing) {
|
|
350
|
-
return existing;
|
|
351
|
-
}
|
|
352
|
-
const descriptor = value.displayName || value.name || `Component${bindings.length}`;
|
|
353
|
-
const baseName = sanitizeIdentifier(descriptor);
|
|
354
|
-
let candidate = baseName;
|
|
355
|
-
let suffix = 1;
|
|
356
|
-
while (bindings.some(binding => binding.name === candidate)) {
|
|
357
|
-
candidate = `${baseName}${suffix++}`;
|
|
358
|
-
}
|
|
359
|
-
const binding = { name: candidate, value };
|
|
360
|
-
bindings.push(binding);
|
|
361
|
-
bindingLookup.set(value, binding);
|
|
362
|
-
return binding;
|
|
363
|
-
};
|
|
364
|
-
const buildTemplate = (strings, values) => {
|
|
365
|
-
const raw = strings.raw ?? strings;
|
|
366
|
-
const placeholders = new Map();
|
|
367
|
-
const bindings = [];
|
|
368
|
-
const bindingLookup = new Map();
|
|
369
|
-
let source = raw[0] ?? '';
|
|
370
|
-
const templateId = invocationCounter++;
|
|
371
|
-
let placeholderIndex = 0;
|
|
372
|
-
for (let idx = 0; idx < values.length; idx++) {
|
|
373
|
-
const chunk = raw[idx] ?? '';
|
|
374
|
-
const nextChunk = raw[idx + 1] ?? '';
|
|
375
|
-
const value = values[idx];
|
|
376
|
-
const isTagNamePosition = OPEN_TAG_RE.test(chunk) || CLOSE_TAG_RE.test(chunk);
|
|
377
|
-
if (isTagNamePosition && typeof value === 'function') {
|
|
378
|
-
const binding = ensureBinding(value, bindings, bindingLookup);
|
|
379
|
-
source += binding.name + nextChunk;
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if (isTagNamePosition && typeof value === 'string') {
|
|
383
|
-
source += value + nextChunk;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
const placeholder = `${PLACEHOLDER_PREFIX}${templateId}_${placeholderIndex++}__`;
|
|
387
|
-
placeholders.set(placeholder, value);
|
|
388
|
-
source += placeholder + nextChunk;
|
|
389
|
-
}
|
|
390
|
-
return { source, placeholders, bindings };
|
|
391
|
-
};
|
|
392
238
|
const jsx = (templates, ...values) => {
|
|
393
239
|
ensureDomAvailable();
|
|
394
|
-
const build = buildTemplate(templates, values);
|
|
395
|
-
const result = (0, oxc_parser_1.parseSync)('inline.jsx', build.source, parserOptions);
|
|
240
|
+
const build = (0, shared_js_1.buildTemplate)(templates, values);
|
|
241
|
+
const result = (0, oxc_parser_1.parseSync)('inline.jsx', build.source, shared_js_1.parserOptions);
|
|
396
242
|
if (result.errors.length > 0) {
|
|
397
|
-
throw new Error(formatParserError(result.errors[0]));
|
|
243
|
+
throw new Error((0, shared_js_1.formatParserError)(result.errors[0]));
|
|
398
244
|
}
|
|
399
|
-
const root = extractRootNode(result.program);
|
|
245
|
+
const root = (0, shared_js_1.extractRootNode)(result.program);
|
|
400
246
|
const ctx = {
|
|
401
247
|
source: build.source,
|
|
402
248
|
placeholders: build.placeholders,
|
package/dist/cjs/loader/jsx.cjs
CHANGED
|
@@ -48,7 +48,7 @@ const TEMPLATE_PARSER_OPTIONS = {
|
|
|
48
48
|
range: true,
|
|
49
49
|
preserveParens: true,
|
|
50
50
|
};
|
|
51
|
-
const
|
|
51
|
+
const DEFAULT_TAGS = ['jsx', 'reactJsx'];
|
|
52
52
|
const escapeTemplateChunk = (chunk) => chunk.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${');
|
|
53
53
|
const formatParserError = (error) => {
|
|
54
54
|
let message = `[jsx-loader] ${error.message}`;
|
|
@@ -187,15 +187,15 @@ const transformTemplateLiteral = (templateSource, resourcePath) => {
|
|
|
187
187
|
const slots = collectSlots(result.program, templateSource);
|
|
188
188
|
return renderTemplateWithSlots(templateSource, slots);
|
|
189
189
|
};
|
|
190
|
-
const
|
|
190
|
+
const getTaggedTemplateName = (node) => {
|
|
191
191
|
if (node.type !== 'TaggedTemplateExpression') {
|
|
192
|
-
return
|
|
192
|
+
return null;
|
|
193
193
|
}
|
|
194
194
|
const tagNode = node.tag;
|
|
195
195
|
if (tagNode.type !== 'Identifier') {
|
|
196
|
-
return
|
|
196
|
+
return null;
|
|
197
197
|
}
|
|
198
|
-
return tagNode.name
|
|
198
|
+
return tagNode.name;
|
|
199
199
|
};
|
|
200
200
|
const TAG_PLACEHOLDER_PREFIX = '__JSX_LOADER_TAG_EXPR_';
|
|
201
201
|
const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
@@ -307,8 +307,9 @@ const transformSource = (source, config) => {
|
|
|
307
307
|
}
|
|
308
308
|
const taggedTemplates = [];
|
|
309
309
|
walkAst(ast.program, node => {
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
const tagName = getTaggedTemplateName(node);
|
|
311
|
+
if (tagName && config.tags.includes(tagName)) {
|
|
312
|
+
taggedTemplates.push({ node, tagName });
|
|
312
313
|
}
|
|
313
314
|
});
|
|
314
315
|
if (!taggedTemplates.length) {
|
|
@@ -317,10 +318,11 @@ const transformSource = (source, config) => {
|
|
|
317
318
|
const magic = new magic_string_1.default(source);
|
|
318
319
|
let mutated = false;
|
|
319
320
|
taggedTemplates
|
|
320
|
-
.sort((a, b) => b.start - a.start)
|
|
321
|
-
.forEach(
|
|
321
|
+
.sort((a, b) => b.node.start - a.node.start)
|
|
322
|
+
.forEach(entry => {
|
|
323
|
+
const { node, tagName } = entry;
|
|
322
324
|
const quasi = node.quasi;
|
|
323
|
-
const templateSource = buildTemplateSource(quasi.quasis, quasi.expressions, source,
|
|
325
|
+
const templateSource = buildTemplateSource(quasi.quasis, quasi.expressions, source, tagName);
|
|
324
326
|
const { code, changed } = transformTemplateLiteral(templateSource.source, config.resourcePath);
|
|
325
327
|
const restored = restoreTemplatePlaceholders(code, templateSource.placeholders);
|
|
326
328
|
const templateChanged = changed || templateSource.mutated;
|
|
@@ -338,11 +340,20 @@ function jsxLoader(input) {
|
|
|
338
340
|
const callback = this.async();
|
|
339
341
|
try {
|
|
340
342
|
const options = this.getOptions?.() ?? {};
|
|
341
|
-
const
|
|
343
|
+
const explicitTags = Array.isArray(options.tags)
|
|
344
|
+
? options.tags.filter((value) => typeof value === 'string' && value.length > 0)
|
|
345
|
+
: null;
|
|
346
|
+
const legacyTag = typeof options.tag === 'string' && options.tag.length > 0 ? options.tag : null;
|
|
347
|
+
const tagList = explicitTags?.length
|
|
348
|
+
? explicitTags
|
|
349
|
+
: legacyTag
|
|
350
|
+
? [legacyTag]
|
|
351
|
+
: DEFAULT_TAGS;
|
|
352
|
+
const tags = Array.from(new Set(tagList));
|
|
342
353
|
const source = typeof input === 'string' ? input : input.toString('utf8');
|
|
343
354
|
const output = transformSource(source, {
|
|
344
355
|
resourcePath: this.resourcePath,
|
|
345
|
-
|
|
356
|
+
tags,
|
|
346
357
|
});
|
|
347
358
|
callback(null, output);
|
|
348
359
|
}
|
|
@@ -7,8 +7,13 @@ type LoaderContext<TOptions> = {
|
|
|
7
7
|
type LoaderOptions = {
|
|
8
8
|
/**
|
|
9
9
|
* Name of the tagged template function. Defaults to `jsx`.
|
|
10
|
+
* Deprecated in favor of `tags`.
|
|
10
11
|
*/
|
|
11
12
|
tag?: string;
|
|
13
|
+
/**
|
|
14
|
+
* List of tagged template function names to transform. Defaults to `['jsx', 'reactJsx']`.
|
|
15
|
+
*/
|
|
16
|
+
tags?: string[];
|
|
12
17
|
};
|
|
13
18
|
export default function jsxLoader(this: LoaderContext<LoaderOptions>, input: string | Buffer): void;
|
|
14
19
|
export {};
|