@tsrx/core 0.1.55 → 0.1.57

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.55",
6
+ "version": "0.1.57",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -108,6 +108,28 @@ function get_argument_clash_reported_names(check_clashes) {
108
108
  return reported_names;
109
109
  }
110
110
 
111
+ /**
112
+ * A `<` opens a tag only when the character after it can begin one: `/` for a
113
+ * closing tag, `>` for a fragment, `{` for a dynamic tag, or an element or
114
+ * component name start. Any other character, or end of input, leaves the `<`
115
+ * as literal text.
116
+ * @param {string} input
117
+ * @param {number} index Index of the `<`.
118
+ */
119
+ function can_start_tag_after_lt(input, index) {
120
+ const next = index + 1 < input.length ? input.charCodeAt(index + 1) : -1;
121
+ return (
122
+ next === CharCode.slash ||
123
+ next === CharCode.greaterThan ||
124
+ next === CharCode.openBrace ||
125
+ next === CharCode.at ||
126
+ next === CharCode.dollar ||
127
+ next === CharCode.underscore ||
128
+ (next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
129
+ (next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ)
130
+ );
131
+ }
132
+
111
133
  /**
112
134
  * @param {string} input
113
135
  * @param {number} i
@@ -299,12 +321,32 @@ export function TSRXPlugin(config) {
299
321
  finishNode(node, type) {
300
322
  const finished = super.finishNode(node, type);
301
323
  if (type === 'TSModuleDeclaration') {
302
- const start = /** @type {number} */ (finished.start);
303
- const source = this.input.slice(start, start + 'namespace'.length);
304
- finished.metadata ??= { path: [] };
305
- finished.metadata.module_keyword = source.startsWith('namespace')
306
- ? 'namespace'
307
- : 'module';
324
+ const declaration = /** @type {AST.TSModuleDeclaration} */ (finished);
325
+ const start = /** @type {number} */ (declaration.start);
326
+ // acorn-typescript still exposes the legacy `global` flag without
327
+ // TSESTree's replacement `kind`; replace it at the parser boundary
328
+ // so downstream consumers only receive the current discriminator.
329
+ const legacy = /** @type {{ global?: boolean }} */ (declaration);
330
+ const prefix = this.input
331
+ .slice(start, declaration.id.start)
332
+ .replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, ' ')
333
+ .trim();
334
+ const kind =
335
+ declaration.kind ??
336
+ (legacy.global ? 'global' : prefix.endsWith('namespace') ? 'namespace' : 'module');
337
+ /** @type {AST.TSModuleDeclaration | null} */
338
+ let current = declaration;
339
+ while (current !== null) {
340
+ const current_legacy = /** @type {{ global?: boolean }} */ (current);
341
+ current.kind = kind;
342
+ delete current_legacy.global;
343
+ current.metadata ??= { path: [] };
344
+ current.metadata.module_keyword = kind;
345
+ const body = /** @type {AST.TSModuleBlock | AST.TSModuleDeclaration} */ (
346
+ /** @type {unknown} */ (current.body)
347
+ );
348
+ current = body?.type === 'TSModuleDeclaration' ? body : null;
349
+ }
308
350
  }
309
351
  return finished;
310
352
  }
@@ -603,7 +645,7 @@ export function TSRXPlugin(config) {
603
645
  }
604
646
  const ch = this.input.charCodeAt(index);
