@tsrx/core 0.1.4 → 0.1.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.1.4",
6
+ "version": "0.1.7",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -31,9 +31,31 @@
31
31
  "types": "./types/runtime/ref.d.ts",
32
32
  "default": "./src/runtime/ref.js"
33
33
  },
34
- "./runtime/*": "./src/runtime/*.js",
34
+ "./runtime/events": {
35
+ "types": "./types/runtime/events.d.ts",
36
+ "default": "./src/runtime/events.js"
37
+ },
38
+ "./runtime/hash": {
39
+ "types": "./types/runtime/hash.d.ts",
40
+ "default": "./src/runtime/hash.js"
41
+ },
42
+ "./runtime/html": {
43
+ "types": "./types/runtime/html.d.ts",
44
+ "default": "./src/runtime/html.js"
45
+ },
46
+ "./runtime/language-helpers": {
47
+ "types": "./types/runtime/language-helpers.d.ts",
48
+ "default": "./src/runtime/language-helpers.js"
49
+ },
50
+ "./runtime/iterable": {
51
+ "types": "./types/runtime/iterable.d.ts",
52
+ "default": "./src/runtime/iterable.js"
53
+ },
35
54
  "./test-harness/source-mappings": "./tests/shared/source-mappings.js",
36
- "./test-harness/compile": "./tests/shared/compile.js"
55
+ "./test-harness/compile": "./tests/shared/compile.js",
56
+ "./test-harness/runtime/*": "./tests/shared/runtime/*.js",
57
+ "./test-harness/runtime/*.js": "./tests/shared/runtime/*.js",
58
+ "./test-harness/runtime/*.tsrx": "./tests/shared/runtime/*.tsrx"
37
59
  },
38
60
  "dependencies": {
39
61
  "@jridgewell/sourcemap-codec": "^1.5.5",
@@ -42,17 +64,28 @@
42
64
  "@types/estree-jsx": "^1.0.5",
43
65
  "@types/estree": "^1.0.8",
44
66
  "acorn": "^8.15.0",
45
- "esrap": "^2.1.0",
67
+ "esrap": "^2.2.7",
46
68
  "is-reference": "^3.0.3",
47
69
  "magic-string": "^0.30.18",
48
70
  "zimmerframe": "^1.1.2"
49
71
  },
50
72
  "devDependencies": {
73
+ "@solidjs/web": "2.0.0-beta.7",
51
74
  "@types/node": "^24.3.0",
52
75
  "@typescript-eslint/types": "^8.40.0",
53
- "typescript": "^5.9.3",
54
76
  "@volar/language-core": "~2.4.28",
55
- "vscode-languageserver-types": "^3.17.5"
77
+ "preact": "^10.27.0",
78
+ "react": "^19.2.0",
79
+ "react-dom": "^19.2.0",
80
+ "solid-js": "2.0.0-beta.7",
81
+ "typescript": "^5.9.3",
82
+ "vscode-languageserver-types": "^3.17.5",
83
+ "vue": "3.6.0-beta.10",
84
+ "vue-jsx-vapor": "^3.2.12",
85
+ "@tsrx/preact": "0.1.7",
86
+ "@tsrx/react": "0.2.7",
87
+ "@tsrx/solid": "0.1.7",
88
+ "@tsrx/vue": "0.1.7"
56
89
  },
