@knighted/jsx 1.5.2 → 1.6.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 +1 -0
- package/dist/cjs/cli/init.cjs +15 -0
- package/dist/cjs/debug/diagnostics.cjs +57 -0
- package/dist/cjs/debug/diagnostics.d.cts +6 -0
- package/dist/cjs/debug/index.cjs +7 -0
- package/dist/cjs/debug/index.d.cts +2 -0
- package/dist/cjs/internal/attribute-resolution.cjs +22 -4
- package/dist/cjs/internal/attribute-resolution.d.cts +5 -0
- package/dist/cjs/internal/dev-environment.cjs +41 -0
- package/dist/cjs/internal/dev-environment.d.cts +4 -0
- package/dist/cjs/internal/event-bindings.cjs +11 -2
- package/dist/cjs/internal/event-bindings.d.cts +5 -1
- package/dist/cjs/internal/template-diagnostics.cjs +171 -0
- package/dist/cjs/internal/template-diagnostics.d.cts +13 -0
- package/dist/cjs/jsx.cjs +2 -2
- package/dist/cjs/loader/jsx.cjs +72 -20
- package/dist/cjs/loader/jsx.d.cts +6 -1
- package/dist/cjs/node/debug/index.cjs +6 -0
- package/dist/cjs/node/debug/index.d.cts +2 -0
- package/dist/cjs/react/react-jsx.cjs +1 -1
- package/dist/cjs/runtime/shared.cjs +41 -22
- package/dist/cjs/runtime/shared.d.cts +5 -2
- package/dist/cli/init.js +17 -0
- package/dist/debug/diagnostics.d.ts +6 -0
- package/dist/debug/diagnostics.js +52 -0
- package/dist/debug/index.d.ts +2 -0
- package/dist/debug/index.js +3 -0
- package/dist/internal/attribute-resolution.d.ts +5 -0
- package/dist/internal/attribute-resolution.js +20 -3
- package/dist/internal/dev-environment.d.ts +4 -0
- package/dist/internal/dev-environment.js +34 -0
- package/dist/internal/event-bindings.d.ts +5 -1
- package/dist/internal/event-bindings.js +9 -1
- package/dist/internal/template-diagnostics.d.ts +13 -0
- package/dist/internal/template-diagnostics.js +167 -0
- package/dist/jsx.js +3 -3
- package/dist/lite/debug/diagnostics.js +1 -0
- package/dist/lite/debug/index.js +8 -0
- package/dist/lite/index.js +8 -4
- package/dist/lite/node/debug/index.js +8 -0
- package/dist/lite/node/index.js +8 -4
- package/dist/lite/node/react/index.js +7 -3
- package/dist/lite/react/index.js +7 -3
- package/dist/loader/jsx.d.ts +6 -1
- package/dist/loader/jsx.js +72 -20
- package/dist/node/debug/index.d.ts +2 -0
- package/dist/node/debug/index.js +6 -0
- package/dist/react/react-jsx.js +2 -2
- package/dist/runtime/shared.d.ts +5 -2
- package/dist/runtime/shared.js +39 -21
- package/package.json +39 -7
package/dist/cjs/loader/jsx.cjs
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.default = jsxLoader;
|
|
7
7
|
const magic_string_1 = __importDefault(require("magic-string"));
|
|
8
8
|
const oxc_parser_1 = require("oxc-parser");
|
|
9
|
+
const template_diagnostics_js_1 = require("../internal/template-diagnostics.cjs");
|
|
9
10
|
const createPlaceholderMap = (placeholders) => new Map(placeholders.map(entry => [entry.marker, entry.code]));
|
|
10
11
|
class ReactTemplateBuilder {
|
|
11
12
|
placeholderMap;
|
|
@@ -383,10 +384,12 @@ const renderTemplateWithSlots = (source, slots) => {
|
|
|
383
384
|
output += escapeTemplateChunk(source.slice(cursor));
|
|
384
385
|
return { code: output, changed: slots.length > 0 };
|
|
385
386
|
};
|
|
386
|
-
const transformTemplateLiteral = (templateSource, resourcePath) => {
|
|
387
|
+
const transformTemplateLiteral = (templateSource, resourcePath, tagName, templates, diagnostics) => {
|
|
387
388
|
const result = (0, oxc_parser_1.parseSync)(`${resourcePath}?jsx-template`, templateSource, TEMPLATE_PARSER_OPTIONS);
|
|
388
389
|
if (result.errors.length > 0) {
|
|
389
|
-
throw new Error(
|
|
390
|
+
throw new Error((0, template_diagnostics_js_1.formatTaggedTemplateParserError)(tagName, templates, diagnostics, result.errors[0], {
|
|
391
|
+
label: 'jsx-loader',
|
|
392
|
+
}));
|
|
390
393
|
}
|
|
391
394
|
const slots = collectSlots(result.program, templateSource);
|
|
392
395
|
return renderTemplateWithSlots(templateSource, slots);
|
|
@@ -455,6 +458,25 @@ const normalizeJsxTextSegments = (value, placeholders) => {
|
|
|
455
458
|
return segments;
|
|
456
459
|
};
|
|
457
460
|
const TAG_PLACEHOLDER_PREFIX = '__JSX_LOADER_TAG_EXPR_';
|
|
461
|
+
const materializeTemplateStrings = (quasis) => {
|
|
462
|
+
const cooked = [];
|
|
463
|
+
const raw = [];
|
|
464
|
+
quasis.forEach(quasi => {
|
|
465
|
+
const value = quasi.value;
|
|
466
|
+
const cookedChunk = typeof value.cooked === 'string' ? value.cooked : (value.raw ?? '');
|
|
467
|
+
const rawChunk = typeof value.raw === 'string' ? value.raw : cookedChunk;
|
|
468
|
+
cooked.push(cookedChunk);
|
|
469
|
+
raw.push(rawChunk);
|
|
470
|
+
});
|
|
471
|
+
const templates = cooked;
|
|
472
|
+
Object.defineProperty(templates, 'raw', {
|
|
473
|
+
value: raw,
|
|
474
|
+
writable: false,
|
|
475
|
+
configurable: false,
|
|
476
|
+
enumerable: false,
|
|
477
|
+
});
|
|
478
|
+
return templates;
|
|
479
|
+
};
|
|
458
480
|
const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
459
481
|
const placeholderMap = new Map();
|
|
460
482
|
const tagPlaceholderMap = new Map();
|
|
@@ -462,6 +484,7 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
|
462
484
|
let placeholderIndex = 0;
|
|
463
485
|
let trimStartNext = 0;
|
|
464
486
|
let mutated = false;
|
|
487
|
+
const expressionRanges = [];
|
|
465
488
|
const registerMarker = (code, isTag) => {
|
|
466
489
|
if (isTag) {
|
|
467
490
|
const existing = tagPlaceholderMap.get(code);
|
|
@@ -477,6 +500,12 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
|
477
500
|
placeholderMap.set(marker, code);
|
|
478
501
|
return marker;
|
|
479
502
|
};
|
|
503
|
+
const appendInsertion = (expressionIndex, insertion) => {
|
|
504
|
+
const start = template.length;
|
|
505
|
+
template += insertion;
|
|
506
|
+
const end = template.length;
|
|
507
|
+
expressionRanges.push({ index: expressionIndex, sourceStart: start, sourceEnd: end });
|
|
508
|
+
};
|
|
480
509
|
quasis.forEach((quasi, index) => {
|
|
481
510
|
let chunk = quasi.value.cooked;
|
|
482
511
|
if (typeof chunk !== 'string') {
|
|
@@ -494,6 +523,7 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
|
494
523
|
if (!expression) {
|
|
495
524
|
return;
|
|
496
525
|
}
|
|
526
|
+
const expressionIndex = index;
|
|
497
527
|
const start = expression.start ?? null;
|
|
498
528
|
const end = expression.end ?? null;
|
|
499
529
|
if (start === null || end === null) {
|
|
@@ -509,7 +539,8 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
|
509
539
|
const code = source.slice(start, end);
|
|
510
540
|
const marker = registerMarker(code, context.type === 'tag');
|
|
511
541
|
const appendMarker = (wrapper) => {
|
|
512
|
-
|
|
542
|
+
const insertion = wrapper ? wrapper(marker) : marker;
|
|
543
|
+
appendInsertion(expressionIndex, insertion);
|
|
513
544
|
};
|
|
514
545
|
switch (context.type) {
|
|
515
546
|
case 'tag':
|
|
@@ -551,15 +582,22 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
|
|
|
551
582
|
marker,
|
|
552
583
|
code,
|
|
553
584
|
})),
|
|
585
|
+
diagnostics: { expressionRanges },
|
|
554
586
|
};
|
|
555
587
|
};
|
|
556
588
|
const restoreTemplatePlaceholders = (code, placeholders) => placeholders.reduce((result, placeholder) => {
|
|
557
589
|
return result.split(placeholder.marker).join(`\${${placeholder.code}}`);
|
|
558
590
|
}, code);
|
|
559
|
-
const
|
|
591
|
+
const createInlineSourceMapComment = (map) => {
|
|
592
|
+
const payload = Buffer.from(JSON.stringify(map), 'utf8').toString('base64');
|
|
593
|
+
return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${payload}`;
|
|
594
|
+
};
|
|
595
|
+
const compileReactTemplate = (templateSource, placeholders, resourcePath, tagName, templates, diagnostics) => {
|
|
560
596
|
const parsed = (0, oxc_parser_1.parseSync)(`${resourcePath}?jsx-react-template`, templateSource, TEMPLATE_PARSER_OPTIONS);
|
|
561
597
|
if (parsed.errors.length > 0) {
|
|
562
|
-
throw new Error(
|
|
598
|
+
throw new Error((0, template_diagnostics_js_1.formatTaggedTemplateParserError)(tagName, templates, diagnostics, parsed.errors[0], {
|
|
599
|
+
label: 'jsx-loader',
|
|
600
|
+
}));
|
|
563
601
|
}
|
|
564
602
|
const root = extractJsxRoot(parsed.program);
|
|
565
603
|
const builder = new ReactTemplateBuilder(placeholders);
|
|
@@ -577,7 +615,7 @@ const isLoaderPlaceholderIdentifier = (node) => {
|
|
|
577
615
|
return (node.name.startsWith(TEMPLATE_EXPR_PLACEHOLDER_PREFIX) ||
|
|
578
616
|
node.name.startsWith(TAG_PLACEHOLDER_PREFIX));
|
|
579
617
|
};
|
|
580
|
-
const transformSource = (source, config) => {
|
|
618
|
+
const transformSource = (source, config, options) => {
|
|
581
619
|
const ast = (0, oxc_parser_1.parseSync)(config.resourcePath, source, MODULE_PARSER_OPTIONS);
|
|
582
620
|
if (ast.errors.length > 0) {
|
|
583
621
|
throw new Error(formatParserError(ast.errors[0]));
|
|
@@ -590,7 +628,7 @@ const transformSource = (source, config) => {
|
|
|
590
628
|
}
|
|
591
629
|
});
|
|
592
630
|
if (!taggedTemplates.length) {
|
|
593
|
-
return { code: source,
|
|
631
|
+
return { code: source, mutated: false };
|
|
594
632
|
}
|
|
595
633
|
const magic = new magic_string_1.default(source);
|
|
596
634
|
let mutated = false;
|
|
@@ -602,8 +640,9 @@ const transformSource = (source, config) => {
|
|
|
602
640
|
const mode = config.tagModes.get(tagName) ?? DEFAULT_MODE;
|
|
603
641
|
const quasi = node.quasi;
|
|
604
642
|
const templateSource = buildTemplateSource(quasi.quasis, quasi.expressions, source, tagName);
|
|
643
|
+
const templateStrings = materializeTemplateStrings(quasi.quasis);
|
|
605
644
|
if (mode === 'runtime') {
|
|
606
|
-
const { code, changed } = transformTemplateLiteral(templateSource.source, config.resourcePath);
|
|
645
|
+
const { code, changed } = transformTemplateLiteral(templateSource.source, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
|
|
607
646
|
const restored = restoreTemplatePlaceholders(code, templateSource.placeholders);
|
|
608
647
|
const templateChanged = changed || templateSource.mutated;
|
|
609
648
|
if (!templateChanged) {
|
|
@@ -616,7 +655,7 @@ const transformSource = (source, config) => {
|
|
|
616
655
|
return;
|
|
617
656
|
}
|
|
618
657
|
if (mode === 'react') {
|
|
619
|
-
const compiled = compileReactTemplate(templateSource.source, templateSource.placeholders, config.resourcePath);
|
|
658
|
+
const compiled = compileReactTemplate(templateSource.source, templateSource.placeholders, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
|
|
620
659
|
helperKinds.add('react');
|
|
621
660
|
magic.overwrite(node.start, node.end, compiled);
|
|
622
661
|
mutated = true;
|
|
@@ -627,11 +666,26 @@ const transformSource = (source, config) => {
|
|
|
627
666
|
// Modes are validated during option parsing; this fallback guards future extensions.
|
|
628
667
|
throw new Error(`[jsx-loader] Transformation mode "${mode}" not implemented yet for tag "${tagName}".`);
|
|
629
668
|
});
|
|
669
|
+
const helperSource = Array.from(helperKinds)
|
|
670
|
+
.map(kind => HELPER_SNIPPETS[kind])
|
|
671
|
+
.filter(Boolean)
|
|
672
|
+
.join('\n');
|
|
673
|
+
if (helperSource) {
|
|
674
|
+
magic.append(`\n${helperSource}`);
|
|
675
|
+
mutated = true;
|
|
676
|
+
}
|
|
677
|
+
const code = mutated ? magic.toString() : source;
|
|
678
|
+
const map = options?.sourceMap && mutated
|
|
679
|
+
? magic.generateMap({
|
|
680
|
+
hires: true,
|
|
681
|
+
source: config.resourcePath,
|
|
682
|
+
includeContent: true,
|
|
683
|
+
})
|
|
684
|
+
: undefined;
|
|
630
685
|
return {
|
|
631
|
-
code
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
.filter(Boolean),
|
|
686
|
+
code,
|
|
687
|
+
map,
|
|
688
|
+
mutated,
|
|
635
689
|
};
|
|
636
690
|
};
|
|
637
691
|
function jsxLoader(input) {
|
|
@@ -668,16 +722,14 @@ function jsxLoader(input) {
|
|
|
668
722
|
}
|
|
669
723
|
});
|
|
670
724
|
const source = typeof input === 'string' ? input : input.toString('utf8');
|
|
671
|
-
const
|
|
725
|
+
const enableSourceMap = options.sourceMap === true;
|
|
726
|
+
const { code, map } = transformSource(source, {
|
|
672
727
|
resourcePath: this.resourcePath,
|
|
673
728
|
tags,
|
|
674
729
|
tagModes,
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
callback(null, code);
|
|
730
|
+
}, { sourceMap: enableSourceMap });
|
|
731
|
+
const output = map && enableSourceMap ? `${code}\n${createInlineSourceMapComment(map)}` : code;
|
|
732
|
+
callback(null, output, map);
|
|
681
733
|
}
|
|
682
734
|
catch (error) {
|
|
683
735
|
callback(error);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type SourceMap } from 'magic-string';
|
|
2
|
+
type LoaderCallback = (error: Error | null, content?: string, map?: SourceMap | null) => void;
|
|
2
3
|
type LoaderContext<TOptions> = {
|
|
3
4
|
resourcePath: string;
|
|
4
5
|
async(): LoaderCallback;
|
|
@@ -24,6 +25,10 @@ type LoaderOptions = {
|
|
|
24
25
|
* Optional per-tag override of the transformation mode. Keys map to tag names.
|
|
25
26
|
*/
|
|
26
27
|
tagModes?: Record<string, LoaderMode | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* When true, generate inline source maps for mutated files.
|
|
30
|
+
*/
|
|
31
|
+
sourceMap?: boolean;
|
|
27
32
|
};
|
|
28
33
|
export default function jsxLoader(this: LoaderContext<LoaderOptions>, input: string | Buffer): void;
|
|
29
34
|
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { enableJsxDebugDiagnostics } from '../../debug/diagnostics.cjs';
|
|
2
|
+
import { ensureNodeDom } from '../bootstrap.cjs';
|
|
3
|
+
import { jsx as baseJsx } from '../../jsx.cjs';
|
|
4
|
+
enableJsxDebugDiagnostics({ mode: 'always' });
|
|
5
|
+
ensureNodeDom();
|
|
6
|
+
export const jsx = baseJsx;
|
|
@@ -127,7 +127,7 @@ const reactJsx = (templates, ...values) => {
|
|
|
127
127
|
const build = (0, shared_js_1.buildTemplate)(templates, values);
|
|
128
128
|
const result = (0, oxc_parser_1.parseSync)('inline.jsx', build.source, shared_js_1.parserOptions);
|
|
129
129
|
if (result.errors.length > 0) {
|
|
130
|
-
throw new Error((0, shared_js_1.
|
|
130
|
+
throw new Error((0, shared_js_1.formatTaggedTemplateParserError)('reactJsx', templates, build.diagnostics, result.errors[0]));
|
|
131
131
|
}
|
|
132
132
|
const root = (0, shared_js_1.extractRootNode)(result.program);
|
|
133
133
|
const ctx = {
|
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildTemplate = exports.ensureBinding = exports.sanitizeIdentifier = exports.evaluateExpression = exports.collectPlaceholderNames = exports.normalizeJsxTextSegments = exports.walkAst = exports.getIdentifierName = exports.extractRootNode = exports.
|
|
3
|
+
exports.buildTemplate = exports.ensureBinding = exports.sanitizeIdentifier = exports.evaluateExpression = exports.collectPlaceholderNames = exports.normalizeJsxTextSegments = exports.walkAst = exports.getIdentifierName = exports.extractRootNode = exports.parserOptions = exports.formatParserError = exports.placeholderPattern = exports.PLACEHOLDER_PREFIX = exports.formatTaggedTemplateParserError = void 0;
|
|
4
|
+
var template_diagnostics_js_1 = require("../internal/template-diagnostics.cjs");
|
|
5
|
+
Object.defineProperty(exports, "formatTaggedTemplateParserError", { enumerable: true, get: function () { return template_diagnostics_js_1.formatTaggedTemplateParserError; } });
|
|
4
6
|
const OPEN_TAG_RE = /<\s*$/;
|
|
5
7
|
const CLOSE_TAG_RE = /<\/\s*$/;
|
|
6
8
|
exports.PLACEHOLDER_PREFIX = '__KX_EXPR__';
|
|
7
9
|
exports.placeholderPattern = new RegExp(`${exports.PLACEHOLDER_PREFIX}\\d+_\\d+__`, 'g');
|
|
8
10
|
let invocationCounter = 0;
|
|
9
|
-
exports.parserOptions = {
|
|
10
|
-
lang: 'jsx',
|
|
11
|
-
sourceType: 'module',
|
|
12
|
-
range: true,
|
|
13
|
-
preserveParens: true,
|
|
14
|
-
};
|
|
15
11
|
const formatParserError = (error) => {
|
|
16
12
|
let message = `[oxc-parser] ${error.message}`;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
message += `\n${label.message}`;
|
|
21
|
-
}
|
|
13
|
+
const primaryLabel = error.labels?.[0];
|
|
14
|
+
if (primaryLabel?.message) {
|
|
15
|
+
message += `\n${primaryLabel.message}`;
|
|
22
16
|
}
|
|
23
17
|
if (error.codeframe) {
|
|
24
18
|
message += `\n${error.codeframe}`;
|
|
25
19
|
}
|
|
20
|
+
if (error.helpMessage) {
|
|
21
|
+
message += `\n${error.helpMessage}`;
|
|
22
|
+
}
|
|
26
23
|
return message;
|
|
27
24
|
};
|
|
28
25
|
exports.formatParserError = formatParserError;
|
|
26
|
+
exports.parserOptions = {
|
|
27
|
+
lang: 'jsx',
|
|
28
|
+
sourceType: 'module',
|
|
29
|
+
range: true,
|
|
30
|
+
preserveParens: true,
|
|
31
|
+
};
|
|
29
32
|
const extractRootNode = (program) => {
|
|
30
33
|
for (const statement of program.body) {
|
|
31
34
|
if (statement.type === 'ExpressionStatement') {
|
|
@@ -183,24 +186,40 @@ const buildTemplate = (strings, values) => {
|
|
|
183
186
|
let source = raw[0] ?? '';
|
|
184
187
|
const templateId = invocationCounter++;
|
|
185
188
|
let placeholderIndex = 0;
|
|
189
|
+
const expressionRanges = [];
|
|
186
190
|
for (let idx = 0; idx < values.length; idx++) {
|
|
187
191
|
const chunk = raw[idx] ?? '';
|
|
188
192
|
const nextChunk = raw[idx + 1] ?? '';
|
|
189
193
|
const value = values[idx];
|
|
190
194
|
const isTagNamePosition = OPEN_TAG_RE.test(chunk) || CLOSE_TAG_RE.test(chunk);
|
|
195
|
+
let insertion;
|
|
191
196
|
if (isTagNamePosition && typeof value === 'function') {
|
|
192
197
|
const binding = (0, exports.ensureBinding)(value, bindings, bindingLookup);
|
|
193
|
-
|
|
194
|
-
continue;
|
|
198
|
+
insertion = binding.name;
|
|
195
199
|
}
|
|
196
|
-
if (isTagNamePosition && typeof value === 'string') {
|
|
197
|
-
|
|
198
|
-
continue;
|
|
200
|
+
else if (isTagNamePosition && typeof value === 'string') {
|
|
201
|
+
insertion = value;
|
|
199
202
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
else {
|
|
204
|
+
const placeholder = `${exports.PLACEHOLDER_PREFIX}${templateId}_${placeholderIndex++}__`;
|
|
205
|
+
placeholders.set(placeholder, value);
|
|
206
|
+
insertion = placeholder;
|
|
207
|
+
}
|
|
208
|
+
const sourceStart = source.length;
|
|
209
|
+
source += insertion;
|
|
210
|
+
const sourceEnd = source.length;
|
|
211
|
+
expressionRanges.push({
|
|
212
|
+
index: idx,
|
|
213
|
+
sourceStart,
|
|
214
|
+
sourceEnd,
|
|
215
|
+
});
|
|
216
|
+
source += nextChunk;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
source,
|
|
220
|
+
placeholders,
|
|
221
|
+
bindings,
|
|
222
|
+
diagnostics: { expressionRanges },
|
|
223
|
+
};
|
|
205
224
|
};
|
|
206
225
|
exports.buildTemplate = buildTemplate;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { Expression, JSXElement, JSXFragment, JSXIdentifier, JSXMemberExpression, JSXNamespacedName, Program } from '@oxc-project/types';
|
|
2
2
|
import type { OxcError, ParserOptions } from 'oxc-parser';
|
|
3
|
+
import type { TemplateDiagnostics } from '../internal/template-diagnostics.cjs';
|
|
4
|
+
export { formatTaggedTemplateParserError } from '../internal/template-diagnostics.cjs';
|
|
5
|
+
export type { TemplateDiagnostics, TemplateExpressionRange, } from '../internal/template-diagnostics.cjs';
|
|
3
6
|
export declare const PLACEHOLDER_PREFIX = "__KX_EXPR__";
|
|
4
7
|
export declare const placeholderPattern: RegExp;
|
|
8
|
+
export declare const formatParserError: (error: OxcError) => string;
|
|
5
9
|
type AnyTemplateFunction = (...args: any[]) => unknown;
|
|
6
10
|
type AnyTemplateConstructor = abstract new (...args: any[]) => unknown;
|
|
7
11
|
export type TemplateComponent = (AnyTemplateFunction | AnyTemplateConstructor) & {
|
|
@@ -16,6 +20,7 @@ export type TemplateBuildResult<TComponent extends TemplateComponent> = {
|
|
|
16
20
|
source: string;
|
|
17
21
|
placeholders: Map<string, unknown>;
|
|
18
22
|
bindings: BindingEntry<TComponent>[];
|
|
23
|
+
diagnostics: TemplateDiagnostics;
|
|
19
24
|
};
|
|
20
25
|
export type TemplateContext<TComponent extends TemplateComponent> = {
|
|
21
26
|
source: string;
|
|
@@ -23,7 +28,6 @@ export type TemplateContext<TComponent extends TemplateComponent> = {
|
|
|
23
28
|
components: Map<string, TComponent>;
|
|
24
29
|
};
|
|
25
30
|
export declare const parserOptions: ParserOptions;
|
|
26
|
-
export declare const formatParserError: (error: OxcError) => string;
|
|
27
31
|
export declare const extractRootNode: (program: Program) => JSXElement | JSXFragment;
|
|
28
32
|
export declare const getIdentifierName: (identifier: JSXIdentifier | JSXNamespacedName | JSXMemberExpression) => string;
|
|
29
33
|
type AnyOxcNode = {
|
|
@@ -37,4 +41,3 @@ export declare const evaluateExpression: <TComponent extends TemplateComponent>(
|
|
|
37
41
|
export declare const sanitizeIdentifier: (value: string) => string;
|
|
38
42
|
export declare const ensureBinding: <TComponent extends TemplateComponent>(value: TComponent, bindings: BindingEntry<TComponent>[], bindingLookup: Map<TComponent, BindingEntry<TComponent>>) => BindingEntry<TComponent>;
|
|
39
43
|
export declare const buildTemplate: <TComponent extends TemplateComponent>(strings: TemplateStringsArray, values: unknown[]) => TemplateBuildResult<TComponent>;
|
|
40
|
-
export {};
|
package/dist/cli/init.js
CHANGED
|
@@ -143,6 +143,23 @@ function parsePackageName(spec) {
|
|
|
143
143
|
return { name, version };
|
|
144
144
|
}
|
|
145
145
|
function readLocalOxcParserVersion(resolver = CLI_REQUIRE) {
|
|
146
|
+
try {
|
|
147
|
+
const jsxPkgPath = resolver.resolve("@knighted/jsx/package.json");
|
|
148
|
+
const jsxRoot = path.dirname(jsxPkgPath);
|
|
149
|
+
const localParserPkg = path.join(
|
|
150
|
+
jsxRoot,
|
|
151
|
+
"node_modules",
|
|
152
|
+
"oxc-parser",
|
|
153
|
+
"package.json"
|
|
154
|
+
);
|
|
155
|
+
if (fs.existsSync(localParserPkg)) {
|
|
156
|
+
const pkgJson = JSON.parse(fs.readFileSync(localParserPkg, "utf8"));
|
|
157
|
+
if (pkgJson && typeof pkgJson.version === "string") {
|
|
158
|
+
return pkgJson.version;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
146
163
|
try {
|
|
147
164
|
const pkg = resolver("oxc-parser/package.json");
|
|
148
165
|
if (pkg && typeof pkg.version === "string") {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type JsxDiagnosticsMode = 'env' | 'always';
|
|
2
|
+
export type EnableJsxDebugDiagnosticsOptions = {
|
|
3
|
+
mode?: JsxDiagnosticsMode;
|
|
4
|
+
};
|
|
5
|
+
export declare const enableJsxDebugDiagnostics: (options?: EnableJsxDebugDiagnosticsOptions) => void;
|
|
6
|
+
export declare const disableJsxDebugDiagnostics: () => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { setAttributeDiagnosticsHooks, } from '../internal/attribute-resolution.js';
|
|
2
|
+
import { setEventDiagnosticsHooks, } from '../internal/event-bindings.js';
|
|
3
|
+
import { createDevError, describeValue, emitDevWarning, isDevEnvironment, } from '../internal/dev-environment.js';
|
|
4
|
+
const isAsciiLowercase = (char) => char >= 'a' && char <= 'z';
|
|
5
|
+
let diagnosticsMode = 'env';
|
|
6
|
+
const shouldRunDiagnostics = () => diagnosticsMode === 'always' || (diagnosticsMode === 'env' && isDevEnvironment());
|
|
7
|
+
const shouldForceWarnings = () => diagnosticsMode === 'always';
|
|
8
|
+
const attributeDiagnostics = {
|
|
9
|
+
warnLowercaseEventProp(name) {
|
|
10
|
+
if (!shouldRunDiagnostics()) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!name.startsWith('on') || name.startsWith('on:') || name.length < 3) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const indicator = name[2] ?? '';
|
|
17
|
+
if (!isAsciiLowercase(indicator)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const suggestion = `${name.slice(0, 2)}${indicator.toUpperCase()}${name.slice(3)}`;
|
|
21
|
+
emitDevWarning(`Use camelCase DOM event props when targeting runtime jsx templates. Received "${name}"; did you mean "${suggestion}"?`, shouldForceWarnings());
|
|
22
|
+
},
|
|
23
|
+
ensureValidDangerouslySetInnerHTML(value) {
|
|
24
|
+
if (!shouldRunDiagnostics()) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
28
|
+
throw createDevError('dangerouslySetInnerHTML expects an object with a string __html field.');
|
|
29
|
+
}
|
|
30
|
+
const html = value.__html;
|
|
31
|
+
if (typeof html !== 'string') {
|
|
32
|
+
throw createDevError(`dangerouslySetInnerHTML.__html must be a string but received ${describeValue(html)}.`);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const eventDiagnostics = {
|
|
37
|
+
onInvalidHandler(propName, value) {
|
|
38
|
+
if (!shouldRunDiagnostics()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw createDevError(`The "${propName}" prop expects a function, EventListenerObject, or descriptor ({ handler }) but received ${describeValue(value)}.`);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export const enableJsxDebugDiagnostics = (options) => {
|
|
45
|
+
diagnosticsMode = options?.mode ?? 'env';
|
|
46
|
+
setAttributeDiagnosticsHooks(attributeDiagnostics);
|
|
47
|
+
setEventDiagnosticsHooks(eventDiagnostics);
|
|
48
|
+
};
|
|
49
|
+
export const disableJsxDebugDiagnostics = () => {
|
|
50
|
+
setAttributeDiagnosticsHooks(null);
|
|
51
|
+
setEventDiagnosticsHooks(null);
|
|
52
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Expression, JSXAttribute, JSXElement, JSXFragment, JSXSpreadAttribute } from '@oxc-project/types';
|
|
2
2
|
import type { TemplateComponent, TemplateContext } from '../runtime/shared.js';
|
|
3
|
+
export type AttributeDiagnosticsHooks = {
|
|
4
|
+
warnLowercaseEventProp?: (name: string) => void;
|
|
5
|
+
ensureValidDangerouslySetInnerHTML?: (value: unknown) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare const setAttributeDiagnosticsHooks: (hooks: AttributeDiagnosticsHooks | null) => void;
|
|
3
8
|
export type Namespace = 'svg' | null;
|
|
4
9
|
export type EvaluateExpressionWithNamespace<TComponent extends TemplateComponent> = (expression: Expression | JSXElement | JSXFragment, ctx: TemplateContext<TComponent>, namespace: Namespace) => unknown;
|
|
5
10
|
export type ResolveAttributesDependencies<TComponent extends TemplateComponent> = {
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
let attributeDiagnostics = null;
|
|
2
|
+
export const setAttributeDiagnosticsHooks = (hooks) => {
|
|
3
|
+
attributeDiagnostics = hooks;
|
|
4
|
+
};
|
|
5
|
+
const warnLowercaseEventProp = (name) => {
|
|
6
|
+
attributeDiagnostics?.warnLowercaseEventProp?.(name);
|
|
7
|
+
};
|
|
8
|
+
const ensureValidDangerouslySetInnerHTML = (value) => {
|
|
9
|
+
attributeDiagnostics?.ensureValidDangerouslySetInnerHTML?.(value);
|
|
10
|
+
};
|
|
1
11
|
export const createResolveAttributes = (deps) => {
|
|
2
12
|
const { getIdentifierName, evaluateExpressionWithNamespace } = deps;
|
|
3
13
|
return (attributes, ctx, namespace) => {
|
|
4
14
|
const props = {};
|
|
15
|
+
const assignProp = (propName, propValue) => {
|
|
16
|
+
if (propName === 'dangerouslySetInnerHTML') {
|
|
17
|
+
ensureValidDangerouslySetInnerHTML(propValue);
|
|
18
|
+
}
|
|
19
|
+
props[propName] = propValue;
|
|
20
|
+
};
|
|
5
21
|
attributes.forEach(attribute => {
|
|
6
22
|
if (attribute.type === 'JSXSpreadAttribute') {
|
|
7
23
|
const spreadValue = evaluateExpressionWithNamespace(attribute.argument, ctx, namespace);
|
|
@@ -13,19 +29,20 @@ export const createResolveAttributes = (deps) => {
|
|
|
13
29
|
return;
|
|
14
30
|
}
|
|
15
31
|
const name = getIdentifierName(attribute.name);
|
|
32
|
+
warnLowercaseEventProp(name);
|
|
16
33
|
if (!attribute.value) {
|
|
17
|
-
|
|
34
|
+
assignProp(name, true);
|
|
18
35
|
return;
|
|
19
36
|
}
|
|
20
37
|
if (attribute.value.type === 'Literal') {
|
|
21
|
-
|
|
38
|
+
assignProp(name, attribute.value.value);
|
|
22
39
|
return;
|
|
23
40
|
}
|
|
24
41
|
if (attribute.value.type === 'JSXExpressionContainer') {
|
|
25
42
|
if (attribute.value.expression.type === 'JSXEmptyExpression') {
|
|
26
43
|
return;
|
|
27
44
|
}
|
|
28
|
-
|
|
45
|
+
assignProp(name, evaluateExpressionWithNamespace(attribute.value.expression, ctx, namespace));
|
|
29
46
|
}
|
|
30
47
|
});
|
|
31
48
|
return props;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const DEV_PREFIX = '@knighted/jsx';
|
|
2
|
+
const formatDevMessage = (message) => `${DEV_PREFIX}: ${message}`;
|
|
3
|
+
export const isDevEnvironment = () => typeof process !== 'undefined' && process.env?.KNIGHTED_JSX_DEBUG === '1';
|
|
4
|
+
export const describeValue = (value) => {
|
|
5
|
+
if (value === null) {
|
|
6
|
+
return 'null';
|
|
7
|
+
}
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
return 'undefined';
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'function') {
|
|
12
|
+
return value.name ? `function ${value.name}` : 'function';
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return 'array';
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'object') {
|
|
18
|
+
const ctor = value.constructor;
|
|
19
|
+
if (ctor && typeof ctor.name === 'string' && ctor.name && ctor.name !== 'Object') {
|
|
20
|
+
return `${ctor.name} instance`;
|
|
21
|
+
}
|
|
22
|
+
return 'object';
|
|
23
|
+
}
|
|
24
|
+
return typeof value;
|
|
25
|
+
};
|
|
26
|
+
export const createDevError = (message) => new Error(formatDevMessage(message));
|
|
27
|
+
export const emitDevWarning = (message, force = false) => {
|
|
28
|
+
if (!force && !isDevEnvironment()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
32
|
+
console.warn(formatDevMessage(message));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export type EventDiagnosticsHooks = {
|
|
2
|
+
onInvalidHandler?: (propName: string, value: unknown) => void;
|
|
3
|
+
};
|
|
4
|
+
export declare const setEventDiagnosticsHooks: (hooks: EventDiagnosticsHooks | null) => void;
|
|
1
5
|
export type ParsedEventBinding = {
|
|
2
6
|
eventName: string;
|
|
3
7
|
capture: boolean;
|
|
@@ -15,4 +19,4 @@ export type ResolvedEventHandler = {
|
|
|
15
19
|
listener: EventListenerOrEventListenerObject;
|
|
16
20
|
options?: AddEventListenerOptions;
|
|
17
21
|
};
|
|
18
|
-
export declare const resolveEventHandlerValue: (value: unknown) => ResolvedEventHandler | null;
|
|
22
|
+
export declare const resolveEventHandlerValue: (propName: string, value: unknown) => ResolvedEventHandler | null;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
let eventDiagnostics = null;
|
|
2
|
+
export const setEventDiagnosticsHooks = (hooks) => {
|
|
3
|
+
eventDiagnostics = hooks;
|
|
4
|
+
};
|
|
1
5
|
const captureSuffix = 'Capture';
|
|
2
6
|
const stripCaptureSuffix = (rawName) => {
|
|
3
7
|
if (rawName.endsWith(captureSuffix)) {
|
|
@@ -50,11 +54,15 @@ const isEventHandlerDescriptor = (value) => {
|
|
|
50
54
|
}
|
|
51
55
|
return isEventListenerObject(handler);
|
|
52
56
|
};
|
|
53
|
-
|
|
57
|
+
const throwInvalidHandlerError = (propName, value) => {
|
|
58
|
+
eventDiagnostics?.onInvalidHandler?.(propName, value);
|
|
59
|
+
};
|
|
60
|
+
export const resolveEventHandlerValue = (propName, value) => {
|
|
54
61
|
if (typeof value === 'function' || isEventListenerObject(value)) {
|
|
55
62
|
return { listener: value };
|
|
56
63
|
}
|
|
57
64
|
if (!isEventHandlerDescriptor(value)) {
|
|
65
|
+
throwInvalidHandlerError(propName, value);
|
|
58
66
|
return null;
|
|
59
67
|
}
|
|
60
68
|
const descriptor = value;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OxcError } from 'oxc-parser';
|
|
2
|
+
export type TemplateExpressionRange = {
|
|
3
|
+
index: number;
|
|
4
|
+
sourceStart: number;
|
|
5
|
+
sourceEnd: number;
|
|
6
|
+
};
|
|
7
|
+
export type TemplateDiagnostics = {
|
|
8
|
+
expressionRanges: TemplateExpressionRange[];
|
|
9
|
+
};
|
|
10
|
+
export type TaggedTemplateFormatOptions = {
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const formatTaggedTemplateParserError: (tagName: string, templates: TemplateStringsArray, diagnostics: TemplateDiagnostics, error: OxcError, options?: TaggedTemplateFormatOptions) => string;
|