@tsrx/core 0.1.11 → 0.1.13

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.11",
6
+ "version": "0.1.13",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -70,22 +70,11 @@
70
70
  "zimmerframe": "^1.1.2"
71
71
  },
72
72
  "devDependencies": {
73
- "@solidjs/web": "2.0.0-beta.7",
74
73
  "@types/node": "^24.3.0",
75
74
  "@typescript-eslint/types": "^8.40.0",
76
75
  "@volar/language-core": "~2.4.28",
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
76
  "typescript": "^5.9.3",
82
- "vscode-languageserver-types": "^3.17.5",
83
- "vue": "3.6.0-beta.12",
84
- "vue-jsx-vapor": "^3.2.14",
85
- "@tsrx/preact": "0.1.11",
86
- "@tsrx/react": "0.2.11",
87
- "@tsrx/solid": "0.1.11",
88
- "@tsrx/vue": "0.1.11"
77
+ "vscode-languageserver-types": "^3.17.5"
89
78
  },
90
79
  "files": [
91
80
  "src",
package/src/plugin.js CHANGED
@@ -275,6 +275,7 @@ export function TSRXPlugin(config) {
275
275
  #componentDepth = 0;
276
276
  #functionBodyDepth = 0;
277
277
  #allowExpressionContainerTrailingSemicolon = false;
278
+ #tsxIslandExpressionDepth = 0;
278
279
 
279
280
  /**
280
281
  * @type {Parse.Parser['finishNode']}
@@ -478,7 +479,7 @@ export function TSRXPlugin(config) {
478
479
  }
479
480
 
480
481
  if (this.type === tt.braceL) {
481
- body.push(this.jsx_parseExpressionContainer());
482
+ body.push(this.#parseTsxIslandExpressionContainer());
482
483
  } else if (this.type === tstt.jsxTagStart) {
483
484
  body.push(super.jsx_parseElement());
484
485
  } else {
@@ -492,6 +493,84 @@ export function TSRXPlugin(config) {
492
493
  }
493
494
  }
494
495
 
496
+ #parseTsxIslandExpressionContainer() {
497
+ this.#tsxIslandExpressionDepth++;
498
+ try {
499
+ if (!this.#isAtReservedTemplateExpressionContainer()) {
500
+ return this.jsx_parseExpressionContainer();
501
+ }
502
+
503
+ const node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
504
+ this.next();
505
+ this.next();
506
+ const expression = /** @type {AST.Expression | ESTreeJSX.JSXEmptyExpression} */ (
507
+ /** @type {unknown} */ (this.parseElement())
508
+ );
509
+ node.expression = expression;
510
+ this.#popTokenContextsAfterTemplateExpressionElement(
511
+ /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (expression)),
512
+ );
513
+ this.expect(tt.braceR);
514
+ return this.finishNode(node, 'JSXExpressionContainer');
515
+ } finally {
516
+ this.#tsxIslandExpressionDepth--;
517
+ }
518
+ }
519
+
520
+ #isAtReservedTemplateExpressionContainer() {
521
+ if (this.type !== tt.braceL) {
522
+ return false;
523
+ }
524
+
525
+ let index = this.start + 1;
526
+ while (index < this.input.length) {
527
+ const ch = this.input.charCodeAt(index);
528
+ if (
529
+ ch === CharCode.space ||
530
+ ch === CharCode.tab ||
531
+ ch === CharCode.lineFeed ||
532
+ ch === CharCode.carriageReturn
533
+ ) {
534
+ index++;
535
+ } else {
536
+ break;
537
+ }
538
+ }
539
+
540
+ if (this.input.charCodeAt(index) !== CharCode.lessThan) {
541
+ return false;
542
+ }
543
+
544
+ return this.#isReservedTemplateTagNameStart(index + 1);
545
+ }
546
+
547
+ /**
548
+ * @param {number} index
549
+ */
550
+ #isReservedTemplateTagNameStart(index) {
551
+ const char_after_tsx = this.input.charCodeAt(index + 3);
552
+ const char_after_tsrx = this.input.charCodeAt(index + 4);
553
+ return (
554
+ (this.input.startsWith('tsx', index) &&
555
+ (index + 3 >= this.input.length ||
556
+ char_after_tsx === CharCode.greaterThan ||
557
+ char_after_tsx === CharCode.slash ||
558
+ char_after_tsx === CharCode.space ||
559
+ char_after_tsx === CharCode.tab ||
560
+ char_after_tsx === CharCode.lineFeed ||
561
+ char_after_tsx === CharCode.carriageReturn ||
562
+ char_after_tsx === CharCode.colon)) ||
563
+ (this.input.startsWith('tsrx', index) &&
564
+ (index + 4 >= this.input.length ||
565
+ char_after_tsrx === CharCode.greaterThan ||
566
+ char_after_tsrx === CharCode.slash ||
567
+ char_after_tsrx === CharCode.space ||
568
+ char_after_tsrx === CharCode.tab ||
569
+ char_after_tsrx === CharCode.lineFeed ||
570
+ char_after_tsrx === CharCode.carriageReturn))
571
+ );
572
+ }
573
+
495
574
  /**
496
575
  * @param {AST.Tsx | AST.TsxCompat} island
497
576
  */
