@knighted/jsx 1.6.3 → 1.7.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.
Files changed (45) hide show
  1. package/dist/cjs/loader/dom-template-builder.cjs +217 -0
  2. package/dist/cjs/loader/dom-template-builder.d.cts +12 -0
  3. package/dist/cjs/loader/helpers/dom-snippets.cjs +149 -0
  4. package/dist/cjs/loader/helpers/dom-snippets.d.cts +2 -0
  5. package/dist/cjs/loader/helpers/format-import-specifier.cjs +30 -0
  6. package/dist/cjs/loader/helpers/format-import-specifier.d.cts +13 -0
  7. package/dist/cjs/loader/helpers/materialize-slice.cjs +37 -0
  8. package/dist/cjs/loader/helpers/materialize-slice.d.cts +1 -0
  9. package/dist/cjs/loader/helpers/parse-range-key.cjs +14 -0
  10. package/dist/cjs/loader/helpers/parse-range-key.d.cts +1 -0
  11. package/dist/cjs/loader/helpers/rewrite-imports-without-tags.cjs +62 -0
  12. package/dist/cjs/loader/helpers/rewrite-imports-without-tags.d.cts +3 -0
  13. package/dist/cjs/loader/jsx.cjs +32 -33
  14. package/dist/cjs/loader/jsx.d.cts +1 -1
  15. package/dist/cjs/loader/modes.cjs +17 -0
  16. package/dist/cjs/loader/modes.d.cts +3 -0
  17. package/dist/cjs/runtime/shared.cjs +3 -13
  18. package/dist/cjs/shared/normalize-text.cjs +22 -0
  19. package/dist/cjs/shared/normalize-text.d.cts +1 -0
  20. package/dist/lite/debug/index.js +7 -7
  21. package/dist/lite/index.js +7 -7
  22. package/dist/lite/node/debug/index.js +7 -7
  23. package/dist/lite/node/index.js +7 -7
  24. package/dist/lite/node/react/index.js +5 -5
  25. package/dist/lite/react/index.js +5 -5
  26. package/dist/loader/dom-template-builder.d.ts +12 -0
  27. package/dist/loader/dom-template-builder.js +213 -0
  28. package/dist/loader/helpers/dom-snippets.d.ts +2 -0
  29. package/dist/loader/helpers/dom-snippets.js +146 -0
  30. package/dist/loader/helpers/format-import-specifier.d.ts +13 -0
  31. package/dist/loader/helpers/format-import-specifier.js +26 -0
  32. package/dist/loader/helpers/materialize-slice.d.ts +1 -0
  33. package/dist/loader/helpers/materialize-slice.js +33 -0
  34. package/dist/loader/helpers/parse-range-key.d.ts +1 -0
  35. package/dist/loader/helpers/parse-range-key.js +10 -0
  36. package/dist/loader/helpers/rewrite-imports-without-tags.d.ts +3 -0
  37. package/dist/loader/helpers/rewrite-imports-without-tags.js +58 -0
  38. package/dist/loader/jsx.d.ts +1 -1
  39. package/dist/loader/jsx.js +28 -29
  40. package/dist/loader/modes.d.ts +3 -0
  41. package/dist/loader/modes.js +13 -0
  42. package/dist/runtime/shared.js +3 -13
  43. package/dist/shared/normalize-text.d.ts +1 -0
  44. package/dist/shared/normalize-text.js +18 -0
  45. package/package.json +2 -2
