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.
- package/dist/contract.d.ts +9 -5
- package/dist/contract.js.map +1 -1
- package/dist/init/hosted.d.ts +100 -8
- package/dist/init/hosted.js +234 -33
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/output.d.ts +5 -3
- package/dist/init/output.js +12 -4
- package/dist/init/output.js.map +1 -1
- package/dist/init/run.js +12 -6
- package/dist/init/run.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/bundler.d.ts +5 -0
- package/sidecar/dist/src/bundler.js +13 -3
- package/sidecar/dist/src/capture/machinery.d.ts +1 -1
- package/sidecar/dist/src/capture/machinery.js +1 -1
- package/sidecar/dist/src/capture/repair-dangling.d.ts +46 -0
- package/sidecar/dist/src/capture/repair-dangling.js +162 -0
- package/sidecar/dist/src/capture/self-check.js +61 -5
- package/sidecar/dist/src/definition-resolver.d.ts +7 -0
- package/sidecar/dist/src/definition-resolver.js +43 -4
- package/sidecar/dist/src/origin.d.ts +45 -0
- package/sidecar/dist/src/origin.js +86 -0
- package/sidecar/dist/src/type-inferrer.d.ts +8 -30
- package/sidecar/dist/src/type-inferrer.js +20 -76
- package/sidecar/dist/src/type-structural-expander.d.ts +38 -6
- package/sidecar/dist/src/type-structural-expander.js +61 -32
- package/templates/skills/carrick-census.md +5 -0
- package/templates/skills/carrick-impact.md +11 -4
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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),
|
|
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
|
|
82
|
-
* branch) breaks reference cycles
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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,
|
|
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
|