@deepseek-ai/dsh-typert-generator 0.2.1-alpha.2 → 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.
@@ -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
- /** The package's own parsed tsconfig. */
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
@@ -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.config(aggregatePath);
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.parsed.options,
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.config(aggregatePath);
311
- for (const reference of aggregate.parsed.projectReferences ?? []) {
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
- if (typeof manifest.name !== 'string')
345
+ const name = manifest.name;
346
+ if (typeof name !== 'string')
322
347
  continue;
323
- const registration = {
324
- face,
325
- name: manifest.name,
326
- root: realPath(packageRoot),
327
- config: this.caches.config(configPath),
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(registration);
361
+ registrations.push(register(face));
332
362
  }
333
363
  else if (configPath === join(packageRoot, 'tsconfig.json')) {
334
- registrations.push({ ...registration, face: 'host', exportSubpaths: hostExportSubpaths(manifest) }, { ...registration, face: 'client', exportSubpaths: clientExportSubpaths(manifest) });
364
+ registrations.push(register('host', hostExportSubpaths(manifest)), register('client', clientExportSubpaths(manifest)));
335
365
  }
336
366
  else {
337
- registrations.push({
338
- ...registration,
339
- exportSubpaths: face === 'host'
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
- parameters.push(modeled);
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
- if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) {
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 exportArgument = expression.arguments[1];
1128
- const exportName = exportArgument === undefined ? undefined : stringLiteralValue(exportArgument);
1129
- if (exportArgument !== undefined && (exportName === undefined || !isRemoteSegment(exportName))) {
1130
- this.fail(exportArgument, 'RemoteScope() name must be a string literal containing only RPC endpoint segment characters');
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.isUnionOrIntersection()) {
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: (flags & ts.TypeFlags.Union) !== 0 ? 'union' : 'intersection',
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
- function parseConfig(path) {
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
- const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(compilerPath), undefined, compilerPath);
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