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.
Files changed (40) hide show
  1. package/package.json +9 -10
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/capture/api.d.ts +15 -0
  4. package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
  5. package/sidecar/dist/src/capture/check-classify.js +20 -0
  6. package/sidecar/dist/src/capture/check-deep.js +2 -1
  7. package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
  8. package/sidecar/dist/src/capture/check-fields.js +54 -36
  9. package/sidecar/dist/src/capture/check-union.d.ts +40 -0
  10. package/sidecar/dist/src/capture/check-union.js +92 -0
  11. package/sidecar/dist/src/capture/check-workspace.d.ts +1 -0
  12. package/sidecar/dist/src/capture/check-workspace.js +13 -2
  13. package/sidecar/dist/src/capture/check.js +5 -0
  14. package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
  15. package/sidecar/dist/src/capture/compiler-options.js +100 -0
  16. package/sidecar/dist/src/capture/deno-project.js +3 -2
  17. package/sidecar/dist/src/capture/index.d.ts +1 -0
  18. package/sidecar/dist/src/capture/index.js +90 -48
  19. package/sidecar/dist/src/capture/installed-package.js +3 -2
  20. package/sidecar/dist/src/capture/self-check.js +3 -2
  21. package/sidecar/dist/src/definition-resolver.d.ts +5 -8
  22. package/sidecar/dist/src/definition-resolver.js +8 -9
  23. package/sidecar/dist/src/index.js +124 -11
  24. package/sidecar/dist/src/infer-timing.d.ts +54 -0
  25. package/sidecar/dist/src/infer-timing.js +124 -0
  26. package/sidecar/dist/src/print-type.d.ts +48 -0
  27. package/sidecar/dist/src/print-type.js +71 -0
  28. package/sidecar/dist/src/printed-names.js +2 -1
  29. package/sidecar/dist/src/program-files.d.ts +44 -0
  30. package/sidecar/dist/src/program-files.js +64 -0
  31. package/sidecar/dist/src/project-loader.d.ts +5 -0
  32. package/sidecar/dist/src/project-loader.js +14 -5
  33. package/sidecar/dist/src/retype.d.ts +4 -2
  34. package/sidecar/dist/src/retype.js +16 -5
  35. package/sidecar/dist/src/type-inferrer.d.ts +157 -1
  36. package/sidecar/dist/src/type-inferrer.js +444 -20
  37. package/sidecar/dist/src/types.d.ts +104 -2
  38. package/sidecar/dist/src/validators.d.ts +166 -80
  39. package/sidecar/dist/src/validators.js +13 -0
  40. package/templates/skills/carrick-reuse.md +6 -5
