@tsrx/core 0.1.48 → 0.1.49
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 +1 -1
- package/src/index.js +0 -1
- package/src/plugin.js +67 -16
- package/src/transform/await.js +14 -12
- package/src/transform/jsx/ast-builders.js +2 -81
- package/src/transform/jsx/helpers.js +11 -6
- package/src/transform/jsx/index.js +951 -727
- package/src/transform/jsx-hoist.js +2 -1
- package/src/transform/jsx-interleave.js +22 -36
- package/src/transform/scoping.js +4 -2
- package/src/transform/segments.js +20 -6
- package/src/utils/ast.js +37 -0
- package/src/utils/builders.js +22 -15
- package/types/helpers.d.ts +1 -2
- package/types/index.d.ts +79 -16
- package/types/jsx-platform.d.ts +18 -8
- package/types/parse.d.ts +2 -4
- package/types/runtime/ref.d.ts +1 -5
package/package.json
CHANGED
package/src/index.js
CHANGED
package/src/plugin.js
CHANGED
|
@@ -273,6 +273,8 @@ export function TSRXPlugin(config) {
|
|
|
273
273
|
#controlFlowBlockAllowsNativeReturn = false;
|
|
274
274
|
#parsingJSXSwitchCaseScriptStatementDepth = 0;
|
|
275
275
|
#templateControlFlowBlockDepth = 0;
|
|
276
|
+
/** @type {AST.NodeWithLocation | null} */
|
|
277
|
+
#lastClauseKeywordSpan = null;
|
|
276
278
|
#templateControlFlowTryDepth = 0;
|
|
277
279
|
/** @type {Parse.Parser['context']} */
|
|
278
280
|
context = [b_stat];
|
|
@@ -1518,43 +1520,57 @@ export function TSRXPlugin(config) {
|
|
|
1518
1520
|
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1519
1521
|
this.#readingJSXControlFlowHeader = true;
|
|
1520
1522
|
try {
|
|
1521
|
-
node =
|
|
1522
|
-
this
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1523
|
+
node = /** @type {AST.JSXForExpression} */ (
|
|
1524
|
+
this.#finishJSXControlFlowExpression(
|
|
1525
|
+
this.parseStatement(null),
|
|
1526
|
+
'JSXForExpression',
|
|
1527
|
+
start,
|
|
1528
|
+
startLoc,
|
|
1529
|
+
)
|
|
1526
1530
|
);
|
|
1527
1531
|
} finally {
|
|
1528
1532
|
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1529
1533
|
this.#templateControlFlowBlockDepth--;
|
|
1530
1534
|
}
|
|
1531
1535
|
if (
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1536
|
+
node.statementType !== 'ForOfStatement' &&
|
|
1537
|
+
node.statementType !== 'ForInStatement' &&
|
|
1538
|
+
node.statementType !== 'ForStatement'
|
|
1535
1539
|
) {
|
|
1536
1540
|
this.raise(start, 'Expected `for` after `@`.');
|
|
1537
1541
|
}
|
|
1538
|
-
if (
|
|
1539
|
-
this.raise(
|
|
1540
|
-
/** @type {any} */ (node).body?.start ?? start,
|
|
1541
|
-
'Expected `{` after JSX control-flow directive.',
|
|
1542
|
-
);
|
|
1542
|
+
if (node.body?.type !== 'BlockStatement') {
|
|
1543
|
+
this.raise(node.body?.start ?? start, 'Expected `{` after JSX control-flow directive.');
|
|
1543
1544
|
}
|
|
1544
1545
|
if (this.#eatJSXForEmptyKeyword()) {
|
|
1545
1546
|
if (this.type !== tt.braceL) {
|
|
1546
1547
|
this.raise(this.start, 'Expected `{` after JSX control-flow directive.');
|
|
1547
1548
|
}
|
|
1549
|
+
const emptyKeyword = this.#lastClauseKeywordSpan;
|
|
1550
|
+
let empty;
|
|
1548
1551
|
this.#templateControlFlowBlockDepth++;
|
|
1549
1552
|
try {
|
|
1550
|
-
|
|
1553
|
+
empty = this.parseBlock();
|
|
1551
1554
|
} finally {
|
|
1552
1555
|
this.#templateControlFlowBlockDepth--;
|
|
1553
1556
|
}
|
|
1557
|
+
node.empty = empty;
|
|
1558
|
+
node.emptyKeyword = emptyKeyword;
|
|
1559
|
+
// `@empty { … }` is part of the `@for` statement, but the node was
|
|
1560
|
+
// already finished at the end of the for BODY (the clause is parsed
|
|
1561
|
+
// after `#finishJSXControlFlowExpression`, unlike `@else`/`@catch`,
|
|
1562
|
+
// which their own `parseStatement` consumes first). Extend it, or
|
|
1563
|
+
// every consumer that slices by range — editor mappings, the
|
|
1564
|
+
// playground's AST/position tracking, formatters, diagnostics —
|
|
1565
|
+
// truncates the statement before its `@empty` clause.
|
|
1566
|
+
node.end = empty.end;
|
|
1567
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.end =
|
|
1568
|
+
/** @type {AST.NodeWithLocation} */ (empty).loc.end;
|
|
1569
|
+
if (node.range) node.range[1] = /** @type {number} */ (empty.end);
|
|
1554
1570
|
} else if (this.#isUnprefixedDirectiveClauseContinuation('empty', ['{'])) {
|
|
1555
1571
|
this.raise(this.start, 'Expected `@empty` after `@for` block.');
|
|
1556
1572
|
} else {
|
|
1557
|
-
|
|
1573
|
+
node.empty = null;
|
|
1558
1574
|
}
|
|
1559
1575
|
return node;
|
|
1560
1576
|
}
|
|
@@ -1584,6 +1600,7 @@ export function TSRXPlugin(config) {
|
|
|
1584
1600
|
* @param {string} keyword
|
|
1585
1601
|
*/
|
|
1586
1602
|
#eatJSXDirectiveClauseKeyword(keyword) {
|
|
1603
|
+
this.#lastClauseKeywordSpan = null;
|
|
1587
1604
|
const keywordStart = skip_whitespace_from(this.input, this.start);
|
|
1588
1605
|
if (this.input.charCodeAt(keywordStart) !== CharCode.at) {
|
|
1589
1606
|
return false;
|
|
@@ -1596,6 +1613,19 @@ export function TSRXPlugin(config) {
|
|
|
1596
1613
|
return false;
|
|
1597
1614
|
}
|
|
1598
1615
|
|
|
1616
|
+
// The clause keyword is the only authored spelling of `@empty`/`@else`/
|
|
1617
|
+
// `@catch` and friends: the clause node itself starts at its `{`, so
|
|
1618
|
+
// without this span nothing in the tree points at the keyword and
|
|
1619
|
+
// tooling cannot resolve a cursor placed on it.
|
|
1620
|
+
const keywordEnd = wordStart + keyword.length;
|
|
1621
|
+
this.#lastClauseKeywordSpan = {
|
|
1622
|
+
start: keywordStart,
|
|
1623
|
+
end: keywordEnd,
|
|
1624
|
+
loc: {
|
|
1625
|
+
start: acorn.getLineInfo(this.input, keywordStart),
|
|
1626
|
+
end: acorn.getLineInfo(this.input, keywordEnd),
|
|
1627
|
+
},
|
|
1628
|
+
};
|
|
1599
1629
|
this.pos = wordStart;
|
|
1600
1630
|
this.start = wordStart;
|
|
1601
1631
|
this.startLoc = acorn.getLineInfo(this.input, wordStart);
|
|
@@ -1714,6 +1744,7 @@ export function TSRXPlugin(config) {
|
|
|
1714
1744
|
node.alternate = null;
|
|
1715
1745
|
|
|
1716
1746
|
if (this.#eatJSXDirectiveClauseKeyword('else')) {
|
|
1747
|
+
node.alternateKeyword = this.#lastClauseKeywordSpan;
|
|
1717
1748
|
node.alternate = this.#eatJSXDirectiveBareClauseKeyword('if')
|
|
1718
1749
|
? this.#parseTemplateIfStatement()
|
|
1719
1750
|
: /** @type {AST.Statement} */ (this.#parseTemplateControlFlowStatement());
|
|
@@ -1758,6 +1789,9 @@ export function TSRXPlugin(config) {
|
|
|
1758
1789
|
this.startNodeAt(clauseStart, clauseStartLoc)
|
|
1759
1790
|
);
|
|
1760
1791
|
current.consequent = [];
|
|
1792
|
+
// `@case`/`@default` is the arm's only authored keyword; the node
|
|
1793
|
+
// itself starts before the leading whitespace.
|
|
1794
|
+
current.keyword = this.#lastClauseKeywordSpan;
|
|
1761
1795
|
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1762
1796
|
this.#readingJSXControlFlowHeader = true;
|
|
1763
1797
|
try {
|
|
@@ -3377,6 +3411,8 @@ export function TSRXPlugin(config) {
|
|
|
3377
3411
|
let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
|
|
3378
3412
|
this.#jsxExpressionContainerDepth++;
|
|
3379
3413
|
let pushed_context_baseline = false;
|
|
3414
|
+
/** @type {number} */
|
|
3415
|
+
let context_baseline;
|
|
3380
3416
|
try {
|
|
3381
3417
|
this.next();
|
|
3382
3418
|
|
|
@@ -3384,7 +3420,8 @@ export function TSRXPlugin(config) {
|
|
|
3384
3420
|
// context is on the stack. A control-flow directive parsed inside this
|
|
3385
3421
|
// container must not strip anything below this floor (see
|
|
3386
3422
|
// `#filterTemplateScriptContexts`).
|
|
3387
|
-
this
|
|
3423
|
+
context_baseline = this.context.length;
|
|
3424
|
+
this.#expressionContainerContextBaselines.push(context_baseline);
|
|
3388
3425
|
this.#expressionContainerPathBaselines.push(this.#path.length);
|
|
3389
3426
|
pushed_context_baseline = true;
|
|
3390
3427
|
|
|
@@ -3401,6 +3438,18 @@ export function TSRXPlugin(config) {
|
|
|
3401
3438
|
this.next();
|
|
3402
3439
|
}
|
|
3403
3440
|
if (!consumeBraceAfterScope) {
|
|
3441
|
+
// A control-flow directive expression restores the context stack from
|
|
3442
|
+
// a snapshot taken inside this container
|
|
3443
|
+
// (`#parseTemplateControlFlowBlock`), so the container's closing `}`
|
|
3444
|
+
// — read while that stale snapshot was active — pops the wrong entry
|
|
3445
|
+
// and leaves stale brace contexts above the enclosing tag's contexts.
|
|
3446
|
+
// Once the `}` has been read the stack must be back at one below the
|
|
3447
|
+
// baseline (the container's own brace context popped); drop anything
|
|
3448
|
+
// above that so the token after `}` (e.g. the `>` finishing the
|
|
3449
|
+
// enclosing opening tag) tokenizes in the right context.
|
|
3450
|
+
if (this.type === tt.braceR && this.context.length >= context_baseline) {
|
|
3451
|
+
this.context.length = context_baseline - 1;
|
|
3452
|
+
}
|
|
3404
3453
|
this.expect(tt.braceR);
|
|
3405
3454
|
}
|
|
3406
3455
|
} finally {
|
|
@@ -3719,6 +3768,7 @@ export function TSRXPlugin(config) {
|
|
|
3719
3768
|
node.handler = null;
|
|
3720
3769
|
|
|
3721
3770
|
if (this.#eatJSXDirectiveClauseKeyword('pending')) {
|
|
3771
|
+
node.pendingKeyword = this.#lastClauseKeywordSpan;
|
|
3722
3772
|
node.pending = this.#parseTemplateControlFlowReturnBlock();
|
|
3723
3773
|
} else if (this.#isUnprefixedDirectiveClauseContinuation('pending', ['{'])) {
|
|
3724
3774
|
this.raise(this.start, 'Expected `@pending` after `@try` block.');
|
|
@@ -3729,6 +3779,7 @@ export function TSRXPlugin(config) {
|
|
|
3729
3779
|
const clauseStart = this.start;
|
|
3730
3780
|
const clauseStartLoc = this.startLoc;
|
|
3731
3781
|
if (this.#eatJSXDirectiveClauseKeyword('catch')) {
|
|
3782
|
+
node.handlerKeyword = this.#lastClauseKeywordSpan;
|
|
3732
3783
|
if (this.type === tt._catch || this.value === 'catch') {
|
|
3733
3784
|
this.next();
|
|
3734
3785
|
}
|
package/src/transform/await.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
|
|
3
|
+
import { child_nodes, is_ast_node } from '../utils/ast.js';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
* @param {
|
|
3
|
-
* @returns {
|
|
6
|
+
* @param {AST.Node[]} body_nodes
|
|
7
|
+
* @returns {AST.TSRXAwaitNode | null}
|
|
4
8
|
*/
|
|
5
9
|
export function find_first_top_level_await_in_tsrx_function_body(body_nodes) {
|
|
6
10
|
for (const node of body_nodes) {
|
|
@@ -12,12 +16,12 @@ export function find_first_top_level_await_in_tsrx_function_body(body_nodes) {
|
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
* @param {
|
|
19
|
+
* @param {AST.Node | AST.Node[] | null | undefined} node
|
|
16
20
|
* @param {boolean} inside_nested_function
|
|
17
|
-
* @returns {
|
|
21
|
+
* @returns {AST.TSRXAwaitNode | null}
|
|
18
22
|
*/
|
|
19
23
|
export function find_first_top_level_await(node, inside_nested_function) {
|
|
20
|
-
if (!node
|
|
24
|
+
if (!node) {
|
|
21
25
|
return null;
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -45,17 +49,15 @@ export function find_first_top_level_await(node, inside_nested_function) {
|
|
|
45
49
|
if (
|
|
46
50
|
node.type === 'AwaitExpression' ||
|
|
47
51
|
(node.type === 'ForOfStatement' && node.await === true) ||
|
|
48
|
-
(node.type === 'JSXForExpression' &&
|
|
52
|
+
(node.type === 'JSXForExpression' &&
|
|
53
|
+
node.statementType === 'ForOfStatement' &&
|
|
54
|
+
node.await === true)
|
|
49
55
|
) {
|
|
50
56
|
return node;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const found = find_first_top_level_await(node[key], false);
|
|
59
|
+
for (const child of child_nodes(node)) {
|
|
60
|
+
const found = find_first_top_level_await(child, false);
|
|
59
61
|
if (found) return found;
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** @import * as ESTreeJSX from 'estree-jsx' */
|
|
3
3
|
|
|
4
4
|
import * as b from '../../utils/builders.js';
|
|
5
|
-
import { has_location } from '../../utils/ast.js';
|
|
5
|
+
import { has_location, is_ast_node } from '../../utils/ast.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* AST-building utilities shared across every JSX target (React, Preact,
|
|
@@ -16,7 +16,7 @@ import { has_location } from '../../utils/ast.js';
|
|
|
16
16
|
*
|
|
17
17
|
* @template {AST.Node} T
|
|
18
18
|
* @param {T} node
|
|
19
|
-
* @param {AST.Node | AST.NodeWithLocation | undefined} source_node
|
|
19
|
+
* @param {AST.Node | AST.NodeWithLocation | null | undefined} source_node
|
|
20
20
|
* @returns {T}
|
|
21
21
|
*/
|
|
22
22
|
export function set_loc(node, source_node) {
|
|
@@ -127,14 +127,6 @@ export function add_extra_source_mappings_from_matching_expression(generated, so
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
131
|
-
* @param {unknown} value
|
|
132
|
-
* @returns {value is AST.Node}
|
|
133
|
-
*/
|
|
134
|
-
function is_ast_node(value) {
|
|
135
|
-
return !!value && typeof value === 'object' && 'type' in value;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
130
|
/**
|
|
139
131
|
* @returns {AST.Literal}
|
|
140
132
|
*/
|
|
@@ -385,77 +377,6 @@ export function flatten_switch_consequent(consequent) {
|
|
|
385
377
|
return result;
|
|
386
378
|
}
|
|
387
379
|
|
|
388
|
-
/**
|
|
389
|
-
* @param {AST.Expression | null | undefined} expression
|
|
390
|
-
* @returns {boolean}
|
|
391
|
-
*/
|
|
392
|
-
function is_static_string_expression(expression) {
|
|
393
|
-
if (!expression) {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
if (expression.type === 'Literal') {
|
|
397
|
-
return typeof expression.value === 'string';
|
|
398
|
-
}
|
|
399
|
-
if (expression.type === 'TemplateLiteral') {
|
|
400
|
-
return expression.expressions.length === 0;
|
|
401
|
-
}
|
|
402
|
-
return false;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Build `expr == null ? '' : expr + ''` — the text-coerce form used when a
|
|
407
|
-
* Ripple `{expr}` child must render as a string in JSX (React/Preact drop
|
|
408
|
-
* booleans; Solid's default child semantics don't either). Solid uses this
|
|
409
|
-
* via `to_jsx_child`; React/Preact wrap it in a JSXExpressionContainer.
|
|
410
|
-
*
|
|
411
|
-
* When the expression is statically a non-null string at the AST level —
|
|
412
|
-
* a string `Literal` (`"hello"`, `'hello'`) or a `TemplateLiteral` with no
|
|
413
|
-
* interpolations (`` `hello` ``) — the coercion is provably a no-op and
|
|
414
|
-
* the literal is emitted as-is. Identifiers and any other expression type still
|
|
415
|
-
* get the ternary because the AST alone can't prove they're non-null strings.
|
|
416
|
-
*
|
|
417
|
-
* @param {AST.Expression} expression
|
|
418
|
-
* @param {AST.Node | AST.NodeWithLocation} [source_node]
|
|
419
|
-
* @returns {AST.Expression}
|
|
420
|
-
*/
|
|
421
|
-
export function to_text_expression(expression, source_node = expression) {
|
|
422
|
-
if (is_static_string_expression(expression)) {
|
|
423
|
-
return set_loc(clone_ast_node(expression), source_node);
|
|
424
|
-
}
|
|
425
|
-
return set_loc(
|
|
426
|
-
/** @type {AST.Expression} */ ({
|
|
427
|
-
type: 'ConditionalExpression',
|
|
428
|
-
test: {
|
|
429
|
-
type: 'BinaryExpression',
|
|
430
|
-
operator: '==',
|
|
431
|
-
left: clone_ast_node(expression),
|
|
432
|
-
right: create_null_literal(),
|
|
433
|
-
metadata: { path: [] },
|
|
434
|
-
},
|
|
435
|
-
consequent: {
|
|
436
|
-
type: 'Literal',
|
|
437
|
-
value: '',
|
|
438
|
-
raw: "''",
|
|
439
|
-
metadata: { path: [] },
|
|
440
|
-
},
|
|
441
|
-
alternate: {
|
|
442
|
-
type: 'BinaryExpression',
|
|
443
|
-
operator: '+',
|
|
444
|
-
left: clone_ast_node(expression),
|
|
445
|
-
right: {
|
|
446
|
-
type: 'Literal',
|
|
447
|
-
value: '',
|
|
448
|
-
raw: "''",
|
|
449
|
-
metadata: { path: [] },
|
|
450
|
-
},
|
|
451
|
-
metadata: { path: [] },
|
|
452
|
-
},
|
|
453
|
-
metadata: { path: [] },
|
|
454
|
-
}),
|
|
455
|
-
source_node,
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
380
|
/**
|
|
460
381
|
* Deep-clone an AST subtree.
|
|
461
382
|
*
|
|
@@ -252,7 +252,7 @@ const LOCATION_WRAPPED_NODE_TYPES = new Set([
|
|
|
252
252
|
|
|
253
253
|
/**
|
|
254
254
|
* @param {AST.Node | null | undefined} node
|
|
255
|
-
* @returns {
|
|
255
|
+
* @returns {node is AST.JSXIfExpression | (AST.IfStatement & { statementType: 'IfStatement' })}
|
|
256
256
|
*/
|
|
257
257
|
export function is_template_if_node(node) {
|
|
258
258
|
return (
|
|
@@ -262,19 +262,24 @@ export function is_template_if_node(node) {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
|
+
* A `@for … of …` directive, in either the expression or the retyped statement
|
|
266
|
+
* form. `JSXForExpression` also covers `@for … in …` and plain `@for (;;)`, so
|
|
267
|
+
* both arms must check `statementType` — callers read for-of-only fields such
|
|
268
|
+
* as `await` off the result.
|
|
269
|
+
*
|
|
265
270
|
* @param {AST.Node | null | undefined} node
|
|
266
|
-
* @returns {
|
|
271
|
+
* @returns {node is AST.JSXForOfExpression | (AST.ForOfStatement & { statementType: 'ForOfStatement' })}
|
|
267
272
|
*/
|
|
268
273
|
export function is_template_for_of_node(node) {
|
|
269
274
|
return (
|
|
270
|
-
node?.type === 'JSXForExpression' ||
|
|
271
|
-
|
|
275
|
+
(node?.type === 'JSXForExpression' || node?.type === 'ForOfStatement') &&
|
|
276
|
+
node.statementType === 'ForOfStatement'
|
|
272
277
|
);
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
/**
|
|
276
281
|
* @param {AST.Node | null | undefined} node
|
|
277
|
-
* @returns {
|
|
282
|
+
* @returns {node is AST.JSXSwitchExpression | (AST.SwitchStatement & { statementType: 'SwitchStatement' })}
|
|
278
283
|
*/
|
|
279
284
|
export function is_template_switch_node(node) {
|
|
280
285
|
return (
|
|
@@ -285,7 +290,7 @@ export function is_template_switch_node(node) {
|
|
|
285
290
|
|
|
286
291
|
/**
|
|
287
292
|
* @param {AST.Node | null | undefined} node
|
|
288
|
-
* @returns {
|
|
293
|
+
* @returns {node is AST.JSXTryExpression | (AST.TryStatement & { statementType: 'TryStatement' })}
|
|
289
294
|
*/
|
|
290
295
|
export function is_template_try_node(node) {
|
|
291
296
|
return (
|