@knighted/jsx 1.5.1 → 1.6.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 (55) hide show
  1. package/README.md +1 -0
  2. package/dist/cjs/debug/diagnostics.cjs +57 -0
  3. package/dist/cjs/debug/diagnostics.d.cts +6 -0
  4. package/dist/cjs/debug/index.cjs +7 -0
  5. package/dist/cjs/debug/index.d.cts +2 -0
  6. package/dist/cjs/internal/attribute-resolution.cjs +55 -0
  7. package/dist/cjs/internal/attribute-resolution.d.cts +15 -0
  8. package/dist/cjs/internal/dev-environment.cjs +41 -0
  9. package/dist/cjs/internal/dev-environment.d.cts +4 -0
  10. package/dist/cjs/internal/event-bindings.cjs +93 -0
  11. package/dist/cjs/internal/event-bindings.d.cts +22 -0
  12. package/dist/cjs/internal/template-diagnostics.cjs +171 -0
  13. package/dist/cjs/internal/template-diagnostics.d.cts +13 -0
  14. package/dist/cjs/jsx.cjs +9 -110
  15. package/dist/cjs/loader/jsx.cjs +92 -20
  16. package/dist/cjs/loader/jsx.d.cts +6 -1
  17. package/dist/cjs/node/bootstrap.cjs +19 -19
  18. package/dist/cjs/node/bootstrap.d.cts +2 -1
  19. package/dist/cjs/node/debug/index.cjs +6 -0
  20. package/dist/cjs/node/debug/index.d.cts +2 -0
  21. package/dist/cjs/node/index.cjs +1 -1
  22. package/dist/cjs/react/react-jsx.cjs +1 -1
  23. package/dist/cjs/runtime/shared.cjs +41 -22
  24. package/dist/cjs/runtime/shared.d.cts +5 -2
  25. package/dist/debug/diagnostics.d.ts +6 -0
  26. package/dist/debug/diagnostics.js +52 -0
  27. package/dist/debug/index.d.ts +2 -0
  28. package/dist/debug/index.js +3 -0
  29. package/dist/internal/attribute-resolution.d.ts +15 -0
  30. package/dist/internal/attribute-resolution.js +50 -0
  31. package/dist/internal/dev-environment.d.ts +4 -0
  32. package/dist/internal/dev-environment.js +34 -0
  33. package/dist/internal/event-bindings.d.ts +22 -0
  34. package/dist/internal/event-bindings.js +87 -0
  35. package/dist/internal/template-diagnostics.d.ts +13 -0
  36. package/dist/internal/template-diagnostics.js +167 -0
  37. package/dist/jsx.js +9 -110
  38. package/dist/lite/debug/diagnostics.js +1 -0
  39. package/dist/lite/debug/index.js +8 -0
  40. package/dist/lite/index.js +8 -4
  41. package/dist/lite/node/debug/index.js +8 -0
  42. package/dist/lite/node/index.js +8 -4
  43. package/dist/lite/node/react/index.js +7 -3
  44. package/dist/lite/react/index.js +7 -3
  45. package/dist/loader/jsx.d.ts +6 -1
  46. package/dist/loader/jsx.js +92 -20
  47. package/dist/node/bootstrap.d.ts +2 -1
  48. package/dist/node/bootstrap.js +19 -19
  49. package/dist/node/debug/index.d.ts +2 -0
  50. package/dist/node/debug/index.js +6 -0
  51. package/dist/node/index.js +1 -1
  52. package/dist/react/react-jsx.js +2 -2
  53. package/dist/runtime/shared.d.ts +5 -2
  54. package/dist/runtime/shared.js +39 -21
  55. package/package.json +40 -8
@@ -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;
@@ -165,6 +166,9 @@ class ReactTemplateBuilder {
165
166
  throw new Error('[jsx-loader] Unable to inline complex expressions in react mode.');
166
167
  }
167
168
  /* c8 ignore next */
169
+ /* v8 ignore next */
170
+ /* istanbul ignore next */
171
+ // Should never happen because OXC always annotates expression ranges.
168
172
  throw new Error('[jsx-loader] Unable to compile expression for react mode.');
169
173
  }
