carrick 0.3.86 → 0.3.88

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.
@@ -33,6 +33,7 @@ import ts from 'typescript';
33
33
  import * as fs from 'node:fs';
34
34
  import * as path from 'node:path';
35
35
  import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
36
+ import { repairDanglingImports } from './repair-dangling.js';
36
37
  import { findDisqualifyingTopTypes, provenanceOf, } from './deep-walk.js';
37
38
  export function selfCheckStub(args) {
38
39
  const typesDir = path.join(args.stubDir, 'types');
@@ -56,7 +57,18 @@ export function selfCheckStub(args) {
56
57
  linked = true;
57
58
  }
58
59
  try {
59
- return runSelfCheck(args, treeFiles);
60
+ const first = runSelfCheck(args, treeFiles);
61
+ // carrick#1397: an emitted declaration that imports a module the checkout
62
+ // does not have makes every alias reaching that file unpublishable, however
63
+ // much of it resolved. Repair the file — drop the import, write `unknown`
64
+ // where its names were used — and check the tree again. The second pass is
65
+ // the verdict: it reads the repaired text, and a name the rewrite did not
66
+ // reach comes back as a `Cannot find name` diagnostic that
67
+ // `repairedNameFailures` folds back into the same dangling specifier.
68
+ const repaired = repairDanglingImports(first.internalFailuresByFile);
69
+ if (repaired.size === 0)
70
+ return first.records;
71
+ return runSelfCheck(args, treeFiles, repaired).records;
60
72
  }
61
73
  finally {
62
74
  // unlinkSync, not rmSync: the link target is a directory and rmSync
@@ -65,7 +77,7 @@ export function selfCheckStub(args) {
65
77
  fs.unlinkSync(linkPath);
66
78
  }
67
79
  }
68
- function runSelfCheck(args, treeFiles) {
80
+ function runSelfCheck(args, treeFiles, repaired) {
69
81
  const options = {
70
82
  noEmit: true,
71
83
  strict: true,
@@ -102,14 +114,25 @@ function runSelfCheck(args, treeFiles) {
102
114
  return entry;
103
115
  };
104
116
  for (const d of diagnostics) {
105
- if ((d.code !== 2307 && d.code !== 2792) || !d.file)
117
+ if (!d.file)
106
118
  continue;
119
+ const abs = path.resolve(d.file.fileName);
107
120
  const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ');
121
+ // A name the repair did not reach: the import that bound it is gone, so
122
+ // the module is no longer reported missing and only this diagnostic is
123
+ // left to say the tree is incomplete. Blamed on the specifier that bound
124
+ // the name, which is the sentence a reader can act on (carrick#1397).
125
+ const orphaned = repairedNameFailure(repaired, abs, d, msg);
126
+ if (orphaned) {
127
+ bucketIn(failuresByFile, abs).internal.add(orphaned);
128
+ continue;
129
+ }
130
+ if (d.code !== 2307 && d.code !== 2792)
131
+ continue;
108
132
  const m = /Cannot find module '([^']+)'/.exec(msg);
109
133
  if (!m)
110
134
  continue;
111
135
  const spec = m[1];
112
- const abs = path.resolve(d.file.fileName);
113
136
  // A surface diagnostic outside every alias statement (a file-level import,
114
137
  // a reference directive) is attributable to no alias and keeps the
115
138
  // service-wide file bucket: soundness over precision, the same fallback
@@ -158,7 +181,40 @@ function runSelfCheck(args, treeFiles) {
158
181
  surfaceFailuresByAlias,
159
182
  }));
160
183
  }