@@ -0,0 +1,33 @@
1
+ import { parseRangeKey } from './parse-range-key.js';
2
+ export const materializeSlice = (start, end, source, replacements) => {
3
+ const exact = replacements.get(`${start}:${end}`);
4
+ if (exact !== undefined) {
5
+ return exact;
6
+ }
7
+ const nested = [];
8
+ replacements.forEach((code, key) => {
9
+ const range = parseRangeKey(key);
10
+ if (!range)
11
+ return;
12
+ const [rStart, rEnd] = range;
13
+ if (rStart >= start && rEnd <= end) {
14
+ nested.push({ start: rStart, end: rEnd, code });
15
+ }
16
+ });
17
+ if (!nested.length) {
18
+ return source.slice(start, end);
19
+ }
20
+ nested.sort((a, b) => a.start - b.start);
21
+ let cursor = start;
22
+ let output = '';
23
+ nested.forEach(entry => {
24
+ if (entry.start < cursor) {
25
+ throw new Error(`[jsx-loader] Overlapping replacement ranges detected (${entry.start}:${entry.end}) within ${start}:${end}. Nested replacements must not overlap.`);
26
+ }
27
+ output += source.slice(cursor, entry.start);
28
+ output += entry.code;
29
+ cursor = entry.end;
30
+ });
31
+ output += source.slice(cursor, end);
32
+ return output;
33
+ };
@@ -0,0 +1 @@
1
+ export declare const parseRangeKey: (key: string) => [number, number] | null;
@@ -0,0 +1,10 @@
1
+ export const parseRangeKey = (key) => {
2
+ const [start, end] = key.split(':').map(entry => Number.parseInt(entry, 10));
3
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
4
+ return null;
5
+ }
6
+ if (end < start) {
7
+ return null;
8
+ }
9
+ return [start, end];
10
+ };
@@ -0,0 +1,3 @@
1
+ import type MagicString from 'magic-string';
2
+ import type { Program } from '@oxc-project/types';
3
+ export declare const rewriteImportsWithoutTags: (program: Program, magic: MagicString, inlineTagNames: Set<string>, originalSource: string) => boolean;
@@ -0,0 +1,58 @@
1
+ import { formatImportSpecifier } from './format-import-specifier.js';
2
+ export const rewriteImportsWithoutTags = (program, magic, inlineTagNames, originalSource) => {
3
+ if (!inlineTagNames.size) {
4
+ return false;
5
+ }
6
+ let mutated = false;
7
+ program.body.forEach(node => {
8
+ if (node.type !== 'ImportDeclaration') {
9
+ return;
10
+ }
11
+ const specifiers = node.specifiers;
12
+ const kept = [];
13
+ let removed = false;
14
+ specifiers.forEach(spec => {
15
+ const localName = spec.local?.name;
16
+ if (!localName) {
17
+ kept.push(spec);
18
+ return;
19
+ }
20
+ const shouldDrop = inlineTagNames.has(localName);
21
+ if (shouldDrop) {
22
+ removed = true;
23
+ return;
24
+ }
25
+ kept.push(spec);
26
+ });
27
+ if (!removed) {
28
+ return;
29
+ }
30
+ if (!kept.length) {
31
+ magic.remove(node.start, node.end);
32
+ mutated = true;
33
+ return;
34
+ }
35
+ const keyword = node.importKind === 'type' ? 'import type' : 'import';
36
+ const bindings = [];
37
+ const defaultSpec = kept.find(spec => spec.type === 'ImportDefaultSpecifier');
38
+ const namespaceSpec = kept.find(spec => spec.type === 'ImportNamespaceSpecifier');
39
+ const namedSpecs = kept.filter(spec => spec.type === 'ImportSpecifier');
40
+ if (defaultSpec) {
41
+ bindings.push(formatImportSpecifier(defaultSpec));
42
+ }
43
+ if (namespaceSpec) {
44
+ bindings.push(formatImportSpecifier(namespaceSpec));
45
+ }
46
+ if (namedSpecs.length) {
47
+ bindings.push(`{ ${namedSpecs.map(formatImportSpecifier).join(', ')} }`);
48
+ }
49
+ const sourceLiteral = node.source;
50
+ const sourceText = sourceLiteral.raw
51
+ ? sourceLiteral.raw
52
+ : originalSource.slice(sourceLiteral.start ?? 0, sourceLiteral.end ?? 0);
53
+ const rewritten = `${keyword} ${bindings.join(', ')} from ${sourceText}`;
54
+ magic.overwrite(node.start, node.end, rewritten);
55
+ mutated = true;
56
+ });
57
+ return mutated;
58
+ };
@@ -1,4 +1,5 @@
1
1
  import { type SourceMap } from 'magic-string';
