carrick 0.3.111 → 0.3.113
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/bin/carrick.mjs +3 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -1
- package/dist/hook/post-edit.js +11 -2
- package/dist/hook/post-edit.js.map +1 -1
- package/dist/hook/post-read.d.ts +2 -0
- package/dist/hook/post-read.js +89 -0
- package/dist/hook/post-read.js.map +1 -0
- package/dist/hook/reuse.d.ts +24 -0
- package/dist/hook/reuse.js +58 -2
- package/dist/hook/reuse.js.map +1 -1
- package/dist/init/settings.js +6 -0
- package/dist/init/settings.js.map +1 -1
- package/dist/render.d.ts +22 -2
- package/dist/render.js +53 -2
- package/dist/render.js.map +1 -1
- package/dist/update.js +3 -0
- package/dist/update.js.map +1 -1
- package/package.json +62 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/scripts/install-scanned-deps.sh +539 -0
- package/sidecar/dist/src/capture/api.d.ts +22 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +8 -0
- package/sidecar/dist/src/capture/check-probe.js +9 -1
- package/sidecar/dist/src/capture/check.d.ts +32 -0
- package/sidecar/dist/src/capture/check.js +74 -5
- package/sidecar/dist/src/capture/checkout.d.ts +38 -0
- package/sidecar/dist/src/capture/checkout.js +139 -0
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +22 -7
- package/sidecar/dist/src/capture/machinery.js +1 -30
- package/sidecar/dist/src/capture/project-references.js +2 -1
- package/sidecar/dist/src/capture/self-check.d.ts +20 -1
- package/sidecar/dist/src/capture/self-check.js +30 -11
- package/sidecar/dist/src/origin.js +1 -29
- package/sidecar/dist/src/project-loader.d.ts +6 -0
- package/sidecar/dist/src/project-loader.js +64 -1
- package/sidecar/dist/src/type-inferrer.d.ts +235 -1
- package/sidecar/dist/src/type-inferrer.js +901 -111
|
@@ -27,17 +27,73 @@ import { buildPoisonIndexes } from './check-poison.js';
|
|
|
27
27
|
import { openProbeProgram, probeDeepFindings } from './check-deep.js';
|
|
28
28
|
import { pairFieldReports } from './check-fields.js';
|
|
29
29
|
import { pairDispatchUnions } from './check-union.js';
|
|
30
|
-
function runProcess(command, args, cwd) {
|
|
30
|
+
function runProcess(command, args, cwd, env) {
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
|
-
const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
32
|
+
const child = spawn(command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
33
33
|
let stdout = '';
|
|
34
34
|
let stderr = '';
|
|
35
35
|
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
36
36
|
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
37
37
|
child.on('error', reject);
|
|
38
|
-
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
38
|
+
child.on('close', (code, signal) => resolve({ code, signal, stdout, stderr }));
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
/** The tail V8 prints on every fatal heap-exhaustion line. */
|
|
42
|
+
const HEAP_OUT_OF_MEMORY = 'JavaScript heap out of memory';
|
|
43
|
+
/**
|
|
44
|
+
* V8's hard size limits print the same tail, and a larger heap does not lift
|
|
45
|
+
* them, so they are not memory deaths.
|
|
46
|
+
*/
|
|
47
|
+
const HARD_SIZE_LIMITS = ['invalid table size', 'invalid array length', 'Invalid array buffer length'];
|
|
48
|
+
/**
|
|
49
|
+
* Whether a process's stderr carries V8's fatal heap-exhaustion line
|
|
50
|
+
* (`Reached heap limit`, `Ineffective mark-compacts near heap limit`, and the
|
|
51
|
+
* other forms all end in `JavaScript heap out of memory`), and not one of the
|
|
52
|
+
* hard size limits that end the same way (carrick#2209).
|
|
53
|
+
*
|
|
54
|
+
* The scanner applies the same rule to the sidecar's own stderr
|
|
55
|
+
* (carrick#2208); the two must stay in step.
|
|
56
|
+
*/
|
|
57
|
+
export function carriesHeapLimitLine(stderr) {
|
|
58
|
+
return stderr
|
|
59
|
+
.split('\n')
|
|
60
|
+
.some((line) => line.includes(HEAP_OUT_OF_MEMORY) &&
|
|
61
|
+
!HARD_SIZE_LIMITS.some((limit) => line.includes(limit)));
|
|
62
|
+
}
|
|
63
|
+
/** V8's default old-space ceiling on a 64-bit host, in MB. */
|
|
64
|
+
const V8_DEFAULT_HEAP_CEILING_MB = 4096;
|
|
65
|
+
/**
|
|
66
|
+
* The heap a Node child of the sidecar (the check's compiler) is given
|
|
67
|
+
* (carrick#2209).
|
|
68
|
+
*
|
|
69
|
+
* The scanner sizes the sidecar's cap to the machine and passes it as a
|
|
70
|
+
* command-line flag, which a child does not inherit, so the child ran at V8's
|
|
71
|
+
* default. Handing it the same cap would overcommit the box: the sidecar's
|
|
72
|
+
* cap is already 3/4 of memory, and two such caps turn a heap error into an
|
|
73
|
+
* OS kill, which the sizing exists to avoid. So the child gets what is left of
|
|
74
|
+
* the sidecar's cap after what the sidecar holds now (`rssMb`), and only when
|
|
75
|
+
* that is more than V8's default; otherwise `NODE_OPTIONS` is left as it is
|
|
76
|
+
* and the child runs at the default, as before.
|
|
77
|
+
*/
|
|
78
|
+
export function childHeap(execArgv, existing, rssMb) {
|
|
79
|
+
let cap;
|
|
80
|
+
for (let i = 0; i < execArgv.length; i++) {
|
|
81
|
+
const match = /^--max[-_]old[-_]space[-_]size(?:=(.*))?$/.exec(execArgv[i]);
|
|
82
|
+
if (!match)
|
|
83
|
+
continue;
|
|
84
|
+
const value = match[1] ?? execArgv[i + 1];
|
|
85
|
+
if (value !== undefined && /^\d+$/.test(value))
|
|
86
|
+
cap = Number(value);
|
|
87
|
+
}
|
|
88
|
+
const budget = cap === undefined ? 0 : Math.floor(cap - rssMb);
|
|
89
|
+
if (budget <= V8_DEFAULT_HEAP_CEILING_MB)
|
|
90
|
+
return { nodeOptions: existing, heapCapMb: null };
|
|
91
|
+
const flag = `--max-old-space-size=${budget}`;
|
|
92
|
+
return {
|
|
93
|
+
nodeOptions: existing && existing.trim() ? `${existing} ${flag}` : flag,
|
|
94
|
+
heapCapMb: budget,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
41
97
|
/** Resolve the sidecar root (where the vendored pnpm/tsc live) from this bundle. */
|
|
42
98
|
function sidecarRoot() {
|
|
43
99
|
// dist/src/capture/check.js -> up three -> sidecar root.
|
|
@@ -198,7 +254,8 @@ export async function runCheck(opts, onProgress) {
|
|
|
198
254
|
}
|
|
199
255
|
// ---- Judge (async, off the event loop) ----------------------------------
|
|
200
256
|
progress('checking', 'type-checking probes');
|
|
201
|
-
const
|
|
257
|
+
const heap = childHeap(process.execArgv, process.env.NODE_OPTIONS, process.memoryUsage().rss / (1024 * 1024));
|
|
258
|
+
const tsc = await runProcess(tscPath, ['--noEmit', '--pretty', 'false', '-p', path.join(ws.probesRel, 'tsconfig.json')], ws.workspaceDir, { ...process.env, NODE_OPTIONS: heap.nodeOptions });
|
|
202
259
|
const diagnostics = parseTscOutput(tsc.stdout + '\n' + tsc.stderr);
|
|
203
260
|
// Abnormal tsc termination: OOM/SIGKILL (exit code null), an internal
|
|
204
261
|
// crash, or a fileless config error (e.g. TS18003, printed without a
|
|
@@ -207,7 +264,9 @@ export async function runCheck(opts, onProgress) {
|
|
|
207
264
|
// WITH parsed diagnostics is the legitimate incompatible path (tsc exits 1
|
|
208
265
|
// on ordinary type errors) and stays untouched. Mirrors the install step's
|
|
209
266
|
// `code === 0` guard.
|
|
210
|
-
|
|
267
|
+
// A compiler a signal ended never finished, so a pair with no diagnostic
|
|
268
|
+
// was never cleared, whatever it printed first (carrick#2209).
|
|
269
|
+
if (tsc.signal !== null || (tsc.code !== 0 && diagnostics.length === 0)) {
|
|
211
270
|
const excerpt = scrubPaths((tsc.stderr || tsc.stdout).trim().slice(0, 2000), scrubCtx);
|
|
212
271
|
errors.push(`tsc terminated abnormally (exit code ${tsc.code ?? 'null'}) ` +
|
|
213
272
|
`with no parseable diagnostics${excerpt ? `: ${excerpt}` : ''}`);
|
|
@@ -224,6 +283,15 @@ export async function runCheck(opts, onProgress) {
|
|
|
224
283
|
...preGated,
|
|
225
284
|
...unresolved,
|
|
226
285
|
]);
|
|
286
|
+
// How the compiler ended, as fields, so the scanner can tell a memory
|
|
287
|
+
// death from any other abnormal end without reading the text
|
|
288
|
+
// (carrick#2209).
|
|
289
|
+
const compilerEnd = {
|
|
290
|
+
exit_code: tsc.code,
|
|
291
|
+
signal: tsc.signal,
|
|
292
|
+
heap_limit: carriesHeapLimitLine(tsc.stderr),
|
|
293
|
+
heap_cap_mb: heap.heapCapMb,
|
|
294
|
+
};
|
|
227
295
|
if (cleanup)
|
|
228
296
|
safeRm(ws);
|
|
229
297
|
return {
|
|
@@ -235,6 +303,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
235
303
|
verdicts,
|
|
236
304
|
degraded_services: dedupeDegraded(degraded),
|
|
237
305
|
errors,
|
|
306
|
+
compiler_end: compilerEnd,
|
|
238
307
|
};
|
|
239
308
|
}
|
|
240
309
|
const { probeDiagsByPair, poison, globalErrors } = attributeDiagnostics(diagnostics, ws, probing);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The checkout a service sits in, and the bound an untrusted scan reads
|
|
3
|
+
* inside (carrick#2230).
|
|
4
|
+
*
|
|
5
|
+
* `CARRICK_UNTRUSTED_TREE` (any non-empty value) is the scan's switch, read
|
|
6
|
+
* the same way the scanner reads it. Unset, nothing here constructs a
|
|
7
|
+
* wrapper and every caller passes the compiler what it passed before. Set,
|
|
8
|
+
* the compiler reads no file whose REAL path (links followed) is outside the
|
|
9
|
+
* checkout, outside the sidecar's own install (its TypeScript library files),
|
|
10
|
+
* and outside the directories the caller names as its own (a capture's stub).
|
|
11
|
+
* A refused file reads as missing, so its types become `any`: the posture an
|
|
12
|
+
* unprepared tree already has.
|
|
13
|
+
*/
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
export declare const UNTRUSTED_TREE_ENV = "CARRICK_UNTRUSTED_TREE";
|
|
16
|
+
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
17
|
+
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
18
|
+
* root itself. */
|
|
19
|
+
export declare function checkoutRootOf(repoRoot: string): string;
|
|
20
|
+
export declare function isInsideCheckout(file: string, repoRoot: string): boolean;
|
|
21
|
+
/** True when the scan runs under the untrusted-tree switch. */
|
|
22
|
+
export declare function untrustedTree(): boolean;
|
|
23
|
+
/** The real roots an untrusted scan may read beneath. */
|
|
24
|
+
export interface ReadBound {
|
|
25
|
+
readonly roots: readonly string[];
|
|
26
|
+
/** Whether the real path of `file` is beneath a root. Never throws. */
|
|
27
|
+
allows(file: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The bound for a scan of the service at `repoRoot`, or `undefined` when the
|
|
31
|
+
* switch is off. `own` names directories the caller itself owns and reads
|
|
32
|
+
* back (a capture's stub, its scratch).
|
|
33
|
+
*/
|
|
34
|
+
export declare function untrustedReadBound(repoRoot: string, own?: readonly string[]): ReadBound | undefined;
|
|
35
|
+
/** `host` with every read the bound refuses answered as a missing file. */
|
|
36
|
+
export declare function boundedCompilerHost(host: ts.CompilerHost, bound: ReadBound | undefined): ts.CompilerHost;
|
|
37
|
+
/** The compiler's own file system, bounded; `ts.sys` itself when no bound. */
|
|
38
|
+
export declare function boundedSys(bound: ReadBound | undefined): ts.System;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The checkout a service sits in, and the bound an untrusted scan reads
|
|
3
|
+
* inside (carrick#2230).
|
|
4
|
+
*
|
|
5
|
+
* `CARRICK_UNTRUSTED_TREE` (any non-empty value) is the scan's switch, read
|
|
6
|
+
* the same way the scanner reads it. Unset, nothing here constructs a
|
|
7
|
+
* wrapper and every caller passes the compiler what it passed before. Set,
|
|
8
|
+
* the compiler reads no file whose REAL path (links followed) is outside the
|
|
9
|
+
* checkout, outside the sidecar's own install (its TypeScript library files),
|
|
10
|
+
* and outside the directories the caller names as its own (a capture's stub).
|
|
11
|
+
* A refused file reads as missing, so its types become `any`: the posture an
|
|
12
|
+
* unprepared tree already has.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import ts from 'typescript';
|
|
18
|
+
export const UNTRUSTED_TREE_ENV = 'CARRICK_UNTRUSTED_TREE';
|
|
19
|
+
const checkoutRoots = new Map();
|
|
20
|
+
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
21
|
+
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
22
|
+
* root itself. */
|
|
23
|
+
export function checkoutRootOf(repoRoot) {
|
|
24
|
+
const key = path.resolve(repoRoot);
|
|
25
|
+
const cached = checkoutRoots.get(key);
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
let dir = key;
|
|
29
|
+
let root = key;
|
|
30
|
+
for (;;) {
|
|
31
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
32
|
+
root = dir;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const parent = path.dirname(dir);
|
|
36
|
+
if (parent === dir)
|
|
37
|
+
break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
checkoutRoots.set(key, root);
|
|
41
|
+
return root;
|
|
42
|
+
}
|
|
43
|
+
export function isInsideCheckout(file, repoRoot) {
|
|
44
|
+
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
45
|
+
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
46
|
+
}
|
|
47
|
+
/** True when the scan runs under the untrusted-tree switch. */
|
|
48
|
+
export function untrustedTree() {
|
|
49
|
+
const value = process.env[UNTRUSTED_TREE_ENV];
|
|
50
|
+
return value !== undefined && value !== '';
|
|
51
|
+
}
|
|
52
|
+
/** `p` with every link resolved; a path that does not exist yet resolves
|
|
53
|
+
* through its deepest existing ancestor. */
|
|
54
|
+
function realOf(p) {
|
|
55
|
+
const abs = path.resolve(p);
|
|
56
|
+
const tail = [];
|
|
57
|
+
let dir = abs;
|
|
58
|
+
for (;;) {
|
|
59
|
+
try {
|
|
60
|
+
return path.join(fs.realpathSync(dir), ...tail.reverse());
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
const parent = path.dirname(dir);
|
|
64
|
+
if (parent === dir)
|
|
65
|
+
return abs;
|
|
66
|
+
tail.push(path.basename(dir));
|
|
67
|
+
dir = parent;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function within(real, root) {
|
|
72
|
+
return real === root || real.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
|
|
73
|
+
}
|
|
74
|
+
/** Where the sidecar's own files live: its install, and the TypeScript package
|
|
75
|
+
* whose `lib.*.d.ts` every program reads (beside the sidecar in the npm
|
|
76
|
+
* package, so not always beneath it). */
|
|
77
|
+
function sidecarRoots() {
|
|
78
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
79
|
+
const roots = [
|
|
80
|
+
path.resolve(here, '..', '..', '..'),
|
|
81
|
+
path.dirname(path.dirname(ts.getDefaultLibFilePath({}))),
|
|
82
|
+
];
|
|
83
|
+
const configured = process.env.CARRICK_SIDECAR_DIR;
|
|
84
|
+
if (configured && configured.trim() !== '')
|
|
85
|
+
roots.push(path.resolve(configured));
|
|
86
|
+
return roots;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The bound for a scan of the service at `repoRoot`, or `undefined` when the
|
|
90
|
+
* switch is off. `own` names directories the caller itself owns and reads
|
|
91
|
+
* back (a capture's stub, its scratch).
|
|
92
|
+
*/
|
|
93
|
+
export function untrustedReadBound(repoRoot, own = []) {
|
|
94
|
+
if (!untrustedTree())
|
|
95
|
+
return undefined;
|
|
96
|
+
const roots = [...new Set([checkoutRootOf(repoRoot), ...sidecarRoots(), ...own].map(realOf))];
|
|
97
|
+
return {
|
|
98
|
+
roots,
|
|
99
|
+
allows: (file) => {
|
|
100
|
+
const real = realOf(file);
|
|
101
|
+
return roots.some((root) => within(real, root));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** `host` with every read the bound refuses answered as a missing file. */
|
|
106
|
+
export function boundedCompilerHost(host, bound) {
|
|
107
|
+
if (!bound)
|
|
108
|
+
return host;
|
|
109
|
+
const getSourceFile = host.getSourceFile.bind(host);
|
|
110
|
+
const read = host.readFile.bind(host);
|
|
111
|
+
const exists = host.fileExists.bind(host);
|
|
112
|
+
const dirExists = host.directoryExists?.bind(host);
|
|
113
|
+
const dirs = host.getDirectories?.bind(host);
|
|
114
|
+
const readDirectory = host.readDirectory?.bind(host);
|
|
115
|
+
host.getSourceFile = (file, ...rest) => (bound.allows(file) ? getSourceFile(file, ...rest) : undefined);
|
|
116
|
+
host.readFile = (file) => (bound.allows(file) ? read(file) : undefined);
|
|
117
|
+
host.fileExists = (file) => bound.allows(file) && exists(file);
|
|
118
|
+
if (dirExists)
|
|
119
|
+
host.directoryExists = (dir) => bound.allows(dir) && dirExists(dir);
|
|
120
|
+
if (dirs)
|
|
121
|
+
host.getDirectories = (dir) => (bound.allows(dir) ? dirs(dir) : []);
|
|
122
|
+
if (readDirectory) {
|
|
123
|
+
host.readDirectory = (root, ...rest) => readDirectory(root, ...rest).filter((file) => bound.allows(file));
|
|
124
|
+
}
|
|
125
|
+
return host;
|
|
126
|
+
}
|
|
127
|
+
/** The compiler's own file system, bounded; `ts.sys` itself when no bound. */
|
|
128
|
+
export function boundedSys(bound) {
|
|
129
|
+
if (!bound)
|
|
130
|
+
return ts.sys;
|
|
131
|
+
return {
|
|
132
|
+
...ts.sys,
|
|
133
|
+
readFile: (file, encoding) => (bound.allows(file) ? ts.sys.readFile(file, encoding) : undefined),
|
|
134
|
+
fileExists: (file) => bound.allows(file) && ts.sys.fileExists(file),
|
|
135
|
+
directoryExists: (dir) => bound.allows(dir) && ts.sys.directoryExists(dir),
|
|
136
|
+
getDirectories: (dir) => (bound.allows(dir) ? ts.sys.getDirectories(dir) : []),
|
|
137
|
+
readDirectory: (root, ...rest) => ts.sys.readDirectory(root, ...rest).filter((file) => bound.allows(file)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -31,6 +31,7 @@ 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
33
|
export { sidecarCompilerOptions } from './compiler-options.js';
|
|
34
|
+
export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV, type ReadBound } from './checkout.js';
|
|
34
35
|
export { serviceConfigPath } from './project-references.js';
|
|
35
36
|
export { findServiceTsconfig } from './service-config.js';
|
|
36
37
|
export { runCheck } from './check.js';
|
|
@@ -44,8 +44,10 @@ import { findServiceTsconfig } from './service-config.js';
|
|
|
44
44
|
import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
|
|
45
45
|
import { WriteGuard } from './guarded-fs.js';
|
|
46
46
|
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
47
|
+
import { boundedCompilerHost, boundedSys, untrustedReadBound } from './checkout.js';
|
|
47
48
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
48
49
|
export { sidecarCompilerOptions } from './compiler-options.js';
|
|
50
|
+
export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV } from './checkout.js';
|
|
49
51
|
export { serviceConfigPath } from './project-references.js';
|
|
50
52
|
export { findServiceTsconfig } from './service-config.js';
|
|
51
53
|
// v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
|
|
@@ -132,6 +134,10 @@ export function captureStub(opts) {
|
|
|
132
134
|
const packageName = `@carrick/${sanitizeServiceName(opts.serviceName)}`;
|
|
133
135
|
const stubDir = path.resolve(opts.outDir);
|
|
134
136
|
const errors = [];
|
|
137
|
+
// Under CARRICK_UNTRUSTED_TREE every compiler read below is held to the
|
|
138
|
+
// checkout, the sidecar's own install and this stub; otherwise undefined
|
|
139
|
+
// and nothing here changes (carrick#2230).
|
|
140
|
+
const bound = untrustedReadBound(repoRoot, [stubDir]);
|
|
135
141
|
// The named config, else the one the init'd project reads too
|
|
136
142
|
// (carrick#1776). A service with neither is typed under defaults below.
|
|
137
143
|
const configPath = opts.tsconfigPath
|
|
@@ -192,7 +198,7 @@ export function captureStub(opts) {
|
|
|
192
198
|
checkJs: false,
|
|
193
199
|
resolveJsonModule: true,
|
|
194
200
|
},
|
|
195
|
-
},
|
|
201
|
+
}, boundedSys(bound), repoRoot);
|
|
196
202
|
}
|
|
197
203
|
else {
|
|
198
204
|
// Each anchor is typed under the project that owns its file: the named
|
|
@@ -260,7 +266,7 @@ export function captureStub(opts) {
|
|
|
260
266
|
// ---- Phase A: analysis program over placeholder entry + anchor sources ----
|
|
261
267
|
let resolved;
|
|
262
268
|
const progress = opts.onProgress ?? (() => { });
|
|
263
|
-
const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard, progress };
|
|
269
|
+
const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard, progress, bound };
|
|
264
270
|
try {
|
|
265
271
|
resolved = ownerGroups && emitProject
|
|
266
272
|
? resolveAnchorsByOwner(opts, ownerGroups, emitProject, analysisCtx, errors)
|
|
@@ -302,7 +308,7 @@ export function captureStub(opts) {
|
|
|
302
308
|
written = emitDeclarations({
|
|
303
309
|
rootNames: [entryPath, ...augmentationSources],
|
|
304
310
|
options: emitOptions,
|
|
305
|
-
host:
|
|
311
|
+
host: programHost(emitOptions, deno, bound),
|
|
306
312
|
staging,
|
|
307
313
|
entryDir,
|
|
308
314
|
});
|
|
@@ -490,8 +496,8 @@ export function captureStub(opts) {
|
|
|
490
496
|
pinned,
|
|
491
497
|
bareCheckout,
|
|
492
498
|
repoRoot: dependencyRoot,
|
|
493
|
-
compilerHost: deno || Object.keys(rewritten.installs).length > 0
|
|
494
|
-
? (options) => withInstalledPackages(options, rewritten.installs, deno ? deno.host(options) : undefined)
|
|
499
|
+
compilerHost: deno || bound || Object.keys(rewritten.installs).length > 0
|
|
500
|
+
? (options) => boundedCompilerHost(withInstalledPackages(options, rewritten.installs, deno ? deno.host(options) : undefined), bound)
|
|
495
501
|
: undefined,
|
|
496
502
|
});
|
|
497
503
|
const fidelity = computeFidelity(aliases);
|
|
@@ -691,6 +697,15 @@ function resolveAnchorsByOwner(opts, groups, emit, ctx, errors) {
|
|
|
691
697
|
}
|
|
692
698
|
return resolved;
|
|
693
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* The compiler host a capture program is built on: the Deno graph's, else the
|
|
702
|
+
* default one (`undefined`), each held to the read bound when there is one.
|
|
703
|
+
*/
|
|
704
|
+
function programHost(options, deno, bound) {
|
|
705
|
+
if (!bound)
|
|
706
|
+
return deno?.host(options);
|
|
707
|
+
return boundedCompilerHost(deno ? deno.host(options) : ts.createCompilerHost(options), bound);
|
|
708
|
+
}
|
|
694
709
|
/** Phase A: build the placeholder entry, then resolve every anchor. */
|
|
695
710
|
function resolveAnchors(opts, parsed, ctx, deno) {
|
|
696
711
|
const placeholderLines = ['// Carrick capture v2 analysis placeholder.'];
|
|
@@ -708,7 +723,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
708
723
|
...parsed.options,
|
|
709
724
|
noEmit: true,
|
|
710
725
|
});
|
|
711
|
-
const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options,
|
|
726
|
+
const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, programHost(options, deno, ctx.bound));
|
|
712
727
|
// The checker binds every file of the program when it is first asked for,
|
|
713
728
|
// which the first anchor would otherwise do: asked for here, so that the
|
|
714
729
|
// report below says the program is whole before any anchor is read.
|
|
@@ -731,7 +746,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
731
746
|
if (!fromEntry.has(specifier)) {
|
|
732
747
|
fromEntry.set(specifier, (deno
|
|
733
748
|
? deno.resolve(specifier, ctx.entryPath, options)
|
|
734
|
-
: ts.resolveModuleName(specifier, ctx.entryPath, options,
|
|
749
|
+
: ts.resolveModuleName(specifier, ctx.entryPath, options, boundedSys(ctx.bound), undefined, undefined, entryMode)
|
|
735
750
|
.resolvedModule)?.resolvedFileName);
|
|
736
751
|
}
|
|
737
752
|
return fromEntry.get(specifier);
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
* user payload that merely shares a member name can never trip it.
|
|
19
19
|
*/
|
|
20
20
|
import ts from 'typescript';
|
|
21
|
-
import
|
|
22
|
-
import * as path from 'node:path';
|
|
21
|
+
import { isInsideCheckout } from './checkout.js';
|
|
23
22
|
/**
|
|
24
23
|
* Strongly-discriminating member names of HTTP transport machinery. Kept
|
|
25
24
|
* identical to `MACHINERY_MEMBER_INDICATORS` in `type-inferrer.ts`. These are
|
|
@@ -171,34 +170,6 @@ export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
|
171
170
|
}
|
|
172
171
|
return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
|
|
173
172
|
}
|
|
174
|
-
const checkoutRoots = new Map();
|
|
175
|
-
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
176
|
-
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
177
|
-
* root itself. */
|
|
178
|
-
function checkoutRootOf(repoRoot) {
|
|
179
|
-
const key = path.resolve(repoRoot);
|
|
180
|
-
const cached = checkoutRoots.get(key);
|
|
181
|
-
if (cached)
|
|
182
|
-
return cached;
|
|
183
|
-
let dir = key;
|
|
184
|
-
let root = key;
|
|
185
|
-
for (;;) {
|
|
186
|
-
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
187
|
-
root = dir;
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
const parent = path.dirname(dir);
|
|
191
|
-
if (parent === dir)
|
|
192
|
-
break;
|
|
193
|
-
dir = parent;
|
|
194
|
-
}
|
|
195
|
-
checkoutRoots.set(key, root);
|
|
196
|
-
return root;
|
|
197
|
-
}
|
|
198
|
-
function isInsideCheckout(file, repoRoot) {
|
|
199
|
-
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
200
|
-
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
201
|
-
}
|
|
202
173
|
/**
|
|
203
174
|
* True when the symbol is declared in a runtime/library origin. Works on a bare
|
|
204
175
|
* checkout: the DOM `Response`/`Request` resolve from the bundled
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import ts from 'typescript';
|
|
22
|
+
import { boundedSys, untrustedReadBound } from './checkout.js';
|
|
22
23
|
/** Parse one config; throws on a file that cannot be read or parsed. */
|
|
23
24
|
function parseConfig(configPath, extendedConfigCache) {
|
|
24
25
|
const host = {
|
|
25
|
-
...
|
|
26
|
+
...boundedSys(untrustedReadBound(path.dirname(configPath))),
|
|
26
27
|
onUnRecoverableConfigFileDiagnostic: (d) => {
|
|
27
28
|
throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
|
|
28
29
|
},
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The stub tree is typechecked standalone with `skipLibCheck: false` --
|
|
6
6
|
* spike-verified as load-bearing: the tree is entirely .d.ts and skipLibCheck
|
|
7
|
-
* skips declaration files wholesale, making the gate vacuous.
|
|
7
|
+
* skips declaration files wholesale, making the gate vacuous. It also sets
|
|
8
|
+
* `skipDefaultLibCheck: true`, which skips only the compiler's own default
|
|
9
|
+
* lib files, never a stub file (see `SELF_CHECK_OPTIONS`). When the
|
|
8
10
|
* source repo has node_modules, resolution is pointed at it via a temporary
|
|
9
11
|
* node_modules symlink inside the stub, so externals resolve exactly as they
|
|
10
12
|
* will at check time against installed pins.
|
|
@@ -46,3 +48,20 @@ export interface SelfCheckArgs {
|
|
|
46
48
|
compilerHost?: (options: ts.CompilerOptions) => ts.CompilerHost;
|
|
47
49
|
}
|
|
48
50
|
export declare function selfCheckStub(args: SelfCheckArgs): CaptureAliasRecord[];
|
|
51
|
+
/**
|
|
52
|
+
* The options of every self-check program.
|
|
53
|
+
*
|
|
54
|
+
* `skipLibCheck: false` keeps every stub `.d.ts` checked (see module header).
|
|
55
|
+
* `skipDefaultLibCheck: true` skips only files the program loaded as a default
|
|
56
|
+
* lib (the default lib, `lib` entries, `/// <reference lib>` targets), which
|
|
57
|
+
* was most of a capture's check time (carrick#2158). It is safe here because
|
|
58
|
+
* the self-check reads only resolution diagnostics (2307, 2792, `Cannot find
|
|
59
|
+
* name|namespace`), attributes them only through the stub tree, and the
|
|
60
|
+
* compiler reports each at the referencing stub node when it checks that stub.
|
|
61
|
+
*
|
|
62
|
+
* It must NOT be set on the judge's `tsc` (`CHECKER_TSCONFIG`): some checks of
|
|
63
|
+
* a merged interface run only when its FIRST declaration is checked, so a stub
|
|
64
|
+
* that augments `interface Window { 0: number }` loses its TS2411 when
|
|
65
|
+
* `lib.dom.d.ts` is skipped, and the judge counts any stub diagnostic.
|
|
66
|
+
*/
|
|
67
|
+
export declare const SELF_CHECK_OPTIONS: Readonly<ts.CompilerOptions>;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The stub tree is typechecked standalone with `skipLibCheck: false` --
|
|
6
6
|
* spike-verified as load-bearing: the tree is entirely .d.ts and skipLibCheck
|
|
7
|
-
* skips declaration files wholesale, making the gate vacuous.
|
|
7
|
+
* skips declaration files wholesale, making the gate vacuous. It also sets
|
|
8
|
+
* `skipDefaultLibCheck: true`, which skips only the compiler's own default
|
|
9
|
+
* lib files, never a stub file (see `SELF_CHECK_OPTIONS`). When the
|
|
8
10
|
* source repo has node_modules, resolution is pointed at it via a temporary
|
|
9
11
|
* node_modules symlink inside the stub, so externals resolve exactly as they
|
|
10
12
|
* will at check time against installed pins.
|
|
@@ -78,17 +80,34 @@ export function selfCheckStub(args) {
|
|
|
78
80
|
args.guard.unlink(linkPath);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The options of every self-check program.
|
|
85
|
+
*
|
|
86
|
+
* `skipLibCheck: false` keeps every stub `.d.ts` checked (see module header).
|
|
87
|
+
* `skipDefaultLibCheck: true` skips only files the program loaded as a default
|
|
88
|
+
* lib (the default lib, `lib` entries, `/// <reference lib>` targets), which
|
|
89
|
+
* was most of a capture's check time (carrick#2158). It is safe here because
|
|
90
|
+
* the self-check reads only resolution diagnostics (2307, 2792, `Cannot find
|
|
91
|
+
* name|namespace`), attributes them only through the stub tree, and the
|
|
92
|
+
* compiler reports each at the referencing stub node when it checks that stub.
|
|
93
|
+
*
|
|
94
|
+
* It must NOT be set on the judge's `tsc` (`CHECKER_TSCONFIG`): some checks of
|
|
95
|
+
* a merged interface run only when its FIRST declaration is checked, so a stub
|
|
96
|
+
* that augments `interface Window { 0: number }` loses its TS2411 when
|
|
97
|
+
* `lib.dom.d.ts` is skipped, and the judge counts any stub diagnostic.
|
|
98
|
+
*/
|
|
99
|
+
export const SELF_CHECK_OPTIONS = sidecarCompilerOptions({
|
|
100
|
+
noEmit: true,
|
|
101
|
+
strict: true,
|
|
102
|
+
// MUST be false: the whole stub tree is .d.ts (see module header).
|
|
103
|
+
skipLibCheck: false,
|
|
104
|
+
skipDefaultLibCheck: true,
|
|
105
|
+
module: ts.ModuleKind.ESNext,
|
|
106
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
107
|
+
types: [],
|
|
108
|
+
});
|
|
81
109
|
function runSelfCheck(args, treeFiles, repaired) {
|
|
82
|
-
const
|
|
83
|
-
noEmit: true,
|
|
84
|
-
strict: true,
|
|
85
|
-
// MUST be false: the whole stub tree is .d.ts (see module header).
|
|
86
|
-
skipLibCheck: false,
|
|
87
|
-
module: ts.ModuleKind.ESNext,
|
|
88
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
89
|
-
types: [],
|
|
90
|
-
});
|
|
91
|
-
const program = ts.createProgram(treeFiles, options, args.compilerHost?.(options));
|
|
110
|
+
const program = ts.createProgram(treeFiles, SELF_CHECK_OPTIONS, args.compilerHost?.(SELF_CHECK_OPTIONS));
|
|
92
111
|
const checker = program.getTypeChecker();
|
|
93
112
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
94
113
|
const surfaceAbs = path.resolve(args.surfaceAbsPath);
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
|
|
14
14
|
* rather than by sharing this module.
|
|
15
15
|
*/
|
|
16
|
-
import * as fs from 'node:fs';
|
|
17
16
|
import * as path from 'node:path';
|
|
17
|
+
import { isInsideCheckout } from './capture/index.js';
|
|
18
18
|
/**
|
|
19
19
|
* True when a declaration's source file is runtime/library origin rather than
|
|
20
20
|
* user source. Four answers, in order:
|
|
@@ -129,31 +129,3 @@ export function registerExternalImports(project, imports) {
|
|
|
129
129
|
export function externalImportsOf(project) {
|
|
130
130
|
return externalImportsByProject.get(project);
|
|
131
131
|
}
|
|
132
|
-
const checkoutRoots = new Map();
|
|
133
|
-
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
134
|
-
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
135
|
-
* root itself. */
|
|
136
|
-
function checkoutRootOf(repoRoot) {
|
|
137
|
-
const key = path.resolve(repoRoot);
|
|
138
|
-
const cached = checkoutRoots.get(key);
|
|
139
|
-
if (cached)
|
|
140
|
-
return cached;
|
|
141
|
-
let dir = key;
|
|
142
|
-
let root = key;
|
|
143
|
-
for (;;) {
|
|
144
|
-
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
145
|
-
root = dir;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
const parent = path.dirname(dir);
|
|
149
|
-
if (parent === dir)
|
|
150
|
-
break;
|
|
151
|
-
dir = parent;
|
|
152
|
-
}
|
|
153
|
-
checkoutRoots.set(key, root);
|
|
154
|
-
return root;
|
|
155
|
-
}
|
|
156
|
-
function isInsideCheckout(file, repoRoot) {
|
|
157
|
-
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
158
|
-
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
159
|
-
}
|
|
@@ -95,6 +95,12 @@ export declare class ProjectLoader {
|
|
|
95
95
|
* Convert a TsconfigSnapshot to ts-morph CompilerOptions
|
|
96
96
|
*/
|
|
97
97
|
private snapshotToCompilerOptions;
|
|
98
|
+
/**
|
|
99
|
+
* Under CARRICK_UNTRUSTED_TREE, the file system every project reads
|
|
100
|
+
* through: nothing whose real path is outside the checkout (carrick#2230).
|
|
101
|
+
* Nothing at all otherwise, so a project is built as it always was.
|
|
102
|
+
*/
|
|
103
|
+
private fileSystemOption;
|
|
98
104
|
/** A ts-morph project built from one tsconfig and the files it lists. */
|
|
99
105
|
private projectFromConfig;
|
|
100
106
|
/**
|