@tsrx/core 0.1.49 → 0.1.51

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.
@@ -1,5 +1,5 @@
1
1
  /** @import * as AST from 'estree' */
2
- /** @import { JsxPlatform } from '@tsrx/core/types' */
2
+ /** @import { JsxPlatform, MaybeLocated } from '@tsrx/core/types' */
3
3
 
4
4
  /**
5
5
  * Type-only lowering of a platform's `module <name> { … }` server-module
@@ -78,6 +78,7 @@
78
78
  */
79
79
 
80
80
  import * as b from '../../utils/builders.js';
81
+ import { is_ast_node } from '../../utils/ast.js';
81
82
 
82
83
  const HOISTED_IMPORT_PREFIX = '__tsrx_server_import$';
83
84
 
@@ -101,12 +102,13 @@ const WALK_SKIP_KEYS = new Set([
101
102
  * them verbatim (where TS flags them) mirrors the build failure instead of
102
103
  * hiding it.
103
104
  *
104
- * @param {any} node
105
+ * @param {AST.Node} node
105
106
  * @param {string} block_name
107
+ * @returns {node is AST.TSModuleDeclaration}
106
108
  */
107
109
  function is_server_module_declaration(node, block_name) {
108
110
  return (
109
- node?.type === 'TSModuleDeclaration' &&
111
+ node.type === 'TSModuleDeclaration' &&
110
112
  node.declare !== true &&
111
113
  node.metadata?.module_keyword === 'module' &&
112
114
  identifier_name(node.id) === block_name
@@ -114,7 +116,7 @@ function is_server_module_declaration(node, block_name) {
114
116
  }
115
117
 
116
118
  /**
117
- * @param {any} node
119
+ * @param {AST.Node | null | undefined} node
118
120
  * @returns {string | null}
119
121
  */
120
122
  function identifier_name(node) {
@@ -124,18 +126,25 @@ function identifier_name(node) {
124
126
  }
125
127
 
126
128
  /**
127
- * @param {any} node
129
+ * @param {AST.Node} node
128
130
  * @param {string} import_specifier
131
+ * @returns {node is AST.ImportDeclaration}
129
132
  */
130
133
  function is_server_import(node, import_specifier) {
131
- return node?.type === 'ImportDeclaration' && node.source?.value === import_specifier;
134
+ return (
135
+ node.type === 'ImportDeclaration' &&
136
+ node.source.type === 'Literal' &&
137
+ node.source.value === import_specifier
138
+ );
132
139
  }
133
140
 
134
141
  /**
135
142
  * Copy `node`'s authored location onto a replacement node.
136
- * @param {any} node
137
- * @param {any} source
138
- * @returns {any}
143
+ *
144
+ * @template {AST.Node} T
145
+ * @param {T} node
146
+ * @param {MaybeLocated | null | undefined} source
147
+ * @returns {T}
139
148
  */
140
149
  function with_location(node, source) {
141
150
  if (source?.start != null) node.start = source.start;
@@ -161,15 +170,15 @@ function with_location(node, source) {
161
170
  * from the wrong length.
162
171
  *
163
172
  * @param {string} name
164
- * @param {any} literal
173
+ * @param {MaybeLocated} literal
165
174
  * @returns {AST.Identifier}
166
175
  */
167
176
  function string_span_namespace_ref(name, literal) {
168
177
  const node = b.id(name);
169
178
  node.metadata.string_literal_source_span = true;
170
179
  if (
171
- typeof literal?.start === 'number' &&
172
- typeof literal?.end === 'number' &&
180
+ typeof literal.start === 'number' &&
181
+ typeof literal.end === 'number' &&
173
182
  literal.end - literal.start >= 2
174
183
  ) {
175
184
  node.start = literal.start + 1;
@@ -198,31 +207,32 @@ function string_span_namespace_ref(name, literal) {
198
207
  * the name as a conflict costs nothing but an alias, and keeps the lowering
199
208
  * from ever changing what the client half of the file typechecks against.
200
209
  *
201
- * @param {any} ast
202
- * @param {any} declaration
210
+ * @param {AST.Program} ast
211
+ * @param {AST.Node} declaration
203
212
  * @returns {Set<string>}
204
213
  */
205
214
  function collect_outside_identifier_names(ast, declaration) {
206
215
  /** @type {Set<string>} */
207
216
  const names = new Set();
217
+ /** @type {WeakSet<object>} */
208
218
  const seen = new WeakSet();
209
- /** @param {any} node */
210
- function walk(node) {
211
- if (node === null || typeof node !== 'object' || seen.has(node) || node === declaration) {
219
+ /** @param {unknown} value */
220
+ function walk(value) {
221
+ if (value === null || typeof value !== 'object' || seen.has(value) || value === declaration) {
212
222
  return;
213
223
  }
214
- seen.add(node);
215
- if (Array.isArray(node)) {
216
- for (const child of node) walk(child);
224
+ seen.add(value);
225
+ if (Array.isArray(value)) {
226
+ for (const child of value) walk(child);
217
227
  return;
218
228
  }
219
- if (typeof node.type !== 'string') return;
220
- if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
221
- names.add(node.name);
229
+ if (!is_ast_node(value)) return;
230
+ if (value.type === 'Identifier' || value.type === 'JSXIdentifier') {
231
+ names.add(value.name);
222
232
  }
223
- for (const [key, value] of Object.entries(node)) {
233
+ for (const [key, child] of Object.entries(value)) {
224
234
  if (WALK_SKIP_KEYS.has(key)) continue;
225
- if (value !== null && typeof value === 'object') walk(value);
235
+ if (child !== null && typeof child === 'object') walk(child);
226
236
  }
227
237
  }
228
238
  walk(ast);
@@ -235,27 +245,25 @@ function collect_outside_identifier_names(ast, declaration) {
235
245
  * locations so hover / rename still target the source. `make_init`
236
246
  * builds a fresh init expression per call (nodes are never shared).
237
247
  *
238
- * @param {any[]} specifiers
248
+ * @param {AST.ImportSpecifier[]} specifiers
239
249
  * @param {() => AST.Expression} make_init
240
- * @param {any} loc_node
250
+ * @param {MaybeLocated} loc_node
251
+ * @returns {AST.VariableDeclaration}
241
252
  */
242
253
  function build_destructure(specifiers, make_init, loc_node) {
243
254
  const pattern = with_location(
244
255
  b.object_pattern(
245
- /** @type {any} */ (
246
- specifiers.map((specifier) =>
247
- with_location(
248
- b.prop(
249
- 'init',
250
- { ...specifier.imported },
251
- { ...specifier.local },
252
- false,
253
- specifier.imported?.type === 'Identifier' &&
254
- specifier.imported.name === specifier.local?.name,
255
- ),
256
- specifier,
256
+ specifiers.map((specifier) =>
257
+ with_location(
258
+ b.assignment_prop(
259
+ { ...specifier.imported },
260
+ { ...specifier.local },
261
+ false,
262
+ specifier.imported.type === 'Identifier' &&
263
+ specifier.imported.name === specifier.local.name,
257
264
  ),
258
- )
265
+ specifier,
266
+ ),
259
267
  ),
260
268
  ),
261
269
  loc_node,
@@ -264,40 +272,24 @@ function build_destructure(specifiers, make_init, loc_node) {
264
272
  return with_location(b.declaration('const', [declarator]), loc_node);
265
273
  }
266
274
 
267
- /**
268
- * `<left>.<right>` qualified name. Callers pass a `right` that carries the
269
- * authored specifier span, so hover / rename on the imported name resolve.
270
- *
271
- * @param {AST.Identifier} left
272
- * @param {any} right
273
- */
274
- function qualified_name(left, right) {
275
- return {
276
- type: 'TSQualifiedName',
277
- left,
278
- right,
279
- metadata: { path: [] },
280
- };
281
- }
282
-
283
275
  /**
284
276
  * `type <local> = <left>.<right>;` for a type-only import specifier.
285
- * `right` defaults to the specifier's imported name; a DEFAULT import
277
+ * Callers pass the specifier's imported name as `right`; a DEFAULT import
286
278
  * passes `default` (which has no authored span of its own).
287
279
  *
288
- * @param {any} specifier
280
+ * `right` carries the authored specifier span, so hover / rename on the
281
+ * imported name resolve.
282
+ *
283
+ * @param {AST.ImportSpecifier | AST.ImportDefaultSpecifier} specifier
289
284
  * @param {() => AST.Identifier} make_left
290
- * @param {any} [right]
285
+ * @param {AST.Identifier} right
286
+ * @returns {AST.TSStatement<AST.TSTypeAliasDeclaration>}
291
287
  */
292
- function build_type_alias(specifier, make_left, right = { ...specifier.imported }) {
288
+ function build_type_alias(specifier, make_left, right) {
293
289
  return with_location(
294
290
  b.ts_type_alias(
295
291
  { ...specifier.local },
296
- /** @type {any} */ ({
297
- type: 'TSTypeReference',
298
- typeName: qualified_name(make_left(), right),
299
- metadata: { path: [] },
300
- }),
292
+ b.ts_type_reference(b.ts_qualified_name(make_left(), right)),
301
293
  ),
302
294
  specifier,
303
295
  );
@@ -309,20 +301,12 @@ function build_type_alias(specifier, make_left, right = { ...specifier.imported
309
301
  * binding, where a `const` keeps only the value and a `type` alias only
310
302
  * the type.
311
303
  *
312
- * @param {any} specifier
313
- * @param {any} module_reference
304
+ * @param {AST.ImportDeclaration['specifiers'][number]} specifier
305
+ * @param {AST.EntityName} module_reference
306
+ * @returns {AST.TSStatement<AST.TSImportEqualsDeclaration>}
314
307
  */
315
308
  function build_import_equals(specifier, module_reference) {
316
- return with_location(
317
- /** @type {any} */ ({
318
- type: 'TSImportEqualsDeclaration',
319
- id: { ...specifier.local },
320
- moduleReference: module_reference,
321
- importKind: 'value',
322
- metadata: { path: [] },
323
- }),
324
- specifier,
325
- );
309
+ return with_location(b.ts_import_equals({ ...specifier.local }, module_reference), specifier);
326
310
  }
327
311
 
328
312
  /**
@@ -341,17 +325,22 @@ function build_import_equals(specifier, module_reference) {
341
325
  * `<ns>.default` is not (a JSON module's namespace has no `default`
342
326
  * member under bundler resolution).
343
327
  *
344
- * @param {any} statement
328
+ * @param {AST.ImportDeclaration} statement
345
329
  * @param {string} hoisted_name
330
+ * @returns {{ hoisted: AST.ImportDeclaration, aliases: AST.Node[] }}
346
331
  */
347
332
  function lower_colliding_import(statement, hoisted_name) {
348
333
  const default_name = hoisted_name + '_default';
334
+ /** @type {AST.Node[]} */
349
335
  const aliases = [];
336
+ /** @type {AST.ImportSpecifier[]} */
350
337
  const destructured_specifiers = [];
351
338
  let needs_default_hoist = false;
352
339
  let needs_namespace_hoist = false;
353
340
  for (const specifier of statement.specifiers) {
354
- const type_only = statement.importKind === 'type' || specifier.importKind === 'type';
341
+ const type_only =
342
+ statement.importKind === 'type' ||
343
+ (specifier.type === 'ImportSpecifier' && specifier.importKind === 'type');
355
344
  if (specifier.type === 'ImportNamespaceSpecifier') {
356
345
  needs_namespace_hoist = true;
357
346
  aliases.push(build_import_equals(specifier, b.id(hoisted_name)));
@@ -367,7 +356,7 @@ function lower_colliding_import(statement, hoisted_name) {
367
356
  );
368
357
  aliases.push(with_location(b.declaration('const', [declarator]), specifier));
369
358
  }
370
- } else if (specifier.imported?.type !== 'Identifier') {
359
+ } else if (specifier.imported.type !== 'Identifier') {
371
360
  // String-named — neither an entity name nor a qualified type
372
361
  // reference can hold a string, so type-only ones land here too:
373
362
  // the destructure is the one PARSEABLE fallback, at the cost of
@@ -376,13 +365,15 @@ function lower_colliding_import(statement, hoisted_name) {
376
365
  destructured_specifiers.push(specifier);
377
366
  } else if (type_only) {
378
367
  needs_namespace_hoist = true;
379
- aliases.push(build_type_alias(specifier, () => b.id(hoisted_name)));
368
+ aliases.push(
369
+ build_type_alias(specifier, () => b.id(hoisted_name), { ...specifier.imported }),
370
+ );
380
371
  } else {
381
372
  needs_namespace_hoist = true;
382
373
  aliases.push(
383
374
  build_import_equals(
384
375
  specifier,
385
- qualified_name(b.id(hoisted_name), { ...specifier.imported }),
376
+ b.ts_qualified_name(b.id(hoisted_name), { ...specifier.imported }),
386
377
  ),
387
378
  );
388
379
  }
@@ -393,6 +384,7 @@ function lower_colliding_import(statement, hoisted_name) {
393
384
 
394
385
  // Only the specifiers the aliases reference — an unreferenced mangled
395
386
  // specifier would draw a spurious `noUnusedLocals` diagnostic.
387
+ /** @type {AST.ImportDeclaration['specifiers']} */
396
388
  const hoist_specifiers = [];
397
389
  if (needs_default_hoist) {
398
390
  hoist_specifiers.push({
@@ -415,6 +407,7 @@ function lower_colliding_import(statement, hoisted_name) {
415
407
  source: with_location({ ...statement.source }, statement.source),
416
408
  importKind: 'value',
417
409
  attributes: statement.attributes,
410
+ metadata: { path: [] },
418
411
  },
419
412
  statement,
420
413
  );
@@ -425,13 +418,17 @@ function lower_colliding_import(statement, hoisted_name) {
425
418
  * Replace the server block with hoisted imports plus a namespace-valued
426
419
  * binding the checker can see through.
427
420
  *
428
- * @param {any} declaration
421
+ * @param {AST.TSModuleDeclaration} declaration
429
422
  * @param {Set<string>} outside_names
430
423
  * @param {string} block_name
424
+ * @returns {AST.Node[]}
431
425
  */
432
426
  function lower_declaration(declaration, outside_names, block_name) {
427
+ /** @type {AST.Node[]} */
433
428
  const hoisted_imports = [];
429
+ /** @type {AST.Node[]} */
434
430
  const aliases = [];
431
+ /** @type {AST.Node[]} */
435
432
  const rest = [];
436
433
  let hoisted_index = 0;
437
434
 
@@ -440,8 +437,8 @@ function lower_declaration(declaration, outside_names, block_name) {
440
437
  rest.push(statement);
441
438
  continue;
442
439
  }
443
- const collides = (statement.specifiers ?? []).some((/** @type {any} */ specifier) =>
444
- outside_names.has(specifier.local?.name),
440
+ const collides = statement.specifiers.some((specifier) =>
441
+ outside_names.has(specifier.local.name),
445
442
  );
446
443
  if (!collides) {
447
444
  // Authored node, hoisted as-is — its locations map 1:1.
@@ -462,15 +459,15 @@ function lower_declaration(declaration, outside_names, block_name) {
462
459
  // Identifier (the authored id could be a string Literal, whose span then
463
460
  // gets the same string-literal mapping treatment as the import source).
464
461
  const id =
465
- declaration.id?.type === 'Literal'
462
+ declaration.id.type === 'Literal'
466
463
  ? string_span_namespace_ref(block_name, declaration.id)
467
464
  : with_location(b.id(block_name), declaration.id);
468
- const namespace = {
465
+ const namespace = /** @type {AST.TSStatement<AST.TSModuleDeclaration>} */ ({
469
466
  ...declaration,
470
467
  id,
471
468
  metadata: { ...declaration.metadata, module_keyword: 'namespace' },
472
469
  body: { ...declaration.body, body: [...aliases, ...rest] },
473
- };
470
+ });
474
471
  return [...hoisted_imports, namespace];
475
472
  }
476
473
 
@@ -488,13 +485,14 @@ function lower_declaration(declaration, outside_names, block_name) {
488
485
  * imports) are a hard compile error in the dialect — those statements
489
486
  * stay verbatim so the editor's TS2307 mirrors the build error.
490
487
  *
491
- * @param {any} statement
488
+ * @param {AST.ImportDeclaration} statement
492
489
  * @param {string} block_name
490
+ * @returns {AST.Node[]}
493
491
  */
494
492
  function lower_server_import(statement, block_name) {
495
- const specifiers = statement.specifiers ?? [];
493
+ const specifiers = statement.specifiers;
496
494
  if (specifiers.length === 0) return [];
497
- if (specifiers.some((/** @type {any} */ s) => s.type !== 'ImportSpecifier')) {
495
+ if (!specifiers.every((specifier) => specifier.type === 'ImportSpecifier')) {
498
496
  return [statement];
499
497
  }
500
498
 
@@ -502,18 +500,23 @@ function lower_server_import(statement, block_name) {
502
500
  // inner span, so hover / go-to-def on the module name resolves to the
503
501
  // lowered block while the literal keeps its string coloring.
504
502
  const namespace_ref = () => string_span_namespace_ref(block_name, statement.source);
503
+ /** @type {AST.Node[]} */
505
504
  const lowered = [];
505
+ /** @type {AST.ImportSpecifier[]} */
506
506
  const destructured_specifiers = [];
507
507
  for (const specifier of specifiers) {
508
- if (specifier.imported?.type !== 'Identifier') {
508
+ if (specifier.imported.type !== 'Identifier') {
509
509
  // String-named — inexpressible in an entity name or qualified type
510
510
  // reference alike, so type-only ones fall back here too.
511
511
  destructured_specifiers.push(specifier);
512
512
  } else if (statement.importKind === 'type' || specifier.importKind === 'type') {
513
- lowered.push(build_type_alias(specifier, namespace_ref));
513
+ lowered.push(build_type_alias(specifier, namespace_ref, { ...specifier.imported }));
514
514
  } else {
515
515
  lowered.push(
516
- build_import_equals(specifier, qualified_name(namespace_ref(), { ...specifier.imported })),
516
+ build_import_equals(
517
+ specifier,
518
+ b.ts_qualified_name(namespace_ref(), { ...specifier.imported }),
519
+ ),
517
520
  );
518
521
  }
519
522
  }
@@ -540,7 +543,9 @@ function lower_server_import(statement, block_name) {
540
543
  */
541
544
  export function lower_server_module_for_types(ast, server_module) {
542
545
  const { blockName: block_name, importSpecifier: import_specifier } = server_module;
543
- const body = /** @type {any[] | undefined} */ (ast?.body);
546
+ // estree's `Statement` union has no TS declarations in it, so the module
547
+ // body is scanned as plain nodes and re-typed on the way back out.
548
+ const body = /** @type {AST.Node[]} */ (ast?.body);
544
549
  if (!Array.isArray(body)) return ast;
545
550
  const declaration = body.find((node) => is_server_module_declaration(node, block_name));
546
551
  // A body-less `module server;` only occurs mid-edit / in loose parses;
@@ -551,6 +556,7 @@ export function lower_server_module_for_types(ast, server_module) {
551
556
  // The lowered namespace claims the authored block name at module scope; a
552
557
  // block import local with the same name must be aliased out of its way.
553
558
  outside_names.add(block_name);
559
+ /** @type {AST.Node[]} */
554
560
  const new_body = [];
555
561
  for (const statement of body) {
556
562
  if (statement === declaration) {
@@ -561,5 +567,5 @@ export function lower_server_module_for_types(ast, server_module) {
561
567
  new_body.push(statement);
562
568
  }
563
569
  }
564
- return { ...ast, body: new_body };
570
+ return { ...ast, body: /** @type {AST.Program['body']} */ (new_body) };
565
571
  }
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  /**
13
- * @param {import('estree').Literal} node
13
+ * @param {AST.Literal} node
14
14
  * @returns {boolean}
15
15
  */
16
16
  export function is_static_literal(node) {
@@ -24,7 +24,7 @@ export function is_static_literal(node) {
24
24
  }
25
25
 
26
26
  /**
27
- * @param {any} node
27
+ * @param {AST.Node | null | undefined} node
28
28
  * @returns {boolean}
29
29
  */
30
30
  export function is_hoist_safe_expression(node) {
@@ -60,7 +60,7 @@ export function is_hoist_safe_expression(node) {
60
60
  }
61
61
 
62
62
  /**
63
- * @param {any} node
63
+ * @param {AST.Node | null | undefined} node
64
64
  * @returns {boolean}
65
65
  */
66
66
  export function is_hoist_safe_jsx_child(node) {
@@ -115,7 +115,7 @@ export function is_hoist_safe_jsx_node(node) {
115
115
 
116
116
  // Lowered dynamic tags reference a component-scoped const and resolve at
117
117
  // runtime — never static, never hoistable.
118
- if (/** @type {any} */ (node).metadata?.dynamicElement === true) {
118
+ if (node.metadata?.dynamicElement === true) {
119
119
  return false;
120
120
  }
121
121