@deepseek-ai/dsh-typert-generator 0.2.1-alpha.2 → 0.2.1-alpha.4
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 +693 -101
- package/lib/types/analyzer.d.ts +34 -2
- package/lib/types/analyzer.js +176 -45
- 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 +171 -72
- 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
|
}
|
|
@@ -870,6 +897,7 @@ class FaceAnalyzer {
|
|
|
870
897
|
if ((method.typeParameters?.length ?? 0) > 0) {
|
|
871
898
|
this.fail(method, 'generic Remote methods are not supported');
|
|
872
899
|
}
|
|
900
|
+
const documentation = documentationOf(method);
|
|
873
901
|
const methodName = method.name.text;
|
|
874
902
|
const exportedMethod = invocation.exportName ?? methodName;
|
|
875
903
|
const lookups = this.lookupDeclarations();
|
|
@@ -934,7 +962,8 @@ class FaceAnalyzer {
|
|
|
934
962
|
if (wires.has(modeled.wire))
|
|
935
963
|
this.fail(parameter, `duplicate Remote wire field ${modeled.wire}`);
|
|
936
964
|
wires.add(modeled.wire);
|
|
937
|
-
|
|
965
|
+
const description = documentation.tags.find(tag => tag.name === 'param' && tag.argument === modeled.name)?.comment?.replace(/^[-—–]\s*/, '');
|
|
966
|
+
parameters.push({ ...modeled, ...(description === undefined ? {} : { description }) });
|
|
938
967
|
}
|
|
939
968
|
let receiver = { kind: 'direct' };
|
|
940
969
|
if (invocation.kind === 'context') {
|
|
@@ -975,6 +1004,7 @@ class FaceAnalyzer {
|
|
|
975
1004
|
boundary: this.remoteBoundary(uplinkType, `${registration.name}#${binding.namespace}/${exportedMethod}:uplink`, false, 'undefined'),
|
|
976
1005
|
};
|
|
977
1006
|
return {
|
|
1007
|
+
...documentation,
|
|
978
1008
|
id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
|
|
979
1009
|
service: binding.service,
|
|
980
1010
|
namespace: binding.namespace,
|
|
@@ -1101,35 +1131,29 @@ class FaceAnalyzer {
|
|
|
1101
1131
|
marker = { kind: 'direct', exportName };
|
|
1102
1132
|
}
|
|
1103
1133
|
else {
|
|
1104
|
-
|
|
1105
|
-
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1106
|
-
}
|
|
1107
|
-
const [property] = argument.properties;
|
|
1108
|
-
if (property === undefined)
|
|
1109
|
-
this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
|
|
1110
|
-
const mode = ts.isPropertyAssignment(property) && memberName(property.name) === 'mode'
|
|
1111
|
-
? stringLiteralValue(property.initializer)
|
|
1112
|
-
: undefined;
|
|
1113
|
-
if (mode !== 'stream')
|
|
1114
|
-
this.fail(property, 'Remote() options must contain exactly mode: "stream"');
|
|
1115
|
-
marker = { kind: 'direct', mode };
|
|
1134
|
+
marker = { kind: 'direct', ...this.remoteOptions(argument, 'Remote') };
|
|
1116
1135
|
}
|
|
1117
1136
|
}
|
|
1118
1137
|
else if (ts.isCallExpression(expression)
|
|
1119
1138
|
&& this.isTypeMetaSymbol(expression.expression, 'RemoteScope')) {
|
|
1120
1139
|
if (expression.arguments.length < 1 || expression.arguments.length > 2) {
|
|
1121
|
-
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');
|
|
1122
1141
|
}
|
|
1123
1142
|
const context = stringLiteralValue(expression.arguments[0]);
|
|
1124
1143
|
if (context === undefined || !isRemoteSegment(context)) {
|
|
1125
1144
|
this.fail(expression.arguments[0] ?? expression, 'RemoteScope() key must be a string literal containing only RPC endpoint segment characters');
|
|
1126
1145
|
}
|
|
1127
|
-
const
|
|
1128
|
-
const exportName =
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
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') };
|
|
1131
1156
|
}
|
|
1132
|
-
marker = { kind: 'context', context, ...exportName === undefined ? {} : { exportName } };
|
|
1133
1157
|
}
|
|
1134
1158
|
else {
|
|
1135
1159
|
continue;
|
|
@@ -1140,6 +1164,38 @@ class FaceAnalyzer {
|
|
|
1140
1164
|
}
|
|
1141
1165
|
return found;
|
|
1142
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
|
+
}
|
|
1143
1199
|
/**
|
|
1144
1200
|
* The item types a Remote method's authored return type declares. Unary
|
|
1145
1201
|
* methods unwrap `Promise<T>`; stream methods unwrap `Iterable<Out>`,
|
|
@@ -1359,6 +1415,12 @@ class FaceAnalyzer {
|
|
|
1359
1415
|
completed.set(type, id);
|
|
1360
1416
|
return id;
|
|
1361
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
|
+
}
|
|
1362
1424
|
const flags = type.flags;
|
|
1363
1425
|
if (this.isRemoteByteArray(type)) {
|
|
1364
1426
|
return add({ kind: 'reference', name: 'Uint8Array', target: { kind: 'standard', name: 'Uint8Array' }, arguments: [] });
|
|
@@ -1402,9 +1464,23 @@ class FaceAnalyzer {
|
|
|
1402
1464
|
const value = type.intrinsicName === 'true';
|
|
1403
1465
|
return add({ kind: 'literal', value, text: String(value) });
|
|
1404
1466
|
}
|
|
1405
|
-
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()) {
|
|
1406
1482
|
return add({
|
|
1407
|
-
kind:
|
|
1483
|
+
kind: 'union',
|
|
1408
1484
|
types: type.types.map(convert),
|
|
1409
1485
|
});
|
|
1410
1486
|
}
|
|
@@ -1447,7 +1523,7 @@ class FaceAnalyzer {
|
|
|
1447
1523
|
const propertyType = this.checker.getTypeOfSymbolAtLocation(property, declaration ?? authoredType);
|
|
1448
1524
|
const symbolKey = property.getName();
|
|
1449
1525
|
members.push({
|
|
1450
|
-
...EMPTY_DOCUMENTATION,
|
|
1526
|
+
...(declaration === undefined ? EMPTY_DOCUMENTATION : documentationOf(declaration)),
|
|
1451
1527
|
id: `${id}#${symbolKey}`,
|
|
1452
1528
|
name: symbolKey,
|
|
1453
1529
|
...(symbolKey.startsWith('__@') ? { computed: 'symbol' } : {}),
|
|
@@ -2427,12 +2503,26 @@ function mergeWorkspaceModels(models) {
|
|
|
2427
2503
|
crossFaceLinks: [...links.values()].sort(compareCrossFaceLinks),
|
|
2428
2504
|
};
|
|
2429
2505
|
}
|
|
2430
|
-
|
|
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) {
|
|
2431
2514
|
const compilerPath = path.split(sep).join('/');
|
|
2432
2515
|
const read = ts.readConfigFile(compilerPath, file => ts.sys.readFile(file));
|
|
2433
2516
|
if (read.error !== undefined)
|
|
2434
2517
|
throw new TypertAnalysisError(formatDiagnostic(read.error));
|
|
2435
|
-
|
|
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);
|
|
2436
2526
|
if (parsed.errors.length > 0)
|
|
2437
2527
|
throw new TypertAnalysisError(parsed.errors.map(formatDiagnostic).join('\n'));
|
|
2438
2528
|
return { path, parsed };
|
|
@@ -2908,6 +2998,47 @@ function isWithin(path, root) {
|
|
|
2908
2998
|
function slash(value) {
|
|
2909
2999
|
return value.replaceAll('\\', '/');
|
|
2910
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
|
+
}
|
|
2911
3042
|
function uniqueBy(values, key) {
|
|
2912
3043
|
const result = new Map();
|
|
2913
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
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of workspace build outputs loaded into an analyzed face program,
|
|
3
|
+
* and of workspace imports that resolve to nothing. Typert reads workspace
|
|
4
|
+
* packages only from source when a caller rejects build outputs, so its
|
|
5
|
+
* result cannot depend on whether, or when, package compilation ran: an
|
|
6
|
+
* import that reaches `lib/` on a compiled tree resolves to nothing on a
|
|
7
|
+
* clean one, and both are rejected.
|
|
8
|
+
* @module @deepseek-ai/dsh-typert-generator/build-output-guard
|
|
9
|
+
*/
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
/** One program file that a workspace package's `lib/` holds, with the imports that load it. */
|
|
12
|
+
export interface BuildOutputLoad {
|
|
13
|
+
/** Absolute path of the build output. */
|
|
14
|
+
readonly file: string;
|
|
15
|
+
/** Program files outside build outputs whose import specifier resolves to {@link file}. */
|
|
16
|
+
readonly importers: readonly BuildOutputImporter[];
|
|
17
|
+
/** Absolute path of the workspace source compiled to {@link file}, when one exists. */
|
|
18
|
+
readonly source: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** One import of a build output from a program file outside build outputs. */
|
|
21
|
+
export interface BuildOutputImporter {
|
|
22
|
+
/** Absolute path of the importing file. */
|
|
23
|
+
readonly file: string;
|
|
24
|
+
/** Import specifier as written. */
|
|
25
|
+
readonly specifier: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* List the program files that a workspace package holds under its `lib/`
|
|
29
|
+
* directory. A package is the nearest directory with a `package.json` at or
|
|
30
|
+
* below `root`; files under `node_modules` are external. Importers are found
|
|
31
|
+
* by resolving each program file's import specifiers again, so this pass
|
|
32
|
+
* costs extra only when an output is present.
|
|
33
|
+
* @param program - the analyzed face program.
|
|
34
|
+
* @param root - workspace root.
|
|
35
|
+
* @param host - module resolution host of the program.
|
|
36
|
+
* @returns loads in file-path order; empty when every workspace file comes from source.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findBuildOutputLoads(program: ts.Program, root: string, host: ts.ModuleResolutionHost): BuildOutputLoad[];
|
|
39
|
+
/** One import of a workspace package from a program source file that module resolution cannot resolve. */
|
|
40
|
+
export interface UnresolvedWorkspaceImport {
|
|
41
|
+
/** Absolute path of the importing file. */
|
|
42
|
+
readonly file: string;
|
|
43
|
+
/** Import specifier as written. */
|
|
44
|
+
readonly specifier: string;
|
|
45
|
+
/** Absolute path of the workspace source the specifier's package export compiles from, when one exists. */
|
|
46
|
+
readonly source: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* List the imports in workspace program files that name a workspace package
|
|
50
|
+
* and resolve to nothing. A workspace package is one whose `node_modules` link,
|
|
51
|
+
* found from the importing file's directories, leads inside `root` outside any
|
|
52
|
+
* `node_modules`; without that link or a `paths` alias the package cannot be
|
|
53
|
+
* identified, and type checking reports the import. The source of a subpath is
|
|
54
|
+
* derived from the package's `exports` `types` condition under `lib/types/`.
|
|
55
|
+
* @param program - the analyzed face program.
|
|
56
|
+
* @param root - workspace root.
|
|
57
|
+
* @param host - module resolution host of the program.
|
|
58
|
+
* @returns unresolved imports in file and specifier order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findUnresolvedWorkspaceImports(program: ts.Program, root: string, host: ts.ModuleResolutionHost): UnresolvedWorkspaceImport[];
|
|
61
|
+
//# sourceMappingURL=build-output-guard.d.ts.map
|