@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.
@@ -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
  }
@@ -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
- 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 }) });
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
- if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) {
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 exportArgument = expression.arguments[1];
1125
- const exportName = exportArgument === undefined ? undefined : stringLiteralValue(exportArgument);
1126
- if (exportArgument !== undefined && (exportName === undefined || !isRemoteSegment(exportName))) {
1127
- 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') };
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.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()) {
1403
1482
  return add({
1404
- kind: (flags & ts.TypeFlags.Union) !== 0 ? 'union' : 'intersection',
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
- return this.allRegistrations
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
- return this.allRegistrations.find(registration => isWithin(path, registration.root))?.name ?? '<external>';
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
- 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) {
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
- 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);
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
- if (nonPublic(member) || (ts.isPropertyDeclaration(member) && ts.isPrivateIdentifier(member.name)))
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
- const absolute = resolve(path);
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