@tsrx/core 0.1.47 → 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.47",
6
+ "version": "0.1.48",
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,
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
+ }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import tsx from 'esrap/languages/tsx';
5
5
  import { should_preserve_comment, format_comment } from '../../comment-utils.js';
6
+ import { with_deferred_imports } from '../imports.js';
6
7
 
7
8
  /**
8
9
  * Zimmerframe provides `path` as the ancestor chain. A native template node in
@@ -75,7 +76,7 @@ export function set_node_path_metadata(node, path) {
75
76
  * TS input — dropping a leading pragma changes how the whole file checks.
76
77
  */
77
78
  export function tsx_with_ts_locations(boundary_tokens = false, comments = undefined) {
78
- const base = tsx({ boundaryTokens: boundary_tokens });
79
+ const base = with_deferred_imports(tsx({ boundaryTokens: boundary_tokens }));
79
80
  const { _: base_visitor, ...base_visitors } = base;
80
81
 
81
82
  const leading_preserved = (/** @type {AST.Program} */ program) => {
package/types/index.d.ts CHANGED
@@ -310,6 +310,7 @@ declare module 'estree' {
310
310
  // Include TypeScript node types and TSRX-specific nodes in NodeMap
311
311
  interface NodeMap {
312
312
  JSXSpreadChild: ESTreeJSX.JSXSpreadChild;
313
+ TSRXImportDeclaration: TSRXImportDeclaration;
313
314
  TSRXJSXElement: TSRXJSXElement;
314
315
  TSRXJSXFragment: TSRXJSXFragment;
315
316
  TSRXJSXOpeningElement: ESTreeJSX.TSRXJSXOpeningElement;
@@ -547,6 +548,13 @@ declare module 'estree' {
547
548
 
548
549
  interface ImportDeclaration {
549
550
  importKind: TSESTree.ImportDeclaration['importKind'];
551
+ phase?: 'defer' | null;
552
+ }
553
+ interface TSRXImportDeclaration extends Omit<ImportDeclaration, 'source'> {
554
+ source: AST.Literal | AST.Identifier;
555
+ }
556
+ interface ImportExpression {
557
+ phase?: 'defer' | null;
550
558
  }
551
559
  interface ImportSpecifier {
552
560
  importKind: TSESTree.ImportSpecifier['importKind'];
@@ -1391,7 +1399,7 @@ export interface AnalysisResult {
1391
1399
  component_metadata: Array<{ id: string }>;
1392
1400
  metadata: {
1393
1401
  serverImportsPresent: boolean;
1394
- serverImportDeclarations: AST.ImportDeclaration[];
1402
+ serverImportDeclarations: AST.TSRXImportDeclaration[];
1395
1403
  serverModule: AST.TSModuleDeclaration | null;
1396
1404
  };
1397
1405
  errors: CompileError[];
package/types/parse.d.ts CHANGED
@@ -420,6 +420,8 @@ export namespace Parse {
420
420
  export interface AcornTypeScriptExtensions {
421
421
  tokTypes: AcornTypeScriptTokTypes;
422
422
  tokContexts: AcornTypeScriptTokContexts;
423
+ /** Whether a token type can start/continue an identifier (incl. TS soft keywords) */
424
+ tokenIsIdentifier(token: TokenType): boolean;
423
425
  }
424
426
 
425
427
  export interface AcornTypeScriptFunctionBodyConfig {
@@ -427,6 +429,31 @@ export namespace Parse {
427
429
  isClassMethod?: boolean;
428
430
  }
429
431
 
432
+ /**
433
+ * Snapshot of tokenizer state returned by `lookahead()`, mirroring
434
+ * @sveltejs/acorn-typescript's saved lookahead state. Callers inspect the
435
+ * upcoming token (`type`/`value`/`start`/`containsEsc`) without consuming it,
436
+ * and pass the snapshot to the `*WithState` contextual helpers.
437
+ */
438
+ export interface LookaheadState {
439
+ pos: number;
440
+ type: TokenType;
441
+ value: string | number | RegExp | bigint | null;
442
+ start: number;
443
+ end: number;
444
+ startLoc: AST.Position;
445
+ endLoc: AST.Position;
446
+ lastTokEnd: number;
447
+ lastTokStart: number;
448
+ lastTokStartLoc: AST.Position;
449
+ lastTokEndLoc: AST.Position;
450
+ context: TokContext[];
451
+ curLine: number;
452
+ lineStart: number;
453
+ curPosition: () => AST.Position;
454
+ containsEsc: boolean;
455
+ }
456
+
430
457
  interface Scope {
431
458
  flags: number;
432
459
  var: string[];
@@ -496,6 +523,11 @@ export namespace Parse {
496
523
  inFunction: boolean;
497
524
  /** Whether @sveltejs/acorn-typescript is currently parsing a TypeScript type */
498
525
  inType: boolean;
526
+ /**
527
+ * `value`/`type` kind of the import or export declaration currently being
528
+ * parsed (@sveltejs/acorn-typescript). `undefined` when not inside one.
529
+ */
530
+ importOrExportOuterKind?: 'value' | 'type';
499
531
  /** Stack of label names for break/continue statements */
500
532
  labels: Array<{ kind: string | null; name?: string; statementStart?: number }>;
501
533
  /** Current scope flags stack */
@@ -756,6 +788,23 @@ export namespace Parse {
756
788
  */
757
789
  expectContextual(name: string): void;
758
790
 
791
+ /**
792
+ * Like `isContextual`, but tests a `lookahead()` snapshot instead of the
793
+ * current token (@sveltejs/acorn-typescript).
794
+ * @param keyword Keyword to check (e.g. "from", "defer")
795
+ * @param state Lookahead snapshot to test
796
+ */
797
+ isContextualWithState(keyword: string, state: LookaheadState): boolean;
798
+
799
+ /**
800
+ * If `state` is the contextual keyword `name`, consume `nextCount` tokens
801
+ * and return true (@sveltejs/acorn-typescript).
802
+ * @param name Contextual keyword to eat (e.g. "type", "defer")
803
+ * @param nextCount Number of tokens to advance when it matches
804
+ * @param state Lookahead snapshot to test
805
+ */
806
+ ts_eatContextualWithState(name: string, nextCount: number, state: LookaheadState): boolean;
807
+
759
808
  /**
760
809
  * Check if semicolon can be inserted at current position (ASI)
761
810
  */
@@ -831,10 +880,11 @@ export namespace Parse {
831
880
  // Lookahead
832
881
  // ============================================================
833
882
  /**
834
- * Look ahead one token without consuming
835
- * @returns Object with type and value of next token
883
+ * Look ahead `number` tokens (default 1) without consuming, returning a
884
+ * snapshot of the tokenizer state at that position.
885
+ * @param number How many tokens to look ahead (default 1)
836
886
  */
837
- lookahead(): { type: TokenType; value: any };
887
+ lookahead(number?: number): LookaheadState;
838
888
 
839
889
  /**
840
890
  * Get next token start position
@@ -1461,7 +1511,17 @@ export namespace Parse {
1461
1511
  // Module Parsing (Import/Export)
1462
1512
  // ============================================================
1463
1513
  /** Parse import declaration */
1464
- parseImport(node: AST.Node): AST.ImportDeclaration;
1514
+ parseImport(node: AST.TSRXImportDeclaration): AST.TSRXImportDeclaration;
1515
+
1516
+ /**
1517
+ * Parse a TypeScript import-equals declaration, `import x = require('y')`
1518
+ * or `import x = A.B` (@sveltejs/acorn-typescript). Typed as
1519
+ * `ImportDeclaration` because estree has no `TSImportEqualsDeclaration`.
1520
+ */
1521
+ tsParseImportEqualsDeclaration(node: AST.Node, isExport?: boolean): AST.ImportDeclaration;
1522
+
1523
+ /** Parse an optional import-attributes clause (`with { … }` / `assert { … }`) */
1524
+ parseMaybeImportAttributes(node: AST.Node): void;
1465
1525
 
1466
1526
  /** Parse import specifiers */
1467
1527
  parseImportSpecifiers(): AST.ImportSpecifier[];