@tsrx/core 0.0.28 → 0.1.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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.28",
6
+ "version": "0.1.1",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -18,7 +18,7 @@ import { error } from './errors.js';
18
18
  import { DIAGNOSTIC_CODES } from './diagnostics.js';
19
19
 
20
20
  const JSX_EXPRESSION_VALUE_ERROR =
21
- 'JSX elements cannot be used as expressions. Wrap with `<>...</>` or `<tsx>...</tsx>` or use elements as statements within a component.';
21
+ 'JSX elements cannot be used as expressions. Wrap JSX with `<>...</>` or `<tsx>...</tsx>`, wrap TSRX templates with `<tsrx>...</tsrx>`, or use elements as statements within a component.';
22
22
 
23
23
  /** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
24
24
  const argument_clash_first_positions = new WeakMap();
@@ -325,7 +325,10 @@ export function TSRXPlugin(config) {
325
325
  }
326
326
 
327
327
  const parent = this.#path.at(-1);
328
- if (!parent || (parent.type !== 'Component' && parent.type !== 'Element')) {
328
+ if (
329
+ !parent ||
330
+ (parent.type !== 'Component' && parent.type !== 'Element' && parent.type !== 'Tsrx')
331
+ ) {
329
332
  return false;
330
333
  }
331
334
 
@@ -1699,7 +1702,22 @@ export function TSRXPlugin(config) {
1699
1702
  chunkStart = this.pos;
1700
1703
 
1701
1704
  while (true) {
1702
- if (this.pos >= this.input.length) this.raise(this.start, 'Unterminated JSX contents');
1705
+ if (this.pos >= this.input.length) {
1706
+ const inside_open_template = this.#path.findLast(
1707
+ (n) =>
1708
+ n.type === 'Element' ||
1709
+ n.type === 'Tsrx' ||
1710
+ n.type === 'TsxCompat' ||
1711
+ n.type === 'Tsx',
1712
+ );
1713
+ if (!inside_open_template) {
1714
+ while (this.curContext() === tstc.tc_expr) {
1715
+ this.context.pop();
1716
+ }
1717
+ return this.finishToken(tt.eof);
1718
+ }
1719
+ this.raise(this.start, 'Unterminated JSX contents');
1720
+ }
1703
1721
  let ch = this.input.charCodeAt(this.pos);
1704
1722
 
1705
1723
  switch (ch) {
@@ -1809,7 +1827,8 @@ export function TSRXPlugin(config) {
1809
1827
  ch === 125 &&
1810
1828
  (this.#path.length === 0 ||
1811
1829
  this.#path.at(-1)?.type === 'Component' ||
1812
- this.#path.at(-1)?.type === 'Element')
1830
+ this.#path.at(-1)?.type === 'Element' ||
1831
+ this.#path.at(-1)?.type === 'Tsrx')
1813
1832
  ) {
1814
1833
  this.#resetTokenStartToCurrentPosition();
1815
1834
  return original.readToken.call(this, ch);
@@ -1849,7 +1868,9 @@ export function TSRXPlugin(config) {
1849
1868
  * Override jsx_parseElement to intercept expression-level JSX.
1850
1869
  * This is called by acorn-jsx's parseExprAtom when it encounters <
1851
1870
  * in expression position. Bare fragments are treated as shorthand
1852
- * for <tsx>...</tsx>; other tags must still use <tsx> or <tsx:*>.
1871
+ * for <tsx>...</tsx>. <tsrx>...</tsrx> admits native TSRX
1872
+ * template syntax as an expression value. Other tags must still use
1873
+ * <tsx>, <tsrx>, or <tsx:*>.
1853
1874
  * @type {Parse.Parser['jsx_parseElement']}
1854
1875
  */
