@knighted/jsx 1.1.0 → 1.2.0-rc.0

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
@@ -15,6 +15,9 @@ npm install @knighted/jsx
15
15
  > [!IMPORTANT]
16
16
  > 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
17
 
18
+ > [!NOTE]
19
+ > 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.
20
+
18
21
  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
22
 
20
23
  ```sh
@@ -41,9 +44,29 @@ const button = jsx`
41
44
  document.body.append(button)
42
45
  ```
43
46
 
47
+ ### React runtime (`reactJsx`)
48
+
49
+ 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):
50
+
51
+ ```ts
52
+ import { reactJsx } from '@knighted/jsx/react'
53
+ import { createRoot } from 'react-dom/client'
54
+
55
+ const view = reactJsx`
56
+ <section className="react-demo">
57
+ <h2>Hello from React</h2>
58
+ <button onClick={${() => console.log('clicked!')}}>Tap me</button>
59
+ </section>
60
+ `
61
+
62
+ createRoot(document.getElementById('root')!).render(view)
63
+ ```
64
+
65
+ 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.
66
+
44
67
  ## Loader integration
45
68
 
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\`…\`` invocation, rebuilds the template with real JSX semantics, and hands back transformed source that can run in any environment.
69
+ 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
70
 
48
71
  ```js
49
72
  // rspack.config.js / webpack.config.js
@@ -57,8 +80,9 @@ export default {
57
80
  {
58
81
  loader: '@knighted/jsx/loader',
59
82
  options: {
60
- // Optional: rename the tagged template identifier (defaults to `jsx`).
61
- tag: 'jsx',
83
+ // Both optional: restrict or rename the tagged templates.
84
+ // tag: 'jsx', // single-tag option
85
+ // tags: ['jsx', 'reactJsx'],
62
86
  },
63
87
  },
64
88
  ],
@@ -68,7 +92,22 @@ export default {
68
92
  }
69
93
  ```
70
94
 
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—run `npm run build`, `npm run setup:wasm`, and `npm run build:fixture`, then serve the folder to see the output in a browser.
95
+ 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:
96
+
97
+ ```sh
98
+ npm run build
99
+ npm run setup:wasm
100
+ npm run build:fixture
101
+ ```
102
+
103
+ 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:
104
+
105
+ ```sh
106
+ # Serve the rspack fixture from the repo root
107
+ npx http-server test/fixtures/rspack-app -p 4173
108
+ ```
109
+
110
+ Visit `http://localhost:4173` (or whichever port you pick) to interact with the Lit + React demo.
72
111
 
73
112
  ### Interpolations
74
113
 
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 OPEN_TAG_RE = /<\s*$/;
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 resolveAttributes = (attributes, ctx) => {
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 = evaluateExpression(attribute.argument, ctx);
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] = evaluateExpression(attribute.value.expression, ctx);
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(evaluateExpression(child.expression, ctx));
175
+ resolved.push(evaluateExpressionWithNamespace(child.expression, ctx, namespace));
225
176
  break;
226
177
  }