161
- return records;
184
+ const internalFailuresByFile = new Map();
185
+ for (const [file, failures] of failuresByFile) {
186
+ // The surface's own failures are a literal anchor's pasted text
187
+ // (carrick#1361), not a declaration with an import to drop.
188
+ if (file === surfaceAbs || failures.internal.size === 0)
189
+ continue;
190
+ // A file already repaired has had its turn: repairing it again would chase
191
+ // its own leftover diagnostics.
192
+ if (repaired?.has(file))
193
+ continue;
194
+ internalFailuresByFile.set(file, failures.internal);
195
+ }
196
+ return { records, internalFailuresByFile };
197
+ }
198
+ /**
199
+ * The specifier to blame for a `Cannot find name` diagnostic in a file this
200
+ * capture repaired, when the name is one the dropped import bound.
201
+ *
202
+ * The rewrite writes `unknown` at every type position it can reach; a use it
203
+ * cannot — and there should be none, since a position where `unknown` is not a
204
+ * type leaves the whole file untouched — would otherwise read as a clean tree,
205
+ * because the module that is missing is no longer imported to be reported.
206
+ * Fail closed: the alias keeps the refusal it had before the repair.
207
+ */
208
+ function repairedNameFailure(repaired, file, diagnostic, message) {
209
+ if (!repaired || diagnostic.code !== 2304)
210
+ return undefined;
211
+ const entry = repaired.get(file);
212
+ if (!entry)
213
+ return undefined;
214
+ const named = /Cannot find name '([^']+)'/.exec(message);
215
+ if (!named || !entry.names.includes(named[1]))
216
+ return undefined;
217
+ return entry.specifiers[0];
162
218
  }
163
219
  /**
164
220
  * Position -> the alias whose `export type` statement span covers it, for
@@ -12,6 +12,13 @@
12
12
  * - `expanded`: the fully *structural* form, with every named member type
13
13
  * inlined to its member structure, recursively.
14
14
  *
15
+ * An alias whose own type did not resolve produces neither: the tree is read
16
+ * with nothing installed, so a reference that leaves it lands on TypeScript's
17
+ * unresolved-reference placeholder, which the compiler prints as the reference
18
+ * text rather than as `any` (see `isUnresolvedReference`). Both forms are then
19
+ * the top type, which is what every downstream rule about an empty answer is
20
+ * written to refuse.
21
+ *
15
22
  * `type.getText(node, NoTruncation)` does NOT inline named members — the
16
23
  * compiler prints a referenced type by its symbol name when that symbol is in
17
24
  * scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
@@ -12,6 +12,13 @@
12
12
  * - `expanded`: the fully *structural* form, with every named member type
13
13
  * inlined to its member structure, recursively.
14
14
  *
15
+ * An alias whose own type did not resolve produces neither: the tree is read
16
+ * with nothing installed, so a reference that leaves it lands on TypeScript's
17
+ * unresolved-reference placeholder, which the compiler prints as the reference
18
+ * text rather than as `any` (see `isUnresolvedReference`). Both forms are then
19
+ * the top type, which is what every downstream rule about an empty answer is
20
+ * written to refuse.
21
+ *
15
22
  * `type.getText(node, NoTruncation)` does NOT inline named members — the
16
23
  * compiler prints a referenced type by its symbol name when that symbol is in
17
24
  * scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
@@ -26,7 +33,7 @@
26
33
  import * as path from 'node:path';
27
34
  import * as fs from 'node:fs';
28
35
  import { Project, Node } from 'ts-morph';
29
- import { expandTypeStructural } from './type-structural-expander.js';
36
+ import { expandTypeStructural, } from './type-structural-expander.js';
30
37
  export class DefinitionResolver {
31
38
  project;
32
39
  constructor(options) {
@@ -71,7 +78,10 @@ export class DefinitionResolver {
71
78
  }
72
79
  const results = [];
73
80
  for (const alias of aliases) {
74
- const result = this.resolveAlias(surface, alias);
81
+ const result = this.resolveAlias(surface, alias, {
82
+ program: stubProject.getProgram().compilerObject,
83
+ repoRoot: stubDir,
84
+ });
75
85
  if (result) {
76
86
  results.push(result);
77
87
  }
@@ -89,7 +99,7 @@ export class DefinitionResolver {
89
99
  /**
90
100
  * Resolve a single alias: the original text and the structural form.
91
101
  */
