@tsrx/core 0.0.14 → 0.0.15

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/README.md CHANGED
@@ -61,15 +61,20 @@ component Button(props: Props) {
61
61
  ### 2. JSX-as-statements
62
62
 
63
63
  Inside a `component` body, JSX elements are valid _statement_ forms. They describe
64
- rendered output and are not expressions — they have no value.
64
+ rendered output and are not expressions — they have no value. Static text may be
65
+ written as a direct double-quoted child; dynamic values and other JavaScript
66
+ expressions stay inside `{}`.
65
67
 
66
68
  ```tsx
67
69
  component Greeting() {
68
- <h1>{'Hello'}</h1>
69
- <p>{'Welcome'}</p>
70
+ <h1>"Hello"</h1>
71
+ <p>"Welcome"</p>
70
72
  }
71
73
  ```
72
74
 
75
+ Only double quotes have direct-child text meaning. Single-quoted strings and
76
+ template literals remain JavaScript expressions and must be written inside `{}`.
77
+
73
78
  Elsewhere (outside a `component` body), JSX remains an expression, as in standard
74
79
  JSX.
75
80
 
@@ -84,9 +89,9 @@ introduced — but framework compilers treat them as _reactive_ boundaries.
84
89
  ```tsx
85
90
  component List(props: { items: Item[]; showHeader: boolean }) {
86
91
  if (props.showHeader) {
87
- <h1>{'Items'}</h1>
92
+ <h1>"Items"</h1>
88
93
  } else {
89
- <h2>{'(no header)'}</h2>
94
+ <h2>"(no header)"</h2>
90
95
  }
91
96
 
92
97
  for (const item of props.items) {
@@ -95,10 +100,10 @@ component List(props: { items: Item[]; showHeader: boolean }) {
95
100
 
96
101
  switch (props.items.length) {
97
102
  case 0:
98
- <p>{'empty'}</p>
103
+ <p>"empty"</p>
99
104
  break;
100
105
  default:
101
- <p>{'has items'}</p>
106
+ <p>"has items"</p>
102
107
  }
103
108
 
104
109
  try {
@@ -169,7 +174,7 @@ children).
169
174
 
170
175
  ```tsx
171
176
  component Page() {
172
- const header = <tsx><h1>{'Hello'}</h1></tsx>;
177
+ const header = <tsx><h1>Hello</h1></tsx>;
173
178
  renderSomewhereElse(header);
174
179
  }
175
180
  ```
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.14",
6
+ "version": "0.0.15",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -175,6 +175,7 @@ export function TSRXPlugin(config) {
175
175
  class TSRXParser extends Parser {
176
176
  /** @type {AST.Node[]} */
177
177
  #path = [];
178
+ #allowTagStartAfterDoubleQuotedText = false;
178
179
  #commentContextId = 0;
179
180
  #loose = false;
180
181
  /** @type {import('../types/index').CompileError[] | undefined} */
@@ -558,6 +559,10 @@ export function TSRXPlugin(config) {
558
559
  * @type {Parse.Parser['getTokenFromCode']}
559
560
  */
560
561
  getTokenFromCode(code) {
562
+ if (code !== 60) {
563
+ this.#allowTagStartAfterDoubleQuotedText = false;
564
+ }
565
+
561
566
  if (code === 60) {
562
567
  // < character
563
568
  const inComponent = this.#path.findLast((n) => n.type === 'Component');
@@ -634,8 +639,14 @@ export function TSRXPlugin(config) {
634
639
  // Inside component template bodies, allow adjacent tags without requiring
635
640
  // a newline/indentation before the next '<'. This is important for inputs
636
641
  // like `<div />` and `</div><style>...</style>` which Prettier formats.
637
- if (prevNonWhitespaceChar === 123 /* '{' */ || prevNonWhitespaceChar === 62 /* '>' */) {
642
+ if (
643
+ (prevNonWhitespaceChar === 34 /* '"' */ &&
644
+ this.#allowTagStartAfterDoubleQuotedText) ||
645
+ prevNonWhitespaceChar === 123 /* '{' */ ||
646
+ prevNonWhitespaceChar === 62 /* '>' */
647
+ ) {
638
648
  if (!isWhitespaceAfterLt) {
649
+ this.#allowTagStartAfterDoubleQuotedText = false;
639
650
  ++this.pos;
640
651
  return this.finishToken(tstt.jsxTagStart);
641
652
  }
@@ -712,6 +723,7 @@ export function TSRXPlugin(config) {
712
723
  }
713
724
  }
714
725
  }
726
+ this.#allowTagStartAfterDoubleQuotedText = false;
715
727
  return super.getTokenFromCode(code);
716
728
  }
717
729
 
@@ -1300,6 +1312,30 @@ export function TSRXPlugin(config) {
1300
1312
  );
1301
1313
  }
1302
1314
 
1315
+ /**
1316
+ * @returns {AST.TextNode}
1317
+ */
1318
+ parseDoubleQuotedTextChild() {
1319
+ const node = /** @type {AST.TextNode} */ (this.startNode());
1320
+ const expression = /** @type {AST.Literal} */ (this.startNode());
1321
+ const raw = this.input.slice(this.start, this.end);
1322
+ const end = this.end;
1323
+ const endLoc = this.endLoc;
1324
+
1325
+ expression.value = this.value;
1326
+ expression.raw = raw;
1327
+ node.expression = this.finishNodeAt(expression, 'Literal', end, endLoc);
1328
+
1329
+ this.#allowTagStartAfterDoubleQuotedText = true;
1330
+ try {
1331
+ this.next();
1332
+ } finally {
1333
+ this.#allowTagStartAfterDoubleQuotedText = false;
1334
+ }
1335
+
1336
+ return this.finishNodeAt(node, 'Text', end, endLoc);
1337
+ }
1338
+
1303
1339
  /**
1304
1340
  * @type {Parse.Parser['jsx_parseAttribute']}
1305
1341
  */
@@ -2293,6 +2329,8 @@ export function TSRXPlugin(config) {
2293
2329
  delete node.text;
2294
2330
  }
2295
2331
  body.push(node);
2332
+ } else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
2333
+ body.push(this.parseDoubleQuotedTextChild());
2296
2334
  } else if (this.type === tt.braceR) {
2297
2335
  // Leaving a component/template body. We may still be in TSX/JSX tokenization
2298
2336
  // context (e.g. after parsing markup), but the closing `}` is a JS token.
@@ -1450,12 +1450,6 @@ export function convert_source_map_to_mappings(
1450
1450
  }
1451
1451
  }
1452
1452
 
1453
- if (node.loc) {
1454
- mappings.push(
1455
- get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
1456
- );
1457
- }
1458
-
1459
1453
  return;
1460
1454
  } else if (node.type === 'SwitchCase') {
1461
1455
  // Visit in source order: test, consequent
@@ -1705,14 +1699,21 @@ export function convert_source_map_to_mappings(
1705
1699
  // Generic spans can be emitted by downstream transforms with sparse source-map
1706
1700
  // coverage around the angle-bracket delimiters. Skip missing whole-node mappings
1707
1701
  // instead of crashing Volar, and rely on child type-node mappings instead.
1708
- const mapping = get_mapping_from_node(
1709
- node,
1710
- src_to_gen_map,
1711
- gen_line_offsets,
1712
- mapping_data_verify_only,
1713
- );
1714
- if (!(mapping instanceof Error)) {
1702
+ try {
1703
+ const mapping = get_mapping_from_node(
1704
+ node,
1705
+ src_to_gen_map,
1706
+ gen_line_offsets,
1707
+ mapping_data_verify_only,
1708
+ );
1715
1709
  mappings.push(mapping);
1710
+ } catch (error) {
1711
+ if (
1712
+ !(error instanceof Error) ||
1713
+ !error.message.startsWith('No source map entry for position')
1714
+ ) {
1715
+ throw error;
1716
+ }
1716
1717
  }
1717
1718
  }
1718
1719
  // Generic type parameters - visit to collect type variable names
package/types/parse.d.ts CHANGED
@@ -1174,6 +1174,8 @@ export namespace Parse {
1174
1174
 
1175
1175
  parseElement(): AST.Element | AST.Tsx | AST.TsxCompat;
1176
1176
 
1177
+ parseDoubleQuotedTextChild(): AST.TextNode;
1178
+
1177
1179
  parseTemplateBody(
1178
1180
  body: (AST.Statement | AST.Node | ESTreeJSX.JSXText | ESTreeJSX.JSXElement['children'])[],
1179
1181
  ): void;