605
647
  if (
606
- ch === CharCode.lessThan ||
648
+ (ch === CharCode.lessThan && can_start_tag_after_lt(this.input, index)) ||
607
649
  ch === CharCode.openBrace ||
608
650
  ch === CharCode.closeBrace ||
609
651
  this.#isCodeBlockStart(index) ||
@@ -1126,7 +1168,7 @@ export function TSRXPlugin(config) {
1126
1168
  while (index < this.input.length) {
1127
1169
  const ch = this.input.charCodeAt(index);
1128
1170
  if (
1129
- ch === CharCode.lessThan ||
1171
+ (ch === CharCode.lessThan && can_start_tag_after_lt(this.input, index)) ||
1130
1172
  ch === CharCode.openBrace ||
1131
1173
  ch === CharCode.closeBrace ||
1132
1174
  this.#isJSXControlFlowDirectiveAt(index) ||
@@ -1227,16 +1269,8 @@ export function TSRXPlugin(config) {
1227
1269
  const index = skip_whitespace_from(this.input, this.start);
1228
1270
  const ch = this.input.charCodeAt(index);
1229
1271
  if (ch === CharCode.lessThan) {
1230
- const next = this.input.charCodeAt(index + 1);
1231
- if (next === CharCode.slash) return false;
1232
- const tagLike =
1233
- next === CharCode.greaterThan ||
1234
- next === CharCode.openBrace ||
1235
- next === CharCode.at ||
1236
- next === CharCode.dollar ||
1237
- next === CharCode.underscore ||
1238
- (next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
1239
- (next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ);
1272
+ if (this.input.charCodeAt(index + 1) === CharCode.slash) return false;
1273
+ const tagLike = can_start_tag_after_lt(this.input, index);
1240
1274
  const previous = this.#previousNonSpaceTabIndex(index);
1241
1275
  const afterSemicolon =
1242
1276
  previous >= 0 && this.input.charCodeAt(previous) === CharCode.semicolon;
@@ -2875,7 +2909,7 @@ export function TSRXPlugin(config) {
2875
2909
  this.#suppressTemplateRawTextToken = false;
2876
2910
  const context = this.curContext();
2877
2911
  if (
2878
- code !== CharCode.lessThan &&
2912
+ (code !== CharCode.lessThan || !can_start_tag_after_lt(this.input, this.pos)) &&
2879
2913
  code !== CharCode.greaterThan &&
2880
2914
  code !== CharCode.openBrace &&
2881
2915
  code !== CharCode.closeBrace &&
@@ -2913,17 +2947,7 @@ export function TSRXPlugin(config) {
2913
2947
  return super.readToken(code);
2914
2948
  }
2915
2949
  if (code === CharCode.lessThan) {
2916
- const next = this.input.charCodeAt(this.pos + 1);
2917
- const isTagLikeAfterLt =
2918
- next === CharCode.slash ||
2919
- next === CharCode.greaterThan ||
2920
- next === CharCode.openBrace ||
2921
- next === CharCode.at ||
2922
- next === CharCode.dollar ||
2923
- next === CharCode.underscore ||
2924
- (next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
2925
- (next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ);
2926
- if (this.exprAllowed && isTagLikeAfterLt) {
2950
+ if (this.exprAllowed && can_start_tag_after_lt(this.input, this.pos)) {
2927
2951
  ++this.pos;
2928
2952
  return this.finishToken(tstt.jsxTagStart);
2929
2953
  }
@@ -3038,21 +3062,7 @@ export function TSRXPlugin(config) {
3038
3062
  // <Something>...</Something>\n\n<Child />
3039
3063
  // <head><style>...</style></head>
3040
3064
  // We only do this when '<' is in a tag-like position.
3041
- const isWhitespaceAfterLt =
3042
- nextChar === CharCode.space ||
3043
- nextChar === CharCode.tab ||
3044
- nextChar === CharCode.lineFeed ||
3045
- nextChar === CharCode.carriageReturn;
3046
- const isTagLikeAfterLt =
3047
- !isWhitespaceAfterLt &&
3048
- (nextChar === CharCode.slash ||
3049
- nextChar === CharCode.greaterThan ||
3050
- nextChar === CharCode.openBrace ||
3051
- nextChar === CharCode.at ||
3052
- nextChar === CharCode.dollar ||
3053
- nextChar === CharCode.underscore ||
3054
- (nextChar >= CharCode.uppercaseA && nextChar <= CharCode.uppercaseZ) ||
3055
- (nextChar >= CharCode.lowercaseA && nextChar <= CharCode.lowercaseZ));
3065
+ const isTagLikeAfterLt = can_start_tag_after_lt(this.input, this.pos);
3056
3066
  const prevAllowsTagStart =
3057
3067
  prevNonWhitespaceChar === null ||
3058
3068
  prevNonWhitespaceChar === CharCode.lineFeed || // '\n'
@@ -3079,7 +3089,7 @@ export function TSRXPlugin(config) {
3079
3089
  prevNonWhitespaceChar === CharCode.openBrace ||
3080
3090
  prevNonWhitespaceChar === CharCode.greaterThan
3081
3091
  ) {
3082
- if (!isWhitespaceAfterLt) {
3092
+ if (isTagLikeAfterLt) {
3083
3093
  ++this.pos;
3084
3094
  return this.finishToken(tstt.jsxTagStart);
3085
3095
  }
@@ -4116,6 +4126,16 @@ export function TSRXPlugin(config) {
4116
4126
 
4117
4127
  case CharCode.lessThan:
4118
4128
  case CharCode.openBrace:
4129
+ // This path reads the children of an element nested in a `{ … }`
4130
+ // expression container, where the raw-text intercept in `readToken`
4131
+ // is off, so the literal-`<` rule has to be applied here too. Keep
4132
+ // scanning rather than finishing the token: the `<` belongs to the
4133
+ // text run, and finishing here would emit an empty text token when
4134
+ // it is the run's first character.
4135
+ if (ch === CharCode.lessThan && !can_start_tag_after_lt(this.input, this.pos)) {
4136
+ ++this.pos;
4137
+ break;
4138
+ }
4119
4139
  if (out || this.pos > chunkStart) {
4120
4140
  return this.finishToken(tstt.jsxText, out + this.input.slice(chunkStart, this.pos));
4121
4141
  }
@@ -4609,10 +4629,13 @@ export function TSRXPlugin(config) {
4609
4629
  } else if (this.type === tstt.jsxText) {
4610
4630
  // A nested element with its own body can leak a JSX expression context,
4611
4631
  // so the whitespace after its closing tag is mis-tokenized as a stale
4612
- // text token whose start was advanced onto the following `<`. Real JSX
4613
- // text never starts at `<`, so drop the leaked context and re-read the
4614
- // tag instead of emitting an empty node.
4615
- if (this.input.charCodeAt(this.start) === CharCode.lessThan) {
4632
+ // text token whose start was advanced onto the following `<`. Text never
4633
+ // starts at a `<` that can open a tag, so drop the leaked context and
4634
+ // re-read the tag instead of emitting an empty node.
4635
+ if (
4636
+ this.input.charCodeAt(this.start) === CharCode.lessThan &&
4637
+ can_start_tag_after_lt(this.input, this.start)
4638
+ ) {
4616
4639
  if (this.#jsxExpressionContainerDepth > 0) {
4617
4640
  // Inside a `{ … }` container the whole-stack counts below are
4618
4641
  // blind: the enclosing template's `tc_expr` contexts sit on the
package/src/scope.js CHANGED
@@ -130,7 +130,7 @@ export function create_scopes(ast, root, parent, error_options) {
130
130
  },
131
131
 
132
132
  TSModuleDeclaration(node, { state, next }) {
133
- const is_submodule = node.metadata?.module_keyword === 'module';
133
+ const is_submodule = node.declare !== true && node.kind === 'module';
134
134
  if (is_submodule && node.id?.type === 'Identifier') {
135
135
  state.scope.declare(node.id, 'normal', 'module', node);
136
136
  }
@@ -152,6 +152,15 @@ export function tsx_with_ts_locations(boundary_tokens = false, comments = undefi
152
152
  context.visit(value.body);
153
153
  },
154
154
 
155
+ // TSRX text may contain a literal `<` — one that cannot start a tag
156
+ // (`<span><3</span>`) or a raw-text `<script>` body — but the printed TSX
157
+ // is re-parsed by a JSX toolchain (esbuild, Babel, SWC), and JSX forbids
158
+ // a bare `<` in text. Emit it as `&lt;`, which those parsers decode back
159
+ // to the same string.
160
+ JSXText: (node, context) => {
161
+ context.write(node.value.replace(/</g, '&lt;'), node);
162
+ },
163
+
155
164
  // esrap's JSXOpeningElement printer doesn't emit `typeArguments`, so generic
156
165
  // component tags like `<RenderProp<User>>` lose the `<User>` in the output.
157
166
  JSXOpeningElement: (node, context) => {
@@ -180,12 +189,18 @@ export function tsx_with_ts_locations(boundary_tokens = false, comments = undefi
180
189
  }
181
190
  },
182
191
  TSModuleDeclaration: (node, context) => {
183
- // Ambient `declare module '…' { }` must keep its `declare` — the
184
- // typeOnly/volar output is real TS and `module '…' { }` alone is a
185
- // syntax error (TS1035). Non-ambient `module name { }` blocks have no
186
- // `declare` and print unchanged.
192
+ // `declare global` is represented as a TSModuleDeclaration whose id is
193
+ // `global`; adding `module` changes it into an unrelated named module.
194
+ // Ambient `declare module '…' { }` must still keep its `declare` —
195
+ // the typeOnly/volar output is real TS and `module '…' { … }` alone is
196
+ // a syntax error (TS1035).
187
197
  if (node.declare) context.write('declare ');
188
- context.write(node.metadata?.module_keyword ?? 'module');
198
+ if (node.kind === 'global') {
199
+ context.visit(node.id);
200
+ context.visit(node.body);
201
+ return;
202
+ }
203
+ context.write(node.kind);
189
204
  context.write(' ');
190
205
  context.visit(node.id);
191
206
  context.visit(node.body);
@@ -110,7 +110,7 @@ function is_server_module_declaration(node, block_name) {
110
110
  return (
111
111
  node.type === 'TSModuleDeclaration' &&
112
112
  node.declare !== true &&
113
- node.metadata?.module_keyword === 'module' &&
113
+ node.kind === 'module' &&
114
114
  identifier_name(node.id) === block_name
115
115
  );
116
116
  }
@@ -465,6 +465,7 @@ function lower_declaration(declaration, outside_names, block_name) {
465
465
  const namespace = /** @type {AST.TSStatement<AST.TSModuleDeclaration>} */ ({
466
466
  ...declaration,
467
467
  id,
468
+ kind: 'namespace',
468
469
  metadata: { ...declaration.metadata, module_keyword: 'namespace' },
469
470
  body: { ...declaration.body, body: [...aliases, ...rest] },
470
471
  });
package/types/index.d.ts CHANGED
@@ -106,7 +106,7 @@ export interface BaseNodeMetaData {
106
106
  has_template?: boolean;
107
107
  source_name?: string;
108
108
  source_length?: number;
109
- module_keyword?: 'module' | 'namespace';
109
+ module_keyword?: 'global' | 'module' | 'namespace';
110
110
  /**
111
111
  * Generated identifier whose SOURCE span sits inside an authored string
112
112
  * literal (e.g. a server-module lowering's namespace reference carrying
@@ -1302,7 +1302,7 @@ declare module 'estree' {
1302
1302
  }
1303
1303
  interface TSModuleDeclaration extends Omit<
1304
1304
  AcornTSNode<TSESTree.TSModuleDeclaration>,
1305
- 'body' | 'id'
1305
+ 'body' | 'global' | 'id'
1306
1306
  > {
1307
1307
  body: TSModuleBlock;
1308
1308
  /** A string literal for `declare module '<specifier>'`. */