1855
1876
  jsx_parseElement() {
@@ -1859,11 +1880,12 @@ export function TSRXPlugin(config) {
1859
1880
  return super.jsx_parseElement();
1860
1881
  }
1861
1882
 
1862
- // Check if the element being parsed IS a <tsx> or <tsx:*> tag
1883
+ // Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
1863
1884
  // Current token is jsxTagStart, this.end is position after '<'
1864
1885
  const tag_name_start = this.end;
1865
1886
  const is_fragment_tag = this.input.charCodeAt(tag_name_start) === 62;
1866
1887
  const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
1888
+ const char_after_tsrx = this.input.charCodeAt(tag_name_start + 4);
1867
1889
  const is_tsx_tag =
1868
1890
  this.input.startsWith('tsx', tag_name_start) &&
1869
1891
  (tag_name_start + 3 >= this.input.length ||
@@ -1874,9 +1896,18 @@ export function TSRXPlugin(config) {
1874
1896
  char_after_tsx === 10 || // newline
1875
1897
  char_after_tsx === 13 || // carriage return
1876
1898
  char_after_tsx === 58); // : (tsx:react)
1877
-
1878
- if (is_fragment_tag || is_tsx_tag) {
1879
- // Use Ripple's parseElement to create a Tsx/TsxCompat node.
1899
+ const is_tsrx_tag =
1900
+ this.input.startsWith('tsrx', tag_name_start) &&
1901
+ (tag_name_start + 4 >= this.input.length ||
1902
+ char_after_tsrx === 62 || // >
1903
+ char_after_tsrx === 47 || // / (self-closing)
1904
+ char_after_tsrx === 32 || // space
1905
+ char_after_tsrx === 9 || // tab
1906
+ char_after_tsrx === 10 || // newline
1907
+ char_after_tsrx === 13); // carriage return
1908
+
1909
+ if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
1910
+ // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
1880
1911
  // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
1881
1912
  this.next();
1882
1913
  return /** @type {import('estree-jsx').JSXElement} */ (
@@ -1907,7 +1938,9 @@ export function TSRXPlugin(config) {
1907
1938
  const start = this.start - 1;
1908
1939
  const position = new acorn.Position(this.curLine, start - this.lineStart);
1909
1940
 
1910
- const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
1941
+ const element = /** @type {AST.Element | AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (
1942
+ this.startNode()
1943
+ );
1911
1944
  element.start = start;
1912
1945
  /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1913
1946
  element.metadata = { path: [] };
@@ -1928,6 +1961,11 @@ export function TSRXPlugin(config) {
1928
1961
  !is_tsx_compat &&
1929
1962
  open.name.type === 'JSXIdentifier' &&
1930
1963
  open.name.name === 'tsx';
1964
+ const is_tsrx =
1965
+ !is_fragment &&
1966
+ !is_tsx_compat &&
1967
+ open.name.type === 'JSXIdentifier' &&
1968
+ open.name.name === 'tsrx';
1931
1969
 
1932
1970
  if (is_tsx_compat) {
1933
1971
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
@@ -1950,6 +1988,15 @@ export function TSRXPlugin(config) {
1950
1988
  `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
1951
1989
  );
1952
1990
  }
1991
+ } else if (is_tsrx) {
1992
+ /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
1993
+
1994
+ if (open.selfClosing) {
1995
+ this.raise(
1996
+ open.start,
1997
+ `TSRX elements cannot be self-closing. '<tsrx />' must have a closing tag '</tsrx>'.`,
1998
+ );
1999
+ }
1953
2000
  } else if (is_fragment) {
1954
2001
  /** @type {AST.Tsx} */ (element).type = 'Tsx';
1955
2002
  } else {
@@ -1987,7 +2034,7 @@ export function TSRXPlugin(config) {
1987
2034
  }
1988
2035
  }
1989
2036
 
1990
- if (!is_tsx_compat && !is_tsx && !is_fragment) {
2037
+ if (!is_tsx_compat && !is_tsx && !is_tsrx && !is_fragment) {
1991
2038
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1992
2039
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1993
2040
  );
@@ -2181,6 +2228,7 @@ export function TSRXPlugin(config) {
2181
2228
  parent?.type === 'Component' ||
2182
2229
  parent?.type === 'Element' ||
2183
2230
  parent?.type === 'Tsx' ||
2231
+ parent?.type === 'Tsrx' ||
2184
2232
  parent?.type === 'TsxCompat';
2185
2233
 
2186
2234
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -2249,7 +2297,21 @@ export function TSRXPlugin(config) {
2249
2297
  this.#popTsxTokenContextBeforeTemplateExpressionChild();
2250
2298
  this.next();
2251
2299
  }
2252
- } else if (this.#path[this.#path.length - 1] === element) {
2300
+ } else if (element.type === 'Tsrx' && this.#path[this.#path.length - 1] === element) {
2301
+ this.#report_broken_markup_error(
2302
+ this.start,
2303
+ "Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
2304
+ );
2305
+ element.unclosed = true;
2306
+ /** @type {AST.SourceLocation} */ (element.loc).end = {
2307
+ .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2308
+ };
2309
+ element.end = element.openingElement.end;
2310
+ this.#path.pop();
2311
+ } else if (
2312
+ element.type === 'Element' &&
2313
+ this.#path[this.#path.length - 1] === element
2314
+ ) {
2253
2315
  // Check if this element was properly closed
2254
2316
  const tagName = this.getElementName(element.id);
2255
2317
  this.#report_broken_markup_error(
@@ -2257,7 +2319,7 @@ export function TSRXPlugin(config) {
2257
2319
  `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
2258
2320
  );
2259
2321
  element.unclosed = true;
2260
- element.loc.end = {
2322
+ /** @type {AST.SourceLocation} */ (element.loc).end = {
2261
2323
  .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2262
2324
  };
2263
2325
  element.end = element.openingElement.end;
@@ -2272,6 +2334,7 @@ export function TSRXPlugin(config) {
2272
2334
  parent?.type === 'Component' ||
2273
2335
  parent?.type === 'Element' ||
2274
2336
  parent?.type === 'Tsx' ||
2337
+ parent?.type === 'Tsrx' ||
2275
2338
  parent?.type === 'TsxCompat';
2276
2339
 
2277
2340
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -2279,7 +2342,13 @@ export function TSRXPlugin(config) {
2279
2342
  }
2280
2343
  }
2281
2344
 
2282
- if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
2345
+ if (
2346
+ element.closingElement &&
2347
+ !is_tsx_compat &&
2348
+ !is_tsx &&
2349
+ !is_tsrx &&
2350
+ element.closingElement.name
2351
+ ) {
2283
2352
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
2284
2353
  element.closingElement.name,
2285
2354
  );
@@ -2480,10 +2549,26 @@ export function TSRXPlugin(config) {
2480
2549
  this.context.pop();
2481
2550
  }
2482
2551
  return;
2483
- } else if (this.type === tstt.jsxTagStart) {
2552
+ } else if (
2553
+ this.type === tstt.jsxTagStart ||
2554
+ (this.input.charCodeAt(this.start) === 60 /* < */ &&
2555
+ this.input.charCodeAt(this.start + 1) === 47) /* / */
2556
+ ) {
2484
2557
  const startPos = this.start;
2485
2558
  const startLoc = this.startLoc;
2486
- this.next();
2559
+ if (this.type === tstt.jsxTagStart) {
2560
+ this.next();
2561
+ } else {
2562
+ // A control-flow block inside <tsrx> can leave the tokenizer
2563
+ // in normal JS mode, so `</tsrx>` may arrive as a relational
2564
+ // `<` token. Re-enter JSX closing-tag parsing manually.
2565
+ this.pos = startPos + 1;
2566
+ this.type = tstt.jsxTagStart;
2567
+ this.start = startPos;
2568
+ this.startLoc = startLoc;
2569
+ this.exprAllowed = false;
2570
+ this.next();
2571
+ }
2487
2572
  if (this.value === '/' || this.type === tt.slash) {
2488
2573
  // Consume '/'
2489
2574
  this.next();
@@ -2500,6 +2585,7 @@ export function TSRXPlugin(config) {
2500
2585
  !currentElement ||
2501
2586
  (currentElement.type !== 'Element' &&
2502
2587
  currentElement.type !== 'Tsx' &&
2588
+ currentElement.type !== 'Tsrx' &&
2503
2589
  currentElement.type !== 'TsxCompat')
2504
2590
  ) {
2505
2591
  this.raise(this.start, 'Unexpected closing tag');
@@ -2522,6 +2608,12 @@ export function TSRXPlugin(config) {
2522
2608
  closingElement.name?.type === 'JSXNamespacedName'
2523
2609
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2524
2610
  : this.getElementName(closingElement.name);
2611
+ } else if (currentElement.type === 'Tsrx') {
2612
+ openingTagName = 'tsrx';
2613
+ closingTagName =
2614
+ closingElement.name?.type === 'JSXNamespacedName'
2615
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2616
+ : this.getElementName(closingElement.name);
2525
2617
  } else {
2526
2618
  // Regular Element node (or fragment)
2527
2619
  openingTagName = currentElement.id ? this.getElementName(currentElement.id) : null;
@@ -2544,7 +2636,12 @@ export function TSRXPlugin(config) {
2544
2636
  const elem = this.#path[this.#path.length - 1];
2545
2637
 
2546
2638
  // Stop at non-Element boundaries (Component, etc.)
2547
- if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
2639
+ if (
2640
+ elem.type !== 'Element' &&
2641
+ elem.type !== 'Tsx' &&
2642
+ elem.type !== 'Tsrx' &&
2643
+ elem.type !== 'TsxCompat'
2644
+ ) {
2548
2645
  break;
2549
2646
  }
2550
2647
 
@@ -2555,9 +2652,11 @@ export function TSRXPlugin(config) {
2555
2652
  ? elem.openingElement.name
2556
2653
  ? 'tsx'
2557
2654
  : null
2558
- : elem.id
2559
- ? this.getElementName(elem.id)
2560
- : null;
2655
+ : elem.type === 'Tsrx'
2656
+ ? 'tsrx'
2657
+ : elem.id
2658
+ ? this.getElementName(elem.id)
2659
+ : null;
2561
2660
 
2562
2661
  // Found matching opening tag
2563
2662
  if (elemName === closingTagName) {
@@ -2576,12 +2675,19 @@ export function TSRXPlugin(config) {
2576
2675
  }
2577
2676
 
2578
2677
  const elementToClose = this.#path[this.#path.length - 1];
2579
- if (elementToClose && elementToClose.type === 'Element') {
2580
- const elementToCloseName = /** @type {AST.Element} */ (elementToClose).id
2581
- ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
2582
- : null;
2678
+ if (
2679
+ elementToClose &&
2680
+ (elementToClose.type === 'Element' || elementToClose.type === 'Tsrx')
2681
+ ) {
2682
+ const elementToCloseName =
2683
+ elementToClose.type === 'Tsrx'
2684
+ ? 'tsrx'
2685
+ : /** @type {AST.Element} */ (elementToClose).id
2686
+ ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
2687
+ : null;
2583
2688
  if (elementToCloseName === closingTagName) {
2584
- /** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
2689
+ /** @type {AST.Element | AST.Tsrx} */ (elementToClose).closingElement =
2690
+ closingElement;
2585
2691
  }
2586
2692
  }
2587
2693
 
package/src/scope.js CHANGED
@@ -127,6 +127,13 @@ export function create_scopes(ast, root, parent, error_options) {
127
127
  next({ scope });
128
128
  },
129
129
 
130
+ Tsrx(node, { state, next }) {
131
+ const scope = state.scope.child();
132
+ scopes.set(node, scope);
133
+
134
+ next({ scope });
135
+ },
136
+
130
137
  TSModuleDeclaration(node, { state, next }) {
131
138
  const is_submodule = node.metadata?.module_keyword === 'module';
132
139
  if (is_submodule && node.id?.type === 'Identifier') {
@@ -175,6 +175,7 @@ export function is_jsx_child(node) {
175
175
  t === 'JSXExpressionContainer' ||
176
176
  t === 'JSXText' ||
177
177
  t === 'Tsx' ||
178
+ t === 'Tsrx' ||
178
179
  t === 'TsxCompat' ||
179
180
  t === 'Element' ||
180
181
  t === 'Text' ||
@@ -390,6 +390,17 @@ export function createJsxTransform(platform) {
390
390
  );
391
391
  },
392
392
 
393
+ Tsrx(node, { next, path, state }) {
394
+ const inner = /** @type {any} */ (next() ?? node);
395
+ const in_jsx_child = in_jsx_child_context(path);
396
+ return /** @type {any} */ (
397
+ wrap_jsx_setup_declarations(
398
+ tsrx_node_to_jsx_expression(inner, state, in_jsx_child),
399
+ in_jsx_child,
400
+ )
401
+ );
402
+ },
403
+
393
404
  TsxCompat(node, { next, path, state }) {
394
405
  const inner = /** @type {any} */ (next() ?? node);
395
406
  const in_jsx_child = in_jsx_child_context(path);
@@ -3379,6 +3390,8 @@ function to_jsx_child(node, transform_context) {
3379
3390
  // We're inside a JSX child position by construction, so keep a
3380
3391
  // JSXExpressionContainer wrapper for bare `{expr}` children.
3381
3392
  return tsx_node_to_jsx_expression(node, true);
3393
+ case 'Tsrx':
3394
+ return tsrx_node_to_jsx_expression(node, transform_context, true);
3382
3395
  case 'TsxCompat':
3383
3396
  return tsx_compat_node_to_jsx_expression(node, transform_context, true);
3384
3397
  case 'Element':
@@ -3413,6 +3426,59 @@ function to_jsx_child(node, transform_context) {
3413
3426
  }
3414
3427
  }
3415
3428
 
3429
+ /**
3430
+ * Lower a `<tsrx>` node's native TSRX template body to a JSX expression.
3431
+ * Unlike `<tsx>`, children have already been parsed and transformed through
3432
+ * the normal TSRX Element/Text/control-flow visitors.
3433
+ *
3434
+ * @param {any} node
3435
+ * @param {TransformContext} transform_context
3436
+ * @param {boolean} [in_jsx_child]
3437
+ * @returns {any}
3438
+ */
3439
+ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = false) {
3440
+ const children = (node.children || []).filter(
3441
+ (/** @type {any} */ child) =>
3442
+ child &&
3443
+ child.type !== 'EmptyStatement' &&
3444
+ (child.type !== 'JSXText' || child.value.trim() !== ''),
3445
+ );
3446
+
3447
+ /** @type {any} */
3448
+ let expression;
3449
+ if (children.length === 0) {
3450
+ expression = create_null_literal();
3451
+ } else if (
3452
+ children.every(is_inline_element_child) &&
3453
+ !children_contain_return_semantics(children)
3454
+ ) {
3455
+ const saved_inside_element_child = transform_context.inside_element_child;
3456
+ transform_context.inside_element_child = true;
3457
+ try {
3458
+ const render_nodes = children.map((/** @type {any} */ child) =>
3459
+ to_jsx_child(child, transform_context),
3460
+ );
3461
+ expression = build_return_expression(render_nodes) || create_null_literal();
3462
+ } finally {
3463
+ transform_context.inside_element_child = saved_inside_element_child;
3464
+ }
3465
+ } else {
3466
+ expression = statement_body_to_jsx_child(children, transform_context).expression;
3467
+ }
3468
+
3469
+ if (
3470
+ in_jsx_child &&
3471
+ expression.type !== 'JSXElement' &&
3472
+ expression.type !== 'JSXFragment' &&
3473
+ expression.type !== 'JSXText' &&
3474
+ expression.type !== 'JSXExpressionContainer'
3475
+ ) {
3476
+ return to_jsx_expression_container(expression, node);
3477
+ }
3478
+
3479
+ return expression;
3480
+ }
3481
+
3416
3482
  /**
3417
3483
  * @param {any} node
3418
3484
  * @param {TransformContext} transform_context
package/types/index.d.ts CHANGED
@@ -205,6 +205,7 @@ declare module 'estree' {
205
205
  interface NodeMap {
206
206
  Component: Component;
207
207
  Tsx: Tsx;
208
+ Tsrx: Tsrx;
208
209
  TsxCompat: TsxCompat;
209
210
  TSRXExpression: TSRXExpression;
210
211
  Html: Html;
@@ -346,6 +347,16 @@ declare module 'estree' {
346
347
  closingElement: ESTreeJSX.JSXClosingElement;
347
348
  }
348
349
 
350
+ interface Tsrx extends AST.BaseNode {
351
+ type: 'Tsrx';
352
+ attributes: Array<any>;
353
+ children: AST.Node[];
354
+ selfClosing?: boolean;
355
+ unclosed?: boolean;
356
+ openingElement: ESTreeJSX.JSXOpeningElement;
357
+ closingElement: ESTreeJSX.JSXClosingElement;
358
+ }
359
+
349
360
  interface TsxCompat extends AST.BaseNode {
350
361
  type: 'TsxCompat';
351
362
  kind: string;
@@ -479,7 +490,7 @@ declare module 'estree' {
479
490
 
480
491
  export type TSRXStatement = AST.Statement | TSESTree.Statement;
481
492
 
482
- export type NodeWithChildren = AST.Element | AST.Tsx | AST.TsxCompat;
493
+ export type NodeWithChildren = AST.Element | AST.Tsx | AST.Tsrx | AST.TsxCompat;
483
494
 
484
495
  export namespace CSS {
485
496
  export interface BaseNode extends AST.NodeWithMaybeComments {
package/types/parse.d.ts CHANGED
@@ -1172,7 +1172,7 @@ export namespace Parse {
1172
1172
  */
1173
1173
  parseTopLevel(node: AST.Program): AST.Program;
1174
1174
 
1175
- parseElement(): AST.Element | AST.Tsx | AST.TsxCompat;
1175
+ parseElement(): AST.Element | AST.Tsx | AST.Tsrx | AST.TsxCompat;
1176
1176
 
1177
1177
  parseDoubleQuotedTextChild(): AST.TextNode;
1178
1178