@deepseek-ai/dsh-typert-generator 0.0.1-rc.1
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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +41 -0
- package/README.zh.md +41 -0
- package/lib/index.js +3922 -0
- package/lib/invariant.js +24 -0
- package/lib/types/analyzer.d.ts +143 -0
- package/lib/types/analyzer.js +2723 -0
- package/lib/types/cordis-catalog.d.ts +148 -0
- package/lib/types/cordis-catalog.js +695 -0
- package/lib/types/emitter.d.ts +55 -0
- package/lib/types/emitter.js +745 -0
- package/lib/types/index.d.ts +16 -0
- package/lib/types/index.js +12 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/model.d.ts +409 -0
- package/lib/types/model.js +48 -0
- package/lib/types/renderer.d.ts +90 -0
- package/lib/types/renderer.js +351 -0
- package/lib/types/tsdown-plugin.d.ts +34 -0
- package/lib/types/tsdown-plugin.js +136 -0
- package/lib/types/workspace.d.ts +36 -0
- package/lib/types/workspace.js +114 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript project analyzer for the compiler-independent Typert model.
|
|
3
|
+
* Programs, symbols, and syntax nodes remain extraction-only implementation
|
|
4
|
+
* details; callers receive only the model declared in {@link ./model.ts}.
|
|
5
|
+
* @module @deepseek-ai/dsh-typert-generator/analyzer
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
/** Analysis failure with a source-oriented diagnostic. */
|
|
11
|
+
export class TypertAnalysisError extends Error {
|
|
12
|
+
name = 'TypertAnalysisError';
|
|
13
|
+
}
|
|
14
|
+
class SourceEditQueued extends Error {
|
|
15
|
+
}
|
|
16
|
+
const EMPTY_DOCUMENTATION = { tags: [] };
|
|
17
|
+
/**
|
|
18
|
+
* Process-wide parse cache for the bundled TypeScript default libraries.
|
|
19
|
+
* `typescript/lib/lib.*.d.ts` content is immutable for the process lifetime,
|
|
20
|
+
* so parses are shared across every {@link WorkspaceCaches} instance; the key
|
|
21
|
+
* carries the parse-affecting settings, keeping reuse exact.
|
|
22
|
+
*/
|
|
23
|
+
const defaultLibraryParses = new Map();
|
|
24
|
+
function defaultLibraryKey(fileName, languageVersionOrOptions) {
|
|
25
|
+
const options = typeof languageVersionOrOptions === 'object'
|
|
26
|
+
? languageVersionOrOptions
|
|
27
|
+
: { languageVersion: languageVersionOrOptions };
|
|
28
|
+
return [
|
|
29
|
+
fileName,
|
|
30
|
+
String(options.languageVersion),
|
|
31
|
+
String(options.impliedNodeFormat ?? ''),
|
|
32
|
+
String(options.jsDocParsingMode ?? ''),
|
|
33
|
+
].join('\0');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Shared memo over one immutable workspace snapshot. Passing one instance to
|
|
37
|
+
* several analyzers (the batched and write-mode children reuse their parent's
|
|
38
|
+
* automatically) reuses parsed tsconfigs, the registration inventory, and
|
|
39
|
+
* per-face compiler hosts whose parsed and bound source files and module
|
|
40
|
+
* resolutions carry across programs. Callers that mutate workspace files
|
|
41
|
+
* between analyses must start from a fresh instance; write-mode source edits
|
|
42
|
+
* invalidate themselves through {@link invalidate}.
|
|
43
|
+
*/
|
|
44
|
+
export class WorkspaceCaches {
|
|
45
|
+
/** Parsed tsconfig files by absolute config path. */
|
|
46
|
+
configs = new Map();
|
|
47
|
+
/** Registration inventories keyed by root and aggregate config paths. */
|
|
48
|
+
registrations = new Map();
|
|
49
|
+
hosts = new Map();
|
|
50
|
+
/**
|
|
51
|
+
* Parse one tsconfig once per workspace snapshot.
|
|
52
|
+
* @param path - absolute config path.
|
|
53
|
+
* @returns the memoized parse result.
|
|
54
|
+
*/
|
|
55
|
+
config(path) {
|
|
56
|
+
let parsed = this.configs.get(path);
|
|
57
|
+
if (parsed === undefined) {
|
|
58
|
+
parsed = parseConfig(path);
|
|
59
|
+
this.configs.set(path, parsed);
|
|
60
|
+
}
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Return the shared compiler host for one face. Every program of one face
|
|
65
|
+
* is built from the same aggregate compiler options (the first call wins),
|
|
66
|
+
* so parsed source files, binder state, and module resolutions are safe to
|
|
67
|
+
* reuse across the face's batched programs.
|
|
68
|
+
* @param face - the face whose programs share this host.
|
|
69
|
+
* @param options - the face's effective compiler options.
|
|
70
|
+
* @returns a compiler host with source-file and module-resolution caches.
|
|
71
|
+
*/
|
|
72
|
+
programHost(face, options) {
|
|
73
|
+
let entry = this.hosts.get(face);
|
|
74
|
+
if (entry === undefined) {
|
|
75
|
+
const host = ts.createCompilerHost(options);
|
|
76
|
+
const files = new Map();
|
|
77
|
+
const resolutionCache = ts.createModuleResolutionCache(host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName), options);
|
|
78
|
+
const base = host.getSourceFile.bind(host);
|
|
79
|
+
// The snapshot contract makes shouldCreateNewSourceFile irrelevant: it
|
|
80
|
+
// only fires under oldProgram reuse, which these fresh programs never
|
|
81
|
+
// request, and invalidate() is the one supported re-read path.
|
|
82
|
+
host.getSourceFile = (fileName, languageVersionOrOptions, onError) => {
|
|
83
|
+
if (isStandardLibraryFile(fileName)) {
|
|
84
|
+
const key = defaultLibraryKey(fileName, languageVersionOrOptions);
|
|
85
|
+
if (!defaultLibraryParses.has(key)) {
|
|
86
|
+
defaultLibraryParses.set(key, base(fileName, languageVersionOrOptions, onError));
|
|
87
|
+
}
|
|
88
|
+
return defaultLibraryParses.get(key);
|
|
89
|
+
}
|
|
90
|
+
if (!files.has(fileName))
|
|
91
|
+
files.set(fileName, base(fileName, languageVersionOrOptions, onError));
|
|
92
|
+
return files.get(fileName);
|
|
93
|
+
};
|
|
94
|
+
host.getModuleResolutionCache = () => resolutionCache;
|
|
95
|
+
entry = { host, files };
|
|
96
|
+
this.hosts.set(face, entry);
|
|
97
|
+
}
|
|
98
|
+
return entry.host;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Drop cached parses of one edited source file so the next analysis reads
|
|
102
|
+
* the written content.
|
|
103
|
+
* @param file - path of the edited file.
|
|
104
|
+
*/
|
|
105
|
+
invalidate(file) {
|
|
106
|
+
const target = realPath(file);
|
|
107
|
+
for (const { files } of this.hosts.values()) {
|
|
108
|
+
for (const key of [...files.keys()]) {
|
|
109
|
+
if (realPath(key) === target)
|
|
110
|
+
files.delete(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Analyze host and client as independent TypeScript programs. */
|
|
116
|
+
export class WorkspaceAnalyzer {
|
|
117
|
+
options;
|
|
118
|
+
queuedEdit;
|
|
119
|
+
crossFaceLinks = new Map();
|
|
120
|
+
checkedProjects = new Set();
|
|
121
|
+
registrations = [];
|
|
122
|
+
caches;
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this.options = {
|
|
125
|
+
root: realPath(options.root),
|
|
126
|
+
hostConfig: options.hostConfig ?? 'tsconfig.host.json',
|
|
127
|
+
clientConfig: options.clientConfig ?? 'tsconfig.client.json',
|
|
128
|
+
faces: options.faces ?? ['host', 'client'],
|
|
129
|
+
checkDiagnostics: options.checkDiagnostics ?? true,
|
|
130
|
+
mode: options.mode ?? 'check',
|
|
131
|
+
...(options.packages === undefined ? {} : { packages: options.packages }),
|
|
132
|
+
};
|
|
133
|
+
this.caches = options.caches ?? new WorkspaceCaches();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build the workspace model. Write mode applies inferred annotations and then
|
|
137
|
+
* returns a fresh check-mode analysis of the edited projects.
|
|
138
|
+
* @returns the independent face models and their explicit cross-face links.
|
|
139
|
+
*/
|
|
140
|
+
analyze() {
|
|
141
|
+
this.registrations = this.loadRegistrations();
|
|
142
|
+
const selected = this.options.packages === undefined
|
|
143
|
+
? undefined
|
|
144
|
+
: new Set(this.options.packages);
|
|
145
|
+
const faces = [];
|
|
146
|
+
try {
|
|
147
|
+
for (const face of this.options.faces) {
|
|
148
|
+
const registrations = this.registrations.filter(registration => registration.face === face && (selected === undefined || selected.has(registration.name)));
|
|
149
|
+
if (registrations.length === 0)
|
|
150
|
+
continue;
|
|
151
|
+
if (this.options.checkDiagnostics) {
|
|
152
|
+
for (const registration of registrations)
|
|
153
|
+
this.checkProject(registration);
|
|
154
|
+
}
|
|
155
|
+
const aggregatePath = resolve(this.options.root, face === 'host' ? this.options.hostConfig : this.options.clientConfig);
|
|
156
|
+
const aggregate = this.caches.config(aggregatePath);
|
|
157
|
+
const rootNames = [...new Set(registrations.flatMap(registration => registration.config.parsed.fileNames))];
|
|
158
|
+
const options = {
|
|
159
|
+
...aggregate.parsed.options,
|
|
160
|
+
composite: false,
|
|
161
|
+
incremental: false,
|
|
162
|
+
noEmit: true,
|
|
163
|
+
};
|
|
164
|
+
const program = ts.createProgram({
|
|
165
|
+
rootNames,
|
|
166
|
+
options,
|
|
167
|
+
host: this.caches.programHost(face, options),
|
|
168
|
+
});
|
|
169
|
+
faces.push(new FaceAnalyzer({
|
|
170
|
+
root: this.options.root,
|
|
171
|
+
face,
|
|
172
|
+
program,
|
|
173
|
+
registrations,
|
|
174
|
+
allRegistrations: this.registrations,
|
|
175
|
+
mode: this.options.mode,
|
|
176
|
+
queueEdit: (edit) => { this.queueEdit(edit); },
|
|
177
|
+
crossFaceLinks: this.crossFaceLinks,
|
|
178
|
+
}).analyze());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (!(error instanceof SourceEditQueued) || this.options.mode !== 'write' || this.queuedEdit === undefined)
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
if (this.queuedEdit !== undefined) {
|
|
186
|
+
this.applyEdit(this.queuedEdit);
|
|
187
|
+
return new WorkspaceAnalyzer({ ...this.options, caches: this.caches, mode: 'write' }).analyze();
|
|
188
|
+
}
|
|
189
|
+
if (this.options.mode === 'write') {
|
|
190
|
+
return new WorkspaceAnalyzer({ ...this.options, caches: this.caches, mode: 'check' }).analyze();
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
faces,
|
|
194
|
+
crossFaceLinks: [...this.crossFaceLinks.values()].sort(compareCrossFaceLinks),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Analyze an explicit package selection through bounded compiler programs.
|
|
199
|
+
* The resulting model is identical in shape to {@link analyze}; stable graph
|
|
200
|
+
* ids let repeated dependency declarations merge without flattening types.
|
|
201
|
+
* @param batchSize - maximum selected packages in one face program.
|
|
202
|
+
* @returns one merged workspace model.
|
|
203
|
+
*/
|
|
204
|
+
analyzeInBatches(batchSize = 8) {
|
|
205
|
+
if (this.options.packages === undefined) {
|
|
206
|
+
throw new TypertAnalysisError('typert: batched analysis requires an explicit package selection');
|
|
207
|
+
}
|
|
208
|
+
if (!Number.isInteger(batchSize) || batchSize < 1) {
|
|
209
|
+
throw new TypertAnalysisError(`typert: batch size must be a positive integer, received ${String(batchSize)}`);
|
|
210
|
+
}
|
|
211
|
+
const batches = [];
|
|
212
|
+
for (let index = 0; index < this.options.packages.length; index += batchSize) {
|
|
213
|
+
batches.push(new WorkspaceAnalyzer({
|
|
214
|
+
...this.options,
|
|
215
|
+
caches: this.caches,
|
|
216
|
+
packages: this.options.packages.slice(index, index + batchSize),
|
|
217
|
+
}).analyze());
|
|
218
|
+
}
|
|
219
|
+
return mergeWorkspaceModels(batches);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Discover package faces from public-export-reachable Cordis augmentations
|
|
223
|
+
* and explicit `@typert` roots without constructing a type-checker program.
|
|
224
|
+
* @returns contributors grouped by package with deterministic face order.
|
|
225
|
+
*/
|
|
226
|
+
discoverPackages() {
|
|
227
|
+
const registrations = this.loadRegistrations()
|
|
228
|
+
.filter(registration => this.options.faces.includes(registration.face))
|
|
229
|
+
.filter(registration => this.registrationHasSurface(registration));
|
|
230
|
+
const packages = new Map();
|
|
231
|
+
for (const registration of registrations) {
|
|
232
|
+
const current = packages.get(registration.name) ?? {
|
|
233
|
+
root: slash(relative(this.options.root, registration.root)),
|
|
234
|
+
faces: new Set(),
|
|
235
|
+
};
|
|
236
|
+
current.faces.add(registration.face);
|
|
237
|
+
packages.set(registration.name, current);
|
|
238
|
+
}
|
|
239
|
+
return [...packages]
|
|
240
|
+
.map(([packageName, value]) => ({
|
|
241
|
+
package: packageName,
|
|
242
|
+
root: value.root,
|
|
243
|
+
faces: [...value.faces].sort(),
|
|
244
|
+
}))
|
|
245
|
+
.sort((left, right) => left.package.localeCompare(right.package));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Index top-level exported type declarations without promoting them to graph
|
|
249
|
+
* roots. Consumers use this lexical index for ambiguity checks while all
|
|
250
|
+
* semantic traversal continues through {@link TypeGraph}.
|
|
251
|
+
* @returns declarations from the selected faces and package projects.
|
|
252
|
+
*/
|
|
253
|
+
indexSourceDeclarations() {
|
|
254
|
+
const selected = this.options.packages === undefined ? undefined : new Set(this.options.packages);
|
|
255
|
+
const declarations = [];
|
|
256
|
+
for (const registration of this.loadRegistrations()) {
|
|
257
|
+
if (!this.options.faces.includes(registration.face)
|
|
258
|
+
|| (selected !== undefined && !selected.has(registration.name)))
|
|
259
|
+
continue;
|
|
260
|
+
for (const file of registration.config.parsed.fileNames) {
|
|
261
|
+
const relativeFile = slash(relative(this.options.root, file));
|
|
262
|
+
if (!existsSync(file)
|
|
263
|
+
|| !isWithin(realPath(file), join(registration.root, 'src'))
|
|
264
|
+
|| !/\.(?:cts|mts|ts)$/.test(file))
|
|
265
|
+
continue;
|
|
266
|
+
const sourceFile = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
267
|
+
for (const statement of sourceFile.statements) {
|
|
268
|
+
if (!isTypeDeclaration(statement)
|
|
269
|
+
|| statement.name === undefined
|
|
270
|
+
|| !hasModifier(statement, ts.SyntaxKind.ExportKeyword))
|
|
271
|
+
continue;
|
|
272
|
+
const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
|
|
273
|
+
declarations.push({
|
|
274
|
+
face: registration.face,
|
|
275
|
+
package: registration.name,
|
|
276
|
+
name: statement.name.text,
|
|
277
|
+
kind: ts.isClassDeclaration(statement)
|
|
278
|
+
? 'class'
|
|
279
|
+
: ts.isInterfaceDeclaration(statement)
|
|
280
|
+
? 'interface'
|
|
281
|
+
: ts.isTypeAliasDeclaration(statement)
|
|
282
|
+
? 'alias'
|
|
283
|
+
: 'enum',
|
|
284
|
+
location: {
|
|
285
|
+
file: relativeFile,
|
|
286
|
+
line: position.line + 1,
|
|
287
|
+
column: position.character + 1,
|
|
288
|
+
},
|
|
289
|
+
text: declarationText(statement),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return uniqueBy(declarations, declaration => `${declaration.face}\0${declaration.location.file}\0${String(declaration.location.line)}\0${declaration.name}`)
|
|
295
|
+
.sort((left, right) => left.face.localeCompare(right.face)
|
|
296
|
+
|| left.location.file.localeCompare(right.location.file)
|
|
297
|
+
|| left.location.line - right.location.line);
|
|
298
|
+
}
|
|
299
|
+
loadRegistrations() {
|
|
300
|
+
const inventoryKey = `${this.options.root}\0${this.options.hostConfig}\0${this.options.clientConfig}`;
|
|
301
|
+
const cached = this.caches.registrations.get(inventoryKey);
|
|
302
|
+
if (cached !== undefined)
|
|
303
|
+
return cached;
|
|
304
|
+
const registrations = [];
|
|
305
|
+
for (const face of ['host', 'client']) {
|
|
306
|
+
const aggregatePath = resolve(this.options.root, face === 'host' ? this.options.hostConfig : this.options.clientConfig);
|
|
307
|
+
if (!existsSync(aggregatePath))
|
|
308
|
+
continue;
|
|
309
|
+
const aggregate = this.caches.config(aggregatePath);
|
|
310
|
+
for (const reference of aggregate.parsed.projectReferences ?? []) {
|
|
311
|
+
const configPath = projectConfigPath(reference.path);
|
|
312
|
+
const packageRoot = dirname(configPath);
|
|
313
|
+
if (!isWithin(realPath(packageRoot), join(this.options.root, 'packages')))
|
|
314
|
+
continue;
|
|
315
|
+
const manifestPath = join(packageRoot, 'package.json');
|
|
316
|
+
if (!existsSync(manifestPath))
|
|
317
|
+
continue;
|
|
318
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
319
|
+
if (typeof manifest.name !== 'string')
|
|
320
|
+
continue;
|
|
321
|
+
const registration = {
|
|
322
|
+
face,
|
|
323
|
+
name: manifest.name,
|
|
324
|
+
root: realPath(packageRoot),
|
|
325
|
+
config: this.caches.config(configPath),
|
|
326
|
+
manifest,
|
|
327
|
+
};
|
|
328
|
+
if (!isDualFacePackage(manifest)) {
|
|
329
|
+
registrations.push(registration);
|
|
330
|
+
}
|
|
331
|
+
else if (configPath === join(packageRoot, 'tsconfig.json')) {
|
|
332
|
+
registrations.push({ ...registration, face: 'host', exportSubpaths: hostExportSubpaths(manifest) }, { ...registration, face: 'client', exportSubpaths: clientExportSubpaths(manifest) });
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
registrations.push({
|
|
336
|
+
...registration,
|
|
337
|
+
exportSubpaths: face === 'host'
|
|
338
|
+
? hostExportSubpaths(manifest)
|
|
339
|
+
: clientExportSubpaths(manifest),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const inventory = uniqueBy(registrations, registration => `${registration.face}\0${registration.name}`)
|
|
345
|
+
.sort((left, right) => left.face.localeCompare(right.face) || left.name.localeCompare(right.name));
|
|
346
|
+
this.caches.registrations.set(inventoryKey, inventory);
|
|
347
|
+
return inventory;
|
|
348
|
+
}
|
|
349
|
+
entrySourcePaths(registration) {
|
|
350
|
+
return packageExportTargets(registration.manifest)
|
|
351
|
+
.filter(([subpath, target]) => (registration.exportSubpaths === undefined
|
|
352
|
+
|| registration.exportSubpaths.includes(subpath))
|
|
353
|
+
&& !target.includes('*')
|
|
354
|
+
&& subpath !== './package.json'
|
|
355
|
+
&& subpath !== './typert'
|
|
356
|
+
&& subpath !== './client/typert'
|
|
357
|
+
&& subpath !== './remote'
|
|
358
|
+
&& !target.endsWith('.json'))
|
|
359
|
+
.map(([, target]) => sourcePathForExport(registration.root, target))
|
|
360
|
+
.filter(existsSync);
|
|
361
|
+
}
|
|
362
|
+
registrationHasSurface(registration) {
|
|
363
|
+
const seen = new Set();
|
|
364
|
+
const queue = this.entrySourcePaths(registration);
|
|
365
|
+
while (queue.length > 0) {
|
|
366
|
+
const file = realPath(queue.shift());
|
|
367
|
+
if (seen.has(file) || !isWithin(file, registration.root))
|
|
368
|
+
continue;
|
|
369
|
+
seen.add(file);
|
|
370
|
+
const source = readFileSync(file, 'utf8');
|
|
371
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
372
|
+
if (sourceFileHasSurface(sourceFile))
|
|
373
|
+
return true;
|
|
374
|
+
for (const imported of ts.preProcessFile(source).importedFiles) {
|
|
375
|
+
const resolved = ts.resolveModuleName(imported.fileName, file, registration.config.parsed.options, ts.sys).resolvedModule;
|
|
376
|
+
if (resolved !== undefined && isWithin(resolved.resolvedFileName, registration.root)) {
|
|
377
|
+
queue.push(resolved.resolvedFileName);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
checkProject(registration) {
|
|
384
|
+
if (this.checkedProjects.has(registration.config.path))
|
|
385
|
+
return;
|
|
386
|
+
this.checkedProjects.add(registration.config.path);
|
|
387
|
+
const program = ts.createProgram({
|
|
388
|
+
rootNames: registration.config.parsed.fileNames,
|
|
389
|
+
options: {
|
|
390
|
+
...registration.config.parsed.options,
|
|
391
|
+
composite: false,
|
|
392
|
+
incremental: false,
|
|
393
|
+
noEmit: true,
|
|
394
|
+
// Source-plane workspace aliases resolve referenced packages to source.
|
|
395
|
+
// Widen only this diagnostic program's root so those imports do not
|
|
396
|
+
// produce an artificial TS6059 before Typert checks the public edge.
|
|
397
|
+
rootDir: this.options.root,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
const diagnostics = [
|
|
401
|
+
...program.getSyntacticDiagnostics(),
|
|
402
|
+
...program.getSemanticDiagnostics(),
|
|
403
|
+
].filter((diagnostic) => diagnostic.file !== undefined
|
|
404
|
+
&& diagnostic.start !== undefined
|
|
405
|
+
&& isWithin(diagnostic.file.fileName, registration.root));
|
|
406
|
+
if (diagnostics.length === 0)
|
|
407
|
+
return;
|
|
408
|
+
throw new TypertAnalysisError(diagnostics
|
|
409
|
+
.map(diagnostic => formatProgramDiagnostic(this.options.root, registration.face, diagnostic))
|
|
410
|
+
.join('\n'));
|
|
411
|
+
}
|
|
412
|
+
queueEdit(edit) {
|
|
413
|
+
this.queuedEdit = edit;
|
|
414
|
+
}
|
|
415
|
+
applyEdit(edit) {
|
|
416
|
+
const source = readFileSync(edit.file, 'utf8');
|
|
417
|
+
writeFileSync(edit.file, source.slice(0, edit.position) + edit.text + source.slice(edit.position));
|
|
418
|
+
this.caches.invalidate(edit.file);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
class FaceAnalyzer {
|
|
422
|
+
root;
|
|
423
|
+
face;
|
|
424
|
+
program;
|
|
425
|
+
checker;
|
|
426
|
+
registrations;
|
|
427
|
+
allRegistrations;
|
|
428
|
+
mode;
|
|
429
|
+
queueEdit;
|
|
430
|
+
crossFaceLinks;
|
|
431
|
+
sourceFiles = new Map();
|
|
432
|
+
declarations = new Map();
|
|
433
|
+
declarationStates = new Set();
|
|
434
|
+
nodes = new Map();
|
|
435
|
+
exportsByPackage = new Map();
|
|
436
|
+
nodeOrdinals = new Map();
|
|
437
|
+
staticLookups;
|
|
438
|
+
staticContexts;
|
|
439
|
+
constructor(options) {
|
|
440
|
+
this.root = options.root;
|
|
441
|
+
this.face = options.face;
|
|
442
|
+
this.program = options.program;
|
|
443
|
+
this.checker = options.program.getTypeChecker();
|
|
444
|
+
this.registrations = options.registrations;
|
|
445
|
+
this.allRegistrations = options.allRegistrations;
|
|
446
|
+
this.mode = options.mode;
|
|
447
|
+
this.queueEdit = options.queueEdit;
|
|
448
|
+
this.crossFaceLinks = options.crossFaceLinks;
|
|
449
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
450
|
+
this.sourceFiles.set(realPath(sourceFile.fileName), sourceFile);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
analyze() {
|
|
454
|
+
for (const registration of this.registrations) {
|
|
455
|
+
this.exportsByPackage.set(registration.name, this.collectExports(registration));
|
|
456
|
+
}
|
|
457
|
+
const packages = this.registrations
|
|
458
|
+
.map(registration => this.analyzePackage(registration))
|
|
459
|
+
.filter(hasPackageSurface);
|
|
460
|
+
this.validateInvocationIdentity(packages);
|
|
461
|
+
return {
|
|
462
|
+
face: this.face,
|
|
463
|
+
packages,
|
|
464
|
+
graph: {
|
|
465
|
+
declarations: [...this.declarations.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
466
|
+
nodes: [...this.nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
analyzePackage(registration) {
|
|
471
|
+
const records = this.exportsByPackage.get(registration.name);
|
|
472
|
+
const reachable = this.reachableFiles(registration, records.map(record => record.sourceFile));
|
|
473
|
+
const services = [];
|
|
474
|
+
const events = [];
|
|
475
|
+
for (const sourceFile of reachable) {
|
|
476
|
+
for (const statement of sourceFile.statements) {
|
|
477
|
+
if (!ts.isModuleDeclaration(statement)
|
|
478
|
+
|| !ts.isStringLiteral(statement.name)
|
|
479
|
+
|| statement.name.text !== '@deepseek-ai/cordis'
|
|
480
|
+
|| statement.body === undefined
|
|
481
|
+
|| !ts.isModuleBlock(statement.body))
|
|
482
|
+
continue;
|
|
483
|
+
for (const member of statement.body.statements) {
|
|
484
|
+
if (!ts.isInterfaceDeclaration(member))
|
|
485
|
+
continue;
|
|
486
|
+
if (member.name.text === 'Context') {
|
|
487
|
+
services.push(...this.collectServices(member, records));
|
|
488
|
+
}
|
|
489
|
+
else if (member.name.text === 'Events') {
|
|
490
|
+
events.push(...this.collectEvents(member));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const explicitServices = this.collectExplicitServices(records);
|
|
496
|
+
const objects = [];
|
|
497
|
+
const schemas = [];
|
|
498
|
+
const seenBusinessSymbols = new Set();
|
|
499
|
+
for (const record of records) {
|
|
500
|
+
const declaration = record.declaration;
|
|
501
|
+
if (!isTypeDeclaration(declaration))
|
|
502
|
+
continue;
|
|
503
|
+
if (this.registrationForFile(declaration.getSourceFile().fileName) === undefined)
|
|
504
|
+
continue;
|
|
505
|
+
const symbol = this.resolveSymbol(record.symbol);
|
|
506
|
+
const symbolId = this.symbolId(symbol);
|
|
507
|
+
if (seenBusinessSymbols.has(symbolId))
|
|
508
|
+
continue;
|
|
509
|
+
const mode = typertMode(declaration);
|
|
510
|
+
if (mode !== 'object' && mode !== 'schema')
|
|
511
|
+
continue;
|
|
512
|
+
seenBusinessSymbols.add(symbolId);
|
|
513
|
+
this.ensureDeclaration(symbol, declaration);
|
|
514
|
+
const documentation = documentationOf(declaration);
|
|
515
|
+
if (mode === 'object') {
|
|
516
|
+
objects.push({
|
|
517
|
+
...documentation,
|
|
518
|
+
export: record.model,
|
|
519
|
+
symbol: symbolId,
|
|
520
|
+
passing: 'reference',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
schemas.push({
|
|
525
|
+
...documentation,
|
|
526
|
+
export: record.model,
|
|
527
|
+
symbol: symbolId,
|
|
528
|
+
type: this.referenceNode(symbol, declaration),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
name: registration.name,
|
|
534
|
+
root: slash(relative(this.root, registration.root)),
|
|
535
|
+
exports: records.map(record => record.model)
|
|
536
|
+
.sort((left, right) => left.subpath.localeCompare(right.subpath) || left.name.localeCompare(right.name)),
|
|
537
|
+
services: uniqueBy([...explicitServices, ...services], service => service.key)
|
|
538
|
+
.sort((left, right) => left.key.localeCompare(right.key)),
|
|
539
|
+
events: uniqueBy(events, event => event.name).sort((left, right) => left.name.localeCompare(right.name)),
|
|
540
|
+
objects: objects.sort((left, right) => left.export.name.localeCompare(right.export.name)),
|
|
541
|
+
schemas: schemas.sort((left, right) => left.export.name.localeCompare(right.export.name)),
|
|
542
|
+
invocations: this.face === 'host'
|
|
543
|
+
? this.collectInvocations(registration, reachable).sort((left, right) => left.id.localeCompare(right.id))
|
|
544
|
+
: [],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
collectExports(registration) {
|
|
548
|
+
const targets = packageExportTargets(registration.manifest)
|
|
549
|
+
.filter(([subpath]) => registration.exportSubpaths === undefined
|
|
550
|
+
|| registration.exportSubpaths.includes(subpath));
|
|
551
|
+
const records = [];
|
|
552
|
+
for (const [subpath, target] of targets) {
|
|
553
|
+
if (target.includes('*') || subpath === './package.json'
|
|
554
|
+
|| subpath === './typert' || subpath === './client/typert' || subpath === './remote'
|
|
555
|
+
// Data exports (bundle patch lists, JSON manifests) carry no TypeScript API.
|
|
556
|
+
|| target.endsWith('.json') || target.endsWith('.yml') || target.endsWith('.yaml'))
|
|
557
|
+
continue;
|
|
558
|
+
const sourcePath = sourcePathForExport(registration.root, target);
|
|
559
|
+
const sourceFile = this.sourceFiles.get(realPath(sourcePath));
|
|
560
|
+
if (sourceFile === undefined) {
|
|
561
|
+
throw new TypertAnalysisError(`typert(${this.face}): ${registration.name} export ${subpath} resolves to missing source ${sourcePath}`);
|
|
562
|
+
}
|
|
563
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
|
|
564
|
+
if (moduleSymbol === undefined)
|
|
565
|
+
continue;
|
|
566
|
+
for (const exported of this.checker.getExportsOfModule(moduleSymbol)) {
|
|
567
|
+
const symbol = this.resolveSymbol(exported);
|
|
568
|
+
const declaration = preferredDeclaration(symbol);
|
|
569
|
+
const aliases = exported === symbol || exported.name === symbol.name
|
|
570
|
+
? [exported.name]
|
|
571
|
+
: [exported.name, symbol.name];
|
|
572
|
+
records.push({
|
|
573
|
+
model: {
|
|
574
|
+
subpath,
|
|
575
|
+
name: exported.name,
|
|
576
|
+
symbol: this.symbolId(symbol),
|
|
577
|
+
aliases,
|
|
578
|
+
},
|
|
579
|
+
symbol,
|
|
580
|
+
declaration,
|
|
581
|
+
sourceFile,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const unique = uniqueBy(records, record => `${record.model.subpath}\0${record.model.name}`);
|
|
586
|
+
this.collectCrossFaceReExports(registration, unique);
|
|
587
|
+
return unique;
|
|
588
|
+
}
|
|
589
|
+
collectCrossFaceReExports(registration, records) {
|
|
590
|
+
const publicSymbols = new Set(records.map(record => record.symbol));
|
|
591
|
+
const entryFiles = uniqueBy(records, record => record.sourceFile.fileName).map(record => record.sourceFile);
|
|
592
|
+
for (const sourceFile of this.reachableFiles(registration, entryFiles)) {
|
|
593
|
+
for (const statement of sourceFile.statements) {
|
|
594
|
+
if (!ts.isExportDeclaration(statement)
|
|
595
|
+
|| statement.moduleSpecifier === undefined
|
|
596
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier))
|
|
597
|
+
continue;
|
|
598
|
+
const module = moduleIdentity(statement.moduleSpecifier.text);
|
|
599
|
+
if (module === undefined)
|
|
600
|
+
continue;
|
|
601
|
+
const toFace = this.allRegistrations
|
|
602
|
+
.find(candidate => candidate.name === module.package && candidate.face !== this.face)?.face;
|
|
603
|
+
if (toFace === undefined)
|
|
604
|
+
continue;
|
|
605
|
+
if (statement.exportClause !== undefined && ts.isNamespaceExport(statement.exportClause)) {
|
|
606
|
+
const namespace = this.resolveSymbol(this.checker.getSymbolAtLocation(statement.exportClause.name));
|
|
607
|
+
if (publicSymbols.has(namespace)) {
|
|
608
|
+
this.fail(statement.exportClause, 'cross-face namespace re-exports are not supported');
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const exports = statement.exportClause === undefined
|
|
613
|
+
? this.moduleExports(statement.moduleSpecifier)
|
|
614
|
+
.map(symbol => ({ symbol: this.resolveSymbol(symbol), requestedName: symbol.name, site: statement }))
|
|
615
|
+
: statement.exportClause.elements.map(element => ({
|
|
616
|
+
symbol: this.resolveSymbol(this.checker.getSymbolAtLocation(element.name)),
|
|
617
|
+
requestedName: element.propertyName?.text ?? element.name.text,
|
|
618
|
+
site: element,
|
|
619
|
+
}));
|
|
620
|
+
for (const exported of exports) {
|
|
621
|
+
if (!publicSymbols.has(exported.symbol))
|
|
622
|
+
continue;
|
|
623
|
+
const name = this.packageExportName(module, exported.symbol, toFace, exported.requestedName);
|
|
624
|
+
if (name === undefined) {
|
|
625
|
+
this.fail(exported.site, `cross-face re-export ${exported.requestedName} is not exported by ${module.package} at ${module.subpath}`);
|
|
626
|
+
}
|
|
627
|
+
this.recordCrossFaceLink(registration.name, toFace, module, name);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
moduleExports(moduleSpecifier) {
|
|
633
|
+
/* v8 ignore next -- a semantically valid export declaration from a resolved module always has a module symbol. */
|
|
634
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(moduleSpecifier);
|
|
635
|
+
return this.checker.getExportsOfModule(moduleSymbol);
|
|
636
|
+
}
|
|
637
|
+
reachableFiles(registration, entryFiles) {
|
|
638
|
+
const reachable = new Map();
|
|
639
|
+
const queue = [...entryFiles];
|
|
640
|
+
while (queue.length > 0) {
|
|
641
|
+
const sourceFile = queue.shift();
|
|
642
|
+
const fileName = realPath(sourceFile.fileName);
|
|
643
|
+
if (reachable.has(fileName) || !isWithin(fileName, registration.root))
|
|
644
|
+
continue;
|
|
645
|
+
reachable.set(fileName, sourceFile);
|
|
646
|
+
for (const statement of sourceFile.statements) {
|
|
647
|
+
if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
|
|
648
|
+
|| statement.moduleSpecifier === undefined
|
|
649
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier))
|
|
650
|
+
continue;
|
|
651
|
+
const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, sourceFile.fileName, this.program.getCompilerOptions(), ts.sys).resolvedModule;
|
|
652
|
+
if (resolved === undefined)
|
|
653
|
+
continue;
|
|
654
|
+
const resolvedPath = realPath(resolved.resolvedFileName);
|
|
655
|
+
if (!isWithin(resolvedPath, registration.root))
|
|
656
|
+
continue;
|
|
657
|
+
queue.push(this.sourceFiles.get(resolvedPath));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return [...reachable.values()].sort((left, right) => left.fileName.localeCompare(right.fileName));
|
|
661
|
+
}
|
|
662
|
+
collectServices(context, records) {
|
|
663
|
+
const bySymbol = new Map();
|
|
664
|
+
for (const record of records) {
|
|
665
|
+
const id = this.symbolId(record.symbol);
|
|
666
|
+
const matches = bySymbol.get(id) ?? [];
|
|
667
|
+
matches.push(record);
|
|
668
|
+
bySymbol.set(id, matches);
|
|
669
|
+
}
|
|
670
|
+
const result = [];
|
|
671
|
+
for (const member of context.members) {
|
|
672
|
+
if (!ts.isPropertySignature(member) || member.type === undefined)
|
|
673
|
+
continue;
|
|
674
|
+
const symbol = this.symbolAtType(member.type);
|
|
675
|
+
if (symbol === undefined)
|
|
676
|
+
continue;
|
|
677
|
+
const symbolId = this.symbolId(symbol);
|
|
678
|
+
const exported = bySymbol.get(symbolId)?.find(record => record.model.name === symbol.name)
|
|
679
|
+
?? bySymbol.get(symbolId)?.find(record => record.model.name !== 'default')
|
|
680
|
+
?? bySymbol.get(symbolId)?.[0];
|
|
681
|
+
if (exported === undefined)
|
|
682
|
+
continue;
|
|
683
|
+
const declaration = preferredDeclaration(symbol);
|
|
684
|
+
if (declaration === undefined || (!ts.isClassDeclaration(declaration) && !ts.isInterfaceDeclaration(declaration))) {
|
|
685
|
+
this.fail(member, `service ${memberName(member.name)} does not resolve to an exported class or interface`);
|
|
686
|
+
}
|
|
687
|
+
const model = this.ensureDeclaration(symbol, declaration);
|
|
688
|
+
const exposed = model.members
|
|
689
|
+
.filter(exposableMember)
|
|
690
|
+
.map(publicMember => publicMember.id);
|
|
691
|
+
result.push({
|
|
692
|
+
...documentationOf(declaration),
|
|
693
|
+
key: memberName(member.name),
|
|
694
|
+
symbol: symbolId,
|
|
695
|
+
export: exported.model,
|
|
696
|
+
members: exposed,
|
|
697
|
+
location: this.location(member),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
collectExplicitServices(records) {
|
|
703
|
+
const result = [];
|
|
704
|
+
const seen = new Set();
|
|
705
|
+
for (const record of records) {
|
|
706
|
+
const tag = typertServiceTag(record.declaration);
|
|
707
|
+
if (tag === undefined)
|
|
708
|
+
continue;
|
|
709
|
+
const words = (ts.getTextOfJSDocComment(tag.comment) ?? '').trim().split(/\s+/);
|
|
710
|
+
if (words.length !== 2 || !isRemoteSegment(words[1] ?? '')) {
|
|
711
|
+
this.fail(tag, '@typert service requires exactly one nonempty Cordis service key without "/"');
|
|
712
|
+
}
|
|
713
|
+
if (!ts.isClassDeclaration(record.declaration)) {
|
|
714
|
+
this.fail(record.declaration, '@typert service requires an exported class');
|
|
715
|
+
}
|
|
716
|
+
const symbol = this.resolveSymbol(record.symbol);
|
|
717
|
+
const symbolId = this.symbolId(symbol);
|
|
718
|
+
if (seen.has(symbolId))
|
|
719
|
+
continue;
|
|
720
|
+
seen.add(symbolId);
|
|
721
|
+
const model = this.ensureDeclaration(symbol, record.declaration);
|
|
722
|
+
result.push({
|
|
723
|
+
...documentationOf(record.declaration),
|
|
724
|
+
key: words[1],
|
|
725
|
+
symbol: symbolId,
|
|
726
|
+
export: record.model,
|
|
727
|
+
members: model.members.filter(exposableMember).map(member => member.id),
|
|
728
|
+
location: this.location(record.declaration),
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
collectInvocations(registration, reachable) {
|
|
734
|
+
const result = [];
|
|
735
|
+
for (const sourceFile of reachable) {
|
|
736
|
+
for (const statement of sourceFile.statements) {
|
|
737
|
+
if (!ts.isClassDeclaration(statement))
|
|
738
|
+
continue;
|
|
739
|
+
const marked = statement.members.flatMap((member) => {
|
|
740
|
+
const invocation = this.remoteMarker(member);
|
|
741
|
+
if (invocation === undefined)
|
|
742
|
+
return [];
|
|
743
|
+
if (!ts.isMethodDeclaration(member)) {
|
|
744
|
+
this.fail(member, 'Remote decorators require a public instance method');
|
|
745
|
+
}
|
|
746
|
+
return [{ method: member, invocation }];
|
|
747
|
+
});
|
|
748
|
+
const first = marked[0];
|
|
749
|
+
if (first === undefined)
|
|
750
|
+
continue;
|
|
751
|
+
const binding = this.gatewayBinding(statement);
|
|
752
|
+
if (binding === undefined) {
|
|
753
|
+
this.fail(first.method, 'Remote methods require GatewayService or readonly typertGateway = bindTypeRTGateway(this, serviceKey)');
|
|
754
|
+
}
|
|
755
|
+
for (const { method, invocation } of marked) {
|
|
756
|
+
result.push(this.invocationModel(registration, binding, method, invocation));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
invocationModel(registration, binding, method, invocation) {
|
|
763
|
+
if (visibilityOf(method) !== 'public' || hasModifier(method, ts.SyntaxKind.StaticKeyword)) {
|
|
764
|
+
this.fail(method, 'Remote decorators require a public instance method');
|
|
765
|
+
}
|
|
766
|
+
if (hasModifier(method, ts.SyntaxKind.AbstractKeyword) || method.body === undefined) {
|
|
767
|
+
this.fail(method, 'Remote methods must have a concrete implementation');
|
|
768
|
+
}
|
|
769
|
+
if (!ts.isIdentifier(method.name)) {
|
|
770
|
+
this.fail(method, 'Remote method names must be identifiers');
|
|
771
|
+
}
|
|
772
|
+
if ((method.typeParameters?.length ?? 0) > 0) {
|
|
773
|
+
this.fail(method, 'generic Remote methods are not supported');
|
|
774
|
+
}
|
|
775
|
+
const methodName = method.name.text;
|
|
776
|
+
const exportedMethod = invocation.exportName ?? methodName;
|
|
777
|
+
const lookups = this.lookupDeclarations();
|
|
778
|
+
const lookupByHost = new Map(lookups.map(lookup => [lookup.hostSymbol, lookup]));
|
|
779
|
+
const parameters = [];
|
|
780
|
+
let cancellation;
|
|
781
|
+
const wires = new Set();
|
|
782
|
+
for (const [parameterIndex, parameter] of method.parameters.entries()) {
|
|
783
|
+
if (!ts.isIdentifier(parameter.name)) {
|
|
784
|
+
this.fail(parameter, 'Remote parameters must use identifier bindings');
|
|
785
|
+
}
|
|
786
|
+
if (parameter.dotDotDotToken !== undefined)
|
|
787
|
+
this.fail(parameter, 'Remote parameters cannot be rest parameters');
|
|
788
|
+
if (parameter.initializer !== undefined)
|
|
789
|
+
this.fail(parameter, 'Remote parameters cannot have default values');
|
|
790
|
+
if (parameter.questionToken !== undefined)
|
|
791
|
+
this.fail(parameter, 'Remote parameters cannot be optional');
|
|
792
|
+
if (parameter.name.text === 'this')
|
|
793
|
+
this.fail(parameter, 'Remote methods cannot declare an explicit this parameter');
|
|
794
|
+
const authoredType = this.requiredType(parameter, parameter.type, 'parameter');
|
|
795
|
+
const cancellationName = parameter.name.text === 'signal';
|
|
796
|
+
const cancellationType = this.isGlobalAbortSignal(authoredType);
|
|
797
|
+
if (cancellationName || cancellationType) {
|
|
798
|
+
if (!cancellationName || !cancellationType) {
|
|
799
|
+
this.fail(parameter, 'Remote cancellation must use a parameter named signal with the global AbortSignal type');
|
|
800
|
+
}
|
|
801
|
+
if (parameterIndex !== method.parameters.length - 1) {
|
|
802
|
+
this.fail(parameter, 'Remote cancellation signal must be the final parameter');
|
|
803
|
+
}
|
|
804
|
+
cancellation = { parameter: 'signal' };
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const hostSymbol = this.symbolAtType(authoredType);
|
|
808
|
+
const lookup = hostSymbol === undefined ? undefined : lookupByHost.get(this.symbolId(hostSymbol));
|
|
809
|
+
let modeled;
|
|
810
|
+
if (lookup !== undefined) {
|
|
811
|
+
if (parameter.name.text !== lookup.key) {
|
|
812
|
+
this.fail(parameter, `lookup parameter for ${lookup.key} must also be named ${lookup.key}`);
|
|
813
|
+
}
|
|
814
|
+
const boundary = this.remoteBoundary(lookup.wireType, `${registration.name}#${binding.namespace}/${exportedMethod}:${lookup.key}Id`, true);
|
|
815
|
+
modeled = {
|
|
816
|
+
name: parameter.name.text,
|
|
817
|
+
wire: `${lookup.key}Id`,
|
|
818
|
+
source: 'lookup',
|
|
819
|
+
lookup: lookup.key,
|
|
820
|
+
boundary,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
if (hostSymbol !== undefined && this.isWorkspaceClass(hostSymbol)) {
|
|
825
|
+
this.fail(parameter, `non-JSON class parameter ${hostSymbol.name} requires a TypeRTLookupMap entry`);
|
|
826
|
+
}
|
|
827
|
+
modeled = {
|
|
828
|
+
name: parameter.name.text,
|
|
829
|
+
wire: parameter.name.text,
|
|
830
|
+
source: 'json',
|
|
831
|
+
boundary: this.remoteBoundary(authoredType, `${registration.name}#${binding.namespace}/${exportedMethod}:${parameter.name.text}`, false),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (wires.has(modeled.wire))
|
|
835
|
+
this.fail(parameter, `duplicate Remote wire field ${modeled.wire}`);
|
|
836
|
+
wires.add(modeled.wire);
|
|
837
|
+
parameters.push(modeled);
|
|
838
|
+
}
|
|
839
|
+
let receiver = { kind: 'direct' };
|
|
840
|
+
if (invocation.kind === 'context') {
|
|
841
|
+
const context = this.contextDeclarations().get(invocation.context);
|
|
842
|
+
if (context === undefined) {
|
|
843
|
+
this.fail(method, `Remote Scope ${invocation.context} has no TypeRTContextMap entry`);
|
|
844
|
+
}
|
|
845
|
+
const wire = `${invocation.context}Id`;
|
|
846
|
+
if (wires.has(wire))
|
|
847
|
+
this.fail(method, `Remote Scope wire field ${wire} conflicts with a method parameter`);
|
|
848
|
+
receiver = {
|
|
849
|
+
kind: 'context',
|
|
850
|
+
context: invocation.context,
|
|
851
|
+
wire,
|
|
852
|
+
boundary: this.remoteBoundary(context.wireType, `${registration.name}#${binding.namespace}/${exportedMethod}:${wire}`, true),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
let scope;
|
|
856
|
+
if (invocation.kind === 'direct') {
|
|
857
|
+
const lookupParameters = parameters.filter(parameter => parameter.source === 'lookup');
|
|
858
|
+
const parameter = lookupParameters.length === 1 ? lookupParameters[0] : undefined;
|
|
859
|
+
const context = parameter?.lookup === undefined
|
|
860
|
+
? undefined
|
|
861
|
+
: this.contextDeclarations().get(parameter.lookup);
|
|
862
|
+
if (parameter !== undefined && context !== undefined) {
|
|
863
|
+
const contextBoundary = this.remoteBoundary(context.wireType, `${registration.name}#${binding.namespace}/${exportedMethod}:scope:${context.key}`, true);
|
|
864
|
+
if (contextBoundary.typeSymbol !== parameter.boundary.typeSymbol) {
|
|
865
|
+
this.fail(method, `Remote scope ${context.key} wire type ${contextBoundary.typeSymbol} does not match lookup wire type ${parameter.boundary.typeSymbol}`);
|
|
866
|
+
}
|
|
867
|
+
scope = { context: context.key, wire: parameter.wire };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const resultType = this.remoteResultType(method);
|
|
871
|
+
return {
|
|
872
|
+
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
873
|
+
service: binding.service,
|
|
874
|
+
namespace: binding.namespace,
|
|
875
|
+
method: exportedMethod,
|
|
876
|
+
...(exportedMethod === methodName ? {} : { implementation: methodName }),
|
|
877
|
+
invocation: receiver,
|
|
878
|
+
...(scope === undefined ? {} : { scope }),
|
|
879
|
+
parameters,
|
|
880
|
+
...(cancellation === undefined ? {} : { cancellation }),
|
|
881
|
+
result: this.remoteBoundary(resultType, `${registration.name}#${binding.namespace}/${exportedMethod}:result`, false),
|
|
882
|
+
location: this.location(method.name),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
gatewayBinding(declaration) {
|
|
886
|
+
const field = this.gatewayFieldBinding(declaration);
|
|
887
|
+
const base = this.gatewayServiceBinding(declaration);
|
|
888
|
+
if (field !== undefined && base !== undefined) {
|
|
889
|
+
this.fail(field.site, 'GatewayService subclasses must not declare a second typertGateway binding');
|
|
890
|
+
}
|
|
891
|
+
return field ?? base;
|
|
892
|
+
}
|
|
893
|
+
gatewayFieldBinding(declaration) {
|
|
894
|
+
const candidates = declaration.members.filter((member) => ts.isPropertyDeclaration(member) && memberName(member.name) === 'typertGateway');
|
|
895
|
+
const [property, duplicate] = candidates;
|
|
896
|
+
if (property === undefined)
|
|
897
|
+
return undefined;
|
|
898
|
+
if (duplicate !== undefined)
|
|
899
|
+
this.fail(duplicate, 'Service has more than one typertGateway field');
|
|
900
|
+
if (visibilityOf(property) !== 'public'
|
|
901
|
+
|| hasModifier(property, ts.SyntaxKind.StaticKeyword)
|
|
902
|
+
|| !hasModifier(property, ts.SyntaxKind.ReadonlyKeyword)) {
|
|
903
|
+
this.fail(property, 'typertGateway must be a public readonly instance field');
|
|
904
|
+
}
|
|
905
|
+
if (property.initializer === undefined
|
|
906
|
+
|| !ts.isCallExpression(property.initializer)
|
|
907
|
+
|| !this.isTypeMetaSymbol(property.initializer.expression, 'bindTypeRTGateway')) {
|
|
908
|
+
this.fail(property, 'typertGateway must call bindTypeRTGateway()');
|
|
909
|
+
}
|
|
910
|
+
const call = property.initializer;
|
|
911
|
+
if (call.arguments.length < 2 || call.arguments.length > 3) {
|
|
912
|
+
this.fail(call, 'bindTypeRTGateway() requires this, service key, and an optional options object');
|
|
913
|
+
}
|
|
914
|
+
if (call.arguments[0]?.kind !== ts.SyntaxKind.ThisKeyword) {
|
|
915
|
+
this.fail(call.arguments[0] ?? call, 'bindTypeRTGateway() first argument must be this');
|
|
916
|
+
}
|
|
917
|
+
return this.gatewayBindingArguments(call, property);
|
|
918
|
+
}
|
|
919
|
+
gatewayServiceBinding(declaration) {
|
|
920
|
+
const heritage = (declaration.heritageClauses ?? [])
|
|
921
|
+
.filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword)
|
|
922
|
+
.flatMap(clause => [...clause.types])
|
|
923
|
+
.find(type => this.isTypeMetaSymbol(type.expression, 'GatewayService'));
|
|
924
|
+
if (heritage === undefined)
|
|
925
|
+
return undefined;
|
|
926
|
+
const constructor = declaration.members.find(ts.isConstructorDeclaration);
|
|
927
|
+
if (constructor?.body === undefined) {
|
|
928
|
+
this.fail(heritage, 'GatewayService subclasses must declare a constructor with super(ctx, serviceKey)');
|
|
929
|
+
}
|
|
930
|
+
const call = constructor.body.statements.flatMap((statement) => {
|
|
931
|
+
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression))
|
|
932
|
+
return [];
|
|
933
|
+
return statement.expression.expression.kind === ts.SyntaxKind.SuperKeyword ? [statement.expression] : [];
|
|
934
|
+
})[0];
|
|
935
|
+
if (call === undefined) {
|
|
936
|
+
this.fail(constructor, 'GatewayService constructor must call super(ctx, serviceKey) directly');
|
|
937
|
+
}
|
|
938
|
+
if (call.arguments.length < 2 || call.arguments.length > 3) {
|
|
939
|
+
this.fail(call, 'GatewayService super() requires context, service key, and an optional options object');
|
|
940
|
+
}
|
|
941
|
+
return this.gatewayBindingArguments(call, heritage);
|
|
942
|
+
}
|
|
943
|
+
gatewayBindingArguments(call, site) {
|
|
944
|
+
const serviceArgument = call.arguments[1];
|
|
945
|
+
if (serviceArgument === undefined)
|
|
946
|
+
this.fail(call, 'Gateway service key must be a string literal');
|
|
947
|
+
const service = stringLiteralValue(serviceArgument);
|
|
948
|
+
if (service === undefined)
|
|
949
|
+
this.fail(serviceArgument, 'Gateway service key must be a string literal');
|
|
950
|
+
let namespace = service;
|
|
951
|
+
const options = call.arguments[2];
|
|
952
|
+
if (options !== undefined) {
|
|
953
|
+
if (!ts.isObjectLiteralExpression(options)) {
|
|
954
|
+
this.fail(options, 'bindTypeRTGateway() options must be an object literal');
|
|
955
|
+
}
|
|
956
|
+
for (const propertyOption of options.properties) {
|
|
957
|
+
if (!ts.isPropertyAssignment(propertyOption)
|
|
958
|
+
|| memberName(propertyOption.name) !== 'namespace') {
|
|
959
|
+
this.fail(propertyOption, 'bindTypeRTGateway() only supports a namespace option');
|
|
960
|
+
}
|
|
961
|
+
const value = stringLiteralValue(propertyOption.initializer);
|
|
962
|
+
if (value === undefined)
|
|
963
|
+
this.fail(propertyOption.initializer, 'Gateway namespace must be a string literal');
|
|
964
|
+
namespace = value;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (!isRemoteSegment(service))
|
|
968
|
+
this.fail(serviceArgument, 'Gateway service key must contain only RPC endpoint segment characters');
|
|
969
|
+
if (!isRemoteSegment(namespace))
|
|
970
|
+
this.fail(options ?? call, 'Gateway namespace must contain only RPC endpoint segment characters');
|
|
971
|
+
return { service, namespace, site };
|
|
972
|
+
}
|
|
973
|
+
remoteMarker(member) {
|
|
974
|
+
let found;
|
|
975
|
+
for (const decorator of ts.canHaveDecorators(member) ? ts.getDecorators(member) ?? [] : []) {
|
|
976
|
+
const expression = decorator.expression;
|
|
977
|
+
let marker;
|
|
978
|
+
if (this.isTypeMetaSymbol(expression, 'Remote')) {
|
|
979
|
+
marker = { kind: 'direct' };
|
|
980
|
+
}
|
|
981
|
+
else if (ts.isCallExpression(expression)
|
|
982
|
+
&& this.isTypeMetaSymbol(expression.expression, 'Remote')) {
|
|
983
|
+
if (expression.arguments.length !== 1)
|
|
984
|
+
this.fail(expression, 'Remote() requires one exported method name');
|
|
985
|
+
const exportName = stringLiteralValue(expression.arguments[0]);
|
|
986
|
+
if (exportName === undefined || !isRemoteSegment(exportName)) {
|
|
987
|
+
this.fail(expression.arguments[0] ?? expression, 'Remote() name must be a string literal containing only RPC endpoint segment characters');
|
|
988
|
+
}
|
|
989
|
+
marker = { kind: 'direct', exportName };
|
|
990
|
+
}
|
|
991
|
+
else if (ts.isCallExpression(expression)
|
|
992
|
+
&& this.isTypeMetaSymbol(expression.expression, 'RemoteScope')) {
|
|
993
|
+
if (expression.arguments.length < 1 || expression.arguments.length > 2) {
|
|
994
|
+
this.fail(expression, 'RemoteScope() requires a Context key and optional exported method name');
|
|
995
|
+
}
|
|
996
|
+
const context = stringLiteralValue(expression.arguments[0]);
|
|
997
|
+
if (context === undefined || !isRemoteSegment(context)) {
|
|
998
|
+
this.fail(expression.arguments[0] ?? expression, 'RemoteScope() key must be a string literal containing only RPC endpoint segment characters');
|
|
999
|
+
}
|
|
1000
|
+
const exportArgument = expression.arguments[1];
|
|
1001
|
+
const exportName = exportArgument === undefined ? undefined : stringLiteralValue(exportArgument);
|
|
1002
|
+
if (exportArgument !== undefined && (exportName === undefined || !isRemoteSegment(exportName))) {
|
|
1003
|
+
this.fail(exportArgument, 'RemoteScope() name must be a string literal containing only RPC endpoint segment characters');
|
|
1004
|
+
}
|
|
1005
|
+
marker = { kind: 'context', context, ...exportName === undefined ? {} : { exportName } };
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (found !== undefined)
|
|
1011
|
+
this.fail(decorator, 'a method can have only one Remote invocation decorator');
|
|
1012
|
+
found = marker;
|
|
1013
|
+
}
|
|
1014
|
+
return found;
|
|
1015
|
+
}
|
|
1016
|
+
remoteResultType(method) {
|
|
1017
|
+
const authored = this.requiredType(method, method.type, 'return');
|
|
1018
|
+
if (!ts.isTypeReferenceNode(authored))
|
|
1019
|
+
return authored;
|
|
1020
|
+
const symbol = this.checker.getSymbolAtLocation(authored.typeName);
|
|
1021
|
+
const resolved = symbol === undefined ? undefined : this.resolveSymbol(symbol);
|
|
1022
|
+
const resultType = authored.typeArguments?.[0];
|
|
1023
|
+
if (resolved?.name !== 'Promise' || resultType === undefined || authored.typeArguments?.length !== 1)
|
|
1024
|
+
return authored;
|
|
1025
|
+
const declaration = preferredDeclaration(resolved);
|
|
1026
|
+
if (declaration === undefined || !isStandardLibraryFile(declaration.getSourceFile().fileName))
|
|
1027
|
+
return authored;
|
|
1028
|
+
return resultType;
|
|
1029
|
+
}
|
|
1030
|
+
isGlobalAbortSignal(type) {
|
|
1031
|
+
const symbol = this.symbolAtType(type);
|
|
1032
|
+
if (symbol?.name !== 'AbortSignal')
|
|
1033
|
+
return false;
|
|
1034
|
+
return symbol.declarations?.some(declaration => isStandardLibraryFile(declaration.getSourceFile().fileName)) === true;
|
|
1035
|
+
}
|
|
1036
|
+
lookupDeclarations() {
|
|
1037
|
+
if (this.staticLookups !== undefined)
|
|
1038
|
+
return this.staticLookups;
|
|
1039
|
+
const byKey = new Map();
|
|
1040
|
+
const byHost = new Map();
|
|
1041
|
+
for (const declaration of this.typeMetaMapMembers('TypeRTLookupMap')) {
|
|
1042
|
+
if (!ts.isPropertySignature(declaration) || declaration.type === undefined) {
|
|
1043
|
+
this.fail(declaration, 'TypeRTLookupMap entries must be required properties');
|
|
1044
|
+
}
|
|
1045
|
+
const key = memberName(declaration.name);
|
|
1046
|
+
if (!isRemoteSegment(key))
|
|
1047
|
+
this.fail(declaration.name, 'TypeRTLookupMap key must contain only RPC endpoint segment characters');
|
|
1048
|
+
if (!ts.isTypeReferenceNode(declaration.type)
|
|
1049
|
+
|| !this.isTypeMetaSymbol(declaration.type.typeName, 'TypeRTLookup')
|
|
1050
|
+
|| declaration.type.typeArguments?.length !== 2) {
|
|
1051
|
+
this.fail(declaration.type, 'TypeRTLookupMap values must be TypeRTLookup<Host, Wire>');
|
|
1052
|
+
}
|
|
1053
|
+
const hostType = declaration.type.typeArguments[0];
|
|
1054
|
+
const wireType = declaration.type.typeArguments[1];
|
|
1055
|
+
if (hostType === undefined || wireType === undefined) {
|
|
1056
|
+
this.fail(declaration.type, 'TypeRTLookupMap values must be TypeRTLookup<Host, Wire>');
|
|
1057
|
+
}
|
|
1058
|
+
const host = this.symbolAtType(hostType);
|
|
1059
|
+
if (host === undefined)
|
|
1060
|
+
this.fail(hostType, 'TypeRTLookup Host must be a named type');
|
|
1061
|
+
const entry = {
|
|
1062
|
+
key,
|
|
1063
|
+
hostSymbol: this.symbolId(host),
|
|
1064
|
+
wireType,
|
|
1065
|
+
site: declaration,
|
|
1066
|
+
};
|
|
1067
|
+
if (byKey.has(key))
|
|
1068
|
+
this.fail(declaration, `duplicate TypeRTLookupMap key ${key}`);
|
|
1069
|
+
if (byHost.has(entry.hostSymbol))
|
|
1070
|
+
this.fail(declaration, `Host type ${host.name} has more than one TypeRT lookup`);
|
|
1071
|
+
byKey.set(key, entry);
|
|
1072
|
+
byHost.set(entry.hostSymbol, entry);
|
|
1073
|
+
}
|
|
1074
|
+
this.staticLookups = [...byKey.values()];
|
|
1075
|
+
return this.staticLookups;
|
|
1076
|
+
}
|
|
1077
|
+
contextDeclarations() {
|
|
1078
|
+
if (this.staticContexts !== undefined)
|
|
1079
|
+
return this.staticContexts;
|
|
1080
|
+
const result = new Map();
|
|
1081
|
+
for (const declaration of this.typeMetaMapMembers('TypeRTContextMap')) {
|
|
1082
|
+
if (!ts.isPropertySignature(declaration) || declaration.type === undefined) {
|
|
1083
|
+
this.fail(declaration, 'TypeRTContextMap entries must be required properties');
|
|
1084
|
+
}
|
|
1085
|
+
const key = memberName(declaration.name);
|
|
1086
|
+
if (!isRemoteSegment(key))
|
|
1087
|
+
this.fail(declaration.name, 'TypeRTContextMap key must contain only RPC endpoint segment characters');
|
|
1088
|
+
if (!ts.isTypeReferenceNode(declaration.type)
|
|
1089
|
+
|| !this.isTypeMetaSymbol(declaration.type.typeName, 'TypeRTContext')
|
|
1090
|
+
|| declaration.type.typeArguments?.length !== 1) {
|
|
1091
|
+
this.fail(declaration.type, 'TypeRTContextMap values must be TypeRTContext<Wire>');
|
|
1092
|
+
}
|
|
1093
|
+
if (result.has(key))
|
|
1094
|
+
this.fail(declaration, `duplicate TypeRTContextMap key ${key}`);
|
|
1095
|
+
const wireType = declaration.type.typeArguments[0];
|
|
1096
|
+
if (wireType === undefined)
|
|
1097
|
+
this.fail(declaration.type, 'TypeRTContextMap values must be TypeRTContext<Wire>');
|
|
1098
|
+
result.set(key, {
|
|
1099
|
+
key,
|
|
1100
|
+
wireType,
|
|
1101
|
+
site: declaration,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
this.staticContexts = result;
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
1107
|
+
typeMetaMapMembers(name) {
|
|
1108
|
+
const result = [];
|
|
1109
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
1110
|
+
for (const statement of sourceFile.statements) {
|
|
1111
|
+
if (!ts.isModuleDeclaration(statement)
|
|
1112
|
+
|| !ts.isStringLiteral(statement.name)
|
|
1113
|
+
|| statement.name.text !== '@deepseek-ai/dsh-type-meta'
|
|
1114
|
+
|| statement.body === undefined
|
|
1115
|
+
|| !ts.isModuleBlock(statement.body))
|
|
1116
|
+
continue;
|
|
1117
|
+
for (const nested of statement.body.statements) {
|
|
1118
|
+
if (ts.isInterfaceDeclaration(nested) && nested.name.text === name)
|
|
1119
|
+
result.push(...nested.members);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
remoteBoundary(authoredType, fallbackTypeSymbol, requireNamed) {
|
|
1126
|
+
const type = this.convertType(authoredType);
|
|
1127
|
+
const codecType = this.resolvedRemoteCodecType(authoredType);
|
|
1128
|
+
const rootSymbol = this.namedWorkspaceType(authoredType);
|
|
1129
|
+
const imports = new Map();
|
|
1130
|
+
const visit = (node) => {
|
|
1131
|
+
if ((ts.isTypeReferenceNode(node) || ts.isImportTypeNode(node))) {
|
|
1132
|
+
const symbol = ts.isTypeReferenceNode(node)
|
|
1133
|
+
? this.checker.getSymbolAtLocation(node.typeName)
|
|
1134
|
+
: node.qualifier === undefined ? undefined : this.checker.getSymbolAtLocation(node.qualifier);
|
|
1135
|
+
if (symbol !== undefined) {
|
|
1136
|
+
const resolved = this.resolveSymbol(symbol);
|
|
1137
|
+
const declaration = preferredDeclaration(resolved);
|
|
1138
|
+
if (declaration !== undefined
|
|
1139
|
+
&& !isStandardLibraryFile(declaration.getSourceFile().fileName)
|
|
1140
|
+
&& this.registrationForFile(declaration.getSourceFile().fileName) !== undefined) {
|
|
1141
|
+
const imported = this.publicRemoteType(resolved, node);
|
|
1142
|
+
imports.set(imported.symbol, imported);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
ts.forEachChild(node, visit);
|
|
1147
|
+
};
|
|
1148
|
+
visit(authoredType);
|
|
1149
|
+
if (rootSymbol !== undefined) {
|
|
1150
|
+
const imported = this.publicRemoteType(rootSymbol, authoredType);
|
|
1151
|
+
return {
|
|
1152
|
+
type,
|
|
1153
|
+
codecType,
|
|
1154
|
+
typeSymbol: `${imported.specifier}#${imported.name}`,
|
|
1155
|
+
imports: [...imports.values()].sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name)),
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
if (requireNamed)
|
|
1159
|
+
this.fail(authoredType, 'lookup and Context wire types must be named public types');
|
|
1160
|
+
return {
|
|
1161
|
+
type,
|
|
1162
|
+
codecType,
|
|
1163
|
+
typeSymbol: fallbackTypeSymbol,
|
|
1164
|
+
imports: [...imports.values()].sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name)),
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Project one authored Remote boundary through the complete face Program.
|
|
1169
|
+
* Consumer declarations retain the authored alias, while codecs use this
|
|
1170
|
+
* concrete graph so declaration-merged mapped and conditional types are
|
|
1171
|
+
* validated without teaching the compiler-independent emitter TypeScript's
|
|
1172
|
+
* type evaluator.
|
|
1173
|
+
*/
|
|
1174
|
+
resolvedRemoteCodecType(authoredType) {
|
|
1175
|
+
const resolvedType = this.checker.getTypeFromTypeNode(authoredType);
|
|
1176
|
+
this.assertRemoteJsonType(resolvedType, authoredType, new Set(), false);
|
|
1177
|
+
const completed = new Map();
|
|
1178
|
+
const active = new Map();
|
|
1179
|
+
const recursiveDeclarations = new Map();
|
|
1180
|
+
const convert = (type) => {
|
|
1181
|
+
const cached = completed.get(type);
|
|
1182
|
+
if (cached !== undefined)
|
|
1183
|
+
return cached;
|
|
1184
|
+
const activeId = active.get(type);
|
|
1185
|
+
if (activeId !== undefined) {
|
|
1186
|
+
if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
|
|
1187
|
+
const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
1188
|
+
const elementId = element === undefined ? undefined : active.get(element);
|
|
1189
|
+
if (element !== undefined && elementId !== undefined) {
|
|
1190
|
+
return this.addNode(authoredType, {
|
|
1191
|
+
kind: 'array',
|
|
1192
|
+
element: this.resolvedCycleReference(element, authoredType, elementId, recursiveDeclarations),
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return this.resolvedCycleReference(type, authoredType, activeId, recursiveDeclarations);
|
|
1197
|
+
}
|
|
1198
|
+
const id = this.allocateNodeId(authoredType);
|
|
1199
|
+
active.set(type, id);
|
|
1200
|
+
try {
|
|
1201
|
+
const add = (model) => {
|
|
1202
|
+
this.nodes.set(id, { id, ...model });
|
|
1203
|
+
completed.set(type, id);
|
|
1204
|
+
return id;
|
|
1205
|
+
};
|
|
1206
|
+
const flags = type.flags;
|
|
1207
|
+
if ((flags & ts.TypeFlags.Any) !== 0)
|
|
1208
|
+
return add({ kind: 'keyword', name: 'any' });
|
|
1209
|
+
if ((flags & ts.TypeFlags.Unknown) !== 0)
|
|
1210
|
+
return add({ kind: 'keyword', name: 'unknown' });
|
|
1211
|
+
if ((flags & ts.TypeFlags.Never) !== 0)
|
|
1212
|
+
return add({ kind: 'keyword', name: 'never' });
|
|
1213
|
+
if ((flags & ts.TypeFlags.String) !== 0)
|
|
1214
|
+
return add({ kind: 'keyword', name: 'string' });
|
|
1215
|
+
if ((flags & ts.TypeFlags.Number) !== 0)
|
|
1216
|
+
return add({ kind: 'keyword', name: 'number' });
|
|
1217
|
+
if ((flags & ts.TypeFlags.BigInt) !== 0)
|
|
1218
|
+
return add({ kind: 'keyword', name: 'bigint' });
|
|
1219
|
+
if ((flags & ts.TypeFlags.Boolean) !== 0)
|
|
1220
|
+
return add({ kind: 'keyword', name: 'boolean' });
|
|
1221
|
+
if ((flags & ts.TypeFlags.ESSymbol) !== 0)
|
|
1222
|
+
return add({ kind: 'keyword', name: 'symbol' });
|
|
1223
|
+
if ((flags & ts.TypeFlags.Undefined) !== 0)
|
|
1224
|
+
return add({ kind: 'keyword', name: 'undefined' });
|
|
1225
|
+
if ((flags & ts.TypeFlags.Void) !== 0)
|
|
1226
|
+
return add({ kind: 'keyword', name: 'void' });
|
|
1227
|
+
if ((flags & ts.TypeFlags.Null) !== 0)
|
|
1228
|
+
return add({ kind: 'literal', value: null, text: 'null' });
|
|
1229
|
+
if ((flags & ts.TypeFlags.StringLiteral) !== 0) {
|
|
1230
|
+
const value = type.value;
|
|
1231
|
+
return add({ kind: 'literal', value, text: JSON.stringify(value) });
|
|
1232
|
+
}
|
|
1233
|
+
if ((flags & ts.TypeFlags.NumberLiteral) !== 0) {
|
|
1234
|
+
const value = type.value;
|
|
1235
|
+
return add({ kind: 'literal', value, text: String(value) });
|
|
1236
|
+
}
|
|
1237
|
+
if ((flags & ts.TypeFlags.BigIntLiteral) !== 0) {
|
|
1238
|
+
const value = type.value;
|
|
1239
|
+
const text = `${value.negative ? '-' : ''}${value.base10Value}n`;
|
|
1240
|
+
return add({ kind: 'literal', value: BigInt(`${value.negative ? '-' : ''}${value.base10Value}`), text });
|
|
1241
|
+
}
|
|
1242
|
+
if ((flags & ts.TypeFlags.BooleanLiteral) !== 0) {
|
|
1243
|
+
const value = type.intrinsicName === 'true';
|
|
1244
|
+
return add({ kind: 'literal', value, text: String(value) });
|
|
1245
|
+
}
|
|
1246
|
+
if (type.isUnionOrIntersection()) {
|
|
1247
|
+
return add({
|
|
1248
|
+
kind: (flags & ts.TypeFlags.Union) !== 0 ? 'union' : 'intersection',
|
|
1249
|
+
types: type.types.map(convert),
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
1253
|
+
this.fail(authoredType, 'Remote codec contains an unresolved type parameter');
|
|
1254
|
+
}
|
|
1255
|
+
if ((flags & ts.TypeFlags.Object) === 0) {
|
|
1256
|
+
this.fail(authoredType, `Remote codec type ${this.checker.typeToString(type, authoredType, ts.TypeFormatFlags.NoTruncation)} has no concrete Zod projection`);
|
|
1257
|
+
}
|
|
1258
|
+
if (this.checker.isTupleType(type)) {
|
|
1259
|
+
const reference = type;
|
|
1260
|
+
const target = reference.target;
|
|
1261
|
+
const arguments_ = this.checker.getTypeArguments(reference);
|
|
1262
|
+
return add({
|
|
1263
|
+
kind: 'tuple',
|
|
1264
|
+
elements: arguments_.map((argument, index) => {
|
|
1265
|
+
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
|
|
1266
|
+
return {
|
|
1267
|
+
type: convert(argument),
|
|
1268
|
+
optional: (elementFlags & ts.ElementFlags.Optional) !== 0,
|
|
1269
|
+
rest: (elementFlags & (ts.ElementFlags.Rest | ts.ElementFlags.Variadic)) !== 0,
|
|
1270
|
+
};
|
|
1271
|
+
}),
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
|
|
1275
|
+
const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
1276
|
+
if (element === undefined)
|
|
1277
|
+
this.fail(authoredType, 'Remote codec array has no element type');
|
|
1278
|
+
return add({ kind: 'array', element: convert(element) });
|
|
1279
|
+
}
|
|
1280
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
|
|
1281
|
+
this.fail(authoredType, 'Remote codec cannot contain callable or constructable values');
|
|
1282
|
+
}
|
|
1283
|
+
const members = [];
|
|
1284
|
+
for (const property of this.checker.getPropertiesOfType(type)) {
|
|
1285
|
+
const declaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
1286
|
+
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, declaration ?? authoredType);
|
|
1287
|
+
const symbolKey = property.getName();
|
|
1288
|
+
members.push({
|
|
1289
|
+
...EMPTY_DOCUMENTATION,
|
|
1290
|
+
id: `${id}#${symbolKey}`,
|
|
1291
|
+
name: symbolKey,
|
|
1292
|
+
...(symbolKey.startsWith('__@') ? { computed: 'symbol' } : {}),
|
|
1293
|
+
optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
|
|
1294
|
+
readonly: declaration !== undefined && hasModifier(declaration, ts.SyntaxKind.ReadonlyKeyword),
|
|
1295
|
+
async: false,
|
|
1296
|
+
abstract: false,
|
|
1297
|
+
static: false,
|
|
1298
|
+
visibility: 'public',
|
|
1299
|
+
location: this.location(authoredType),
|
|
1300
|
+
text: '',
|
|
1301
|
+
kind: 'property',
|
|
1302
|
+
type: convert(propertyType),
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
for (const [index, info] of this.checker.getIndexInfosOfType(type).entries()) {
|
|
1306
|
+
members.push({
|
|
1307
|
+
...EMPTY_DOCUMENTATION,
|
|
1308
|
+
id: `${id}#index:${String(index)}`,
|
|
1309
|
+
name: '(index)',
|
|
1310
|
+
optional: false,
|
|
1311
|
+
readonly: info.isReadonly,
|
|
1312
|
+
async: false,
|
|
1313
|
+
abstract: false,
|
|
1314
|
+
static: false,
|
|
1315
|
+
visibility: 'public',
|
|
1316
|
+
location: this.location(authoredType),
|
|
1317
|
+
text: '',
|
|
1318
|
+
kind: 'index',
|
|
1319
|
+
signature: {
|
|
1320
|
+
typeParameters: [],
|
|
1321
|
+
parameters: [{
|
|
1322
|
+
name: 'key',
|
|
1323
|
+
binding: 'identifier',
|
|
1324
|
+
type: convert(info.keyType),
|
|
1325
|
+
optional: false,
|
|
1326
|
+
rest: false,
|
|
1327
|
+
receiver: false,
|
|
1328
|
+
}],
|
|
1329
|
+
returns: convert(info.type),
|
|
1330
|
+
},
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
return add({ kind: 'object', members });
|
|
1334
|
+
}
|
|
1335
|
+
finally {
|
|
1336
|
+
active.delete(type);
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
return convert(resolvedType);
|
|
1340
|
+
}
|
|
1341
|
+
assertRemoteJsonType(type, site, active, allowUndefined) {
|
|
1342
|
+
const flags = type.flags;
|
|
1343
|
+
if ((flags & ts.TypeFlags.Undefined) !== 0 && allowUndefined)
|
|
1344
|
+
return;
|
|
1345
|
+
if ((flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
|
|
1346
|
+
this.fail(site, `Remote boundary contains unconstrained ${this.checker.typeToString(type)} data`);
|
|
1347
|
+
}
|
|
1348
|
+
if ((flags & (ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0) {
|
|
1349
|
+
this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
|
|
1350
|
+
}
|
|
1351
|
+
if ((flags & (ts.TypeFlags.StringLike
|
|
1352
|
+
| ts.TypeFlags.NumberLike
|
|
1353
|
+
| ts.TypeFlags.BooleanLike
|
|
1354
|
+
| ts.TypeFlags.Null
|
|
1355
|
+
| ts.TypeFlags.Never)) !== 0)
|
|
1356
|
+
return;
|
|
1357
|
+
if (type.isUnion()) {
|
|
1358
|
+
for (const member of type.types)
|
|
1359
|
+
this.assertRemoteJsonType(member, site, active, allowUndefined);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (type.isIntersection()) {
|
|
1363
|
+
const material = type.types.filter(member => !this.isRemotePhantomConstraint(member));
|
|
1364
|
+
if (material.length === 0)
|
|
1365
|
+
this.fail(site, 'Remote boundary contains a symbol-only object');
|
|
1366
|
+
for (const member of material)
|
|
1367
|
+
this.assertRemoteJsonType(member, site, active, false);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
1371
|
+
this.fail(site, 'Remote boundary contains an unresolved type parameter');
|
|
1372
|
+
}
|
|
1373
|
+
if ((flags & ts.TypeFlags.Object) === 0) {
|
|
1374
|
+
this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
|
|
1375
|
+
}
|
|
1376
|
+
const symbol = type.getSymbol();
|
|
1377
|
+
const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
1378
|
+
if (declaration !== undefined && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration))) {
|
|
1379
|
+
this.fail(site, `Remote boundary contains class instance ${symbol?.name ?? this.checker.typeToString(type)}`);
|
|
1380
|
+
}
|
|
1381
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
|
|
1382
|
+
this.fail(site, 'Remote boundary contains callable or constructable data');
|
|
1383
|
+
}
|
|
1384
|
+
if (active.has(type))
|
|
1385
|
+
return;
|
|
1386
|
+
active.add(type);
|
|
1387
|
+
try {
|
|
1388
|
+
if (this.checker.isTupleType(type)) {
|
|
1389
|
+
const reference = type;
|
|
1390
|
+
const target = reference.target;
|
|
1391
|
+
const arguments_ = this.checker.getTypeArguments(reference);
|
|
1392
|
+
arguments_.forEach((argument, index) => {
|
|
1393
|
+
const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
|
|
1394
|
+
this.assertRemoteJsonType(argument, site, active, (elementFlags & ts.ElementFlags.Optional) !== 0);
|
|
1395
|
+
});
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
|
|
1399
|
+
const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
1400
|
+
if (element === undefined)
|
|
1401
|
+
this.fail(site, 'Remote boundary array has no element type');
|
|
1402
|
+
this.assertRemoteJsonType(element, site, active, false);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const properties = this.checker.getPropertiesOfType(type);
|
|
1406
|
+
if (properties.some(property => property.getName().startsWith('__@'))) {
|
|
1407
|
+
this.fail(site, 'Remote boundary contains a symbol-keyed property');
|
|
1408
|
+
}
|
|
1409
|
+
for (const property of properties) {
|
|
1410
|
+
const propertyDeclaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
1411
|
+
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, propertyDeclaration ?? site);
|
|
1412
|
+
this.assertRemoteJsonType(propertyType, site, active, (property.flags & ts.SymbolFlags.Optional) !== 0);
|
|
1413
|
+
}
|
|
1414
|
+
for (const info of this.checker.getIndexInfosOfType(type)) {
|
|
1415
|
+
if ((info.keyType.flags & ts.TypeFlags.ESSymbolLike) !== 0) {
|
|
1416
|
+
this.fail(site, 'Remote boundary contains a symbol index signature');
|
|
1417
|
+
}
|
|
1418
|
+
this.assertRemoteJsonType(info.type, site, active, false);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
finally {
|
|
1422
|
+
active.delete(type);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
isRemotePhantomConstraint(type) {
|
|
1426
|
+
if ((type.flags & ts.TypeFlags.Unknown) !== 0)
|
|
1427
|
+
return true;
|
|
1428
|
+
if ((type.flags & ts.TypeFlags.Any) !== 0 || (type.flags & ts.TypeFlags.Object) === 0)
|
|
1429
|
+
return false;
|
|
1430
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
|
|
1431
|
+
return false;
|
|
1432
|
+
if (this.checker.getIndexInfosOfType(type).length > 0)
|
|
1433
|
+
return false;
|
|
1434
|
+
return this.checker.getPropertiesOfType(type).every(property => property.getName().startsWith('__@'));
|
|
1435
|
+
}
|
|
1436
|
+
resolvedCycleReference(type, site, resolvedType, recursiveDeclarations) {
|
|
1437
|
+
const symbol = type.aliasSymbol ?? type.getSymbol();
|
|
1438
|
+
if (symbol === undefined)
|
|
1439
|
+
this.fail(site, 'Remote codec contains an unnamed recursive type');
|
|
1440
|
+
const resolved = this.resolveSymbol(symbol);
|
|
1441
|
+
const declaration = preferredDeclaration(resolved);
|
|
1442
|
+
if (declaration === undefined || isStandardLibraryFile(declaration.getSourceFile().fileName)) {
|
|
1443
|
+
this.fail(site, `Remote codec recursive type ${resolved.name} has no workspace declaration`);
|
|
1444
|
+
}
|
|
1445
|
+
const owner = this.registrationForFile(declaration.getSourceFile().fileName);
|
|
1446
|
+
if (owner === undefined)
|
|
1447
|
+
this.fail(site, `Remote codec recursive type ${resolved.name} is not owned by this face`);
|
|
1448
|
+
let id = recursiveDeclarations.get(type);
|
|
1449
|
+
if (id === undefined) {
|
|
1450
|
+
id = `${this.symbolId(resolved)}#remote-codec:${resolvedType}`;
|
|
1451
|
+
recursiveDeclarations.set(type, id);
|
|
1452
|
+
this.declarations.set(id, {
|
|
1453
|
+
...EMPTY_DOCUMENTATION,
|
|
1454
|
+
id,
|
|
1455
|
+
package: owner.name,
|
|
1456
|
+
name: `${resolved.name}RemoteCodec`,
|
|
1457
|
+
kind: 'alias',
|
|
1458
|
+
abstract: false,
|
|
1459
|
+
exported: false,
|
|
1460
|
+
location: this.location(declaration),
|
|
1461
|
+
text: '',
|
|
1462
|
+
typeParameters: [],
|
|
1463
|
+
extends: [],
|
|
1464
|
+
implements: [],
|
|
1465
|
+
members: [],
|
|
1466
|
+
type: resolvedType,
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return this.addNode(site, {
|
|
1470
|
+
kind: 'reference',
|
|
1471
|
+
name: `${resolved.name}RemoteCodec`,
|
|
1472
|
+
target: { kind: 'declaration', symbol: id },
|
|
1473
|
+
arguments: [],
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
namedWorkspaceType(node) {
|
|
1477
|
+
if (!ts.isTypeReferenceNode(node) && !ts.isImportTypeNode(node))
|
|
1478
|
+
return undefined;
|
|
1479
|
+
const symbol = ts.isTypeReferenceNode(node)
|
|
1480
|
+
? this.checker.getSymbolAtLocation(node.typeName)
|
|
1481
|
+
: node.qualifier === undefined ? undefined : this.checker.getSymbolAtLocation(node.qualifier);
|
|
1482
|
+
if (symbol === undefined)
|
|
1483
|
+
return undefined;
|
|
1484
|
+
const resolved = this.resolveSymbol(symbol);
|
|
1485
|
+
const declaration = preferredDeclaration(resolved);
|
|
1486
|
+
if (declaration === undefined
|
|
1487
|
+
|| isStandardLibraryFile(declaration.getSourceFile().fileName)
|
|
1488
|
+
|| this.registrationForFile(declaration.getSourceFile().fileName) === undefined)
|
|
1489
|
+
return undefined;
|
|
1490
|
+
return resolved;
|
|
1491
|
+
}
|
|
1492
|
+
publicRemoteType(symbol, site) {
|
|
1493
|
+
const declaration = preferredDeclaration(symbol);
|
|
1494
|
+
if (declaration === undefined)
|
|
1495
|
+
this.fail(site, `type ${symbol.name} has no declaration`);
|
|
1496
|
+
const registration = this.registrationForFile(declaration.getSourceFile().fileName);
|
|
1497
|
+
if (registration === undefined)
|
|
1498
|
+
this.fail(site, `type ${symbol.name} is not owned by a workspace package`);
|
|
1499
|
+
const candidates = [];
|
|
1500
|
+
for (const [subpath, target] of packageExportTargets(registration.manifest)) {
|
|
1501
|
+
if (subpath === '.' || subpath === './package.json' || subpath === './typert'
|
|
1502
|
+
|| subpath === './client/typert' || subpath === './remote' || target.includes('*'))
|
|
1503
|
+
continue;
|
|
1504
|
+
const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
|
|
1505
|
+
if (sourceFile === undefined)
|
|
1506
|
+
continue;
|
|
1507
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
|
|
1508
|
+
if (moduleSymbol === undefined)
|
|
1509
|
+
continue;
|
|
1510
|
+
for (const exported of this.checker.getExportsOfModule(moduleSymbol)) {
|
|
1511
|
+
if (this.resolveSymbol(exported) !== symbol)
|
|
1512
|
+
continue;
|
|
1513
|
+
candidates.push({
|
|
1514
|
+
symbol: this.symbolId(symbol),
|
|
1515
|
+
specifier: packageExportSpecifier(registration.name, subpath),
|
|
1516
|
+
name: exported.name,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const selected = candidates.sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name))[0];
|
|
1521
|
+
if (selected === undefined) {
|
|
1522
|
+
this.fail(site, `Remote boundary type ${symbol.name} must be exported from a public non-root type subpath`);
|
|
1523
|
+
}
|
|
1524
|
+
return selected;
|
|
1525
|
+
}
|
|
1526
|
+
isWorkspaceClass(symbol) {
|
|
1527
|
+
const declaration = preferredDeclaration(symbol);
|
|
1528
|
+
return declaration !== undefined
|
|
1529
|
+
&& ts.isClassDeclaration(declaration)
|
|
1530
|
+
&& this.registrationForFile(declaration.getSourceFile().fileName) !== undefined;
|
|
1531
|
+
}
|
|
1532
|
+
isTypeMetaSymbol(node, name) {
|
|
1533
|
+
const symbol = this.checker.getSymbolAtLocation(node);
|
|
1534
|
+
if (symbol === undefined)
|
|
1535
|
+
return false;
|
|
1536
|
+
const resolved = this.resolveSymbol(symbol);
|
|
1537
|
+
if (resolved.name !== name)
|
|
1538
|
+
return false;
|
|
1539
|
+
const declaration = preferredDeclaration(resolved);
|
|
1540
|
+
if (declaration === undefined)
|
|
1541
|
+
return false;
|
|
1542
|
+
const registration = this.registrationForFile(declaration.getSourceFile().fileName);
|
|
1543
|
+
if (registration?.name === '@deepseek-ai/dsh-type-meta')
|
|
1544
|
+
return true;
|
|
1545
|
+
for (let current = declaration; current !== undefined; current = optionalParent(current)) {
|
|
1546
|
+
if (ts.isModuleDeclaration(current)
|
|
1547
|
+
&& ts.isStringLiteral(current.name)
|
|
1548
|
+
&& current.name.text === '@deepseek-ai/dsh-type-meta')
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
validateInvocationIdentity(packages) {
|
|
1554
|
+
const endpoints = new Map();
|
|
1555
|
+
const ids = new Map();
|
|
1556
|
+
for (const invocation of packages.flatMap(packageModel => packageModel.invocations)) {
|
|
1557
|
+
const endpoint = `${invocation.namespace}/${invocation.method}`;
|
|
1558
|
+
const existingEndpoint = endpoints.get(endpoint);
|
|
1559
|
+
if (existingEndpoint !== undefined) {
|
|
1560
|
+
throw new TypertAnalysisError(`typert(${this.face}): ${invocation.location.file}:${String(invocation.location.line)}:${String(invocation.location.column)}: Remote endpoint ${endpoint} conflicts with ${existingEndpoint.id}`);
|
|
1561
|
+
}
|
|
1562
|
+
const existingId = ids.get(invocation.id);
|
|
1563
|
+
if (existingId !== undefined) {
|
|
1564
|
+
throw new TypertAnalysisError(`typert(${this.face}): ${invocation.location.file}:${String(invocation.location.line)}:${String(invocation.location.column)}: Remote invocation id ${invocation.id} conflicts with ${existingId.id}`);
|
|
1565
|
+
}
|
|
1566
|
+
endpoints.set(endpoint, invocation);
|
|
1567
|
+
ids.set(invocation.id, invocation);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
collectEvents(events) {
|
|
1571
|
+
const result = [];
|
|
1572
|
+
for (const member of events.members) {
|
|
1573
|
+
const documentation = documentationOf(member);
|
|
1574
|
+
const mode = documentation.tags.find(tag => tag.name === 'mode')?.comment?.trim();
|
|
1575
|
+
if (ts.isMethodSignature(member)) {
|
|
1576
|
+
const signature = this.signature(member, member.type);
|
|
1577
|
+
result.push({
|
|
1578
|
+
...documentation,
|
|
1579
|
+
name: memberName(member.name),
|
|
1580
|
+
signature: this.addNode(member, { kind: 'function', signature }),
|
|
1581
|
+
text: memberText(member),
|
|
1582
|
+
...(mode === undefined ? {} : { mode }),
|
|
1583
|
+
location: this.location(member),
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
else if (ts.isPropertySignature(member) && member.type !== undefined) {
|
|
1587
|
+
result.push({
|
|
1588
|
+
...documentation,
|
|
1589
|
+
name: memberName(member.name),
|
|
1590
|
+
signature: this.convertType(member.type),
|
|
1591
|
+
text: memberText(member),
|
|
1592
|
+
...(mode === undefined ? {} : { mode }),
|
|
1593
|
+
location: this.location(member),
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return result;
|
|
1598
|
+
}
|
|
1599
|
+
ensureDeclaration(symbol, selected) {
|
|
1600
|
+
const resolved = this.resolveSymbol(symbol);
|
|
1601
|
+
const id = this.symbolId(resolved);
|
|
1602
|
+
const existing = this.declarations.get(id);
|
|
1603
|
+
if (existing !== undefined)
|
|
1604
|
+
return existing;
|
|
1605
|
+
const declarationParts = resolved.declarations.filter(isTypeDeclaration);
|
|
1606
|
+
if (declarationParts.length > 1 && !declarationParts.every(ts.isInterfaceDeclaration)) {
|
|
1607
|
+
this.fail(selected, `merged ${ts.SyntaxKind[selected.kind]} declaration ${resolved.name} is not supported`);
|
|
1608
|
+
}
|
|
1609
|
+
if (selected.name === undefined) {
|
|
1610
|
+
this.fail(selected, `anonymous ${ts.SyntaxKind[selected.kind]} cannot be represented as a named type declaration`);
|
|
1611
|
+
}
|
|
1612
|
+
const owner = this.registrationForFile(selected.getSourceFile().fileName);
|
|
1613
|
+
this.declarationStates.add(id);
|
|
1614
|
+
if (declarationParts.length > 1) {
|
|
1615
|
+
const analyzedParts = declarationParts.map((declarationPart) => {
|
|
1616
|
+
const part = declarationPart;
|
|
1617
|
+
const partOwner = this.registrationForFile(part.getSourceFile().fileName);
|
|
1618
|
+
if (partOwner === undefined) {
|
|
1619
|
+
this.fail(part, `merged interface ${resolved.name} contains a declaration outside this face`);
|
|
1620
|
+
}
|
|
1621
|
+
const typeParameters = this.typeParameters(part.typeParameters);
|
|
1622
|
+
const heritage = this.heritage(part);
|
|
1623
|
+
const members = this.members(part.members, id);
|
|
1624
|
+
return {
|
|
1625
|
+
typeParameters,
|
|
1626
|
+
heritage,
|
|
1627
|
+
members,
|
|
1628
|
+
model: {
|
|
1629
|
+
...documentationOf(part),
|
|
1630
|
+
package: partOwner.name,
|
|
1631
|
+
location: this.location(part),
|
|
1632
|
+
typeParameters,
|
|
1633
|
+
extends: heritage.extends,
|
|
1634
|
+
members: members.map(member => member.id),
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
});
|
|
1638
|
+
const parameters = this.mergeTypeParameters(analyzedParts.map(part => part.typeParameters), selected, resolved.name);
|
|
1639
|
+
const model = {
|
|
1640
|
+
...documentationOf(selected),
|
|
1641
|
+
id,
|
|
1642
|
+
package: owner.name,
|
|
1643
|
+
name: declarationName(selected),
|
|
1644
|
+
kind: 'interface',
|
|
1645
|
+
abstract: false,
|
|
1646
|
+
exported: hasModifier(selected, ts.SyntaxKind.ExportKeyword),
|
|
1647
|
+
location: this.location(selected),
|
|
1648
|
+
text: declarationText(selected),
|
|
1649
|
+
typeParameters: parameters,
|
|
1650
|
+
extends: analyzedParts.flatMap(part => part.heritage.extends),
|
|
1651
|
+
implements: [],
|
|
1652
|
+
members: analyzedParts.flatMap(part => part.members),
|
|
1653
|
+
parts: analyzedParts.map(part => part.model),
|
|
1654
|
+
};
|
|
1655
|
+
this.declarations.set(id, model);
|
|
1656
|
+
this.declarationStates.delete(id);
|
|
1657
|
+
return model;
|
|
1658
|
+
}
|
|
1659
|
+
const parameters = ts.isEnumDeclaration(selected) ? [] : this.typeParameters(selected.typeParameters);
|
|
1660
|
+
const heritage = ts.isTypeAliasDeclaration(selected) || ts.isEnumDeclaration(selected)
|
|
1661
|
+
? { extends: [], implements: [] }
|
|
1662
|
+
: this.heritage(selected);
|
|
1663
|
+
const kind = ts.isClassDeclaration(selected)
|
|
1664
|
+
? 'class'
|
|
1665
|
+
: ts.isInterfaceDeclaration(selected)
|
|
1666
|
+
? 'interface'
|
|
1667
|
+
: ts.isTypeAliasDeclaration(selected)
|
|
1668
|
+
? 'alias'
|
|
1669
|
+
: 'enum';
|
|
1670
|
+
const model = {
|
|
1671
|
+
...documentationOf(selected),
|
|
1672
|
+
id,
|
|
1673
|
+
package: owner.name,
|
|
1674
|
+
name: declarationName(selected),
|
|
1675
|
+
kind,
|
|
1676
|
+
abstract: hasModifier(selected, ts.SyntaxKind.AbstractKeyword),
|
|
1677
|
+
exported: hasModifier(selected, ts.SyntaxKind.ExportKeyword),
|
|
1678
|
+
location: this.location(selected),
|
|
1679
|
+
text: declarationText(selected),
|
|
1680
|
+
typeParameters: parameters,
|
|
1681
|
+
extends: heritage.extends,
|
|
1682
|
+
implements: heritage.implements,
|
|
1683
|
+
members: ts.isTypeAliasDeclaration(selected) || ts.isEnumDeclaration(selected)
|
|
1684
|
+
? []
|
|
1685
|
+
: this.members(selected.members, id),
|
|
1686
|
+
...(ts.isTypeAliasDeclaration(selected) ? { type: this.convertType(selected.type) } : {}),
|
|
1687
|
+
...(ts.isEnumDeclaration(selected) ? { enumMembers: this.enumMembers(selected) } : {}),
|
|
1688
|
+
};
|
|
1689
|
+
this.declarations.set(id, model);
|
|
1690
|
+
this.declarationStates.delete(id);
|
|
1691
|
+
return model;
|
|
1692
|
+
}
|
|
1693
|
+
enumMembers(declaration) {
|
|
1694
|
+
return declaration.members.map(member => ({
|
|
1695
|
+
...documentationOf(member),
|
|
1696
|
+
name: memberName(member.name),
|
|
1697
|
+
...(member.initializer === undefined ? {} : { initializer: member.initializer.getText() }),
|
|
1698
|
+
location: this.location(member),
|
|
1699
|
+
}));
|
|
1700
|
+
}
|
|
1701
|
+
heritage(declaration) {
|
|
1702
|
+
const result = { extends: [], implements: [] };
|
|
1703
|
+
for (const clause of declaration.heritageClauses ?? []) {
|
|
1704
|
+
const target = clause.token === ts.SyntaxKind.ExtendsKeyword ? result.extends : result.implements;
|
|
1705
|
+
for (const type of clause.types)
|
|
1706
|
+
target.push(this.convertHeritage(type));
|
|
1707
|
+
}
|
|
1708
|
+
return result;
|
|
1709
|
+
}
|
|
1710
|
+
convertHeritage(node) {
|
|
1711
|
+
const symbol = this.checker.getSymbolAtLocation(node.expression);
|
|
1712
|
+
return this.addNode(node, {
|
|
1713
|
+
kind: 'reference',
|
|
1714
|
+
name: node.expression.getText(),
|
|
1715
|
+
target: this.targetForReference(this.resolveSymbol(symbol), node),
|
|
1716
|
+
arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
members(members, ownerId) {
|
|
1720
|
+
const result = [];
|
|
1721
|
+
for (const member of members) {
|
|
1722
|
+
if (ts.isPropertyDeclaration(member)
|
|
1723
|
+
&& memberName(member.name) === 'typertGateway'
|
|
1724
|
+
&& member.initializer !== undefined
|
|
1725
|
+
&& ts.isCallExpression(member.initializer)
|
|
1726
|
+
&& this.isTypeMetaSymbol(member.initializer.expression, 'bindTypeRTGateway'))
|
|
1727
|
+
continue;
|
|
1728
|
+
const visibility = visibilityOf(member);
|
|
1729
|
+
const isStatic = hasModifier(member, ts.SyntaxKind.StaticKeyword);
|
|
1730
|
+
if (visibility !== 'public' || isStatic || ts.isConstructorDeclaration(member))
|
|
1731
|
+
continue;
|
|
1732
|
+
const base = this.memberBase(member, ownerId, visibility, isStatic);
|
|
1733
|
+
if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
|
|
1734
|
+
const type = this.requiredType(member, member.type, 'property');
|
|
1735
|
+
result.push({ ...base, kind: 'property', type: this.convertType(type) });
|
|
1736
|
+
}
|
|
1737
|
+
else if (ts.isMethodSignature(member) || ts.isMethodDeclaration(member)) {
|
|
1738
|
+
result.push({ ...base, kind: 'method', signature: this.signature(member, member.type) });
|
|
1739
|
+
}
|
|
1740
|
+
else if (ts.isGetAccessorDeclaration(member)) {
|
|
1741
|
+
result.push({ ...base, kind: 'getter', signature: this.signature(member, member.type) });
|
|
1742
|
+
}
|
|
1743
|
+
else if (ts.isSetAccessorDeclaration(member)) {
|
|
1744
|
+
result.push({ ...base, kind: 'setter', signature: this.signature(member, member.type) });
|
|
1745
|
+
}
|
|
1746
|
+
else if (ts.isCallSignatureDeclaration(member)) {
|
|
1747
|
+
result.push({ ...base, kind: 'call', signature: this.signature(member, member.type) });
|
|
1748
|
+
}
|
|
1749
|
+
else if (ts.isConstructSignatureDeclaration(member)) {
|
|
1750
|
+
result.push({ ...base, kind: 'construct', signature: this.signature(member, member.type) });
|
|
1751
|
+
}
|
|
1752
|
+
else if (ts.isIndexSignatureDeclaration(member)) {
|
|
1753
|
+
result.push({ ...base, kind: 'index', signature: this.signature(member, member.type) });
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
memberBase(member, ownerId, visibility, isStatic) {
|
|
1759
|
+
const identity = member.name !== undefined
|
|
1760
|
+
? this.memberIdentity(member.name)
|
|
1761
|
+
: {
|
|
1762
|
+
name: ts.isCallSignatureDeclaration(member)
|
|
1763
|
+
? '(call)'
|
|
1764
|
+
: ts.isConstructSignatureDeclaration(member)
|
|
1765
|
+
? '(construct)'
|
|
1766
|
+
: '(index)',
|
|
1767
|
+
};
|
|
1768
|
+
return {
|
|
1769
|
+
...documentationOf(member),
|
|
1770
|
+
id: `${ownerId}#${identity.name}@${String(member.getStart())}`,
|
|
1771
|
+
...identity,
|
|
1772
|
+
optional: 'questionToken' in member && member.questionToken !== undefined,
|
|
1773
|
+
readonly: hasModifier(member, ts.SyntaxKind.ReadonlyKeyword),
|
|
1774
|
+
async: hasModifier(member, ts.SyntaxKind.AsyncKeyword),
|
|
1775
|
+
abstract: hasModifier(member, ts.SyntaxKind.AbstractKeyword),
|
|
1776
|
+
static: isStatic,
|
|
1777
|
+
visibility,
|
|
1778
|
+
location: this.location(member),
|
|
1779
|
+
text: memberText(member),
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
memberIdentity(name) {
|
|
1783
|
+
if (!ts.isComputedPropertyName(name))
|
|
1784
|
+
return { name: memberName(name) };
|
|
1785
|
+
const expression = name.expression;
|
|
1786
|
+
if (ts.isStringLiteral(expression) || ts.isNumericLiteral(expression)
|
|
1787
|
+
|| ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
1788
|
+
return { name: memberName(name), jsonName: expression.text };
|
|
1789
|
+
}
|
|
1790
|
+
const type = this.checker.getTypeAtLocation(expression);
|
|
1791
|
+
return {
|
|
1792
|
+
name: memberName(name),
|
|
1793
|
+
computed: (type.flags & ts.TypeFlags.UniqueESSymbol) !== 0 ? 'symbol' : 'dynamic',
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
signature(node, explicitReturn) {
|
|
1797
|
+
const parameters = node.parameters.map(parameter => ({
|
|
1798
|
+
name: memberName(parameter.name),
|
|
1799
|
+
binding: ts.isIdentifier(parameter.name)
|
|
1800
|
+
? 'identifier'
|
|
1801
|
+
: ts.isObjectBindingPattern(parameter.name)
|
|
1802
|
+
? 'object'
|
|
1803
|
+
: 'array',
|
|
1804
|
+
type: this.convertType(this.requiredType(parameter, parameter.type, 'parameter')),
|
|
1805
|
+
optional: parameter.questionToken !== undefined || parameter.initializer !== undefined,
|
|
1806
|
+
rest: parameter.dotDotDotToken !== undefined,
|
|
1807
|
+
receiver: ts.isIdentifier(parameter.name) && parameter.name.text === 'this',
|
|
1808
|
+
...(parameter.initializer === undefined ? {} : { initializer: parameter.initializer.getText() }),
|
|
1809
|
+
}));
|
|
1810
|
+
return {
|
|
1811
|
+
typeParameters: this.typeParameters(node.typeParameters),
|
|
1812
|
+
parameters,
|
|
1813
|
+
returns: ts.isSetAccessorDeclaration(node)
|
|
1814
|
+
? this.addNode(node, { kind: 'keyword', name: 'void' })
|
|
1815
|
+
: this.convertType(this.requiredType(node, explicitReturn, 'return')),
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
typeParameters(parameters) {
|
|
1819
|
+
return parameters?.map(parameter => ({
|
|
1820
|
+
id: `${this.locationKey(parameter)}#${parameter.name.text}`,
|
|
1821
|
+
name: parameter.name.text,
|
|
1822
|
+
const: hasModifier(parameter, ts.SyntaxKind.ConstKeyword),
|
|
1823
|
+
...(parameter.constraint === undefined ? {} : { constraint: this.convertType(parameter.constraint) }),
|
|
1824
|
+
...(parameter.default === undefined ? {} : { default: this.convertType(parameter.default) }),
|
|
1825
|
+
...(hasModifier(parameter, ts.SyntaxKind.InKeyword) && hasModifier(parameter, ts.SyntaxKind.OutKeyword)
|
|
1826
|
+
? { variance: 'in-out' }
|
|
1827
|
+
: hasModifier(parameter, ts.SyntaxKind.InKeyword)
|
|
1828
|
+
? { variance: 'in' }
|
|
1829
|
+
: hasModifier(parameter, ts.SyntaxKind.OutKeyword)
|
|
1830
|
+
? { variance: 'out' }
|
|
1831
|
+
: {}),
|
|
1832
|
+
})) ?? [];
|
|
1833
|
+
}
|
|
1834
|
+
mergeTypeParameters(parts, site, declarationName) {
|
|
1835
|
+
const first = parts[0];
|
|
1836
|
+
return first.map((parameter, index) => {
|
|
1837
|
+
const peers = parts.map(part => part[index]);
|
|
1838
|
+
const constraint = peers.find(peer => peer.constraint !== undefined)?.constraint;
|
|
1839
|
+
const fallback = peers.find(peer => peer.default !== undefined)?.default;
|
|
1840
|
+
const variances = [...new Set(peers.flatMap(peer => peer.variance === undefined ? [] : [peer.variance]))];
|
|
1841
|
+
if (variances.length > 1) {
|
|
1842
|
+
this.fail(site, `merged interface ${declarationName} has incompatible variance modifiers`);
|
|
1843
|
+
}
|
|
1844
|
+
return {
|
|
1845
|
+
id: parameter.id,
|
|
1846
|
+
name: parameter.name,
|
|
1847
|
+
const: peers.some(peer => peer.const),
|
|
1848
|
+
...(constraint === undefined ? {} : { constraint }),
|
|
1849
|
+
...(fallback === undefined ? {} : { default: fallback }),
|
|
1850
|
+
...(variances[0] === undefined ? {} : { variance: variances[0] }),
|
|
1851
|
+
};
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
requiredType(owner, type, purpose) {
|
|
1855
|
+
if (type !== undefined)
|
|
1856
|
+
return type;
|
|
1857
|
+
if (this.mode === 'check') {
|
|
1858
|
+
this.fail(owner, `public ${purpose} is missing an explicit type annotation`);
|
|
1859
|
+
}
|
|
1860
|
+
const inferred = this.inferType(owner, purpose);
|
|
1861
|
+
const rendered = ts.createPrinter().printNode(ts.EmitHint.Unspecified, inferred, owner.getSourceFile());
|
|
1862
|
+
const position = annotationPosition(owner, purpose);
|
|
1863
|
+
this.queueEdit({ file: realPath(owner.getSourceFile().fileName), position, text: `: ${rendered}` });
|
|
1864
|
+
throw new SourceEditQueued();
|
|
1865
|
+
}
|
|
1866
|
+
inferType(owner, purpose) {
|
|
1867
|
+
let type;
|
|
1868
|
+
if (purpose === 'return') {
|
|
1869
|
+
const signature = this.checker.getSignatureFromDeclaration(owner);
|
|
1870
|
+
type = this.checker.getReturnTypeOfSignature(signature);
|
|
1871
|
+
}
|
|
1872
|
+
else {
|
|
1873
|
+
type = this.checker.getTypeAtLocation(owner);
|
|
1874
|
+
}
|
|
1875
|
+
return this.checker.typeToTypeNode(type, owner, ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope);
|
|
1876
|
+
}
|
|
1877
|
+
convertType(node) {
|
|
1878
|
+
const id = this.allocateNodeId(node);
|
|
1879
|
+
const add = (model) => {
|
|
1880
|
+
this.nodes.set(id, { id, ...model });
|
|
1881
|
+
return id;
|
|
1882
|
+
};
|
|
1883
|
+
const keyword = keywordName(node.kind);
|
|
1884
|
+
if (keyword !== undefined)
|
|
1885
|
+
return add({ kind: 'keyword', name: keyword });
|
|
1886
|
+
if (ts.isParenthesizedTypeNode(node)) {
|
|
1887
|
+
return add({ kind: 'parenthesized', type: this.convertType(node.type) });
|
|
1888
|
+
}
|
|
1889
|
+
if (ts.isLiteralTypeNode(node))
|
|
1890
|
+
return add(literalModel(node));
|
|
1891
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
1892
|
+
const symbol = this.checker.getSymbolAtLocation(node.typeName);
|
|
1893
|
+
return add({
|
|
1894
|
+
kind: 'reference',
|
|
1895
|
+
name: node.typeName.getText(),
|
|
1896
|
+
target: this.targetForReference(this.resolveSymbol(symbol), node),
|
|
1897
|
+
arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
|
|
1901
|
+
return add({
|
|
1902
|
+
kind: ts.isUnionTypeNode(node) ? 'union' : 'intersection',
|
|
1903
|
+
types: node.types.map(type => this.convertType(type)),
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
if (ts.isArrayTypeNode(node))
|
|
1907
|
+
return add({ kind: 'array', element: this.convertType(node.elementType) });
|
|
1908
|
+
if (ts.isTupleTypeNode(node)) {
|
|
1909
|
+
return add({
|
|
1910
|
+
kind: 'tuple',
|
|
1911
|
+
elements: node.elements.map((element) => {
|
|
1912
|
+
const named = ts.isNamedTupleMember(element) ? element : undefined;
|
|
1913
|
+
const raw = named?.type ?? element;
|
|
1914
|
+
const optional = named?.questionToken !== undefined || ts.isOptionalTypeNode(raw);
|
|
1915
|
+
const rest = named?.dotDotDotToken !== undefined || ts.isRestTypeNode(raw);
|
|
1916
|
+
const type = ts.isOptionalTypeNode(raw) || ts.isRestTypeNode(raw) ? raw.type : raw;
|
|
1917
|
+
return {
|
|
1918
|
+
...(named === undefined ? {} : { name: named.name.text }),
|
|
1919
|
+
type: this.convertType(type),
|
|
1920
|
+
optional,
|
|
1921
|
+
rest,
|
|
1922
|
+
};
|
|
1923
|
+
}),
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
if (ts.isTypeLiteralNode(node))
|
|
1927
|
+
return add({ kind: 'object', members: this.members(node.members, id) });
|
|
1928
|
+
if (ts.isFunctionTypeNode(node)) {
|
|
1929
|
+
return add({ kind: 'function', signature: this.signature(node, node.type) });
|
|
1930
|
+
}
|
|
1931
|
+
if (ts.isConstructorTypeNode(node)) {
|
|
1932
|
+
return add({
|
|
1933
|
+
kind: 'constructor',
|
|
1934
|
+
abstract: hasModifier(node, ts.SyntaxKind.AbstractKeyword),
|
|
1935
|
+
signature: this.signature(node, node.type),
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
if (ts.isIndexedAccessTypeNode(node)) {
|
|
1939
|
+
return add({
|
|
1940
|
+
kind: 'indexed-access',
|
|
1941
|
+
object: this.convertType(node.objectType),
|
|
1942
|
+
index: this.convertType(node.indexType),
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
if (ts.isTypeOperatorNode(node)) {
|
|
1946
|
+
return add({
|
|
1947
|
+
kind: 'operator',
|
|
1948
|
+
operator: ts.tokenToString(node.operator),
|
|
1949
|
+
type: this.convertType(node.type),
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
if (ts.isConditionalTypeNode(node)) {
|
|
1953
|
+
return add({
|
|
1954
|
+
kind: 'conditional',
|
|
1955
|
+
check: this.convertType(node.checkType),
|
|
1956
|
+
extends: this.convertType(node.extendsType),
|
|
1957
|
+
whenTrue: this.convertType(node.trueType),
|
|
1958
|
+
whenFalse: this.convertType(node.falseType),
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
if (ts.isInferTypeNode(node)) {
|
|
1962
|
+
return add({ kind: 'infer', parameter: this.typeParameters(ts.factory.createNodeArray([node.typeParameter]))[0] });
|
|
1963
|
+
}
|
|
1964
|
+
if (ts.isMappedTypeNode(node)) {
|
|
1965
|
+
const parameter = this.typeParameters(ts.factory.createNodeArray([node.typeParameter]))[0];
|
|
1966
|
+
return add({
|
|
1967
|
+
kind: 'mapped',
|
|
1968
|
+
parameter,
|
|
1969
|
+
...(node.nameType === undefined ? {} : { nameType: this.convertType(node.nameType) }),
|
|
1970
|
+
...(node.type === undefined ? {} : { value: this.convertType(node.type) }),
|
|
1971
|
+
readonly: modifierMode(node.readonlyToken),
|
|
1972
|
+
optional: modifierMode(node.questionToken),
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
if (ts.isTemplateLiteralTypeNode(node)) {
|
|
1976
|
+
return add({
|
|
1977
|
+
kind: 'template-literal',
|
|
1978
|
+
head: node.head.text,
|
|
1979
|
+
spans: node.templateSpans.map(span => ({ type: this.convertType(span.type), text: span.literal.text })),
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
if (ts.isTypeQueryNode(node)) {
|
|
1983
|
+
return add({
|
|
1984
|
+
kind: 'type-query',
|
|
1985
|
+
expression: node.exprName.getText(),
|
|
1986
|
+
arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
if (ts.isImportTypeNode(node)) {
|
|
1990
|
+
const argument = node.argument;
|
|
1991
|
+
const symbol = node.qualifier === undefined ? undefined : this.checker.getSymbolAtLocation(node.qualifier);
|
|
1992
|
+
return add({
|
|
1993
|
+
kind: 'import-type',
|
|
1994
|
+
module: argument.literal.text,
|
|
1995
|
+
...(node.qualifier === undefined ? {} : { qualifier: node.qualifier.getText() }),
|
|
1996
|
+
arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
|
|
1997
|
+
typeof: node.isTypeOf,
|
|
1998
|
+
...(node.attributes === undefined ? {} : { attributes: importTypeAttributesText(node) }),
|
|
1999
|
+
...(symbol === undefined ? {} : { target: this.targetForReference(this.resolveSymbol(symbol), node) }),
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
if (ts.isTypePredicateNode(node)) {
|
|
2003
|
+
return add({
|
|
2004
|
+
kind: 'predicate',
|
|
2005
|
+
asserts: node.assertsModifier !== undefined,
|
|
2006
|
+
parameter: node.parameterName.getText(),
|
|
2007
|
+
...(node.type === undefined ? {} : { type: this.convertType(node.type) }),
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
/* v8 ignore else -- every source TypeNode kind accepted by TypeScript is handled above; this arm keeps
|
|
2011
|
+
* future compiler kinds fail-loud. */
|
|
2012
|
+
if (ts.isThisTypeNode(node))
|
|
2013
|
+
return add({ kind: 'this' });
|
|
2014
|
+
/* v8 ignore next -- paired with the exhaustive TypeNode guard above. */
|
|
2015
|
+
this.fail(node, `unsupported TypeScript type node ${ts.SyntaxKind[node.kind]}`);
|
|
2016
|
+
}
|
|
2017
|
+
addNode(site, model) {
|
|
2018
|
+
const id = this.allocateNodeId(site);
|
|
2019
|
+
this.nodes.set(id, { id, ...model });
|
|
2020
|
+
return id;
|
|
2021
|
+
}
|
|
2022
|
+
referenceNode(symbol, site) {
|
|
2023
|
+
return this.addNode(site, {
|
|
2024
|
+
kind: 'reference',
|
|
2025
|
+
name: symbol.name,
|
|
2026
|
+
target: { kind: 'declaration', symbol: this.symbolId(symbol) },
|
|
2027
|
+
arguments: [],
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
targetForReference(symbol, site) {
|
|
2031
|
+
const declaration = preferredDeclaration(symbol);
|
|
2032
|
+
/* v8 ignore next -- a symbol from a semantically valid source type reference always has a declaration. */
|
|
2033
|
+
if (declaration === undefined)
|
|
2034
|
+
this.fail(site, `type symbol ${symbol.name} has no declaration`);
|
|
2035
|
+
if (ts.isTypeParameterDeclaration(declaration)) {
|
|
2036
|
+
return {
|
|
2037
|
+
kind: 'type-parameter',
|
|
2038
|
+
parameter: `${this.locationKey(declaration)}#${declaration.name.text}`,
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
if (isStandardLibraryFile(declaration.getSourceFile().fileName)) {
|
|
2042
|
+
return { kind: 'standard', name: symbol.name };
|
|
2043
|
+
}
|
|
2044
|
+
const moduleSpecifier = moduleSpecifierOf(site);
|
|
2045
|
+
const module = moduleSpecifier === undefined ? undefined : moduleIdentity(moduleSpecifier);
|
|
2046
|
+
const from = this.registrationForFile(site.getSourceFile().fileName);
|
|
2047
|
+
const owner = this.registrationForFile(declaration.getSourceFile().fileName);
|
|
2048
|
+
if (owner !== undefined) {
|
|
2049
|
+
if (owner.name !== from.name) {
|
|
2050
|
+
if (module === undefined) {
|
|
2051
|
+
this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
|
|
2052
|
+
}
|
|
2053
|
+
const exportName = authoredExportName(site, moduleSpecifier);
|
|
2054
|
+
if (this.packageExportName(module, symbol, owner.face, exportName) === undefined) {
|
|
2055
|
+
this.fail(site, `package reference ${exportName} is not exported by ${module.package} at ${module.subpath}`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const typeDeclaration = declaration;
|
|
2059
|
+
if (!this.declarationStates.has(this.symbolId(symbol)))
|
|
2060
|
+
this.ensureDeclaration(symbol, typeDeclaration);
|
|
2061
|
+
return { kind: 'declaration', symbol: this.symbolId(symbol) };
|
|
2062
|
+
}
|
|
2063
|
+
const packageFaces = module === undefined
|
|
2064
|
+
? []
|
|
2065
|
+
: [...new Set(this.allRegistrations.filter(candidate => candidate.name === module.package).map(candidate => candidate.face))];
|
|
2066
|
+
const otherFace = packageFaces.find(face => face !== this.face);
|
|
2067
|
+
if (otherFace !== undefined && module !== undefined) {
|
|
2068
|
+
const requestedName = authoredExportName(site, moduleSpecifier);
|
|
2069
|
+
const exportName = this.packageExportName(module, symbol, otherFace, requestedName);
|
|
2070
|
+
if (exportName === undefined) {
|
|
2071
|
+
this.fail(site, `cross-face reference ${requestedName} is not exported by ${module.package} at ${module.subpath}`);
|
|
2072
|
+
}
|
|
2073
|
+
this.recordCrossFaceLink(from.name, otherFace, module, exportName);
|
|
2074
|
+
return {
|
|
2075
|
+
kind: 'cross-face',
|
|
2076
|
+
face: otherFace,
|
|
2077
|
+
package: module.package,
|
|
2078
|
+
subpath: module.subpath,
|
|
2079
|
+
name: exportName,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
if (module !== undefined) {
|
|
2083
|
+
return {
|
|
2084
|
+
kind: 'external',
|
|
2085
|
+
module: module.package,
|
|
2086
|
+
subpath: module.subpath,
|
|
2087
|
+
name: symbol.name,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
const external = externalModuleIdentityForFile(declaration.getSourceFile().fileName);
|
|
2091
|
+
if (external !== undefined) {
|
|
2092
|
+
return {
|
|
2093
|
+
kind: 'external',
|
|
2094
|
+
module: external.package,
|
|
2095
|
+
subpath: external.subpath,
|
|
2096
|
+
name: symbol.name,
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
this.fail(site, `reference to ${symbol.name} crosses a package or face without an explicit import`);
|
|
2100
|
+
}
|
|
2101
|
+
recordCrossFaceLink(fromPackage, toFace, module, name) {
|
|
2102
|
+
const link = {
|
|
2103
|
+
fromFace: this.face,
|
|
2104
|
+
fromPackage,
|
|
2105
|
+
toFace,
|
|
2106
|
+
toPackage: module.package,
|
|
2107
|
+
subpath: module.subpath,
|
|
2108
|
+
name,
|
|
2109
|
+
};
|
|
2110
|
+
const key = [
|
|
2111
|
+
link.fromFace,
|
|
2112
|
+
link.fromPackage,
|
|
2113
|
+
link.toFace,
|
|
2114
|
+
link.toPackage,
|
|
2115
|
+
link.subpath,
|
|
2116
|
+
link.name,
|
|
2117
|
+
].join('\0');
|
|
2118
|
+
this.crossFaceLinks.set(key, link);
|
|
2119
|
+
}
|
|
2120
|
+
packageExportName(module, symbol, face, requestedName) {
|
|
2121
|
+
const registration = this.allRegistrations.find(candidate => candidate.face === face && candidate.name === module.package);
|
|
2122
|
+
const target = packageExportTargets(registration.manifest)
|
|
2123
|
+
.find(([subpath]) => subpath === module.subpath)?.[1];
|
|
2124
|
+
if (target === undefined)
|
|
2125
|
+
return undefined;
|
|
2126
|
+
const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
|
|
2127
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
|
|
2128
|
+
const exported = this.checker.getExportsOfModule(moduleSymbol)
|
|
2129
|
+
.find(candidate => candidate.name === requestedName && this.resolveSymbol(candidate) === symbol);
|
|
2130
|
+
return exported?.name;
|
|
2131
|
+
}
|
|
2132
|
+
symbolAtType(node) {
|
|
2133
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
2134
|
+
return this.resolveSymbol(this.checker.getSymbolAtLocation(node.typeName));
|
|
2135
|
+
}
|
|
2136
|
+
const type = this.checker.getTypeAtLocation(node);
|
|
2137
|
+
const symbol = type.aliasSymbol ?? type.getSymbol();
|
|
2138
|
+
return symbol === undefined ? undefined : this.resolveSymbol(symbol);
|
|
2139
|
+
}
|
|
2140
|
+
resolveSymbol(symbol) {
|
|
2141
|
+
return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : this.checker.getAliasedSymbol(symbol);
|
|
2142
|
+
}
|
|
2143
|
+
symbolId(symbol) {
|
|
2144
|
+
const declaration = preferredDeclaration(symbol);
|
|
2145
|
+
if (declaration === undefined)
|
|
2146
|
+
return `symbol:${symbol.name}`;
|
|
2147
|
+
const location = this.location(declaration);
|
|
2148
|
+
return `${this.packageNameForFile(declaration.getSourceFile().fileName)}:${location.file}#${symbol.name}`;
|
|
2149
|
+
}
|
|
2150
|
+
registrationForFile(file) {
|
|
2151
|
+
const path = realPath(file);
|
|
2152
|
+
return this.allRegistrations
|
|
2153
|
+
.find(registration => registration.face === this.face && isWithin(path, registration.root));
|
|
2154
|
+
}
|
|
2155
|
+
packageNameForFile(file) {
|
|
2156
|
+
const path = realPath(file);
|
|
2157
|
+
return this.allRegistrations.find(registration => isWithin(path, registration.root))?.name ?? '<external>';
|
|
2158
|
+
}
|
|
2159
|
+
allocateNodeId(site) {
|
|
2160
|
+
const location = this.locationKey(site);
|
|
2161
|
+
const ordinal = (this.nodeOrdinals.get(location) ?? 0) + 1;
|
|
2162
|
+
this.nodeOrdinals.set(location, ordinal);
|
|
2163
|
+
return `type:${location}#${String(ordinal)}`;
|
|
2164
|
+
}
|
|
2165
|
+
locationKey(node) {
|
|
2166
|
+
const location = this.location(node);
|
|
2167
|
+
return `${location.file}:${String(location.line)}:${String(location.column)}`;
|
|
2168
|
+
}
|
|
2169
|
+
location(node) {
|
|
2170
|
+
const sourceFile = node.getSourceFile();
|
|
2171
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
2172
|
+
return {
|
|
2173
|
+
file: slash(relative(this.root, sourceFile.fileName)),
|
|
2174
|
+
line: position.line + 1,
|
|
2175
|
+
column: position.character + 1,
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
fail(node, message) {
|
|
2179
|
+
const location = this.location(node);
|
|
2180
|
+
throw new TypertAnalysisError(`typert(${this.face}): ${location.file}:${String(location.line)}:${String(location.column)}: ${message}`);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
function mergeWorkspaceModels(models) {
|
|
2184
|
+
const faces = new Map();
|
|
2185
|
+
const links = new Map();
|
|
2186
|
+
for (const model of models) {
|
|
2187
|
+
for (const face of model.faces) {
|
|
2188
|
+
const merged = faces.get(face.face) ?? {
|
|
2189
|
+
packages: new Map(),
|
|
2190
|
+
declarations: new Map(),
|
|
2191
|
+
nodes: new Map(),
|
|
2192
|
+
};
|
|
2193
|
+
for (const packageModel of face.packages)
|
|
2194
|
+
merged.packages.set(packageModel.name, packageModel);
|
|
2195
|
+
for (const declaration of face.graph.declarations) {
|
|
2196
|
+
if (!merged.declarations.has(declaration.id))
|
|
2197
|
+
merged.declarations.set(declaration.id, declaration);
|
|
2198
|
+
}
|
|
2199
|
+
for (const node of face.graph.nodes) {
|
|
2200
|
+
if (!merged.nodes.has(node.id))
|
|
2201
|
+
merged.nodes.set(node.id, node);
|
|
2202
|
+
}
|
|
2203
|
+
faces.set(face.face, merged);
|
|
2204
|
+
}
|
|
2205
|
+
for (const link of model.crossFaceLinks) {
|
|
2206
|
+
links.set([
|
|
2207
|
+
link.fromFace,
|
|
2208
|
+
link.fromPackage,
|
|
2209
|
+
link.toFace,
|
|
2210
|
+
link.toPackage,
|
|
2211
|
+
link.subpath,
|
|
2212
|
+
link.name,
|
|
2213
|
+
].join('\0'), link);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
faces: [...faces].sort(([left], [right]) => (left === 'host' ? 0 : 1) - (right === 'host' ? 0 : 1)).map(([face, model]) => ({
|
|
2218
|
+
face,
|
|
2219
|
+
packages: [...model.packages.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
|
2220
|
+
graph: {
|
|
2221
|
+
declarations: [...model.declarations.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
2222
|
+
nodes: [...model.nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
2223
|
+
},
|
|
2224
|
+
})),
|
|
2225
|
+
crossFaceLinks: [...links.values()].sort(compareCrossFaceLinks),
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function parseConfig(path) {
|
|
2229
|
+
const compilerPath = path.split(sep).join('/');
|
|
2230
|
+
const read = ts.readConfigFile(compilerPath, file => ts.sys.readFile(file));
|
|
2231
|
+
if (read.error !== undefined)
|
|
2232
|
+
throw new TypertAnalysisError(formatDiagnostic(read.error));
|
|
2233
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(compilerPath), undefined, compilerPath);
|
|
2234
|
+
if (parsed.errors.length > 0)
|
|
2235
|
+
throw new TypertAnalysisError(parsed.errors.map(formatDiagnostic).join('\n'));
|
|
2236
|
+
return { path, parsed };
|
|
2237
|
+
}
|
|
2238
|
+
function projectConfigPath(path) {
|
|
2239
|
+
if (extname(path) === '.json')
|
|
2240
|
+
return path;
|
|
2241
|
+
return join(path, 'tsconfig.json');
|
|
2242
|
+
}
|
|
2243
|
+
function sourceFileHasSurface(sourceFile) {
|
|
2244
|
+
for (const statement of sourceFile.statements) {
|
|
2245
|
+
if ((ts.isClassDeclaration(statement)
|
|
2246
|
+
|| ts.isInterfaceDeclaration(statement)
|
|
2247
|
+
|| ts.isTypeAliasDeclaration(statement)
|
|
2248
|
+
|| ts.isEnumDeclaration(statement))
|
|
2249
|
+
&& (typertMode(statement) !== undefined || typertServiceTag(statement) !== undefined))
|
|
2250
|
+
return true;
|
|
2251
|
+
if (ts.isClassDeclaration(statement)) {
|
|
2252
|
+
for (const member of statement.members) {
|
|
2253
|
+
if (ts.isPropertyDeclaration(member)
|
|
2254
|
+
&& memberName(member.name) === 'typertGateway'
|
|
2255
|
+
&& member.initializer !== undefined
|
|
2256
|
+
&& ts.isCallExpression(member.initializer)
|
|
2257
|
+
&& expressionName(member.initializer.expression) === 'bindTypeRTGateway')
|
|
2258
|
+
return true;
|
|
2259
|
+
for (const decorator of ts.canHaveDecorators(member) ? ts.getDecorators(member) ?? [] : []) {
|
|
2260
|
+
const expression = ts.isCallExpression(decorator.expression)
|
|
2261
|
+
? decorator.expression.expression
|
|
2262
|
+
: decorator.expression;
|
|
2263
|
+
const name = expressionName(expression);
|
|
2264
|
+
if (name === 'Remote' || name === 'RemoteScope')
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
if (!ts.isModuleDeclaration(statement)
|
|
2270
|
+
|| !ts.isStringLiteral(statement.name)
|
|
2271
|
+
|| statement.name.text !== '@deepseek-ai/cordis'
|
|
2272
|
+
|| statement.body === undefined
|
|
2273
|
+
|| !ts.isModuleBlock(statement.body))
|
|
2274
|
+
continue;
|
|
2275
|
+
if (statement.body.statements.some(member => ts.isInterfaceDeclaration(member)
|
|
2276
|
+
&& (member.name.text === 'Context' || member.name.text === 'Events')
|
|
2277
|
+
&& member.members.length > 0))
|
|
2278
|
+
return true;
|
|
2279
|
+
}
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
function hasPackageSurface(model) {
|
|
2283
|
+
return model.services.length > 0
|
|
2284
|
+
|| model.events.length > 0
|
|
2285
|
+
|| model.objects.length > 0
|
|
2286
|
+
|| model.schemas.length > 0
|
|
2287
|
+
|| model.invocations.length > 0;
|
|
2288
|
+
}
|
|
2289
|
+
function isDualFacePackage(manifest) {
|
|
2290
|
+
const dsh = manifest.dsh;
|
|
2291
|
+
const client = dsh !== null && typeof dsh === 'object'
|
|
2292
|
+
? dsh.client
|
|
2293
|
+
: undefined;
|
|
2294
|
+
return client !== null
|
|
2295
|
+
&& typeof client === 'object'
|
|
2296
|
+
&& clientExportSubpaths(manifest).length > 0;
|
|
2297
|
+
}
|
|
2298
|
+
function hostExportSubpaths(manifest) {
|
|
2299
|
+
return packageExportTargets(manifest)
|
|
2300
|
+
.map(([subpath]) => subpath)
|
|
2301
|
+
.filter(subpath => subpath !== './client'
|
|
2302
|
+
&& !subpath.startsWith('./client/')
|
|
2303
|
+
&& subpath !== './remote');
|
|
2304
|
+
}
|
|
2305
|
+
function clientExportSubpaths(manifest) {
|
|
2306
|
+
return packageExportTargets(manifest)
|
|
2307
|
+
.map(([subpath]) => subpath)
|
|
2308
|
+
.filter(subpath => subpath === './client' || subpath.startsWith('./client/'));
|
|
2309
|
+
}
|
|
2310
|
+
function packageExportTargets(manifest) {
|
|
2311
|
+
const exportsField = manifest.exports;
|
|
2312
|
+
if (typeof exportsField === 'string')
|
|
2313
|
+
return [['.', exportsField]];
|
|
2314
|
+
if (exportsField === null || typeof exportsField !== 'object') {
|
|
2315
|
+
const types = manifest.types;
|
|
2316
|
+
return typeof types === 'string' ? [['.', types]] : [];
|
|
2317
|
+
}
|
|
2318
|
+
if (Array.isArray(exportsField)
|
|
2319
|
+
|| !Object.keys(exportsField).some(key => key.startsWith('.'))) {
|
|
2320
|
+
const target = exportTarget(exportsField);
|
|
2321
|
+
return target === undefined ? [] : [['.', target]];
|
|
2322
|
+
}
|
|
2323
|
+
const result = [];
|
|
2324
|
+
for (const [subpath, value] of Object.entries(exportsField)) {
|
|
2325
|
+
if (!subpath.startsWith('.'))
|
|
2326
|
+
continue;
|
|
2327
|
+
const target = exportTarget(value);
|
|
2328
|
+
if (target !== undefined)
|
|
2329
|
+
result.push([subpath, target]);
|
|
2330
|
+
}
|
|
2331
|
+
return result.sort(([left], [right]) => left.localeCompare(right));
|
|
2332
|
+
}
|
|
2333
|
+
function exportTarget(value) {
|
|
2334
|
+
if (typeof value === 'string')
|
|
2335
|
+
return value;
|
|
2336
|
+
if (Array.isArray(value)) {
|
|
2337
|
+
for (const candidate of value) {
|
|
2338
|
+
const target = exportTarget(candidate);
|
|
2339
|
+
if (target !== undefined)
|
|
2340
|
+
return target;
|
|
2341
|
+
}
|
|
2342
|
+
return undefined;
|
|
2343
|
+
}
|
|
2344
|
+
if (value === null || typeof value !== 'object')
|
|
2345
|
+
return undefined;
|
|
2346
|
+
const conditions = value;
|
|
2347
|
+
for (const key of ['types', 'import', 'default']) {
|
|
2348
|
+
const target = exportTarget(conditions[key]);
|
|
2349
|
+
if (target !== undefined)
|
|
2350
|
+
return target;
|
|
2351
|
+
}
|
|
2352
|
+
for (const candidate of Object.values(conditions)) {
|
|
2353
|
+
const target = exportTarget(candidate);
|
|
2354
|
+
if (target !== undefined)
|
|
2355
|
+
return target;
|
|
2356
|
+
}
|
|
2357
|
+
return undefined;
|
|
2358
|
+
}
|
|
2359
|
+
function sourcePathForExport(packageRoot, target) {
|
|
2360
|
+
const normalized = target.replace(/^\.\//, '');
|
|
2361
|
+
if (normalized.startsWith('lib/types/')) {
|
|
2362
|
+
return resolve(packageRoot, 'src', normalized.slice('lib/types/'.length).replace(/\.d\.(?:mts|cts|ts)$/, '.ts'));
|
|
2363
|
+
}
|
|
2364
|
+
if (normalized.startsWith('lib/')) {
|
|
2365
|
+
return resolve(packageRoot, 'src', normalized.slice('lib/'.length).replace(/\.(?:mjs|cjs|js|d\.ts)$/, '.ts'));
|
|
2366
|
+
}
|
|
2367
|
+
return resolve(packageRoot, normalized);
|
|
2368
|
+
}
|
|
2369
|
+
function preferredDeclaration(symbol) {
|
|
2370
|
+
return symbol.declarations?.find(isTypeDeclaration)
|
|
2371
|
+
?? symbol.valueDeclaration
|
|
2372
|
+
?? symbol.declarations?.[0];
|
|
2373
|
+
}
|
|
2374
|
+
function optionalParent(node) {
|
|
2375
|
+
return node.parent;
|
|
2376
|
+
}
|
|
2377
|
+
function isTypeDeclaration(node) {
|
|
2378
|
+
return ts.isClassDeclaration(node)
|
|
2379
|
+
|| ts.isInterfaceDeclaration(node)
|
|
2380
|
+
|| ts.isTypeAliasDeclaration(node)
|
|
2381
|
+
|| ts.isEnumDeclaration(node);
|
|
2382
|
+
}
|
|
2383
|
+
function declarationName(declaration) {
|
|
2384
|
+
return declaration.name.text;
|
|
2385
|
+
}
|
|
2386
|
+
function memberText(member) {
|
|
2387
|
+
const sourceFile = member.getSourceFile();
|
|
2388
|
+
const full = member.getText(sourceFile);
|
|
2389
|
+
const body = member.body;
|
|
2390
|
+
const signature = body === undefined ? full : full.slice(0, full.length - body.getText(sourceFile).length);
|
|
2391
|
+
return signature.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim();
|
|
2392
|
+
}
|
|
2393
|
+
function declarationText(declaration) {
|
|
2394
|
+
const printer = ts.createPrinter({ removeComments: true });
|
|
2395
|
+
const projected = ts.isClassDeclaration(declaration) ? classShape(declaration) : declaration;
|
|
2396
|
+
return printer.printNode(ts.EmitHint.Unspecified, projected, declaration.getSourceFile()).replace(/\r/g, '');
|
|
2397
|
+
}
|
|
2398
|
+
function classShape(node) {
|
|
2399
|
+
const nonPublic = (member) => (ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined)?.some(modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword) ?? false;
|
|
2400
|
+
const members = node.members.flatMap((member) => {
|
|
2401
|
+
if (nonPublic(member) || (ts.isPropertyDeclaration(member) && ts.isPrivateIdentifier(member.name)))
|
|
2402
|
+
return [];
|
|
2403
|
+
if (ts.isMethodDeclaration(member)) {
|
|
2404
|
+
return [ts.factory.updateMethodDeclaration(member, member.modifiers, member.asteriskToken, member.name, member.questionToken, member.typeParameters, member.parameters, member.type, undefined)];
|
|
2405
|
+
}
|
|
2406
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
2407
|
+
return [ts.factory.updateConstructorDeclaration(member, member.modifiers, member.parameters, undefined)];
|
|
2408
|
+
}
|
|
2409
|
+
if (ts.isGetAccessorDeclaration(member)) {
|
|
2410
|
+
return [ts.factory.updateGetAccessorDeclaration(member, member.modifiers, member.name, member.parameters, member.type, undefined)];
|
|
2411
|
+
}
|
|
2412
|
+
if (ts.isSetAccessorDeclaration(member)) {
|
|
2413
|
+
return [ts.factory.updateSetAccessorDeclaration(member, member.modifiers, member.name, member.parameters, undefined)];
|
|
2414
|
+
}
|
|
2415
|
+
if (ts.isPropertyDeclaration(member)) {
|
|
2416
|
+
return [ts.factory.updatePropertyDeclaration(member, member.modifiers, member.name, member.questionToken ?? member.exclamationToken, member.type, undefined)];
|
|
2417
|
+
}
|
|
2418
|
+
return [member];
|
|
2419
|
+
});
|
|
2420
|
+
return ts.factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, members);
|
|
2421
|
+
}
|
|
2422
|
+
function documentationOf(node) {
|
|
2423
|
+
const blocks = ts.getJSDocCommentsAndTags(node).filter(ts.isJSDoc);
|
|
2424
|
+
const block = blocks.at(-1);
|
|
2425
|
+
if (block === undefined)
|
|
2426
|
+
return EMPTY_DOCUMENTATION;
|
|
2427
|
+
const description = normalizedDocText(ts.getTextOfJSDocComment(block.comment));
|
|
2428
|
+
const tags = ts.getJSDocTags(node).map((tag) => {
|
|
2429
|
+
const named = tag;
|
|
2430
|
+
const comment = normalizedDocText(ts.getTextOfJSDocComment(tag.comment));
|
|
2431
|
+
return {
|
|
2432
|
+
name: tag.tagName.text,
|
|
2433
|
+
...(named.name === undefined ? {} : { argument: named.name.getText() }),
|
|
2434
|
+
...(comment === undefined ? {} : { comment }),
|
|
2435
|
+
text: tag.getText(tag.getSourceFile()).trim(),
|
|
2436
|
+
};
|
|
2437
|
+
});
|
|
2438
|
+
return {
|
|
2439
|
+
...(description === undefined ? {} : {
|
|
2440
|
+
description,
|
|
2441
|
+
summary: firstSentence(description),
|
|
2442
|
+
}),
|
|
2443
|
+
tags,
|
|
2444
|
+
jsDoc: rawJsDoc(node),
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function normalizedDocText(value) {
|
|
2448
|
+
if (value === undefined)
|
|
2449
|
+
return undefined;
|
|
2450
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
2451
|
+
/* v8 ignore next -- TypeScript represents whitespace-only JSDoc as undefined before this helper is called. */
|
|
2452
|
+
return normalized.length === 0 ? undefined : normalized;
|
|
2453
|
+
}
|
|
2454
|
+
function firstSentence(value) {
|
|
2455
|
+
return (/^(.*?[.!?])(?:\s|$)/.exec(value)?.[1] ?? value).trim();
|
|
2456
|
+
}
|
|
2457
|
+
function rawJsDoc(node) {
|
|
2458
|
+
const sourceFile = node.getSourceFile();
|
|
2459
|
+
const source = sourceFile.getFullText();
|
|
2460
|
+
const ranges = ts.getLeadingCommentRanges(source, node.getFullStart());
|
|
2461
|
+
const range = ranges.filter(candidate => source.slice(candidate.pos, candidate.pos + 3) === '/**').at(-1);
|
|
2462
|
+
const raw = source.slice(range.pos, range.end);
|
|
2463
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(range.pos);
|
|
2464
|
+
const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
|
|
2465
|
+
const indent = source.slice(lineStart, range.pos);
|
|
2466
|
+
return raw.split('\n')
|
|
2467
|
+
.map((text, index) => index > 0 && text.startsWith(indent) ? text.slice(indent.length) : text)
|
|
2468
|
+
.join('\n');
|
|
2469
|
+
}
|
|
2470
|
+
function typertMode(node) {
|
|
2471
|
+
for (const tag of ts.getJSDocTags(node)) {
|
|
2472
|
+
if (tag.tagName.text !== 'typert')
|
|
2473
|
+
continue;
|
|
2474
|
+
const mode = (ts.getTextOfJSDocComment(tag.comment) ?? '').trim().split(/\s+/, 1)[0];
|
|
2475
|
+
if (mode === 'object')
|
|
2476
|
+
return 'object';
|
|
2477
|
+
if (mode === '' || mode === 'schema' || mode === 'type')
|
|
2478
|
+
return 'schema';
|
|
2479
|
+
}
|
|
2480
|
+
return undefined;
|
|
2481
|
+
}
|
|
2482
|
+
function typertServiceTag(node) {
|
|
2483
|
+
return ts.getJSDocTags(node).find(tag => tag.tagName.text === 'typert'
|
|
2484
|
+
&& (ts.getTextOfJSDocComment(tag.comment) ?? '').trim().split(/\s+/, 1)[0] === 'service');
|
|
2485
|
+
}
|
|
2486
|
+
function memberName(name) {
|
|
2487
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name)
|
|
2488
|
+
|| ts.isNumericLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name))
|
|
2489
|
+
return name.text;
|
|
2490
|
+
if (ts.isComputedPropertyName(name))
|
|
2491
|
+
return `[${name.expression.getText()}]`;
|
|
2492
|
+
return name.getText();
|
|
2493
|
+
}
|
|
2494
|
+
function stringLiteralValue(node) {
|
|
2495
|
+
return node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
2496
|
+
? node.text
|
|
2497
|
+
: undefined;
|
|
2498
|
+
}
|
|
2499
|
+
function isRemoteSegment(value) {
|
|
2500
|
+
// Generation bootstraps workspace artifacts before dsh-type-meta is built,
|
|
2501
|
+
// so this extraction-only copy must mirror isTypeRTRemoteSegment().
|
|
2502
|
+
return value !== '.' && value !== '..' && /^[A-Za-z0-9_$.-]+$/.test(value);
|
|
2503
|
+
}
|
|
2504
|
+
function expressionName(node) {
|
|
2505
|
+
if (ts.isIdentifier(node))
|
|
2506
|
+
return node.text;
|
|
2507
|
+
if (ts.isPropertyAccessExpression(node))
|
|
2508
|
+
return node.name.text;
|
|
2509
|
+
return undefined;
|
|
2510
|
+
}
|
|
2511
|
+
function packageExportSpecifier(packageName, subpath) {
|
|
2512
|
+
return subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`;
|
|
2513
|
+
}
|
|
2514
|
+
function visibilityOf(node) {
|
|
2515
|
+
if ('name' in node && node.name !== undefined && ts.isPrivateIdentifier(node.name))
|
|
2516
|
+
return 'private';
|
|
2517
|
+
if (hasModifier(node, ts.SyntaxKind.PrivateKeyword))
|
|
2518
|
+
return 'private';
|
|
2519
|
+
if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword))
|
|
2520
|
+
return 'protected';
|
|
2521
|
+
return 'public';
|
|
2522
|
+
}
|
|
2523
|
+
function hasModifier(node, kind) {
|
|
2524
|
+
return (ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined)?.some(modifier => modifier.kind === kind) ?? false;
|
|
2525
|
+
}
|
|
2526
|
+
function exposableMember(member) {
|
|
2527
|
+
return member.visibility === 'public' && !member.static;
|
|
2528
|
+
}
|
|
2529
|
+
function keywordName(kind) {
|
|
2530
|
+
switch (kind) {
|
|
2531
|
+
case ts.SyntaxKind.AnyKeyword: return 'any';
|
|
2532
|
+
case ts.SyntaxKind.BigIntKeyword: return 'bigint';
|
|
2533
|
+
case ts.SyntaxKind.BooleanKeyword: return 'boolean';
|
|
2534
|
+
case ts.SyntaxKind.NeverKeyword: return 'never';
|
|
2535
|
+
case ts.SyntaxKind.NumberKeyword: return 'number';
|
|
2536
|
+
case ts.SyntaxKind.ObjectKeyword: return 'object';
|
|
2537
|
+
case ts.SyntaxKind.StringKeyword: return 'string';
|
|
2538
|
+
case ts.SyntaxKind.SymbolKeyword: return 'symbol';
|
|
2539
|
+
case ts.SyntaxKind.UndefinedKeyword: return 'undefined';
|
|
2540
|
+
case ts.SyntaxKind.UnknownKeyword: return 'unknown';
|
|
2541
|
+
case ts.SyntaxKind.VoidKeyword: return 'void';
|
|
2542
|
+
default: return undefined;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
function literalModel(node) {
|
|
2546
|
+
const literal = node.literal;
|
|
2547
|
+
if (ts.isStringLiteral(literal))
|
|
2548
|
+
return { kind: 'literal', value: literal.text, text: literal.getText() };
|
|
2549
|
+
if (ts.isNoSubstitutionTemplateLiteral(literal)) {
|
|
2550
|
+
return { kind: 'literal', value: literal.text, text: literal.getText() };
|
|
2551
|
+
}
|
|
2552
|
+
if (ts.isNumericLiteral(literal))
|
|
2553
|
+
return { kind: 'literal', value: Number(literal.text), text: literal.getText() };
|
|
2554
|
+
if (ts.isBigIntLiteral(literal))
|
|
2555
|
+
return { kind: 'literal', value: BigInt(literal.text.slice(0, -1)), text: literal.getText() };
|
|
2556
|
+
if (literal.kind === ts.SyntaxKind.TrueKeyword)
|
|
2557
|
+
return { kind: 'literal', value: true, text: 'true' };
|
|
2558
|
+
if (literal.kind === ts.SyntaxKind.FalseKeyword)
|
|
2559
|
+
return { kind: 'literal', value: false, text: 'false' };
|
|
2560
|
+
if (literal.kind === ts.SyntaxKind.NullKeyword)
|
|
2561
|
+
return { kind: 'literal', value: null, text: 'null' };
|
|
2562
|
+
/* v8 ignore else -- all remaining LiteralTypeNode syntax is a signed numeric or bigint literal. */
|
|
2563
|
+
if (ts.isPrefixUnaryExpression(literal)
|
|
2564
|
+
&& (ts.isNumericLiteral(literal.operand) || ts.isBigIntLiteral(literal.operand))) {
|
|
2565
|
+
return {
|
|
2566
|
+
kind: 'literal',
|
|
2567
|
+
value: ts.isBigIntLiteral(literal.operand)
|
|
2568
|
+
? BigInt(literal.getText().slice(0, -1))
|
|
2569
|
+
: Number(literal.getText()),
|
|
2570
|
+
text: literal.getText(),
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
/* v8 ignore next -- TypeScript's LiteralTypeNode grammar is exhausted above; this contains future compiler syntax. */
|
|
2574
|
+
throw new TypertAnalysisError(`typert: unsupported literal type ${literal.getText()}`);
|
|
2575
|
+
}
|
|
2576
|
+
function modifierMode(token) {
|
|
2577
|
+
if (token?.kind === ts.SyntaxKind.PlusToken)
|
|
2578
|
+
return 'add';
|
|
2579
|
+
if (token?.kind === ts.SyntaxKind.MinusToken)
|
|
2580
|
+
return 'remove';
|
|
2581
|
+
return token === undefined ? 'preserve' : 'add';
|
|
2582
|
+
}
|
|
2583
|
+
function annotationPosition(node, purpose) {
|
|
2584
|
+
if (purpose === 'return')
|
|
2585
|
+
return node.parameters.end + 1;
|
|
2586
|
+
return node.name.end;
|
|
2587
|
+
}
|
|
2588
|
+
function moduleSpecifierOf(node) {
|
|
2589
|
+
if (ts.isImportTypeNode(node)) {
|
|
2590
|
+
const argument = node.argument;
|
|
2591
|
+
return argument.literal.text;
|
|
2592
|
+
}
|
|
2593
|
+
const symbol = ts.isTypeReferenceNode(node)
|
|
2594
|
+
? node.typeName
|
|
2595
|
+
: node.expression;
|
|
2596
|
+
const sourceFile = node.getSourceFile();
|
|
2597
|
+
const first = ts.isIdentifier(symbol) ? symbol.text : symbol.getFirstToken(sourceFile)?.getText(sourceFile);
|
|
2598
|
+
for (const statement of sourceFile.statements) {
|
|
2599
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause === undefined
|
|
2600
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier))
|
|
2601
|
+
continue;
|
|
2602
|
+
if (statement.importClause.name?.text === first)
|
|
2603
|
+
return statement.moduleSpecifier.text;
|
|
2604
|
+
const bindings = statement.importClause.namedBindings;
|
|
2605
|
+
if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === first) {
|
|
2606
|
+
return statement.moduleSpecifier.text;
|
|
2607
|
+
}
|
|
2608
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)
|
|
2609
|
+
&& bindings.elements.some(element => element.name.text === first))
|
|
2610
|
+
return statement.moduleSpecifier.text;
|
|
2611
|
+
}
|
|
2612
|
+
return undefined;
|
|
2613
|
+
}
|
|
2614
|
+
function authoredExportName(node, moduleSpecifier) {
|
|
2615
|
+
if (ts.isImportTypeNode(node))
|
|
2616
|
+
return node.qualifier.getText().split('.')[0];
|
|
2617
|
+
const referenced = ts.isTypeReferenceNode(node)
|
|
2618
|
+
? node.typeName.getText().split('.')
|
|
2619
|
+
: node.expression.getText().split('.');
|
|
2620
|
+
const localName = referenced[0];
|
|
2621
|
+
for (const statement of node.getSourceFile().statements) {
|
|
2622
|
+
if (!ts.isImportDeclaration(statement)
|
|
2623
|
+
|| statement.importClause === undefined
|
|
2624
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier)
|
|
2625
|
+
|| statement.moduleSpecifier.text !== moduleSpecifier)
|
|
2626
|
+
continue;
|
|
2627
|
+
if (statement.importClause.name?.text === localName)
|
|
2628
|
+
return 'default';
|
|
2629
|
+
const bindings = statement.importClause.namedBindings;
|
|
2630
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)) {
|
|
2631
|
+
const imported = bindings.elements.find(element => element.name.text === localName);
|
|
2632
|
+
if (imported !== undefined)
|
|
2633
|
+
return imported.propertyName?.text ?? imported.name.text;
|
|
2634
|
+
}
|
|
2635
|
+
if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === localName) {
|
|
2636
|
+
return referenced[1];
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
/* v8 ignore next -- moduleSpecifierOf returns only the matching import inspected by this loop. */
|
|
2640
|
+
throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
|
|
2641
|
+
}
|
|
2642
|
+
function importTypeAttributesText(node) {
|
|
2643
|
+
const sourceFile = node.getSourceFile();
|
|
2644
|
+
const children = node.getChildren(sourceFile);
|
|
2645
|
+
const comma = children.find(child => child.kind === ts.SyntaxKind.CommaToken);
|
|
2646
|
+
const close = children.find(child => child.kind === ts.SyntaxKind.CloseParenToken);
|
|
2647
|
+
return sourceFile.text.slice(comma.end, close.pos).trim();
|
|
2648
|
+
}
|
|
2649
|
+
function moduleIdentity(specifier) {
|
|
2650
|
+
if (specifier.startsWith('.') || specifier.startsWith('/'))
|
|
2651
|
+
return undefined;
|
|
2652
|
+
const parts = specifier.split('/');
|
|
2653
|
+
const packageLength = specifier.startsWith('@') ? 2 : 1;
|
|
2654
|
+
const packageName = parts.slice(0, packageLength).join('/');
|
|
2655
|
+
const rest = parts.slice(packageLength).join('/');
|
|
2656
|
+
return {
|
|
2657
|
+
package: packageName,
|
|
2658
|
+
subpath: rest.length === 0 ? '.' : `./${rest}`,
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
function externalModuleIdentityForFile(file) {
|
|
2662
|
+
const normalized = slash(file);
|
|
2663
|
+
const marker = '/node_modules/';
|
|
2664
|
+
const index = normalized.lastIndexOf(marker);
|
|
2665
|
+
if (index < 0)
|
|
2666
|
+
return undefined;
|
|
2667
|
+
const parts = normalized.slice(index + marker.length).split('/');
|
|
2668
|
+
const packageLength = parts[0].startsWith('@') ? 2 : 1;
|
|
2669
|
+
const packageName = parts.slice(0, packageLength).join('/');
|
|
2670
|
+
return { package: packageName, subpath: '.' };
|
|
2671
|
+
}
|
|
2672
|
+
function isStandardLibraryFile(file) {
|
|
2673
|
+
const base = file.replaceAll('\\', '/');
|
|
2674
|
+
return /\/typescript\/lib\/lib\.[^/]+\.d\.ts$/.test(base);
|
|
2675
|
+
}
|
|
2676
|
+
function formatDiagnostic(diagnostic) {
|
|
2677
|
+
return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
2678
|
+
}
|
|
2679
|
+
function formatProgramDiagnostic(root, face, diagnostic) {
|
|
2680
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
2681
|
+
const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
2682
|
+
const file = slash(relative(root, diagnostic.file.fileName));
|
|
2683
|
+
return `typert(${face}): ${file}:${String(position.line + 1)}:${String(position.character + 1)}: TypeScript TS${String(diagnostic.code)}: ${message}`;
|
|
2684
|
+
}
|
|
2685
|
+
const realPathCache = new Map();
|
|
2686
|
+
function realPath(path) {
|
|
2687
|
+
const absolute = resolve(path);
|
|
2688
|
+
const cached = realPathCache.get(absolute);
|
|
2689
|
+
if (cached !== undefined)
|
|
2690
|
+
return cached;
|
|
2691
|
+
// Only existing paths are memoized: a path can come into existence later,
|
|
2692
|
+
// but an existing path's canonical form is stable for the process lifetime
|
|
2693
|
+
// (analysis edits rewrite file contents, never the directory tree).
|
|
2694
|
+
if (!existsSync(absolute))
|
|
2695
|
+
return absolute;
|
|
2696
|
+
const resolved = realpathSync(absolute);
|
|
2697
|
+
realPathCache.set(absolute, resolved);
|
|
2698
|
+
return resolved;
|
|
2699
|
+
}
|
|
2700
|
+
function isWithin(path, root) {
|
|
2701
|
+
const absolute = realPath(path);
|
|
2702
|
+
const parent = realPath(root);
|
|
2703
|
+
return absolute === parent || absolute.startsWith(parent + sep);
|
|
2704
|
+
}
|
|
2705
|
+
function slash(value) {
|
|
2706
|
+
return value.replaceAll('\\', '/');
|
|
2707
|
+
}
|
|
2708
|
+
function uniqueBy(values, key) {
|
|
2709
|
+
const result = new Map();
|
|
2710
|
+
for (const value of values)
|
|
2711
|
+
if (!result.has(key(value)))
|
|
2712
|
+
result.set(key(value), value);
|
|
2713
|
+
return [...result.values()];
|
|
2714
|
+
}
|
|
2715
|
+
function compareCrossFaceLinks(left, right) {
|
|
2716
|
+
return left.fromFace.localeCompare(right.fromFace)
|
|
2717
|
+
|| left.fromPackage.localeCompare(right.fromPackage)
|
|
2718
|
+
|| left.toFace.localeCompare(right.toFace)
|
|
2719
|
+
|| left.toPackage.localeCompare(right.toPackage)
|
|
2720
|
+
|| left.subpath.localeCompare(right.subpath)
|
|
2721
|
+
|| left.name.localeCompare(right.name);
|
|
2722
|
+
}
|
|
2723
|
+
//# sourceMappingURL=analyzer.js.map
|