@tsrx/core 0.1.47 → 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 +1 -1
- package/src/plugin.js +177 -42
- package/src/transform/await.js +14 -12
- package/src/transform/imports.js +96 -0
- package/src/transform/jsx/ast-builders.js +2 -81
- package/src/transform/jsx/helpers.js +13 -7
- 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 +88 -17
- package/types/jsx-platform.d.ts +18 -8
- package/types/parse.d.ts +66 -8
- package/types/runtime/ref.d.ts +1 -5
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -149,6 +149,7 @@ export { normalize_css_property_name as normalizeCssPropertyName } from './utils
|
|
|
149
149
|
export { escape, escape_script as escapeScript } from './utils/escaping.js';
|
|
150
150
|
|
|
151
151
|
// Transform
|
|
152
|
+
export { with_deferred_imports as withDeferredImports } from './transform/imports.js';
|
|
152
153
|
export {
|
|
153
154
|
add_jsx_setup_declaration as addJsxSetupDeclaration,
|
|
154
155
|
clone_switch_helper_invocation as cloneSwitchHelperInvocation,
|
|
@@ -203,7 +204,6 @@ export {
|
|
|
203
204
|
is_component_jsx_name,
|
|
204
205
|
is_jsx_child,
|
|
205
206
|
set_loc,
|
|
206
|
-
to_text_expression,
|
|
207
207
|
} from './transform/jsx/ast-builders.js';
|
|
208
208
|
export {
|
|
209
209
|
render_stylesheets as renderStylesheets,
|
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
|
}
|
|
@@ -4688,6 +4739,67 @@ export function TSRXPlugin(config) {
|
|
|
4688
4739
|
this.parseTemplateBody(body);
|
|
4689
4740
|
}
|
|
4690
4741
|
|
|
4742
|
+
/**
|
|
4743
|
+
* Parse the argument list of a deferred dynamic import,
|
|
4744
|
+
* `import.defer(specifier, options?)`, starting at the opening paren.
|
|
4745
|
+
*
|
|
4746
|
+
* This mirrors Acorn's ES2025 `import(...)` grammar (optional `options`
|
|
4747
|
+
* argument, optional trailing comma), which neither inherited parser
|
|
4748
|
+
* produces here: acorn-typescript's `parseDynamicImport` emits legacy
|
|
4749
|
+
* `arguments`, and Acorn's own only enables the `options` shape at
|
|
4750
|
+
* `ecmaVersion >= 16` while TSRX parses at 13.
|
|
4751
|
+
*
|
|
4752
|
+
* @param {AST.ImportExpression} node
|
|
4753
|
+
* @returns {AST.ImportExpression}
|
|
4754
|
+
*/
|
|
4755
|
+
parseDeferredDynamicImport(node) {
|
|
4756
|
+
this.next(); // `(`
|
|
4757
|
+
node.source = this.parseMaybeAssign();
|
|
4758
|
+
node.options = null;
|
|
4759
|
+
|
|
4760
|
+
if (!this.eat(tt.parenR)) {
|
|
4761
|
+
this.expect(tt.comma);
|
|
4762
|
+
if (!this.afterTrailingComma(tt.parenR)) {
|
|
4763
|
+
node.options = this.parseMaybeAssign();
|
|
4764
|
+
if (!this.eat(tt.parenR)) {
|
|
4765
|
+
this.expect(tt.comma);
|
|
4766
|
+
if (!this.afterTrailingComma(tt.parenR)) this.unexpected();
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
return this.finishNode(node, 'ImportExpression');
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
/**
|
|
4775
|
+
* Recognize the deferred dynamic-import form
|
|
4776
|
+
* `import.defer(specifier, options?)` before Acorn parses `import.<name>`
|
|
4777
|
+
* as an `import.meta` member access. Ordinary `import()` and `import.meta`
|
|
4778
|
+
* fall through to Acorn unchanged, so the proposal never alters their
|
|
4779
|
+
* existing AST shape.
|
|
4780
|
+
* @type {Parse.Parser['parseExprImport']}
|
|
4781
|
+
*/
|
|
4782
|
+
parseExprImport(forNew) {
|
|
4783
|
+
if (
|
|
4784
|
+
!forNew &&
|
|
4785
|
+
this.lookahead().type === tt.dot &&
|
|
4786
|
+
this.isContextualWithState('defer', this.lookahead(2))
|
|
4787
|
+
) {
|
|
4788
|
+
const node = /** @type {AST.ImportExpression} */ (this.startNode());
|
|
4789
|
+
if (this.containsEsc) {
|
|
4790
|
+
this.raiseRecoverable(this.start, 'Escape sequence in keyword import');
|
|
4791
|
+
}
|
|
4792
|
+
this.next(); // `import`
|
|
4793
|
+
this.next(); // `.`
|
|
4794
|
+
this.next(); // `defer`
|
|
4795
|
+
node.phase = 'defer';
|
|
4796
|
+
if (this.type !== tt.parenL) this.unexpected();
|
|
4797
|
+
return this.parseDeferredDynamicImport(node);
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
return super.parseExprImport(forNew);
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4691
4803
|
/**
|
|
4692
4804
|
* Parse proposal-style imports from an inline module declaration:
|
|
4693
4805
|
* `import { foo } from server;`
|
|
@@ -4698,54 +4810,77 @@ export function TSRXPlugin(config) {
|
|
|
4698
4810
|
* @type {Parse.Parser['parseImport']}
|
|
4699
4811
|
*/
|
|
4700
4812
|
parseImport(node) {
|
|
4701
|
-
const tokenIsIdentifier =
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
let
|
|
4705
|
-
|
|
4706
|
-
|
|
4813
|
+
const tokenIsIdentifier = Parser.acornTypeScript.tokenIsIdentifier;
|
|
4814
|
+
let enterHead = this.lookahead();
|
|
4815
|
+
let deferred = false;
|
|
4816
|
+
let defer_start = -1;
|
|
4817
|
+
node.importKind = 'value';
|
|
4818
|
+
this.importOrExportOuterKind = 'value';
|
|
4707
4819
|
if (tokenIsIdentifier(enterHead.type) || this.match(tt.star) || this.match(tt.braceL)) {
|
|
4708
|
-
let ahead =
|
|
4709
|
-
|
|
4820
|
+
let ahead = this.lookahead(2);
|
|
4821
|
+
// `defer` and `type` are only phase/kind modifiers when the following
|
|
4822
|
+
// token cannot continue a default import (`, `/`from`) or an
|
|
4823
|
+
// import-equals declaration (`=`); otherwise they are ordinary bindings.
|
|
4824
|
+
const head_modifies =
|
|
4710
4825
|
ahead.type !== tt.comma &&
|
|
4711
|
-
!
|
|
4712
|
-
ahead.type !== tt.eq
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4826
|
+
!this.isContextualWithState('from', ahead) &&
|
|
4827
|
+
ahead.type !== tt.eq;
|
|
4828
|
+
// The namespace-only restriction is checked after parsing the clause,
|
|
4829
|
+
// which also gives invalid named/default deferred imports a focused
|
|
4830
|
+
// diagnostic.
|
|
4831
|
+
if (head_modifies && this.isContextualWithState('defer', enterHead)) {
|
|
4832
|
+
deferred = true;
|
|
4833
|
+
defer_start = enterHead.start;
|
|
4834
|
+
node.phase = 'defer';
|
|
4835
|
+
this.ts_eatContextualWithState('defer', 1, enterHead);
|
|
4836
|
+
enterHead = this.lookahead();
|
|
4837
|
+
ahead = this.lookahead(2);
|
|
4838
|
+
} else if (head_modifies && this.ts_eatContextualWithState('type', 1, enterHead)) {
|
|
4839
|
+
this.importOrExportOuterKind = 'type';
|
|
4840
|
+
node.importKind = 'type';
|
|
4841
|
+
enterHead = this.lookahead();
|
|
4842
|
+
ahead = this.lookahead(2);
|
|
4719
4843
|
}
|
|
4720
4844
|
if (tokenIsIdentifier(enterHead.type) && ahead.type === tt.eq) {
|
|
4721
4845
|
this.next();
|
|
4722
|
-
const importNode =
|
|
4723
|
-
|
|
4846
|
+
const importNode = this.tsParseImportEqualsDeclaration(node);
|
|
4847
|
+
this.importOrExportOuterKind = 'value';
|
|
4724
4848
|
return importNode;
|
|
4725
4849
|
}
|
|
4726
4850
|
}
|
|
4727
4851
|
this.next();
|
|
4728
4852
|
if (this.type === tt.string) {
|
|
4729
|
-
|
|
4730
|
-
|
|
4853
|
+
node.specifiers = [];
|
|
4854
|
+
node.source = /** @type {AST.Literal} */ (this.parseExprAtom());
|
|
4731
4855
|
} else {
|
|
4732
|
-
|
|
4856
|
+
node.specifiers = this.parseImportSpecifiers();
|
|
4733
4857
|
this.expectContextual('from');
|
|
4734
4858
|
if (this.type === tt.string) {
|
|
4735
|
-
|
|
4859
|
+
node.source = /** @type {AST.Literal} */ (this.parseExprAtom());
|
|
4736
4860
|
} else if (tokenIsIdentifier(this.type)) {
|
|
4737
4861
|
const source = this.parseIdent(false);
|
|
4738
4862
|
source.metadata ??= { path: [] };
|
|
4739
|
-
|
|
4863
|
+
node.source = source;
|
|
4740
4864
|
} else {
|
|
4741
4865
|
this.unexpected();
|
|
4742
4866
|
}
|
|
4743
4867
|
}
|
|
4744
|
-
|
|
4868
|
+
if (
|
|
4869
|
+
deferred &&
|
|
4870
|
+
(node.specifiers.length !== 1 ||
|
|
4871
|
+
node.specifiers[0].type !== 'ImportNamespaceSpecifier' ||
|
|
4872
|
+
node.source.type !== 'Literal')
|
|
4873
|
+
) {
|
|
4874
|
+
this.raise(
|
|
4875
|
+
defer_start,
|
|
4876
|
+
'`import defer` only supports a namespace import from a string literal.',
|
|
4877
|
+
);
|
|
4878
|
+
}
|
|
4879
|
+
this.parseMaybeImportAttributes(node);
|
|
4745
4880
|
this.semicolon();
|
|
4746
4881
|
this.finishNode(node, 'ImportDeclaration');
|
|
4747
|
-
|
|
4748
|
-
return
|
|
4882
|
+
this.importOrExportOuterKind = 'value';
|
|
4883
|
+
return node;
|
|
4749
4884
|
}
|
|
4750
4885
|
|
|
4751
4886
|
/**
|
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
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add TSRX import-phase support to an esrap TS/TSX visitor set. esrap 2.3
|
|
5
|
+
* understands the rest of ImportDeclaration but does not print its Stage 3
|
|
6
|
+
* `phase` field yet, so delegating would silently turn a deferred import into
|
|
7
|
+
* an eager one.
|
|
8
|
+
*
|
|
9
|
+
* @template {Record<string, any>} T
|
|
10
|
+
* @param {T} visitors
|
|
11
|
+
* @returns {T}
|
|
12
|
+
*/
|
|
13
|
+
export function with_deferred_imports(visitors) {
|
|
14
|
+
const print_import_declaration = visitors.ImportDeclaration;
|
|
15
|
+
const print_import_expression = visitors.ImportExpression;
|
|
16
|
+
if (
|
|
17
|
+
typeof print_import_declaration !== 'function' ||
|
|
18
|
+
typeof print_import_expression !== 'function'
|
|
19
|
+
) {
|
|
20
|
+
throw new TypeError('Deferred imports require a complete esrap TS or TSX visitor set.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return /** @type {T} */ ({
|
|
24
|
+
...visitors,
|
|
25
|
+
/**
|
|
26
|
+
* @param {AST.ImportDeclaration} node
|
|
27
|
+
* @param {import('esrap').Context} context
|
|
28
|
+
*/
|
|
29
|
+
ImportDeclaration(node, context) {
|
|
30
|
+
const import_node = /** @type {AST.ImportDeclaration & { phase?: 'defer' | null }} */ (node);
|
|
31
|
+
if (import_node.phase !== 'defer') {
|
|
32
|
+
print_import_declaration(node, context);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [specifier] = node.specifiers;
|
|
37
|
+
if (node.specifiers.length !== 1 || specifier.type !== 'ImportNamespaceSpecifier') {
|
|
38
|
+
throw new Error('`import defer` only supports a namespace import.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (node.loc) context.location(node.loc.start.line, node.loc.start.column);
|
|
42
|
+
context.write('import defer ');
|
|
43
|
+
if (specifier.loc) {
|
|
44
|
+
context.location(specifier.loc.start.line, specifier.loc.start.column);
|
|
45
|
+
}
|
|
46
|
+
context.write('* as ');
|
|
47
|
+
context.visit(specifier.local);
|
|
48
|
+
context.write(' from ');
|
|
49
|
+
context.visit(node.source);
|
|
50
|
+
|
|
51
|
+
const attributes =
|
|
52
|
+
/** @type {Array<{ key: AST.Identifier | AST.Literal, value: AST.Literal }>} */ (
|
|
53
|
+
/** @type {any} */ (node).attributes ?? /** @type {any} */ (node).assertions ?? []
|
|
54
|
+
);
|
|
55
|
+
if (attributes.length > 0) {
|
|
56
|
+
context.write(' with { ');
|
|
57
|
+
for (let index = 0; index < attributes.length; index++) {
|
|
58
|
+
context.visit(attributes[index].key);
|
|
59
|
+
context.write(': ');
|
|
60
|
+
context.visit(attributes[index].value);
|
|
61
|
+
if (index + 1 !== attributes.length) context.write(', ');
|
|
62
|
+
}
|
|
63
|
+
context.write(' }');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
context.write(';');
|
|
67
|
+
if (node.loc) context.location(node.loc.end.line, node.loc.end.column);
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* @param {AST.ImportExpression} node
|
|
71
|
+
* @param {import('esrap').Context} context
|
|
72
|
+
*/
|
|
73
|
+
ImportExpression(node, context) {
|
|
74
|
+
const import_node = /** @type {AST.ImportExpression & { phase?: 'defer' | null }} */ (node);
|
|
75
|
+
if (import_node.phase !== 'defer') {
|
|
76
|
+
print_import_expression(node, context);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (node.loc) context.location(node.loc.start.line, node.loc.start.column);
|
|
81
|
+
context.write('import.defer(');
|
|
82
|
+
context.visit(node.source);
|
|
83
|
+
|
|
84
|
+
const options =
|
|
85
|
+
node.options ??
|
|
86
|
+
/** @type {AST.Expression | undefined} */ (/** @type {any} */ (node).arguments?.[0]);
|
|
87
|
+
if (options) {
|
|
88
|
+
context.write(', ');
|
|
89
|
+
context.visit(options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
context.write(')');
|
|
93
|
+
if (node.loc) context.location(node.loc.end.line, node.loc.end.column);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -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
|
*
|