carrick 0.3.102 → 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
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
import * as os from 'node:os';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
/** A write or delete the guard refused, or a root it would not accept. */
|
|
28
|
+
export class WriteRefused extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(`${message} (carrick#1748)`);
|
|
31
|
+
this.name = 'WriteRefused';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class WriteGuard {
|
|
35
|
+
dirs;
|
|
36
|
+
files;
|
|
37
|
+
protect;
|
|
38
|
+
constructor(dirs, files, protect) {
|
|
39
|
+
this.dirs = dirs;
|
|
40
|
+
this.files = files;
|
|
41
|
+
this.protect = protect;
|
|
42
|
+
}
|
|
43
|
+
/** A guard over `roots`. Throws when a root equals or contains a protected tree. */
|
|
44
|
+
static of(roots) {
|
|
45
|
+
const protect = (roots.protect ?? []).map(landing);
|
|
46
|
+
const dirs = (roots.dirs ?? []).map(landing);
|
|
47
|
+
const files = (roots.files ?? []).map(entry);
|
|
48
|
+
for (const root of [...dirs, ...files]) {
|
|
49
|
+
const covered = protect.find((tree) => within(root, tree));
|
|
50
|
+
if (covered !== undefined) {
|
|
51
|
+
throw new WriteRefused(`refused write root ${root}: it is or contains the scanned tree ${covered}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return new WriteGuard(dirs, files, protect);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A new, empty directory under `parent` (the OS temp dir by default) and a
|
|
58
|
+
* guard over it alone. `mkdtemp` picks an unused name, so creating it cannot
|
|
59
|
+
* overwrite anything; `parent` is created when missing.
|
|
60
|
+
*/
|
|
61
|
+
static scratch(prefix, parent = os.tmpdir()) {
|
|
62
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
63
|
+
const dir = fs.mkdtempSync(path.join(parent, prefix));
|
|
64
|
+
return { dir, guard: WriteGuard.of({ dirs: [dir] }) };
|
|
65
|
+
}
|
|
66
|
+
/** This guard with more roots, under the same protected trees. */
|
|
67
|
+
with(roots) {
|
|
68
|
+
return WriteGuard.of({
|
|
69
|
+
dirs: [...this.dirs, ...(roots.dirs ?? [])],
|
|
70
|
+
files: [...this.files, ...(roots.files ?? [])],
|
|
71
|
+
protect: [...this.protect],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** A guard over `dir` alone, which must already be writable under this one. */
|
|
75
|
+
narrow(dir) {
|
|
76
|
+
this.check(dir, landing(dir), 'narrow to');
|
|
77
|
+
return WriteGuard.of({ dirs: [dir], protect: [...this.protect] });
|
|
78
|
+
}
|
|
79
|
+
/** Whether a write to `p` would land inside this guard's roots. */
|
|
80
|
+
allowsWrite(p) {
|
|
81
|
+
return this.holds(landing(p));
|
|
82
|
+
}
|
|
83
|
+
/** Throws unless `p` (a subprocess's working directory, say) lies inside the roots. */
|
|
84
|
+
assertWithin(p) {
|
|
85
|
+
this.check(p, landing(p), 'work in');
|
|
86
|
+
}
|
|
87
|
+
writeFile(p, data) {
|
|
88
|
+
this.check(p, landing(p), 'write');
|
|
89
|
+
fs.writeFileSync(p, data);
|
|
90
|
+
}
|
|
91
|
+
/** `mkdir -p`. A directory that already exists is no write and passes unchecked. */
|
|
92
|
+
mkdir(p) {
|
|
93
|
+
if (isDirectory(p))
|
|
94
|
+
return;
|
|
95
|
+
this.check(p, landing(p), 'create directory');
|
|
96
|
+
fs.mkdirSync(p, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
copyFile(from, to) {
|
|
99
|
+
this.check(to, landing(to), 'copy into');
|
|
100
|
+
fs.copyFileSync(from, to);
|
|
101
|
+
}
|
|
102
|
+
/** `cp -R`. Links in the source are copied as links, never written through. */
|
|
103
|
+
copyTree(from, to, filter) {
|
|
104
|
+
this.check(to, landing(to), 'copy into');
|
|
105
|
+
fs.cpSync(from, to, { recursive: true, filter });
|
|
106
|
+
}
|
|
107
|
+
/** `rm -rf`. A link is removed itself; its target is left alone. */
|
|
108
|
+
remove(p) {
|
|
109
|
+
this.check(p, entry(p), 'delete');
|
|
110
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
unlink(p) {
|
|
113
|
+
this.check(p, entry(p), 'delete');
|
|
114
|
+
fs.unlinkSync(p);
|
|
115
|
+
}
|
|
116
|
+
symlink(target, linkPath, type) {
|
|
117
|
+
this.check(linkPath, entry(linkPath), 'link');
|
|
118
|
+
fs.symlinkSync(target, linkPath, type);
|
|
119
|
+
}
|
|
120
|
+
holds(resolved) {
|
|
121
|
+
return this.files.includes(resolved) || this.dirs.some((root) => within(root, resolved));
|
|
122
|
+
}
|
|
123
|
+
check(p, resolved, verb) {
|
|
124
|
+
if (this.holds(resolved))
|
|
125
|
+
return;
|
|
126
|
+
throw new WriteRefused(`refused to ${verb} ${p}` +
|
|
127
|
+
(resolved === path.resolve(p) ? '' : ` (resolves to ${resolved})`) +
|
|
128
|
+
': outside this run\'s scratch, stub and cache roots');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Whether `p` is `root` or lies beneath it. Both are resolved paths. */
|
|
132
|
+
function within(root, p) {
|
|
133
|
+
const rel = path.relative(root, p);
|
|
134
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
|
|
135
|
+
}
|
|
136
|
+
function isDirectory(p) {
|
|
137
|
+
try {
|
|
138
|
+
return fs.statSync(p).isDirectory();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Where a write to `p` lands: every link resolved, through the deepest
|
|
146
|
+
* existing ancestor when `p` does not exist yet, and through a dangling link's
|
|
147
|
+
* target (writing to a dangling link creates its target).
|
|
148
|
+
*/
|
|
149
|
+
function landing(p, hops = 0) {
|
|
150
|
+
const abs = path.resolve(p);
|
|
151
|
+
try {
|
|
152
|
+
return fs.realpathSync.native(abs);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Missing, or a link whose target is missing.
|
|
156
|
+
}
|
|
157
|
+
let link;
|
|
158
|
+
try {
|
|
159
|
+
if (fs.lstatSync(abs).isSymbolicLink())
|
|
160
|
+
link = fs.readlinkSync(abs);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Not there at all.
|
|
164
|
+
}
|
|
165
|
+
if (link !== undefined) {
|
|
166
|
+
if (hops > 40)
|
|
167
|
+
throw new WriteRefused(`refused ${abs}: too many symbolic links`);
|
|
168
|
+
return landing(path.resolve(path.dirname(abs), link), hops + 1);
|
|
169
|
+
}
|
|
170
|
+
const parent = path.dirname(abs);
|
|
171
|
+
if (parent === abs)
|
|
172
|
+
return abs;
|
|
173
|
+
return path.join(landing(parent, hops), path.basename(abs));
|
|
174
|
+
}
|
|
175
|
+
/** The directory entry `p` names: its parent resolved, its own name kept. */
|
|
176
|
+
function entry(p) {
|
|
177
|
+
const abs = path.resolve(p);
|
|
178
|
+
const parent = path.dirname(abs);
|
|
179
|
+
if (parent === abs)
|
|
180
|
+
return abs;
|
|
181
|
+
return path.join(landing(parent), path.basename(abs));
|
|
182
|
+
}
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
import ts from 'typescript';
|
|
31
31
|
import * as fs from 'node:fs';
|
|
32
32
|
import * as path from 'node:path';
|
|
33
|
-
import * as os from 'node:os';
|
|
34
33
|
import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
|
|
35
34
|
import { findAugmentationFiles } from './augmentations.js';
|
|
36
35
|
import { installedVersions, lockfileVersions } from './lockfile.js';
|
|
@@ -40,6 +39,8 @@ import { selfCheckStub } from './self-check.js';
|
|
|
40
39
|
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
41
40
|
import { DenoProject, findDenoConfig } from './deno-project.js';
|
|
42
41
|
import { emitsAlike, ProjectGraph } from './project-references.js';
|
|
42
|
+
import { placeEmittedTree } from './outside-root.js';
|
|
43
|
+
import { WriteGuard } from './guarded-fs.js';
|
|
43
44
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
44
45
|
export { serviceConfigPath } from './project-references.js';
|
|
45
46
|
// v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
|
|
@@ -137,6 +138,17 @@ export function captureStub(opts) {
|
|
|
137
138
|
// references others and the anchors' files do not all belong to one project.
|
|
138
139
|
let ownerGroups;
|
|
139
140
|
let emitProject;
|
|
141
|
+
// Everything this capture writes, it writes through `guard` (carrick#1748):
|
|
142
|
+
// the stub dir, the staging dir, and the surface entry. A stub dir is
|
|
143
|
+
// emptied before it is written, so one that is or holds the repo is refused
|
|
144
|
+
// before anything is touched.
|
|
145
|
+
let guard;
|
|
146
|
+
try {
|
|
147
|
+
guard = WriteGuard.of({ dirs: [stubDir], protect: [repoRoot] });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
151
|
+
}
|
|
140
152
|
let deno;
|
|
141
153
|
try {
|
|
142
154
|
const config = findDenoConfig(repoRoot, opts.tsconfigPath);
|
|
@@ -225,10 +237,19 @@ export function captureStub(opts) {
|
|
|
225
237
|
const entryPath = deno
|
|
226
238
|
? path.join(deno.cacheDir, `${surfaceEntry}.ts`)
|
|
227
239
|
: path.join(entryDir, `${surfaceEntry}.ts`);
|
|
228
|
-
|
|
240
|
+
// The entry is the one file a capture writes inside the repo itself: tsc
|
|
241
|
+
// only emits it from inside rootDir. It is written, read and deleted, and
|
|
242
|
+
// the guard holds it to exactly that path. A Deno entry sits in the cache.
|
|
243
|
+
try {
|
|
244
|
+
guard = guard.with(deno ? { dirs: [deno.cacheDir] } : { files: [entryPath] });
|
|
245
|
+
guard.mkdir(path.dirname(entryPath));
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
249
|
+
}
|
|
229
250
|
// ---- Phase A: analysis program over placeholder entry + anchor sources ----
|
|
230
251
|
let resolved;
|
|
231
|
-
const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath };
|
|
252
|
+
const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard };
|
|
232
253
|
try {
|
|
233
254
|
resolved = ownerGroups && emitProject
|
|
234
255
|
? resolveAnchorsByOwner(opts, ownerGroups, emitProject, analysisCtx, errors)
|
|
@@ -247,7 +268,8 @@ export function captureStub(opts) {
|
|
|
247
268
|
: '';
|
|
248
269
|
entryLines.push(`export type ${anchor.request.alias} = ${anchor.aliasText};${comment}`);
|
|
249
270
|
}
|
|
250
|
-
const
|
|
271
|
+
const scratch = WriteGuard.scratch('carrick-capture-v2-');
|
|
272
|
+
const staging = scratch.dir;
|
|
251
273
|
const emitted = new Map();
|
|
252
274
|
// Input .d.ts files (ambient stubs, augmentation declarations, local
|
|
253
275
|
// hand-written declarations in the import closure) are never re-emitted by
|
|
@@ -256,7 +278,7 @@ export function captureStub(opts) {
|
|
|
256
278
|
const sourceByEmitted = new Map();
|
|
257
279
|
let emitPartial = false;
|
|
258
280
|
try {
|
|
259
|
-
|
|
281
|
+
guard.writeFile(entryPath, entryLines.join('\n') + '\n');
|
|
260
282
|
const emitOptions = {
|
|
261
283
|
...parsed.options,
|
|
262
284
|
// The load-bearing trio: emit declarations without checking, so
|
|
@@ -306,8 +328,8 @@ export function captureStub(opts) {
|
|
|
306
328
|
}
|
|
307
329
|
finally {
|
|
308
330
|
if (fs.existsSync(entryPath))
|
|
309
|
-
|
|
310
|
-
|
|
331
|
+
guard.unlink(entryPath);
|
|
332
|
+
scratch.guard.remove(staging);
|
|
311
333
|
}
|
|
312
334
|
// ---- Partial-emit recovery ----
|
|
313
335
|
// The corpus-2 notifications-svc shape: one file's declaration emit was
|
|
@@ -326,22 +348,28 @@ export function captureStub(opts) {
|
|
|
326
348
|
}
|
|
327
349
|
// ---- Relocate the emitted tree into the stub package ----
|
|
328
350
|
const typesDir = path.join(stubDir, 'types');
|
|
329
|
-
|
|
330
|
-
|
|
351
|
+
guard.remove(stubDir);
|
|
352
|
+
guard.mkdir(typesDir);
|
|
353
|
+
// From here on, only the stub is written.
|
|
354
|
+
const stubGuard = guard.narrow(stubDir);
|
|
355
|
+
// A declaration for a source outside rootDir arrives at the source's own
|
|
356
|
+
// path; it is placed under the tree too (carrick#1770).
|
|
357
|
+
const placed = placeEmittedTree({ emitted, staging, entryDir, surfaceDeclaration });
|
|
331
358
|
const emittedFiles = [];
|
|
332
359
|
let surfaceAbsPath = '';
|
|
333
|
-
for (const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
360
|
+
for (const fileName of emitted.keys()) {
|
|
361
|
+
const stagingRel = path.relative(staging, fileName).split(path.sep).join('/');
|
|
362
|
+
const rel = placed.relOf.get(fileName);
|
|
363
|
+
const text = placed.textOf.get(fileName);
|
|
364
|
+
if (rel !== stagingRel) {
|
|
365
|
+
const source = sourceByEmitted.get(stagingRel);
|
|
366
|
+
sourceByEmitted.delete(stagingRel);
|
|
339
367
|
if (source)
|
|
340
368
|
sourceByEmitted.set(rel, source);
|
|
341
369
|
}
|
|
342
370
|
const dest = path.join(typesDir, rel);
|
|
343
|
-
|
|
344
|
-
|
|
371
|
+
stubGuard.mkdir(path.dirname(dest));
|
|
372
|
+
stubGuard.writeFile(dest, text);
|
|
345
373
|
emittedFiles.push(rel);
|
|
346
374
|
if (rel === 'surface.d.ts')
|
|
347
375
|
surfaceAbsPath = dest;
|
|
@@ -354,8 +382,8 @@ export function captureStub(opts) {
|
|
|
354
382
|
if (emittedFiles.includes(rel))
|
|
355
383
|
continue;
|
|
356
384
|
const dest = path.join(typesDir, rel);
|
|
357
|
-
|
|
358
|
-
|
|
385
|
+
stubGuard.mkdir(path.dirname(dest));
|
|
386
|
+
stubGuard.writeFile(dest, text);
|
|
359
387
|
emittedFiles.push(rel);
|
|
360
388
|
}
|
|
361
389
|
// Tree-relative names of augmentation files that made it into the tree
|
|
@@ -364,6 +392,9 @@ export function captureStub(opts) {
|
|
|
364
392
|
.map((abs) => {
|
|
365
393
|
const noDts = abs.replace(/\.d\.ts$/, '');
|
|
366
394
|
const noExt = noDts === abs ? abs.replace(/\.(ts|tsx|mts|cts)$/, '') : noDts;
|
|
395
|
+
const outside = placed.outside.get(path.resolve(noExt));
|
|
396
|
+
if (outside !== undefined)
|
|
397
|
+
return outside;
|
|
367
398
|
const rel = path.relative(entryDir, noExt).split(path.sep).join('/');
|
|
368
399
|
return `${rel}.d.ts`;
|
|
369
400
|
})
|
|
@@ -372,19 +403,21 @@ export function captureStub(opts) {
|
|
|
372
403
|
// ---- Post-emit specifier rewrite (paths mappings + absolute internals) ----
|
|
373
404
|
let denoRewrites = 0;
|
|
374
405
|
try {
|
|
375
|
-
denoRewrites = deno?.rewrite(typesDir, emittedFiles, sourceByEmitted) ?? 0;
|
|
406
|
+
denoRewrites = deno?.rewrite(stubGuard, typesDir, emittedFiles, sourceByEmitted) ?? 0;
|
|
376
407
|
}
|
|
377
408
|
catch (err) {
|
|
378
409
|
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
379
410
|
}
|
|
380
411
|
const rewritten = rewriteEmittedSpecifiers({
|
|
412
|
+
guard: stubGuard,
|
|
413
|
+
outside: placed.outside,
|
|
381
414
|
typesDir,
|
|
382
415
|
files: emittedFiles,
|
|
383
416
|
options: parsed.options,
|
|
384
417
|
configPath: projectConfigPath,
|
|
385
418
|
entryDir,
|
|
386
419
|
});
|
|
387
|
-
const specifierRewrites = denoRewrites + rewritten.rewrites;
|
|
420
|
+
const specifierRewrites = denoRewrites + placed.rewrites + rewritten.rewrites;
|
|
388
421
|
// ---- Pin external deps: installed node_modules first, lockfile fallback ----
|
|
389
422
|
// Externals are collected AFTER the rewrite pass: a rewritten paths
|
|
390
423
|
// specifier is internal, not a dependency.
|
|
@@ -427,14 +460,14 @@ export function captureStub(opts) {
|
|
|
427
460
|
}
|
|
428
461
|
const dependencyRoot = deno?.config.workspaceRoot ?? repoRoot;
|
|
429
462
|
const bareCheckout = !deno && !fs.existsSync(path.join(dependencyRoot, 'node_modules'));
|
|
430
|
-
|
|
463
|
+
stubGuard.writeFile(path.join(stubDir, 'package.json'), JSON.stringify({
|
|
431
464
|
name: packageName,
|
|
432
465
|
version: '0.0.0-carrick',
|
|
433
466
|
private: true,
|
|
434
467
|
types: './types/surface.d.ts',
|
|
435
468
|
dependencies: pinned,
|
|
436
469
|
}, null, 2) + '\n');
|
|
437
|
-
|
|
470
|
+
stubGuard.writeFile(path.join(stubDir, 'tsconfig.snapshot.json'), JSON.stringify({
|
|
438
471
|
ts_version: ts.version,
|
|
439
472
|
strict: parsed.options.strict ?? false,
|
|
440
473
|
strictNullChecks: parsed.options.strictNullChecks ?? parsed.options.strict ?? false,
|
|
@@ -444,6 +477,7 @@ export function captureStub(opts) {
|
|
|
444
477
|
}, null, 2) + '\n');
|
|
445
478
|
// ---- Capture-time self-check (per-alias closure attribution) ----
|
|
446
479
|
const aliases = selfCheckStub({
|
|
480
|
+
guard: stubGuard,
|
|
447
481
|
stubDir,
|
|
448
482
|
surfaceAbsPath,
|
|
449
483
|
resolved,
|
|
@@ -455,7 +489,7 @@ export function captureStub(opts) {
|
|
|
455
489
|
: undefined,
|
|
456
490
|
});
|
|
457
491
|
const fidelity = computeFidelity(aliases);
|
|
458
|
-
|
|
492
|
+
stubGuard.writeFile(path.join(stubDir, 'carrick-manifest.json'), JSON.stringify({
|
|
459
493
|
package_name: packageName,
|
|
460
494
|
ts_version: ts.version,
|
|
461
495
|
bare_checkout: bareCheckout,
|
|
@@ -600,7 +634,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
600
634
|
for (const anchor of opts.anchors) {
|
|
601
635
|
placeholderLines.push(`export type ${anchor.alias} = unknown;`);
|
|
602
636
|
}
|
|
603
|
-
|
|
637
|
+
ctx.guard.writeFile(ctx.entryPath, placeholderLines.join('\n') + '\n');
|
|
604
638
|
try {
|
|
605
639
|
const anchorSources = [
|
|
606
640
|
...new Set(opts.anchors
|
|
@@ -638,6 +672,6 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
638
672
|
}
|
|
639
673
|
finally {
|
|
640
674
|
if (fs.existsSync(ctx.entryPath))
|
|
641
|
-
|
|
675
|
+
ctx.guard.unlink(ctx.entryPath);
|
|
642
676
|
}
|
|
643
677
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Place every emitted declaration inside the stub's types tree
|
|
3
|
+
* (carrick#1770).
|
|
4
|
+
*
|
|
5
|
+
* The tree mirrors the emit's rootDir. A program can also reach a source
|
|
6
|
+
* outside it: a service that reads a sibling package's SOURCE through a
|
|
7
|
+
* `paths` mapping or a relative import. tsc does not emit that file's
|
|
8
|
+
* declaration under `outDir`; it hands the write callback the source's own
|
|
9
|
+
* path with `.d.ts`. Such a file is placed under `OUTSIDE_DIR`, mirroring its
|
|
10
|
+
* path from the deepest directory it shares with rootDir, and every relative
|
|
11
|
+
* specifier that crosses between the two parts of the tree is rewritten to
|
|
12
|
+
* where its target now sits. Specifiers are read the way tsc wrote them:
|
|
13
|
+
* relative to the SOURCE location of the file that holds them.
|
|
14
|
+
*
|
|
15
|
+
* A capture whose program stays inside rootDir is placed exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
/** Tree directory holding declarations of sources outside rootDir. */
|
|
18
|
+
export declare const OUTSIDE_DIR = "__outside__";
|
|
19
|
+
export interface PlacedTree {
|
|
20
|
+
/** Emitted file name (as tsc gave it) -> tree-relative POSIX path. */
|
|
21
|
+
relOf: Map<string, string>;
|
|
22
|
+
/** Emitted file name -> declaration text, cross-root specifiers rewritten. */
|
|
23
|
+
textOf: Map<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Absolute source-side path of each outside declaration, extension
|
|
26
|
+
* stripped -> its tree-relative path. Empty when nothing was outside.
|
|
27
|
+
*/
|
|
28
|
+
outside: Map<string, string>;
|
|
29
|
+
/** Relative specifiers rewritten across the root. */
|
|
30
|
+
rewrites: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Where each emitted declaration goes. `emitted` maps tsc's file name to its
|
|
34
|
+
* text; `surfaceDeclaration` is renamed to `surface.d.ts` at the tree root.
|
|
35
|
+
*/
|
|
36
|
+
export declare function placeEmittedTree(args: {
|
|
37
|
+
emitted: Map<string, string>;
|
|
38
|
+
staging: string;
|
|
39
|
+
entryDir: string;
|
|
40
|
+
surfaceDeclaration: string;
|
|
41
|
+
}): PlacedTree;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Place every emitted declaration inside the stub's types tree
|
|
3
|
+
* (carrick#1770).
|
|
4
|
+
*
|
|
5
|
+
* The tree mirrors the emit's rootDir. A program can also reach a source
|
|
6
|
+
* outside it: a service that reads a sibling package's SOURCE through a
|
|
7
|
+
* `paths` mapping or a relative import. tsc does not emit that file's
|
|
8
|
+
* declaration under `outDir`; it hands the write callback the source's own
|
|
9
|
+
* path with `.d.ts`. Such a file is placed under `OUTSIDE_DIR`, mirroring its
|
|
10
|
+
* path from the deepest directory it shares with rootDir, and every relative
|
|
11
|
+
* specifier that crosses between the two parts of the tree is rewritten to
|
|
12
|
+
* where its target now sits. Specifiers are read the way tsc wrote them:
|
|
13
|
+
* relative to the SOURCE location of the file that holds them.
|
|
14
|
+
*
|
|
15
|
+
* A capture whose program stays inside rootDir is placed exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { rewriteSpecifiers } from './specifiers.js';
|
|
19
|
+
/** Tree directory holding declarations of sources outside rootDir. */
|
|
20
|
+
export const OUTSIDE_DIR = '__outside__';
|
|
21
|
+
const DECLARATION_EXT = /\.d\.(ts|mts|cts)$/;
|
|
22
|
+
/**
|
|
23
|
+
* Where each emitted declaration goes. `emitted` maps tsc's file name to its
|
|
24
|
+
* text; `surfaceDeclaration` is renamed to `surface.d.ts` at the tree root.
|
|
25
|
+
*/
|
|
26
|
+
export function placeEmittedTree(args) {
|
|
27
|
+
const relOf = new Map();
|
|
28
|
+
const textOf = new Map(args.emitted);
|
|
29
|
+
// Where tsc would have put each file in the source tree: the place its
|
|
30
|
+
// relative specifiers are written from.
|
|
31
|
+
const sourceSideOf = new Map();
|
|
32
|
+
const outsideFiles = [];
|
|
33
|
+
for (const fileName of args.emitted.keys()) {
|
|
34
|
+
const rel = posix(path.relative(args.staging, fileName));
|
|
35
|
+
if (escapes(rel)) {
|
|
36
|
+
outsideFiles.push(fileName);
|
|
37
|
+
sourceSideOf.set(fileName, path.resolve(fileName));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
sourceSideOf.set(fileName, path.join(args.entryDir, rel));
|
|
41
|
+
relOf.set(fileName, path.basename(rel) === args.surfaceDeclaration ? 'surface.d.ts' : rel);
|
|
42
|
+
}
|
|
43
|
+
const outside = new Map();
|
|
44
|
+
if (outsideFiles.length === 0)
|
|
45
|
+
return { relOf, textOf, outside, rewrites: 0 };
|
|
46
|
+
const shared = commonDirectory([args.entryDir, ...outsideFiles.map((f) => path.dirname(path.resolve(f)))]);
|
|
47
|
+
for (const fileName of outsideFiles) {
|
|
48
|
+
const rel = path.posix.join(OUTSIDE_DIR, posix(path.relative(shared, path.resolve(fileName))));
|
|
49
|
+
relOf.set(fileName, rel);
|
|
50
|
+
outside.set(path.resolve(fileName).replace(DECLARATION_EXT, ''), rel);
|
|
51
|
+
}
|
|
52
|
+
// Source-side module id (absolute, extensionless) -> tree path.
|
|
53
|
+
const treeOf = new Map();
|
|
54
|
+
for (const [fileName, rel] of relOf) {
|
|
55
|
+
treeOf.set(sourceSideOf.get(fileName).replace(DECLARATION_EXT, ''), rel);
|
|
56
|
+
}
|
|
57
|
+
let rewrites = 0;
|
|
58
|
+
for (const [fileName, rel] of relOf) {
|
|
59
|
+
const fromSource = path.dirname(sourceSideOf.get(fileName));
|
|
60
|
+
const fromTree = path.posix.dirname(rel);
|
|
61
|
+
const result = rewriteSpecifiers(textOf.get(fileName), (spec) => {
|
|
62
|
+
if (!spec.startsWith('./') && !spec.startsWith('../'))
|
|
63
|
+
return undefined;
|
|
64
|
+
const extension = /\.(m|c)?js$/.exec(spec)?.[0] ?? '';
|
|
65
|
+
const target = path.resolve(fromSource, spec.slice(0, spec.length - extension.length));
|
|
66
|
+
const targetRel = treeOf.get(target) ?? treeOf.get(path.join(target, 'index'));
|
|
67
|
+
if (targetRel === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
const targetId = targetRel.replace(DECLARATION_EXT, '');
|
|
70
|
+
// Unchanged when the specifier, read from the file's place in the tree,
|
|
71
|
+
// already reaches the same declaration.
|
|
72
|
+
const literal = path.posix.normalize(path.posix.join(fromTree, spec.slice(0, spec.length - extension.length)));
|
|
73
|
+
if (literal === targetId || `${literal}/index` === targetId)
|
|
74
|
+
return undefined;
|
|
75
|
+
let next = path.posix.relative(fromTree, targetId);
|
|
76
|
+
if (!next.startsWith('.'))
|
|
77
|
+
next = `./${next}`;
|
|
78
|
+
return next + extension;
|
|
79
|
+
});
|
|
80
|
+
textOf.set(fileName, result.text);
|
|
81
|
+
rewrites += result.rewrites;
|
|
82
|
+
}
|
|
83
|
+
return { relOf, textOf, outside, rewrites };
|
|
84
|
+
}
|
|
85
|
+
function posix(p) {
|
|
86
|
+
return p.split(path.sep).join('/');
|
|
87
|
+
}
|
|
88
|
+
function escapes(rel) {
|
|
89
|
+
return rel === '..' || rel.startsWith('../') || path.isAbsolute(rel);
|
|
90
|
+
}
|
|
91
|
+
/** The deepest directory containing every one of `dirs` (absolute). */
|
|
92
|
+
function commonDirectory(dirs) {
|
|
93
|
+
let shared = path.resolve(dirs[0]);
|
|
94
|
+
for (const dir of dirs.slice(1)) {
|
|
95
|
+
const resolved = path.resolve(dir);
|
|
96
|
+
while (shared !== path.dirname(shared) && path.relative(shared, resolved).startsWith('..')) {
|
|
97
|
+
shared = path.dirname(shared);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return shared;
|
|
101
|
+
}
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
* as dangling internals with a recorded reason, which is the honest outcome.
|
|
14
14
|
*/
|
|
15
15
|
import ts from 'typescript';
|
|
16
|
+
import type { WriteGuard } from './guarded-fs.js';
|
|
16
17
|
import { type PathsPattern } from './specifiers.js';
|
|
17
18
|
export interface RewriteArgs {
|
|
19
|
+
/** Writes are held to the stub (carrick#1748). */
|
|
20
|
+
guard: WriteGuard;
|
|
18
21
|
/** Absolute path of the stub's types/ directory. */
|
|
19
22
|
typesDir: string;
|
|
20
23
|
/** Tree-relative emitted file paths (as recorded in emitted_files, without
|
|
@@ -26,6 +29,11 @@ export interface RewriteArgs {
|
|
|
26
29
|
configPath: string;
|
|
27
30
|
/** Effective rootDir the emit ran with (tree layout mirrors it). */
|
|
28
31
|
entryDir: string;
|
|
32
|
+
/**
|
|
33
|
+
* Declarations of sources outside rootDir, placed under the tree
|
|
34
|
+
* (carrick#1770): absolute source-side path without extension -> tree path.
|
|
35
|
+
*/
|
|
36
|
+
outside: Map<string, string>;
|
|
29
37
|
}
|
|
30
38
|
export declare function parsePathsPatterns(options: ts.CompilerOptions, configPath: string): PathsPattern[];
|
|
31
39
|
export interface RewriteResult {
|
|
@@ -37,11 +37,13 @@ export function parsePathsPatterns(options, configPath) {
|
|
|
37
37
|
return patterns;
|
|
38
38
|
}
|
|
39
39
|
/** Map an absolute source path to its tree-relative emitted file, if any. */
|
|
40
|
-
function treeFileFor(absSource, entryDir, emitted) {
|
|
40
|
+
function treeFileFor(absSource, entryDir, emitted, outside) {
|
|
41
41
|
const noExt = absSource.replace(/\.(d\.ts|ts|tsx|mts|cts)$/, '');
|
|
42
42
|
const rel = path.relative(entryDir, noExt).split(path.sep).join('/');
|
|
43
|
-
if (rel.startsWith('..'))
|
|
44
|
-
|
|
43
|
+
if (rel.startsWith('..')) {
|
|
44
|
+
const resolved = path.resolve(noExt);
|
|
45
|
+
return outside.get(resolved) ?? outside.get(path.join(resolved, 'index'));
|
|
46
|
+
}
|
|
45
47
|
for (const candidate of [`${rel}.d.ts`, `${rel}/index.d.ts`]) {
|
|
46
48
|
if (emitted.has(candidate))
|
|
47
49
|
return candidate;
|
|
@@ -89,7 +91,7 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
89
91
|
// Import types first, while the member name is still attached to its
|
|
90
92
|
// specifier; an in-tree target is left for the general pass below.
|
|
91
93
|
const text = original.replace(ABSOLUTE_IMPORT_TYPE, (whole, open, quote, spec, close, dot, name) => {
|
|
92
|
-
if (treeFileFor(spec, args.entryDir, emitted))
|
|
94
|
+
if (treeFileFor(spec, args.entryDir, emitted, args.outside))
|
|
93
95
|
return whole;
|
|
94
96
|
const replacement = installed(spec, name);
|
|
95
97
|
if (replacement === undefined)
|
|
@@ -100,7 +102,7 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
100
102
|
const { text: rewritten, rewrites } = rewriteSpecifiers(text, (spec) => {
|
|
101
103
|
// Absolute paths: an emitted tree file, else an installed package.
|
|
102
104
|
if (spec.startsWith('/')) {
|
|
103
|
-
const target = treeFileFor(spec, args.entryDir, emitted);
|
|
105
|
+
const target = treeFileFor(spec, args.entryDir, emitted, args.outside);
|
|
104
106
|
return target ? relativeSpecifier(file, target) : installed(spec);
|
|
105
107
|
}
|
|
106
108
|
if (isRelative(spec))
|
|
@@ -113,7 +115,7 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
113
115
|
continue;
|
|
114
116
|
for (const targetTemplate of pattern.targets) {
|
|
115
117
|
const absTarget = targetTemplate.replace('*', star);
|
|
116
|
-
const target = treeFileFor(absTarget, args.entryDir, emitted);
|
|
118
|
+
const target = treeFileFor(absTarget, args.entryDir, emitted, args.outside);
|
|
117
119
|
if (target)
|
|
118
120
|
return relativeSpecifier(file, target);
|
|
119
121
|
}
|
|
@@ -124,7 +126,7 @@ export function rewriteEmittedSpecifiers(args) {
|
|
|
124
126
|
return undefined;
|
|
125
127
|
});
|
|
126
128
|
if (rewrites + importTypeRewrites > 0) {
|
|
127
|
-
|
|
129
|
+
args.guard.writeFile(absFile, rewritten);
|
|
128
130
|
total += rewrites + importTypeRewrites;
|
|
129
131
|
}
|
|
130
132
|
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Seam: node builtins + `typescript`, like the rest of this directory.
|
|
30
30
|
*/
|
|
31
|
+
import type { WriteGuard } from './guarded-fs.js';
|
|
31
32
|
/** What one file's repair removed, for the caller's re-check. */
|
|
32
33
|
export interface RepairedFile {
|
|
33
34
|
/** Specifiers whose import statements were dropped. */
|
|
@@ -42,5 +43,14 @@ export interface RepairedFile {
|
|
|
42
43
|
* `failing` maps an absolute file path to the specifiers the stub could not
|
|
43
44
|
* resolve from it. Returns the files actually rewritten; a file whose names
|
|
44
45
|
* cannot all be replaced by `unknown` is left exactly as it was.
|
|
46
|
+
*
|
|
47
|
+
* Only a file `tree` (a guard over the stub's types tree) lets it write is
|
|
48
|
+
* repaired (carrick#1742). The self-check program also loads files the stub
|
|
49
|
+
* merely reaches: the repo's own sources, through a workspace package linked
|
|
50
|
+
* into the `node_modules` the stub borrows, or a runtime's dependency cache.
|
|
51
|
+
* Those report their missing modules exactly as an emitted declaration does,
|
|
52
|
+
* and they belong to the user: a scan reads them and never writes them. The
|
|
53
|
+
* guard decides on the resolved path, so a file reached through the link is
|
|
54
|
+
* the repo's, whatever path the program loaded it by.
|
|
45
55
|
*/
|
|
46
|
-
export declare function repairDanglingImports(failing: Map<string, Set<string
|
|
56
|
+
export declare function repairDanglingImports(failing: Map<string, Set<string>>, tree: WriteGuard): Map<string, RepairedFile>;
|