@tsrx/core 0.0.28 → 0.1.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/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.0",
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
 
@@ -1809,7 +1812,8 @@ export function TSRXPlugin(config) {
1809
1812
  ch === 125 &&
1810
1813
  (this.#path.length === 0 ||
1811
1814
  this.#path.at(-1)?.type === 'Component' ||
1812
- this.#path.at(-1)?.type === 'Element')
1815
+ this.#path.at(-1)?.type === 'Element' ||
1816
+ this.#path.at(-1)?.type === 'Tsrx')
1813
1817
  ) {
1814
1818
  this.#resetTokenStartToCurrentPosition();
1815
1819
  return original.readToken.call(this, ch);
@@ -1849,7 +1853,9 @@ export function TSRXPlugin(config) {
1849
1853
  * Override jsx_parseElement to intercept expression-level JSX.
1850
1854
  * This is called by acorn-jsx's parseExprAtom when it encounters <
1851
1855
  * in expression position. Bare fragments are treated as shorthand
1852
- * for <tsx>...</tsx>; other tags must still use <tsx> or <tsx:*>.
1856
+ * for <tsx>...</tsx>. <tsrx>...</tsrx> admits native TSRX
1857
+ * template syntax as an expression value. Other tags must still use
1858
+ * <tsx>, <tsrx>, or <tsx:*>.
1853
1859
  * @type {Parse.Parser['jsx_parseElement']}
1854
1860
  */
1855
1861
  jsx_parseElement() {
@@ -1859,11 +1865,12 @@ export function TSRXPlugin(config) {
1859
1865
  return super.jsx_parseElement();
1860
1866
  }
1861
1867
 
1862
- // Check if the element being parsed IS a <tsx> or <tsx:*> tag
1868
+ // Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
1863
1869
  // Current token is jsxTagStart, this.end is position after '<'
1864
1870
  const tag_name_start = this.end;
1865
1871
  const is_fragment_tag = this.input.charCodeAt(tag_name_start) === 62;
1866
1872
  const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
1873
+ const char_after_tsrx = this.input.charCodeAt(tag_name_start + 4);
1867
1874
  const is_tsx_tag =
1868
1875
  this.input.startsWith('tsx', tag_name_start) &&
1869
1876
  (tag_name_start + 3 >= this.input.length ||
@@ -1874,9 +1881,18 @@ export function TSRXPlugin(config) {
1874
1881
  char_after_tsx === 10 || // newline
1875
1882
  char_after_tsx === 13 || // carriage return
1876
1883
  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.
1884
+ const is_tsrx_tag =
1885
+ this.input.startsWith('tsrx', tag_name_start) &&
1886
+ (tag_name_start + 4 >= this.input.length ||
1887
+ char_after_tsrx === 62 || // >
1888
+ char_after_tsrx === 47 || // / (self-closing)
1889
+ char_after_tsrx === 32 || // space
1890
+ char_after_tsrx === 9 || // tab
1891
+ char_after_tsrx === 10 || // newline
1892
+ char_after_tsrx === 13); // carriage return
1893
+
1894
+ if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
1895
+ // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
1880
1896
  // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
1881
1897
  this.next();
1882
1898
  return /** @type {import('estree-jsx').JSXElement} */ (
@@ -1907,7 +1923,9 @@ export function TSRXPlugin(config) {
1907
1923
  const start = this.start - 1;
1908
1924
  const position = new acorn.Position(this.curLine, start - this.lineStart);
1909
1925
 
1910
- const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
1926
+ const element = /** @type {AST.Element | AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (
1927
+ this.startNode()
1928
+ );
1911
1929
  element.start = start;
1912
1930
  /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1913
1931
  element.metadata = { path: [] };
@@ -1928,6 +1946,11 @@ export function TSRXPlugin(config) {
1928
1946
  !is_tsx_compat &&
1929
1947
  open.name.type === 'JSXIdentifier' &&
1930
1948
  open.name.name === 'tsx';
1949
+ const is_tsrx =
1950
+ !is_fragment &&
1951
+ !is_tsx_compat &&
1952
+ open.name.type === 'JSXIdentifier' &&
1953
+ open.name.name === 'tsrx';
1931
1954
 
1932
1955
  if (is_tsx_compat) {
1933
1956
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
@@ -1950,6 +1973,15 @@ export function TSRXPlugin(config) {
1950
1973
  `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
1951
1974
  );
1952
1975
  }
1976
+ } else if (is_tsrx) {
1977
+ /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
1978
+
1979
+ if (open.selfClosing) {
1980
+ this.raise(
1981
+ open.start,
1982
+ `TSRX elements cannot be self-closing. '<tsrx />' must have a closing tag '</tsrx>'.`,
1983
+ );
1984
+ }
1953
1985
  } else if (is_fragment) {
1954
1986
  /** @type {AST.Tsx} */ (element).type = 'Tsx';
1955
1987
  } else {
@@ -1987,7 +2019,7 @@ export function TSRXPlugin(config) {
1987
2019
  }
1988
2020
  }
1989
2021
 
1990
- if (!is_tsx_compat && !is_tsx && !is_fragment) {
2022
+ if (!is_tsx_compat && !is_tsx && !is_tsrx && !is_fragment) {
1991
2023
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1992
2024
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1993
2025
  );
@@ -2181,6 +2213,7 @@ export function TSRXPlugin(config) {
2181
2213
  parent?.type === 'Component' ||
2182
2214
  parent?.type === 'Element' ||
2183
2215
  parent?.type === 'Tsx' ||
2216
+ parent?.type === 'Tsrx' ||
2184
2217
  parent?.type === 'TsxCompat';
2185
2218
 
2186
2219
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -2249,7 +2282,21 @@ export function TSRXPlugin(config) {
2249
2282
  this.#popTsxTokenContextBeforeTemplateExpressionChild();
2250
2283
  this.next();
2251
2284
  }
2252
- } else if (this.#path[this.#path.length - 1] === element) {
2285
+ } else if (element.type === 'Tsrx' && this.#path[this.#path.length - 1] === element) {
2286
+ this.#report_broken_markup_error(
2287
+ this.start,
2288
+ "Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
2289
+ );
2290
+ element.unclosed = true;
2291
+ /** @type {AST.SourceLocation} */ (element.loc).end = {
2292
+ .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2293
+ };
2294
+ element.end = element.openingElement.end;
2295
+ this.#path.pop();
2296
+ } else if (
2297
+ element.type === 'Element' &&
2298
+ this.#path[this.#path.length - 1] === element
2299
+ ) {
2253
2300
  // Check if this element was properly closed
2254
2301
  const tagName = this.getElementName(element.id);
2255
2302
  this.#report_broken_markup_error(
@@ -2257,7 +2304,7 @@ export function TSRXPlugin(config) {
2257
2304
  `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
2258
2305
  );
2259
2306
  element.unclosed = true;
2260
- element.loc.end = {
2307
+ /** @type {AST.SourceLocation} */ (element.loc).end = {
2261
2308
  .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2262
2309
  };
2263
2310
  element.end = element.openingElement.end;
@@ -2272,6 +2319,7 @@ export function TSRXPlugin(config) {
2272
2319
  parent?.type === 'Component' ||
2273
2320
  parent?.type === 'Element' ||
2274
2321
  parent?.type === 'Tsx' ||
2322
+ parent?.type === 'Tsrx' ||
2275
2323
  parent?.type === 'TsxCompat';
2276
2324
 
2277
2325
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -2279,7 +2327,13 @@ export function TSRXPlugin(config) {
2279
2327
  }
2280
2328
  }
2281
2329
 
2282
- if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
2330
+ if (
2331
+ element.closingElement &&
2332
+ !is_tsx_compat &&
2333
+ !is_tsx &&
2334
+ !is_tsrx &&
2335
+ element.closingElement.name
2336
+ ) {
2283
2337
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
2284
2338
  element.closingElement.name,
2285
2339
  );
@@ -2480,10 +2534,26 @@ export function TSRXPlugin(config) {
2480
2534
  this.context.pop();
2481
2535
  }
2482
2536
  return;
2483
- } else if (this.type === tstt.jsxTagStart) {
2537
+ } else if (
2538
+ this.type === tstt.jsxTagStart ||
2539
+ (this.input.charCodeAt(this.start) === 60 /* < */ &&
2540
+ this.input.charCodeAt(this.start + 1) === 47) /* / */
2541
+ ) {
2484
2542
  const startPos = this.start;
2485
2543
  const startLoc = this.startLoc;
2486
- this.next();
2544
+ if (this.type === tstt.jsxTagStart) {
2545
+ this.next();
2546
+ } else {
2547
+ // A control-flow block inside <tsrx> can leave the tokenizer
2548
+ // in normal JS mode, so `</tsrx>` may arrive as a relational
2549
+ // `<` token. Re-enter JSX closing-tag parsing manually.
2550
+ this.pos = startPos + 1;
2551
+ this.type = tstt.jsxTagStart;
2552
+ this.start = startPos;
2553
+ this.startLoc = startLoc;
2554
+ this.exprAllowed = false;
2555
+ this.next();
2556
+ }
2487
2557
  if (this.value === '/' || this.type === tt.slash) {
2488
2558
  // Consume '/'
2489
2559
  this.next();
@@ -2500,6 +2570,7 @@ export function TSRXPlugin(config) {
2500
2570
  !currentElement ||
2501
2571
  (currentElement.type !== 'Element' &&
2502
2572
  currentElement.type !== 'Tsx' &&
2573
+ currentElement.type !== 'Tsrx' &&
2503
2574
  currentElement.type !== 'TsxCompat')
2504
2575
  ) {
2505
2576
  this.raise(this.start, 'Unexpected closing tag');
@@ -2522,6 +2593,12 @@ export function TSRXPlugin(config) {
2522
2593
  closingElement.name?.type === 'JSXNamespacedName'
2523
2594
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2524
2595
  : this.getElementName(closingElement.name);
2596
+ } else if (currentElement.type === 'Tsrx') {
2597
+ openingTagName = 'tsrx';
2598
+ closingTagName =
2599
+ closingElement.name?.type === 'JSXNamespacedName'
2600
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2601
+ : this.getElementName(closingElement.name);
2525
2602
  } else {
2526
2603
  // Regular Element node (or fragment)
2527
2604
  openingTagName = currentElement.id ? this.getElementName(currentElement.id) : null;
@@ -2544,7 +2621,12 @@ export function TSRXPlugin(config) {
2544
2621
  const elem = this.#path[this.#path.length - 1];
2545
2622
 
2546
2623
  // Stop at non-Element boundaries (Component, etc.)
2547
- if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
2624
+ if (
2625
+ elem.type !== 'Element' &&
2626
+ elem.type !== 'Tsx' &&
2627
+ elem.type !== 'Tsrx' &&
2628
+ elem.type !== 'TsxCompat'
2629
+ ) {
2548
2630
  break;
2549
2631
  }
2550
2632
 
@@ -2555,9 +2637,11 @@ export function TSRXPlugin(config) {
2555
2637
  ? elem.openingElement.name
2556
2638
  ? 'tsx'
2557
2639
  : null
2558
- : elem.id
2559
- ? this.getElementName(elem.id)
2560
- : null;
2640
+ : elem.type === 'Tsrx'
2641
+ ? 'tsrx'
2642
+ : elem.id
2643
+ ? this.getElementName(elem.id)
2644
+ : null;
2561
2645
 
2562
2646
  // Found matching opening tag
2563
2647
  if (elemName === closingTagName) {
@@ -2576,12 +2660,19 @@ export function TSRXPlugin(config) {
2576
2660
  }
2577
2661
 
2578
2662
  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;
2663
+ if (
2664
+ elementToClose &&
2665
+ (elementToClose.type === 'Element' || elementToClose.type === 'Tsrx')
2666
+ ) {
2667
+ const elementToCloseName =
2668
+ elementToClose.type === 'Tsrx'
2669
+ ? 'tsrx'
2670
+ : /** @type {AST.Element} */ (elementToClose).id
2671
+ ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
2672
+ : null;
2583
2673
  if (elementToCloseName === closingTagName) {
2584
- /** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
2674
+ /** @type {AST.Element | AST.Tsrx} */ (elementToClose).closingElement =
2675
+ closingElement;
2585
2676
  }
2586
2677
  }
2587
2678
 
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