@@ -0,0 +1,124 @@
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
+ /**
19
+ * How many of its slowest requests a batch's answer names.
20
+ *
21
+ * Enough for a reader adding batches up to say what the slowest hundredth of
22
+ * a whole pass took: a pass's slowest requests are among its batches'
23
+ * slowest, and the last one a batch names is the most any it left out can
24
+ * have taken.
25
+ */
26
+ export const SLOWEST_SLOTS = 25;
27
+ /** Milliseconds to the microsecond: sums of these stay readable. */
28
+ const rounded = (ms) => Math.round(ms * 1000) / 1000;
29
+ /** How long something that started at `startedMs` has taken by `nowMs`. */
30
+ export function elapsedMs(startedMs, nowMs) {
31
+ return rounded(nowMs - startedMs);
32
+ }
33
+ /** What this process has spent in each phase since it started, in milliseconds. */
34
+ const spent = { type: 0, print: 0 };
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 function timedPhase(phase, work) {
43
+ const started = performance.now();
44
+ try {
45
+ return work();
46
+ }
47
+ finally {
48
+ spent[phase] += performance.now() - started;
49
+ }
50
+ }
51
+ /** What each phase has come to so far, in milliseconds. */
52
+ export function phaseClock() {
53
+ return { ...spent };
54
+ }
55
+ /**
56
+ * The `count` slowest of `slots`, slowest first. Requests that took the same
57
+ * time keep the order they are in.
58
+ */
59
+ function slowestOf(slots, count) {
60
+ return [...slots].sort((a, b) => b.ms - a.ms).slice(0, count);
61
+ }
62
+ /**
63
+ * The one of `slots` that printed the longest type, the earliest of those
64
+ * that tie; undefined when none printed anything.
65
+ */
66
+ function longestPrintedOf(slots) {
67
+ let longest;
68
+ for (const slot of slots) {
69
+ if (slot.printed_length > (longest?.printed_length ?? 0))
70
+ longest = slot;
71
+ }
72
+ return longest;
73
+ }
74
+ /**
75
+ * What a batch's answer says of the time its requests took. `slots` holds one
76
+ * timing per request, in the order the batch was done with them.
77
+ */
78
+ export function inferTiming(slots) {
79
+ let slotsMs = 0;
80
+ let typeMs = 0;
81
+ let printMs = 0;
82
+ let firstInFile = 0;
83
+ let firstInFileMs = 0;
84
+ for (const slot of slots) {
85
+ slotsMs += slot.ms;
86
+ typeMs += slot.type_ms;
87
+ printMs += slot.print_ms;
88
+ if (slot.first_in_file) {
89
+ firstInFile += 1;
90
+ firstInFileMs += slot.ms;
91
+ }
92
+ }
93
+ const longest = longestPrintedOf(slots);
94
+ return {
95
+ slots: slots.length,
96
+ slots_ms: rounded(slotsMs),
97
+ type_ms: rounded(typeMs),
98
+ print_ms: rounded(printMs),
99
+ first_in_file_slots: firstInFile,
100
+ first_in_file_ms: rounded(firstInFileMs),
101
+ slowest: slowestOf(slots, SLOWEST_SLOTS),
102
+ ...(longest ? { longest_printed: longest } : {}),
103
+ };
104
+ }
105
+ /**
106
+ * One timing for a request several projects answered, each with its own:
107
+ * the counts and the times added up, and the slowest of all of them.
108
+ */
109
+ export function mergeInferTimings(parts) {
110
+ if (parts.length === 1)
111
+ return parts[0];
112
+ const sum = (read) => parts.reduce((total, part) => total + read(part), 0);
113
+ const longest = longestPrintedOf(parts.flatMap((part) => part.longest_printed ?? []));
114
+ return {
115
+ slots: sum((part) => part.slots),
116
+ slots_ms: rounded(sum((part) => part.slots_ms)),
117
+ type_ms: rounded(sum((part) => part.type_ms)),
118
+ print_ms: rounded(sum((part) => part.print_ms)),
119
+ first_in_file_slots: sum((part) => part.first_in_file_slots),
120
+ first_in_file_ms: rounded(sum((part) => part.first_in_file_ms)),
121
+ slowest: slowestOf(parts.flatMap((part) => part.slowest), SLOWEST_SLOTS),
122
+ ...(longest ? { longest_printed: longest } : {}),
123
+ };
124
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The compiler's own print of a type, with a module the print names written
3
+ * as the file that declares it (carrick#2019).
4
+ *
5
+ * When a type comes from a module the enclosing file does not import, the
6
+ * compiler writes it as `import("<module>").Name`. TypeScript 5 wrote the
7
+ * declaring file's path there: the node builder had no module-specifier host
8
+ * unless a declaration emit gave it one. TypeScript 6 gives every print a
9
+ * host, so the same type printed at an enclosing node reads
10
+ * `import("../model").Name` or `import("<package>").Name`: a specifier that
11
+ * depends on which file it was printed from, so one type printed from two
12
+ * files gives two texts. Everything downstream reads the declaring file's
13
+ * path: the scanner's path scrub writes it repo-relative or as
14
+ * `<package>@<version>`, and a capture pastes an inferred text into its
15
+ * surface entry, where a specifier relative to another file names nothing.
16
+ *
17
+ * These functions run the compiler's own code with no host: a tracker whose
18
+ * `moduleResolverHost` is `false` (falsy, so the builder writes the declaring
19
+ * file; not nullish, so the builder does not supply its own). Nothing in the
20
+ * text is rewritten afterwards. `typeToStringAsDeclared` is the checker's
21
+ * `typeToString`, step for step, with that one tracker.
22
+ *
23
+ * Only prints with an enclosing node differ: with none, the compiler has no
24
+ * file to write a specifier relative to and writes the declaring file
25
+ * anyway. A print that must name modules the way a declaration file would
26
+ * (the capture's surface emit) passes a host of its own and does not use
27
+ * this.
28
+ */
29
+ import { ts } from 'ts-morph';
30
+ /**
31
+ * The compiler's internal `SymbolTracker`: callbacks the node builder reports
32
+ * to, and the module-specifier host it writes specifiers with.
33
+ */
34
+ export interface SymbolTracker {
35
+ moduleResolverHost?: unknown;
36
+ }
37
+ /**
38
+ * The checker's `typeToTypeNode`, writing each module a node names as the
39
+ * file that declares it. A caller's own tracker keeps its callbacks.
40
+ */
41
+ export declare function typeToTypeNodeAsDeclared(checker: ts.TypeChecker, type: ts.Type, enclosing: ts.Node | undefined, flags: ts.NodeBuilderFlags, tracker?: SymbolTracker): ts.TypeNode | undefined;
42
+ /**
43
+ * The checker's `typeToString(type, enclosing, flags)`, writing each module
44
+ * the print names as the file that declares it. `noErrorTruncation` is the
45
+ * program's option of that name, which `typeToString` reads when `flags`
46
+ * leave truncation on.
47
+ */
48
+ export declare function typeToStringAsDeclared(checker: ts.TypeChecker, type: ts.Type, enclosing: ts.Node | undefined, flags?: ts.TypeFormatFlags, noErrorTruncation?: boolean): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The compiler's own print of a type, with a module the print names written
3
+ * as the file that declares it (carrick#2019).
4
+ *
5
+ * When a type comes from a module the enclosing file does not import, the
6
+ * compiler writes it as `import("<module>").Name`. TypeScript 5 wrote the
7
+ * declaring file's path there: the node builder had no module-specifier host
8
+ * unless a declaration emit gave it one. TypeScript 6 gives every print a
9
+ * host, so the same type printed at an enclosing node reads
10
+ * `import("../model").Name` or `import("<package>").Name`: a specifier that
11
+ * depends on which file it was printed from, so one type printed from two
12
+ * files gives two texts. Everything downstream reads the declaring file's
13
+ * path: the scanner's path scrub writes it repo-relative or as
14
+ * `<package>@<version>`, and a capture pastes an inferred text into its
15
+ * surface entry, where a specifier relative to another file names nothing.
16
+ *
17
+ * These functions run the compiler's own code with no host: a tracker whose
18
+ * `moduleResolverHost` is `false` (falsy, so the builder writes the declaring
19
+ * file; not nullish, so the builder does not supply its own). Nothing in the
20
+ * text is rewritten afterwards. `typeToStringAsDeclared` is the checker's
21
+ * `typeToString`, step for step, with that one tracker.
22
+ *
23
+ * Only prints with an enclosing node differ: with none, the compiler has no
24
+ * file to write a specifier relative to and writes the declaring file
25
+ * anyway. A print that must name modules the way a declaration file would
26
+ * (the capture's surface emit) passes a host of its own and does not use
27
+ * this.
28
+ */
29
+ import { ts } from 'ts-morph';
30
+ /** A symbol tracker that leaves the node builder without a module-specifier host. */
31
+ const NO_SPECIFIER_HOST = { moduleResolverHost: false };
32
+ /** The checker's own default for `typeToString`. */
33
+ const DEFAULT_TYPE_FORMAT = ts.TypeFormatFlags.AllowUniqueESSymbolType | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
34
+ /** `typeToString`'s caps: a print this long is cut, `...` included. */
35
+ const DEFAULT_MAXIMUM_LENGTH = 160;
36
+ const NO_TRUNCATION_MAXIMUM_LENGTH = 1_000_000;
37
+ /** `typeToString`'s printers: comments removed, except for the unresolved type. */
38
+ const PRINTER = ts.createPrinter({ removeComments: true });
39
+ const PRINTER_WITH_COMMENTS = ts.createPrinter({});
40
+ /**
41
+ * The checker's `typeToTypeNode`, writing each module a node names as the
42
+ * file that declares it. A caller's own tracker keeps its callbacks.
43
+ */
44
+ export function typeToTypeNodeAsDeclared(checker, type, enclosing, flags, tracker) {
45
+ const withoutHost = tracker
46
+ ? Object.assign(Object.create(tracker), { moduleResolverHost: false })
47
+ : NO_SPECIFIER_HOST;
48
+ return checker.typeToTypeNode(type, enclosing, flags, undefined, withoutHost);
49
+ }
50
+ /**
51
+ * The checker's `typeToString(type, enclosing, flags)`, writing each module
52
+ * the print names as the file that declares it. `noErrorTruncation` is the
53
+ * program's option of that name, which `typeToString` reads when `flags`
54
+ * leave truncation on.
55
+ */
56
+ export function typeToStringAsDeclared(checker, type, enclosing, flags = DEFAULT_TYPE_FORMAT, noErrorTruncation = false) {
57
+ const noTruncation = noErrorTruncation || (flags & ts.TypeFormatFlags.NoTruncation) !== 0;
58
+ const node = typeToTypeNodeAsDeclared(checker, type, enclosing, (flags & ts.TypeFormatFlags.NodeBuilderFlagsMask) |
59
+ ts.NodeBuilderFlags.IgnoreErrors |
60
+ (noTruncation ? ts.NodeBuilderFlags.NoTruncation : 0));
61
+ // `typeToString` asserts a node here too (`Debug.fail`). Falling back to it
62
+ // would print the other specifier form, so fail the same way it does.
63
+ if (node === undefined)
64
+ throw new Error('the node builder gave no type node to print');
65
+ const unresolved = type.intrinsicName === 'unresolved';
66
+ const writer = ts.createTextWriter('');
67
+ (unresolved ? PRINTER_WITH_COMMENTS : PRINTER).writeNode(ts.EmitHint.Unspecified, node, enclosing?.getSourceFile(), writer);
68
+ const result = writer.getText();
69
+ const maximum = (noTruncation ? NO_TRUNCATION_MAXIMUM_LENGTH : DEFAULT_MAXIMUM_LENGTH) * 2;
70
+ return result.length >= maximum ? result.substring(0, maximum - 3) + '...' : result;
71
+ }
@@ -23,6 +23,7 @@
23
23
  * no more than keeping the list.
24
24
  */
25
25
  import { ts } from 'ts-morph';
26
+ import { typeToTypeNodeAsDeclared } from './print-type.js';
26
27
  /** The prints of the inference running now, when one is recording. */
27
28
  let recording;
28
29
  /**
@@ -82,7 +83,7 @@ export class PrintedTypes {
82
83
  visited.set(type, at.add(enclosing));
83
84
  let node;
84
85
  try {
85
- node = checker.typeToTypeNode(type, enclosing, NODE_FLAGS);
86
+ node = typeToTypeNodeAsDeclared(checker, type, enclosing, NODE_FLAGS);
86
87
  }
87
88
  catch {
88
89
  continue;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The files a program was built from, and files added to a program in an
3
+ * order the caller gives (carrick#2027).
4
+ *
5
+ * With `stableTypeOrdering` the compiler orders two types of the same name by
6
+ * where their declarations sit in the program's file list. That list follows
7
+ * the program's root files: the compiler lists each root after the files it
8
+ * imports. ts-morph hands the compiler every file it has loaded as a root, in
9
+ * the order it loaded them, and a process asked about a file outside its
10
+ * tsconfig loads it when it is asked. Two processes asked the same questions
11
+ * in different orders therefore build from roots in different orders, and
12
+ * print a union of two same-named types in different orders.
13
+ *
14
+ * The roots decide the whole program, not only its order. A dependency file
15
+ * the compiler reached by resolving an import is not the same to it as the
16
+ * same file named as a root: a second copy of a package reached by resolution
17
+ * is folded into the first, and named as a root it is read as a file of its
18
+ * own and its imports are followed. So the list here is the roots themselves.
19
+ * A process that has built only what its tsconfig names, given another
20
+ * process's roots in order, ends with the same roots and builds the same
21
+ * program.
22
+ */
23
+ import type { Project } from 'ts-morph';
24
+ /**
25
+ * The root files `project`'s program was built from, in order, as absolute
26
+ * paths: every file the project had loaded when the program was last built,
27
+ * the compiler's library files and the dependencies earlier builds reached
28
+ * among them.
29
+ */
30
+ export declare function listProgramFiles(project: Project): string[];
31
+ /**
32
+ * Add to `project` each of `files` it has not loaded, in the given order, and
33
+ * build the program once if any was added. A file it has loaded keeps its
34
+ * place. A file that cannot be read is skipped and reported to `onSkipped`.
35
+ *
36
+ * Nothing is built before the files join. A project built from a tsconfig has
37
+ * built once already, from the files the tsconfig names, as every process's
38
+ * does; one built from source patterns or a Deno graph has loaded only its
39
+ * own files, and the given list names whatever the other process's first
40
+ * build loaded, in the place that build put it.
41
+ *
42
+ * @returns how many files were added
43
+ */
44
+ export declare function addProgramFiles(project: Project, files: readonly string[], onSkipped: (file: string, reason: string) => void): number;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The files a program was built from, and files added to a program in an
3
+ * order the caller gives (carrick#2027).
4
+ *
5
+ * With `stableTypeOrdering` the compiler orders two types of the same name by
6
+ * where their declarations sit in the program's file list. That list follows
7
+ * the program's root files: the compiler lists each root after the files it
8
+ * imports. ts-morph hands the compiler every file it has loaded as a root, in
9
+ * the order it loaded them, and a process asked about a file outside its
10
+ * tsconfig loads it when it is asked. Two processes asked the same questions
11
+ * in different orders therefore build from roots in different orders, and
12
+ * print a union of two same-named types in different orders.
13
+ *
14
+ * The roots decide the whole program, not only its order. A dependency file
15
+ * the compiler reached by resolving an import is not the same to it as the
16
+ * same file named as a root: a second copy of a package reached by resolution
17
+ * is folded into the first, and named as a root it is read as a file of its
18
+ * own and its imports are followed. So the list here is the roots themselves.
19
+ * A process that has built only what its tsconfig names, given another
20
+ * process's roots in order, ends with the same roots and builds the same
21
+ * program.
22
+ */
23
+ /**
24
+ * The root files `project`'s program was built from, in order, as absolute
25
+ * paths: every file the project had loaded when the program was last built,
26
+ * the compiler's library files and the dependencies earlier builds reached
27
+ * among them.
28
+ */
29
+ export function listProgramFiles(project) {
30
+ return [...project.getProgram().compilerObject.getRootFileNames()];
31
+ }
32
+ /**
33
+ * Add to `project` each of `files` it has not loaded, in the given order, and
34
+ * build the program once if any was added. A file it has loaded keeps its
35
+ * place. A file that cannot be read is skipped and reported to `onSkipped`.
36
+ *
37
+ * Nothing is built before the files join. A project built from a tsconfig has
38
+ * built once already, from the files the tsconfig names, as every process's
39
+ * does; one built from source patterns or a Deno graph has loaded only its
40
+ * own files, and the given list names whatever the other process's first
41
+ * build loaded, in the place that build put it.
42
+ *
43
+ * @returns how many files were added
44
+ */
45
+ export function addProgramFiles(project, files, onSkipped) {
46
+ let added = 0;
47
+ for (const file of new Set(files)) {
48
+ if (project.getSourceFile(file) !== undefined)
49
+ continue;
50
+ try {
51
+ project.addSourceFileAtPath(file);
52
+ added += 1;
53
+ }
54
+ catch (err) {
55
+ onSkipped(file, err instanceof Error ? err.message : String(err));
56
+ }
57
+ }
58
+ // ts-morph drops its program on each add and builds the next one when it is
59
+ // read, so reading it once here is the one rebuild the adds need, done in
60
+ // this request rather than in the next one.
61
+ if (added > 0)
62
+ project.getProgram().compilerObject;
63
+ return added;
64
+ }
@@ -120,6 +120,11 @@ export declare class ProjectLoader {
120
120
  * @throws Error if load() has not succeeded, or if the build fails
121
121
  */
122
122
  getProject(): Project;
123
+ /**
124
+ * The default project when a request has built it, and `undefined` before
125
+ * that, without building it (carrick#2027).
126
+ */
127
+ builtProject(): Project | undefined;
123
128
  /**
124
129
  * The registry package a resolved file belongs to, when the module graph
125
130
  * names one (carrick#1260). `undefined` for a project that did not resolve
@@ -9,10 +9,10 @@
9
9
  * - Tsconfig snapshot (closed/merged extends chains) for synthetic monorepo
10
10
  * - Pinned dependency snapshots for deterministic builds
11
11
  */
12
- import { Project } from 'ts-morph';
12
+ import { Project, getCompilerOptionsFromTsConfig } from 'ts-morph';
13
13
  import * as path from 'node:path';
14
14
  import * as fs from 'node:fs';
15
- import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath } from './capture/index.js';
15
+ import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath, sidecarCompilerOptions, } from './capture/index.js';
16
16
  import { moduleFormatResolutionHost } from './module-format.js';
17
17
  import { ExternalImports, registerExternalImports } from './origin.js';
18
18
  /**
@@ -177,7 +177,7 @@ export class ProjectLoader {
177
177
  this.log('Project will load with tsconfig snapshot');
178
178
  this.buildProject = () => {
179
179
  const project = new Project({
180
- compilerOptions: this.snapshotToCompilerOptions(snapshot),
180
+ compilerOptions: sidecarCompilerOptions(this.snapshotToCompilerOptions(snapshot)),
181
181
  skipAddingFilesFromTsConfig: true,
182
182
  });
183
183
  this.addDefaultSourceFiles(project);
@@ -197,7 +197,7 @@ export class ProjectLoader {
197
197
  // rebuilds that drop them (carrick#1731).
198
198
  const imports = new ExternalImports();
199
199
  const project = new Project({
200
- compilerOptions: deno.parsed.options,
200
+ compilerOptions: sidecarCompilerOptions(deno.parsed.options),
201
201
  skipAddingFilesFromTsConfig: true,
202
202
  resolutionHost: imports.recording((host, getOptions) => ({
203
203
  resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
@@ -221,7 +221,7 @@ export class ProjectLoader {
221
221
  this.log('No tsconfig.json found, using default compiler options');
222
222
  this.buildProject = () => {
223
223
  const project = new Project({
224
- compilerOptions: DEFAULT_COMPILER_OPTIONS,
224
+ compilerOptions: sidecarCompilerOptions(DEFAULT_COMPILER_OPTIONS),
225
225
  skipAddingFilesFromTsConfig: true,
226
226
  });
227
227
  // Add source files from common locations
@@ -323,6 +323,8 @@ export class ProjectLoader {
323
323
  const imports = new ExternalImports();
324
324
  const project = new Project({
325
325
  tsConfigFilePath: configPath,
326
+ // Over the tsconfig's own options: ts-morph lays these on top.
327
+ compilerOptions: sidecarCompilerOptions(getCompilerOptionsFromTsConfig(configPath).options),
326
328
  skipAddingFilesFromTsConfig: false,
327
329
  // Each import resolves in its file's own module format (carrick#1619),
328
330
  // and every external-library answer is kept past the program rebuilds
@@ -385,6 +387,13 @@ export class ProjectLoader {
385
387
  }
386
388
  return this.project;
387
389
  }
390
+ /**
391
+ * The default project when a request has built it, and `undefined` before
392
+ * that, without building it (carrick#2027).
393
+ */
394
+ builtProject() {
395
+ return this.project ?? undefined;
396
+ }
388
397
  /**
389
398
  * The registry package a resolved file belongs to, when the module graph
390
399
  * names one (carrick#1260). `undefined` for a project that did not resolve
@@ -53,9 +53,11 @@ export declare class Retyper {
53
53
  * Judge every item, spending at most `budgetMs`. Each item rebuilds the
54
54
  * program at least twice, so a consumer with many calls could otherwise
55
55
  * outrun the caller's read deadline and lose every answer; the items the
56
- * budget does not reach abstain and say so.
56
+ * budget does not reach abstain and say so. `onJudged` is called after each
57
+ * item the budget reached, so the caller can report progress while the
58
+ * event loop is blocked (carrick#1945).
57
59
  */
58
- run(items: RetypeItem[], budgetMs: number): RetypeOutcome[];
60
+ run(items: RetypeItem[], budgetMs: number, onJudged?: () => void): RetypeOutcome[];
59
61
  private runOne;
60
62
  /**
61
63
  * The rewrite that states the producer's UNWIDENED return (carrick#1516),
@@ -27,6 +27,7 @@
27
27
  import { Node, SyntaxKind, ts } from 'ts-morph';
28
28
  import { readsResponseStatus, statusesIn, SUCCEEDED, testsOnPath, } from './failure-path.js';
29
29
  import { lineIndex } from './line-index.js';
30
+ import { typeToStringAsDeclared } from './print-type.js';
30
31
  import { fileDiagnostics } from './unwidened.js';
31
32
  /** Names appended to a file that is not ours carry this prefix. */
32
33
  const PREFIX = '__carrick_';
@@ -53,9 +54,11 @@ export class Retyper {
53
54
  * Judge every item, spending at most `budgetMs`. Each item rebuilds the
54
55
  * program at least twice, so a consumer with many calls could otherwise
55
56
  * outrun the caller's read deadline and lose every answer; the items the
56
- * budget does not reach abstain and say so.
57
+ * budget does not reach abstain and say so. `onJudged` is called after each
58
+ * item the budget reached, so the caller can report progress while the
59
+ * event loop is blocked (carrick#1945).
57
60
  */
58
- run(items, budgetMs) {
61
+ run(items, budgetMs, onJudged) {
59
62
  const deadline = performance.now() + budgetMs;
60
63
  // What each file said before any rewrite. Every rewrite is undone, so it
61
64
  // is the same for every item in the file.
@@ -64,12 +67,15 @@ export class Retyper {
64
67
  if (performance.now() >= deadline) {
65
68
  return abstain(item, `the retype check ran out of its ${budgetMs}ms budget`);
66
69
  }
70
+ let outcome;
67
71
  try {
68
- return this.runOne(item, before);
72
+ outcome = this.runOne(item, before);
69
73
  }
70
74
  catch (err) {
71
- return abstain(item, `the retype check failed: ${err instanceof Error ? err.message : String(err)}`);
75
+ outcome = abstain(item, `the retype check failed: ${err instanceof Error ? err.message : String(err)}`);
72
76
  }
77
+ onJudged?.();
78
+ return outcome;
73
79
  });
74
80
  }
75
81
  runOne(item, before) {
@@ -228,7 +234,12 @@ export class Retyper {
228
234
  };
229
235
  }
230
236
  return (`the consumer call takes no type argument and returns ` +
231
- `'${call.getType().getText(call)}', so it cannot be retyped`);
237
+ `'${typeToStringAsDeclared(call.getProject().getTypeChecker().compilerObject, call.getType().compilerType, call.compilerNode,
238
+ // ts-morph's own default for Type.getText, which this print used.
239
+ ts.TypeFormatFlags.UseTypeOfFunction |
240
+ ts.TypeFormatFlags.NoTruncation |
241
+ ts.TypeFormatFlags.UseFullyQualifiedType |
242
+ ts.TypeFormatFlags.WriteTypeArgumentsOfSignature)}', so it cannot be retyped`);
232
243
  }
233
244
  /**
234
245
  * Apply one rewrite, type-check, restore. Returns the diagnostics the