carrick 0.3.107 → 0.3.109
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/package.json +9 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/api.d.ts +15 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
- package/sidecar/dist/src/capture/check-classify.js +20 -0
- package/sidecar/dist/src/capture/check-deep.js +2 -1
- package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
- package/sidecar/dist/src/capture/check-fields.js +54 -36
- package/sidecar/dist/src/capture/check-union.d.ts +40 -0
- package/sidecar/dist/src/capture/check-union.js +92 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +1 -0
- package/sidecar/dist/src/capture/check-workspace.js +13 -2
- package/sidecar/dist/src/capture/check.js +5 -0
- package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
- package/sidecar/dist/src/capture/compiler-options.js +100 -0
- package/sidecar/dist/src/capture/deno-project.js +3 -2
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +90 -48
- package/sidecar/dist/src/capture/installed-package.js +3 -2
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/definition-resolver.d.ts +5 -8
- package/sidecar/dist/src/definition-resolver.js +8 -9
- package/sidecar/dist/src/index.js +124 -11
- package/sidecar/dist/src/infer-timing.d.ts +54 -0
- package/sidecar/dist/src/infer-timing.js +124 -0
- package/sidecar/dist/src/print-type.d.ts +48 -0
- package/sidecar/dist/src/print-type.js +71 -0
- package/sidecar/dist/src/printed-names.js +2 -1
- package/sidecar/dist/src/program-files.d.ts +44 -0
- package/sidecar/dist/src/program-files.js +64 -0
- package/sidecar/dist/src/project-loader.d.ts +5 -0
- package/sidecar/dist/src/project-loader.js +14 -5
- package/sidecar/dist/src/retype.d.ts +4 -2
- package/sidecar/dist/src/retype.js +16 -5
- package/sidecar/dist/src/type-inferrer.d.ts +157 -1
- package/sidecar/dist/src/type-inferrer.js +444 -20
- package/sidecar/dist/src/types.d.ts +104 -2
- package/sidecar/dist/src/validators.d.ts +166 -80
- package/sidecar/dist/src/validators.js +13 -0
- package/templates/skills/carrick-reuse.md +6 -5
|
@@ -5,6 +5,7 @@ import * as path from 'node:path';
|
|
|
5
5
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
8
9
|
import { rewriteSpecifiers } from './specifiers.js';
|
|
9
10
|
import { WriteGuard } from './guarded-fs.js';
|
|
10
11
|
function readConfig(file) {
|
|
@@ -452,11 +453,11 @@ export class DenoProject {
|
|
|
452
453
|
/** Runtime declarations belong to their producer, not the checker's globals. */
|
|
453
454
|
function isolateRuntime(guard, typesDir, files, runtimeRel) {
|
|
454
455
|
const runtimePath = path.join(typesDir, runtimeRel);
|
|
455
|
-
const program = ts.createProgram(files.map(file => path.join(typesDir, file)), {
|
|
456
|
+
const program = ts.createProgram(files.map(file => path.join(typesDir, file)), sidecarCompilerOptions({
|
|
456
457
|
strict: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext,
|
|
457
458
|
module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
458
459
|
types: [],
|
|
459
|
-
});
|
|
460
|
+
}));
|
|
460
461
|
const checker = program.getTypeChecker();
|
|
461
462
|
const runtime = program.getSourceFile(runtimePath);
|
|
462
463
|
const retained = new Set();
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
31
31
|
export type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
32
32
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
33
|
+
export { sidecarCompilerOptions } from './compiler-options.js';
|
|
33
34
|
export { serviceConfigPath } from './project-references.js';
|
|
34
35
|
export { findServiceTsconfig } from './service-config.js';
|
|
35
36
|
export { runCheck } from './check.js';
|
|
@@ -42,7 +42,9 @@ import { emitsAlike, ProjectGraph } from './project-references.js';
|
|
|
42
42
|
import { findServiceTsconfig } from './service-config.js';
|
|
43
43
|
import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
|
|
44
44
|
import { WriteGuard } from './guarded-fs.js';
|
|
45
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
45
46
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
47
|
+
export { sidecarCompilerOptions } from './compiler-options.js';
|
|
46
48
|
export { serviceConfigPath } from './project-references.js';
|
|
47
49
|
export { findServiceTsconfig } from './service-config.js';
|
|
48
50
|
// v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
|
|
@@ -256,7 +258,8 @@ export function captureStub(opts) {
|
|
|
256
258
|
}
|
|
257
259
|
// ---- Phase A: analysis program over placeholder entry + anchor sources ----
|
|
258
260
|
let resolved;
|
|
259
|
-
const
|
|
261
|
+
const progress = opts.onProgress ?? (() => { });
|
|
262
|
+
const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard, progress };
|
|
260
263
|
try {
|
|
261
264
|
resolved = ownerGroups && emitProject
|
|
262
265
|
? resolveAnchorsByOwner(opts, ownerGroups, emitProject, analysisCtx, errors)
|
|
@@ -277,16 +280,11 @@ export function captureStub(opts) {
|
|
|
277
280
|
}
|
|
278
281
|
const scratch = WriteGuard.scratch('carrick-capture-v2-');
|
|
279
282
|
const staging = scratch.dir;
|
|
280
|
-
|
|
281
|
-
// Input .d.ts files (ambient stubs, augmentation declarations, local
|
|
282
|
-
// hand-written declarations in the import closure) are never re-emitted by
|
|
283
|
-
// tsc; they must ship verbatim or the tree's references to them dangle.
|
|
284
|
-
const declarationSources = new Map();
|
|
285
|
-
const sourceByEmitted = new Map();
|
|
286
|
-
let emitPartial = false;
|
|
283
|
+
let written;
|
|
287
284
|
try {
|
|
285
|
+
progress('emit', 'emitting declarations');
|
|
288
286
|
guard.writeFile(entryPath, entryLines.join('\n') + '\n');
|
|
289
|
-
const emitOptions = {
|
|
287
|
+
const emitOptions = sidecarCompilerOptions({
|
|
290
288
|
...parsed.options,
|
|
291
289
|
// The load-bearing trio: emit declarations without checking, so
|
|
292
290
|
// type-error-laden and bare (no node_modules) checkouts still emit.
|
|
@@ -299,36 +297,14 @@ export function captureStub(opts) {
|
|
|
299
297
|
incremental: false,
|
|
300
298
|
outDir: staging,
|
|
301
299
|
rootDir: entryDir,
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// emitSkipped is PER-PROGRAM even when only one file's declaration emit
|
|
311
|
-
// failed (e.g. TS4023 from a hand-rolled ambient stub shadowing a real
|
|
312
|
-
// package): every other file's .d.ts was still written to the callback.
|
|
313
|
-
// Fail wholesale only when nothing at all emitted; otherwise keep the
|
|
314
|
-
// emitted subset and demote exactly the aliases it cannot support.
|
|
315
|
-
if (emitResult.emitSkipped && emitted.size === 0) {
|
|
316
|
-
return fail(stubDir, packageName, ['declaration emit was skipped']);
|
|
317
|
-
}
|
|
318
|
-
emitPartial = emitResult.emitSkipped;
|
|
319
|
-
for (const d of emitResult.diagnostics) {
|
|
320
|
-
errors.push(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
|
|
321
|
-
}
|
|
322
|
-
for (const sourceFile of program.getSourceFiles()) {
|
|
323
|
-
if (!sourceFile.isDeclarationFile)
|
|
324
|
-
continue;
|
|
325
|
-
const abs = path.resolve(sourceFile.fileName);
|
|
326
|
-
const rel = path.relative(entryDir, abs).split(path.sep).join('/');
|
|
327
|
-
if (rel.startsWith('..') || rel.includes('node_modules/'))
|
|
328
|
-
continue;
|
|
329
|
-
declarationSources.set(rel, sourceFile.getFullText());
|
|
330
|
-
sourceByEmitted.set(rel, abs);
|
|
331
|
-
}
|
|
300
|
+
});
|
|
301
|
+
written = emitDeclarations({
|
|
302
|
+
rootNames: [entryPath, ...augmentationSources],
|
|
303
|
+
options: emitOptions,
|
|
304
|
+
host: deno?.host(emitOptions),
|
|
305
|
+
staging,
|
|
306
|
+
entryDir,
|
|
307
|
+
});
|
|
332
308
|
}
|
|
333
309
|
catch (err) {
|
|
334
310
|
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
@@ -338,6 +314,9 @@ export function captureStub(opts) {
|
|
|
338
314
|
guard.unlink(entryPath);
|
|
339
315
|
scratch.guard.remove(staging);
|
|
340
316
|
}
|
|
317
|
+
const { emitted, declarationSources, sourceByEmitted } = written;
|
|
318
|
+
const emitPartial = written.partial;
|
|
319
|
+
errors.push(...written.diagnostics);
|
|
341
320
|
// ---- Partial-emit recovery ----
|
|
342
321
|
// The corpus-2 notifications-svc shape: one file's declaration emit was
|
|
343
322
|
// skipped but the rest of the tree emitted fine. Keep the tree; demote any
|
|
@@ -490,6 +469,7 @@ export function captureStub(opts) {
|
|
|
490
469
|
target: parsed.options.target !== undefined ? ts.ScriptTarget[parsed.options.target] : undefined,
|
|
491
470
|
}, null, 2) + '\n');
|
|
492
471
|
// ---- Capture-time self-check (per-alias closure attribution) ----
|
|
472
|
+
progress('self-check', 'checking the stub');
|
|
493
473
|
const aliases = selfCheckStub({
|
|
494
474
|
guard: stubGuard,
|
|
495
475
|
stubDir,
|
|
@@ -526,6 +506,58 @@ export function captureStub(opts) {
|
|
|
526
506
|
errors,
|
|
527
507
|
};
|
|
528
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Phase B: build the program of the final entry and emit its declarations.
|
|
511
|
+
*
|
|
512
|
+
* A function of its own so that its program dies with it (carrick#1916). The
|
|
513
|
+
* program is as large as the service, and nothing after the emit reads it:
|
|
514
|
+
* what the capture needs is the text returned here. While the emit ran inside
|
|
515
|
+
* `captureStub`, that one long function's frame went on holding the program
|
|
516
|
+
* for as long as the capture ran, so the self-check loaded its own program
|
|
517
|
+
* beside it; on a 2,345-file service that was 1,290 MB of a 2,741 MB peak.
|
|
518
|
+
*
|
|
519
|
+
* The caller builds the options and the host. A host is held by its program
|
|
520
|
+
* and holds none itself, so the caller keeping one keeps no program alive.
|
|
521
|
+
*
|
|
522
|
+
* Throws when the emit was skipped and wrote nothing.
|
|
523
|
+
*/
|
|
524
|
+
function emitDeclarations(args) {
|
|
525
|
+
const emitted = new Map();
|
|
526
|
+
const declarationSources = new Map();
|
|
527
|
+
const sourceByEmitted = new Map();
|
|
528
|
+
const program = ts.createProgram(args.rootNames, sidecarCompilerOptions(args.options), args.host);
|
|
529
|
+
const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
|
|
530
|
+
emitted.set(fileName, text);
|
|
531
|
+
if (sources?.[0])
|
|
532
|
+
sourceByEmitted.set(path.relative(args.staging, fileName).split(path.sep).join('/'), sources[0].fileName);
|
|
533
|
+
}, undefined,
|
|
534
|
+
/* emitOnlyDtsFiles */ true);
|
|
535
|
+
// emitSkipped is PER-PROGRAM even when only one file's declaration emit
|
|
536
|
+
// failed (e.g. TS4023 from a hand-rolled ambient stub shadowing a real
|
|
537
|
+
// package): every other file's .d.ts was still written to the callback.
|
|
538
|
+
// Fail wholesale only when nothing at all emitted; otherwise keep the
|
|
539
|
+
// emitted subset and demote exactly the aliases it cannot support.
|
|
540
|
+
if (emitResult.emitSkipped && emitted.size === 0) {
|
|
541
|
+
throw new Error('declaration emit was skipped');
|
|
542
|
+
}
|
|
543
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
544
|
+
if (!sourceFile.isDeclarationFile)
|
|
545
|
+
continue;
|
|
546
|
+
const abs = path.resolve(sourceFile.fileName);
|
|
547
|
+
const rel = path.relative(args.entryDir, abs).split(path.sep).join('/');
|
|
548
|
+
if (rel.startsWith('..') || rel.includes('node_modules/'))
|
|
549
|
+
continue;
|
|
550
|
+
declarationSources.set(rel, sourceFile.getFullText());
|
|
551
|
+
sourceByEmitted.set(rel, abs);
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
emitted,
|
|
555
|
+
declarationSources,
|
|
556
|
+
sourceByEmitted,
|
|
557
|
+
partial: emitResult.emitSkipped,
|
|
558
|
+
diagnostics: emitResult.diagnostics.map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n')),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
529
561
|
/**
|
|
530
562
|
* Partial-emit demotion: demote every anchor whose alias text names, by a
|
|
531
563
|
* relative or an absolute path, a module the stub will not resolve, and
|
|
@@ -655,15 +687,21 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
655
687
|
}
|
|
656
688
|
ctx.guard.writeFile(ctx.entryPath, placeholderLines.join('\n') + '\n');
|
|
657
689
|
try {
|
|
690
|
+
ctx.progress('program', 'building the program the anchors are read in');
|
|
658
691
|
const anchorSources = [
|
|
659
692
|
...new Set(opts.anchors
|
|
660
693
|
.flatMap((a) => (a.source_file ? [path.join(ctx.repoRoot, a.source_file)] : []))),
|
|
661
694
|
].filter((f) => fs.existsSync(f));
|
|
662
|
-
const options = {
|
|
695
|
+
const options = sidecarCompilerOptions({
|
|
663
696
|
...parsed.options,
|
|
664
697
|
noEmit: true,
|
|
665
|
-
};
|
|
698
|
+
});
|
|
666
699
|
const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, deno?.host(options));
|
|
700
|
+
// The checker binds every file of the program when it is first asked for,
|
|
701
|
+
// which the first anchor would otherwise do: asked for here, so that the
|
|
702
|
+
// report below says the program is whole before any anchor is read.
|
|
703
|
+
program.getTypeChecker();
|
|
704
|
+
ctx.progress('anchors', `0 of ${opts.anchors.length}`);
|
|
667
705
|
const entrySource = program.getSourceFile(ctx.entryPath);
|
|
668
706
|
const placeholders = new Map();
|
|
669
707
|
if (entrySource) {
|
|
@@ -696,13 +734,17 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
696
734
|
siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file, resolveFromEntry));
|
|
697
735
|
}
|
|
698
736
|
}
|
|
699
|
-
return opts.anchors.map((request) =>
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
737
|
+
return opts.anchors.map((request, index) => {
|
|
738
|
+
const anchor = resolveAnchor(program, request, {
|
|
739
|
+
repoRoot: ctx.repoRoot,
|
|
740
|
+
entryDir: ctx.entryDir,
|
|
741
|
+
placeholder: placeholders.get(request.alias),
|
|
742
|
+
siblingSymbolSpecs,
|
|
743
|
+
resolveFromEntry,
|
|
744
|
+
});
|
|
745
|
+
ctx.progress('anchors', `${index + 1} of ${opts.anchors.length}`);
|
|
746
|
+
return anchor;
|
|
747
|
+
});
|
|
706
748
|
}
|
|
707
749
|
finally {
|
|
708
750
|
if (fs.existsSync(ctx.entryPath))
|
|
@@ -24,16 +24,17 @@
|
|
|
24
24
|
import ts from 'typescript';
|
|
25
25
|
import * as fs from 'node:fs';
|
|
26
26
|
import * as path from 'node:path';
|
|
27
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
27
28
|
import { isPublishedSemver } from './lockfile.js';
|
|
28
29
|
const DECLARATION_SUFFIXES = ['', '.d.ts', '.d.mts', '.d.cts', '.ts', '.mts', '.cts', '/index.d.ts'];
|
|
29
|
-
const RESOLUTION_OPTIONS = {
|
|
30
|
+
const RESOLUTION_OPTIONS = sidecarCompilerOptions({
|
|
30
31
|
module: ts.ModuleKind.ESNext,
|
|
31
32
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
32
33
|
target: ts.ScriptTarget.ESNext,
|
|
33
34
|
noEmit: true,
|
|
34
35
|
skipLibCheck: true,
|
|
35
36
|
types: [],
|
|
36
|
-
};
|
|
37
|
+
});
|
|
37
38
|
/** DefinitelyTyped packages are imported by the runtime name (TS6137). */
|
|
38
39
|
function publicNameOf(name) {
|
|
39
40
|
if (!name.startsWith('@types/'))
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import ts from 'typescript';
|
|
33
33
|
import * as fs from 'node:fs';
|
|
34
34
|
import * as path from 'node:path';
|
|
35
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
35
36
|
import { collectSpecifiers, declarationCandidates, isDeclarationFileName, isRelative, packageNameOf, } from './specifiers.js';
|
|
36
37
|
import { repairDanglingImports } from './repair-dangling.js';
|
|
37
38
|
import { findDisqualifyingTopTypes, findUnresolvedPlaceholders, isErrorPlaceholder, provenanceOf, unemittedModuleProvenance, unresolvedInTreeProvenance, } from './deep-walk.js';
|
|
@@ -78,7 +79,7 @@ export function selfCheckStub(args) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
function runSelfCheck(args, treeFiles, repaired) {
|
|
81
|
-
const options = {
|
|
82
|
+
const options = sidecarCompilerOptions({
|
|
82
83
|
noEmit: true,
|
|
83
84
|
strict: true,
|
|
84
85
|
// MUST be false: the whole stub tree is .d.ts (see module header).
|
|
@@ -86,7 +87,7 @@ function runSelfCheck(args, treeFiles, repaired) {
|
|
|
86
87
|
module: ts.ModuleKind.ESNext,
|
|
87
88
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
88
89
|
types: [],
|
|
89
|
-
};
|
|
90
|
+
});
|
|
90
91
|
const program = ts.createProgram(treeFiles, options, args.compilerHost?.(options));
|
|
91
92
|
const checker = program.getTypeChecker();
|
|
92
93
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
@@ -26,11 +26,12 @@
|
|
|
26
26
|
* inference path in `type-inferrer.ts`), which walks the resolved `Type` and
|
|
27
27
|
* rebuilds the inlined text.
|
|
28
28
|
*
|
|
29
|
-
* Each resolve call builds its own throwaway
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Each resolve call builds its own throwaway project over the stub tree and
|
|
30
|
+
* reads nothing else: not the service's project, and not whether the sidecar
|
|
31
|
+
* was ever initialised. So the request costs what the stub costs, in a process
|
|
32
|
+
* that has built nothing as in one that has (carrick#1927), and the sidecar's
|
|
33
|
+
* long-lived project never sees stub files.
|
|
32
34
|
*/
|
|
33
|
-
import { Project } from 'ts-morph';
|
|
34
35
|
export interface ResolvedDefinition {
|
|
35
36
|
type_alias: string;
|
|
36
37
|
/** Original declaration text as written (preserves named types) */
|
|
@@ -39,10 +40,6 @@ export interface ResolvedDefinition {
|
|
|
39
40
|
expanded: string;
|
|
40
41
|
}
|
|
41
42
|
export declare class DefinitionResolver {
|
|
42
|
-
private readonly project;
|
|
43
|
-
constructor(options: {
|
|
44
|
-
project: Project;
|
|
45
|
-
});
|
|
46
43
|
/**
|
|
47
44
|
* Resolve surface aliases from a capture stub package directory
|
|
48
45
|
* (`<stub_dir>/types/surface.d.ts` + its declaration tree).
|
|
@@ -26,19 +26,18 @@
|
|
|
26
26
|
* inference path in `type-inferrer.ts`), which walks the resolved `Type` and
|
|
27
27
|
* rebuilds the inlined text.
|
|
28
28
|
*
|
|
29
|
-
* Each resolve call builds its own throwaway
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Each resolve call builds its own throwaway project over the stub tree and
|
|
30
|
+
* reads nothing else: not the service's project, and not whether the sidecar
|
|
31
|
+
* was ever initialised. So the request costs what the stub costs, in a process
|
|
32
|
+
* that has built nothing as in one that has (carrick#1927), and the sidecar's
|
|
33
|
+
* long-lived project never sees stub files.
|
|
32
34
|
*/
|
|
33
35
|
import * as path from 'node:path';
|
|
34
36
|
import * as fs from 'node:fs';
|
|
35
37
|
import { Project, Node } from 'ts-morph';
|
|
38
|
+
import { sidecarCompilerOptions } from './capture/index.js';
|
|
36
39
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
37
40
|
export class DefinitionResolver {
|
|
38
|
-
project;
|
|
39
|
-
constructor(options) {
|
|
40
|
-
this.project = options.project;
|
|
41
|
-
}
|
|
42
41
|
/**
|
|
43
42
|
* Resolve surface aliases from a capture stub package directory
|
|
44
43
|
* (`<stub_dir>/types/surface.d.ts` + its declaration tree).
|
|
@@ -59,13 +58,13 @@ export class DefinitionResolver {
|
|
|
59
58
|
}
|
|
60
59
|
try {
|
|
61
60
|
const stubProject = new Project({
|
|
62
|
-
compilerOptions: {
|
|
61
|
+
compilerOptions: sidecarCompilerOptions({
|
|
63
62
|
target: 99, // ESNext
|
|
64
63
|
module: 99, // ESNext
|
|
65
64
|
moduleResolution: 100, // Bundler
|
|
66
65
|
strict: true,
|
|
67
66
|
skipLibCheck: true,
|
|
68
|
-
},
|
|
67
|
+
}),
|
|
69
68
|
skipAddingFilesFromTsConfig: true,
|
|
70
69
|
});
|
|
71
70
|
for (const filePath of walkDtsFiles(typesDir)) {
|
|
@@ -27,6 +27,8 @@ import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck,
|
|
|
27
27
|
import { Retyper } from './retype.js';
|
|
28
28
|
import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
29
29
|
import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
|
|
30
|
+
import { mergeInferTimings } from './infer-timing.js';
|
|
31
|
+
import { addProgramFiles, listProgramFiles } from './program-files.js';
|
|
30
32
|
// ===========================================================================
|
|
31
33
|
// Module-level state
|
|
32
34
|
// ===========================================================================
|
|
@@ -38,6 +40,20 @@ let initTimeMs = null;
|
|
|
38
40
|
* the service's files (carrick#1604).
|
|
39
41
|
*/
|
|
40
42
|
let components = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* Reads a capture stub's own declaration tree and nothing of the init'd
|
|
45
|
+
* project, so it is not one of the project's components: taking it from them
|
|
46
|
+
* would build the service's whole program to answer about the stub
|
|
47
|
+
* (carrick#1927).
|
|
48
|
+
*/
|
|
49
|
+
const definitionResolver = new DefinitionResolver();
|
|
50
|
+
/** The init'd project loader, or the error every project-backed action gives without one. */
|
|
51
|
+
function initializedLoader() {
|
|
52
|
+
if (!projectLoader?.isInitialized()) {
|
|
53
|
+
throw new Error('Sidecar not initialized. Call init first.');
|
|
54
|
+
}
|
|
55
|
+
return projectLoader;
|
|
56
|
+
}
|
|
41
57
|
/**
|
|
42
58
|
* Get the project-backed components, building the project if this is the
|
|
43
59
|
* first request that needs it.
|
|
@@ -45,15 +61,12 @@ let components = new Map();
|
|
|
45
61
|
* @throws if init has not run, or if the project cannot be built
|
|
46
62
|
*/
|
|
47
63
|
function projectComponents(key = '') {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
// Bound here rather than read from the module slot inside the components:
|
|
65
|
+
// a re-init drops `components` and points the slot at another service, and
|
|
66
|
+
// nothing built over this project may follow it there.
|
|
67
|
+
const loader = initializedLoader();
|
|
51
68
|
let built = components.get(key);
|
|
52
69
|
if (!built) {
|
|
53
|
-
// Bound here rather than read from the module slot inside the components:
|
|
54
|
-
// a re-init drops `components` and points the slot at another service, and
|
|
55
|
-
// nothing built over this project may follow it there.
|
|
56
|
-
const loader = projectLoader;
|
|
57
70
|
const project = loader.getProjectFor(key);
|
|
58
71
|
const repoRoot = loader.getRepoRoot();
|
|
59
72
|
// The module graph, where the project resolved through one, is the only
|
|
@@ -67,7 +80,6 @@ function projectComponents(key = '') {
|
|
|
67
80
|
built = {
|
|
68
81
|
typeBundler: new TypeBundler({ project, repoRoot }),
|
|
69
82
|
typeInferrer,
|
|
70
|
-
definitionResolver: new DefinitionResolver({ project }),
|
|
71
83
|
// Locates calls exactly as `infer` does, so it rewrites the node the
|
|
72
84
|
// consumer's published type came from (carrick#1491).
|
|
73
85
|
// The walk is typed against the capture bundle's compiler copy and
|
|
@@ -218,6 +230,10 @@ function handleBundle(request) {
|
|
|
218
230
|
function handleCaptureV2(request) {
|
|
219
231
|
try {
|
|
220
232
|
log(`capture_v2 for service '${request.service_name}' (${request.anchors.length} anchor(s))`);
|
|
233
|
+
// A frame for each stage as the capture reaches it, and within a stage
|
|
234
|
+
// (one report per anchor) at most one per interval.
|
|
235
|
+
let stage;
|
|
236
|
+
const withinStage = atMostEvery(PROGRESS_INTERVAL_MS, (phase, message) => writeProgress(request.request_id, phase, message));
|
|
221
237
|
const result = captureStub({
|
|
222
238
|
repoRoot: request.repo_root,
|
|
223
239
|
serviceName: request.service_name,
|
|
@@ -225,6 +241,12 @@ function handleCaptureV2(request) {
|
|
|
225
241
|
outDir: request.out_dir,
|
|
226
242
|
tsconfigPath: request.tsconfig_path,
|
|
227
243
|
scanRoot: request.scan_root,
|
|
244
|
+
onProgress: (phase, message) => {
|
|
245
|
+
if (phase === stage)
|
|
246
|
+
return withinStage(phase, message);
|
|
247
|
+
stage = phase;
|
|
248
|
+
writeProgress(request.request_id, phase, message);
|
|
249
|
+
},
|
|
228
250
|
});
|
|
229
251
|
return {
|
|
230
252
|
request_id: request.request_id,
|
|
@@ -316,6 +338,9 @@ function handleInfer(request) {
|
|
|
316
338
|
request_id: request.request_id,
|
|
317
339
|
status: result.success ? 'success' : 'error',
|
|
318
340
|
inferred_types: result.inferred_types,
|
|
341
|
+
// Beside the answers and never in them: what the requests cost, for
|
|
342
|
+
// the caller's log (carrick#1985).
|
|
343
|
+
infer_timing: mergeInferTimings(results.map((r) => r.timing)),
|
|
319
344
|
errors: result.errors,
|
|
320
345
|
};
|
|
321
346
|
}
|
|
@@ -345,11 +370,19 @@ function handleRetypeCheck(request) {
|
|
|
345
370
|
const groups = byProject(request.items.map((item, index) => ({ item, index })), ({ item }) => item.file_path);
|
|
346
371
|
const deadline = performance.now() + budget;
|
|
347
372
|
const outcomes = new Array(request.items.length);
|
|
373
|
+
// One count for the request, whichever programs its items span, reported
|
|
374
|
+
// as `infer` reports its batch (carrick#1914, carrick#1945).
|
|
375
|
+
let judged = 0;
|
|
376
|
+
const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'retype', `${judged} of ${request.items.length}`));
|
|
377
|
+
const onJudged = () => {
|
|
378
|
+
judged += 1;
|
|
379
|
+
report();
|
|
380
|
+
};
|
|
348
381
|
for (const [key, entries] of groups) {
|
|
349
382
|
// One budget for the request, whichever programs it spans.
|
|
350
383
|
const remaining = groups.size === 1 ? budget : Math.max(0, deadline - performance.now());
|
|
351
384
|
projectComponents(key)
|
|
352
|
-
.retyper.run(entries.map(({ item }) => item), remaining)
|
|
385
|
+
.retyper.run(entries.map(({ item }) => item), remaining, onJudged)
|
|
353
386
|
.forEach((outcome, position) => {
|
|
354
387
|
outcomes[entries[position].index] = outcome;
|
|
355
388
|
});
|
|
@@ -446,12 +479,13 @@ function handleListLibrarySurface(request) {
|
|
|
446
479
|
}
|
|
447
480
|
/**
|
|
448
481
|
* Handle the 'resolve_definitions' action - resolve surface aliases from a
|
|
449
|
-
* v2 capture stub package's declaration tree.
|
|
482
|
+
* v2 capture stub package's declaration tree. Stateless, as `capture_v2` is:
|
|
483
|
+
* it needs no init and builds no project but the stub's own.
|
|
450
484
|
*/
|
|
451
485
|
function handleResolveDefinitions(request) {
|
|
452
486
|
try {
|
|
453
487
|
log(`Resolving ${request.aliases.length} type alias(es) from ${request.stub_dir}`);
|
|
454
|
-
const results =
|
|
488
|
+
const results = definitionResolver.resolveFromStub(request.stub_dir, request.aliases);
|
|
455
489
|
return {
|
|
456
490
|
request_id: request.request_id,
|
|
457
491
|
status: 'success',
|
|
@@ -472,6 +506,55 @@ function handleResolveDefinitions(request) {
|
|
|
472
506
|
};
|
|
473
507
|
}
|
|
474
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Handle the 'list_program_files' action - the root files the default
|
|
511
|
+
* project's program was built from, in order (carrick#2027). It is the only
|
|
512
|
+
* program a request grows: a file a referenced project owns is in that
|
|
513
|
+
* project's program from its tsconfig, so a program built for an owning
|
|
514
|
+
* project (carrick#1604) holds the same files in every process. A process
|
|
515
|
+
* that has not built the default project lists nothing, and does not build
|
|
516
|
+
* it to answer.
|
|
517
|
+
*/
|
|
518
|
+
function handleListProgramFiles(request) {
|
|
519
|
+
try {
|
|
520
|
+
const project = initializedLoader().builtProject();
|
|
521
|
+
const files = project ? listProgramFiles(project) : [];
|
|
522
|
+
log(`Listing ${files.length} program file(s)`);
|
|
523
|
+
return { request_id: request.request_id, status: 'success', files };
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
527
|
+
logError(`Listing program files failed: ${error}`);
|
|
528
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Handle the 'add_program_files' action - add the given files to the default
|
|
533
|
+
* project's program in the given order, rebuilding it at most once
|
|
534
|
+
* (carrick#2027). A file a referenced project owns is skipped, as an `infer`
|
|
535
|
+
* on it would never add it to this program: its owner's program already holds
|
|
536
|
+
* it.
|
|
537
|
+
*/
|
|
538
|
+
function handleAddProgramFiles(request) {
|
|
539
|
+
try {
|
|
540
|
+
const loader = initializedLoader();
|
|
541
|
+
const startTime = performance.now();
|
|
542
|
+
const files = request.files
|
|
543
|
+
.map((file) => path.resolve(loader.getRepoRoot(), file))
|
|
544
|
+
.filter((file) => loader.projectKeyFor(file) === '');
|
|
545
|
+
const added = files.length === 0
|
|
546
|
+
? 0
|
|
547
|
+
: addProgramFiles(loader.getProject(), files, (file, reason) => logError(`Not added to the program: ${file}: ${reason}`));
|
|
548
|
+
log(`Added ${added} of ${request.files.length} file(s) to the program in ` +
|
|
549
|
+
`${Math.round(performance.now() - startTime)}ms`);
|
|
550
|
+
return { request_id: request.request_id, status: 'success', added };
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
554
|
+
logError(`Adding program files failed: ${error}`);
|
|
555
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
475
558
|
/**
|
|
476
559
|
* Handle the 'health' action - report initialization status
|
|
477
560
|
*/
|
|
@@ -524,6 +607,10 @@ function handleRequest(request) {
|
|
|
524
607
|
return handleVerifyLibraryClaims(request);
|
|
525
608
|
case 'list_library_surface':
|
|
526
609
|
return handleListLibrarySurface(request);
|
|
610
|
+
case 'list_program_files':
|
|
611
|
+
return handleListProgramFiles(request);
|
|
612
|
+
case 'add_program_files':
|
|
613
|
+
return handleAddProgramFiles(request);
|
|
527
614
|
case 'health':
|
|
528
615
|
return handleHealth(request);
|
|
529
616
|
case 'shutdown':
|
|
@@ -619,6 +706,31 @@ function processLine(line) {
|
|
|
619
706
|
const response = handleRequest(parseResult.request);
|
|
620
707
|
writeResponse(response);
|
|
621
708
|
}
|
|
709
|
+
/** How often the sidecar looks for the process that started it. */
|
|
710
|
+
const PARENT_CHECK_MS = 1000;
|
|
711
|
+
/**
|
|
712
|
+
* Exit once the process that started this one is gone (carrick#2029).
|
|
713
|
+
*
|
|
714
|
+
* Its death closes stdin, which ends this process, unless another process
|
|
715
|
+
* still holds the write end. Then the only sign is that this process has a
|
|
716
|
+
* new parent: whatever adopts it (init, or a subreaper on Linux) has a pid of
|
|
717
|
+
* its own. So the parent at startup is kept and compared, never 1. A process
|
|
718
|
+
* already adopted at startup has no parent to watch, and stdin alone decides.
|
|
719
|
+
*
|
|
720
|
+
* The timer is unref'd: it never keeps an otherwise finished process alive.
|
|
721
|
+
* A handler that blocks the event loop holds it off until it returns.
|
|
722
|
+
*/
|
|
723
|
+
function exitWhenParentIsGone() {
|
|
724
|
+
const startedBy = process.ppid;
|
|
725
|
+
if (startedBy <= 1)
|
|
726
|
+
return;
|
|
727
|
+
setInterval(() => {
|
|
728
|
+
if (process.ppid !== startedBy) {
|
|
729
|
+
log(`parent ${startedBy} is gone, exiting`);
|
|
730
|
+
process.exit(0);
|
|
731
|
+
}
|
|
732
|
+
}, PARENT_CHECK_MS).unref();
|
|
733
|
+
}
|
|
622
734
|
/**
|
|
623
735
|
* Start the message loop
|
|
624
736
|
*/
|
|
@@ -634,6 +746,7 @@ function main() {
|
|
|
634
746
|
log('stdin closed, exiting');
|
|
635
747
|
process.exit(0);
|
|
636
748
|
});
|
|
749
|
+
exitWhenParentIsGone();
|
|
637
750
|
// Handle process signals
|
|
638
751
|
process.on('SIGINT', () => {
|
|
639
752
|
log('SIGINT received, exiting');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which requests of an `infer` batch were slow, and at what (carrick#1985).
|
|
3
|
+
*
|
|
4
|
+
* A pass can send thousands of requests and spend most of its time in a few
|
|
5
|
+
* of them. The inferrer times every request; this module turns those timings
|
|
6
|
+
* into what the batch's answer carries: how many there were and what they
|
|
7
|
+
* add up to, the slowest few by name, the request that printed the longest
|
|
8
|
+
* type, and the requests that were the first asked of their file, apart.
|
|
9
|
+
*
|
|
10
|
+
* A request's time is also told apart by what it went on: the compiler call
|
|
11
|
+
* that computes the type, and the print of that type to text. The two have
|
|
12
|
+
* different remedies, and a printed length alone cannot say which one a slow
|
|
13
|
+
* request was: a type of four characters can take seconds to compute, and a
|
|
14
|
+
* type of a million can take half a second to print.
|
|
15
|
+
*
|
|
16
|
+
* It measures and names. It changes no answer and holds no type text.
|
|
17
|
+
*/
|
|
18
|
+
import type { InferSlotTiming, InferTiming } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* How many of its slowest requests a batch's answer names.
|
|
21
|
+
*
|
|
22
|
+
* Enough for a reader adding batches up to say what the slowest hundredth of
|
|
23
|
+
* a whole pass took: a pass's slowest requests are among its batches'
|
|
24
|
+
* slowest, and the last one a batch names is the most any it left out can
|
|
25
|
+
* have taken.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SLOWEST_SLOTS = 25;
|
|
28
|
+
/** How long something that started at `startedMs` has taken by `nowMs`. */
|
|
29
|
+
export declare function elapsedMs(startedMs: number, nowMs: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* The parts of a request's time that are told apart: `type` is the compiler
|
|
32
|
+
* call that computes a type, `print` is the print of a type to text.
|
|
33
|
+
*/
|
|
34
|
+
export type TimedPhase = 'type' | 'print';
|
|
35
|
+
/**
|
|
36
|
+
* Run `work`, and count how long it took as `phase`.
|
|
37
|
+
*
|
|
38
|
+
* The count is the process's, not a request's: whoever wants one request's
|
|
39
|
+
* share reads `phaseClock` before the request and after it. One request runs
|
|
40
|
+
* at a time, so the difference is that request's and nobody else's.
|
|
41
|
+
*/
|
|
42
|
+
export declare function timedPhase<T>(phase: TimedPhase, work: () => T): T;
|
|
43
|
+
/** What each phase has come to so far, in milliseconds. */
|
|
44
|
+
export declare function phaseClock(): Readonly<Record<TimedPhase, number>>;
|
|
45
|
+
/**
|
|
46
|
+
* What a batch's answer says of the time its requests took. `slots` holds one
|
|
47
|
+
* timing per request, in the order the batch was done with them.
|
|
48
|
+
*/
|
|
49
|
+
export declare function inferTiming(slots: readonly InferSlotTiming[]): InferTiming;
|
|
50
|
+
/**
|
|
51
|
+
* One timing for a request several projects answered, each with its own:
|
|
52
|
+
* the counts and the times added up, and the slowest of all of them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mergeInferTimings(parts: readonly InferTiming[]): InferTiming;
|