carrick 0.3.133 → 0.3.134
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 +6 -0
- package/dist/contract.js.map +1 -1
- package/dist/hook/post-edit.d.ts +10 -5
- package/dist/hook/post-edit.js +80 -23
- package/dist/hook/post-edit.js.map +1 -1
- package/dist/hook/refresh.d.ts +23 -3
- package/dist/hook/refresh.js +48 -6
- package/dist/hook/refresh.js.map +1 -1
- package/dist/hook/reuse.d.ts +7 -21
- package/dist/hook/reuse.js +23 -36
- package/dist/hook/reuse.js.map +1 -1
- package/dist/hook/session-start.js +15 -2
- package/dist/hook/session-start.js.map +1 -1
- package/dist/hook/stop.js.map +1 -1
- package/dist/init/agents-section.js +1 -1
- package/dist/init/agents-section.js.map +1 -1
- package/dist/init/doctor.js +4 -0
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/hosted.d.ts +1 -0
- package/dist/init/hosted.js +4 -1
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/remove.js +7 -1
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repo-copies.js +3 -1
- package/dist/init/repo-copies.js.map +1 -1
- package/dist/init/run.d.ts +1 -1
- package/dist/init/run.js +10 -2
- package/dist/init/run.js.map +1 -1
- package/dist/init/task-skills.d.ts +22 -1
- package/dist/init/task-skills.js +65 -1
- package/dist/init/task-skills.js.map +1 -1
- package/dist/render.d.ts +41 -16
- package/dist/render.js +91 -102
- package/dist/render.js.map +1 -1
- package/package.json +10 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.js +18 -1
- package/sidecar/dist/src/capture/api.d.ts +17 -4
- package/sidecar/dist/src/capture/check-workspace.js +1 -1
- package/sidecar/dist/src/capture/check.d.ts +44 -0
- package/sidecar/dist/src/capture/check.js +112 -10
- package/sidecar/dist/src/capture/installed-package.d.ts +5 -0
- package/sidecar/dist/src/capture/installed-package.js +64 -6
- package/sidecar/dist/src/capture/paths-rewrite.js +3 -3
- package/sidecar/dist/src/capture/result-label.d.ts +42 -0
- package/sidecar/dist/src/capture/result-label.js +91 -0
- package/sidecar/dist/src/capture/self-check.js +13 -2
- package/sidecar/dist/src/index.js +116 -4
- package/sidecar/dist/src/infer-cache.d.ts +125 -0
- package/sidecar/dist/src/infer-cache.js +244 -0
- package/sidecar/dist/src/library-claims.js +42 -4
- package/sidecar/dist/src/node-index.d.ts +53 -0
- package/sidecar/dist/src/node-index.js +100 -0
- package/sidecar/dist/src/project-loader.d.ts +11 -0
- package/sidecar/dist/src/project-loader.js +15 -0
- package/sidecar/dist/src/result-candidates.d.ts +28 -0
- package/sidecar/dist/src/result-candidates.js +209 -0
- package/sidecar/dist/src/type-inferrer.d.ts +27 -0
- package/sidecar/dist/src/type-inferrer.js +175 -43
- package/sidecar/dist/src/types.d.ts +52 -0
- package/sidecar/dist/src/unwidened.js +10 -1
- package/sidecar/dist/src/validators.d.ts +919 -197
- package/sidecar/dist/src/validators.js +32 -0
- package/templates/skills/carrick-reuse.md +11 -0
|
@@ -61,6 +61,31 @@ export function carriesHeapLimitLine(stderr) {
|
|
|
61
61
|
.some((line) => line.includes(HEAP_OUT_OF_MEMORY) &&
|
|
62
62
|
!HARD_SIZE_LIMITS.some((limit) => line.includes(limit)));
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether the native compiler (TypeScript 7, a Go program) died out of
|
|
66
|
+
* memory. The Go runtime prints `fatal error: runtime: out of memory` and
|
|
67
|
+
* exits 2; V8's line above never appears, and a heap cap passed through
|
|
68
|
+
* `NODE_OPTIONS` reaches only the launcher, not the compiler.
|
|
69
|
+
*/
|
|
70
|
+
export function carriesNativeOutOfMemoryLine(output) {
|
|
71
|
+
return output.split('\n').some((line) => line.includes('fatal error: runtime: out of memory'));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether a diagnostic is about the compiler's configuration rather than a
|
|
75
|
+
* file it checked: one placed in the tsconfig the judge wrote, or "no inputs"
|
|
76
|
+
* (TS18003). Keyed on the file, not on the TS5xxx option range: some of those
|
|
77
|
+
* codes sit in a source file (TS5097, an import ending in `.ts`) and poison
|
|
78
|
+
* only the stub that holds it.
|
|
79
|
+
*/
|
|
80
|
+
export function isConfigDiagnostic(d) {
|
|
81
|
+
return path.posix.basename(d.file) === 'tsconfig.json' || d.code === 18003;
|
|
82
|
+
}
|
|
83
|
+
/** Why the compiler ended abnormally, when its output names a known cause. */
|
|
84
|
+
export function abnormalCause(output) {
|
|
85
|
+
if (carriesNativeOutOfMemoryLine(output))
|
|
86
|
+
return 'the type checker ran out of memory';
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
64
89
|
/** V8's default old-space ceiling on a 64-bit host, in MB. */
|
|
65
90
|
const V8_DEFAULT_HEAP_CEILING_MB = 4096;
|
|
66
91
|
/**
|
|
@@ -119,9 +144,16 @@ function sidecarRoot() {
|
|
|
119
144
|
* which is the caller's "not found" signal and the most useful thing to name.
|
|
120
145
|
*/
|
|
121
146
|
export function resolveVendoredBin(name, from = sidecarRoot()) {
|
|
122
|
-
|
|
147
|
+
return resolveVendored(path.join('.bin', name), from);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The first `node_modules/<rel>` at or above `from`, the same walk as
|
|
151
|
+
* {@link resolveVendoredBin}; the sidecar-local path when none exists.
|
|
152
|
+
*/
|
|
153
|
+
export function resolveVendored(rel, from = sidecarRoot()) {
|
|
154
|
+
const local = path.join(from, 'node_modules', rel);
|
|
123
155
|
for (let dir = from;; dir = path.dirname(dir)) {
|
|
124
|
-
const candidate = path.join(dir, 'node_modules',
|
|
156
|
+
const candidate = path.join(dir, 'node_modules', rel);
|
|
125
157
|
if (fs.existsSync(candidate))
|
|
126
158
|
return candidate;
|
|
127
159
|
if (path.dirname(dir) === dir)
|
|
@@ -131,6 +163,58 @@ export function resolveVendoredBin(name, from = sidecarRoot()) {
|
|
|
131
163
|
function binPath(name) {
|
|
132
164
|
return resolveVendoredBin(name);
|
|
133
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The package that brings the judge's compiler: TypeScript 7 under an alias
|
|
168
|
+
* beside the TypeScript 6 that capture and inference read through ts-morph.
|
|
169
|
+
* Its platform package holds the native executable the judge runs. Both
|
|
170
|
+
* packages declare a `tsc` bin, so `node_modules/.bin/tsc` may be either; the
|
|
171
|
+
* judge never reads it.
|
|
172
|
+
*/
|
|
173
|
+
export const JUDGE_COMPILER_PACKAGE = 'typescript-native';
|
|
174
|
+
/**
|
|
175
|
+
* The judge's tsc: the host's native executable, from the platform package
|
|
176
|
+
* the compiler installs as an optional dependency (`@typescript/
|
|
177
|
+
* typescript-<platform>-<arch>`), found by the same walk as the bins. Where it
|
|
178
|
+
* is not installed (an install that omits optional dependencies, such as the
|
|
179
|
+
* cloud runner's, or a pnpm store the walk does not reach), TypeScript 6's own
|
|
180
|
+
* tsc, which ships for ts-morph anyway. The two read every probe the same
|
|
181
|
+
* (measured on 1,018 real probes; the one wording difference moves only a
|
|
182
|
+
* verdict's `codes`), so the fallback changes no verdict, only the time.
|
|
183
|
+
*/
|
|
184
|
+
export function judgeTscPath(from = sidecarRoot(), platform = process.platform, arch = process.arch) {
|
|
185
|
+
const exe = platform === 'win32' ? 'tsc.exe' : 'tsc';
|
|
186
|
+
const native = path.join('@typescript', `typescript-${platform}-${arch}`, 'lib', exe);
|
|
187
|
+
const found = resolveVendored(native, from);
|
|
188
|
+
if (fs.existsSync(found))
|
|
189
|
+
return found;
|
|
190
|
+
return resolveVendored(path.join('typescript', 'bin', 'tsc'), from);
|
|
191
|
+
}
|
|
192
|
+
/** Which compiler a tsc path is, in words, for the run log and the result. */
|
|
193
|
+
export function judgeLabel(tscPath) {
|
|
194
|
+
const version = compilerVersion(tscPath);
|
|
195
|
+
const parts = tscPath.split(/[\\/]/);
|
|
196
|
+
if (parts.some((p) => p.startsWith('typescript-') && p !== JUDGE_COMPILER_PACKAGE)) {
|
|
197
|
+
return `TypeScript ${version} (native)`;
|
|
198
|
+
}
|
|
199
|
+
if (parts.includes('typescript') && parts[parts.length - 2] === 'bin') {
|
|
200
|
+
return `TypeScript ${version} (native compiler not installed)`;
|
|
201
|
+
}
|
|
202
|
+
return `a stand-in compiler at ${tscPath}`;
|
|
203
|
+
}
|
|
204
|
+
/** The version of the compiler a tsc path belongs to, read from the package
|
|
205
|
+
* that holds it; the compiler-API version for a stand-in with no package. */
|
|
206
|
+
function compilerVersion(tscPath) {
|
|
207
|
+
try {
|
|
208
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(path.dirname(tscPath), '..', 'package.json'), 'utf8'));
|
|
209
|
+
const compiler = manifest.name === 'typescript' || manifest.name?.startsWith('@typescript/typescript-');
|
|
210
|
+
if (compiler && manifest.version)
|
|
211
|
+
return manifest.version;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// A stand-in compiler (tests) sits in no package.
|
|
215
|
+
}
|
|
216
|
+
return ts.version;
|
|
217
|
+
}
|
|
134
218
|
function unverifiableAll(plans, gate, diagnostic) {
|
|
135
219
|
return plans.map((plan) => ({
|
|
136
220
|
pair_id: plan.pairId,
|
|
@@ -155,8 +239,9 @@ export async function runCheck(opts, onProgress) {
|
|
|
155
239
|
const progress = onProgress ?? (() => { });
|
|
156
240
|
const cleanup = opts.cleanup !== false;
|
|
157
241
|
const pnpmPath = opts.pnpmPath ?? binPath('pnpm');
|
|
158
|
-
const tscPath = opts.tscPath ??
|
|
159
|
-
const tsVersion =
|
|
242
|
+
const tscPath = opts.tscPath ?? judgeTscPath();
|
|
243
|
+
const tsVersion = compilerVersion(tscPath);
|
|
244
|
+
const judge = judgeLabel(tscPath);
|
|
160
245
|
// Isolation is non-negotiable: without the vendored pnpm we would fall back
|
|
161
246
|
// to a flat npm install that manufactures nominal false-incompatibles. Fail
|
|
162
247
|
// explicitly instead (design Check step 2).
|
|
@@ -169,6 +254,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
169
254
|
isolation: 'unavailable',
|
|
170
255
|
install_ok: false,
|
|
171
256
|
ts_version: tsVersion,
|
|
257
|
+
judge,
|
|
172
258
|
verdicts: sortVerdicts([
|
|
173
259
|
...unverifiableAll(probing, 'isolation:unavailable', 'type isolation unavailable (pnpm missing); compatibility cannot be verified.'),
|
|
174
260
|
...preGated,
|
|
@@ -248,13 +334,14 @@ export async function runCheck(opts, onProgress) {
|
|
|
248
334
|
isolation: 'pnpm',
|
|
249
335
|
install_ok: false,
|
|
250
336
|
ts_version: tsVersion,
|
|
337
|
+
judge,
|
|
251
338
|
verdicts,
|
|
252
339
|
degraded_services: degraded,
|
|
253
340
|
errors,
|
|
254
341
|
};
|
|
255
342
|
}
|
|
256
343
|
// ---- Judge (async, off the event loop) ----------------------------------
|
|
257
|
-
progress('checking',
|
|
344
|
+
progress('checking', `type-checking probes (judge: ${judge})`);
|
|
258
345
|
const heap = childHeap(process.execArgv, process.env.NODE_OPTIONS, process.memoryUsage().rss / (1024 * 1024));
|
|
259
346
|
const tsc = await runProcess(tscPath, ['--noEmit', '--pretty', 'false', '-p', path.join(ws.probesRel, 'tsconfig.json')], ws.workspaceDir, { ...process.env, NODE_OPTIONS: heap.nodeOptions });
|
|
260
347
|
const diagnostics = parseTscOutput(tsc.stdout + '\n' + tsc.stderr);
|
|
@@ -267,8 +354,20 @@ export async function runCheck(opts, onProgress) {
|
|
|
267
354
|
// `code === 0` guard.
|
|
268
355
|
// A compiler a signal ended never finished, so a pair with no diagnostic
|
|
269
356
|
// was never cleared, whatever it printed first (carrick#2209).
|
|
270
|
-
|
|
271
|
-
|
|
357
|
+
//
|
|
358
|
+
// A diagnostic about the configuration (a tsconfig the compiler refuses,
|
|
359
|
+
// an option it does not know) is printed WITH a position, in the tsconfig,
|
|
360
|
+
// and belongs to no pair: TypeScript 7 refuses an option and checks
|
|
361
|
+
// nothing, so without this every pair read compatible. Such a run checked
|
|
362
|
+
// no pair, whatever else it printed.
|
|
363
|
+
const configError = diagnostics.find(isConfigDiagnostic);
|
|
364
|
+
if (tsc.signal !== null ||
|
|
365
|
+
(tsc.code !== 0 && diagnostics.length === 0) ||
|
|
366
|
+
configError !== undefined) {
|
|
367
|
+
const excerpt = scrubPaths((configError
|
|
368
|
+
? `${configError.file}(${configError.line},${configError.col}): error TS${configError.code}: ${configError.message}`
|
|
369
|
+
: (tsc.stderr || tsc.stdout).trim()).slice(0, 2000), scrubCtx);
|
|
370
|
+
const cause = abnormalCause(tsc.stderr + '\n' + tsc.stdout);
|
|
272
371
|
errors.push(`tsc terminated abnormally (exit code ${tsc.code ?? 'null'}) ` +
|
|
273
372
|
`with no parseable diagnostics${excerpt ? `: ${excerpt}` : ''}`);
|
|
274
373
|
for (const s of opts.stubs) {
|
|
@@ -278,8 +377,9 @@ export async function runCheck(opts, onProgress) {
|
|
|
278
377
|
});
|
|
279
378
|
}
|
|
280
379
|
const verdicts = sortVerdicts([
|
|
281
|
-
...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'})
|
|
282
|
-
|
|
380
|
+
...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'})` +
|
|
381
|
+
(cause ? ` because ${cause}` : '') +
|
|
382
|
+
'; compatibility cannot be verified.' +
|
|
283
383
|
(excerpt ? ` Compiler output: ${excerpt}` : '')),
|
|
284
384
|
...preGated,
|
|
285
385
|
...unresolved,
|
|
@@ -290,7 +390,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
290
390
|
const compilerEnd = {
|
|
291
391
|
exit_code: tsc.code,
|
|
292
392
|
signal: tsc.signal,
|
|
293
|
-
heap_limit: carriesHeapLimitLine(tsc.stderr),
|
|
393
|
+
heap_limit: carriesHeapLimitLine(tsc.stderr) || carriesNativeOutOfMemoryLine(tsc.stderr + '\n' + tsc.stdout),
|
|
294
394
|
heap_cap_mb: heap.heapCapMb,
|
|
295
395
|
};
|
|
296
396
|
if (cleanup)
|
|
@@ -301,6 +401,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
301
401
|
isolation: 'pnpm',
|
|
302
402
|
install_ok: true,
|
|
303
403
|
ts_version: tsVersion,
|
|
404
|
+
judge,
|
|
304
405
|
verdicts,
|
|
305
406
|
degraded_services: dedupeDegraded(degraded),
|
|
306
407
|
errors,
|
|
@@ -359,6 +460,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
359
460
|
isolation: 'pnpm',
|
|
360
461
|
install_ok: true,
|
|
361
462
|
ts_version: tsVersion,
|
|
463
|
+
judge,
|
|
362
464
|
verdicts,
|
|
363
465
|
degraded_services: dedupeDegraded(degraded),
|
|
364
466
|
errors,
|
|
@@ -35,6 +35,11 @@ export interface InstalledPackageSpecifier {
|
|
|
35
35
|
name: string;
|
|
36
36
|
version: string;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* The name the entry exports the declaration under, set only when it differs
|
|
40
|
+
* from the name the path's declaration carries (`export { O as OmitKeys }`).
|
|
41
|
+
*/
|
|
42
|
+
exportName?: string;
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
45
|
* The `@types/*` package that serves a bare name's declarations, for callers
|
|
@@ -245,29 +245,78 @@ function entrySubpaths(manifest) {
|
|
|
245
245
|
}
|
|
246
246
|
return [''];
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Declaration keys (`file\0declaredName`) of what `file` exports as `name`.
|
|
250
|
+
* A chunk can declare a type under its public name and export it under a
|
|
251
|
+
* short one (`export { type OmitKeyof as O }`), so the name read off the
|
|
252
|
+
* import is not the name the declaration carries.
|
|
253
|
+
*/
|
|
254
|
+
function declarationKeysOfExport(file, name) {
|
|
255
|
+
const program = ts.createProgram([file], RESOLUTION_OPTIONS);
|
|
256
|
+
const checker = program.getTypeChecker();
|
|
257
|
+
const source = program.getSourceFile(file);
|
|
258
|
+
const moduleSymbol = source && checker.getSymbolAtLocation(source);
|
|
259
|
+
const symbol = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === name);
|
|
260
|
+
if (!symbol)
|
|
261
|
+
return [];
|
|
262
|
+
let target = symbol;
|
|
263
|
+
for (let hops = 0; target.flags & ts.SymbolFlags.Alias && hops < 16; hops++) {
|
|
264
|
+
const next = checker.getAliasedSymbol(target);
|
|
265
|
+
if (next === target)
|
|
266
|
+
break;
|
|
267
|
+
target = next;
|
|
268
|
+
}
|
|
269
|
+
const keys = [];
|
|
270
|
+
for (const declaration of target.declarations ?? []) {
|
|
271
|
+
try {
|
|
272
|
+
keys.push(`${fs.realpathSync(declaration.getSourceFile().fileName)}\0${target.getName()}`);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// declaration outside the real file system: ignore
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return keys;
|
|
279
|
+
}
|
|
248
280
|
const moduleExportsCache = new Map();
|
|
249
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* Exports of `entryFile`: by exported name (with the real files declaring it)
|
|
283
|
+
* and by declaration identity (the file and the name the declaration carries
|
|
284
|
+
* there, whatever name the entry exports it under).
|
|
285
|
+
*/
|
|
250
286
|
function exportsOf(entryFile) {
|
|
251
287
|
const cached = moduleExportsCache.get(entryFile);
|
|
252
288
|
if (cached)
|
|
253
289
|
return cached;
|
|
254
|
-
const exported = new Map();
|
|
290
|
+
const exported = { byName: new Map(), byDeclaration: new Map() };
|
|
255
291
|
const program = ts.createProgram([entryFile], RESOLUTION_OPTIONS);
|
|
256
292
|
const checker = program.getTypeChecker();
|
|
257
293
|
const source = program.getSourceFile(entryFile);
|
|
258
294
|
const moduleSymbol = source && checker.getSymbolAtLocation(source);
|
|
259
295
|
for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : []) {
|
|
260
|
-
|
|
296
|
+
let target = symbol;
|
|
297
|
+
// Follow the alias chain until it stops (an entry can re-export a re-export).
|
|
298
|
+
for (let hops = 0; target.flags & ts.SymbolFlags.Alias && hops < 16; hops++) {
|
|
299
|
+
const next = checker.getAliasedSymbol(target);
|
|
300
|
+
if (next === target)
|
|
301
|
+
break;
|
|
302
|
+
target = next;
|
|
303
|
+
}
|
|
261
304
|
const files = new Set();
|
|
262
305
|
for (const declaration of target.declarations ?? []) {
|
|
263
306
|
try {
|
|
264
|
-
|
|
307
|
+
const real = fs.realpathSync(declaration.getSourceFile().fileName);
|
|
308
|
+
files.add(real);
|
|
309
|
+
const key = `${real}\0${target.getName()}`;
|
|
310
|
+
// The first export of a declaration wins; its own name wins over a rename.
|
|
311
|
+
if (!exported.byDeclaration.has(key) || symbol.getName() === target.getName()) {
|
|
312
|
+
exported.byDeclaration.set(key, symbol.getName());
|
|
313
|
+
}
|
|
265
314
|
}
|
|
266
315
|
catch {
|
|
267
316
|
// declaration outside the real file system: ignore
|
|
268
317
|
}
|
|
269
318
|
}
|
|
270
|
-
exported.set(symbol.getName(), files);
|
|
319
|
+
exported.byName.set(symbol.getName(), files);
|
|
271
320
|
}
|
|
272
321
|
moduleExportsCache.set(entryFile, exported);
|
|
273
322
|
return exported;
|
|
@@ -304,9 +353,18 @@ export function installedPackageSpecifier(absoluteSpecifier, importedName) {
|
|
|
304
353
|
const entryFile = resolveAgainstInstall(publicName + entry, root, manifest.name)?.resolvedFileName;
|
|
305
354
|
if (!entryFile)
|
|
306
355
|
continue;
|
|
307
|
-
|
|
356
|
+
const entryExports = exportsOf(entryFile);
|
|
357
|
+
if (entryExports.byName.get(importedName)?.has(file)) {
|
|
308
358
|
return withPin(publicName + entry);
|
|
309
359
|
}
|
|
360
|
+
// The entry may export the declaration under another name.
|
|
361
|
+
const renamed = entryExports.byDeclaration.get(`${file}\0${importedName}`) ??
|
|
362
|
+
declarationKeysOfExport(file, importedName)
|
|
363
|
+
.map((key) => entryExports.byDeclaration.get(key))
|
|
364
|
+
.find((name) => name !== undefined);
|
|
365
|
+
if (renamed !== undefined) {
|
|
366
|
+
return { ...withPin(publicName + entry), exportName: renamed };
|
|
367
|
+
}
|
|
310
368
|
}
|
|
311
369
|
}
|
|
312
370
|
return withPin(publicName + (candidates[0] ?? `/${withoutExtension(subpath)}`));
|
|
@@ -82,7 +82,7 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
82
82
|
installs[mapped.install.name] = mapped.install.root;
|
|
83
83
|
if (mapped.pin)
|
|
84
84
|
pins[mapped.pin.name] = mapped.pin.version;
|
|
85
|
-
return mapped.specifier;
|
|
85
|
+
return { specifier: mapped.specifier, exportName: mapped.exportName };
|
|
86
86
|
};
|
|
87
87
|
for (const file of args.files) {
|
|
88
88
|
const absFile = path.join(args.typesDir, file);
|
|
@@ -97,13 +97,13 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
97
97
|
if (replacement === undefined)
|
|
98
98
|
return whole;
|
|
99
99
|
importTypeRewrites++;
|
|
100
|
-
return `${open}${quote}${replacement}${quote}${close}${dot}${name}`;
|
|
100
|
+
return `${open}${quote}${replacement.specifier}${quote}${close}${dot}${replacement.exportName ?? name}`;
|
|
101
101
|
});
|
|
102
102
|
const { text: rewritten, rewrites } = rewriteSpecifiers(text, (spec) => {
|
|
103
103
|
// Absolute paths: an emitted tree file, else an installed package.
|
|
104
104
|
if (spec.startsWith('/')) {
|
|
105
105
|
const target = treeFileFor(spec, args.entryDir, emitted, args.outside);
|
|
106
|
-
return target ? relativeSpecifier(file, target) : installed(spec);
|
|
106
|
+
return target ? relativeSpecifier(file, target) : installed(spec)?.specifier;
|
|
107
107
|
}
|
|
108
108
|
if (isRelative(spec))
|
|
109
109
|
return undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a labelled call's result (carrick#2818).
|
|
3
|
+
*
|
|
4
|
+
* A request library's call rarely returns the response body itself: the body
|
|
5
|
+
* sits in a member of what comes back (`data`), behind a parse method
|
|
6
|
+
* (`json()`), or is the whole value. The library store labels which, per
|
|
7
|
+
* operation, and the scanner sends the label on the consumer's infer anchor.
|
|
8
|
+
* This reads the labelled part of the located call's type, so the contract the
|
|
9
|
+
* consumer states is the body and never the library's envelope.
|
|
10
|
+
*
|
|
11
|
+
* The label is one of:
|
|
12
|
+
* - `{ whole: true }`: the awaited type;
|
|
13
|
+
* - `{ member: [m] }`: `m` on every non-nullish constituent of the awaited
|
|
14
|
+
* type, `undefined` and `null` removed from what is read;
|
|
15
|
+
* - `{ parse: p }`: the awaited return of calling `p()`, read off the type as
|
|
16
|
+
* the call returns it (a promise-like with a `json()` method is a thenable,
|
|
17
|
+
* and awaiting it first would drop the method).
|
|
18
|
+
*
|
|
19
|
+
* A label the type cannot satisfy abstains with `result_member_missing`.
|
|
20
|
+
*/
|
|
21
|
+
import ts from 'typescript';
|
|
22
|
+
/** The wire form of a result label. Exactly one key is set. */
|
|
23
|
+
export type ResultLabel = {
|
|
24
|
+
whole: true;
|
|
25
|
+
} | {
|
|
26
|
+
member: [string];
|
|
27
|
+
} | {
|
|
28
|
+
parse: string;
|
|
29
|
+
};
|
|
30
|
+
export type ResultRead = {
|
|
31
|
+
type: ts.Type;
|
|
32
|
+
} | {
|
|
33
|
+
abstain: string;
|
|
34
|
+
};
|
|
35
|
+
/** The constituents of a union, or the type alone. */
|
|
36
|
+
export declare function constituentsOf(type: ts.Type): readonly ts.Type[];
|
|
37
|
+
/**
|
|
38
|
+
* Read `label` off a call's type. `raw` is the type as the call returns it and
|
|
39
|
+
* `awaited` the type after the transport unwrap; `at` is the node properties
|
|
40
|
+
* are instantiated at.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readResultLabel(checker: ts.TypeChecker, raw: ts.Type, awaited: ts.Type, label: ResultLabel, at: ts.Node): ResultRead;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a labelled call's result (carrick#2818).
|
|
3
|
+
*
|
|
4
|
+
* A request library's call rarely returns the response body itself: the body
|
|
5
|
+
* sits in a member of what comes back (`data`), behind a parse method
|
|
6
|
+
* (`json()`), or is the whole value. The library store labels which, per
|
|
7
|
+
* operation, and the scanner sends the label on the consumer's infer anchor.
|
|
8
|
+
* This reads the labelled part of the located call's type, so the contract the
|
|
9
|
+
* consumer states is the body and never the library's envelope.
|
|
10
|
+
*
|
|
11
|
+
* The label is one of:
|
|
12
|
+
* - `{ whole: true }`: the awaited type;
|
|
13
|
+
* - `{ member: [m] }`: `m` on every non-nullish constituent of the awaited
|
|
14
|
+
* type, `undefined` and `null` removed from what is read;
|
|
15
|
+
* - `{ parse: p }`: the awaited return of calling `p()`, read off the type as
|
|
16
|
+
* the call returns it (a promise-like with a `json()` method is a thenable,
|
|
17
|
+
* and awaiting it first would drop the method).
|
|
18
|
+
*
|
|
19
|
+
* A label the type cannot satisfy abstains with `result_member_missing`.
|
|
20
|
+
*/
|
|
21
|
+
import ts from 'typescript';
|
|
22
|
+
const NULLISH = ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void;
|
|
23
|
+
/** The constituents of a union, or the type alone. */
|
|
24
|
+
export function constituentsOf(type) {
|
|
25
|
+
return type.isUnion() ? type.types : [type];
|
|
26
|
+
}
|
|
27
|
+
function missing(what) {
|
|
28
|
+
return { abstain: `result_member_missing: ${what}` };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read `label` off a call's type. `raw` is the type as the call returns it and
|
|
32
|
+
* `awaited` the type after the transport unwrap; `at` is the node properties
|
|
33
|
+
* are instantiated at.
|
|
34
|
+
*/
|
|
35
|
+
export function readResultLabel(checker, raw, awaited, label, at) {
|
|
36
|
+
if ('whole' in label)
|
|
37
|
+
return { type: awaited };
|
|
38
|
+
if ('member' in label)
|
|
39
|
+
return readMember(checker, awaited, label.member[0], at);
|
|
40
|
+
return readParse(checker, raw, awaited, label.parse, at);
|
|
41
|
+
}
|
|
42
|
+
function readMember(checker, awaited, name, at) {
|
|
43
|
+
const read = [];
|
|
44
|
+
for (const constituent of constituentsOf(awaited)) {
|
|
45
|
+
if (constituent.flags & NULLISH)
|
|
46
|
+
continue;
|
|
47
|
+
const property = checker.getPropertyOfType(constituent, name);
|
|
48
|
+
if (!property)
|
|
49
|
+
return missing(`the labelled member '${name}' is not on every constituent`);
|
|
50
|
+
const held = checker.getTypeOfSymbolAtLocation(property, at);
|
|
51
|
+
for (const part of constituentsOf(held)) {
|
|
52
|
+
if (!(part.flags & NULLISH))
|
|
53
|
+
read.push(part);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (read.length === 0)
|
|
57
|
+
return missing(`the labelled member '${name}' holds no value`);
|
|
58
|
+
if (read.length === 1)
|
|
59
|
+
return { type: read[0] };
|
|
60
|
+
const union = checker.getUnionType;
|
|
61
|
+
// Without the checker's own union the members cannot be joined honestly;
|
|
62
|
+
// abstaining beats publishing the first of them.
|
|
63
|
+
if (!union)
|
|
64
|
+
return missing(`the labelled member '${name}' holds several types`);
|
|
65
|
+
return { type: union.call(checker, read) };
|
|
66
|
+
}
|
|
67
|
+
function readParse(checker, raw, awaited, name, at) {
|
|
68
|
+
// The method is looked up on the call's own type first: it is the thenable
|
|
69
|
+
// that carries `json()`, and its awaited value does not.
|
|
70
|
+
for (const holder of [raw, awaited]) {
|
|
71
|
+
const method = checker.getPropertyOfType(holder, name);
|
|
72
|
+
if (!method)
|
|
73
|
+
continue;
|
|
74
|
+
const signature = checker
|
|
75
|
+
.getSignaturesOfType(checker.getTypeOfSymbolAtLocation(method, at), ts.SignatureKind.Call)
|
|
76
|
+
.find((candidate) => candidate.parameters.every((p) => isOptional(p)));
|
|
77
|
+
if (!signature)
|
|
78
|
+
continue;
|
|
79
|
+
const returned = checker.getReturnTypeOfSignature(signature);
|
|
80
|
+
return { type: checker.getAwaitedType(returned) ?? returned };
|
|
81
|
+
}
|
|
82
|
+
return missing(`the labelled parse method '${name}' is not callable with no arguments`);
|
|
83
|
+
}
|
|
84
|
+
function isOptional(parameter) {
|
|
85
|
+
const declaration = parameter.valueDeclaration;
|
|
86
|
+
return (!!declaration &&
|
|
87
|
+
ts.isParameter(declaration) &&
|
|
88
|
+
(declaration.questionToken !== undefined ||
|
|
89
|
+
declaration.initializer !== undefined ||
|
|
90
|
+
declaration.dotDotDotToken !== undefined));
|
|
91
|
+
}
|
|
@@ -232,6 +232,17 @@ function runSelfCheck(args, treeFiles, repaired) {
|
|
|
232
232
|
* because the module that is missing is no longer imported to be reported.
|
|
233
233
|
* Fail closed: the alias keeps the refusal it had before the repair.
|
|
234
234
|
*/
|
|
235
|
+
/**
|
|
236
|
+
* Detail for a pinned external that does not resolve on an installed checkout.
|
|
237
|
+
* A subpath on the specifier means the rewrite found no entry of the package
|
|
238
|
+
* exporting a declaration from that file, so it named the file itself.
|
|
239
|
+
*/
|
|
240
|
+
function unresolvedExternalDetail(spec) {
|
|
241
|
+
const pkg = packageNameOf(spec);
|
|
242
|
+
const subpath = spec.slice(pkg.length + 1);
|
|
243
|
+
const base = `unresolved pinned external '${spec}' on an installed checkout`;
|
|
244
|
+
return subpath ? `${base}: no entry of '${pkg}' exports a declaration from '${subpath}'` : base;
|
|
245
|
+
}
|
|
235
246
|
function repairedNameFailure(repaired, file, diagnostic, message) {
|
|
236
247
|
if (!repaired || diagnostic.code !== 2304)
|
|
237
248
|
return undefined;
|
|
@@ -432,7 +443,7 @@ function checkedRecord(anchor, ctx) {
|
|
|
432
443
|
}
|
|
433
444
|
else {
|
|
434
445
|
outcome = 'decayed_internal';
|
|
435
|
-
detail =
|
|
446
|
+
detail = unresolvedExternalDetail(blamedExternal);
|
|
436
447
|
}
|
|
437
448
|
}
|
|
438
449
|
else {
|
|
@@ -452,7 +463,7 @@ function checkedRecord(anchor, ctx) {
|
|
|
452
463
|
}
|
|
453
464
|
else {
|
|
454
465
|
outcome = 'decayed_internal';
|
|
455
|
-
detail =
|
|
466
|
+
detail = unresolvedExternalDetail(blamedExternal);
|
|
456
467
|
}
|
|
457
468
|
}
|
|
458
469
|
return {
|