@tsrx/core 0.0.5 → 0.0.6

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.5",
6
+ "version": "0.0.6",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -155,6 +155,15 @@ export {
155
155
  apply_lazy_transforms as applyLazyTransforms,
156
156
  replace_lazy_params as replaceLazyParams,
157
157
  } from './transform/lazy.js';
158
+ export {
159
+ find_first_top_level_await as findFirstTopLevelAwait,
160
+ find_first_top_level_await_in_component_body as findFirstTopLevelAwaitInComponentBody,
161
+ } from './transform/await.js';
162
+ export {
163
+ is_interleaved_body as isInterleavedBody,
164
+ is_capturable_jsx_child as isCapturableJsxChild,
165
+ capture_jsx_child as captureJsxChild,
166
+ } from './transform/jsx-interleave.js';
158
167
 
159
168
  // Analyze
160
169
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
package/src/plugin.js CHANGED
@@ -439,6 +439,10 @@ export function TSRXPlugin(config) {
439
439
  const isTagLikeAfterLt =
440
440
  !isWhitespaceAfterLt &&
441
441
  (nextChar === 47 || // '/'
442
+ nextChar === 62 || // '>' (fragments: <>)
443
+ nextChar === 64 || // '@'
444
+ nextChar === 36 || // '$'
445
+ nextChar === 95 || // '_'
442
446
  (nextChar >= 65 && nextChar <= 90) || // A-Z
443
447
  (nextChar >= 97 && nextChar <= 122)); // a-z
444
448
  const prevAllowsTagStart =
@@ -503,13 +507,11 @@ export function TSRXPlugin(config) {
503
507
  }
504
508
  }
505
509
 
506
- // Check if the character after < is not whitespace
507
- if (allWhitespace && this.pos + 1 < this.input.length) {
508
- const nextChar = this.input.charCodeAt(this.pos + 1);
509
- if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
510
- ++this.pos;
511
- return this.finishToken(tstt.jsxTagStart);
512
- }
510
+ // At the start of a line inside template bodies, only treat `<` as
511
+ // a tag start when the following character can actually begin a tag.
512
+ if (allWhitespace && isTagLikeAfterLt) {
513
+ ++this.pos;
514
+ return this.finishToken(tstt.jsxTagStart);
513
515
  }
514
516
  }
515
517
  }