2
+ import { type LoaderMode } from './modes.js';
2
3
  type LoaderCallback = (error: Error | null, content?: string, map?: SourceMap | null) => void;
3
4
  type LoaderContext<TOptions> = {
4
5
  resourcePath: string;
@@ -7,7 +8,6 @@ type LoaderContext<TOptions> = {
7
8
  async(): LoaderCallback;
8
9
  getOptions?: () => Partial<TOptions>;
9
10
  };
10
- type LoaderMode = 'runtime' | 'react';
11
11
  type LoaderOptions = {
12
12
  /**
13
13
  * Name of the tagged template function. Defaults to `jsx`.
@@ -1,6 +1,11 @@
1
1
  import MagicString from 'magic-string';
2
2
  import { parseSync } from 'oxc-parser';
3
3
  import { formatTaggedTemplateParserError, } from '../internal/template-diagnostics.js';
4
+ import { compileDomTemplate, DOM_HELPER_SNIPPETS } from './dom-template-builder.js';
5
+ import { materializeSlice } from './helpers/materialize-slice.js';
6
+ import { rewriteImportsWithoutTags } from './helpers/rewrite-imports-without-tags.js';
7
+ import { DEFAULT_MODE, parseLoaderMode } from './modes.js';
8
+ import { normalizeJsxText } from '../shared/normalize-text.js';
4
9
  const createPlaceholderMap = (placeholders) => new Map(placeholders.map(entry => [entry.marker, entry.code]));
5
10
  class ReactTemplateBuilder {
6
11
  placeholderMap;
@@ -217,25 +222,13 @@ const TEMPLATE_PARSER_OPTIONS = {
217
222
  preserveParens: true,
218
223
  };
219
224
  const DEFAULT_TAGS = ['jsx', 'reactJsx'];
220
- const DEFAULT_MODE = 'runtime';
221
225
  const WEB_TARGETS = new Set(['web', 'webworker', 'electron-renderer', 'node-webkit']);
222
226
  const isWebTarget = (target) => target ? WEB_TARGETS.has(target) : false;
223
227
  const HELPER_SNIPPETS = {
224
228
  react: `const __jsxReactMergeProps = (...sources) => Object.assign({}, ...sources)
225
229
  const __jsxReact = (type, props, ...children) => React.createElement(type, props, ...children)
226
230
  `,
227
- };
228
- const parseLoaderMode = (value) => {
229
- if (typeof value !== 'string') {
230
- return null;
231
- }
232
- switch (value) {
233
- case 'runtime':
234
- case 'react':
235
- return value;
236
- default:
237
- return null;
238
- }
231
+ ...DOM_HELPER_SNIPPETS,
239
232
  };
240
233
  const escapeTemplateChunk = (chunk) => chunk.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${');
241
234
  const formatParserError = (error) => {
@@ -412,19 +405,8 @@ const extractJsxRoot = (program) => {
412
405
  throw new Error('[jsx-loader] Expected the template to contain a single JSX root node.');
413
406
  };
414
407
  const normalizeJsxTextSegments = (value, placeholders) => {
415
- const collapsed = value.replace(/\r/g, '').replace(/\n\s+/g, ' ');
416
- const leadingWhitespace = value.match(/^\s*/)?.[0] ?? '';
417
- const trailingWhitespace = value.match(/\s*$/)?.[0] ?? '';
418
- const trimStart = /\n/.test(leadingWhitespace);
419
- const trimEnd = /\n/.test(trailingWhitespace);
420
- let normalized = collapsed;
421
- if (trimStart) {
422
- normalized = normalized.replace(/^\s+/, '');
423
- }
424
- if (trimEnd) {
425
- normalized = normalized.replace(/\s+$/, '');
426
- }
427
- if (normalized.length === 0 || normalized.trim().length === 0) {
408
+ const normalized = normalizeJsxText(value);
409
+ if (!normalized) {
428
410
  return [];
429
411
  }
430
412
  const segments = [];
@@ -473,7 +455,7 @@ const materializeTemplateStrings = (quasis) => {
473
455
  });
474
456
  return templates;
475
457
  };
