@deepseek-ai/dsh-typert-generator 0.2.1-alpha.1 → 0.2.1-alpha.3
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/README.i18n.yaml +16 -10
- package/README.md +30 -3
- package/README.zh.md +30 -3
- package/lib/index.js +714 -105
- package/lib/types/analyzer.d.ts +34 -2
- package/lib/types/analyzer.js +201 -49
- package/lib/types/artifact-files.d.ts +30 -0
- package/lib/types/artifact-files.js +57 -0
- package/lib/types/build-output-guard.d.ts +61 -0
- package/lib/types/build-output-guard.js +193 -0
- package/lib/types/emitter.d.ts +3 -0
- package/lib/types/emitter.js +148 -48
- package/lib/types/index.d.ts +3 -2
- package/lib/types/index.js +2 -1
- package/lib/types/model.d.ts +6 -2
- package/lib/types/tool-schema.d.ts +20 -0
- package/lib/types/tool-schema.js +156 -0
- package/lib/types/tsdown-plugin.d.ts +21 -6
- package/lib/types/tsdown-plugin.js +176 -77
- package/lib/types/worker.d.ts +28 -0
- package/lib/types/worker.js +49 -0
- package/lib/types/workspace.d.ts +34 -2
- package/lib/types/workspace.js +55 -7
- package/package.json +7 -6
package/lib/types/analyzer.d.ts
CHANGED
|
@@ -30,6 +30,15 @@ export interface WorkspaceAnalyzerOptions {
|
|
|
30
30
|
readonly mode?: AnalysisMode;
|
|
31
31
|
/** Shared workspace memo; supply one instance to reuse parses across analyzers. */
|
|
32
32
|
readonly caches?: WorkspaceCaches;
|
|
33
|
+
/**
|
|
34
|
+
* Fail analysis when a face program loads a file from a workspace
|
|
35
|
+
* package's `lib/` directory, or cannot resolve an import of a workspace
|
|
36
|
+
* package (default false). Enable it for faces that import no generated
|
|
37
|
+
* output, so that the result depends only on workspace source and never on
|
|
38
|
+
* whether package compilation ran; the failure names each importing
|
|
39
|
+
* specifier and the source a `paths` alias should map it to.
|
|
40
|
+
*/
|
|
41
|
+
readonly rejectBuildOutputs?: boolean;
|
|
33
42
|
}
|
|
34
43
|
/** One package face whose public export graph contains Typert business declarations. */
|
|
35
44
|
export interface DiscoveredTypertPackage {
|
|
@@ -44,6 +53,15 @@ export interface ParsedConfig {
|
|
|
44
53
|
/** The TypeScript parse result. */
|
|
45
54
|
readonly parsed: ts.ParsedCommandLine;
|
|
46
55
|
}
|
|
56
|
+
/** The settings Typert reads from one face aggregate tsconfig. */
|
|
57
|
+
export interface AggregateConfig {
|
|
58
|
+
/** Absolute config path. */
|
|
59
|
+
readonly path: string;
|
|
60
|
+
/** The aggregate's effective compiler options. */
|
|
61
|
+
readonly options: ts.CompilerOptions;
|
|
62
|
+
/** The package projects the aggregate references. */
|
|
63
|
+
readonly projectReferences: readonly ts.ProjectReference[];
|
|
64
|
+
}
|
|
47
65
|
/** One package face registration discovered from an aggregate tsconfig. */
|
|
48
66
|
export interface PackageRegistration {
|
|
49
67
|
/** The face whose aggregate references this package project. */
|
|
@@ -52,7 +70,11 @@ export interface PackageRegistration {
|
|
|
52
70
|
readonly name: string;
|
|
53
71
|
/** Real package root directory. */
|
|
54
72
|
readonly root: string;
|
|
55
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The package's own tsconfig, parsed on first read. Typert parses only the
|
|
75
|
+
* tsconfigs of packages it analyzes; validating every referenced tsconfig
|
|
76
|
+
* belongs to the preceding `tsc -b`.
|
|
77
|
+
*/
|
|
56
78
|
readonly config: ParsedConfig;
|
|
57
79
|
/** The parsed package.json content. */
|
|
58
80
|
readonly manifest: Record<string, unknown>;
|
|
@@ -73,6 +95,7 @@ export declare class WorkspaceCaches {
|
|
|
73
95
|
readonly configs: Map<string, ParsedConfig>;
|
|
74
96
|
/** Registration inventories keyed by root and aggregate config paths. */
|
|
75
97
|
readonly registrations: Map<string, PackageRegistration[]>;
|
|
98
|
+
private readonly aggregates;
|
|
76
99
|
private readonly hosts;
|
|
77
100
|
/**
|
|
78
101
|
* Parse one tsconfig once per workspace snapshot.
|
|
@@ -80,6 +103,14 @@ export declare class WorkspaceCaches {
|
|
|
80
103
|
* @returns the memoized parse result.
|
|
81
104
|
*/
|
|
82
105
|
config(path: string): ParsedConfig;
|
|
106
|
+
/**
|
|
107
|
+
* Read one face aggregate tsconfig once per workspace snapshot. An aggregate
|
|
108
|
+
* that declares `references` is parsed without expanding its `include`
|
|
109
|
+
* patterns, which affect neither its options nor its references.
|
|
110
|
+
* @param path - absolute aggregate config path.
|
|
111
|
+
* @returns the memoized compiler options and project references.
|
|
112
|
+
*/
|
|
113
|
+
aggregate(path: string): AggregateConfig;
|
|
83
114
|
/**
|
|
84
115
|
* Return the shared compiler host for one face. Every program of one face
|
|
85
116
|
* is built from the same aggregate compiler options (the first call wins),
|
|
@@ -123,9 +154,10 @@ export declare class WorkspaceAnalyzer {
|
|
|
123
154
|
/**
|
|
124
155
|
* Discover package faces from public-export-reachable Cordis augmentations
|
|
125
156
|
* and explicit `@typert` roots without constructing a type-checker program.
|
|
157
|
+
* @param includeManifest - optional `package.json` predicate; a rejected package's sources and project config are not read.
|
|
126
158
|
* @returns contributors grouped by package with deterministic face order.
|
|
127
159
|
*/
|
|
128
|
-
discoverPackages(): DiscoveredTypertPackage[];
|
|
160
|
+
discoverPackages(includeManifest?: (manifest: Readonly<Record<string, unknown>>) => boolean): DiscoveredTypertPackage[];
|
|
129
161
|
/**
|
|
130
162
|
* Index top-level exported type declarations without promoting them to graph
|
|
131
163
|
* roots. Consumers use this lexical index for ambiguity checks while all
|
package/lib/types/analyzer.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import ts from 'typescript';
|
|
10
|
+
import { findBuildOutputLoads, findUnresolvedWorkspaceImports } from "./build-output-guard.js";
|
|
10
11
|
const PUBLIC_REMOTE_TYPE_ROOTS = new Set([
|
|
11
12
|
'@deepseek-ai/dsh-util-values',
|
|
12
13
|
]);
|
|
@@ -49,6 +50,7 @@ export class WorkspaceCaches {
|
|
|
49
50
|
configs = new Map();
|
|
50
51
|
/** Registration inventories keyed by root and aggregate config paths. */
|
|
51
52
|
registrations = new Map();
|
|
53
|
+
aggregates = new Map();
|
|
52
54
|
hosts = new Map();
|
|
53
55
|
/**
|
|
54
56
|
* Parse one tsconfig once per workspace snapshot.
|
|
@@ -63,6 +65,22 @@ export class WorkspaceCaches {
|
|
|
63
65
|
}
|
|
64
66
|
return parsed;
|
|
65
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Read one face aggregate tsconfig once per workspace snapshot. An aggregate
|
|
70
|
+
* that declares `references` is parsed without expanding its `include`
|
|
71
|
+
* patterns, which affect neither its options nor its references.
|
|
72
|
+
* @param path - absolute aggregate config path.
|
|
73
|
+
* @returns the memoized compiler options and project references.
|
|
74
|
+
*/
|
|
75
|
+
aggregate(path) {
|
|
76
|
+
let aggregate = this.aggregates.get(path);
|
|
77
|
+
if (aggregate === undefined) {
|
|
78
|
+
const { parsed } = parseConfig(path, true);
|
|
79
|
+
aggregate = { path, options: parsed.options, projectReferences: parsed.projectReferences ?? [] };
|
|
80
|
+
this.aggregates.set(path, aggregate);
|
|
81
|
+
}
|
|
82
|
+
return aggregate;
|
|
83
|
+
}
|
|
66
84
|
/**
|
|
67
85
|
* Return the shared compiler host for one face. Every program of one face
|
|
68
86
|
* is built from the same aggregate compiler options (the first call wins),
|
|
@@ -131,6 +149,7 @@ export class WorkspaceAnalyzer {
|
|
|
131
149
|
faces: options.faces ?? ['host', 'client'],
|
|
132
150
|
checkDiagnostics: options.checkDiagnostics ?? true,
|
|
133
151
|
mode: options.mode ?? 'check',
|
|
152
|
+
rejectBuildOutputs: options.rejectBuildOutputs ?? false,
|
|
134
153
|
...(options.packages === undefined ? {} : { packages: options.packages }),
|
|
135
154
|
};
|
|
136
155
|
this.caches = options.caches ?? new WorkspaceCaches();
|
|
@@ -156,16 +175,19 @@ export class WorkspaceAnalyzer {
|
|
|
156
175
|
this.checkProject(registration);
|
|
157
176
|
}
|
|
158
177
|
const aggregatePath = resolve(this.options.root, face === 'host' ? this.options.hostConfig : this.options.clientConfig);
|
|
159
|
-
const aggregate = this.caches.
|
|
178
|
+
const aggregate = this.caches.aggregate(aggregatePath);
|
|
160
179
|
const rootNames = [...new Set(registrations.flatMap(registration => registration.config.parsed.fileNames))];
|
|
161
180
|
const options = {
|
|
162
|
-
...aggregate.
|
|
181
|
+
...aggregate.options,
|
|
163
182
|
composite: false,
|
|
164
183
|
incremental: false,
|
|
165
184
|
noEmit: true,
|
|
166
185
|
};
|
|
167
186
|
const host = this.caches.programHost(face, options);
|
|
168
187
|
const program = ts.createProgram({ rootNames, options, host });
|
|
188
|
+
if (this.options.rejectBuildOutputs) {
|
|
189
|
+
rejectBuildOutputLoads(face, this.options.root, aggregatePath, findBuildOutputLoads(program, this.options.root, host), findUnresolvedWorkspaceImports(program, this.options.root, host));
|
|
190
|
+
}
|
|
169
191
|
faces.push(new FaceAnalyzer({
|
|
170
192
|
root: this.options.root,
|
|
171
193
|
face,
|
|
@@ -222,11 +244,13 @@ export class WorkspaceAnalyzer {
|
|
|
222
244
|
/**
|
|
223
245
|
* Discover package faces from public-export-reachable Cordis augmentations
|
|
224
246
|
* and explicit `@typert` roots without constructing a type-checker program.
|
|
247
|
+
* @param includeManifest - optional `package.json` predicate; a rejected package's sources and project config are not read.
|
|
225
248
|
* @returns contributors grouped by package with deterministic face order.
|
|
226
249
|
*/
|
|
227
|
-
discoverPackages() {
|
|
250
|
+
discoverPackages(includeManifest) {
|
|
228
251
|
const registrations = this.loadRegistrations()
|
|
229
|
-
.filter(registration => this.options.faces.includes(registration.face)
|
|
252
|
+
.filter(registration => this.options.faces.includes(registration.face)
|
|
253
|
+
&& (includeManifest === undefined || includeManifest(registration.manifest)))
|
|
230
254
|
.filter(registration => this.registrationHasSurface(registration));
|
|
231
255
|
const packages = new Map();
|
|
232
256
|
for (const registration of registrations) {
|
|
@@ -307,8 +331,8 @@ export class WorkspaceAnalyzer {
|
|
|
307
331
|
const aggregatePath = resolve(this.options.root, face === 'host' ? this.options.hostConfig : this.options.clientConfig);
|
|
308
332
|
if (!existsSync(aggregatePath))
|
|
309
333
|
continue;
|
|
310
|
-
const aggregate = this.caches.
|
|
311
|
-
for (const reference of aggregate.
|
|
334
|
+
const aggregate = this.caches.aggregate(aggregatePath);
|
|
335
|
+
for (const reference of aggregate.projectReferences) {
|
|
312
336
|
const configPath = projectConfigPath(reference.path);
|
|
313
337
|
const packageRoot = dirname(configPath);
|
|
314
338
|
if (!isWithin(realPath(packageRoot), join(this.options.root, 'packages'))
|
|
@@ -318,28 +342,31 @@ export class WorkspaceAnalyzer {
|
|
|
318
342
|
if (!existsSync(manifestPath))
|
|
319
343
|
continue;
|
|
320
344
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
321
|
-
|
|
345
|
+
const name = manifest.name;
|
|
346
|
+
if (typeof name !== 'string')
|
|
322
347
|
continue;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
348
|
+
const root = realPath(packageRoot);
|
|
349
|
+
const caches = this.caches;
|
|
350
|
+
// The project config is parsed on first use, so discovery and analysis
|
|
351
|
+
// read only the projects they select.
|
|
352
|
+
const register = (registrationFace, exportSubpaths) => ({
|
|
353
|
+
face: registrationFace,
|
|
354
|
+
name,
|
|
355
|
+
root,
|
|
356
|
+
get config() { return caches.config(configPath); },
|
|
328
357
|
manifest,
|
|
329
|
-
|
|
358
|
+
...(exportSubpaths === undefined ? {} : { exportSubpaths }),
|
|
359
|
+
});
|
|
330
360
|
if (!isDualFacePackage(manifest)) {
|
|
331
|
-
registrations.push(
|
|
361
|
+
registrations.push(register(face));
|
|
332
362
|
}
|
|
333
363
|
else if (configPath === join(packageRoot, 'tsconfig.json')) {
|
|
334
|
-
registrations.push(
|
|
364
|
+
registrations.push(register('host', hostExportSubpaths(manifest)), register('client', clientExportSubpaths(manifest)));
|
|
335
365
|
}
|
|
336
366
|
else {
|
|
337
|
-
registrations.push(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
? hostExportSubpaths(manifest)
|
|
341
|
-
: clientExportSubpaths(manifest),
|
|
342
|
-
});
|
|
367
|
+
registrations.push(register(face, face === 'host'
|
|
368
|
+
? hostExportSubpaths(manifest)
|
|
369
|
+
: clientExportSubpaths(manifest)));
|
|
343
370
|
}
|
|
344
371
|
}
|
|
345
372
|
}
|
|
@@ -437,6 +464,9 @@ class FaceAnalyzer {
|
|
|
437
464
|
nodes = new Map();
|
|
438
465
|
exportsByPackage = new Map();
|
|
439
466
|
nodeOrdinals = new Map();
|
|
467
|
+
/** Registration and package lookups per source path; the registration set is fixed per analyzer. */
|
|
468
|
+
registrationByFile = new Map();
|
|
469
|
+
packageNameByFile = new Map();
|
|
440
470
|
staticLookups;
|
|
441
471
|
staticContexts;
|
|
442
472
|
constructor(options) {
|
|
@@ -867,6 +897,7 @@ class FaceAnalyzer {
|
|
|
867
897
|
if ((method.typeParameters?.length ?? 0) > 0) {
|
|
868
898
|
this.fail(method, 'generic Remote methods are not supported');
|
|
869
899
|
}
|
|
900
|
+
const documentation = documentationOf(method);
|
|
870
901
|
const methodName = method.name.text;
|
|
871
902
|
const exportedMethod = invocation.exportName ?? methodName;
|
|
872
903
|
const lookups = this.lookupDeclarations();
|
|
@@ -931,7 +962,8 @@ class FaceAnalyzer {
|
|
|
931
962
|
if (wires.has(modeled.wire))
|
|
932
963
|
this.fail(parameter, `duplicate Remote wire field ${modeled.wire}`);
|
|
933
964
|
wires.add(modeled.wire);
|
|
934
|
-
|
|
965
|
+
const description = documentation.tags.find(tag => tag.name === 'param' && tag.argument === modeled.name)?.comment?.replace(/^[-—–]\s*/, '');
|
|
966
|
+
parameters.push({ ...modeled, ...(description === undefined ? {} : { description }) });
|
|
935
967
|
}
|
|
936
968
|
let receiver = { kind: 'direct' };
|
|
937
969
|
if (invocation.kind === 'context') {
|
|
@@ -972,6 +1004,7 @@ class FaceAnalyzer {
|
|
|
972
1004
|
boundary: this.remoteBoundary(uplinkType, `${registration.name}#${binding.namespace}/${exportedMethod}:uplink`, false, 'undefined'),
|
|
973
1005
|
};
|
|
974
1006
|
return {
|
|
1007
|
+
...documentation,
|
|
975
1008
|
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
976
1009
|
service: binding.service,
|
|
977
1010
|
namespace: binding.namespace,
|
|
@@ -1098,35 +1131,29 @@ class FaceAnalyzer {
|
|
|
1098
1131
|
marker = { kind: 'direct', exportName };
|
|
1099
1132
|
}
|
|
1100
1133
|
else {
|
|
1101
|
-
|
|
1102
|
-
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1103
|
-
}
|
|
1104
|
-
const [property] = argument.properties;
|
|
1105
|
-
if (property === undefined)
|
|
1106
|
-
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1107
|
-
const mode = ts.isPropertyAssignment(property) && memberName(property.name) === 'mode'
|
|
1108
|
-
? stringLiteralValue(property.initializer)
|
|
1109
|
-
: undefined;
|
|
1110
|
-
if (mode !== 'stream')
|
|
1111
|
-
this.fail(property, 'Remote() options must contain exactly mode: "stream"');
|
|
1112
|
-
marker = { kind: 'direct', mode };
|
|
1134
|
+
marker = { kind: 'direct', ...this.remoteOptions(argument, 'Remote') };
|
|
1113
1135
|
}
|
|
1114
1136
|
}
|
|
1115
1137
|
else if (ts.isCallExpression(expression)
|
|
1116
1138
|
&& this.isTypeMetaSymbol(expression.expression, 'RemoteScope')) {
|
|
1117
1139
|
if (expression.arguments.length < 1 || expression.arguments.length > 2) {
|
|
1118
|
-
this.fail(expression, 'RemoteScope() requires a Context key and optional exported method name');
|
|
1140
|
+
this.fail(expression, 'RemoteScope() requires a Context key and an optional exported method name or options object');
|
|
1119
1141
|
}
|
|
1120
1142
|
const context = stringLiteralValue(expression.arguments[0]);
|
|
1121
1143
|
if (context === undefined || !isRemoteSegment(context)) {
|
|
1122
1144
|
this.fail(expression.arguments[0] ?? expression, 'RemoteScope() key must be a string literal containing only RPC endpoint segment characters');
|
|
1123
1145
|
}
|
|
1124
|
-
const
|
|
1125
|
-
const exportName =
|
|
1126
|
-
if (
|
|
1127
|
-
|
|
1146
|
+
const option = expression.arguments[1];
|
|
1147
|
+
const exportName = option === undefined ? undefined : stringLiteralValue(option);
|
|
1148
|
+
if (option === undefined || exportName !== undefined) {
|
|
1149
|
+
if (exportName !== undefined && !isRemoteSegment(exportName)) {
|
|
1150
|
+
this.fail(option ?? expression, 'RemoteScope() name must be a string literal containing only RPC endpoint segment characters');
|
|
1151
|
+
}
|
|
1152
|
+
marker = { kind: 'context', context, ...exportName === undefined ? {} : { exportName } };
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
marker = { kind: 'context', context, ...this.remoteOptions(option, 'RemoteScope') };
|
|
1128
1156
|
}
|
|
1129
|
-
marker = { kind: 'context', context, ...exportName === undefined ? {} : { exportName } };
|
|
1130
1157
|
}
|
|
1131
1158
|
else {
|
|
1132
1159
|
continue;
|
|
@@ -1137,6 +1164,38 @@ class FaceAnalyzer {
|
|
|
1137
1164
|
}
|
|
1138
1165
|
return found;
|
|
1139
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Read the options object of `@Remote` or `@RemoteScope`. `resources`
|
|
1169
|
+
* accepts any expression: its readers run only at runtime, where the
|
|
1170
|
+
* Gateway reads them from the prototype descriptor, so generation records
|
|
1171
|
+
* nothing for them.
|
|
1172
|
+
*/
|
|
1173
|
+
remoteOptions(argument, decorator) {
|
|
1174
|
+
const requirement = decorator === 'Remote'
|
|
1175
|
+
? 'Remote() options require name, mode: "stream", or resources'
|
|
1176
|
+
: 'RemoteScope() options require name or resources';
|
|
1177
|
+
if (!ts.isObjectLiteralExpression(argument) || argument.properties.length === 0)
|
|
1178
|
+
this.fail(argument, requirement);
|
|
1179
|
+
let options = {};
|
|
1180
|
+
for (const property of argument.properties) {
|
|
1181
|
+
if (!ts.isPropertyAssignment(property))
|
|
1182
|
+
this.fail(property, `${decorator}() options must use named values`);
|
|
1183
|
+
const name = memberName(property.name);
|
|
1184
|
+
if (decorator === 'Remote' && name === 'mode' && stringLiteralValue(property.initializer) === 'stream') {
|
|
1185
|
+
options = { ...options, mode: 'stream' };
|
|
1186
|
+
}
|
|
1187
|
+
else if (name === 'name') {
|
|
1188
|
+
const value = stringLiteralValue(property.initializer);
|
|
1189
|
+
if (value === undefined || !isRemoteSegment(value))
|
|
1190
|
+
this.fail(property, `${decorator}() name must be an endpoint segment`);
|
|
1191
|
+
options = { ...options, exportName: value };
|
|
1192
|
+
}
|
|
1193
|
+
else if (name !== 'resources') {
|
|
1194
|
+
this.fail(property, requirement);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return options;
|
|
1198
|
+
}
|
|
1140
1199
|
/**
|
|
1141
1200
|
* The item types a Remote method's authored return type declares. Unary
|
|
1142
1201
|
* methods unwrap `Promise<T>`; stream methods unwrap `Iterable<Out>`,
|
|
@@ -1356,6 +1415,12 @@ class FaceAnalyzer {
|
|
|
1356
1415
|
completed.set(type, id);
|
|
1357
1416
|
return id;
|
|
1358
1417
|
};
|
|
1418
|
+
const alias = type.aliasSymbol;
|
|
1419
|
+
const aliasDeclaration = alias === undefined ? undefined : preferredDeclaration(this.resolveSymbol(alias));
|
|
1420
|
+
if (alias?.name === 'JsonValue' && aliasDeclaration !== undefined
|
|
1421
|
+
&& this.registrationForFile(aliasDeclaration.getSourceFile().fileName)?.name === '@deepseek-ai/dsh-util-values') {
|
|
1422
|
+
return add({ kind: 'keyword', name: 'unknown', json: true });
|
|
1423
|
+
}
|
|
1359
1424
|
const flags = type.flags;
|
|
1360
1425
|
if (this.isRemoteByteArray(type)) {
|
|
1361
1426
|
return add({ kind: 'reference', name: 'Uint8Array', target: { kind: 'standard', name: 'Uint8Array' }, arguments: [] });
|
|
@@ -1399,9 +1464,23 @@ class FaceAnalyzer {
|
|
|
1399
1464
|
const value = type.intrinsicName === 'true';
|
|
1400
1465
|
return add({ kind: 'literal', value, text: String(value) });
|
|
1401
1466
|
}
|
|
1402
|
-
if (type.
|
|
1467
|
+
if (type.isIntersection()) {
|
|
1468
|
+
const material = type.types.filter(member => !this.isRemotePhantomConstraint(member));
|
|
1469
|
+
if (material.length === 1) {
|
|
1470
|
+
const projected = convert(material[0]);
|
|
1471
|
+
const { id: _projectedId, ...value } = this.nodes.get(projected);
|
|
1472
|
+
return add(value);
|
|
1473
|
+
}
|
|
1474
|
+
// Object intersections contribute one request object; closing each operand
|
|
1475
|
+
// separately would reject fields declared by its sibling operands.
|
|
1476
|
+
if (!material.every(member => (member.flags & ts.TypeFlags.Object) !== 0
|
|
1477
|
+
&& !this.checker.isArrayType(member) && !this.checker.isTupleType(member))) {
|
|
1478
|
+
return add({ kind: 'intersection', types: material.map(convert) });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
else if (type.isUnion()) {
|
|
1403
1482
|
return add({
|
|
1404
|
-
kind:
|
|
1483
|
+
kind: 'union',
|
|
1405
1484
|
types: type.types.map(convert),
|
|
1406
1485
|
});
|
|
1407
1486
|
}
|
|
@@ -1444,7 +1523,7 @@ class FaceAnalyzer {
|
|
|
1444
1523
|
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, declaration ?? authoredType);
|
|
1445
1524
|
const symbolKey = property.getName();
|
|
1446
1525
|
members.push({
|
|
1447
|
-
...EMPTY_DOCUMENTATION,
|
|
1526
|
+
...(declaration === undefined ? EMPTY_DOCUMENTATION : documentationOf(declaration)),
|
|
1448
1527
|
id: `${id}#${symbolKey}`,
|
|
1449
1528
|
name: symbolKey,
|
|
1450
1529
|
...(symbolKey.startsWith('__@') ? { computed: 'symbol' } : {}),
|
|
@@ -2337,13 +2416,23 @@ class FaceAnalyzer {
|
|
|
2337
2416
|
return `${this.packageNameForFile(declaration.getSourceFile().fileName)}:${location.file}#${symbol.name}`;
|
|
2338
2417
|
}
|
|
2339
2418
|
registrationForFile(file) {
|
|
2419
|
+
if (this.registrationByFile.has(file))
|
|
2420
|
+
return this.registrationByFile.get(file);
|
|
2340
2421
|
const path = realPath(file);
|
|
2341
|
-
|
|
2422
|
+
const registration = this.allRegistrations
|
|
2342
2423
|
.find(registration => registration.face === this.face && isWithin(path, registration.root));
|
|
2424
|
+
this.registrationByFile.set(file, registration);
|
|
2425
|
+
return registration;
|
|
2343
2426
|
}
|
|
2344
2427
|
packageNameForFile(file) {
|
|
2428
|
+
const cached = this.packageNameByFile.get(file);
|
|
2429
|
+
if (cached !== undefined)
|
|
2430
|
+
return cached;
|
|
2345
2431
|
const path = realPath(file);
|
|
2346
|
-
|
|
2432
|
+
const name = this.allRegistrations
|
|
2433
|
+
.find(registration => isWithin(path, registration.root))?.name ?? '<external>';
|
|
2434
|
+
this.packageNameByFile.set(file, name);
|
|
2435
|
+
return name;
|
|
2347
2436
|
}
|
|
2348
2437
|
allocateNodeId(site) {
|
|
2349
2438
|
const location = this.locationKey(site);
|
|
@@ -2414,12 +2503,26 @@ function mergeWorkspaceModels(models) {
|
|
|
2414
2503
|
crossFaceLinks: [...links.values()].sort(compareCrossFaceLinks),
|
|
2415
2504
|
};
|
|
2416
2505
|
}
|
|
2417
|
-
|
|
2506
|
+
/** {@link ts.sys} config reads without directory listing, so `include` patterns match no files. */
|
|
2507
|
+
const UNLISTED_CONFIG_HOST = {
|
|
2508
|
+
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
|
|
2509
|
+
readDirectory: () => [],
|
|
2510
|
+
fileExists: file => ts.sys.fileExists(file),
|
|
2511
|
+
readFile: file => ts.sys.readFile(file),
|
|
2512
|
+
};
|
|
2513
|
+
function parseConfig(path, aggregate = false) {
|
|
2418
2514
|
const compilerPath = path.split(sep).join('/');
|
|
2419
2515
|
const read = ts.readConfigFile(compilerPath, file => ts.sys.readFile(file));
|
|
2420
2516
|
if (read.error !== undefined)
|
|
2421
2517
|
throw new TypertAnalysisError(formatDiagnostic(read.error));
|
|
2422
|
-
|
|
2518
|
+
// Include expansion only fills `fileNames`, and TypeScript reports missing
|
|
2519
|
+
// inputs only for a config without `files` or `references`; skipping it for
|
|
2520
|
+
// an aggregate with `references` keeps diagnostics, options, and references.
|
|
2521
|
+
const raw = read.config;
|
|
2522
|
+
const host = aggregate && typeof raw === 'object' && raw !== null && Object.hasOwn(raw, 'references')
|
|
2523
|
+
? UNLISTED_CONFIG_HOST
|
|
2524
|
+
: ts.sys;
|
|
2525
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, host, dirname(compilerPath), undefined, compilerPath);
|
|
2423
2526
|
if (parsed.errors.length > 0)
|
|
2424
2527
|
throw new TypertAnalysisError(parsed.errors.map(formatDiagnostic).join('\n'));
|
|
2425
2528
|
return { path, parsed };
|
|
@@ -2587,7 +2690,9 @@ function declarationText(declaration) {
|
|
|
2587
2690
|
function classShape(node) {
|
|
2588
2691
|
const nonPublic = (member) => (ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined)?.some(modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword) ?? false;
|
|
2589
2692
|
const members = node.members.flatMap((member) => {
|
|
2590
|
-
|
|
2693
|
+
const name = ts.getNameOfDeclaration(member);
|
|
2694
|
+
if (ts.isClassStaticBlockDeclaration(member) || nonPublic(member)
|
|
2695
|
+
|| (name !== undefined && ts.isPrivateIdentifier(name)))
|
|
2591
2696
|
return [];
|
|
2592
2697
|
if (ts.isMethodDeclaration(member)) {
|
|
2593
2698
|
return [ts.factory.updateMethodDeclaration(member, member.modifiers, member.asteriskToken, member.name, member.questionToken, member.typeParameters, member.parameters, member.type, undefined)];
|
|
@@ -2865,8 +2970,14 @@ function formatProgramDiagnostic(root, face, diagnostic) {
|
|
|
2865
2970
|
return `typert(${face}): ${file}:${String(position.line + 1)}:${String(position.character + 1)}: TypeScript TS${String(diagnostic.code)}: ${message}`;
|
|
2866
2971
|
}
|
|
2867
2972
|
const realPathCache = new Map();
|
|
2973
|
+
/** Normalized form per requested path; `resolve` reads only the process working directory. */
|
|
2974
|
+
const absolutePathCache = new Map();
|
|
2868
2975
|
function realPath(path) {
|
|
2869
|
-
|
|
2976
|
+
let absolute = absolutePathCache.get(path);
|
|
2977
|
+
if (absolute === undefined) {
|
|
2978
|
+
absolute = resolve(path);
|
|
2979
|
+
absolutePathCache.set(path, absolute);
|
|
2980
|
+
}
|
|
2870
2981
|
const cached = realPathCache.get(absolute);
|
|
2871
2982
|
if (cached !== undefined)
|
|
2872
2983
|
return cached;
|
|
@@ -2887,6 +2998,47 @@ function isWithin(path, root) {
|
|
|
2887
2998
|
function slash(value) {
|
|
2888
2999
|
return value.replaceAll('\\', '/');
|
|
2889
3000
|
}
|
|
3001
|
+
function rejectBuildOutputLoads(face, root, aggregatePath, loads, unresolved) {
|
|
3002
|
+
if (loads.length === 0 && unresolved.length === 0)
|
|
3003
|
+
return;
|
|
3004
|
+
const path = (file) => slash(relative(root, file));
|
|
3005
|
+
const config = path(aggregatePath);
|
|
3006
|
+
const lines = [];
|
|
3007
|
+
const addAlias = (specifier, source) => {
|
|
3008
|
+
lines.push(` add ${JSON.stringify(specifier)}: [${JSON.stringify(`./${path(source)}`)}] to the compilerOptions.paths of ${config} or the config it extends`);
|
|
3009
|
+
};
|
|
3010
|
+
if (unresolved.length > 0) {
|
|
3011
|
+
lines.push(`typert(${face}): the ${face} program cannot resolve workspace imports, which would analyze as unknown types; map each import below to source:`);
|
|
3012
|
+
for (const entry of unresolved) {
|
|
3013
|
+
lines.push(` '${entry.specifier}' imported by ${path(entry.file)}`);
|
|
3014
|
+
if (entry.source === undefined)
|
|
3015
|
+
lines.push(` add a compilerOptions.paths alias for it to ${config} or the config it extends`);
|
|
3016
|
+
else
|
|
3017
|
+
addAlias(entry.specifier, entry.source);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
if (loads.length > 0) {
|
|
3021
|
+
lines.push(`typert(${face}): the ${face} program loads workspace build outputs instead of source; resolve each import below to source so that generation does not depend on package compilation:`);
|
|
3022
|
+
}
|
|
3023
|
+
for (const load of loads) {
|
|
3024
|
+
lines.push(` ${path(load.file)}`);
|
|
3025
|
+
if (load.importers.length === 0) {
|
|
3026
|
+
lines.push(' no program source imports it directly; another build output or a reference directive loads it');
|
|
3027
|
+
}
|
|
3028
|
+
for (const importer of load.importers) {
|
|
3029
|
+
lines.push(` imported as '${importer.specifier}' by ${path(importer.file)}`);
|
|
3030
|
+
}
|
|
3031
|
+
const specifiers = [...new Set(load.importers.map(importer => importer.specifier))];
|
|
3032
|
+
if (load.source === undefined) {
|
|
3033
|
+
lines.push(' no workspace source compiles to it; import a source module instead');
|
|
3034
|
+
}
|
|
3035
|
+
else {
|
|
3036
|
+
for (const specifier of specifiers)
|
|
3037
|
+
addAlias(specifier, load.source);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
throw new TypertAnalysisError(lines.join('\n'));
|
|
3041
|
+
}
|
|
2890
3042
|
function uniqueBy(values, key) {
|
|
2891
3043
|
const result = new Map();
|
|
2892
3044
|
for (const value of values)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-local files of generated Typert artifacts. The tsdown plugin and
|
|
3
|
+
* standalone workspace generation both write through this module, so a pass
|
|
4
|
+
* over the same artifacts leaves the same bytes on disk.
|
|
5
|
+
* @module @deepseek-ai/dsh-typert-generator/artifact-files
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkspaceEmitResult } from './workspace.ts';
|
|
8
|
+
/**
|
|
9
|
+
* Write one package's artifacts under `<packageDir>/lib`: `typert.<face>.js`
|
|
10
|
+
* and `typert.<face>.d.ts` per artifact, plus the three
|
|
11
|
+
* `typert.remote-client.*` files when an artifact carries Remote methods. A
|
|
12
|
+
* Host artifact without Remote methods removes any Remote client files left
|
|
13
|
+
* by an earlier pass. Files are written one by one, so a failed write can
|
|
14
|
+
* leave this package's outputs partially replaced.
|
|
15
|
+
* @param packageDir - absolute package root.
|
|
16
|
+
* @param artifacts - artifacts of that package only.
|
|
17
|
+
* @throws A file-system error when the output directory or a file cannot be written.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writePackageArtifacts(packageDir: string, artifacts: readonly WorkspaceEmitResult[]): void;
|
|
20
|
+
/**
|
|
21
|
+
* Write every artifact of one workspace pass into its own package, in
|
|
22
|
+
* artifact order, with {@link writePackageArtifacts}. A failed write stops
|
|
23
|
+
* the pass; earlier packages keep their new files and later ones keep their
|
|
24
|
+
* previous files.
|
|
25
|
+
* @param root - workspace root that each artifact's `packageRoot` is relative to.
|
|
26
|
+
* @param artifacts - artifacts returned by one `WorkspaceTypertGenerator` pass.
|
|
27
|
+
* @throws A file-system error when an output directory or file cannot be written.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeWorkspaceArtifacts(root: string, artifacts: readonly WorkspaceEmitResult[]): void;
|
|
30
|
+
//# sourceMappingURL=artifact-files.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-local files of generated Typert artifacts. The tsdown plugin and
|
|
3
|
+
* standalone workspace generation both write through this module, so a pass
|
|
4
|
+
* over the same artifacts leaves the same bytes on disk.
|
|
5
|
+
* @module @deepseek-ai/dsh-typert-generator/artifact-files
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
const REMOTE_CLIENT_FILES = [
|
|
10
|
+
'typert.remote-client.js',
|
|
11
|
+
'typert.remote-client.d.ts',
|
|
12
|
+
'typert.remote-client.d.ts.map',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Write one package's artifacts under `<packageDir>/lib`: `typert.<face>.js`
|
|
16
|
+
* and `typert.<face>.d.ts` per artifact, plus the three
|
|
17
|
+
* `typert.remote-client.*` files when an artifact carries Remote methods. A
|
|
18
|
+
* Host artifact without Remote methods removes any Remote client files left
|
|
19
|
+
* by an earlier pass. Files are written one by one, so a failed write can
|
|
20
|
+
* leave this package's outputs partially replaced.
|
|
21
|
+
* @param packageDir - absolute package root.
|
|
22
|
+
* @param artifacts - artifacts of that package only.
|
|
23
|
+
* @throws A file-system error when the output directory or a file cannot be written.
|
|
24
|
+
*/
|
|
25
|
+
export function writePackageArtifacts(packageDir, artifacts) {
|
|
26
|
+
const output = join(packageDir, 'lib');
|
|
27
|
+
mkdirSync(output, { recursive: true });
|
|
28
|
+
let emittedRemote = false;
|
|
29
|
+
for (const artifact of artifacts) {
|
|
30
|
+
writeFileSync(join(output, `typert.${artifact.face}.js`), artifact.js);
|
|
31
|
+
writeFileSync(join(output, `typert.${artifact.face}.d.ts`), artifact.dts);
|
|
32
|
+
if (artifact.remote !== undefined) {
|
|
33
|
+
emittedRemote = true;
|
|
34
|
+
writeFileSync(join(output, 'typert.remote-client.js'), artifact.remote.js);
|
|
35
|
+
writeFileSync(join(output, 'typert.remote-client.d.ts'), artifact.remote.dts);
|
|
36
|
+
writeFileSync(join(output, 'typert.remote-client.d.ts.map'), artifact.remote.dtsMap);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!emittedRemote && artifacts.some(artifact => artifact.face === 'host')) {
|
|
40
|
+
for (const file of REMOTE_CLIENT_FILES)
|
|
41
|
+
rmSync(join(output, file), { force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Write every artifact of one workspace pass into its own package, in
|
|
46
|
+
* artifact order, with {@link writePackageArtifacts}. A failed write stops
|
|
47
|
+
* the pass; earlier packages keep their new files and later ones keep their
|
|
48
|
+
* previous files.
|
|
49
|
+
* @param root - workspace root that each artifact's `packageRoot` is relative to.
|
|
50
|
+
* @param artifacts - artifacts returned by one `WorkspaceTypertGenerator` pass.
|
|
51
|
+
* @throws A file-system error when an output directory or file cannot be written.
|
|
52
|
+
*/
|
|
53
|
+
export function writeWorkspaceArtifacts(root, artifacts) {
|
|
54
|
+
for (const artifact of artifacts)
|
|
55
|
+
writePackageArtifacts(join(root, artifact.packageRoot), [artifact]);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=artifact-files.js.map
|