@tsrx/core 0.1.43 → 0.1.44
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 +1 -1
- package/src/source-map-utils.js +18 -0
- package/src/transform/jsx/index.js +14 -0
- package/src/transform/jsx/server-module.js +565 -0
- package/src/transform/segments.js +19 -0
- package/types/index.d.ts +15 -1
- package/types/jsx-platform.d.ts +20 -0
package/package.json
CHANGED
package/src/source-map-utils.js
CHANGED
|
@@ -61,6 +61,24 @@ export const mapping_data_completion_only = {
|
|
|
61
61
|
completion: true,
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Full language support minus editor repainting, for a generated IDENTIFIER
|
|
66
|
+
* whose SOURCE span sits inside a string literal (e.g. the namespace
|
|
67
|
+
* reference a server-module lowering derives from the authored `'server'`
|
|
68
|
+
* import specifier). Hover, go-to-def, references, and diagnostics resolve
|
|
69
|
+
* through the mapping — `semantic` stays truthy via the object form, which
|
|
70
|
+
* Volar's `isHoverEnabled` accepts — but semantic TOKENS are suppressed with
|
|
71
|
+
* `shouldHighlight: () => false` so the span keeps its authored TextMate
|
|
72
|
+
* (string) coloring instead of being repainted as a variable. Completion is
|
|
73
|
+
* off: identifier completions inside a string literal are never valid.
|
|
74
|
+
* @type {Partial<VolarCodeMapping['data']>}
|
|
75
|
+
*/
|
|
76
|
+
export const mapping_data_string_span = {
|
|
77
|
+
...mapping_data,
|
|
78
|
+
completion: false,
|
|
79
|
+
semantic: { shouldHighlight: () => false },
|
|
80
|
+
};
|
|
81
|
+
|
|
64
82
|
/**
|
|
65
83
|
* Convert byte offset to line/column
|
|
66
84
|
* @param {number} offset
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
capture_jsx_child as captureJsxChild,
|
|
61
61
|
} from '../jsx-interleave.js';
|
|
62
62
|
import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
63
|
+
import { lower_server_module_for_types } from './server-module.js';
|
|
63
64
|
|
|
64
65
|
const TEMPLATE_FRAGMENT_ERROR =
|
|
65
66
|
'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only in expression position.';
|
|
@@ -631,6 +632,19 @@ export function createJsxTransform(platform) {
|
|
|
631
632
|
...(platform.hooks?.initialState?.() ?? {}),
|
|
632
633
|
};
|
|
633
634
|
|
|
635
|
+
// Opt-in server-module dialect (`module <name> { … }` plus its boundary
|
|
636
|
+
// `import … from '<specifier>'`): lower to plain checkable TS before any
|
|
637
|
+
// other pass sees the program. TYPE-ONLY output only — the runtime/build
|
|
638
|
+
// emit never reaches this branch, because the platform's own compiler
|
|
639
|
+
// owns the dialect's real codegen (isolation validation, RPC stubs).
|
|
640
|
+
// Copy-on-write: a program without a server block passes through as the
|
|
641
|
+
// same object.
|
|
642
|
+
if (transform_context.typeOnly && platform.serverModule) {
|
|
643
|
+
ast = /** @type {any} */ (
|
|
644
|
+
lower_server_module_for_types(/** @type {any} */ (ast), platform.serverModule)
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
634
648
|
ast = expand_child_code_blocks(/** @type {any} */ (ast));
|
|
635
649
|
ast = wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
|
|
636
650
|
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import { JsxPlatform } from '@tsrx/core/types' */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type-only lowering of a platform's `module <name> { … }` server-module
|
|
6
|
+
* dialect (opt-in via `platform.serverModule`).
|
|
7
|
+
*
|
|
8
|
+
* The platform's runtime compiler owns the real semantics of a server
|
|
9
|
+
* block: it validates isolation, emits the server namespace for SSR, and
|
|
10
|
+
* replaces `import { fn } from '<specifier>'` with RPC stubs for the
|
|
11
|
+
* browser. The type-only path never runs that codegen — it prints the
|
|
12
|
+
* parsed AST as virtual TSX, which used to render the block verbatim.
|
|
13
|
+
* Verbatim `module server { import … }` can NEVER typecheck: a static
|
|
14
|
+
* import inside a namespace body is TS1147, and the companion
|
|
15
|
+
* `import { fn } from 'server'` is TS2307 (no such module). So the dialect
|
|
16
|
+
* was un-typecheckable in editors.
|
|
17
|
+
*
|
|
18
|
+
* This module rewrites the PARSED ast (before the type-only transform
|
|
19
|
+
* prints it) into plain, checkable TypeScript with identical types:
|
|
20
|
+
*
|
|
21
|
+
* module server { import { db } from './db.ts';
|
|
22
|
+
* import { db } from './db.ts'; namespace server {
|
|
23
|
+
* export function f() { … } → export function f() { … }
|
|
24
|
+
* } }
|
|
25
|
+
* import { f } from 'server'; import f = server.f;
|
|
26
|
+
*
|
|
27
|
+
* The lowered namespace keeps the AUTHORED name and identifier location, so
|
|
28
|
+
* hovering the block's name resolves, and the aliases' namespace
|
|
29
|
+
* references mark the namespace as used (a server block nobody imports from
|
|
30
|
+
* is the ONE case `noUnusedLocals` still flags — on the authored name, which
|
|
31
|
+
* is the correct signal). A `declare module '<specifier>'` bridge (which
|
|
32
|
+
* would have let the authored import statement survive verbatim) is NOT
|
|
33
|
+
* possible: the virtual TSX is a module, where `declare module 'server'` is
|
|
34
|
+
* a module AUGMENTATION — TS2664 when no module 'server' exists, and TS2666
|
|
35
|
+
* for the `export =` even when a global stub supplies one; augmentations
|
|
36
|
+
* also merge program-wide, which would break the dialect's file-local
|
|
37
|
+
* semantics. Each lowered alias keeps the import's specifier locations, so
|
|
38
|
+
* hover/rename on the imported names and on the `'<specifier>'` source
|
|
39
|
+
* still resolve.
|
|
40
|
+
*
|
|
41
|
+
* Block imports hoist to module top level (namespaces close over module
|
|
42
|
+
* scope, so the body still resolves them — `noUnusedLocals` counts those
|
|
43
|
+
* uses), and each boundary import becomes `import x = server.x` aliases
|
|
44
|
+
* that keep every meaning of the export — an authored named import binds
|
|
45
|
+
* value AND type, so a class or enum stays usable as a type (type-only
|
|
46
|
+
* specifiers become `type x = server.x` aliases; a string-named import
|
|
47
|
+
* falls back to a value-only destructure). When a
|
|
48
|
+
* hoisted import's local name is also used anywhere in the client module
|
|
49
|
+
* (the compiler's isolation rule stops the server block from referencing
|
|
50
|
+
* client bindings, but nothing stops both sides from importing — or
|
|
51
|
+
* referencing a global named — `db`), hoisting it verbatim would collide or
|
|
52
|
+
* shadow. Those imports hoist as a mangled namespace import instead —
|
|
53
|
+
* always a VALUE import keeping the authored `with { … }` attributes,
|
|
54
|
+
* because an import-equals alias cannot reference a type-only import
|
|
55
|
+
* (TS1380) — and each original binding is rebuilt inside the namespace:
|
|
56
|
+
* `import db = __tsrx_server_import$0.db;` for value specifiers (the one
|
|
57
|
+
* alias form that keeps EVERY meaning, so a colliding class import stays
|
|
58
|
+
* usable as a type) and `type T = __tsrx_server_import$0.T;` for type-only
|
|
59
|
+
* ones. Three corners cannot use import-equals: a string-named import
|
|
60
|
+
* (`'x y' as db`) keeps a value-only destructure even when type-only — a
|
|
61
|
+
* string can never appear in an entity name OR a qualified type
|
|
62
|
+
* reference, so the destructure is the one parseable fallback; a value
|
|
63
|
+
* DEFAULT import hoists an extra
|
|
64
|
+
* mangled DEFAULT specifier rebound with `const` (no entity name reaches
|
|
65
|
+
* a default — `import db = ns.default` is TS1359 and the default binding
|
|
66
|
+
* itself has no namespace meaning — so a colliding default CLASS import
|
|
67
|
+
* keeps only its value meaning, while a type-only default becomes
|
|
68
|
+
* `type x = ns.default`); and a type-only NAMESPACE import rebinds as a
|
|
69
|
+
* plain import-equals, which adds a value meaning the authored form
|
|
70
|
+
* lacked. Each is an acceptable corner — a collision already requires the
|
|
71
|
+
* client half to use the same name.
|
|
72
|
+
*
|
|
73
|
+
* The rewrite is copy-on-write: every replacement node is built with
|
|
74
|
+
* spreads/builders and carries the ORIGINAL node's start/end/loc wherever it
|
|
75
|
+
* corresponds to authored code, so the transform's esrap print emits real
|
|
76
|
+
* source-mapped segments and hover / go-to-def / diagnostics keep mapping
|
|
77
|
+
* back to the source. The original parse is never mutated.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
import * as b from '../../utils/builders.js';
|
|
81
|
+
|
|
82
|
+
const HOISTED_IMPORT_PREFIX = '__tsrx_server_import$';
|
|
83
|
+
|
|
84
|
+
/** Object keys that never contain child AST nodes. */
|
|
85
|
+
const WALK_SKIP_KEYS = new Set([
|
|
86
|
+
'loc',
|
|
87
|
+
'start',
|
|
88
|
+
'end',
|
|
89
|
+
'range',
|
|
90
|
+
'parent',
|
|
91
|
+
'metadata',
|
|
92
|
+
'leadingComments',
|
|
93
|
+
'trailingComments',
|
|
94
|
+
'comments',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A non-ambient `TSModuleDeclaration` authored with the `module` keyword,
|
|
99
|
+
* narrowed to the one name the platform's dialect supports. Blocks with any
|
|
100
|
+
* other name are a hard compile error in the runtime compiler, so leaving
|
|
101
|
+
* them verbatim (where TS flags them) mirrors the build failure instead of
|
|
102
|
+
* hiding it.
|
|
103
|
+
*
|
|
104
|
+
* @param {any} node
|
|
105
|
+
* @param {string} block_name
|
|
106
|
+
*/
|
|
107
|
+
function is_server_module_declaration(node, block_name) {
|
|
108
|
+
return (
|
|
109
|
+
node?.type === 'TSModuleDeclaration' &&
|
|
110
|
+
node.declare !== true &&
|
|
111
|
+
node.metadata?.module_keyword === 'module' &&
|
|
112
|
+
identifier_name(node.id) === block_name
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {any} node
|
|
118
|
+
* @returns {string | null}
|
|
119
|
+
*/
|
|
120
|
+
function identifier_name(node) {
|
|
121
|
+
if (node?.type === 'Identifier') return node.name;
|
|
122
|
+
if (node?.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {any} node
|
|
128
|
+
* @param {string} import_specifier
|
|
129
|
+
*/
|
|
130
|
+
function is_server_import(node, import_specifier) {
|
|
131
|
+
return node?.type === 'ImportDeclaration' && node.source?.value === import_specifier;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Copy `node`'s authored location onto a replacement node.
|
|
136
|
+
* @param {any} node
|
|
137
|
+
* @param {any} source
|
|
138
|
+
* @returns {any}
|
|
139
|
+
*/
|
|
140
|
+
function with_location(node, source) {
|
|
141
|
+
if (source?.start != null) node.start = source.start;
|
|
142
|
+
if (source?.end != null) node.end = source.end;
|
|
143
|
+
if (source?.loc != null) node.loc = source.loc;
|
|
144
|
+
node.metadata ??= { path: [] };
|
|
145
|
+
return node;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A namespace reference standing in for an authored STRING-LITERAL span (the
|
|
150
|
+
* `'server'` import source, or a string-named block id). It carries the
|
|
151
|
+
* literal's INNER span — the text between the quotes — as its source
|
|
152
|
+
* mapping: `source_length` pins the mapped length to that inner text, since
|
|
153
|
+
* the GENERATED identifier is the block name, whose length the authored
|
|
154
|
+
* specifier need not share (`blockName` and `importSpecifier` may differ).
|
|
155
|
+
* The `string_literal_source_span` metadata flag makes the mapping
|
|
156
|
+
* collector serve hover/navigation WITHOUT semantic tokens: repainting part
|
|
157
|
+
* of an authored string literal as a namespace token breaks the editor's
|
|
158
|
+
* string coloring (the quotes stay outside the mapping for the same
|
|
159
|
+
* reason). A literal without usable positions gets NO loc at all — the
|
|
160
|
+
* collector then skips the mapping entirely rather than deriving a span
|
|
161
|
+
* from the wrong length.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} name
|
|
164
|
+
* @param {any} literal
|
|
165
|
+
* @returns {AST.Identifier}
|
|
166
|
+
*/
|
|
167
|
+
function string_span_namespace_ref(name, literal) {
|
|
168
|
+
const node = b.id(name);
|
|
169
|
+
node.metadata.string_literal_source_span = true;
|
|
170
|
+
if (
|
|
171
|
+
typeof literal?.start === 'number' &&
|
|
172
|
+
typeof literal?.end === 'number' &&
|
|
173
|
+
literal.end - literal.start >= 2
|
|
174
|
+
) {
|
|
175
|
+
node.start = literal.start + 1;
|
|
176
|
+
node.end = literal.end - 1;
|
|
177
|
+
node.metadata.source_length = literal.end - literal.start - 2;
|
|
178
|
+
if (literal.loc != null) {
|
|
179
|
+
// Fresh position objects: the authored literal's own loc must survive
|
|
180
|
+
// the transform untouched (copy-on-write), and a module specifier is
|
|
181
|
+
// always single-line, so shifting columns inside it is safe.
|
|
182
|
+
node.loc = {
|
|
183
|
+
start: { line: literal.loc.start.line, column: literal.loc.start.column + 1 },
|
|
184
|
+
end: { line: literal.loc.end.line, column: literal.loc.end.column - 1 },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return node;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Every identifier name that appears OUTSIDE the server block. This
|
|
193
|
+
* deliberately over-approximates "top-level client bindings": a hoisted
|
|
194
|
+
* server import may not only collide with a client import of the same name
|
|
195
|
+
* (a TS2300 duplicate we would introduce) but also shadow a GLOBAL the
|
|
196
|
+
* client code references (e.g. a server-side `import { crypto } from …`
|
|
197
|
+
* changing what client `crypto` resolves to). Treating any outside use of
|
|
198
|
+
* the name as a conflict costs nothing but an alias, and keeps the lowering
|
|
199
|
+
* from ever changing what the client half of the file typechecks against.
|
|
200
|
+
*
|
|
201
|
+
* @param {any} ast
|
|
202
|
+
* @param {any} declaration
|
|
203
|
+
* @returns {Set<string>}
|
|
204
|
+
*/
|
|
205
|
+
function collect_outside_identifier_names(ast, declaration) {
|
|
206
|
+
/** @type {Set<string>} */
|
|
207
|
+
const names = new Set();
|
|
208
|
+
const seen = new WeakSet();
|
|
209
|
+
/** @param {any} node */
|
|
210
|
+
function walk(node) {
|
|
211
|
+
if (node === null || typeof node !== 'object' || seen.has(node) || node === declaration) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
seen.add(node);
|
|
215
|
+
if (Array.isArray(node)) {
|
|
216
|
+
for (const child of node) walk(child);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (typeof node.type !== 'string') return;
|
|
220
|
+
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
|
221
|
+
names.add(node.name);
|
|
222
|
+
}
|
|
223
|
+
for (const [key, value] of Object.entries(node)) {
|
|
224
|
+
if (WALK_SKIP_KEYS.has(key)) continue;
|
|
225
|
+
if (value !== null && typeof value === 'object') walk(value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
walk(ast);
|
|
229
|
+
return names;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `const { a, b: c } = <init>;` rebinding each specifier's imported
|
|
234
|
+
* name to its local name. Property nodes keep the authored specifier
|
|
235
|
+
* locations so hover / rename still target the source. `make_init`
|
|
236
|
+
* builds a fresh init expression per call (nodes are never shared).
|
|
237
|
+
*
|
|
238
|
+
* @param {any[]} specifiers
|
|
239
|
+
* @param {() => AST.Expression} make_init
|
|
240
|
+
* @param {any} loc_node
|
|
241
|
+
*/
|
|
242
|
+
function build_destructure(specifiers, make_init, loc_node) {
|
|
243
|
+
const pattern = with_location(
|
|
244
|
+
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,
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
loc_node,
|
|
262
|
+
);
|
|
263
|
+
const declarator = with_location(b.declarator(pattern, make_init()), loc_node);
|
|
264
|
+
return with_location(b.declaration('const', [declarator]), loc_node);
|
|
265
|
+
}
|
|
266
|
+
|
|
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
|
+
/**
|
|
284
|
+
* `type <local> = <left>.<right>;` for a type-only import specifier.
|
|
285
|
+
* `right` defaults to the specifier's imported name; a DEFAULT import
|
|
286
|
+
* passes `default` (which has no authored span of its own).
|
|
287
|
+
*
|
|
288
|
+
* @param {any} specifier
|
|
289
|
+
* @param {() => AST.Identifier} make_left
|
|
290
|
+
* @param {any} [right]
|
|
291
|
+
*/
|
|
292
|
+
function build_type_alias(specifier, make_left, right = { ...specifier.imported }) {
|
|
293
|
+
return with_location(
|
|
294
|
+
b.ts_type_alias(
|
|
295
|
+
{ ...specifier.local },
|
|
296
|
+
/** @type {any} */ ({
|
|
297
|
+
type: 'TSTypeReference',
|
|
298
|
+
typeName: qualified_name(make_left(), right),
|
|
299
|
+
metadata: { path: [] },
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
specifier,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `import <local> = <reference>;` — an import-equals alias, the one form
|
|
308
|
+
* that preserves every meaning (value, type, namespace) of the referenced
|
|
309
|
+
* binding, where a `const` keeps only the value and a `type` alias only
|
|
310
|
+
* the type.
|
|
311
|
+
*
|
|
312
|
+
* @param {any} specifier
|
|
313
|
+
* @param {any} module_reference
|
|
314
|
+
*/
|
|
315
|
+
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
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Lower one block import whose local name(s) collide with outside code:
|
|
330
|
+
* hoist under mangled names as a VALUE import (an import-equals alias
|
|
331
|
+
* cannot reference a type-only import — TS1380) and rebuild each original
|
|
332
|
+
* binding inside the namespace body. Named value specifiers and namespace
|
|
333
|
+
* specifiers become `import x = …` aliases keeping every meaning of the
|
|
334
|
+
* mangled namespace specifier; type-only specifiers become `type` aliases
|
|
335
|
+
* (`<ns>.default` for a type-only default); string-named imports — even
|
|
336
|
+
* type-only ones — destructure the namespace object (a string cannot
|
|
337
|
+
* appear in an entity name or qualified type reference). A value DEFAULT import has no entity-name path at all
|
|
338
|
+
* (`import x = <ns>.default` is TS1359, and a default-import binding has
|
|
339
|
+
* no namespace meaning), so it hoists an extra mangled DEFAULT specifier
|
|
340
|
+
* and rebinds it with `const` — exact default-import semantics, which
|
|
341
|
+
* `<ns>.default` is not (a JSON module's namespace has no `default`
|
|
342
|
+
* member under bundler resolution).
|
|
343
|
+
*
|
|
344
|
+
* @param {any} statement
|
|
345
|
+
* @param {string} hoisted_name
|
|
346
|
+
*/
|
|
347
|
+
function lower_colliding_import(statement, hoisted_name) {
|
|
348
|
+
const default_name = hoisted_name + '_default';
|
|
349
|
+
const aliases = [];
|
|
350
|
+
const destructured_specifiers = [];
|
|
351
|
+
let needs_default_hoist = false;
|
|
352
|
+
let needs_namespace_hoist = false;
|
|
353
|
+
for (const specifier of statement.specifiers) {
|
|
354
|
+
const type_only = statement.importKind === 'type' || specifier.importKind === 'type';
|
|
355
|
+
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
356
|
+
needs_namespace_hoist = true;
|
|
357
|
+
aliases.push(build_import_equals(specifier, b.id(hoisted_name)));
|
|
358
|
+
} else if (specifier.type === 'ImportDefaultSpecifier') {
|
|
359
|
+
if (type_only) {
|
|
360
|
+
needs_namespace_hoist = true;
|
|
361
|
+
aliases.push(build_type_alias(specifier, () => b.id(hoisted_name), b.id('default')));
|
|
362
|
+
} else {
|
|
363
|
+
needs_default_hoist = true;
|
|
364
|
+
const declarator = with_location(
|
|
365
|
+
b.declarator({ ...specifier.local }, b.id(default_name)),
|
|
366
|
+
specifier,
|
|
367
|
+
);
|
|
368
|
+
aliases.push(with_location(b.declaration('const', [declarator]), specifier));
|
|
369
|
+
}
|
|
370
|
+
} else if (specifier.imported?.type !== 'Identifier') {
|
|
371
|
+
// String-named — neither an entity name nor a qualified type
|
|
372
|
+
// reference can hold a string, so type-only ones land here too:
|
|
373
|
+
// the destructure is the one PARSEABLE fallback, at the cost of
|
|
374
|
+
// binding only a value.
|
|
375
|
+
needs_namespace_hoist = true;
|
|
376
|
+
destructured_specifiers.push(specifier);
|
|
377
|
+
} else if (type_only) {
|
|
378
|
+
needs_namespace_hoist = true;
|
|
379
|
+
aliases.push(build_type_alias(specifier, () => b.id(hoisted_name)));
|
|
380
|
+
} else {
|
|
381
|
+
needs_namespace_hoist = true;
|
|
382
|
+
aliases.push(
|
|
383
|
+
build_import_equals(
|
|
384
|
+
specifier,
|
|
385
|
+
qualified_name(b.id(hoisted_name), { ...specifier.imported }),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (destructured_specifiers.length > 0) {
|
|
391
|
+
aliases.push(build_destructure(destructured_specifiers, () => b.id(hoisted_name), statement));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Only the specifiers the aliases reference — an unreferenced mangled
|
|
395
|
+
// specifier would draw a spurious `noUnusedLocals` diagnostic.
|
|
396
|
+
const hoist_specifiers = [];
|
|
397
|
+
if (needs_default_hoist) {
|
|
398
|
+
hoist_specifiers.push({
|
|
399
|
+
type: 'ImportDefaultSpecifier',
|
|
400
|
+
local: b.id(default_name),
|
|
401
|
+
metadata: { path: [] },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (needs_namespace_hoist) {
|
|
405
|
+
hoist_specifiers.push({
|
|
406
|
+
type: 'ImportNamespaceSpecifier',
|
|
407
|
+
local: b.id(hoisted_name),
|
|
408
|
+
metadata: { path: [] },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const hoisted = with_location(
|
|
412
|
+
{
|
|
413
|
+
type: 'ImportDeclaration',
|
|
414
|
+
specifiers: hoist_specifiers,
|
|
415
|
+
source: with_location({ ...statement.source }, statement.source),
|
|
416
|
+
importKind: 'value',
|
|
417
|
+
attributes: statement.attributes,
|
|
418
|
+
},
|
|
419
|
+
statement,
|
|
420
|
+
);
|
|
421
|
+
return { hoisted, aliases };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Replace the server block with hoisted imports plus a namespace-valued
|
|
426
|
+
* binding the checker can see through.
|
|
427
|
+
*
|
|
428
|
+
* @param {any} declaration
|
|
429
|
+
* @param {Set<string>} outside_names
|
|
430
|
+
* @param {string} block_name
|
|
431
|
+
*/
|
|
432
|
+
function lower_declaration(declaration, outside_names, block_name) {
|
|
433
|
+
const hoisted_imports = [];
|
|
434
|
+
const aliases = [];
|
|
435
|
+
const rest = [];
|
|
436
|
+
let hoisted_index = 0;
|
|
437
|
+
|
|
438
|
+
for (const statement of declaration.body?.body ?? []) {
|
|
439
|
+
if (statement.type !== 'ImportDeclaration') {
|
|
440
|
+
rest.push(statement);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const collides = (statement.specifiers ?? []).some((/** @type {any} */ specifier) =>
|
|
444
|
+
outside_names.has(specifier.local?.name),
|
|
445
|
+
);
|
|
446
|
+
if (!collides) {
|
|
447
|
+
// Authored node, hoisted as-is — its locations map 1:1.
|
|
448
|
+
hoisted_imports.push(statement);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const lowered = lower_colliding_import(statement, HOISTED_IMPORT_PREFIX + hoisted_index++);
|
|
452
|
+
hoisted_imports.push(lowered.hoisted);
|
|
453
|
+
aliases.push(...lowered.aliases);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// The lowered namespace deliberately keeps the authored block name: the
|
|
457
|
+
// authored `module <name>` id already claims that name in the file (the
|
|
458
|
+
// runtime compiler declares it as a module binding), so reusing it cannot
|
|
459
|
+
// introduce a new collision, and it is what makes hover on the block's
|
|
460
|
+
// name — and on the boundary import's source, whose span the destructure's
|
|
461
|
+
// init identifier carries — resolve to the block. The id is always an
|
|
462
|
+
// Identifier (the authored id could be a string Literal, whose span then
|
|
463
|
+
// gets the same string-literal mapping treatment as the import source).
|
|
464
|
+
const id =
|
|
465
|
+
declaration.id?.type === 'Literal'
|
|
466
|
+
? string_span_namespace_ref(block_name, declaration.id)
|
|
467
|
+
: with_location(b.id(block_name), declaration.id);
|
|
468
|
+
const namespace = {
|
|
469
|
+
...declaration,
|
|
470
|
+
id,
|
|
471
|
+
metadata: { ...declaration.metadata, module_keyword: 'namespace' },
|
|
472
|
+
body: { ...declaration.body, body: [...aliases, ...rest] },
|
|
473
|
+
};
|
|
474
|
+
return [...hoisted_imports, namespace];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Rewrite one `import { x, type T } from '<specifier>'` statement into
|
|
479
|
+
* bindings on the lowered namespace. Value specifiers become
|
|
480
|
+
* `import x = <ns>.x;` aliases — the authored import carries EVERY
|
|
481
|
+
* meaning of the export, so a `const { x } = <ns>;` destructure would
|
|
482
|
+
* strip the type meaning of a class or enum. Type-only specifiers become
|
|
483
|
+
* `type T = <ns>.T;` aliases; a string-named import — even a type-only
|
|
484
|
+
* one, since neither an entity name nor a qualified type reference can
|
|
485
|
+
* express it — falls back to a value-only destructure. A specifier-less
|
|
486
|
+
* boundary import binds nothing and is dropped (the runtime compiler
|
|
487
|
+
* accepts and elides it), while non-named specifiers (default / namespace
|
|
488
|
+
* imports) are a hard compile error in the dialect — those statements
|
|
489
|
+
* stay verbatim so the editor's TS2307 mirrors the build error.
|
|
490
|
+
*
|
|
491
|
+
* @param {any} statement
|
|
492
|
+
* @param {string} block_name
|
|
493
|
+
*/
|
|
494
|
+
function lower_server_import(statement, block_name) {
|
|
495
|
+
const specifiers = statement.specifiers ?? [];
|
|
496
|
+
if (specifiers.length === 0) return [];
|
|
497
|
+
if (specifiers.some((/** @type {any} */ s) => s.type !== 'ImportSpecifier')) {
|
|
498
|
+
return [statement];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Each reference to the namespace carries the authored import source's
|
|
502
|
+
// inner span, so hover / go-to-def on the module name resolves to the
|
|
503
|
+
// lowered block while the literal keeps its string coloring.
|
|
504
|
+
const namespace_ref = () => string_span_namespace_ref(block_name, statement.source);
|
|
505
|
+
const lowered = [];
|
|
506
|
+
const destructured_specifiers = [];
|
|
507
|
+
for (const specifier of specifiers) {
|
|
508
|
+
if (specifier.imported?.type !== 'Identifier') {
|
|
509
|
+
// String-named — inexpressible in an entity name or qualified type
|
|
510
|
+
// reference alike, so type-only ones fall back here too.
|
|
511
|
+
destructured_specifiers.push(specifier);
|
|
512
|
+
} else if (statement.importKind === 'type' || specifier.importKind === 'type') {
|
|
513
|
+
lowered.push(build_type_alias(specifier, namespace_ref));
|
|
514
|
+
} else {
|
|
515
|
+
lowered.push(
|
|
516
|
+
build_import_equals(specifier, qualified_name(namespace_ref(), { ...specifier.imported })),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (destructured_specifiers.length > 0) {
|
|
521
|
+
lowered.push(build_destructure(destructured_specifiers, namespace_ref, statement));
|
|
522
|
+
}
|
|
523
|
+
return lowered;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Lower the server-module dialect in a parsed program to plain TS the type
|
|
528
|
+
* checker accepts. Returns the ORIGINAL ast unchanged (same object) when
|
|
529
|
+
* the file has no server block; otherwise returns a new Program that shares
|
|
530
|
+
* every untouched statement with the original parse.
|
|
531
|
+
*
|
|
532
|
+
* Only the FIRST server declaration is lowered — a second one is a hard
|
|
533
|
+
* compile error in the runtime compiler, and leaving it verbatim surfaces a
|
|
534
|
+
* TS error in the same place. Likewise a boundary import without any server
|
|
535
|
+
* block stays verbatim (TS2307), mirroring the build error.
|
|
536
|
+
*
|
|
537
|
+
* @param {AST.Program} ast
|
|
538
|
+
* @param {NonNullable<JsxPlatform['serverModule']>} server_module
|
|
539
|
+
* @returns {AST.Program}
|
|
540
|
+
*/
|
|
541
|
+
export function lower_server_module_for_types(ast, server_module) {
|
|
542
|
+
const { blockName: block_name, importSpecifier: import_specifier } = server_module;
|
|
543
|
+
const body = /** @type {any[] | undefined} */ (ast?.body);
|
|
544
|
+
if (!Array.isArray(body)) return ast;
|
|
545
|
+
const declaration = body.find((node) => is_server_module_declaration(node, block_name));
|
|
546
|
+
// A body-less `module server;` only occurs mid-edit / in loose parses;
|
|
547
|
+
// leave it for TS to flag rather than fabricating an empty namespace.
|
|
548
|
+
if (declaration === undefined || !Array.isArray(declaration.body?.body)) return ast;
|
|
549
|
+
|
|
550
|
+
const outside_names = collect_outside_identifier_names(ast, declaration);
|
|
551
|
+
// The lowered namespace claims the authored block name at module scope; a
|
|
552
|
+
// block import local with the same name must be aliased out of its way.
|
|
553
|
+
outside_names.add(block_name);
|
|
554
|
+
const new_body = [];
|
|
555
|
+
for (const statement of body) {
|
|
556
|
+
if (statement === declaration) {
|
|
557
|
+
new_body.push(...lower_declaration(declaration, outside_names, block_name));
|
|
558
|
+
} else if (is_server_import(statement, import_specifier)) {
|
|
559
|
+
new_body.push(...lower_server_import(statement, block_name));
|
|
560
|
+
} else {
|
|
561
|
+
new_body.push(statement);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { ...ast, body: new_body };
|
|
565
|
+
}
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
mapping_data_verify_only,
|
|
61
61
|
mapping_data_verify_complete,
|
|
62
62
|
mapping_data_completion_only,
|
|
63
|
+
mapping_data_string_span,
|
|
63
64
|
build_line_offsets,
|
|
64
65
|
get_mapping_from_node,
|
|
65
66
|
} from '../source-map-utils.js';
|
|
@@ -586,6 +587,14 @@ export function convert_source_map_to_mappings(
|
|
|
586
587
|
if (node.metadata?.disable_verification) {
|
|
587
588
|
token.mappingData = { ...mapping_data, verification: false };
|
|
588
589
|
}
|
|
590
|
+
// A generated identifier whose source span sits inside a string
|
|
591
|
+
// literal (e.g. a server-module lowering's namespace reference
|
|
592
|
+
// carrying the authored `'server'` import specifier): serve
|
|
593
|
+
// hover/navigation but never semantic tokens, so the span keeps
|
|
594
|
+
// its TextMate string coloring.
|
|
595
|
+
if (node.metadata?.string_literal_source_span) {
|
|
596
|
+
token.mappingData = mapping_data_string_span;
|
|
597
|
+
}
|
|
589
598
|
tokens.push(token);
|
|
590
599
|
add_extra_source_mapping_tokens(node);
|
|
591
600
|
|
|
@@ -2231,6 +2240,16 @@ export function convert_source_map_to_mappings(
|
|
|
2231
2240
|
visit(node.expression);
|
|
2232
2241
|
}
|
|
2233
2242
|
return;
|
|
2243
|
+
} else if (node.type === 'TSImportEqualsDeclaration') {
|
|
2244
|
+
// TypeScript import alias: import foo = ns.bar;
|
|
2245
|
+
// Visit in source order: id, then the referenced entity name
|
|
2246
|
+
if (node.id) {
|
|
2247
|
+
visit(node.id);
|
|
2248
|
+
}
|
|
2249
|
+
if (node.moduleReference) {
|
|
2250
|
+
visit(node.moduleReference);
|
|
2251
|
+
}
|
|
2252
|
+
return;
|
|
2234
2253
|
} else if (node.type === 'TSInstantiationExpression') {
|
|
2235
2254
|
// TypeScript instantiation expression: new Foo<T>()
|
|
2236
2255
|
if (node.expression) {
|
package/types/index.d.ts
CHANGED
|
@@ -79,6 +79,14 @@ interface BaseNodeMetaData {
|
|
|
79
79
|
source_name?: string;
|
|
80
80
|
source_length?: number;
|
|
81
81
|
module_keyword?: 'module' | 'namespace';
|
|
82
|
+
/**
|
|
83
|
+
* Generated identifier whose SOURCE span sits inside an authored string
|
|
84
|
+
* literal (e.g. a server-module lowering's namespace reference carrying
|
|
85
|
+
* the `'server'` import specifier). The mapping collector serves
|
|
86
|
+
* hover/navigation from it but disables semantic tokens so the span
|
|
87
|
+
* keeps its string coloring.
|
|
88
|
+
*/
|
|
89
|
+
string_literal_source_span?: boolean;
|
|
82
90
|
is_capitalized?: boolean;
|
|
83
91
|
commentContainerId?: number;
|
|
84
92
|
parenthesized?: boolean;
|
|
@@ -971,7 +979,13 @@ declare module 'estree' {
|
|
|
971
979
|
typeParameters: TSTypeParameterDeclaration | undefined;
|
|
972
980
|
parameters: Parameter[];
|
|
973
981
|
}
|
|
974
|
-
interface TSImportEqualsDeclaration extends
|
|
982
|
+
interface TSImportEqualsDeclaration extends Omit<
|
|
983
|
+
AcornTSNode<TSESTree.TSImportEqualsDeclaration>,
|
|
984
|
+
'id' | 'moduleReference'
|
|
985
|
+
> {
|
|
986
|
+
id: AST.Identifier;
|
|
987
|
+
moduleReference: EntityName | TSExternalModuleReference;
|
|
988
|
+
}
|
|
975
989
|
interface TSImportType extends Omit<
|
|
976
990
|
AcornTSNode<TSESTree.TSImportType>,
|
|
977
991
|
'argument' | 'qualifier' | 'typeParameters'
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -434,6 +434,26 @@ export interface JsxPlatform {
|
|
|
434
434
|
unsupportedTryPendingMessage?: string;
|
|
435
435
|
};
|
|
436
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Opt-in lowering for a platform's file-local server-module dialect in
|
|
439
|
+
* TYPE-ONLY output. When set, a non-ambient `module <blockName> { … }`
|
|
440
|
+
* block and its companion `import { x } from '<importSpecifier>'`
|
|
441
|
+
* statements are rewritten into plain checkable TS (block imports hoisted
|
|
442
|
+
* to module scope, the block itself lowered to a namespace keeping the
|
|
443
|
+
* authored name, boundary imports lowered to destructures / `type`
|
|
444
|
+
* aliases on that namespace) before the type-only transform prints them.
|
|
445
|
+
* Verbatim, the dialect can never typecheck: a static import inside a
|
|
446
|
+
* namespace body is TS1147 and the boundary import is TS2307. Runtime /
|
|
447
|
+
* build output is untouched — the platform's own compiler owns the
|
|
448
|
+
* dialect's real codegen. When absent, the pre-pass is skipped entirely.
|
|
449
|
+
*/
|
|
450
|
+
serverModule?: {
|
|
451
|
+
/** Authored block name (`module server { … }` → `'server'`). */
|
|
452
|
+
blockName: string;
|
|
453
|
+
/** Boundary import's module specifier (`from 'server'` → `'server'`). */
|
|
454
|
+
importSpecifier: string;
|
|
455
|
+
};
|
|
456
|
+
|
|
437
457
|
/**
|
|
438
458
|
* Optional overrides for parts of the transform that diverge substantially
|
|
439
459
|
* between platforms (control flow, component lowering, imports, element
|