@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.
- package/dist/cjs/loader/dom-template-builder.cjs +217 -0
- package/dist/cjs/loader/dom-template-builder.d.cts +12 -0
- package/dist/cjs/loader/helpers/dom-snippets.cjs +149 -0
- package/dist/cjs/loader/helpers/dom-snippets.d.cts +2 -0
- package/dist/cjs/loader/helpers/format-import-specifier.cjs +30 -0
- package/dist/cjs/loader/helpers/format-import-specifier.d.cts +13 -0
- package/dist/cjs/loader/helpers/materialize-slice.cjs +37 -0
- package/dist/cjs/loader/helpers/materialize-slice.d.cts +1 -0
- package/dist/cjs/loader/helpers/parse-range-key.cjs +14 -0
- package/dist/cjs/loader/helpers/parse-range-key.d.cts +1 -0
- package/dist/cjs/loader/helpers/rewrite-imports-without-tags.cjs +62 -0
- package/dist/cjs/loader/helpers/rewrite-imports-without-tags.d.cts +3 -0
- package/dist/cjs/loader/jsx.cjs +32 -33
- package/dist/cjs/loader/jsx.d.cts +1 -1
- package/dist/cjs/loader/modes.cjs +17 -0
- package/dist/cjs/loader/modes.d.cts +3 -0
- package/dist/cjs/runtime/shared.cjs +3 -13
- package/dist/cjs/shared/normalize-text.cjs +22 -0
- package/dist/cjs/shared/normalize-text.d.cts +1 -0
- package/dist/lite/debug/index.js +7 -7
- package/dist/lite/index.js +7 -7
- package/dist/lite/node/debug/index.js +7 -7
- package/dist/lite/node/index.js +7 -7
- package/dist/lite/node/react/index.js +5 -5
- package/dist/lite/react/index.js +5 -5
- package/dist/loader/dom-template-builder.d.ts +12 -0
- package/dist/loader/dom-template-builder.js +213 -0
- package/dist/loader/helpers/dom-snippets.d.ts +2 -0
- package/dist/loader/helpers/dom-snippets.js +146 -0
- package/dist/loader/helpers/format-import-specifier.d.ts +13 -0
- package/dist/loader/helpers/format-import-specifier.js +26 -0
- package/dist/loader/helpers/materialize-slice.d.ts +1 -0
- package/dist/loader/helpers/materialize-slice.js +33 -0
- package/dist/loader/helpers/parse-range-key.d.ts +1 -0
- package/dist/loader/helpers/parse-range-key.js +10 -0
- package/dist/loader/helpers/rewrite-imports-without-tags.d.ts +3 -0
- package/dist/loader/helpers/rewrite-imports-without-tags.js +58 -0
- package/dist/loader/jsx.d.ts +1 -1
- package/dist/loader/jsx.js +28 -29
- package/dist/loader/modes.d.ts +3 -0
- package/dist/loader/modes.js +13 -0
- package/dist/runtime/shared.js +3 -13
- package/dist/shared/normalize-text.d.ts +1 -0
- package/dist/shared/normalize-text.js +18 -0
- 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,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
|
+
};
|
package/dist/loader/jsx.d.ts
CHANGED
|
@@ -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`.
|
package/dist/loader/jsx.js
CHANGED
|
@@ -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
|
|
416
|
-
|
|
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 =
|
|
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)
|
package/dist/runtime/shared.js
CHANGED
|
@@ -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
|
|
74
|
-
|
|
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.
|
|
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.
|
|
111
|
+
"node": ">=22.21.1"
|
|
112
112
|
},
|
|
113
113
|
"engineStrict": true,
|
|
114
114
|
"scripts": {
|