170
174
  buildCreateElement(type, props, children) {
@@ -276,11 +280,15 @@ const shouldInterpolateName = (name) => /^[A-Z]/.test(name.name);
276
280
  const addSlot = (slots, source, range) => {
277
281
  if (!range) {
278
282
  /* c8 ignore next */
283
+ /* v8 ignore next */
284
+ // OXC always provides ranges; guard defends against malformed AST nodes.
279
285
  return;
280
286
  }
281
287
  const [start, end] = range;
282
288
  if (start === end) {
283
289
  /* c8 ignore next */
290
+ /* v8 ignore next */
291
+ // Zero-length ranges indicate parser bugs and would emit empty slices.
284
292
  return;
285
293
  }
286
294
  slots.push({
@@ -293,6 +301,9 @@ const collectSlots = (program, source) => {
293
301
  const slots = [];
294
302
  const recordComponentName = (name) => {
295
303
  if (!name) {
304
+ /* c8 ignore next */
305
+ /* v8 ignore next */
306
+ // JSX elements emitted by OXC always carry a name; this is defensive.
296
307
  return;
297
308
  }
298
309
  switch (name.type) {
@@ -362,6 +373,8 @@ const renderTemplateWithSlots = (source, slots) => {
362
373
  slots.forEach(slot => {
363
374
  if (slot.start < cursor) {
364
375
  /* c8 ignore next */
376
+ /* v8 ignore next */
377
+ // Slots are generated from non-overlapping JSX ranges; this protects against parser regressions.
365
378
  throw new Error('Overlapping JSX expressions detected inside template literal.');
366
379
  }
367
380
  output += escapeTemplateChunk(source.slice(cursor, slot.start));
@@ -371,10 +384,12 @@ const renderTemplateWithSlots = (source, slots) => {
371
384
  output += escapeTemplateChunk(source.slice(cursor));
372
385
  return { code: output, changed: slots.length > 0 };
373
386
  };
374
- const transformTemplateLiteral = (templateSource, resourcePath) => {
387
+ const transformTemplateLiteral = (templateSource, resourcePath, tagName, templates, diagnostics) => {
375
388
  const result = (0, oxc_parser_1.parseSync)(`${resourcePath}?jsx-template`, templateSource, TEMPLATE_PARSER_OPTIONS);
376
389
  if (result.errors.length > 0) {
377
- throw new Error(formatParserError(result.errors[0]));
390
+ throw new Error((0, template_diagnostics_js_1.formatTaggedTemplateParserError)(tagName, templates, diagnostics, result.errors[0], {
391
+ label: 'jsx-loader',
392
+ }));
378
393
  }
379
394
  const slots = collectSlots(result.program, templateSource);
380
395
  return renderTemplateWithSlots(templateSource, slots);
@@ -443,6 +458,25 @@ const normalizeJsxTextSegments = (value, placeholders) => {
443
458
  return segments;
444
459
  };
445
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
+ };
446
480
  const buildTemplateSource = (quasis, expressions, source, tag) => {
447
481
  const placeholderMap = new Map();
448
482
  const tagPlaceholderMap = new Map();
@@ -450,6 +484,7 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
450
484
  let placeholderIndex = 0;
451
485
  let trimStartNext = 0;
452
486
  let mutated = false;
487
+ const expressionRanges = [];
453
488
  const registerMarker = (code, isTag) => {
454
489
  if (isTag) {
455
490
  const existing = tagPlaceholderMap.get(code);
@@ -465,10 +500,18 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
465
500
  placeholderMap.set(marker, code);
466
501
  return marker;
467
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
+ };
468
509
  quasis.forEach((quasi, index) => {
469
510
  let chunk = quasi.value.cooked;
470
511
  if (typeof chunk !== 'string') {
471
512
  /* c8 ignore next */
513
+ /* v8 ignore next */
514
+ // Cooked text is always available for valid templates; fall back shields invalid escape sequences.
472
515
  chunk = quasi.value.raw ?? '';
473
516
  }
474
517
  if (trimStartNext > 0) {
@@ -480,10 +523,13 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
480
523
  if (!expression) {
481
524
  return;
482
525
  }
526
+ const expressionIndex = index;
483
527
  const start = expression.start ?? null;
484
528
  const end = expression.end ?? null;
485
529
  if (start === null || end === null) {
486
530
  /* c8 ignore next */
531
+ /* v8 ignore next */
532
+ // Expressions parsed from tagged templates always include start/end ranges.
487
533
  throw new Error('Unable to read template expression source range.');
488
534
  }
489
535
  const nextChunk = quasis[index + 1];
@@ -493,7 +539,8 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
493
539
  const code = source.slice(start, end);
494
540
  const marker = registerMarker(code, context.type === 'tag');
495
541
  const appendMarker = (wrapper) => {
496
- template += wrapper ? wrapper(marker) : marker;
542
+ const insertion = wrapper ? wrapper(marker) : marker;
543
+ appendInsertion(expressionIndex, insertion);
497
544
  };
498
545
  switch (context.type) {
499
546
  case 'tag':
@@ -535,15 +582,22 @@ const buildTemplateSource = (quasis, expressions, source, tag) => {
535
582
  marker,
536
583
  code,
537
584
  })),
585
+ diagnostics: { expressionRanges },
538
586
  };
539
587
  };
540
588
  const restoreTemplatePlaceholders = (code, placeholders) => placeholders.reduce((result, placeholder) => {
541
589
  return result.split(placeholder.marker).join(`\${${placeholder.code}}`);
542
590
  }, code);
543
- const compileReactTemplate = (templateSource, placeholders, resourcePath) => {
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) => {
544
596
  const parsed = (0, oxc_parser_1.parseSync)(`${resourcePath}?jsx-react-template`, templateSource, TEMPLATE_PARSER_OPTIONS);
545
597
  if (parsed.errors.length > 0) {
546
- throw new Error(formatParserError(parsed.errors[0]));
598
+ throw new Error((0, template_diagnostics_js_1.formatTaggedTemplateParserError)(tagName, templates, diagnostics, parsed.errors[0], {
599
+ label: 'jsx-loader',
600
+ }));
547
601
  }
548
602
  const root = extractJsxRoot(parsed.program);
549
603
  const builder = new ReactTemplateBuilder(placeholders);
@@ -554,12 +608,14 @@ const isLoaderPlaceholderIdentifier = (node) => {
554
608
  (node.type !== 'Identifier' && node.type !== 'JSXIdentifier') ||
555
609
  typeof node.name !== 'string') {
556
610
  /* c8 ignore next */
611
+ /* v8 ignore next */
612
+ // Visitor only calls this helper with identifier-like nodes; guard prevents crashes on malformed ASTs.
557
613
  return false;
558
614
  }
559
615
  return (node.name.startsWith(TEMPLATE_EXPR_PLACEHOLDER_PREFIX) ||
560
616
  node.name.startsWith(TAG_PLACEHOLDER_PREFIX));
561
617
  };
562
- const transformSource = (source, config) => {
618
+ const transformSource = (source, config, options) => {
563
619
  const ast = (0, oxc_parser_1.parseSync)(config.resourcePath, source, MODULE_PARSER_OPTIONS);
564
620
  if (ast.errors.length > 0) {
565
621
  throw new Error(formatParserError(ast.errors[0]));
@@ -572,7 +628,7 @@ const transformSource = (source, config) => {
572
628
  }
573
629
  });
574
630
  if (!taggedTemplates.length) {
575
- return { code: source, helpers: [] };
631
+ return { code: source, mutated: false };
576
632
  }
577
633
  const magic = new magic_string_1.default(source);
578
634
  let mutated = false;
@@ -584,8 +640,9 @@ const transformSource = (source, config) => {
584
640
  const mode = config.tagModes.get(tagName) ?? DEFAULT_MODE;
585
641
  const quasi = node.quasi;
586
642
  const templateSource = buildTemplateSource(quasi.quasis, quasi.expressions, source, tagName);
643
+ const templateStrings = materializeTemplateStrings(quasi.quasis);
587
644
  if (mode === 'runtime') {
588
- const { code, changed } = transformTemplateLiteral(templateSource.source, config.resourcePath);
645
+ const { code, changed } = transformTemplateLiteral(templateSource.source, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
589
646
  const restored = restoreTemplatePlaceholders(code, templateSource.placeholders);
590
647
  const templateChanged = changed || templateSource.mutated;
591
648
  if (!templateChanged) {
@@ -598,20 +655,37 @@ const transformSource = (source, config) => {
598
655
  return;
599
656
  }
600
657
  if (mode === 'react') {
601
- const compiled = compileReactTemplate(templateSource.source, templateSource.placeholders, config.resourcePath);
658
+ const compiled = compileReactTemplate(templateSource.source, templateSource.placeholders, config.resourcePath, tagName, templateStrings, templateSource.diagnostics);
602
659
  helperKinds.add('react');
603
660
  magic.overwrite(node.start, node.end, compiled);
604
661
  mutated = true;
605
662
  return;
606
663
  }
607
664
  /* c8 ignore next */
665
+ /* v8 ignore next */
666
+ // Modes are validated during option parsing; this fallback guards future extensions.
608
667
  throw new Error(`[jsx-loader] Transformation mode "${mode}" not implemented yet for tag "${tagName}".`);
609
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;
610
685
  return {
611
- code: mutated ? magic.toString() : source,
612
- helpers: Array.from(helperKinds)
613
- .map(kind => HELPER_SNIPPETS[kind])
614
- .filter(Boolean),
686
+ code,
687
+ map,
688
+ mutated,
615
689
  };
616
690
  };
617
691
  function jsxLoader(input) {
@@ -648,16 +722,14 @@ function jsxLoader(input) {
648
722
  }
649
723
  });
650
724
  const source = typeof input === 'string' ? input : input.toString('utf8');
651
- const { code, helpers } = transformSource(source, {
725
+ const enableSourceMap = options.sourceMap === true;
726
+ const { code, map } = transformSource(source, {
652
727
  resourcePath: this.resourcePath,
653
728
  tags,
654
729
  tagModes,
655
- });
656
- if (helpers.length) {
657
- callback(null, `${code}\n${helpers.join('\n')}`);
658
- return;
659
- }
660
- callback(null, code);
730
+ }, { sourceMap: enableSourceMap });
731
+ const output = map && enableSourceMap ? `${code}\n${createInlineSourceMapComment(map)}` : code;
732
+ callback(null, output, map);
661
733
  }
662
734
  catch (error) {
663
735
  callback(error);
@@ -1,4 +1,5 @@
1
- type LoaderCallback = (err: Error | null, content?: string) => void;
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 {};
@@ -1,3 +1,7 @@
1
+ import { createRequire } from 'node:module';
2
+ const nodeRequire = createRequire(import.meta.url);
3
+ let requireOverride = null;
4
+ const resolveRequire = () => requireOverride ?? nodeRequire;
1
5
  const DOM_TEMPLATE = '<!doctype html><html><body></body></html>';
2
6
  const GLOBAL_KEYS = [
3
7
  'window',
@@ -23,13 +27,13 @@ const assignGlobalTargets = (windowObj) => {
23
27
  }
24
28
  });
25
29
  };
26
- const loadLinkedom = async () => {
27
- const { parseHTML } = await import('linkedom');
30
+ const loadLinkedom = () => {
31
+ const { parseHTML } = resolveRequire()('linkedom');
28
32
  const { window } = parseHTML(DOM_TEMPLATE);
29
33
  return window;
30
34
  };
31
- const loadJsdom = async () => {
32
- const { JSDOM } = await import('jsdom');
35
+ const loadJsdom = () => {
36
+ const { JSDOM } = resolveRequire()('jsdom');
33
37
  const { window } = new JSDOM(DOM_TEMPLATE);
34
38
  return window;
35
39
  };
@@ -52,11 +56,11 @@ const selectLoaders = () => {
52
56
  }
53
57
  return [loadLinkedom, loadJsdom];
54
58
  };
55
- const createShimWindow = async () => {
59
+ const createShimWindow = () => {
56
60
  const errors = [];
57
61
  for (const loader of selectLoaders()) {
58
62
  try {
59
- return await loader();
63
+ return loader();
60
64
  }
61
65
  catch (error) {
62
66
  errors.push(error);
@@ -65,19 +69,15 @@ const createShimWindow = async () => {
65
69
  const help = 'Unable to bootstrap a DOM-like environment. Install "linkedom" or "jsdom" (both optional peer dependencies) or set KNIGHTED_JSX_NODE_SHIM to pick one explicitly.';
66
70
  throw new AggregateError(errors, help);
67
71
  };
68
- let bootstrapPromise = null;
69
- export const ensureNodeDom = async () => {
70
- if (hasDom()) {
72
+ let bootstrapped = false;
73
+ export const ensureNodeDom = () => {
74
+ if (hasDom() || bootstrapped) {
71
75
  return;
72
76
  }
73
- if (!bootstrapPromise) {
74
- bootstrapPromise = (async () => {
75
- const windowObj = await createShimWindow();
76
- assignGlobalTargets(windowObj);
77
- })().catch(error => {
78
- bootstrapPromise = null;
79
- throw error;
80
- });
81
- }
82
- return bootstrapPromise;
77
+ const windowObj = createShimWindow();
78
+ assignGlobalTargets(windowObj);
79
+ bootstrapped = true;
80
+ };
81
+ export const __setNodeRequireForTesting = (mockRequire) => {
82
+ requireOverride = mockRequire;
83
83
  };
@@ -1 +1,2 @@
1
- export declare const ensureNodeDom: () => Promise<void>;
1
+ export declare const ensureNodeDom: () => void;
2
+ export declare const __setNodeRequireForTesting: (mockRequire: NodeJS.Require | null) => void;
@@ -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;
@@ -0,0 +1,2 @@
1
+ export declare const jsx: (templates: TemplateStringsArray, ...values: unknown[]) => import("../../jsx.cjs").JsxRenderable;
2
+ export type { JsxRenderable, JsxComponent } from '../../jsx.cjs';
@@ -1,4 +1,4 @@
1
1
  import { ensureNodeDom } from './bootstrap.cjs';
2
2
  import { jsx as baseJsx } from '../jsx.cjs';
3
- await ensureNodeDom();
3
+ ensureNodeDom();
4
4
  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.formatParserError)(result.errors[0]));
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.formatParserError = exports.parserOptions = exports.placeholderPattern = exports.PLACEHOLDER_PREFIX = void 0;
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
- if (error.labels?.length) {
18
- const label = error.labels[0];
19
- if (label.message) {
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
- source += binding.name + nextChunk;
194
- continue;
198
+ insertion = binding.name;
195
199
  }
196
- if (isTagNamePosition && typeof value === 'string') {
197
- source += value + nextChunk;
198
- continue;
200
+ else if (isTagNamePosition && typeof value === 'string') {
201
+ insertion = value;
199
202
  }
200
- const placeholder = `${exports.PLACEHOLDER_PREFIX}${templateId}_${placeholderIndex++}__`;
201
- placeholders.set(placeholder, value);
202
- source += placeholder + nextChunk;
203
- }
204
- return { source, placeholders, bindings };
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 {};
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export { jsx } from '../jsx.js';
2
+ export type { JsxRenderable, JsxComponent } from '../jsx.js';
@@ -0,0 +1,3 @@
1
+ import { enableJsxDebugDiagnostics } from './diagnostics.js';
2
+ enableJsxDebugDiagnostics({ mode: 'always' });
3
+ export { jsx } from '../jsx.js';
@@ -0,0 +1,15 @@
1
+ import type { Expression, JSXAttribute, JSXElement, JSXFragment, JSXSpreadAttribute } from '@oxc-project/types';
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;
8
+ export type Namespace = 'svg' | null;
9
+ export type EvaluateExpressionWithNamespace<TComponent extends TemplateComponent> = (expression: Expression | JSXElement | JSXFragment, ctx: TemplateContext<TComponent>, namespace: Namespace) => unknown;
10
+ export type ResolveAttributesDependencies<TComponent extends TemplateComponent> = {
11
+ getIdentifierName: (name: JSXAttribute['name']) => string;
12
+ evaluateExpressionWithNamespace: EvaluateExpressionWithNamespace<TComponent>;
13
+ };
14
+ export type ResolveAttributesFn<TComponent extends TemplateComponent> = (attributes: (JSXAttribute | JSXSpreadAttribute)[], ctx: TemplateContext<TComponent>, namespace: Namespace) => Record<string, unknown>;
15
+ export declare const createResolveAttributes: <TComponent extends TemplateComponent>(deps: ResolveAttributesDependencies<TComponent>) => ResolveAttributesFn<TComponent>;
@@ -0,0 +1,50 @@
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
+ };
11
+ export const createResolveAttributes = (deps) => {
12
+ const { getIdentifierName, evaluateExpressionWithNamespace } = deps;
13
+ return (attributes, ctx, namespace) => {
14
+ const props = {};
15
+ const assignProp = (propName, propValue) => {
16
+ if (propName === 'dangerouslySetInnerHTML') {
17
+ ensureValidDangerouslySetInnerHTML(propValue);
18
+ }
19
+ props[propName] = propValue;
20
+ };
21
+ attributes.forEach(attribute => {
22
+ if (attribute.type === 'JSXSpreadAttribute') {
23
+ const spreadValue = evaluateExpressionWithNamespace(attribute.argument, ctx, namespace);
24
+ if (spreadValue &&
25
+ typeof spreadValue === 'object' &&
26
+ !Array.isArray(spreadValue)) {
27
+ Object.assign(props, spreadValue);
28
+ }
29
+ return;
30
+ }
31
+ const name = getIdentifierName(attribute.name);
32
+ warnLowercaseEventProp(name);
33
+ if (!attribute.value) {
34
+ assignProp(name, true);
35
+ return;
36
+ }
37
+ if (attribute.value.type === 'Literal') {
38
+ assignProp(name, attribute.value.value);
39
+ return;
40
+ }
41
+ if (attribute.value.type === 'JSXExpressionContainer') {
42
+ if (attribute.value.expression.type === 'JSXEmptyExpression') {
43
+ return;
44
+ }
45
+ assignProp(name, evaluateExpressionWithNamespace(attribute.value.expression, ctx, namespace));
46
+ }
47
+ });
48
+ return props;
49
+ };
50
+ };
@@ -0,0 +1,4 @@
1
+ export declare const isDevEnvironment: () => boolean;
2
+ export declare const describeValue: (value: unknown) => string;
3
+ export declare const createDevError: (message: string) => Error;
4
+ export declare const emitDevWarning: (message: string, force?: boolean) => void;