227
178
  case 'JSXSpreadChild': {
228
- const spreadValue = evaluateExpression(child.expression, ctx);
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,
@@ -48,7 +48,7 @@ const TEMPLATE_PARSER_OPTIONS = {
48
48
  range: true,
49
49
  preserveParens: true,
50
50
  };
51
- const DEFAULT_TAG = 'jsx';
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 isTargetTaggedTemplate = (node, source, tag) => {
190
+ const getTaggedTemplateName = (node) => {
191
191
  if (node.type !== 'TaggedTemplateExpression') {
192
- return false;
192
+ return null;
193
193
  }
194
194
  const tagNode = node.tag;
195
195
  if (tagNode.type !== 'Identifier') {
196
- return false;
196
+ return null;
197
197
  }
198
- return tagNode.name === tag;
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
- if (isTargetTaggedTemplate(node, source, config.tag)) {
311
- taggedTemplates.push(node);
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(node => {
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, config.tag);
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 tag = options.tag ?? DEFAULT_TAG;
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
- tag,
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 {};
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reactJsx = void 0;
4
+ var react_jsx_js_1 = require("./react-jsx.cjs");
5
+ Object.defineProperty(exports, "reactJsx", { enumerable: true, get: function () { return react_jsx_js_1.reactJsx; } });
@@ -0,0 +1,2 @@
1
+ export { reactJsx } from './react-jsx.cjs';
2
+ export type { ReactJsxComponent } from './react-jsx.cjs';
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reactJsx = void 0;
4
+ const oxc_parser_1 = require("oxc-parser");
5
+ const shared_js_1 = require("../runtime/shared.cjs");
6
+ const react_1 = require("react");
7
+ const isIterable = (value) => {
8
+ if (!value || typeof value === 'string') {
9
+ return false;
10
+ }
11
+ return typeof value[Symbol.iterator] === 'function';
12
+ };
13
+ const isPromiseLike = (value) => {
14
+ if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
15
+ return false;
16
+ }
17
+ return typeof value.then === 'function';
18
+ };
19
+ const appendReactChild = (bucket, value) => {
20
+ if (value === null || value === undefined) {
21
+ return;
22
+ }
23
+ if (typeof value === 'boolean') {
24
+ return;
25
+ }
26
+ if (isPromiseLike(value)) {
27
+ throw new Error('Async values are not supported inside reactJsx template results.');
28
+ }
29
+ if (Array.isArray(value)) {
30
+ value.forEach(entry => appendReactChild(bucket, entry));
31
+ return;
32
+ }
33
+ if (isIterable(value)) {
34
+ for (const entry of value) {
35
+ appendReactChild(bucket, entry);
36
+ }
37
+ return;
38
+ }
39
+ bucket.push(value);
40
+ };
41
+ const evaluateExpressionForReact = (expression, ctx) => (0, shared_js_1.evaluateExpression)(expression, ctx, node => evaluateReactJsxNode(node, ctx));
42
+ const resolveAttributes = (attributes, ctx) => {
43
+ const props = {};
44
+ attributes.forEach(attribute => {
45
+ if (attribute.type === 'JSXSpreadAttribute') {
46
+ const spreadValue = evaluateExpressionForReact(attribute.argument, ctx);
47
+ if (spreadValue && typeof spreadValue === 'object' && !Array.isArray(spreadValue)) {
48
+ Object.assign(props, spreadValue);
49
+ }
50
+ return;
51
+ }
52
+ const name = (0, shared_js_1.getIdentifierName)(attribute.name);
53
+ if (!attribute.value) {
54
+ props[name] = true;
55
+ return;
56
+ }
57
+ if (attribute.value.type === 'Literal') {
58
+ props[name] = attribute.value.value;
59
+ return;
60
+ }
61
+ if (attribute.value.type === 'JSXExpressionContainer') {
62
+ if (attribute.value.expression.type === 'JSXEmptyExpression') {
63
+ return;
64
+ }
65
+ props[name] = evaluateExpressionForReact(attribute.value.expression, ctx);
66
+ }
67
+ });
68
+ return props;
69
+ };
70
+ const evaluateReactJsxChildren = (children, ctx) => {
71
+ const resolved = [];
72
+ children.forEach(child => {
73
+ switch (child.type) {
74
+ case 'JSXText': {
75
+ const text = (0, shared_js_1.normalizeJsxText)(child.value);
76
+ if (text) {
77
+ resolved.push(text);
78
+ }
79
+ break;
80
+ }
81
+ case 'JSXExpressionContainer': {
82
+ if (child.expression.type === 'JSXEmptyExpression') {
83
+ break;
84
+ }
85
+ appendReactChild(resolved, evaluateExpressionForReact(child.expression, ctx));
86
+ break;
87
+ }
88
+ case 'JSXSpreadChild': {
89
+ const spreadValue = evaluateExpressionForReact(child.expression, ctx);
90
+ if (spreadValue !== undefined && spreadValue !== null) {
91
+ appendReactChild(resolved, spreadValue);
92
+ }
93
+ break;
94
+ }
95
+ case 'JSXElement':
96
+ case 'JSXFragment': {
97
+ resolved.push(evaluateReactJsxNode(child, ctx));
98
+ break;
99
+ }
100
+ }
101
+ });
102
+ return resolved;
103
+ };
104
+ const createReactElement = (type, props, children) => {
105
+ return (0, react_1.createElement)(type, props, ...children);
106
+ };
107
+ const evaluateReactJsxElement = (element, ctx) => {
108
+ const opening = element.openingElement;
109
+ const tagName = (0, shared_js_1.getIdentifierName)(opening.name);
110
+ const component = ctx.components.get(tagName);
111
+ const props = resolveAttributes(opening.attributes, ctx);
112
+ const childValues = evaluateReactJsxChildren(element.children, ctx);
113
+ if (component) {
114
+ return createReactElement(component, props, childValues);
115
+ }
116
+ if (/[A-Z]/.test(tagName[0] ?? '')) {
117
+ throw new Error(`Unknown component "${tagName}". Did you interpolate it with the template literal?`);
118
+ }
119
+ return createReactElement(tagName, props, childValues);
120
+ };
121
+ const evaluateReactJsxNode = (node, ctx) => {
122
+ if (node.type === 'JSXFragment') {
123
+ const children = evaluateReactJsxChildren(node.children, ctx);
124
+ return (0, react_1.createElement)(react_1.Fragment, null, ...children);
125
+ }
126
+ return evaluateReactJsxElement(node, ctx);
127
+ };
128
+ const reactJsx = (templates, ...values) => {
129
+ const build = (0, shared_js_1.buildTemplate)(templates, values);
130
+ const result = (0, oxc_parser_1.parseSync)('inline.jsx', build.source, shared_js_1.parserOptions);
131
+ if (result.errors.length > 0) {
132
+ throw new Error((0, shared_js_1.formatParserError)(result.errors[0]));
133
+ }
134
+ const root = (0, shared_js_1.extractRootNode)(result.program);
135
+ const ctx = {
136
+ source: build.source,
137
+ placeholders: build.placeholders,
138
+ components: new Map(build.bindings.map(binding => [binding.name, binding.value])),
139
+ };
140
+ return evaluateReactJsxNode(root, ctx);
141
+ };
142
+ exports.reactJsx = reactJsx;
@@ -0,0 +1,5 @@
1
+ import { type ComponentType, type ReactElement, type ReactNode } from 'react';
2
+ export type ReactJsxComponent<Props = Record<string, unknown>> = ComponentType<Props & {
3
+ children?: ReactNode;
4
+ }>;
5
+ export declare const reactJsx: (templates: TemplateStringsArray, ...values: unknown[]) => ReactElement;