@@ -1473,7 +1475,7 @@ export function TSRXPlugin(config) {
1473
1475
  */
1474
1476
  parseElement() {
1475
1477
  const inside_head = this.#path.findLast(
1476
- (n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
1478
+ (n) => n.type === 'Element' && n.id && n.id.type === 'Identifier' && n.id.name === 'head',
1477
1479
  );
1478
1480
  // Adjust the start so we capture the `<` as part of the element
1479
1481
  const start = this.start - 1;
@@ -1492,10 +1494,14 @@ export function TSRXPlugin(config) {
1492
1494
  // Always attach the concrete opening element node for accurate source mapping
1493
1495
  element.openingElement = open;
1494
1496
 
1495
- // Check if this is a namespaced element (tsx:react)
1496
- const is_tsx_compat = open.name.type === 'JSXNamespacedName';
1497
+ // Fragments (<>) produce JSXOpeningFragment with no `name` property
1498
+ const is_fragment = !open.name;
1499
+ const is_tsx_compat = !is_fragment && open.name.type === 'JSXNamespacedName';
1497
1500
  const is_tsx =
1498
- !is_tsx_compat && open.name.type === 'JSXIdentifier' && open.name.name === 'tsx';
1501
+ !is_fragment &&
1502
+ !is_tsx_compat &&
1503
+ open.name.type === 'JSXIdentifier' &&
1504
+ open.name.name === 'tsx';
1499
1505
 
1500
1506
  if (is_tsx_compat) {
1501
1507
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
@@ -1545,11 +1551,13 @@ export function TSRXPlugin(config) {
1545
1551
  }
1546
1552
  }
1547
1553
 
1548
- if (!is_tsx_compat && !is_tsx) {
1554
+ if (!is_tsx_compat && !is_tsx && !is_fragment) {
1549
1555
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1550
1556
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1551
1557
  );
1552
1558
  element.selfClosing = open.selfClosing;
1559
+ } else if (is_fragment) {
1560
+ element.selfClosing = false;
1553
1561
  }
1554
1562
 
1555
1563
  element.attributes = open.attributes;
@@ -1563,6 +1571,10 @@ export function TSRXPlugin(config) {
1563
1571
  this.pos--;
1564
1572
  this.next();
1565
1573
  }
1574
+ } else if (is_fragment) {
1575
+ this.enterScope(0);
1576
+ this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
1577
+ this.exitScope();
1566
1578
  } else {
1567
1579
  if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
1568
1580
  let content = '';
@@ -1815,7 +1827,7 @@ export function TSRXPlugin(config) {
1815
1827
  }
1816
1828
  }
1817
1829
 
1818
- if (element.closingElement && !is_tsx_compat && !is_tsx) {
1830
+ if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
1819
1831
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
1820
1832
  element.closingElement.name,
1821
1833
  );
@@ -2040,12 +2052,13 @@ export function TSRXPlugin(config) {
2040
2052
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2041
2053
  : this.getElementName(closingElement.name);
2042
2054
  } else {
2043
- // Regular Element node
2044
- openingTagName = this.getElementName(currentElement.id);
2045
- closingTagName =
2046
- closingElement.name?.type === 'JSXNamespacedName'
2055
+ // Regular Element node (or fragment)
2056
+ openingTagName = currentElement.id ? this.getElementName(currentElement.id) : null;
2057
+ closingTagName = closingElement.name
2058
+ ? closingElement.name?.type === 'JSXNamespacedName'
2047
2059
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2048
- : this.getElementName(closingElement.name);
2060
+ : this.getElementName(closingElement.name)
2061
+ : null;
2049
2062
  }
2050
2063
 
2051
2064
  if (openingTagName !== closingTagName) {
@@ -2069,7 +2082,9 @@ export function TSRXPlugin(config) {
2069
2082
  ? 'tsx:' + elem.kind
2070
2083
  : elem.type === 'Tsx'
2071
2084
  ? 'tsx'
2072
- : this.getElementName(elem.id);
2085
+ : elem.id
2086
+ ? this.getElementName(elem.id)
2087
+ : null;
2073
2088
 
2074
2089
  // Found matching opening tag
2075
2090
  if (elemName === closingTagName) {
@@ -2090,9 +2105,9 @@ export function TSRXPlugin(config) {
2090
2105
 
2091
2106
  const elementToClose = this.#path[this.#path.length - 1];
2092
2107
  if (elementToClose && elementToClose.type === 'Element') {
2093
- const elementToCloseName = this.getElementName(
2094
- /** @type {AST.Element} */ (elementToClose).id,
2095
- );
2108
+ const elementToCloseName = /** @type {AST.Element} */ (elementToClose).id
2109
+ ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
2110
+ : null;
2096
2111
  if (elementToCloseName === closingTagName) {
2097
2112
  /** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
2098
2113
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @param {any[]} body_nodes
3
+ * @returns {any | null}
4
+ */
5
+ export function find_first_top_level_await_in_component_body(body_nodes) {
6
+ for (const node of body_nodes) {
7
+ const found = find_first_top_level_await(node, false);
8
+ if (found) return found;
9
+ }
10
+
11
+ return null;
12
+ }
13
+
14
+ /**
15
+ * @param {any} node
16
+ * @param {boolean} inside_nested_function
17
+ * @returns {any | null}
18
+ */
19
+ export function find_first_top_level_await(node, inside_nested_function) {
20
+ if (!node || typeof node !== 'object') {
21
+ return null;
22
+ }
23
+
24
+ if (Array.isArray(node)) {
25
+ for (const child of node) {
26
+ const found = find_first_top_level_await(child, inside_nested_function);
27
+ if (found) return found;
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ if (
34
+ node.type === 'FunctionDeclaration' ||
35
+ node.type === 'FunctionExpression' ||
36
+ node.type === 'ArrowFunctionExpression'
37
+ ) {
38
+ return inside_nested_function ? null : find_first_top_level_await(node.body, true);
39
+ }
40
+
41
+ if (inside_nested_function) {
42
+ return null;
43
+ }
44
+
45
+ if (node.type === 'AwaitExpression' || (node.type === 'ForOfStatement' && node.await === true)) {
46
+ return node;
47
+ }
48
+
49
+ for (const key of Object.keys(node)) {
50
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
51
+ continue;
52
+ }
53
+
54
+ const found = find_first_top_level_await(node[key], false);
55
+ if (found) return found;
56
+ }
57
+
58
+ return null;
59
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Helpers for preserving source-order semantics when non-JSX statements are
3
+ * interleaved with JSX children inside a component or element body.
4
+ *
5
+ * Without these, targets like React and Solid would hoist all statements
6
+ * before any JSX is constructed, so mutations between sibling JSX children
7
+ * would be observed by every sibling instead of only the ones that appear
8
+ * textually after the mutation.
9
+ */
10
+
11
+ /**
12
+ * Returns true when the body contains a non-JSX statement that appears
13
+ * after a JSX child. In that case JSX children must be captured at their
14
+ * source position so mutations in following statements do not retroactively
15
+ * change what earlier children rendered.
16
+ *
17
+ * The `is_jsx_child` predicate is target-specific — each target recognizes
18
+ * a different set of JSX-bearing node types (Ripple `Element`, `Text`,
19
+ * `TSRXExpression`, etc. plus plain JSX nodes).
20
+ *
21
+ * @param {any[]} body_nodes
22
+ * @param {(node: any) => boolean} is_jsx_child
23
+ * @returns {boolean}
24
+ */
25
+ export function is_interleaved_body(body_nodes, is_jsx_child) {
26
+ let seen_jsx = false;
27
+ for (const child of body_nodes) {
28
+ if (is_jsx_child(child)) {
29
+ seen_jsx = true;
30
+ } else if (seen_jsx) {
31
+ return true;
32
+ }
33
+ }
34
+ return false;
35
+ }
36
+
37
+ /**
38
+ * Only JSX nodes that evaluate to a single expression can be hoisted into a
39
+ * `const`. Static text children (`JSXText`) are inert and don't need
40
+ * capturing — their position relative to mutations doesn't change output.
41
+ *
42
+ * @param {any} jsx
43
+ * @returns {boolean}
44
+ */
45
+ export function is_capturable_jsx_child(jsx) {
46
+ if (!jsx) return false;
47
+ const t = jsx.type;
48
+ return t === 'JSXElement' || t === 'JSXFragment' || t === 'JSXExpressionContainer';
49
+ }
50
+
51
+ /**
52
+ * Build a `VariableDeclaration` that captures a JSX child into a const at
53
+ * its source position, along with a JSXExpressionContainer referencing the
54
+ * capture. The caller inserts the declaration into the enclosing block's
55
+ * statements in source order and uses the reference in place of the JSX
56
+ * child inside the returned fragment.
57
+ *
58
+ * @param {any} jsx
59
+ * @param {number} capture_index
60
+ * @returns {{ declaration: any, reference: any }}
61
+ */
62
+ export function capture_jsx_child(jsx, capture_index) {
63
+ const name = `_tsrx_child_${capture_index}`;
64
+ const init = jsx.type === 'JSXExpressionContainer' ? jsx.expression : jsx;
65
+
66
+ const declaration = /** @type {any} */ ({
67
+ type: 'VariableDeclaration',
68
+ kind: 'const',
69
+ declarations: [
70
+ /** @type {any} */ ({
71
+ type: 'VariableDeclarator',
72
+ id: /** @type {any} */ ({
73
+ type: 'Identifier',
74
+ name,
75
+ metadata: { path: [] },
76
+ }),
77
+ init,
78
+ metadata: { path: [] },
79
+ }),
80
+ ],
81
+ metadata: { path: [] },
82
+ });
83
+
84
+ // NOTE: JSXExpressionContainer nodes are intentionally created without
85
+ // loc — they're synthetic wrappers whose source positions don't
86
+ // correspond to source-map entries and adding loc causes Volar mapping
87
+ // failures.
88
+ const reference = /** @type {any} */ ({
89
+ type: 'JSXExpressionContainer',
90
+ expression: /** @type {any} */ ({
91
+ type: 'Identifier',
92
+ name,
93
+ metadata: { path: [] },
94
+ }),
95
+ metadata: { path: [] },
96
+ });
97
+
98
+ return { declaration, reference };
99
+ }