@@ -747,7 +826,11 @@ export function TSRXPlugin(config) {
747
826
  }
748
827
 
749
828
  #isDoubleQuotedTextChildStart() {
750
- if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
829
+ const current_template_node = this.#path.findLast(
830
+ (n) =>
831
+ n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
832
+ );
833
+ if (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') {
751
834
  return false;
752
835
  }
753
836
 
@@ -1949,8 +2032,17 @@ export function TSRXPlugin(config) {
1949
2032
  );
1950
2033
 
1951
2034
  if (this.eat(tt.braceL)) {
1952
- const inside_tsx = this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1953
- if (inside_tsx) {
2035
+ const current_template_node = this.#path.findLast(
2036
+ (n) =>
2037
+ n.type === 'Element' ||
2038
+ n.type === 'Tsx' ||
2039
+ n.type === 'Tsrx' ||
2040
+ n.type === 'TsxCompat',
2041
+ );
2042
+ if (
2043
+ current_template_node?.type === 'Tsx' ||
2044
+ current_template_node?.type === 'TsxCompat'
2045
+ ) {
1954
2046
  if (this.type === tt.ellipsis) {
1955
2047
  this.expect(tt.ellipsis);
1956
2048
  /** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
@@ -2202,10 +2294,11 @@ export function TSRXPlugin(config) {
2202
2294
 
2203
2295
  /** @type {Parse.Parser['jsx_readToken']} */
2204
2296
  jsx_readToken() {
2205
- const inside_tsx_compat = this.#path.findLast(
2206
- (n) => n.type === 'TsxCompat' || n.type === 'Tsx',
2297
+ const current_template_node = this.#path.findLast(
2298
+ (n) =>
2299
+ n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2207
2300
  );
2208
- if (inside_tsx_compat) {
2301
+ if (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') {
2209
2302
  return super.jsx_readToken();
2210
2303
  }
2211
2304
  let out = '',
@@ -2385,12 +2478,6 @@ export function TSRXPlugin(config) {
2385
2478
  * @type {Parse.Parser['jsx_parseElement']}
2386
2479
  */
2387
2480
  jsx_parseElement() {
2388
- const inside_tsx = this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
2389
- if (inside_tsx) {
2390
- // Inside tsx/tsx:*, let acorn-jsx handle it normally
2391
- return super.jsx_parseElement();
2392
- }
2393
-
2394
2481
  // Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
2395
2482
  // Current token is jsxTagStart, this.end is position after '<'
2396
2483
  const tag_name_start = this.end;
@@ -2417,6 +2504,20 @@ export function TSRXPlugin(config) {
2417
2504
  char_after_tsrx === CharCode.lineFeed ||
2418
2505
  char_after_tsrx === CharCode.carriageReturn);
2419
2506
 
2507
+ const current_template_node = this.#path.findLast(
2508
+ (n) =>
2509
+ n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2510
+ );
2511
+ if (
2512
+ (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') &&
2513
+ !is_tsrx_tag
2514
+ ) {
2515
+ // Inside tsx/tsx:*, let acorn-jsx handle regular TSX tags normally.
2516
+ // Nested <tsrx> still needs Ripple's native template parser so it
2517
+ // can lower through the same path as <tsrx> in component bodies.
2518
+ return super.jsx_parseElement();
2519
+ }
2520
+
2420
2521
  if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
2421
2522
  // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
2422
2523
  // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
@@ -2424,9 +2525,21 @@ export function TSRXPlugin(config) {
2424
2525
  const parsed = /** @type {import('estree-jsx').JSXElement} */ (
2425
2526
  /** @type {unknown} */ (this.parseElement())
2426
2527
  );
2427
- this.#popTokenContextsAfterTemplateExpressionElement(
2428
- /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2429
- );
2528
+ if (
2529
+ current_template_node?.type !== 'Tsx' &&
2530
+ current_template_node?.type !== 'TsxCompat'
2531
+ ) {
2532
+ this.#popTokenContextsAfterTemplateExpressionElement(
2533
+ /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2534
+ );
2535
+ } else if (this.type === tt.braceR && this.curContext() === tstc.tc_expr) {
2536
+ if (this.#tsxIslandExpressionDepth === 0) {
2537
+ // Acorn still owns the surrounding JSX expression container.
2538
+ // Keep a block-expression context for its closing `}` so the
2539
+ // parent TSX tag continues tokenizing as JSX afterward.
2540
+ this.context.push(b_expr);
2541
+ }
2542
+ }
2430
2543
  return parsed;
2431
2544
  }
2432
2545
 
@@ -2887,9 +3000,24 @@ export function TSRXPlugin(config) {
2887
3000
  parseTemplateBody(body) {
2888
3001
  const inside_func =
2889
3002
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
2890
- const inside_tsx_island = this.#path.findLast(
2891
- (n) => n.type === 'Tsx' || n.type === 'TsxCompat',
3003
+ const current_template_node = this.#path.findLast(
3004
+ (n) =>
3005
+ n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2892
3006
  );
3007
+ const inside_tsx_island =
3008
+ current_template_node?.type === 'Tsx' || current_template_node?.type === 'TsxCompat'
3009
+ ? current_template_node
3010
+ : null;
3011
+
3012
+ if (current_template_node?.type === 'Tsrx' && this.type === tstt.jsxText) {
3013
+ while (this.curContext() === tstc.tc_expr) {
3014
+ this.context.pop();
3015
+ }
3016
+ this.pos = this.start;
3017
+ this.next();
3018
+ this.parseTemplateBody(body);
3019
+ return;
3020
+ }
2893
3021
 
2894
3022
  if (!inside_func) {
2895
3023
  if (this.type.label === 'continue') {
package/src/scope.js CHANGED
@@ -205,11 +205,18 @@ export function create_scopes(ast, root, parent, error_options) {
205
205
  for (const declarator of node.declarations) {
206
206
  /** @type {Binding[]} */
207
207
  const bindings = [];
208
+ const initial = /** @type {AST.Expression | AST.Tsx | AST.Tsrx | null} */ (declarator.init);
208
209
 
209
210
  state.scope.declarators.set(declarator, bindings);
210
211
 
211
212
  for (const id of extract_identifiers(declarator.id)) {
212
- const binding = state.scope.declare(id, 'normal', node.kind, declarator.init);
213
+ const binding = state.scope.declare(id, 'normal', node.kind, initial);
214
+ if (initial?.type === 'Tsx' || initial?.type === 'Tsrx') {
215
+ binding.metadata = {
216
+ ...(binding.metadata ?? {}),
217
+ is_template_value: true,
218
+ };
219
+ }
213
220
  bindings.push(binding);
214
221
  }
215
222
  }
@@ -294,6 +294,25 @@ export function build_line_offsets(text) {
294
294
  return offsets;
295
295
  }
296
296
 
297
+ /**
298
+ * ONLY USE THIS FOR TESTS
299
+ *
300
+ * @param {CodeMapping[]} mappings
301
+ * @param {number} source_offset
302
+ * @param {number} generated_offset
303
+ * @param {number} length
304
+ * @returns {CodeMapping | undefined}
305
+ */
306
+ export function find_exact_mapping(mappings, source_offset, generated_offset, length) {
307
+ return mappings.find(
308
+ (mapping) =>
309
+ mapping.sourceOffsets[0] === source_offset &&
310
+ mapping.generatedOffsets[0] === generated_offset &&
311
+ mapping.lengths[0] === length &&
312
+ mapping.generatedLengths[0] === length,
313
+ );
314
+ }
315
+
297
316
  /**
298
317
  * DO NOT EXPORT THIS FUNCTION!
299
318
  * THE FIX NEEDS TO HAPPEN IN THE TRANSFORMER, SEGMENTS OR PARSER
@@ -221,6 +221,8 @@ export function tsx_with_ts_locations() {
221
221
  // JS nodes whose esrap printer emits no location marker, causing
222
222
  // segments.js get_mapping_from_node() to throw when it asks for the
223
223
  // generated position of the node's start (or end).
224
+ 'ClassDeclaration',
225
+ 'ClassExpression',
224
226
  'IfStatement',
225
227
  'NewExpression',
226
228
  'MemberExpression',
@@ -345,7 +345,7 @@ function extract_classes(node, src_to_gen_map, gen_line_offsets, src_line_offset
345
345
  * @param {PostProcessingChanges } post_processing_changes - Optional post-processing changes
346
346
  * @param {number[]} line_offsets - Pre-computed line offsets array for generated code
347
347
  * @param {CompileError[]} [errors]
348
- * @returns {Omit<VolarMappingsResult, 'errors'>}
348
+ * @returns {Omit<VolarMappingsResult, 'errors' | 'sourceAst'>}
349
349
  */
350
350
  export function convert_source_map_to_mappings(
351
351
  ast,
@@ -1528,6 +1528,18 @@ export function convert_source_map_to_mappings(
1528
1528
  }
1529
1529
  return;
1530
1530
  } else if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
1531
+ if (node.loc) {
1532
+ tokens.push({
1533
+ source: 'class',
1534
+ generated: 'class',
1535
+ loc: {
1536
+ start: { line: node.loc.start.line, column: node.loc.start.column },
1537
+ end: { line: node.loc.start.line, column: node.loc.start.column + 'class'.length },
1538
+ },
1539
+ metadata: {},
1540
+ });
1541
+ }
1542
+
1531
1543
  // Visit in source order: id, superClass, body
1532
1544
  if (node.id) {
1533
1545
  visit(node.id);
@@ -2402,6 +2414,7 @@ export function create_volar_mappings_result({
2402
2414
 
2403
2415
  return {
2404
2416
  ...result,
2417
+ sourceAst: ast_from_source,
2405
2418
  errors,
2406
2419
  };
2407
2420
  }
package/types/index.d.ts CHANGED
@@ -1247,7 +1247,9 @@ export interface Binding {
1247
1247
  | AST.FunctionDeclaration
1248
1248
  | AST.ClassDeclaration
1249
1249
  | AST.ImportDeclaration
1250
- | AST.TSModuleDeclaration;
1250
+ | AST.TSModuleDeclaration
1251
+ | AST.Tsx
1252
+ | AST.Tsrx;
1251
1253
  /** Whether this binding has been reassigned */
1252
1254
  reassigned: boolean;
1253
1255
  /** Whether this binding has been mutated (property access) */
@@ -1261,6 +1263,7 @@ export interface Binding {
1261
1263
  is_dynamic_component?: boolean;
1262
1264
  pattern?: AST.Identifier;
1263
1265
  is_ripple_object?: boolean;
1266
+ is_template_value?: boolean;
1264
1267
  } | null;
1265
1268
  /** Kind of binding */
1266
1269
  kind: BindingKind;
@@ -1340,7 +1343,9 @@ export interface ScopeInterface {
1340
1343
  | AST.FunctionDeclaration
1341
1344
  | AST.ClassDeclaration
1342
1345
  | AST.ImportDeclaration
1343
- | AST.TSModuleDeclaration,
1346
+ | AST.TSModuleDeclaration
1347
+ | AST.Tsx
1348
+ | AST.Tsrx,
1344
1349
  ): Binding;
1345
1350
  /** Get binding by name */
1346
1351
  get(name: string): Binding | null;
@@ -1416,6 +1421,7 @@ export interface TransformServerState extends BaseState {
1416
1421
  template_child?: boolean;
1417
1422
  skip_regular_blocks?: boolean;
1418
1423
  in_regular_block?: boolean;
1424
+ jsx_to_tsrx_element?: boolean;
1419
1425
  }
1420
1426
 
1421
1427
  export type UpdateList = Array<
@@ -1450,6 +1456,7 @@ export interface TransformClientState extends BaseState {
1450
1456
  skip_children_traversal: boolean;
1451
1457
  return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1452
1458
  is_tsrx_element?: boolean;
1459
+ jsx_to_tsrx_element?: boolean;
1453
1460
  }
1454
1461
 
1455
1462
  /** Override zimmerframe types and provide our own */
@@ -1585,6 +1592,7 @@ export interface VolarMappingsResult {
1585
1592
  mappings: CodeMapping[];
1586
1593
  cssMappings: CodeMapping[];
1587
1594
  errors: CompileError[];
1595
+ sourceAst: AST.Program;
1588
1596
  }
1589
1597
 
1590
1598
  /**