@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 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.47",
6
+ "version": "0.1.49",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
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 = this.#finishJSXControlFlowExpression(
1522
- this.parseStatement(null),
1523
- 'JSXForExpression',
1524
- start,
1525
- startLoc,
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
- /** @type {any} */ (node).statementType !== 'ForOfStatement' &&
1533
- /** @type {any} */ (node).statementType !== 'ForInStatement' &&
1534
- /** @type {any} */ (node).statementType !== 'ForStatement'
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 (/** @type {any} */ (node).body?.type !== 'BlockStatement') {
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
- /** @type {any} */ (node).empty = this.parseBlock();
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
- /** @type {any} */ (node).empty = null;
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.#expressionContainerContextBaselines.push(this.context.length);
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 = /** @type {any} */ (Parser.acornTypeScript).tokenIsIdentifier;
4702
- const parser = /** @type {any} */ (this);
4703
- const import_node = /** @type {any} */ (node);
4704
- let enterHead = parser.lookahead();
4705
- import_node.importKind = 'value';
4706
- parser.importOrExportOuterKind = 'value';
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 = parser.lookahead(2);
4709
- if (
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
- !parser.isContextualWithState('from', ahead) &&
4712
- ahead.type !== tt.eq &&
4713
- parser.ts_eatContextualWithState('type', 1, enterHead)
4714
- ) {
4715
- parser.importOrExportOuterKind = 'type';
4716
- import_node.importKind = 'type';
4717
- enterHead = parser.lookahead();
4718
- ahead = parser.lookahead(2);
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 = parser.tsParseImportEqualsDeclaration(node);
4723
- parser.importOrExportOuterKind = 'value';
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
- import_node.specifiers = [];
4730
- import_node.source = this.parseExprAtom();
4853
+ node.specifiers = [];
4854
+ node.source = /** @type {AST.Literal} */ (this.parseExprAtom());
4731
4855
  } else {
4732
- import_node.specifiers = this.parseImportSpecifiers();
4856
+ node.specifiers = this.parseImportSpecifiers();
4733
4857
  this.expectContextual('from');
4734
4858
  if (this.type === tt.string) {
4735
- import_node.source = this.parseExprAtom();
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
- import_node.source = source;
4863
+ node.source = source;
4740
4864
  } else {
4741
4865
  this.unexpected();
4742
4866
  }
4743
4867
  }
4744
- parser.parseMaybeImportAttributes(node);
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
- parser.importOrExportOuterKind = 'value';
4748
- return import_node;
4882
+ this.importOrExportOuterKind = 'value';
4883
+ return node;
4749
4884
  }
4750
4885
 
4751
4886
  /**
@@ -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 {any[]} body_nodes
3
- * @returns {any | null}
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 {any} node
19
+ * @param {AST.Node | AST.Node[] | null | undefined} node
16
20
  * @param {boolean} inside_nested_function
17
- * @returns {any | null}
21
+ * @returns {AST.TSRXAwaitNode | null}
18
22
  */
19
23
  export function find_first_top_level_await(node, inside_nested_function) {
20
- if (!node || typeof node !== 'object') {
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' && node.await === true)
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 key of Object.keys(node)) {
54
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
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
  *