57
90
  "files": [
58
91
  "src",
@@ -4,4 +4,5 @@ export const DIAGNOSTIC_CODES = {
4
4
  FUNCTION_COMPONENT_SYNTAX: 'tsrx-function-component-syntax',
5
5
  UNCLOSED_TAG: 'tsrx-unclosed-tag',
6
6
  MISMATCHED_CLOSING_TAG: 'tsrx-mismatched-closing-tag',
7
+ TEMPLATE_EXPRESSION_TRAILING_SEMICOLON: 'tsrx-template-expression-trailing-semicolon',
7
8
  };
package/src/index.js CHANGED
@@ -88,6 +88,7 @@ export {
88
88
  is_class_node as isClassNode,
89
89
  is_component_node as isComponentNode,
90
90
  is_function_node as isFunctionNode,
91
+ is_function_or_component_node as isFunctionOrComponentNode,
91
92
  is_inside_component as isInsideComponent,
92
93
  } from './utils/ast.js';
93
94
 
@@ -142,6 +143,9 @@ export { escape, escape_script as escapeScript } from './utils/escaping.js';
142
143
  // Transform
143
144
  export {
144
145
  add_jsx_setup_declaration as addJsxSetupDeclaration,
146
+ clone_switch_helper_invocation as cloneSwitchHelperInvocation,
147
+ collect_param_bindings as collectParamBindings,
148
+ collect_statement_bindings as collectStatementBindings,
145
149
  createJsxTransform,
146
150
  CREATE_REF_PROP_INTERNAL_NAME,
147
151
  extract_jsx_setup_declarations as extractJsxSetupDeclarations,
@@ -149,6 +153,7 @@ export {
149
153
  MERGE_REFS_INTERNAL_NAME,
150
154
  merge_duplicate_refs as mergeDuplicateRefs,
151
155
  NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
156
+ plan_switch_lift as planSwitchLift,
152
157
  return_value_body_to_expression as returnValueBodyToExpression,
153
158
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
154
159
  to_jsx_attribute as toJsxAttribute,
@@ -165,13 +170,16 @@ export {
165
170
  clone_expression_node,
166
171
  clone_identifier,
167
172
  clone_jsx_name,
173
+ contains_component_jsx,
168
174
  create_compile_error,
169
175
  create_generated_identifier,
170
176
  create_null_literal,
177
+ expand_switch_cases_for_fallthrough,
171
178
  flatten_switch_consequent,
172
179
  get_for_of_iteration_params,
173
180
  identifier_to_jsx_name,
174
181
  is_bare_render_expression,
182
+ is_component_jsx_name,
175
183
  is_dynamic_element_id,
176
184
  is_jsx_child,
177
185
  set_loc,
@@ -198,11 +206,9 @@ export {
198
206
  export {
199
207
  create_lazy_context as createLazyContext,
200
208
  collect_lazy_bindings as collectLazyBindings,
201
- collect_lazy_bindings_from_component as collectLazyBindingsFromComponent,
202
209
  collect_lazy_bindings_from_statements as collectLazyBindingsFromStatements,
203
210
  preallocate_lazy_ids as preallocateLazyIds,
204
211
  apply_lazy_transforms as applyLazyTransforms,
205
- replace_lazy_params as replaceLazyParams,
206
212
  } from './transform/lazy.js';
207
213
  export {
208
214
  find_first_top_level_await as findFirstTopLevelAwait,
package/src/plugin.js CHANGED
@@ -224,6 +224,7 @@ export function TSRXPlugin(config) {
224
224
  #filename = null;
225
225
  #componentDepth = 0;
226
226
  #functionBodyDepth = 0;
227
+ #allowExpressionContainerTrailingSemicolon = false;
227
228
 
228
229
  /**
229
230
  * @type {Parse.Parser['finishNode']}
@@ -352,7 +353,14 @@ export function TSRXPlugin(config) {
352
353
  }
353
354
 
354
355
  #parseNativeTemplateExpressionContainer() {
355
- const node = this.jsx_parseExpressionContainer();
356
+ const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
357
+ this.#allowExpressionContainerTrailingSemicolon = true;
358
+ let node;
359
+ try {
360
+ node = this.jsx_parseExpressionContainer();
361
+ } finally {
362
+ this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
363
+ }
356
364
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
357
365
  // but convert other expressions to native TSRX child nodes.
358
366
  if (node.expression.type !== 'JSXEmptyExpression') {
@@ -644,6 +652,18 @@ export function TSRXPlugin(config) {
644
652
  ctx.length = ci - 1;
645
653
  return;
646
654
  }
655
+ // Statement-bodied `<tsrx>` attributes can leave the attribute's
656
+ // expression contexts above the still-open JSX tag context. Strip
657
+ // those so a following `/>` stays in JSX opening-tag mode.
658
+ if (
659
+ this.type === tt.braceR &&
660
+ top === tstc.tc_expr &&
661
+ second === b_expr &&
662
+ ctx[ci - 2] === tstc.tc_oTag
663
+ ) {
664
+ ctx.length = ci - 1;
665
+ return;
666
+ }
647
667
  // Closing token after the template at expression position. For `}`
648
668
  // only pop if it actually closes this `b_expr` — otherwise the
649
669
  // brace targets an inner callback/object body that should pop it
@@ -1771,6 +1791,16 @@ export function TSRXPlugin(config) {
1771
1791
  '"style" is a TSRX keyword and must be used in the form {style "class_name"}',
1772
1792
  );
1773
1793
  }
1794
+ if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
1795
+ if (this.#collect) {
1796
+ this.#report_recoverable_error(
1797
+ this.start,
1798
+ 'TSRX expression containers do not use semicolons. Remove this semicolon.',
1799
+ DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
1800
+ );
1801
+ }
1802
+ this.next();
1803
+ }
1774
1804
  this.expect(tt.braceR);
1775
1805
 
1776
1806
  return this.finishNode(node, 'JSXExpressionContainer');
@@ -0,0 +1,10 @@
1
+ export {
2
+ event_name_from_capture,
3
+ get_attribute_event_name,
4
+ get_original_event_name,
5
+ is_capture_event,
6
+ is_event_attribute,
7
+ is_non_delegated,
8
+ is_passive_event,
9
+ normalize_event_name,
10
+ } from '../utils/events.js';
@@ -0,0 +1 @@
1
+ export { simple_hash, strong_hash } from '../utils/hashing.js';
@@ -0,0 +1,3 @@
1
+ export { is_boolean_attribute } from '../utils/dom.js';
2
+ export { escape, escape_script } from '../utils/escaping.js';
3
+ export { normalize_css_property_name } from '../utils/normalize_css_property_name.js';
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @template T
3
+ * @template U
4
+ * @param {Iterable<T> | Iterator<T>} iterable
5
+ * @param {(item: T, index: number, is_last: boolean) => U} fn
6
+ * @param {() => U | U[]} [tail]
7
+ * @returns {U[]}
8
+ */
9
+ export function map_iterable(iterable, fn, tail) {
10
+ if (Array.isArray(iterable)) {
11
+ return map_array(iterable, fn, tail);
12
+ }
13
+
14
+ /** @type {Iterator<T>} */
15
+ var iterator;
16
+ var iterable_prop = /** @type {Iterable<T>} */ (iterable)[Symbol.iterator];
17
+
18
+ if (typeof iterable_prop === 'function') {
19
+ iterator = iterable_prop.call(iterable);
20
+ } else if (typeof (/** @type {Iterator<T>} */ (iterable).next) === 'function') {
21
+ iterator = Iterator.from(iterable);
22
+ } else {
23
+ throw new TypeError('The loop target has to be an Iterable');
24
+ }
25
+
26
+ var current = iterator.next();
27
+ if (current.done) {
28
+ if (!tail) {
29
+ return [];
30
+ }
31
+ var tail_value = tail();
32
+ if (Array.isArray(tail_value)) {
33
+ return tail_value;
34
+ }
35
+ return [tail_value];
36
+ }
37
+
38
+ var index = 0;
39
+ var result = [];
40
+ while (true) {
41
+ var next = iterator.next();
42
+ var value = fn(current.value, index++, !!next.done);
43
+ if (Array.isArray(value)) {
44
+ for (var j = 0; j < value.length; j++) {
45
+ result.push(value[j]);
46
+ }
47
+ } else {
48
+ result.push(value);
49
+ }
50
+ if (next.done) {
51
+ break;
52
+ }
53
+ current = next;
54
+ }
55
+ if (tail) {
56
+ var tail_value = tail();
57
+ if (Array.isArray(tail_value)) {
58
+ for (var j = 0; j < tail_value.length; j++) {
59
+ result.push(tail_value[j]);
60
+ }
61
+ } else {
62
+ result.push(tail_value);
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * @template T
70
+ * @template U
71
+ * @param {Array<T>} array
72
+ * @param {(item: T, index: number, is_last: boolean) => U} fn
73
+ * @param {() => U | U[]} [tail]
74
+ * @returns {U[]}
75
+ */
76
+ function map_array(array, fn, tail) {
77
+ var length = array.length;
78
+ if (length === 0) {
79
+ if (!tail) {
80
+ return [];
81
+ }
82
+ var tail_value = tail();
83
+ if (Array.isArray(tail_value)) {
84
+ return tail_value;
85
+ }
86
+ return [tail_value];
87
+ }
88
+ var result = [];
89
+ for (var i = 0; i < length; i++) {
90
+ var value = fn(array[i], i, i === length - 1);
91
+ if (Array.isArray(value)) {
92
+ for (var j = 0; j < value.length; j++) {
93
+ result.push(value[j]);
94
+ }
95
+ } else {
96
+ result.push(value);
97
+ }
98
+ }
99
+ if (tail) {
100
+ var tail_value = tail();
101
+ if (Array.isArray(tail_value)) {
102
+ for (var j = 0; j < tail_value.length; j++) {
103
+ result.push(tail_value[j]);
104
+ }
105
+ } else {
106
+ result.push(tail_value);
107
+ }
108
+ }
109
+ return result;
110
+ }
@@ -17,10 +17,15 @@
17
17
  * css?: AST.Element['metadata']['css']
18
18
  * },
19
19
  * }} CodePosition
20
+ * @typedef {{
21
+ * column: number,
22
+ * position: CodePosition,
23
+ * }} SourceLineGeneratedPosition
20
24
  */
21
25
 
22
26
  /** @typedef {Map<string, CodePosition[]>} CodeToGeneratedMap */
23
27
  /** @typedef {Map<string, {line: number, column: number}[]>} GeneratedToSourceMap */
28
+ /** @typedef {Map<number, SourceLineGeneratedPosition[]>} SourceLineGeneratedMap */
24
29
 
25
30
  import { decode } from '@jridgewell/sourcemap-codec';
26
31
 
@@ -83,18 +88,22 @@ export const offset_to_line_col = (offset, line_offsets) => {
83
88
  * @param {PostProcessingChanges} post_processing_changes - Optional post-processing changes to apply
84
89
  * @param {LineOffsets} line_offsets - Pre-computed line offsets array
85
90
  * @param {string} generated_code - The final generated code (after post-processing)
86
- * @returns {[CodeToGeneratedMap, GeneratedToSourceMap]} Tuple of [source-to-generated map, generated-to-source map]
91
+ * @param {boolean} [include_source_line_generated_map] - Whether to build the optional source-line predecessor lookup
92
+ * @returns {[CodeToGeneratedMap, GeneratedToSourceMap, SourceLineGeneratedMap | null]} Tuple of [source-to-generated map, generated-to-source map, source-line generated map]
87
93
  */
88
94
  export function build_src_to_gen_map(
89
95
  source_map,
90
96
  post_processing_changes,
91
97
  line_offsets,
92
98
  generated_code,
99
+ include_source_line_generated_map = false,
93
100
  ) {
94
101
  /** @type {CodeToGeneratedMap} */
95
102
  const map = new Map();
96
103
  /** @type {GeneratedToSourceMap} */
97
104
  const reverse_map = new Map();
105
+ /** @type {SourceLineGeneratedMap | null} */
106
+ const source_line_generated_map = include_source_line_generated_map ? new Map() : null;
98
107
 
99
108
  // Decode the VLQ-encoded mappings string
100
109
  const decoded = decode(source_map.mappings);
@@ -192,6 +201,14 @@ export function build_src_to_gen_map(
192
201
  map.set(key, []);
193
202
  }
194
203
  /** @type {CodePosition[]} */ (map.get(key)).push(gen_pos);
204
+ if (source_line_generated_map) {
205
+ if (!source_line_generated_map.has(segment.sourceLine)) {
206
+ source_line_generated_map.set(segment.sourceLine, []);
207
+ }
208
+ /** @type {SourceLineGeneratedPosition[]} */ (
209
+ source_line_generated_map.get(segment.sourceLine)
210
+ ).push({ column: segment.sourceColumn, position: gen_pos });
211
+ }
195
212
 
196
213
  // Store reverse mapping (generated to source)
197
214
  const gen_key = `${gen_pos.line}:${gen_pos.column}`;
@@ -206,7 +223,7 @@ export function build_src_to_gen_map(
206
223
  }
207
224
  }
208
225
 
209
- return [map, reverse_map];
226
+ return [map, reverse_map, source_line_generated_map];
210
227
  }
211
228
 
212
229
  /**
@@ -162,6 +162,74 @@ export function identifier_to_jsx_name(id) {
162
162
  return id;
163
163
  }
164
164
 
165
+ /**
166
+ * A JSX tag name refers to a *component* (rather than a host/DOM tag) iff:
167
+ * - it's a `JSXIdentifier` whose first character is uppercase (the convention
168
+ * every framework's JSX runtime keys off — `<div>` is a host element,
169
+ * `<Foo>` is a component), or
170
+ * - it's a `JSXMemberExpression` (e.g. `<Icons.Button />`).
171
+ *
172
+ * Used by platforms that veto static-hoisting of component JSX (Vue, Solid)
173
+ * and by core's narrower bare-component-invocation predicate.
174
+ *
175
+ * @param {any} name
176
+ * @returns {boolean}
177
+ */
178
+ export function is_component_jsx_name(name) {
179
+ if (!name || typeof name !== 'object') {
180
+ return false;
181
+ }
182
+
183
+ if (name.type === 'JSXIdentifier') {
184
+ const first = name.name?.[0];
185
+ return first != null && first >= 'A' && first <= 'Z';
186
+ }
187
+
188
+ if (name.type === 'JSXMemberExpression') {
189
+ return true;
190
+ }
191
+
192
+ return false;
193
+ }
194
+
195
+ /**
196
+ * Does this JSX subtree contain any component-shaped element (anywhere —
197
+ * including nested under host elements or inside expression containers)?
198
+ * Vue and Solid use this as their `canHoistStaticNode` predicate: hoisting a
199
+ * subtree that invokes a component into a module-level constant pins that
200
+ * component instance to module identity, which doesn't help either framework
201
+ * the way it helps React, so it's wasted output.
202
+ *
203
+ * @param {any} node
204
+ * @returns {boolean}
205
+ */
206
+ export function contains_component_jsx(node) {
207
+ if (!node || typeof node !== 'object') {
208
+ return false;
209
+ }
210
+
211
+ if (node.type === 'JSXElement') {
212
+ if (is_component_jsx_name(node.openingElement?.name)) {
213
+ return true;
214
+ }
215
+ return node.children?.some(contains_component_jsx) ?? false;
216
+ }
217
+
218
+ if (node.type === 'JSXFragment') {
219
+ return node.children?.some(contains_component_jsx) ?? false;
220
+ }
221
+
222
+ if (node.type === 'JSXExpressionContainer') {
223
+ return contains_component_jsx(node.expression);
224
+ }
225
+
226
+ if (Array.isArray(node)) {
227
+ return node.some(contains_component_jsx);
228
+ }
229
+
230
+ return false;
231
+ }
232
+
165
233
  /**
166
234
  * @param {any} node
167
235
  * @returns {boolean}
@@ -302,6 +370,58 @@ export function flatten_switch_consequent(consequent) {
302
370
  return result;
303
371
  }
304
372
 
373
+ /**
374
+ * Compute fall-through expansions for each `case` in a `switch`. JavaScript
375
+ * `switch` semantics say that once a case body executes, execution continues
376
+ * into the bodies of subsequent cases until a `break` or terminal `return` is
377
+ * hit. We pre-compute, per case, the flat list of statements that should run
378
+ * when that case is the entry point — so downstream targets (which render each
379
+ * case independently rather than executing fall-through at runtime) still
380
+ * produce the right output.
381
+ *
382
+ * Walking right-to-left lets each case reuse the next case's already-expanded
383
+ * tail without recomputation. Downstream nodes are deep-cloned when absorbed
384
+ * so each case's expanded body owns its own AST subtree.
385
+ *
386
+ * @param {any[]} cases
387
+ * @returns {Array<{ test: any, body: any[], source: any }>}
388
+ */
389
+ export function expand_switch_cases_for_fallthrough(cases) {
390
+ /** @type {Array<{ test: any, body: any[], source: any }>} */
391
+ const expanded = new Array(cases.length);
392
+ for (let i = cases.length - 1; i >= 0; i--) {
393
+ const consequent = flatten_switch_consequent(cases[i].consequent || []);
394
+ const body = [];
395
+ let has_terminal = false;
396
+ for (const child of consequent) {
397
+ if (child.type === 'BreakStatement') {
398
+ has_terminal = true;
399
+ break;
400
+ }
401
+ body.push(child);
402
+ if (child.type === 'ReturnStatement') {
403
+ has_terminal = true;
404
+ break;
405
+ }
406
+ }
407
+ // Strip locations from cloned downstream nodes. Only the original case
408
+ // (one entry up the chain) keeps `loc`/`start`/`end`; clones inlined
409
+ // into upstream cases would otherwise point editor IntelliSense at the
410
+ // same source range multiple times (one hover/go-to-definition per
411
+ // fall-through entry point), producing double/triple results in Volar.
412
+ const downstream =
413
+ !has_terminal && i + 1 < cases.length
414
+ ? expanded[i + 1].body.map((n) => clone_expression_node(n, false))
415
+ : [];
416
+ expanded[i] = {
417
+ test: cases[i].test,
418
+ body: [...body, ...downstream],
419
+ source: cases[i],
420
+ };
421
+ }
422
+ return expanded;
423
+ }
424
+
305
425
  /**
306
426
  * @param {AST.Expression | null | undefined} expression
307
427
  * @returns {boolean}