92
- resolveAlias(sourceFile, alias) {
102
+ resolveAlias(sourceFile, alias, origin) {
93
103
  const decl = sourceFile.getTypeAlias(alias) ??
94
104
  sourceFile.getInterface(alias) ??
95
105
  sourceFile.getClass(alias) ??
@@ -98,6 +108,12 @@ export class DefinitionResolver {
98
108
  return null;
99
109
  try {
100
110
  const type = decl.getType();
111
+ // An alias whose own type did not resolve answers the top type it IS,
112
+ // not the reference the compiler echoes for it (carrick#1444).
113
+ if (isUnresolvedReference(type)) {
114
+ this.log(`${alias} names a type this tree cannot resolve; answering any`);
115
+ return { type_alias: alias, definition: 'any', expanded: 'any' };
116
+ }
101
117
  // As-written form: prefer the alias target's own declaration (the real
102
118
  // `interface Order {...}` in the tree) over the surface's import-type
103
119
  // line, so named shapes read naturally. Fall back to the alias line for
@@ -118,7 +134,7 @@ export class DefinitionResolver {
118
134
  }
119
135
  }
120
136
  // Structural form — every named member inlined to its shape.
121
- const expanded = expandTypeStructural(type);
137
+ const expanded = expandTypeStructural(type, origin);
122
138
  return { type_alias: alias, definition, expanded };
123
139
  }
124
140
  catch (err) {
@@ -133,6 +149,29 @@ export class DefinitionResolver {
133
149
  console.error(`[sidecar:definition-resolver:error] ${message}`);
134
150
  }
135
151
  }
152
+ /**
153
+ * True when a type is TypeScript's unresolved-reference placeholder:
154
+ * `TypeFlags.Any` carrying the internal `intrinsicName === 'error'` (stable
155
+ * since TS 1.x). `capture/deep-walk.ts` tests the same two facts for the same
156
+ * reason; the seam forbids importing it from here, so the pin is that both read
157
+ * the flag and the intrinsic name and nothing else.
158
+ *
159
+ * The compiler prints this placeholder as the reference text it FAILED to
160
+ * resolve, never as `any`. That print is what makes an unresolvable alias read
161
+ * as a confident name: an instantiation over a dependency's internal generics
162
+ * has no members in it and resolves nowhere, and every scanner-side rule that
163
+ * refuses an empty answer asks the TEXT (`text_is_bare_top_type`), so nothing
164
+ * downstream can tell the difference (carrick#1444).
165
+ *
166
+ * Asked only of the alias's own type. At a member position the same echo is
167
+ * the producer's own vocabulary (`status: OrderStatus`) inside a shape that
168
+ * otherwise resolved, and replacing it with `any` would remove a name a reader
169
+ * can look up in the producing repo.
170
+ */
171
+ function isUnresolvedReference(type) {
172
+ return (type.isAny() &&
173
+ type.compilerType.intrinsicName === 'error');
174
+ }
136
175
  /** All .d.ts files under a directory, depth-first, deterministic order. */
137
176
  function walkDtsFiles(dir) {
138
177
  const out = [];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Where a declaration comes from: the user's own source, or the runtime and
3
+ * the packages it installed.
4
+ *
5
+ * One module because more than one layer asks the question — the inference
6
+ * path (`type-inferrer.ts`, deciding whether a type is framework machinery)
7
+ * and the structural printer (`type-structural-expander.ts`, deciding whether
8
+ * to inline a type's members or keep it by name). They must answer it the same
9
+ * way or one layer inlines what the other suppresses.
10
+ *
11
+ * The capture side keeps its own copy in `capture/machinery.ts`: that seam
12
+ * forbids importing anything but node builtins, `typescript` and its own
13
+ * bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
14
+ * rather than by sharing this module.
15
+ */
16
+ import type { ts } from 'ts-morph';
17
+ /**
18
+ * True when a declaration's source file is runtime/library origin rather than
19
+ * user source. Four answers, in order:
20
+ *
21
+ * 1. The runtime declarations Carrick materialises for a non-Node runtime
22
+ * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
23
+ * modules it copies beside them. Carrick's own artefact layout, not a
24
+ * guess about anyone else's.
25
+ * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
26
+ * classifies it; on a bare checkout the DOM `Response` resolves from here.
27
+ * 3. An install under a `node_modules` segment, however it entered the
28
+ * program (an import, or a root the loader registered).
29
+ * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
30
+ * This is the graph-backed answer for a project whose resolution does not
31
+ * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
32
+ * types from its own cache, a path with no `node_modules` segment, and
33
+ * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
34
+ * the graph, which the compiler records on the file. Nothing crosses the
35
+ * capture seam; both programs are built with that host. One exclusion: a
36
+ * workspace package reached through a `node_modules` symlink is also
37
+ * marked external by the compiler but is the user's own source, so a file
38
+ * inside the checkout (the nearest `.git` above the service root) that
39
+ * carries no `node_modules` segment stays user source.
40
+ *
41
+ * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
42
+ * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
43
+ * the pair on a real program.
44
+ */
45
+ export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Where a declaration comes from: the user's own source, or the runtime and
3
+ * the packages it installed.
4
+ *
5
+ * One module because more than one layer asks the question — the inference
6
+ * path (`type-inferrer.ts`, deciding whether a type is framework machinery)
7
+ * and the structural printer (`type-structural-expander.ts`, deciding whether
8
+ * to inline a type's members or keep it by name). They must answer it the same
9
+ * way or one layer inlines what the other suppresses.
10
+ *
11
+ * The capture side keeps its own copy in `capture/machinery.ts`: that seam
12
+ * forbids importing anything but node builtins, `typescript` and its own
13
+ * bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
14
+ * rather than by sharing this module.
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ /**
19
+ * True when a declaration's source file is runtime/library origin rather than
20
+ * user source. Four answers, in order:
21
+ *
22
+ * 1. The runtime declarations Carrick materialises for a non-Node runtime
23
+ * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
24
+ * modules it copies beside them. Carrick's own artefact layout, not a
25
+ * guess about anyone else's.
26
+ * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
27
+ * classifies it; on a bare checkout the DOM `Response` resolves from here.
28
+ * 3. An install under a `node_modules` segment, however it entered the
29
+ * program (an import, or a root the loader registered).
30
+ * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
31
+ * This is the graph-backed answer for a project whose resolution does not
32
+ * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
33
+ * types from its own cache, a path with no `node_modules` segment, and
34
+ * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
35
+ * the graph, which the compiler records on the file. Nothing crosses the
36
+ * capture seam; both programs are built with that host. One exclusion: a
37
+ * workspace package reached through a `node_modules` symlink is also
38
+ * marked external by the compiler but is the user's own source, so a file
39
+ * inside the checkout (the nearest `.git` above the service root) that
40
+ * carries no `node_modules` segment stays user source.
41
+ *
42
+ * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
43
+ * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
44
+ * the pair on a real program.
45
+ */
46
+ export function isExternalOrigin(program, sourceFile, repoRoot) {
47
+ const file = sourceFile.fileName.replace(/\\/g, '/');
48
+ if (file.includes('/.carrick/deno/')) {
49
+ return true;
50
+ }
51
+ if (program.isSourceFileDefaultLibrary(sourceFile)) {
52
+ return true;
53
+ }
54
+ if (file.includes('/node_modules/')) {
55
+ return true;
56
+ }
57
+ return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
58
+ }
59
+ const checkoutRoots = new Map();
60
+ /** The checkout the service root sits in: the nearest ancestor holding a
61
+ * `.git` entry (a directory, or the file a worktree carries), else the service
62
+ * root itself. */
63
+ function checkoutRootOf(repoRoot) {
64
+ const key = path.resolve(repoRoot);
65
+ const cached = checkoutRoots.get(key);
66
+ if (cached)
67
+ return cached;
68
+ let dir = key;
69
+ let root = key;
70
+ for (;;) {
71
+ if (fs.existsSync(path.join(dir, '.git'))) {
72
+ root = dir;
73
+ break;
74
+ }
75
+ const parent = path.dirname(dir);
76
+ if (parent === dir)
77
+ break;
78
+ dir = parent;
79
+ }
80
+ checkoutRoots.set(key, root);
81
+ return root;
82
+ }
83
+ function isInsideCheckout(file, repoRoot) {
84
+ const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
85
+ return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
86
+ }
@@ -17,7 +17,7 @@
17
17
  * (redirects, 204s), the LLM emits null and we fall back to the containing
18
18
  * function's return type.
19
19
  */
20
- import { Project, ts } from 'ts-morph';
20
+ import { Project } from 'ts-morph';
21
21
  import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js';
22
22
  /**
23
23
  * Strongly-discriminating member names of HTTP transport machinery — the
@@ -38,35 +38,6 @@ import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js
38
38
  * test (`machinery-indicator-mirror.test.ts`) asserts the two sets stay equal.
39
39
  */
40
40
  export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
41
- /**
42
- * True when a declaration's source file is runtime/library origin rather than
43
- * user source. Four answers, in order:
44
- *
45
- * 1. The runtime declarations Carrick materialises for a non-Node runtime
46
- * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
47
- * modules it copies beside them. Carrick's own artefact layout, not a
48
- * guess about anyone else's.
49
- * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
50
- * classifies it; on a bare checkout the DOM `Response` resolves from here.
51
- * 3. An install under a `node_modules` segment, however it entered the
52
- * program (an import, or a root the loader registered).
53
- * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
54
- * This is the graph-backed answer for a project whose resolution does not
55
- * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
56
- * types from its own cache, a path with no `node_modules` segment, and
57
- * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
58
- * the graph, which the compiler records on the file. Nothing crosses the
59
- * capture seam; both programs are built with that host. One exclusion: a
60
- * workspace package reached through a `node_modules` symlink is also
61
- * marked external by the compiler but is the user's own source, so a file
62
- * inside the checkout (the nearest `.git` above the service root) that
63
- * carries no `node_modules` segment stays user source.
64
- *
65
- * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
66
- * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
67
- * the pair on a real program.
68
- */
69
- export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
70
41
  /**
71
42
  * Options for TypeInferrer construction
72
43
  */
@@ -99,6 +70,13 @@ export declare class TypeInferrer {
99
70
  private readonly packageOf;
100
71
  private readonly repoRoot;
101
72
  constructor(options: TypeInferrerOptions);
73
+ /**
74
+ * What the structural printer needs to tell the user's own declarations from
75
+ * the runtime's and its packages' — the same program and service root this
76
+ * class asks `isExternalOrigin` about machinery, so the two layers cannot
77
+ * classify one declaration two ways.
78
+ */
79
+ private expandOrigin;
102
80
  /**
103
81
  * Infer types for the given requests
104
82
  *
@@ -18,9 +18,8 @@
18
18
  * function's return type.
19
19
  */
20
20
  import { Node, SyntaxKind, ts, } from 'ts-morph';
21
- import * as fs from 'node:fs';
22
- import * as path from 'node:path';
23
21
  import { validateInferRequestItem } from './validators.js';
22
+ import { isExternalOrigin } from './origin.js';
24
23
  import { expandTypeStructural, } from './type-structural-expander.js';
25
24
  /**
26
25
  * TS/lib globals and primitives that must never be emitted as a deterministic
@@ -152,75 +151,6 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
152
151
  'statusText',
153
152
  'headers',
154
153
  ]);
155
- /**
156
- * True when a declaration's source file is runtime/library origin rather than
157
- * user source. Four answers, in order:
158
- *
159
- * 1. The runtime declarations Carrick materialises for a non-Node runtime
160
- * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
161
- * modules it copies beside them. Carrick's own artefact layout, not a
162
- * guess about anyone else's.
163
- * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
164
- * classifies it; on a bare checkout the DOM `Response` resolves from here.
165
- * 3. An install under a `node_modules` segment, however it entered the
166
- * program (an import, or a root the loader registered).
167
- * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
168
- * This is the graph-backed answer for a project whose resolution does not
169
- * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
170
- * types from its own cache, a path with no `node_modules` segment, and
171
- * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
172
- * the graph, which the compiler records on the file. Nothing crosses the
173
- * capture seam; both programs are built with that host. One exclusion: a
174
- * workspace package reached through a `node_modules` symlink is also
175
- * marked external by the compiler but is the user's own source, so a file
176
- * inside the checkout (the nearest `.git` above the service root) that
177
- * carries no `node_modules` segment stays user source.
178
- *
179
- * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
180
- * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
181
- * the pair on a real program.
182
- */
183
- export function isExternalOrigin(program, sourceFile, repoRoot) {
184
- const file = sourceFile.fileName.replace(/\\/g, '/');
185
- if (file.includes('/.carrick/deno/')) {
186
- return true;
187
- }
188
- if (program.isSourceFileDefaultLibrary(sourceFile)) {
189
- return true;
190
- }
191
- if (file.includes('/node_modules/')) {
192
- return true;
193
- }
194
- return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
195
- }
196
- const checkoutRoots = new Map();
197
- /** The checkout the service root sits in: the nearest ancestor holding a
198
- * `.git` entry (a directory, or the file a worktree carries), else the service
199
- * root itself. */
200
- function checkoutRootOf(repoRoot) {
201
- const key = path.resolve(repoRoot);
202
- const cached = checkoutRoots.get(key);
203
- if (cached)
204
- return cached;
205
- let dir = key;
206
- let root = key;
207
- for (;;) {
208
- if (fs.existsSync(path.join(dir, '.git'))) {
209
- root = dir;
210
- break;
211
- }
212
- const parent = path.dirname(dir);
213
- if (parent === dir)
214
- break;
215
- dir = parent;
216
- }
217
- checkoutRoots.set(key, root);
218
- return root;
219
- }
220
- function isInsideCheckout(file, repoRoot) {
221
- const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
222
- return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
223
- }
224
154
  /**
225
155
  * How far the response-helper recovery (carrick#631) descends through nested
226
156
  * calls looking for the payload argument. `cors(request, json(payload))` needs
@@ -304,6 +234,18 @@ export class TypeInferrer {
304
234
  this.packageOf = options.packageOf;
305
235
  this.repoRoot = options.repoRoot;
306
236
  }
237
+ /**
238
+ * What the structural printer needs to tell the user's own declarations from
239
+ * the runtime's and its packages' — the same program and service root this
240
+ * class asks `isExternalOrigin` about machinery, so the two layers cannot
241
+ * classify one declaration two ways.
242
+ */
243
+ expandOrigin() {
244
+ return {
245
+ program: this.project.getProgram().compilerObject,
246
+ repoRoot: this.repoRoot,
247
+ };
248
+ }
307
249
  /**
308
250
  * Infer types for the given requests
309
251
  *
@@ -2610,7 +2552,7 @@ export class TypeInferrer {
2610
2552
  const fallback = typeNode.getText();
2611
2553
  try {
2612
2554
  const annotationType = this.unwrapPromiseType(typeNode.getType());
2613
- const expanded = expandTypeStructural(annotationType, new Set(), 0, undefined, wire);
2555
+ const expanded = expandTypeStructural(annotationType, this.expandOrigin(), { wire });
2614
2556
  // Only prefer the structural form when expansion actually inlined an
2615
2557
  // object shape; otherwise keep the annotation text (e.g. a bare
2616
2558
  // primitive or a library type the expander leaves by name). A wire
@@ -2618,7 +2560,7 @@ export class TypeInferrer {
2618
2560
  // `Date` annotation sends a string (carrick#1163).
2619
2561
  if (expanded.startsWith('{'))
2620
2562
  return expanded;
2621
- return wire === 'json' && expanded !== expandTypeStructural(annotationType)
2563
+ return wire === 'json' && expanded !== expandTypeStructural(annotationType, this.expandOrigin())
2622
2564
  ? expanded
2623
2565
  : fallback;
2624
2566
  }
@@ -2644,12 +2586,12 @@ export class TypeInferrer {
2644
2586
  */
2645
2587
  expandResolvedTypeStructural(type, fallback, wire = 'declared') {
2646
2588
  try {
2647
- const expanded = expandTypeStructural(type, new Set(), 0, undefined, wire);
2589
+ const expanded = expandTypeStructural(type, this.expandOrigin(), { wire });
2648
2590
  // A wire print that differs from the declared one is the answer even
2649
2591
  // without an inlined object: a bare `Date` payload sends a string.
2650
2592
  if (wire === 'json' &&
2651
2593
  !expanded.includes('{') &&
2652
- expanded !== expandTypeStructural(type)) {
2594
+ expanded !== expandTypeStructural(type, this.expandOrigin())) {
2653
2595
  return expanded;
2654
2596
  }
2655
2597
  // Prefer the expanded form whenever an object got inlined, not only when
@@ -4518,7 +4460,9 @@ export class TypeInferrer {
4518
4460
  const overrides = { types, applied: new Set(), at };
4519
4461
  let text;
4520
4462
  try {
4521
- text = expandTypeStructural(this.unwrapPromiseType(input), new Set(), 0, overrides);
4463
+ text = expandTypeStructural(this.unwrapPromiseType(input), this.expandOrigin(), {
4464
+ overrides,
4465
+ });
4522
4466
  }
4523
4467
  catch {
4524
4468
  return null;
@@ -28,7 +28,7 @@
28
28
  * `type-inferrer.ts` (consumer-side inference), so both paths emit the same
29
29
  * structural form rather than a dangling name.
30
30
  */
31
- import { type Node, type Type } from 'ts-morph';
31
+ import { type Node, type Type, ts } from 'ts-morph';
32
32
  /**
33
33
  * Bound on the structural-expansion recursion. Deep enough for every realistic
34
34
  * request/response shape; a backstop against pathological/recursive types the
@@ -73,17 +73,49 @@ export interface MemberOverrides {
73
73
  * signature, never from a list of type names.
74
74
  */
75
75
  export type WireFormat = 'declared' | 'json';
76
+ /**
77
+ * The program the walked type belongs to, and the service root inside it.
78
+ *
79
+ * Required, not optional, because the walk cannot decide from a type alone
80
+ * whether a declaration is the user's source or something the runtime
81
+ * installed: a path test answers that only where resolution goes through
82
+ * `node_modules`, and a runtime that serves an npm dependency's types out of
83
+ * its own cache leaves no such segment in the path (carrick#1264). The program
84
+ * carries the resolver's own verdict, so it is what `isExternalOrigin` is
85
+ * asked — the same instrument the inference path uses, so the two layers
86
+ * cannot disagree about which types to inline.
87
+ */
88
+ export interface ExpandOrigin {
89
+ readonly program: ts.Program;
90
+ readonly repoRoot: string;
91
+ }
92
+ /** Everything `expandTypeStructural` takes besides the type and its origin. */
93
+ export interface ExpandOptions {
94
+ /** Substitutions at named member positions; see `MemberOverrides`. */
95
+ readonly overrides?: MemberOverrides;
96
+ /** Which representation to print; see `WireFormat`. */
97
+ readonly wire?: WireFormat;
98
+ /**
99
+ * Where in the recursion the walk starts. Production callers never set it —
100
+ * depth is walk state — but the `MAX_EXPANSION_DEPTH` backstop is reachable
101
+ * from a shallow type only by starting the walk at the bound, which is how
102
+ * its union ordering is tested.
103
+ */
104
+ readonly depth?: number;
105
+ }
76
106
  /**
77
107
  * Recursively render a `Type` as fully-inlined structural text.
78
108
  *
79
109
  * Named object/interface types are expanded to their member structure;
80
110
  * primitives, literals, library types (`Date`, `Promise`, tuples, …) and
81
- * functions stay by name. The `seen` set (object type ids on the current
82
- * branch) breaks reference cycles; `depth` is a hard backstop. `overrides`
83
- * substitutes a type at named member positions (`MemberOverrides`); without
84
- * it the print is unchanged. `wire` picks the representation (`WireFormat`).
111
+ * functions stay by name — `origin` is what decides which is which. A cycle
112
+ * set (object type ids on the current branch) breaks reference cycles and
113
+ * `MAX_EXPANSION_DEPTH` is a hard backstop; both are walk state, not caller
114
+ * state. `overrides` substitutes a type at named member positions
115
+ * (`MemberOverrides`); without it the print is unchanged. `wire` picks the
116
+ * representation (`WireFormat`).
85
117
  */
86
- export declare function expandTypeStructural(type: Type, seen?: Set<number>, depth?: number, overrides?: MemberOverrides, wire?: WireFormat): string;
118
+ export declare function expandTypeStructural(type: Type, origin: ExpandOrigin, options?: ExpandOptions): string;
87
119
  /**
88
120
  * The type `JSON.stringify` serialises in place of a value of `type`: the
89
121
  * return type of the value's own `toJSON()`, or `undefined` when it declares