476
- const buildTemplateSource = (quasis, expressions, source, tag) => {
458
+ const buildTemplateSource = (quasis, expressions, source, tag, replacements) => {
477
459
  const placeholderMap = new Map();
478
460
  const tagPlaceholderMap = new Map();
479
461
  let template = '';
@@ -532,7 +514,7 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
532
514
  const nextValue = nextChunk?.value;
533
515
  const rightText = nextValue?.cooked ?? nextValue?.raw ?? '';
534
516
  const context = getTemplateExpressionContext(chunk, rightText);
535
- const code = source.slice(start, end);
517
+ const code = materializeSlice(start, end, source, replacements);
536
518
  const marker = registerMarker(code, context.type === 'tag');
537
519
  const appendMarker = (wrapper) => {
538
520
  const insertion = wrapper ? wrapper(marker) : marker;
@@ -629,13 +611,15 @@ const transformSource = (source, config, options) => {
629
611
  const magic = new MagicString(source);
630
612
  let mutated = false;
631
613
  const helperKinds = new Set();
614
+ const replacements = new Map();
615
+ const inlineTags = new Set();
632
616
  taggedTemplates
633
617
  .sort((a, b) => b.node.start - a.node.start)
634
618
  .forEach(entry => {
635
619
  const { node, tagName } = entry;
636
620
  const mode = config.tagModes.get(tagName) ?? DEFAULT_MODE;
637
621
  const quasi = node.quasi;
638
- const templateSource = buildTemplateSource(quasi.quasis, quasi.expressions, source, tagName);
622
+ const templateSource = buildTemplateSource(quasi.quasis, quasi.expressions, source, tagName, replacements);
639
623
  const templateStrings = materializeTemplateStrings(quasi.quasis);
640
624
  if (mode === 'runtime') {
641
625
  const { code, changed } = transformTemplateLiteral(templateSource.source, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
@@ -647,6 +631,7 @@ const transformSource = (source, config, options) => {
647
631
  const tagSource = source.slice(node.tag.start, node.tag.end);
648
632
  const replacement = `${tagSource}\`${restored}\``;
649
633
  magic.overwrite(node.start, node.end, replacement);
634
+ replacements.set(`${node.start}:${node.end}`, replacement);
650
635
  mutated = true;
651
636
  return;
652
637
  }
@@ -654,7 +639,18 @@ const transformSource = (source, config, options) => {
654
639
  const compiled = compileReactTemplate(templateSource.source, templateSource.placeholders, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
655
640
  helperKinds.add('react');
656
641
  magic.overwrite(node.start, node.end, compiled);
642
+ replacements.set(`${node.start}:${node.end}`, compiled);
657
643
  mutated = true;
644
+ inlineTags.add(tagName);
645
+ return;
646
+ }
647
+ if (mode === 'dom') {
648
+ const result = compileDomTemplate(templateSource.source, templateSource.placeholders, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
649
+ result.helpers.forEach(helper => helperKinds.add(helper));
650
+ magic.overwrite(node.start, node.end, result.code);
651
+ replacements.set(`${node.start}:${node.end}`, result.code);
652
+ mutated = true;
653
+ inlineTags.add(tagName);
658
654
  return;
659
655
  }
660
656
  /* c8 ignore next */
@@ -662,6 +658,9 @@ const transformSource = (source, config, options) => {
662
658
  // Modes are validated during option parsing; this fallback guards future extensions.
663
659
  throw new Error(`[jsx-loader] Transformation mode "${mode}" not implemented yet for tag "${tagName}".`);
664
660
  });
661
+ if (rewriteImportsWithoutTags(ast.program, magic, inlineTags, source)) {
662
+ mutated = true;
663
+ }
665
664
  const helperSource = Array.from(helperKinds)
666
665
  .map(kind => HELPER_SNIPPETS[kind])
667
666
  .filter(Boolean)
@@ -0,0 +1,3 @@
1
+ export type LoaderMode = 'runtime' | 'react' | 'dom';
2
+ export declare const DEFAULT_MODE: LoaderMode;
3
+ export declare const parseLoaderMode: (value: unknown) => LoaderMode | null;
@@ -0,0 +1,13 @@
1
+ export const DEFAULT_MODE = 'runtime';
2
+ export const parseLoaderMode = (value) => {
3
+ if (typeof value !== 'string')
4
+ return null;
5
+ switch (value) {
6
+ case 'runtime':
7
+ case 'react':
8
+ case 'dom':
9
+ return value;
10
+ default:
11
+ return null;
12
+ }
13
+ };
@@ -1,3 +1,4 @@
1
+ import { normalizeJsxText } from '../shared/normalize-text.js';
1
2
  export { formatTaggedTemplateParserError } from '../internal/template-diagnostics.js';
2
3
  const OPEN_TAG_RE = /<\s*$/;
3
4
  const CLOSE_TAG_RE = /<\/\s*$/;
@@ -70,19 +71,8 @@ export const walkAst = (node, visitor) => {
70
71
  });
71
72
  };
72
73
  export const normalizeJsxTextSegments = (value, placeholders) => {
73
- const collapsed = value.replace(/\r/g, '').replace(/\n\s+/g, ' ');
74
- const leadingWhitespace = value.match(/^\s*/)?.[0] ?? '';
75
- const trailingWhitespace = value.match(/\s*$/)?.[0] ?? '';
76
- const trimStart = /\n/.test(leadingWhitespace);
77
- const trimEnd = /\n/.test(trailingWhitespace);
78
- let normalized = collapsed;
79
- if (trimStart) {
80
- normalized = normalized.replace(/^\s+/, '');
81
- }
82
- if (trimEnd) {
83
- normalized = normalized.replace(/\s+$/, '');
84
- }
85
- if (normalized.length === 0 || normalized.trim().length === 0) {
74
+ const normalized = normalizeJsxText(value);
75
+ if (!normalized) {
86
76
  return [];
87
77
  }
88
78
  const segments = [];
@@ -0,0 +1 @@
1
+ export declare const normalizeJsxText: (value: string) => string | null;
@@ -0,0 +1,18 @@
1
+ export const normalizeJsxText = (value) => {
2
+ const collapsed = value.replace(/\r/g, '').replace(/\n\s+/g, ' ');
3
+ const leadingWhitespace = value.match(/^\s*/)?.[0] ?? '';
4
+ const trailingWhitespace = value.match(/\s*$/)?.[0] ?? '';
5
+ const trimStart = /\n/.test(leadingWhitespace);
6
+ const trimEnd = /\n/.test(trailingWhitespace);
7
+ let normalized = collapsed;
8
+ if (trimStart) {
9
+ normalized = normalized.replace(/^\s+/, '');
10
+ }
11
+ if (trimEnd) {
12
+ normalized = normalized.replace(/\s+$/, '');
13
+ }
14
+ if (normalized.length === 0 || normalized.trim().length === 0) {
15
+ return null;
16
+ }
17
+ return normalized;
18
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/jsx",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
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",
@@ -108,7 +108,7 @@
108
108
  "./package.json": "./package.json"
109
109
  },
110
110
  "engines": {
111
- "node": ">=22.17.0"
111
+ "node": ">=22.21.1"
112
112
  },
113
113
  "engineStrict": true,
114
114
  "scripts": {