carrick 0.3.101 → 0.3.103
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 +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +3 -92
- package/sidecar/dist/src/bundler.js +5 -265
- package/sidecar/dist/src/capture/anchors.js +113 -8
- package/sidecar/dist/src/capture/check-fields.js +19 -5
- package/sidecar/dist/src/capture/check-probe.d.ts +5 -0
- package/sidecar/dist/src/capture/check-probe.js +31 -2
- package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
- package/sidecar/dist/src/capture/check-workspace.js +13 -16
- package/sidecar/dist/src/capture/check.js +5 -5
- package/sidecar/dist/src/capture/deep-walk.js +45 -12
- package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
- package/sidecar/dist/src/capture/deno-project.js +22 -18
- package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
- package/sidecar/dist/src/capture/guarded-fs.js +182 -0
- package/sidecar/dist/src/capture/index.js +60 -26
- package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
- package/sidecar/dist/src/capture/outside-root.js +101 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
- package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
- package/sidecar/dist/src/capture/repair-dangling.js +15 -4
- package/sidecar/dist/src/capture/self-check.d.ts +3 -0
- package/sidecar/dist/src/capture/self-check.js +3 -3
- package/sidecar/dist/src/index.d.ts +1 -1
- package/sidecar/dist/src/index.js +2 -115
- package/sidecar/dist/src/origin.d.ts +49 -8
- package/sidecar/dist/src/origin.js +81 -8
- package/sidecar/dist/src/project-loader.js +15 -5
- package/sidecar/dist/src/type-inferrer.d.ts +72 -0
- package/sidecar/dist/src/type-inferrer.js +248 -8
- package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
- package/sidecar/dist/src/type-structural-expander.js +1 -1
- package/sidecar/dist/src/types.d.ts +31 -178
- package/sidecar/dist/src/validators.d.ts +58 -914
- package/sidecar/dist/src/validators.js +0 -55
- package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
- package/sidecar/dist/src/monorepo-builder.js +0 -584
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* at compare time (ts_check type-checker `unwrapGraphqlPayload`, deleted in
|
|
24
24
|
* WP8); the type-level port here is its v2-native equivalent.
|
|
25
25
|
*
|
|
26
|
+
* GraphQL pairs also read the consumer's `__typename` as optional, at any depth
|
|
27
|
+
* (carrick#1759): the server adds that meta-field to every object a selection
|
|
28
|
+
* asks for, so a resolver's return type never states it, while a consumer type
|
|
29
|
+
* generated from a document that selects it declares it required.
|
|
30
|
+
*
|
|
26
31
|
* Seam: node builtins + `typescript` + this bundle only. No imports needed here.
|
|
27
32
|
*/
|
|
28
33
|
/**
|
|
@@ -172,12 +177,36 @@ export function buildProbe(spec, packageOf) {
|
|
|
172
177
|
push(`type GqlPayloadKeys<T> = { [K in keyof T]-?: [GqlObjLike<T[K]>] extends [true] ? K : never }[keyof T];`);
|
|
173
178
|
push(`type GqlSingleKey<K> = [K] extends [never] ? never : ([GqlIsUnion<K>] extends [false] ? K : never);`);
|
|
174
179
|
push(`type GqlPayloadOf<S> = [S] extends [readonly unknown[]] ? never : [GqlIsUnion<S>] extends [true] ? never : [S] extends [object] ? ([GqlSingleKey<GqlPayloadKeys<S>>] extends [never] ? never : S[GqlSingleKey<GqlPayloadKeys<S>> & keyof S]) : never;`);
|
|
175
|
-
|
|
180
|
+
// carrick#1759: the server supplies `__typename` on every object a
|
|
181
|
+
// selection asks for, so the consumer's `__typename` is read as OPTIONAL,
|
|
182
|
+
// at any depth. It keeps its declared type: a producer that states a
|
|
183
|
+
// different one is still a mismatch, and tsc stays the judge of that.
|
|
184
|
+
//
|
|
185
|
+
// - only an object that declares `__typename` is rebuilt, through one
|
|
186
|
+
// anonymous mapped type, so modifiers survive, the field walk sees an
|
|
187
|
+
// object root, and a headline prints its members, not a helper name;
|
|
188
|
+
// - arrays and tuples map homomorphically, so they stay arrays, and a
|
|
189
|
+
// function member is kept whole (a mapped function loses its call
|
|
190
|
+
// signature and would accept anything);
|
|
191
|
+
// - the depth bound ends the expansion; below it the consumer type is
|
|
192
|
+
// compared as declared, which can only keep a mismatch;
|
|
193
|
+
// - `GqlExpected` keeps the declared type whenever relaxing changes
|
|
194
|
+
// nothing observable, so a consumer with no `__typename` to relax is
|
|
195
|
+
// judged, and named in the headline, exactly as before.
|
|
196
|
+
//
|
|
197
|
+
// The envelope short-circuit below tests the RELAXED consumer: a bare
|
|
198
|
+
// payload with one object-shaped property would otherwise fail it on
|
|
199
|
+
// `__typename` alone and unwrap onto that property.
|
|
200
|
+
push(`type GqlDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];`);
|
|
201
|
+
push(`type GqlTypenameParts<T, D extends number> = { [K in keyof T as K extends '__typename' ? never : K]: GqlTypenameOptional<T[K], GqlDepth[D]> } & { [K in keyof T as K extends '__typename' ? K : never]?: T[K] };`);
|
|
202
|
+
push(`type GqlTypenameOptional<T, D extends number = 16> = [D] extends [never] ? T : T extends (...args: any[]) => any ? T : T extends object ? ('__typename' extends keyof T ? { [K in keyof GqlTypenameParts<T, D>]: GqlTypenameParts<T, D>[K] } : { [K in keyof T]: GqlTypenameOptional<T[K], GqlDepth[D]> }) : T;`);
|
|
203
|
+
push(`type GqlExpected = [GqlTypenameOptional<Expected>] extends [Expected] ? Expected : GqlTypenameOptional<Expected>;`);
|
|
204
|
+
push(`type GqlComparand = [Sent] extends [GqlExpected] ? Sent : ([GqlPayloadOf<Sent>] extends [never] ? Sent : GqlPayloadOf<Sent>);`);
|
|
176
205
|
gateLines.set(push(`type _G_comparand_any = Assert<Not<IsAny<GqlComparand>>>;`), 'sent:any');
|
|
177
206
|
gateLines.set(push(`type _G_comparand_unknown = Assert<Not<IsUnknown<GqlComparand>>>;`), 'sent:unknown');
|
|
178
207
|
gateLines.set(push(`type _G_comparand_never = Assert<Not<IsNever<GqlComparand>>>;`), 'sent:never');
|
|
179
208
|
push(`declare const sentComparand: GqlComparand;`);
|
|
180
|
-
assignmentLine = push(`const expected:
|
|
209
|
+
assignmentLine = push(`const expected: GqlExpected = sentComparand;`);
|
|
181
210
|
}
|
|
182
211
|
else {
|
|
183
212
|
assignmentLine = push(`const expected: Expected = sent;`);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { CheckStubInput } from './api.js';
|
|
16
16
|
import type { ProbePlan } from './check-probe.js';
|
|
17
|
+
import { WriteGuard } from './guarded-fs.js';
|
|
17
18
|
export interface AssembledStub {
|
|
18
19
|
serviceName: string;
|
|
19
20
|
/** Workspace package dir under packages/ (== the @carrick/<dir> suffix). */
|
|
@@ -23,6 +24,8 @@ export interface AssembledStub {
|
|
|
23
24
|
}
|
|
24
25
|
export interface AssembledWorkspace {
|
|
25
26
|
workspaceDir: string;
|
|
27
|
+
/** Every write and the clean-up are held to `workspaceDir` (carrick#1748). */
|
|
28
|
+
guard: WriteGuard;
|
|
26
29
|
probesDir: string;
|
|
27
30
|
/** Relative (forward-slash) probes dir, e.g. packages/carrick-probes. */
|
|
28
31
|
probesRel: string;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Seam: node builtins + this bundle only.
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
|
-
import * as os from 'node:os';
|
|
17
16
|
import * as path from 'node:path';
|
|
17
|
+
import { WriteGuard } from './guarded-fs.js';
|
|
18
18
|
const PROBES_PACKAGE = 'carrick-probes';
|
|
19
19
|
function parseVersion(v) {
|
|
20
20
|
const core = v.replace(/^[^\d]*/, '').split('-')[0].split('+')[0];
|
|
@@ -108,13 +108,12 @@ function packageDirOf(packageName) {
|
|
|
108
108
|
}
|
|
109
109
|
/** Create the scratch workspace and copy in the stub packages. */
|
|
110
110
|
export function assembleWorkspace(opts) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const workspaceDir = fs.mkdtempSync(path.join(root, 'carrick-check-v2-'));
|
|
111
|
+
// A fresh directory under the caller's root (the OS temp dir by default).
|
|
112
|
+
const { dir: workspaceDir, guard } = WriteGuard.scratch('carrick-check-v2-', opts.workspaceRoot);
|
|
114
113
|
const packagesDir = path.join(workspaceDir, 'packages');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
guard.mkdir(packagesDir);
|
|
115
|
+
guard.writeFile(path.join(workspaceDir, '.npmrc'), NPMRC);
|
|
116
|
+
guard.writeFile(path.join(workspaceDir, 'pnpm-workspace.yaml'), 'packages:\n - "packages/*"\n');
|
|
118
117
|
const assembled = [];
|
|
119
118
|
const dependencySets = [];
|
|
120
119
|
const svcToPkg = new Map();
|
|
@@ -124,10 +123,7 @@ export function assembleWorkspace(opts) {
|
|
|
124
123
|
const packageName = readStubPackageName(stub.stub_dir);
|
|
125
124
|
const packageDir = packageDirOf(packageName);
|
|
126
125
|
const dest = path.join(packagesDir, packageDir);
|
|
127
|
-
|
|
128
|
-
recursive: true,
|
|
129
|
-
filter: (src) => !src.split(path.sep).includes('node_modules'),
|
|
130
|
-
});
|
|
126
|
+
guard.copyTree(stub.stub_dir, dest, (src) => !src.split(path.sep).includes('node_modules'));
|
|
131
127
|
assembled.push({ serviceName: stub.service_name, packageDir, packageName });
|
|
132
128
|
dependencySets.push({ dependencies: readStubDependencies(stub.stub_dir) });
|
|
133
129
|
svcToPkg.set(stub.service_name, packageName);
|
|
@@ -136,26 +132,27 @@ export function assembleWorkspace(opts) {
|
|
|
136
132
|
}
|
|
137
133
|
// Root manifest carries the semver-dedupe overrides.
|
|
138
134
|
const overrides = computeDedupeOverrides(dependencySets);
|
|
139
|
-
|
|
135
|
+
guard.writeFile(path.join(workspaceDir, 'package.json'), JSON.stringify({
|
|
140
136
|
name: 'carrick-check-workspace',
|
|
141
137
|
version: '0.0.0',
|
|
142
138
|
private: true,
|
|
143
139
|
...(Object.keys(overrides).length > 0 ? { pnpm: { overrides } } : {}),
|
|
144
140
|
}, null, 2) + '\n');
|
|
145
141
|
const probesDir = path.join(packagesDir, PROBES_PACKAGE);
|
|
146
|
-
|
|
142
|
+
guard.mkdir(path.join(probesDir, 'probes'));
|
|
147
143
|
const probeDeps = {};
|
|
148
144
|
for (const stub of assembled)
|
|
149
145
|
probeDeps[stub.packageName] = 'workspace:*';
|
|
150
|
-
|
|
146
|
+
guard.writeFile(path.join(probesDir, 'package.json'), JSON.stringify({
|
|
151
147
|
name: PROBES_PACKAGE,
|
|
152
148
|
version: '0.0.0',
|
|
153
149
|
private: true,
|
|
154
150
|
dependencies: probeDeps,
|
|
155
151
|
}, null, 2) + '\n');
|
|
156
|
-
|
|
152
|
+
guard.writeFile(path.join(probesDir, 'tsconfig.json'), CHECKER_TSCONFIG);
|
|
157
153
|
return {
|
|
158
154
|
workspaceDir,
|
|
155
|
+
guard,
|
|
159
156
|
probesDir,
|
|
160
157
|
probesRel: `packages/${PROBES_PACKAGE}`,
|
|
161
158
|
stubs: assembled,
|
|
@@ -172,7 +169,7 @@ export function assembleWorkspace(opts) {
|
|
|
172
169
|
/** Write the generated probe files into the assembled probes package. */
|
|
173
170
|
export function writeProbes(ws, plans) {
|
|
174
171
|
for (const plan of plans) {
|
|
175
|
-
|
|
172
|
+
ws.guard.writeFile(path.join(ws.probesDir, 'probes', plan.fileName), plan.source);
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
175
|
const NPMRC = [
|
|
@@ -214,7 +214,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
214
214
|
...unresolved,
|
|
215
215
|
]);
|
|
216
216
|
if (cleanup)
|
|
217
|
-
safeRm(ws
|
|
217
|
+
safeRm(ws);
|
|
218
218
|
return {
|
|
219
219
|
success: false,
|
|
220
220
|
workspace_dir: cleanup ? '' : ws.workspaceDir,
|
|
@@ -254,7 +254,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
254
254
|
...unresolved,
|
|
255
255
|
]);
|
|
256
256
|
if (cleanup)
|
|
257
|
-
safeRm(ws
|
|
257
|
+
safeRm(ws);
|
|
258
258
|
return {
|
|
259
259
|
success: false,
|
|
260
260
|
workspace_dir: cleanup ? '' : ws.workspaceDir,
|
|
@@ -306,7 +306,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
306
306
|
]);
|
|
307
307
|
const workspaceDirOut = cleanup ? '' : ws.workspaceDir;
|
|
308
308
|
if (cleanup)
|
|
309
|
-
safeRm(ws
|
|
309
|
+
safeRm(ws);
|
|
310
310
|
return {
|
|
311
311
|
success: true,
|
|
312
312
|
workspace_dir: workspaceDirOut,
|
|
@@ -453,9 +453,9 @@ function planPairs(opts, packageOf) {
|
|
|
453
453
|
function fnvOfSpec(spec) {
|
|
454
454
|
return buildProbe(spec, (s) => `@carrick/${s}`).pairId;
|
|
455
455
|
}
|
|
456
|
-
function safeRm(
|
|
456
|
+
function safeRm(ws) {
|
|
457
457
|
try {
|
|
458
|
-
|
|
458
|
+
ws.guard.remove(ws.workspaceDir);
|
|
459
459
|
}
|
|
460
460
|
catch {
|
|
461
461
|
/* best effort */
|
|
@@ -17,7 +17,10 @@ import ts from 'typescript';
|
|
|
17
17
|
/** Cap on findings reported per alias. The FIRST one is what the check phase
|
|
18
18
|
* pre-gates on, so verdicts never depend on this number; the rest are there to
|
|
19
19
|
* tell a reader which fields are `any` (carrick#376), and a type with more
|
|
20
|
-
* than this many is already better described by "this type is not typed".
|
|
20
|
+
* than this many is already better described by "this type is not typed".
|
|
21
|
+
* The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
|
|
22
|
+
* `src/engine/type_compat_v2.rs`): a record this long may have stopped
|
|
23
|
+
* listing, so it cannot vouch for every position (carrick#1752). */
|
|
21
24
|
const MAX_DEEP_FINDINGS = 32;
|
|
22
25
|
/**
|
|
23
26
|
* Depth-bounded structural walk for a disqualifying top type at ANY depth:
|
|
@@ -97,6 +100,16 @@ function walkTopTypes(root, program, checker, location, flagOf) {
|
|
|
97
100
|
// about the whole type and makes any further finding meaningless.
|
|
98
101
|
const found = [];
|
|
99
102
|
let exhausted = false;
|
|
103
|
+
// One finding per kind and position, under the cap. Two union members that
|
|
104
|
+
// both type `x` reach the same position twice.
|
|
105
|
+
const named = new Set();
|
|
106
|
+
const note = (kind, path) => {
|
|
107
|
+
const key = `${kind}\u0000${path}`;
|
|
108
|
+
if (named.has(key) || found.length >= MAX_DEEP_FINDINGS)
|
|
109
|
+
return;
|
|
110
|
+
named.add(key);
|
|
111
|
+
found.push({ kind, path });
|
|
112
|
+
};
|
|
100
113
|
const walk = (t, path, depth) => {
|
|
101
114
|
// Genuine cycle handling — NOT fail-open. `t` is already on the walk stack
|
|
102
115
|
// (or was fully explored earlier), so the owning frame completes it;
|
|
@@ -107,8 +120,25 @@ function walkTopTypes(root, program, checker, location, flagOf) {
|
|
|
107
120
|
// any shallower re-entry — so `seen` never memoizes a truncated visit as
|
|
108
121
|
// clean. And a type fully explored clean at depth d1 is clean at any d2 < d1
|
|
109
122
|
// (the shallower reach is a superset of the deeper), so reusing it is safe.
|
|
110
|
-
if (exhausted
|
|
123
|
+
if (exhausted)
|
|
124
|
+
return;
|
|
125
|
+
if (seen.has(t)) {
|
|
126
|
+
// A type met before has nothing left to walk, but a top type is a leaf
|
|
127
|
+
// that sits at every position typed with it, and `any` and `unknown`
|
|
128
|
+
// are intrinsics: the compiler hands out one object for each, however
|
|
129
|
+
// many members hold it. So this position is named too. Without it the
|
|
130
|
+
// record named the first `unknown` and the first `any` and nothing after
|
|
131
|
+
// them, and a member the source declares `unknown` hid a later one that
|
|
132
|
+
// an unresolved import left (carrick#1752). An object type met again is
|
|
133
|
+
// one declaration reached twice, and the first path already named
|
|
134
|
+
// every top type inside it, with its cause. The first finding, which
|
|
135
|
+
// the check phase pre-gates on, does not move: a repeat always follows
|
|
136
|
+
// the first meeting.
|
|
137
|
+
const kind = depth > 0 ? flagOf(t) : undefined;
|
|
138
|
+
if (kind)
|
|
139
|
+
note(kind, path);
|
|
111
140
|
return;
|
|
141
|
+
}
|
|
112
142
|
// Budget exhaustion FAILS CLOSED. Returning "clean" here would let an
|
|
113
143
|
// `any` buried past the depth/node budget read compatible — the fail-open
|
|
114
144
|
// cliff a bigger number only relocates. Instead abstain: the alias demotes
|
|
@@ -140,8 +170,7 @@ function walkTopTypes(root, program, checker, location, flagOf) {
|
|
|
140
170
|
if (depth > 0) {
|
|
141
171
|
const kind = flagOf(t);
|
|
142
172
|
if (kind) {
|
|
143
|
-
|
|
144
|
-
found.push({ kind, path });
|
|
173
|
+
note(kind, path);
|
|
145
174
|
return;
|
|
146
175
|
}
|
|
147
176
|
}
|
|
@@ -170,16 +199,20 @@ function walkTopTypes(root, program, checker, location, flagOf) {
|
|
|
170
199
|
return;
|
|
171
200
|
}
|
|
172
201
|
// Type arguments: arrays, tuples, Promise<T>, Map<K, V>, ...
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
const args = t.objectFlags & ts.ObjectFlags.Reference
|
|
203
|
+
? checker.getTypeArguments(t)
|
|
204
|
+
: [];
|
|
205
|
+
for (let i = 0; i < args.length; i++) {
|
|
206
|
+
walk(args[i], `${path}<${i}>`, depth + 1);
|
|
207
|
+
if (exhausted)
|
|
208
|
+
return;
|
|
180
209
|
}
|
|
181
|
-
// Index signatures: { [k: string]: T }, Record<string, T>.
|
|
210
|
+
// Index signatures: { [k: string]: T }, Record<string, T>. An array's
|
|
211
|
+
// number index is its element type argument again, the position walked
|
|
212
|
+
// just above under `<0>`: walking it twice would name one element twice.
|
|
182
213
|
for (const info of checker.getIndexInfosOfType(t)) {
|
|
214
|
+
if (args.includes(info.type))
|
|
215
|
+
continue;
|
|
183
216
|
walk(info.type, `${path}[index]`, depth + 1);
|
|
184
217
|
if (exhausted)
|
|
185
218
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Deno owns module resolution; both compiler frontends consume its graph. */
|
|
2
2
|
import ts from 'typescript';
|
|
3
|
+
import { WriteGuard } from './guarded-fs.js';
|
|
3
4
|
export interface DenoConfig {
|
|
4
5
|
configPath: string;
|
|
5
6
|
workspaceRoot: string;
|
|
@@ -48,5 +49,5 @@ export declare class DenoProject {
|
|
|
48
49
|
/** Turn a Deno npm alias into its real package export, with the exact pin. */
|
|
49
50
|
private externalName;
|
|
50
51
|
/** Relocate Deno's per-file resolutions into the portable declaration tree. */
|
|
51
|
-
rewrite(typesDir: string, files: string[], sourceByEmitted: Map<string, string>): number;
|
|
52
|
+
rewrite(guard: WriteGuard, typesDir: string, files: string[], sourceByEmitted: Map<string, string>): number;
|
|
52
53
|
}
|
|
@@ -6,6 +6,7 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import { rewriteSpecifiers } from './specifiers.js';
|
|
9
|
+
import { WriteGuard } from './guarded-fs.js';
|
|
9
10
|
function readConfig(file) {
|
|
10
11
|
const parsed = ts.parseConfigFileTextToJson(file, fs.readFileSync(file, 'utf8'));
|
|
11
12
|
if (parsed.error)
|
|
@@ -104,7 +105,13 @@ export class DenoProject {
|
|
|
104
105
|
this.repoRoot = repoRoot;
|
|
105
106
|
const cache = path.join(config.workspaceRoot, '.carrick', 'deno', createHash('sha256').update(path.resolve(repoRoot)).digest('hex').slice(0, 16));
|
|
106
107
|
this.cacheDir = cache;
|
|
107
|
-
|
|
108
|
+
// The graph root lives within the service so compilerOptions.types uses
|
|
109
|
+
// that member's import-map scope, just like the real source files do.
|
|
110
|
+
const entry = path.join(repoRoot, '.carrick', 'deno', 'graph.ts');
|
|
111
|
+
// Its cache and its graph root are the only places a Deno project writes,
|
|
112
|
+
// both inside `.carrick/deno` (carrick#1748).
|
|
113
|
+
const guard = WriteGuard.of({ dirs: [cache, path.dirname(entry)], protect: [repoRoot, config.workspaceRoot] });
|
|
114
|
+
guard.mkdir(cache);
|
|
108
115
|
const raw = { ...config.compilerOptions };
|
|
109
116
|
const libs = Array.isArray(raw.lib) ? raw.lib : ['deno.window'];
|
|
110
117
|
const denoLibs = libs.filter(l => l.startsWith('deno.'));
|
|
@@ -138,24 +145,21 @@ export class DenoProject {
|
|
|
138
145
|
(ts.isInterfaceDeclaration(s) && s.name.text === 'ImportMeta')).map(s => s.getFullText(source)).join('\n');
|
|
139
146
|
}
|
|
140
147
|
const globalPath = path.join(cache, 'runtime.d.ts');
|
|
141
|
-
|
|
148
|
+
guard.writeFile(globalPath, text);
|
|
142
149
|
this.globals.push(globalPath);
|
|
143
150
|
}
|
|
144
|
-
|
|
145
|
-
// that member's import-map scope, just like the real source files do.
|
|
146
|
-
const entry = path.join(repoRoot, '.carrick', 'deno', 'graph.ts');
|
|
147
|
-
fs.mkdirSync(path.dirname(entry), { recursive: true });
|
|
151
|
+
guard.mkdir(path.dirname(entry));
|
|
148
152
|
const roots = this.parsed.fileNames.map(f => pathToFileURL(f).href);
|
|
149
153
|
const extraTypes = config.compilerOptions.types;
|
|
150
154
|
if (Array.isArray(extraTypes))
|
|
151
155
|
roots.push(...extraTypes.filter((t) => typeof t === 'string'));
|
|
152
|
-
|
|
156
|
+
guard.writeFile(entry, roots.map(s => `import ${JSON.stringify(s)};`).join('\n'));
|
|
153
157
|
let graph;
|
|
154
158
|
try {
|
|
155
159
|
graph = JSON.parse(runDeno(['info', '--json', '--frozen', '--node-modules-dir=none', '--config', config.configPath, entry], repoRoot));
|
|
156
160
|
}
|
|
157
161
|
finally {
|
|
158
|
-
|
|
162
|
+
guard.remove(entry);
|
|
159
163
|
}
|
|
160
164
|
if (!Array.isArray(graph.modules))
|
|
161
165
|
throw new Error('Unsupported deno info JSON: missing modules array');
|
|
@@ -181,8 +185,8 @@ export class DenoProject {
|
|
|
181
185
|
continue;
|
|
182
186
|
}
|
|
183
187
|
local = path.join(cache, 'remote', createHash('sha256').update(module.specifier).digest('hex') + extension);
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
guard.mkdir(path.dirname(local));
|
|
189
|
+
guard.copyFile(module.local, local);
|
|
186
190
|
}
|
|
187
191
|
this.localPaths.set(module.specifier, path.resolve(local));
|
|
188
192
|
this.specifiersByPath.set(path.resolve(local), module.specifier);
|
|
@@ -210,7 +214,7 @@ export class DenoProject {
|
|
|
210
214
|
this.globals.push(local);
|
|
211
215
|
}
|
|
212
216
|
}
|
|
213
|
-
|
|
217
|
+
guard.writeFile(path.join(cache, 'resolution-diagnostics.json'), JSON.stringify(this.diagnostics, null, 2));
|
|
214
218
|
this.parsed.fileNames.push(...this.globals);
|
|
215
219
|
}
|
|
216
220
|
targetPath(resolution, from = this.repoRoot) {
|
|
@@ -404,7 +408,7 @@ export class DenoProject {
|
|
|
404
408
|
return undefined;
|
|
405
409
|
}
|
|
406
410
|
/** Relocate Deno's per-file resolutions into the portable declaration tree. */
|
|
407
|
-
rewrite(typesDir, files, sourceByEmitted) {
|
|
411
|
+
rewrite(guard, typesDir, files, sourceByEmitted) {
|
|
408
412
|
let count = 0;
|
|
409
413
|
const emittedBySource = new Map([...sourceByEmitted].map(([rel, source]) => [path.resolve(source), rel]));
|
|
410
414
|
for (const rel of files) {
|
|
@@ -427,26 +431,26 @@ export class DenoProject {
|
|
|
427
431
|
return relative;
|
|
428
432
|
});
|
|
429
433
|
if (result.rewrites)
|
|
430
|
-
|
|
434
|
+
guard.writeFile(file, result.text);
|
|
431
435
|
count += result.rewrites;
|
|
432
436
|
}
|
|
433
437
|
const runtime = this.globals.find(file => path.basename(file) === 'runtime.d.ts');
|
|
434
438
|
const runtimeRel = runtime && emittedBySource.get(path.resolve(runtime));
|
|
435
439
|
if (runtimeRel)
|
|
436
|
-
isolateRuntime(typesDir, files, runtimeRel);
|
|
440
|
+
isolateRuntime(guard, typesDir, files, runtimeRel);
|
|
437
441
|
const references = this.globals.filter(file => file !== runtime)
|
|
438
442
|
.map(file => emittedBySource.get(path.resolve(file)))
|
|
439
443
|
.filter((file) => file !== undefined)
|
|
440
444
|
.map(file => `/// <reference path=${JSON.stringify('./' + file)} />`);
|
|
441
445
|
if (references.length) {
|
|
442
446
|
const surface = path.join(typesDir, 'surface.d.ts');
|
|
443
|
-
|
|
447
|
+
guard.writeFile(surface, references.join('\n') + '\n' + fs.readFileSync(surface, 'utf8'));
|
|
444
448
|
}
|
|
445
449
|
return count;
|
|
446
450
|
}
|
|
447
451
|
}
|
|
448
452
|
/** Runtime declarations belong to their producer, not the checker's globals. */
|
|
449
|
-
function isolateRuntime(typesDir, files, runtimeRel) {
|
|
453
|
+
function isolateRuntime(guard, typesDir, files, runtimeRel) {
|
|
450
454
|
const runtimePath = path.join(typesDir, runtimeRel);
|
|
451
455
|
const program = ts.createProgram(files.map(file => path.join(typesDir, file)), {
|
|
452
456
|
strict: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext,
|
|
@@ -510,7 +514,7 @@ function isolateRuntime(typesDir, files, runtimeRel) {
|
|
|
510
514
|
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
511
515
|
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
512
516
|
if (edits.length)
|
|
513
|
-
|
|
517
|
+
guard.writeFile(file, text);
|
|
514
518
|
}
|
|
515
519
|
while (pending.length) {
|
|
516
520
|
const visit = (node) => {
|
|
@@ -545,5 +549,5 @@ function isolateRuntime(typesDir, files, runtimeRel) {
|
|
|
545
549
|
}
|
|
546
550
|
}
|
|
547
551
|
const text = ts.createPrinter().printFile(ts.factory.updateSourceFile(runtime, statements));
|
|
548
|
-
|
|
552
|
+
guard.writeFile(runtimePath, '// Runtime declarations from deno types. Copyright the Deno authors. MIT license.\n/// <reference lib="esnext" />\n' + text + `\nexport { ${[...names].sort().join(', ')} };\n`);
|
|
549
553
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every file the sidecar writes or deletes goes through a {@link WriteGuard}
|
|
3
|
+
* (carrick#1748).
|
|
4
|
+
*
|
|
5
|
+
* A scan reads the repo it scans and never writes it. carrick#1742 showed how
|
|
6
|
+
* easily that breaks: the capture self-check reaches the repo's own sources
|
|
7
|
+
* through a `node_modules` link, and a pass that "only rewrites its own
|
|
8
|
+
* declarations" rewrote them. Each write site used to be safe only by its own
|
|
9
|
+
* reasoning. Now each run names the directories it owns (its scratch, the stub
|
|
10
|
+
* it emits, a runtime cache) and every write and delete is checked against
|
|
11
|
+
* them, on the RESOLVED path, before it happens.
|
|
12
|
+
*
|
|
13
|
+
* Resolution follows the operation:
|
|
14
|
+
* - a write lands where the path's links lead, so the whole path is resolved,
|
|
15
|
+
* and a path that does not exist yet resolves through its deepest existing
|
|
16
|
+
* ancestor (a dangling link resolves through its target);
|
|
17
|
+
* - a delete, or a new link, touches the directory entry itself, so only the
|
|
18
|
+
* parent is resolved. Unlinking the stub's `node_modules` link is allowed;
|
|
19
|
+
* writing through it into the repo's packages is refused.
|
|
20
|
+
*
|
|
21
|
+
* `test/no-raw-writes.test.ts` fails when any other source file calls a
|
|
22
|
+
* mutating `fs` API.
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
/** A write or delete the guard refused, or a root it would not accept. */
|
|
26
|
+
export declare class WriteRefused extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
export interface WriteRoots {
|
|
30
|
+
/** Directories a run may write anywhere beneath, each one included. */
|
|
31
|
+
dirs?: string[];
|
|
32
|
+
/** Single files a run may write and delete, and nothing beside them. */
|
|
33
|
+
files?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Trees the run reads: no root may be one of them or contain one. A root
|
|
36
|
+
* that contained the scanned repo would let a stub's clean-up delete it.
|
|
37
|
+
*/
|
|
38
|
+
protect?: string[];
|
|
39
|
+
}
|
|
40
|
+
export declare class WriteGuard {
|
|
41
|
+
private readonly dirs;
|
|
42
|
+
private readonly files;
|
|
43
|
+
private readonly protect;
|
|
44
|
+
private constructor();
|
|
45
|
+
/** A guard over `roots`. Throws when a root equals or contains a protected tree. */
|
|
46
|
+
static of(roots: WriteRoots): WriteGuard;
|
|
47
|
+
/**
|
|
48
|
+
* A new, empty directory under `parent` (the OS temp dir by default) and a
|
|
49
|
+
* guard over it alone. `mkdtemp` picks an unused name, so creating it cannot
|
|
50
|
+
* overwrite anything; `parent` is created when missing.
|
|
51
|
+
*/
|
|
52
|
+
static scratch(prefix: string, parent?: string): {
|
|
53
|
+
dir: string;
|
|
54
|
+
guard: WriteGuard;
|
|
55
|
+
};
|
|
56
|
+
/** This guard with more roots, under the same protected trees. */
|
|
57
|
+
with(roots: Omit<WriteRoots, 'protect'>): WriteGuard;
|
|
58
|
+
/** A guard over `dir` alone, which must already be writable under this one. */
|
|
59
|
+
narrow(dir: string): WriteGuard;
|
|
60
|
+
/** Whether a write to `p` would land inside this guard's roots. */
|
|
61
|
+
allowsWrite(p: string): boolean;
|
|
62
|
+
/** Throws unless `p` (a subprocess's working directory, say) lies inside the roots. */
|
|
63
|
+
assertWithin(p: string): void;
|
|
64
|
+
writeFile(p: string, data: string | NodeJS.ArrayBufferView): void;
|
|
65
|
+
/** `mkdir -p`. A directory that already exists is no write and passes unchecked. */
|
|
66
|
+
mkdir(p: string): void;
|
|
67
|
+
copyFile(from: string, to: string): void;
|
|
68
|
+
/** `cp -R`. Links in the source are copied as links, never written through. */
|
|
69
|
+
copyTree(from: string, to: string, filter?: (source: string) => boolean): void;
|
|
70
|
+
/** `rm -rf`. A link is removed itself; its target is left alone. */
|
|
71
|
+
remove(p: string): void;
|
|
72
|
+
unlink(p: string): void;
|
|
73
|
+
symlink(target: string, linkPath: string, type?: fs.symlink.Type): void;
|
|
74
|
+
private holds;
|
|
75
|
+
private check;
|
|
76
|
+
}
|