@tsrx/core 0.1.46 → 0.1.48

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.46",
6
+ "version": "0.1.48",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -89,6 +89,7 @@ export {
89
89
  is_function_node as isFunctionNode,
90
90
  is_function_or_class_node as isFunctionOrClassNode,
91
91
  is_function_or_component_node as isFunctionOrComponentNode,
92
+ has_location,
92
93
  is_inside_component as isInsideComponent,
93
94
  is_template_directive as isTemplateDirective,
94
95
  is_tsrx_render_output_node as isTsrxRenderOutputNode,
@@ -148,6 +149,7 @@ export { normalize_css_property_name as normalizeCssPropertyName } from './utils
148
149
  export { escape, escape_script as escapeScript } from './utils/escaping.js';
149
150
 
150
151
  // Transform
152
+ export { with_deferred_imports as withDeferredImports } from './transform/imports.js';
151
153
  export {
152
154
  add_jsx_setup_declaration as addJsxSetupDeclaration,
153
155
  clone_switch_helper_invocation as cloneSwitchHelperInvocation,
@@ -167,7 +169,6 @@ export {
167
169
  plan_switch_lift as planSwitchLift,
168
170
  return_value_body_to_expression as returnValueBodyToExpression,
169
171
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
170
- to_jsx_attribute as toJsxAttribute,
171
172
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
172
173
  wrap_edge_whitespace as wrapEdgeWhitespace,
173
174
  } from './transform/jsx/index.js';
@@ -189,7 +190,7 @@ export {
189
190
  } from './transform/style-ref.js';
190
191
  export {
191
192
  add_extra_source_mappings_from_matching_expression,
192
- clone_expression_node,
193
+ clone_ast_node,
193
194
  clone_identifier,
194
195
  clone_jsx_name,
195
196
  contains_component_jsx,
@@ -8,6 +8,7 @@
8
8
  import * as acorn from 'acorn';
9
9
  import { tsPlugin } from '@sveltejs/acorn-typescript';
10
10
  import { walk } from 'zimmerframe';
11
+ import { has_location } from '../utils/ast.js';
11
12
 
12
13
  /**
13
14
  * @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
@@ -607,8 +608,8 @@ export function get_comment_handlers(source, comments, index = 0) {
607
608
  }
608
609
 
609
610
  const ancestorElements = path
610
- .filter((ancestor) => isNativeTemplateNode(ancestor) && ancestor.loc)
611
- .map((ancestor) => /** @type {AST.NodeWithLocation} */ (ancestor))
611
+ .filter(has_location)
612
+ .filter(isNativeTemplateNode)
612
613
  .sort((a, b) => a.loc.start.line - b.loc.start.line);
613
614
 
614
615
  const targetAncestor = ancestorElements.find(
package/src/plugin.js CHANGED
@@ -4688,6 +4688,67 @@ export function TSRXPlugin(config) {
4688
4688
  this.parseTemplateBody(body);
4689
4689
  }
4690
4690
 
4691
+ /**
4692
+ * Parse the argument list of a deferred dynamic import,
4693
+ * `import.defer(specifier, options?)`, starting at the opening paren.
4694
+ *
4695
+ * This mirrors Acorn's ES2025 `import(...)` grammar (optional `options`
4696
+ * argument, optional trailing comma), which neither inherited parser
4697
+ * produces here: acorn-typescript's `parseDynamicImport` emits legacy
4698
+ * `arguments`, and Acorn's own only enables the `options` shape at
4699
+ * `ecmaVersion >= 16` while TSRX parses at 13.
4700
+ *
4701
+ * @param {AST.ImportExpression} node
4702
+ * @returns {AST.ImportExpression}
4703
+ */
4704
+ parseDeferredDynamicImport(node) {
4705
+ this.next(); // `(`
4706
+ node.source = this.parseMaybeAssign();
4707
+ node.options = null;
4708
+
4709
+ if (!this.eat(tt.parenR)) {
4710
+ this.expect(tt.comma);
4711
+ if (!this.afterTrailingComma(tt.parenR)) {
4712
+ node.options = this.parseMaybeAssign();
4713
+ if (!this.eat(tt.parenR)) {
4714
+ this.expect(tt.comma);
4715
+ if (!this.afterTrailingComma(tt.parenR)) this.unexpected();
4716
+ }
4717
+ }
4718
+ }
4719
+
4720
+ return this.finishNode(node, 'ImportExpression');
4721
+ }
4722
+
4723
+ /**
4724
+ * Recognize the deferred dynamic-import form
4725
+ * `import.defer(specifier, options?)` before Acorn parses `import.<name>`
4726
+ * as an `import.meta` member access. Ordinary `import()` and `import.meta`
4727
+ * fall through to Acorn unchanged, so the proposal never alters their
4728
+ * existing AST shape.
4729
+ * @type {Parse.Parser['parseExprImport']}
4730
+ */
4731
+ parseExprImport(forNew) {
4732
+ if (
4733
+ !forNew &&
4734
+ this.lookahead().type === tt.dot &&
4735
+ this.isContextualWithState('defer', this.lookahead(2))
4736
+ ) {
4737
+ const node = /** @type {AST.ImportExpression} */ (this.startNode());
4738
+ if (this.containsEsc) {
4739
+ this.raiseRecoverable(this.start, 'Escape sequence in keyword import');
4740
+ }
4741
+ this.next(); // `import`
4742
+ this.next(); // `.`
4743
+ this.next(); // `defer`
4744
+ node.phase = 'defer';
4745
+ if (this.type !== tt.parenL) this.unexpected();
4746
+ return this.parseDeferredDynamicImport(node);
4747
+ }
4748
+
4749
+ return super.parseExprImport(forNew);
4750
+ }
4751
+
4691
4752
  /**
4692
4753
  * Parse proposal-style imports from an inline module declaration:
4693
4754
  * `import { foo } from server;`
@@ -4698,54 +4759,77 @@ export function TSRXPlugin(config) {
4698
4759
  * @type {Parse.Parser['parseImport']}
4699
4760
  */
4700
4761
  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';
4762
+ const tokenIsIdentifier = Parser.acornTypeScript.tokenIsIdentifier;
4763
+ let enterHead = this.lookahead();
4764
+ let deferred = false;
4765
+ let defer_start = -1;
4766
+ node.importKind = 'value';
4767
+ this.importOrExportOuterKind = 'value';
4707
4768
  if (tokenIsIdentifier(enterHead.type) || this.match(tt.star) || this.match(tt.braceL)) {
4708
- let ahead = parser.lookahead(2);
4709
- if (
4769
+ let ahead = this.lookahead(2);
4770
+ // `defer` and `type` are only phase/kind modifiers when the following
4771
+ // token cannot continue a default import (`, `/`from`) or an
4772
+ // import-equals declaration (`=`); otherwise they are ordinary bindings.
4773
+ const head_modifies =
4710
4774
  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);
4775
+ !this.isContextualWithState('from', ahead) &&
4776
+ ahead.type !== tt.eq;
4777
+ // The namespace-only restriction is checked after parsing the clause,
4778
+ // which also gives invalid named/default deferred imports a focused
4779
+ // diagnostic.
4780
+ if (head_modifies && this.isContextualWithState('defer', enterHead)) {
4781
+ deferred = true;
4782
+ defer_start = enterHead.start;
4783
+ node.phase = 'defer';
4784
+ this.ts_eatContextualWithState('defer', 1, enterHead);
4785
+ enterHead = this.lookahead();
4786
+ ahead = this.lookahead(2);
4787
+ } else if (head_modifies && this.ts_eatContextualWithState('type', 1, enterHead)) {
4788
+ this.importOrExportOuterKind = 'type';
4789
+ node.importKind = 'type';
4790
+ enterHead = this.lookahead();
4791
+ ahead = this.lookahead(2);
4719
4792
  }
4720
4793
  if (tokenIsIdentifier(enterHead.type) && ahead.type === tt.eq) {
4721
4794
  this.next();
4722
- const importNode = parser.tsParseImportEqualsDeclaration(node);
4723
- parser.importOrExportOuterKind = 'value';
4795
+ const importNode = this.tsParseImportEqualsDeclaration(node);
4796
+ this.importOrExportOuterKind = 'value';
4724
4797
  return importNode;
4725
4798
  }
4726
4799
  }
4727
4800
  this.next();
4728
4801
  if (this.type === tt.string) {
4729
- import_node.specifiers = [];
4730
- import_node.source = this.parseExprAtom();
4802
+ node.specifiers = [];
4803
+ node.source = /** @type {AST.Literal} */ (this.parseExprAtom());
4731
4804
  } else {
4732
- import_node.specifiers = this.parseImportSpecifiers();
4805
+ node.specifiers = this.parseImportSpecifiers();
4733
4806
  this.expectContextual('from');
4734
4807
  if (this.type === tt.string) {
4735
- import_node.source = this.parseExprAtom();
4808
+ node.source = /** @type {AST.Literal} */ (this.parseExprAtom());
4736
4809
  } else if (tokenIsIdentifier(this.type)) {
4737
4810
  const source = this.parseIdent(false);
4738
4811
  source.metadata ??= { path: [] };
4739
- import_node.source = source;
4812
+ node.source = source;
4740
4813
  } else {
4741
4814
  this.unexpected();
4742
4815
  }
4743
4816
  }
4744
- parser.parseMaybeImportAttributes(node);
4817
+ if (
4818
+ deferred &&
4819
+ (node.specifiers.length !== 1 ||
4820
+ node.specifiers[0].type !== 'ImportNamespaceSpecifier' ||
4821
+ node.source.type !== 'Literal')
4822
+ ) {
4823
+ this.raise(
4824
+ defer_start,
4825
+ '`import defer` only supports a namespace import from a string literal.',
4826
+ );
4827
+ }
4828
+ this.parseMaybeImportAttributes(node);
4745
4829
  this.semicolon();
4746
4830
  this.finishNode(node, 'ImportDeclaration');
4747
- parser.importOrExportOuterKind = 'value';
4748
- return import_node;
4831
+ this.importOrExportOuterKind = 'value';
4832
+ return node;
4749
4833
  }
4750
4834
 
4751
4835
  /**
@@ -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
+ }