carrick 0.3.102 → 0.3.104
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/api.d.ts +6 -0
- package/sidecar/dist/src/capture/check-classify.js +27 -0
- package/sidecar/dist/src/capture/check-fields.js +19 -5
- package/sidecar/dist/src/capture/check-probe.d.ts +6 -1
- package/sidecar/dist/src/capture/check-probe.js +45 -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.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +65 -27
- 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/capture/service-config.d.ts +19 -0
- package/sidecar/dist/src/capture/service-config.js +57 -0
- package/sidecar/dist/src/index.d.ts +1 -1
- package/sidecar/dist/src/index.js +4 -115
- package/sidecar/dist/src/origin.d.ts +49 -8
- package/sidecar/dist/src/origin.js +81 -8
- package/sidecar/dist/src/project-loader.d.ts +10 -2
- package/sidecar/dist/src/project-loader.js +22 -21
- 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 +39 -178
- package/sidecar/dist/src/validators.d.ts +70 -914
- package/sidecar/dist/src/validators.js +2 -55
- package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
- package/sidecar/dist/src/monorepo-builder.js +0 -584
|
@@ -37,17 +37,28 @@ import * as fs from 'node:fs';
|
|
|
37
37
|
* `failing` maps an absolute file path to the specifiers the stub could not
|
|
38
38
|
* resolve from it. Returns the files actually rewritten; a file whose names
|
|
39
39
|
* cannot all be replaced by `unknown` is left exactly as it was.
|
|
40
|
+
*
|
|
41
|
+
* Only a file `tree` (a guard over the stub's types tree) lets it write is
|
|
42
|
+
* repaired (carrick#1742). The self-check program also loads files the stub
|
|
43
|
+
* merely reaches: the repo's own sources, through a workspace package linked
|
|
44
|
+
* into the `node_modules` the stub borrows, or a runtime's dependency cache.
|
|
45
|
+
* Those report their missing modules exactly as an emitted declaration does,
|
|
46
|
+
* and they belong to the user: a scan reads them and never writes them. The
|
|
47
|
+
* guard decides on the resolved path, so a file reached through the link is
|
|
48
|
+
* the repo's, whatever path the program loaded it by.
|
|
40
49
|
*/
|
|
41
|
-
export function repairDanglingImports(failing) {
|
|
50
|
+
export function repairDanglingImports(failing, tree) {
|
|
42
51
|
const repaired = new Map();
|
|
43
52
|
for (const [file, specifiers] of failing) {
|
|
44
|
-
|
|
53
|
+
if (!tree.allowsWrite(file))
|
|
54
|
+
continue;
|
|
55
|
+
const result = repairFile(file, specifiers, tree);
|
|
45
56
|
if (result)
|
|
46
57
|
repaired.set(file, result);
|
|
47
58
|
}
|
|
48
59
|
return repaired;
|
|
49
60
|
}
|
|
50
|
-
function repairFile(file, specifiers) {
|
|
61
|
+
function repairFile(file, specifiers, tree) {
|
|
51
62
|
let text;
|
|
52
63
|
try {
|
|
53
64
|
text = fs.readFileSync(file, 'utf8');
|
|
@@ -116,7 +127,7 @@ function repairFile(file, specifiers) {
|
|
|
116
127
|
for (const edit of edits) {
|
|
117
128
|
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
118
129
|
}
|
|
119
|
-
|
|
130
|
+
tree.writeFile(file, out);
|
|
120
131
|
return { specifiers: [...removedSpecifiers].sort(), names: [...names].sort() };
|
|
121
132
|
}
|
|
122
133
|
/** The module specifier an import STATEMENT names, if it names one. */
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
import ts from 'typescript';
|
|
33
33
|
import type { CaptureAliasRecord } from './api.js';
|
|
34
34
|
import type { ResolvedAnchor } from './anchors.js';
|
|
35
|
+
import type { WriteGuard } from './guarded-fs.js';
|
|
35
36
|
export interface SelfCheckArgs {
|
|
37
|
+
/** Writes are held to the stub (carrick#1748). */
|
|
38
|
+
guard: WriteGuard;
|
|
36
39
|
stubDir: string;
|
|
37
40
|
surfaceAbsPath: string;
|
|
38
41
|
resolved: ResolvedAnchor[];
|
|
@@ -53,7 +53,7 @@ export function selfCheckStub(args) {
|
|
|
53
53
|
const linkPath = path.join(args.stubDir, 'node_modules');
|
|
54
54
|
let linked = false;
|
|
55
55
|
if (!args.bareCheckout && fs.existsSync(repoNodeModules) && !fs.existsSync(linkPath)) {
|
|
56
|
-
|
|
56
|
+
args.guard.symlink(repoNodeModules, linkPath, 'dir');
|
|
57
57
|
linked = true;
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
@@ -65,7 +65,7 @@ export function selfCheckStub(args) {
|
|
|
65
65
|
// the verdict: it reads the repaired text, and a name the rewrite did not
|
|
66
66
|
// reach comes back as a `Cannot find name` diagnostic that
|
|
67
67
|
// `repairedNameFailures` folds back into the same dangling specifier.
|
|
68
|
-
const repaired = repairDanglingImports(first.internalFailuresByFile);
|
|
68
|
+
const repaired = repairDanglingImports(first.internalFailuresByFile, args.guard.narrow(typesDir));
|
|
69
69
|
if (repaired.size === 0)
|
|
70
70
|
return first.records;
|
|
71
71
|
return runSelfCheck(args, treeFiles, repaired).records;
|
|
@@ -74,7 +74,7 @@ export function selfCheckStub(args) {
|
|
|
74
74
|
// unlinkSync, not rmSync: the link target is a directory and rmSync
|
|
75
75
|
// refuses symlinks-to-directories with EISDIR.
|
|
76
76
|
if (linked)
|
|
77
|
-
|
|
77
|
+
args.guard.unlink(linkPath);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
function runSelfCheck(args, treeFiles, repaired) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tsconfig types a service that names none (carrick#1776).
|
|
3
|
+
*
|
|
4
|
+
* Both readers of a service, the init'd project and capture, ask this one
|
|
5
|
+
* function, so they type the service under the same options.
|
|
6
|
+
*
|
|
7
|
+
* The service root is searched first, for the names a service keeps its own
|
|
8
|
+
* config under. Above it, the search is `tsc`'s own when it is run with no
|
|
9
|
+
* `-p`: the nearest `tsconfig.json` in an ancestor directory. A layout that
|
|
10
|
+
* keeps one config above several services (their module resolution, `paths`,
|
|
11
|
+
* `lib`) is typed under that config, as the repo's own `tsc` types it; without
|
|
12
|
+
* it, every import only that config resolves read `any`.
|
|
13
|
+
*
|
|
14
|
+
* The walk stops at the scan root, inclusive: a config outside the scanned
|
|
15
|
+
* tree is never read. With no scan root, or a service outside it, only the
|
|
16
|
+
* service root is searched.
|
|
17
|
+
*/
|
|
18
|
+
/** Absolute path of the config that types the service, or undefined for none. */
|
|
19
|
+
export declare function findServiceTsconfig(serviceRoot: string, scanRoot?: string): string | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tsconfig types a service that names none (carrick#1776).
|
|
3
|
+
*
|
|
4
|
+
* Both readers of a service, the init'd project and capture, ask this one
|
|
5
|
+
* function, so they type the service under the same options.
|
|
6
|
+
*
|
|
7
|
+
* The service root is searched first, for the names a service keeps its own
|
|
8
|
+
* config under. Above it, the search is `tsc`'s own when it is run with no
|
|
9
|
+
* `-p`: the nearest `tsconfig.json` in an ancestor directory. A layout that
|
|
10
|
+
* keeps one config above several services (their module resolution, `paths`,
|
|
11
|
+
* `lib`) is typed under that config, as the repo's own `tsc` types it; without
|
|
12
|
+
* it, every import only that config resolves read `any`.
|
|
13
|
+
*
|
|
14
|
+
* The walk stops at the scan root, inclusive: a config outside the scanned
|
|
15
|
+
* tree is never read. With no scan root, or a service outside it, only the
|
|
16
|
+
* service root is searched.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
/** The names a service root is searched for, in order. */
|
|
21
|
+
const SERVICE_ROOT_NAMES = ['tsconfig.json', 'tsconfig.build.json', 'tsconfig.app.json'];
|
|
22
|
+
/** The name an ancestor directory is searched for, as `tsc` searches. */
|
|
23
|
+
const ANCESTOR_NAME = 'tsconfig.json';
|
|
24
|
+
/** Absolute path of the config that types the service, or undefined for none. */
|
|
25
|
+
export function findServiceTsconfig(serviceRoot, scanRoot) {
|
|
26
|
+
const root = path.resolve(serviceRoot);
|
|
27
|
+
for (const name of SERVICE_ROOT_NAMES) {
|
|
28
|
+
const candidate = path.join(root, name);
|
|
29
|
+
if (fs.existsSync(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
if (scanRoot === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
// Containment is decided on real paths, so a symlink on either end cannot
|
|
35
|
+
// hide the service inside the scan root or place it outside; the walk then
|
|
36
|
+
// climbs the path as given, one directory per level between them.
|
|
37
|
+
const fromBound = path.relative(realPath(scanRoot), realPath(root));
|
|
38
|
+
if (fromBound === '' || fromBound.startsWith('..') || path.isAbsolute(fromBound))
|
|
39
|
+
return undefined;
|
|
40
|
+
let dir = root;
|
|
41
|
+
for (let level = fromBound.split(path.sep).length; level > 0; level--) {
|
|
42
|
+
dir = path.dirname(dir);
|
|
43
|
+
const candidate = path.join(dir, ANCESTOR_NAME);
|
|
44
|
+
if (fs.existsSync(candidate))
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/** The path with every symlink resolved; the path as given when it does not exist. */
|
|
50
|
+
function realPath(p) {
|
|
51
|
+
try {
|
|
52
|
+
return fs.realpathSync(p);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return path.resolve(p);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module implements a message loop that:
|
|
5
5
|
* 1. Listens on stdin for JSON requests
|
|
6
|
-
* 2. Processes each request (
|
|
6
|
+
* 2. Processes each request (see handleRequest for the actions)
|
|
7
7
|
* 3. Writes JSON responses to stdout
|
|
8
8
|
*
|
|
9
9
|
* IMPORTANT:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module implements a message loop that:
|
|
5
5
|
* 1. Listens on stdin for JSON requests
|
|
6
|
-
* 2. Processes each request (
|
|
6
|
+
* 2. Processes each request (see handleRequest for the actions)
|
|
7
7
|
* 3. Writes JSON responses to stdout
|
|
8
8
|
*
|
|
9
9
|
* IMPORTANT:
|
|
@@ -15,9 +15,8 @@ import * as path from 'node:path';
|
|
|
15
15
|
import * as readline from 'node:readline';
|
|
16
16
|
import { parseRequest } from './validators.js';
|
|
17
17
|
import { ProjectLoader } from './project-loader.js';
|
|
18
|
-
import { TypeBundler
|
|
18
|
+
import { TypeBundler } from './bundler.js';
|
|
19
19
|
import { TypeInferrer } from './type-inferrer.js';
|
|
20
|
-
import { MonorepoBuilder } from './monorepo-builder.js';
|
|
21
20
|
import { DefinitionResolver } from './definition-resolver.js';
|
|
22
21
|
import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
|
|
23
22
|
import { Retyper } from './retype.js';
|
|
@@ -26,7 +25,6 @@ import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
|
|
|
26
25
|
// Module-level state
|
|
27
26
|
// ===========================================================================
|
|
28
27
|
let projectLoader = null;
|
|
29
|
-
let monorepoBuilder = null;
|
|
30
28
|
let initTimeMs = null;
|
|
31
29
|
/**
|
|
32
30
|
* Components by project key (`ProjectLoader.projectKeyFor`): `''` is the
|
|
@@ -62,7 +60,6 @@ function projectComponents(key = '') {
|
|
|
62
60
|
});
|
|
63
61
|
built = {
|
|
64
62
|
typeBundler: new TypeBundler({ project, repoRoot }),
|
|
65
|
-
surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
|
|
66
63
|
typeInferrer,
|
|
67
64
|
definitionResolver: new DefinitionResolver({ project }),
|
|
68
65
|
// Locates calls exactly as `infer` does, so it rewrites the node the
|
|
@@ -128,6 +125,7 @@ function handleInit(request) {
|
|
|
128
125
|
projectLoader = new ProjectLoader({
|
|
129
126
|
repoRoot: request.repo_root,
|
|
130
127
|
tsconfigPath: request.tsconfig_path,
|
|
128
|
+
scanRoot: request.scan_root,
|
|
131
129
|
tsconfigSnapshot: request.tsconfig_snapshot,
|
|
132
130
|
pinnedDependencies: request.pinned_dependencies,
|
|
133
131
|
});
|
|
@@ -143,8 +141,6 @@ function handleInit(request) {
|
|
|
143
141
|
// The project and everything that reads it are built by the first request
|
|
144
142
|
// that needs them, so readiness costs the same on a bare checkout as on
|
|
145
143
|
// one with its dependencies installed (carrick#749).
|
|
146
|
-
// Initialize monorepo builder (doesn't need project)
|
|
147
|
-
monorepoBuilder = new MonorepoBuilder();
|
|
148
144
|
initTimeMs = result.initTimeMs || Math.round(performance.now() - startTime);
|
|
149
145
|
log(`Initialization complete in ${initTimeMs}ms`);
|
|
150
146
|
return {
|
|
@@ -202,38 +198,6 @@ function handleBundle(request) {
|
|
|
202
198
|
};
|
|
203
199
|
}
|
|
204
200
|
}
|
|
205
|
-
/**
|
|
206
|
-
* Handle the 'emit_surface' action - emit a surface .d.ts with rewritten specifiers
|
|
207
|
-
*/
|
|
208
|
-
function handleEmitSurface(request) {
|
|
209
|
-
try {
|
|
210
|
-
log(`Emitting surface for repo '${request.repo_name}' with ${request.payloads.length} payload(s)`);
|
|
211
|
-
const result = projectComponents().surfaceEmitter.emit(request.repo_name, request.payloads, request.output_path);
|
|
212
|
-
if (!result.success) {
|
|
213
|
-
return {
|
|
214
|
-
request_id: request.request_id,
|
|
215
|
-
status: 'error',
|
|
216
|
-
errors: result.errors,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
request_id: request.request_id,
|
|
221
|
-
status: 'success',
|
|
222
|
-
output_path: result.output_path,
|
|
223
|
-
surface_content: result.surface_content,
|
|
224
|
-
manifest: result.manifest,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
229
|
-
logError(`Surface emission failed: ${error}`);
|
|
230
|
-
return {
|
|
231
|
-
request_id: request.request_id,
|
|
232
|
-
status: 'error',
|
|
233
|
-
errors: [error],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
201
|
/**
|
|
238
202
|
* Handle the 'capture_v2' action - v2 "tsc as serializer" capture.
|
|
239
203
|
* Stateless by design: unlike bundle/infer it needs no init'd ts-morph
|
|
@@ -250,6 +214,7 @@ function handleCaptureV2(request) {
|
|
|
250
214
|
anchors: request.anchors,
|
|
251
215
|
outDir: request.out_dir,
|
|
252
216
|
tsconfigPath: request.tsconfig_path,
|
|
217
|
+
scanRoot: request.scan_root,
|
|
253
218
|
});
|
|
254
219
|
return {
|
|
255
220
|
request_id: request.request_id,
|
|
@@ -461,76 +426,6 @@ function handleListLibrarySurface(request) {
|
|
|
461
426
|
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
462
427
|
}
|
|
463
428
|
}
|
|
464
|
-
/**
|
|
465
|
-
* Handle the 'build_workspace' action - build synthetic monorepo workspace
|
|
466
|
-
*/
|
|
467
|
-
function handleBuildWorkspace(request) {
|
|
468
|
-
// MonorepoBuilder doesn't require project initialization
|
|
469
|
-
if (!monorepoBuilder) {
|
|
470
|
-
monorepoBuilder = new MonorepoBuilder();
|
|
471
|
-
}
|
|
472
|
-
try {
|
|
473
|
-
log(`Building synthetic workspace with ${request.repos.length} repo(s)`);
|
|
474
|
-
const result = monorepoBuilder.build(request.repos, request.workspace_root);
|
|
475
|
-
if (!result.success) {
|
|
476
|
-
return {
|
|
477
|
-
request_id: request.request_id,
|
|
478
|
-
status: 'error',
|
|
479
|
-
errors: result.errors,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
return {
|
|
483
|
-
request_id: request.request_id,
|
|
484
|
-
status: 'success',
|
|
485
|
-
workspace_path: result.workspace_path,
|
|
486
|
-
stub_packages: result.stub_packages,
|
|
487
|
-
checker_path: result.checker_path,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
catch (err) {
|
|
491
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
492
|
-
logError(`Workspace build failed: ${error}`);
|
|
493
|
-
return {
|
|
494
|
-
request_id: request.request_id,
|
|
495
|
-
status: 'error',
|
|
496
|
-
errors: [error],
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Handle the 'check_compatibility' action - run type compatibility checks
|
|
502
|
-
*/
|
|
503
|
-
function handleCheckCompatibility(request) {
|
|
504
|
-
if (!monorepoBuilder) {
|
|
505
|
-
monorepoBuilder = new MonorepoBuilder();
|
|
506
|
-
}
|
|
507
|
-
try {
|
|
508
|
-
log(`Running compatibility checks: ${request.checks.length} check(s)`);
|
|
509
|
-
const result = monorepoBuilder.checkCompatibility(request.workspace_root, request.checks);
|
|
510
|
-
if (!result.success) {
|
|
511
|
-
return {
|
|
512
|
-
request_id: request.request_id,
|
|
513
|
-
status: 'error',
|
|
514
|
-
errors: result.errors,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
request_id: request.request_id,
|
|
519
|
-
status: 'success',
|
|
520
|
-
results: result.results,
|
|
521
|
-
diagnostics: result.diagnostics,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
catch (err) {
|
|
525
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
526
|
-
logError(`Compatibility check failed: ${error}`);
|
|
527
|
-
return {
|
|
528
|
-
request_id: request.request_id,
|
|
529
|
-
status: 'error',
|
|
530
|
-
errors: [error],
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
429
|
/**
|
|
535
430
|
* Handle the 'resolve_definitions' action - resolve surface aliases from a
|
|
536
431
|
* v2 capture stub package's declaration tree.
|
|
@@ -597,16 +492,10 @@ function handleRequest(request) {
|
|
|
597
492
|
return handleInit(request);
|
|
598
493
|
case 'bundle':
|
|
599
494
|
return handleBundle(request);
|
|
600
|
-
case 'emit_surface':
|
|
601
|
-
return handleEmitSurface(request);
|
|
602
495
|
case 'capture_v2':
|
|
603
496
|
return handleCaptureV2(request);
|
|
604
497
|
case 'infer':
|
|
605
498
|
return handleInfer(request);
|
|
606
|
-
case 'build_workspace':
|
|
607
|
-
return handleBuildWorkspace(request);
|
|
608
|
-
case 'check_compatibility':
|
|
609
|
-
return handleCheckCompatibility(request);
|
|
610
499
|
case 'resolve_definitions':
|
|
611
500
|
return handleResolveDefinitions(request);
|
|
612
501
|
case 'retype_check':
|
|
@@ -13,7 +13,7 @@
|
|
|
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 type { ts } from 'ts-morph';
|
|
16
|
+
import type { Project, ResolutionHostFactory, ts } from 'ts-morph';
|
|
17
17
|
/**
|
|
18
18
|
* True when a declaration's source file is runtime/library origin rather than
|
|
19
19
|
* user source. Four answers, in order:
|
|
@@ -32,14 +32,55 @@ import type { ts } from 'ts-morph';
|
|
|
32
32
|
* types from its own cache, a path with no `node_modules` segment, and
|
|
33
33
|
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
34
34
|
* the graph, which the compiler records on the file. Nothing crosses the
|
|
35
|
-
* capture seam; both programs are built with that host.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* capture seam; both programs are built with that host. The compiler's
|
|
36
|
+
* record does not survive an edit to a ts-morph project, so `imports`, the
|
|
37
|
+
* resolver's answers kept for the project's life, is read beside it
|
|
38
|
+
* (`ExternalImports`, carrick#1731). One exclusion: a workspace package
|
|
39
|
+
* reached through a `node_modules` symlink is also marked external by the
|
|
40
|
+
* compiler but is the user's own source, so a file inside the checkout
|
|
41
|
+
* (the nearest `.git` above the service root) that carries no
|
|
42
|
+
* `node_modules` segment stays user source.
|
|
40
43
|
*
|
|
41
44
|
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
42
45
|
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
43
|
-
* the pair on a real program.
|
|
46
|
+
* the pair on a real program. The capture builds each program once and never
|
|
47
|
+
* edits it, so it has no `imports` to read.
|
|
44
48
|
*/
|
|
45
|
-
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
|
|
49
|
+
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string, imports?: ExternalImports): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Every file a ts-morph project's resolver reached as an external-library
|
|
52
|
+
* import, recorded as the resolver answers (carrick#1731).
|
|
53
|
+
*
|
|
54
|
+
* The compiler keeps that verdict on each program (rule 4 of
|
|
55
|
+
* `isExternalOrigin`), but ts-morph rebuilds the program after any edit to the
|
|
56
|
+
* project, such as a probe file created and removed or a file rewritten and
|
|
57
|
+
* restored, and passes every file it has loaded as a ROOT file. The compiler
|
|
58
|
+
* marks no root file as external, so after the first edit no dependency is.
|
|
59
|
+
* Where a path test cannot answer (a Deno project serves npm types from its own
|
|
60
|
+
* cache), every library type then read as the user's: framework machinery was
|
|
61
|
+
* published as a contract and the printer inlined library types it keeps by
|
|
62
|
+
* name. The resolver's answer for a file does not change with the project's
|
|
63
|
+
* history, so it is kept here for as long as the project lives.
|
|
64
|
+
*
|
|
65
|
+
* The compiler's verdict also carries down: a file a library imports, even by
|
|
66
|
+
* a relative path the resolver answers as not external, is reached while the
|
|
67
|
+
* compiler is inside an external import and is marked external too. A Deno
|
|
68
|
+
* npm package's own relative imports are answered that way, so an answer for
|
|
69
|
+
* an import made from a recorded file is recorded as well.
|
|
70
|
+
*/
|
|
71
|
+
export declare class ExternalImports {
|
|
72
|
+
private readonly files;
|
|
73
|
+
/** `factory` as it was, with every answer it gives recorded. */
|
|
74
|
+
recording(factory: ResolutionHostFactory): ResolutionHostFactory;
|
|
75
|
+
/** Whether a resolution reached `fileName` as an external-library import. */
|
|
76
|
+
has(fileName: string): boolean;
|
|
77
|
+
/** Record the external answers to `containingFile`'s imports. */
|
|
78
|
+
private record;
|
|
79
|
+
}
|
|
80
|
+
/** Keep `imports` as the record of `project`, which resolves through it. */
|
|
81
|
+
export declare function registerExternalImports(project: Project, imports: ExternalImports): void;
|
|
82
|
+
/**
|
|
83
|
+
* The record of a project the loader built with a recording resolver, or
|
|
84
|
+
* `undefined` for any other project, which reads the compiler's flag alone.
|
|
85
|
+
*/
|
|
86
|
+
export declare function externalImportsOf(project: Project): ExternalImports | undefined;
|
|
@@ -33,17 +33,21 @@ import * as path from 'node:path';
|
|
|
33
33
|
* types from its own cache, a path with no `node_modules` segment, and
|
|
34
34
|
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
35
35
|
* the graph, which the compiler records on the file. Nothing crosses the
|
|
36
|
-
* capture seam; both programs are built with that host.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* capture seam; both programs are built with that host. The compiler's
|
|
37
|
+
* record does not survive an edit to a ts-morph project, so `imports`, the
|
|
38
|
+
* resolver's answers kept for the project's life, is read beside it
|
|
39
|
+
* (`ExternalImports`, carrick#1731). One exclusion: a workspace package
|
|
40
|
+
* reached through a `node_modules` symlink is also marked external by the
|
|
41
|
+
* compiler but is the user's own source, so a file inside the checkout
|
|
42
|
+
* (the nearest `.git` above the service root) that carries no
|
|
43
|
+
* `node_modules` segment stays user source.
|
|
41
44
|
*
|
|
42
45
|
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
43
46
|
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
44
|
-
* the pair on a real program.
|
|
47
|
+
* the pair on a real program. The capture builds each program once and never
|
|
48
|
+
* edits it, so it has no `imports` to read.
|
|
45
49
|
*/
|
|
46
|
-
export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
50
|
+
export function isExternalOrigin(program, sourceFile, repoRoot, imports) {
|
|
47
51
|
const file = sourceFile.fileName.replace(/\\/g, '/');
|
|
48
52
|
if (file.includes('/.carrick/deno/')) {
|
|
49
53
|
return true;
|
|
@@ -54,7 +58,76 @@ export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
|
54
58
|
if (file.includes('/node_modules/')) {
|
|
55
59
|
return true;
|
|
56
60
|
}
|
|
57
|
-
|
|
61
|
+
const external = program.isSourceFileFromExternalLibrary(sourceFile) || imports?.has(file) === true;
|
|
62
|
+
return external && !isInsideCheckout(file, repoRoot);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every file a ts-morph project's resolver reached as an external-library
|
|
66
|
+
* import, recorded as the resolver answers (carrick#1731).
|
|
67
|
+
*
|
|
68
|
+
* The compiler keeps that verdict on each program (rule 4 of
|
|
69
|
+
* `isExternalOrigin`), but ts-morph rebuilds the program after any edit to the
|
|
70
|
+
* project, such as a probe file created and removed or a file rewritten and
|
|
71
|
+
* restored, and passes every file it has loaded as a ROOT file. The compiler
|
|
72
|
+
* marks no root file as external, so after the first edit no dependency is.
|
|
73
|
+
* Where a path test cannot answer (a Deno project serves npm types from its own
|
|
74
|
+
* cache), every library type then read as the user's: framework machinery was
|
|
75
|
+
* published as a contract and the printer inlined library types it keeps by
|
|
76
|
+
* name. The resolver's answer for a file does not change with the project's
|
|
77
|
+
* history, so it is kept here for as long as the project lives.
|
|
78
|
+
*
|
|
79
|
+
* The compiler's verdict also carries down: a file a library imports, even by
|
|
80
|
+
* a relative path the resolver answers as not external, is reached while the
|
|
81
|
+
* compiler is inside an external import and is marked external too. A Deno
|
|
82
|
+
* npm package's own relative imports are answered that way, so an answer for
|
|
83
|
+
* an import made from a recorded file is recorded as well.
|
|
84
|
+
*/
|
|
85
|
+
export class ExternalImports {
|
|
86
|
+
files = new Set();
|
|
87
|
+
/** `factory` as it was, with every answer it gives recorded. */
|
|
88
|
+
recording(factory) {
|
|
89
|
+
return (moduleResolutionHost, getCompilerOptions) => {
|
|
90
|
+
const host = factory(moduleResolutionHost, getCompilerOptions);
|
|
91
|
+
return {
|
|
92
|
+
...host,
|
|
93
|
+
...(host.resolveModuleNames && {
|
|
94
|
+
resolveModuleNames: (...args) => this.record(args[1], host.resolveModuleNames(...args)),
|
|
95
|
+
}),
|
|
96
|
+
...(host.resolveTypeReferenceDirectives && {
|
|
97
|
+
resolveTypeReferenceDirectives: (...args) => this.record(args[1], host.resolveTypeReferenceDirectives(...args)),
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Whether a resolution reached `fileName` as an external-library import. */
|
|
103
|
+
has(fileName) {
|
|
104
|
+
return this.files.has(normalised(fileName));
|
|
105
|
+
}
|
|
106
|
+
/** Record the external answers to `containingFile`'s imports. */
|
|
107
|
+
record(containingFile, answers) {
|
|
108
|
+
const fromLibrary = this.has(containingFile);
|
|
109
|
+
for (const answer of answers) {
|
|
110
|
+
if (answer?.resolvedFileName && (fromLibrary || answer.isExternalLibraryImport)) {
|
|
111
|
+
this.files.add(normalised(answer.resolvedFileName));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return answers;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function normalised(fileName) {
|
|
118
|
+
return path.resolve(fileName).replace(/\\/g, '/');
|
|
119
|
+
}
|
|
120
|
+
const externalImportsByProject = new WeakMap();
|
|
121
|
+
/** Keep `imports` as the record of `project`, which resolves through it. */
|
|
122
|
+
export function registerExternalImports(project, imports) {
|
|
123
|
+
externalImportsByProject.set(project, imports);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The record of a project the loader built with a recording resolver, or
|
|
127
|
+
* `undefined` for any other project, which reads the compiler's flag alone.
|
|
128
|
+
*/
|
|
129
|
+
export function externalImportsOf(project) {
|
|
130
|
+
return externalImportsByProject.get(project);
|
|
58
131
|
}
|
|
59
132
|
const checkoutRoots = new Map();
|
|
60
133
|
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
@@ -19,6 +19,12 @@ export interface ProjectLoaderOptions {
|
|
|
19
19
|
repoRoot: string;
|
|
20
20
|
/** Optional path to tsconfig.json (relative to repo root or absolute) */
|
|
21
21
|
tsconfigPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The scanned repo's root, the upper bound of the search for a tsconfig
|
|
24
|
+
* above `repoRoot` when none is named (carrick#1776). Without it only
|
|
25
|
+
* `repoRoot` is searched.
|
|
26
|
+
*/
|
|
27
|
+
scanRoot?: string;
|
|
22
28
|
/** Optional tsconfig snapshot (closed/merged) - preferred over tsconfigPath */
|
|
23
29
|
tsconfigSnapshot?: TsconfigSnapshot;
|
|
24
30
|
/** Optional pinned dependencies for this repo */
|
|
@@ -69,6 +75,7 @@ export declare class ProjectLoader {
|
|
|
69
75
|
private readonly ownerProjects;
|
|
70
76
|
private readonly repoRoot;
|
|
71
77
|
private readonly tsconfigPath;
|
|
78
|
+
private readonly scanRoot;
|
|
72
79
|
private readonly tsconfigSnapshot;
|
|
73
80
|
private readonly pinnedDependencies;
|
|
74
81
|
private initialized;
|
|
@@ -140,9 +147,10 @@ export declare class ProjectLoader {
|
|
|
140
147
|
*/
|
|
141
148
|
getPinnedDependencies(): PinnedDependencySnapshot | undefined;
|
|
142
149
|
/**
|
|
143
|
-
*
|
|
150
|
+
* The tsconfig to build from: the named one when it exists, else the one
|
|
151
|
+
* `findServiceTsconfig` finds for the service, which capture also reads.
|
|
144
152
|
*
|
|
145
|
-
* @returns Absolute path to tsconfig
|
|
153
|
+
* @returns Absolute path to the tsconfig, or undefined if not found
|
|
146
154
|
*/
|
|
147
155
|
private findTsConfig;
|
|
148
156
|
/**
|