@tsrx/core 0.0.5 → 0.0.7

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.7",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -155,6 +155,22 @@ 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';
167
+ export {
168
+ is_static_literal as isStaticLiteral,
169
+ is_hoist_safe_expression as isHoistSafeExpression,
170
+ is_hoist_safe_jsx_child as isHoistSafeJsxChild,
171
+ is_hoist_safe_jsx_attribute as isHoistSafeJsxAttribute,
172
+ is_hoist_safe_jsx_node as isHoistSafeJsxNode,
173
+ } from './transform/jsx-hoist.js';
158
174
 
159
175
  // Analyze
160
176
  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,119 @@
1
+ /** @import * as ESTreeJSX from 'estree-jsx' */
2
+
3
+ /**
4
+ * Predicates that decide whether a JSX subtree can be safely hoisted out of a
5
+ * component body into a module-level `const`. A subtree is hoist-safe only
6
+ * when evaluating it at module-load time produces the same value as
7
+ * evaluating it on every render — i.e. it contains no identifier references,
8
+ * no calls, no spreads, and no other render-time expressions.
9
+ */
10
+
11
+ /**
12
+ * @param {import('estree').Literal} node
13
+ * @returns {boolean}
14
+ */
15
+ export function is_static_literal(node) {
16
+ return (
17
+ node.value === null ||
18
+ typeof node.value === 'string' ||
19
+ typeof node.value === 'number' ||
20
+ typeof node.value === 'boolean' ||
21
+ typeof node.value === 'bigint'
22
+ );
23
+ }
24
+
25
+ /**
26
+ * @param {any} node
27
+ * @returns {boolean}
28
+ */
29
+ export function is_hoist_safe_expression(node) {
30
+ if (!node || typeof node !== 'object') return false;
31
+
32
+ switch (node.type) {
33
+ case 'Literal':
34
+ return is_static_literal(node);
35
+ case 'TemplateLiteral':
36
+ return node.expressions.length === 0;
37
+ case 'UnaryExpression':
38
+ return node.operator !== 'delete' && is_hoist_safe_expression(node.argument);
39
+ case 'BinaryExpression':
40
+ case 'LogicalExpression':
41
+ return is_hoist_safe_expression(node.left) && is_hoist_safe_expression(node.right);
42
+ case 'ConditionalExpression':
43
+ return (
44
+ is_hoist_safe_expression(node.test) &&
45
+ is_hoist_safe_expression(node.consequent) &&
46
+ is_hoist_safe_expression(node.alternate)
47
+ );
48
+ case 'SequenceExpression':
49
+ return node.expressions.every(is_hoist_safe_expression);
50
+ case 'ParenthesizedExpression':
51
+ return is_hoist_safe_expression(node.expression);
52
+ case 'JSXElement':
53
+ return is_hoist_safe_jsx_node(node);
54
+ case 'JSXFragment':
55
+ return node.children.every(is_hoist_safe_jsx_child);
56
+ default:
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * @param {any} node
63
+ * @returns {boolean}
64
+ */
65
+ export function is_hoist_safe_jsx_child(node) {
66
+ if (!node || typeof node !== 'object') return false;
67
+
68
+ switch (node.type) {
69
+ case 'JSXText':
70
+ return true;
71
+ case 'JSXElement':
72
+ return is_hoist_safe_jsx_node(node);
73
+ case 'JSXFragment':
74
+ return node.children.every(is_hoist_safe_jsx_child);
75
+ case 'JSXExpressionContainer':
76
+ return (
77
+ node.expression.type !== 'JSXEmptyExpression' && is_hoist_safe_expression(node.expression)
78
+ );
79
+ default:
80
+ return false;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @param {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} attribute
86
+ * @returns {boolean}
87
+ */
88
+ export function is_hoist_safe_jsx_attribute(attribute) {
89
+ if (attribute.type === 'JSXSpreadAttribute') return false;
90
+ if (attribute.value == null) return true;
91
+
92
+ if (attribute.value.type === 'Literal') {
93
+ return is_static_literal(attribute.value);
94
+ }
95
+
96
+ if (attribute.value.type === 'JSXExpressionContainer') {
97
+ return (
98
+ attribute.value.expression.type !== 'JSXEmptyExpression' &&
99
+ is_hoist_safe_expression(attribute.value.expression)
100
+ );
101
+ }
102
+
103
+ return false;
104
+ }
105
+
106
+ /**
107
+ * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
108
+ * @returns {boolean}
109
+ */
110
+ export function is_hoist_safe_jsx_node(node) {
111
+ if (node.type === 'JSXFragment') {
112
+ return node.children.every(is_hoist_safe_jsx_child);
113
+ }
114
+
115
+ return (
116
+ node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
117
+ node.children.every(is_hoist_safe_jsx_child)
118
+ );
119
+ }
@@ -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
+ }
package/types/index.d.ts CHANGED
@@ -98,6 +98,12 @@ declare module 'estree' {
98
98
  };
99
99
  }
100
100
 
101
+ interface SimpleCallExpression {
102
+ metadata: BaseNodeMetaData & {
103
+ hash?: string;
104
+ };
105
+ }
106
+
101
107
  type Accessibility = 'public' | 'protected' | 'private'; // missing in acorn-typescript types
102
108
  interface MethodDefinition {
103
109
  typeParameters?: TSTypeParameterDeclaration;
@@ -1335,6 +1341,11 @@ export interface AnalysisState extends BaseState {
1335
1341
  styleClasses?: StyleClasses;
1336
1342
  };
1337
1343
  mode: CompileOptions['mode'];
1344
+ // keep this as an object as we destructure
1345
+ module: {
1346
+ // Incremented counter for generating unique track/trackAsync hashes
1347
+ track_id: number;
1348
+ };
1338
1349
  }
1339
1350
 
1340
1351
  export interface TransformServerState extends BaseState {