@cplieger/deadset-ts 8.0.0-dev.7 → 8.0.0-dev.9
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/dist/bin/node-host.js +21 -3
- package/dist/src/analysis.js +94 -299
- package/dist/src/config.js +6 -83
- package/dist/src/configuration-files.js +2 -9
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/consumers.js +3 -30
- package/dist/src/contract/config.generated.js +544 -0
- package/dist/src/contract/contract.generated.js +20 -10
- package/dist/src/contract/report.generated.js +3 -2
- package/dist/src/conventions.js +3 -8
- package/dist/src/edges.js +36 -112
- package/dist/src/exempt-classes.js +0 -2
- package/dist/src/exempt.js +1 -1
- package/dist/src/failure.js +36 -0
- package/dist/src/findings/completion.js +5 -4
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/intra-function.js +1 -1
- package/dist/src/findings/non-code-artifacts.js +2 -2
- package/dist/src/findings/unused-declarations.js +1 -1
- package/dist/src/findings/visibility-narrowing.js +2 -2
- package/dist/src/findings-pass.js +1 -1
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +7 -0
- package/dist/src/injection-container.js +1 -1
- package/dist/src/json-document.js +194 -67
- package/dist/src/kinds.js +0 -7
- package/dist/src/load/classify.js +236 -0
- package/dist/src/load/diagnostics.js +79 -0
- package/dist/src/load/discover.js +230 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +22 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +155 -0
- package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
- package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
- package/dist/src/{workspace.js → load/workspace.js} +21 -17
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/print.js +10 -79
- package/dist/src/report-schema.js +3 -224
- package/dist/src/resolve.js +245 -715
- package/dist/src/run.js +59 -72
- package/dist/src/schema.js +320 -344
- package/dist/src/scope.js +37 -113
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/session.js +22 -28
- package/dist/src/source-files.js +1 -1
- package/dist/src/suppress-file.js +15 -17
- package/dist/src/suppress.js +3 -3
- package/dist/src/template-field.js +4 -4
- package/dist/src/type-errors.js +11 -216
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +100 -122
- package/dist/src/verbs/describe.js +5 -12
- package/dist/src/verbs/explain.js +3 -3
- package/dist/src/verbs/print-projects.js +2 -2
- package/dist/src/verbs/version.js +12 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +95 -460
- package/src/config.ts +31 -298
- package/src/configuration-files.ts +8 -10
- package/src/configured-declarations.ts +18 -23
- package/src/consumers.ts +6 -57
- package/src/contract/config.generated.ts +798 -0
- package/src/contract/contract.generated.ts +20 -10
- package/src/contract/report.generated.ts +3 -2
- package/src/conventions.ts +6 -12
- package/src/edges.ts +39 -124
- package/src/exempt-classes.ts +0 -5
- package/src/exempt.ts +4 -4
- package/src/failure.ts +63 -0
- package/src/findings/completion.ts +5 -6
- package/src/findings/emitter.ts +2 -2
- package/src/findings/emitters.ts +1 -1
- package/src/findings/intra-function.ts +1 -1
- package/src/findings/non-code-artifacts.ts +4 -4
- package/src/findings/unused-declarations.ts +1 -1
- package/src/findings/visibility-narrowing.ts +2 -2
- package/src/findings-pass.ts +3 -3
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +8 -0
- package/src/host.ts +12 -5
- package/src/injection-container.ts +1 -1
- package/src/json-document.ts +236 -76
- package/src/kinds.ts +0 -8
- package/src/load/classify.ts +352 -0
- package/src/load/diagnostics.ts +141 -0
- package/src/load/discover.ts +378 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +188 -0
- package/src/load/load.ts +61 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +258 -0
- package/src/{test-globals.ts → load/test-globals.ts} +59 -65
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
- package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
- package/src/{workspace.ts → load/workspace.ts} +28 -21
- package/src/outside-tests.ts +1 -1
- package/src/partial.ts +1 -1
- package/src/print.ts +11 -92
- package/src/report-schema.ts +3 -299
- package/src/resolve.ts +281 -1105
- package/src/run.ts +68 -80
- package/src/schema.ts +391 -389
- package/src/scope.ts +45 -131
- package/src/serialization-contract.ts +1 -1
- package/src/session.ts +52 -53
- package/src/source-files.ts +1 -1
- package/src/suppress-file.ts +15 -20
- package/src/suppress.ts +3 -3
- package/src/template-field.ts +10 -7
- package/src/type-errors.ts +18 -288
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +139 -157
- package/src/verbs/describe.ts +4 -11
- package/src/verbs/explain.ts +3 -7
- package/src/verbs/print-projects.ts +2 -2
- package/src/verbs/verb.ts +2 -2
- package/src/verbs/version.ts +13 -2
- package/dist/src/base-url.js +0 -18
- package/dist/src/discover.js +0 -374
- package/dist/src/setup-failure.js +0 -19
- package/dist/src/test-joins.js +0 -101
- package/src/base-url.ts +0 -24
- package/src/discover.ts +0 -548
- package/src/setup-failure.ts +0 -37
- package/src/test-joins.ts +0 -118
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { LANGUAGE_ROWS } from "../contract/contract.generated.js";
|
|
2
|
+
import { filesBelow } from "../fs/tree.js";
|
|
3
|
+
import { packageScope } from "../inventory.js";
|
|
4
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "../paths.js";
|
|
5
|
+
import { LANGUAGE, message } from "../product.js";
|
|
6
|
+
import { globExpression } from "../text/glob.js";
|
|
7
|
+
import { failureExit } from "../failure.js";
|
|
8
|
+
/** The names of the files a program of the language is read from. */
|
|
9
|
+
export const MARKERS = LANGUAGE_ROWS[LANGUAGE].markers;
|
|
10
|
+
const MARKER_EXPRESSIONS = MARKERS.map(globExpression);
|
|
11
|
+
/** Whether one file name is a compiler configuration's: one of the language row's markers. */
|
|
12
|
+
export function isConfigFile(name) {
|
|
13
|
+
return MARKER_EXPRESSIONS.some((marker) => marker.test(name));
|
|
14
|
+
}
|
|
15
|
+
/** Whether the run analyzes one configuration as the target's. */
|
|
16
|
+
export function isTargetConfiguration(configuration) {
|
|
17
|
+
return configuration.origin !== "consumer";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The first line of one error about the target, every mention of the target root spelled
|
|
21
|
+
* relative to it: a path below the root loses the root and its separator, and the root
|
|
22
|
+
* itself is `.`.
|
|
23
|
+
*/
|
|
24
|
+
export function targetRelative(text, targetRoot) {
|
|
25
|
+
const root = targetRoot.endsWith("/") ? targetRoot.slice(0, -1) : targetRoot;
|
|
26
|
+
const [first = ""] = text.split(/\r?\n/u);
|
|
27
|
+
const spelled = first.split(`${root}/`).join("").split(root).join(".").trim();
|
|
28
|
+
return spelled === "" ? "the configuration could not be built" : spelled;
|
|
29
|
+
}
|
|
30
|
+
/** The configuration files below `root`, by absolute path in ascending order. */
|
|
31
|
+
function configFilesUnder(host, root) {
|
|
32
|
+
return filesBelow(host, root, isConfigFile).map((path) => joinPath(root, path));
|
|
33
|
+
}
|
|
34
|
+
/** The configuration files one parsed configuration's `references` name. */
|
|
35
|
+
function referencesOf(parsed) {
|
|
36
|
+
// The parse answers a directory reference as the directory; the compiler reads a
|
|
37
|
+
// path not ending `.json` as its `tsconfig.json`:
|
|
38
|
+
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/core/projectreference.go#L18
|
|
39
|
+
return (parsed.projectReferences ?? []).map(({ path }) => path.endsWith(".json") ? path : joinPath(path, "tsconfig.json"));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
43
|
+
* passed. A snapshot that opens two configurations whose `extends` chains reach one such
|
|
44
|
+
* cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
|
|
45
|
+
* so the analysis refuses one before any snapshot is asked to open it.
|
|
46
|
+
*/
|
|
47
|
+
const CIRCULAR_EXTENDS = 18000;
|
|
48
|
+
/** One configuration file's parse, or why it cannot be read. */
|
|
49
|
+
function readConfiguration(engine, configFile) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = engine.parseConfigFile(configFile);
|
|
52
|
+
const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
|
|
53
|
+
// The compiler leaves the message's `{0}` unfilled:
|
|
54
|
+
// https://github.com/microsoft/TypeScript/pull/64755
|
|
55
|
+
return circular === undefined
|
|
56
|
+
? { parsed }
|
|
57
|
+
: { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { reason: message(error) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every configuration file reached from `pending`, each once, each followed by the
|
|
65
|
+
* configurations it references that `follows` admits, transitively. `unreadable` is told
|
|
66
|
+
* each configuration the compiler cannot read, which the walk then goes on without.
|
|
67
|
+
*/
|
|
68
|
+
function derivedFrom(engine, host, pending, follows, unreadable) {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const found = [];
|
|
71
|
+
for (let configFile = pending.shift(); configFile !== undefined; configFile = pending.shift()) {
|
|
72
|
+
const path = resolvePath(host.workingDirectory(), configFile);
|
|
73
|
+
if (seen.has(path)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
seen.add(path);
|
|
77
|
+
const read = readConfiguration(engine, path);
|
|
78
|
+
if ("reason" in read) {
|
|
79
|
+
unreadable(path, read.reason);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
found.push({ configFile: path, parsed: read.parsed });
|
|
83
|
+
pending.push(...referencesOf(read.parsed).filter(follows));
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
/** The name one module publishes itself under: the scope's, else its manifest's. */
|
|
88
|
+
export function moduleIdentity(host, module) {
|
|
89
|
+
return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The compiler configurations one consumer is loaded through: every one under its root and
|
|
93
|
+
* each one those reference, transitively. A consumer that is absent, no directory, holds no
|
|
94
|
+
* configuration or holds one the compiler cannot read ends the run naming it, because a
|
|
95
|
+
* finding computed without a declared consumer would claim a reference set the run never read.
|
|
96
|
+
*/
|
|
97
|
+
function consumerProjects(engine, host, root) {
|
|
98
|
+
const kind = host.kindOf(root);
|
|
99
|
+
if (kind === "absent") {
|
|
100
|
+
throw failureExit("missing-consumer", `the scope declares the consumer ${root}, and nothing is at that path: check the consumer out at ${root} and install its dependencies`);
|
|
101
|
+
}
|
|
102
|
+
if (kind !== "directory") {
|
|
103
|
+
throw failureExit("missing-consumer", `the scope declares the consumer ${root}, which is not a directory: check the consumer out at ${root}`);
|
|
104
|
+
}
|
|
105
|
+
const found = derivedFrom(engine, host, [...configFilesUnder(host, root)], () => true, (configFile, reason) => {
|
|
106
|
+
throw failureExit("missing-consumer", `the consumer ${root} does not load, ${configFile} cannot be read as a compiler configuration: ${reason}`);
|
|
107
|
+
});
|
|
108
|
+
if (found.length === 0) {
|
|
109
|
+
throw failureExit("missing-consumer", `the consumer ${root} holds no compiler configuration, so none of its references can be read: check the consumer out at ${root} with its ${MARKERS.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
/** A target configuration a matrix or the invocation names that cannot be built, ending the run. */
|
|
114
|
+
function unbuilt(named, reason) {
|
|
115
|
+
throw failureExit("no-program", `${named} ${reason}: correct the configuration, or name the configurations to analyze`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The projects a build matrix declares, in its order, each entry a file below the target
|
|
119
|
+
* root: an entry naming an absent file, a directory or a file the compiler cannot read ends
|
|
120
|
+
* the run naming it, and one naming an earlier entry's file adds nothing. A reference below
|
|
121
|
+
* the root the matrix does not name ends the run naming both, because the compiler builds a
|
|
122
|
+
* referenced project's sources into the referencing program without reporting their
|
|
123
|
+
* diagnostics. A reference outside the root is an outside project.
|
|
124
|
+
*/
|
|
125
|
+
function declaredProjects(engine, host, targetRoot, entries) {
|
|
126
|
+
const projects = [];
|
|
127
|
+
const references = new Map();
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const configFile = joinPath(targetRoot, entry.project);
|
|
130
|
+
const named = `the project ${JSON.stringify(entry.id)} (${configFile})`;
|
|
131
|
+
const kind = host.kindOf(configFile);
|
|
132
|
+
if (kind !== "file") {
|
|
133
|
+
unbuilt(named, kind === "absent"
|
|
134
|
+
? "does not exist"
|
|
135
|
+
: "is a directory, and a project names its compiler configuration file");
|
|
136
|
+
}
|
|
137
|
+
if (references.has(configFile)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const read = readConfiguration(engine, configFile);
|
|
141
|
+
if ("reason" in read) {
|
|
142
|
+
unbuilt(named, `cannot be read as a compiler configuration: ${read.reason}`);
|
|
143
|
+
}
|
|
144
|
+
references.set(configFile, referencesOf(read.parsed));
|
|
145
|
+
projects.push({ id: entry.id, configFile, origin: "declared", parsed: read.parsed });
|
|
146
|
+
}
|
|
147
|
+
for (const project of projects) {
|
|
148
|
+
const unnamed = references
|
|
149
|
+
.get(project.configFile)
|
|
150
|
+
?.find((path) => !references.has(path) && relativePath(targetRoot, path) !== undefined);
|
|
151
|
+
if (unnamed !== undefined) {
|
|
152
|
+
unbuilt(`the project ${JSON.stringify(project.id)} (${project.configFile})`, `references ${unnamed}, which the build matrix does not name. A referenced project is built into the program that references it, so the matrix names it too`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return projects;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The projects a run derives where the matrix declares none: the configuration the scope
|
|
159
|
+
* names for the target where its path names one, every compiler configuration under the
|
|
160
|
+
* target root outside a skipped directory, then each configuration below the root those
|
|
161
|
+
* reference, transitively, each once. A configuration the scope names ends the run where it
|
|
162
|
+
* cannot be read; one the walk found is dropped and named.
|
|
163
|
+
*/
|
|
164
|
+
function derivedProjects(engine, host, scope) {
|
|
165
|
+
const targetRoot = scope.target.path;
|
|
166
|
+
const fromScope = scope.target.path.endsWith(".json")
|
|
167
|
+
? [resolvePath(host.workingDirectory(), scope.target.path)]
|
|
168
|
+
: [];
|
|
169
|
+
const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
|
|
170
|
+
const rootDir = resolvePath(host.workingDirectory(), fromScope.length > 0 ? dirnamePath(targetRoot) : targetRoot);
|
|
171
|
+
const idOf = (configFile) => relativePath(targetRoot, configFile) ?? configFile;
|
|
172
|
+
const notBuilt = [];
|
|
173
|
+
const found = derivedFrom(engine, host, [...fromScope, ...configFilesUnder(host, targetRoot)], (reference) => relativePath(rootDir, reference) !== undefined, (configFile, reason) => {
|
|
174
|
+
if (fromScope.includes(configFile)) {
|
|
175
|
+
unbuilt(configFile, `cannot be read as a compiler configuration: ${reason}`);
|
|
176
|
+
}
|
|
177
|
+
notBuilt.push({
|
|
178
|
+
id: idOf(configFile),
|
|
179
|
+
configFile,
|
|
180
|
+
error: targetRelative(reason, absoluteRoot),
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
configurations: found.map(({ configFile, parsed }) => ({
|
|
185
|
+
id: idOf(configFile),
|
|
186
|
+
configFile,
|
|
187
|
+
origin: fromScope.includes(configFile) ? "scope" : "derived",
|
|
188
|
+
parsed,
|
|
189
|
+
})),
|
|
190
|
+
notBuilt,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Every configuration one run opens: the build matrix's project entries in its order, or,
|
|
195
|
+
* where it holds none, those derived from the scope and the target tree, then each declared
|
|
196
|
+
* consumer's, a configuration the target or an earlier consumer opens being theirs alone. A
|
|
197
|
+
* target holding none ends the run with the setup failure `no-program`. No build output is
|
|
198
|
+
* read: a project is the files its configuration names.
|
|
199
|
+
*/
|
|
200
|
+
export function discoverProjects(engine, host, scope, configurations = []) {
|
|
201
|
+
const declared = configurations.filter((entry) => "project" in entry);
|
|
202
|
+
const target = declared.length > 0
|
|
203
|
+
? {
|
|
204
|
+
mode: "declared",
|
|
205
|
+
configurations: declaredProjects(engine, host, scope.target.path, declared),
|
|
206
|
+
notBuilt: [],
|
|
207
|
+
}
|
|
208
|
+
: { mode: "derived", ...derivedProjects(engine, host, scope) };
|
|
209
|
+
if (target.configurations.length === 0 && target.notBuilt.length === 0) {
|
|
210
|
+
throw failureExit("no-program", `${scope.target.path} holds no compiler configuration, no file named ${MARKERS.join(", ")}: run from the directory that holds one, or name the languages to analyze`);
|
|
211
|
+
}
|
|
212
|
+
const opened = new Set(target.configurations.map((one) => one.configFile));
|
|
213
|
+
const consumers = [];
|
|
214
|
+
const consumed = [];
|
|
215
|
+
for (const module of scope.consumers) {
|
|
216
|
+
const read = consumerProjects(engine, host, module.path);
|
|
217
|
+
const consumer = { id: moduleIdentity(host, module), path: module.path };
|
|
218
|
+
consumers.push(consumer);
|
|
219
|
+
for (const { configFile, parsed } of read.filter((one) => !opened.has(one.configFile))) {
|
|
220
|
+
opened.add(configFile);
|
|
221
|
+
consumed.push({ id: configFile, configFile, origin: "consumer", parsed, consumer });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
mode: target.mode,
|
|
226
|
+
configurations: [...target.configurations, ...consumed],
|
|
227
|
+
notBuilt: target.notBuilt,
|
|
228
|
+
consumers,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* manifest or a document names is read back to its source through this one rule,
|
|
5
5
|
* whether the path is the target's own or a workspace package's.
|
|
6
6
|
*/
|
|
7
|
-
import { dirnamePath, joinPath, relativePath } from "
|
|
7
|
+
import { dirnamePath, joinPath, relativePath } from "../paths.js";
|
|
8
8
|
/** The source extensions the compiler emits each output extension from. */
|
|
9
9
|
const EMITTED = [
|
|
10
10
|
{ emitted: ".d.ts", sources: [".ts", ".tsx"] },
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/ast";
|
|
2
|
+
import { packageOfSpecifier } from "../dependencies.js";
|
|
3
|
+
import { MANIFEST } from "../fs/manifest.js";
|
|
4
|
+
import { entriesOf } from "../fs/tree.js";
|
|
5
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "../paths.js";
|
|
6
|
+
import { dependencySpecifiers, installedPackage, manifestsAbove } from "./module-lookup.js";
|
|
7
|
+
/** The string literal holding one offset, innermost first, a doc comment's included. */
|
|
8
|
+
function literalAt(file, offset) {
|
|
9
|
+
let found;
|
|
10
|
+
const visit = (node) => {
|
|
11
|
+
if (offset < node.pos || offset >= node.end) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
|
|
15
|
+
found = node.text;
|
|
16
|
+
}
|
|
17
|
+
// `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
|
|
18
|
+
for (const doc of node.jsDoc ?? []) {
|
|
19
|
+
visit(doc);
|
|
20
|
+
}
|
|
21
|
+
node.forEachChild(visit);
|
|
22
|
+
};
|
|
23
|
+
file.forEachChild(visit);
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
|
|
28
|
+
* is the specifier, or `undefined` where the key does not match it.
|
|
29
|
+
*/
|
|
30
|
+
function mappedBy(key, specifier) {
|
|
31
|
+
const star = key.indexOf("*");
|
|
32
|
+
if (star < 0) {
|
|
33
|
+
return key === specifier ? "" : undefined;
|
|
34
|
+
}
|
|
35
|
+
const prefix = key.slice(0, star);
|
|
36
|
+
const suffix = key.slice(star + 1);
|
|
37
|
+
return specifier.length >= prefix.length + suffix.length &&
|
|
38
|
+
specifier.startsWith(prefix) &&
|
|
39
|
+
specifier.endsWith(suffix)
|
|
40
|
+
? specifier.slice(prefix.length, specifier.length - suffix.length)
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
|
|
45
|
+
* https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
|
|
46
|
+
*/
|
|
47
|
+
function substituted(target, star) {
|
|
48
|
+
const at = target.indexOf("*");
|
|
49
|
+
return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether a path names something on disk, as written or with any extension; under
|
|
53
|
+
* `fileOnly`, only a file counts.
|
|
54
|
+
*/
|
|
55
|
+
function provided(host, path, fileOnly) {
|
|
56
|
+
const counts = (at) => fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
|
|
57
|
+
if (counts(path)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const dir = dirnamePath(path);
|
|
61
|
+
const stem = `${path.slice(dir.length + 1)}.`;
|
|
62
|
+
return entriesOf(host, dir).some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
|
|
63
|
+
}
|
|
64
|
+
/** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
|
|
65
|
+
function declaringManifest(host, dir, targetRoot, name) {
|
|
66
|
+
for (const { dir: at, manifest } of manifestsAbove(host, dir, targetRoot)) {
|
|
67
|
+
if (dependencySpecifiers(manifest, name).length > 0) {
|
|
68
|
+
return joinPath(at, MANIFEST);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Why one import that the compiler cannot resolve names a module the project expects,
|
|
75
|
+
* with the fix, or undefined where the module is one the project does not expect.
|
|
76
|
+
*/
|
|
77
|
+
function expectedModule(context, importer, specifier) {
|
|
78
|
+
const { host, targetRoot } = context;
|
|
79
|
+
const fix = "run the generator or the build that writes it";
|
|
80
|
+
if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
|
|
81
|
+
return provided(host, resolvePath(dirnamePath(importer), specifier), false)
|
|
82
|
+
? undefined
|
|
83
|
+
: `which names no file of the target: ${fix}`;
|
|
84
|
+
}
|
|
85
|
+
if (specifier.startsWith("#")) {
|
|
86
|
+
return `which the manifest's imports map to no file: ${fix}`;
|
|
87
|
+
}
|
|
88
|
+
const mapped = context.paths.entries.flatMap(({ key, targets }) => {
|
|
89
|
+
const star = mappedBy(key, specifier);
|
|
90
|
+
return star === undefined
|
|
91
|
+
? []
|
|
92
|
+
: targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
|
|
93
|
+
});
|
|
94
|
+
const name = packageOfSpecifier(specifier);
|
|
95
|
+
// The compiler looks up in `node_modules` what `paths` does not resolve, so the error
|
|
96
|
+
// of an installed package's import is the import's own, a subpath its exports omit:
|
|
97
|
+
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
|
|
98
|
+
if (name !== undefined && installedPackage(host, dirnamePath(importer), name) !== undefined) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (mapped.length > 0) {
|
|
102
|
+
return mapped.some((path) => provided(host, path, true))
|
|
103
|
+
? undefined
|
|
104
|
+
: `which the compiler configuration's paths map to no file: ${fix}`;
|
|
105
|
+
}
|
|
106
|
+
const manifest = name === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: declaringManifest(host, dirnamePath(importer), targetRoot, name);
|
|
109
|
+
// No run requires a nested package that is no workspace member to be installed.
|
|
110
|
+
if (manifest === undefined ||
|
|
111
|
+
(dirnamePath(manifest) !== targetRoot && !context.member(manifest))) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const where = relativePath(targetRoot, manifest) ?? manifest;
|
|
115
|
+
return `a dependency ${where} declares that is not installed: run the package manager's install`;
|
|
116
|
+
}
|
|
117
|
+
/** The `paths` mapping of the configuration at `configFile` whose parse answered `options`. */
|
|
118
|
+
export function pathsOf(configFile, options) {
|
|
119
|
+
// The parse answers `pathsBasePath`, the directory of the configuration that writes
|
|
120
|
+
// `paths` (an extended one included), though the client's option type omits it:
|
|
121
|
+
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/tsoptions/tsconfigparsing.go#L1120
|
|
122
|
+
const declaredAt = options.pathsBasePath;
|
|
123
|
+
return {
|
|
124
|
+
base: typeof declaredAt === "string" ? declaredAt : dirnamePath(configFile),
|
|
125
|
+
entries: Object.entries(options.paths ?? {}).map(([key, targets]) => ({ key, targets })),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Why one import the compiler cannot resolve, at `offset` of `file`, names a module the project
|
|
130
|
+
* expects and nothing provides, with the fix; undefined where the project does not expect it.
|
|
131
|
+
*/
|
|
132
|
+
export function missingModule(context, file, offset) {
|
|
133
|
+
const specifier = literalAt(file, offset);
|
|
134
|
+
const reason = specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
|
|
135
|
+
return specifier === undefined || reason === undefined ? undefined : { specifier, reason };
|
|
136
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolvePath } from "../paths.js";
|
|
2
|
+
import { openSession } from "../session.js";
|
|
3
|
+
import { classify } from "./classify.js";
|
|
4
|
+
import { planRun, programBuilds } from "./plan.js";
|
|
5
|
+
/**
|
|
6
|
+
* Plans the run, opens every configuration in one snapshot of one client, classifies every
|
|
7
|
+
* project, and hands the analyzed and the consumer projects to `read` while the client answers.
|
|
8
|
+
* A run that cannot be analyzed ends with one {@link FailureExit} before `read` is called.
|
|
9
|
+
*/
|
|
10
|
+
export function loadRun(engine, host, scope, config, read) {
|
|
11
|
+
const plan = planRun(engine, host, scope, config);
|
|
12
|
+
return openSession(engine, plan.plans.map((one) => one.configFile), { programs: programBuilds(host, plan.plans) }, (session, unanswered) => {
|
|
13
|
+
const classified = classify(host, plan, new Map(session.views.map((view) => [view.configFile, view])));
|
|
14
|
+
const runFiles = new Set(session.views.flatMap((view) => view.ownSourceFiles().map((file) => resolvePath(host.workingDirectory(), file.fileName))));
|
|
15
|
+
return {
|
|
16
|
+
plan,
|
|
17
|
+
classified,
|
|
18
|
+
result: read({ plan, classified, runFiles }),
|
|
19
|
+
unanswered: unanswered(),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a package an import names is installed and which manifests above an importer declare
|
|
3
|
+
* it: the leaf both the expected-module reading and the workspace resolution read.
|
|
4
|
+
*/
|
|
5
|
+
import { DEPENDENCY_SECTIONS, INSTALLED_PACKAGES, manifestAt } from "../fs/manifest.js";
|
|
6
|
+
import { isRecord } from "../json-document.js";
|
|
7
|
+
import { ancestors, joinPath } from "../paths.js";
|
|
8
|
+
/**
|
|
9
|
+
* The directory `node_modules/<name>` holds for an importer in `dir`: the nearest one at or
|
|
10
|
+
* above `dir`, above any target root too, that is a directory, a link to one included, as the
|
|
11
|
+
* compiler looks a package up. Undefined where no ancestor installs it.
|
|
12
|
+
*/
|
|
13
|
+
export function installedPackage(host, dir, name) {
|
|
14
|
+
return ancestors(dir)
|
|
15
|
+
.map((at) => joinPath(at, INSTALLED_PACKAGES, name))
|
|
16
|
+
.find((candidate) => host.kindOf(candidate) === "directory");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The manifests at or above `dir` and no higher than `top`, nearest first; a directory outside
|
|
20
|
+
* `top` reads its own manifest alone.
|
|
21
|
+
*/
|
|
22
|
+
export function* manifestsAbove(host, dir, top) {
|
|
23
|
+
const chain = ancestors(dir, top);
|
|
24
|
+
for (const at of chain.length > 0 ? chain : [dir]) {
|
|
25
|
+
const manifest = manifestAt(host, at);
|
|
26
|
+
if (manifest !== undefined) {
|
|
27
|
+
yield { dir: at, manifest };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The specifiers one manifest depends on `name` with, one per dependency section naming it. */
|
|
32
|
+
export function dependencySpecifiers(manifest, name) {
|
|
33
|
+
return DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
34
|
+
const declared = manifest[section];
|
|
35
|
+
return isRecord(declared) && Object.hasOwn(declared, name) ? [declared[name]] : [];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readConventions } from "../conventions.js";
|
|
2
|
+
import { holdsManifest, inInstalledPackages, MANIFEST } from "../fs/manifest.js";
|
|
3
|
+
import { packageEntries, readManifest } from "../manifest.js";
|
|
4
|
+
import { ancestors, dirnamePath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
5
|
+
import { bytewise } from "../text/compare.js";
|
|
6
|
+
import { globExpression } from "../text/glob.js";
|
|
7
|
+
import { discoverProjects, isTargetConfiguration, } from "./discover.js";
|
|
8
|
+
import { FailureExit } from "../failure.js";
|
|
9
|
+
import { joinedTestFiles, joinedTests } from "./test-joins.js";
|
|
10
|
+
import { testGlobals } from "./test-globals.js";
|
|
11
|
+
import { findWorkspace } from "./workspace.js";
|
|
12
|
+
import { writtenConfiguration } from "./workspace-programs.js";
|
|
13
|
+
import { workspaceRun } from "./workspace-run.js";
|
|
14
|
+
/**
|
|
15
|
+
* The `paths` one configuration's resolved options map under the compilers before version
|
|
16
|
+
* 7, each target absolute: a target is read against the effective `baseUrl`, and a specifier
|
|
17
|
+
* no other pattern maps is looked up below it. Undefined where no `baseUrl` is set, so the
|
|
18
|
+
* options' own `paths` stand. Version 7 reads no `baseUrl`:
|
|
19
|
+
* https://github.com/microsoft/TypeScript/issues/62207
|
|
20
|
+
*/
|
|
21
|
+
export function baseUrlPaths(options) {
|
|
22
|
+
const baseUrl = Reflect.get(options, "baseUrl");
|
|
23
|
+
if (typeof baseUrl !== "string") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return Object.fromEntries(Object.entries({ "*": ["*"], ...options.paths }).map(([key, targets]) => [
|
|
27
|
+
key,
|
|
28
|
+
targets.map((target) => resolvePath(baseUrl, target)),
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
31
|
+
/** One configuration, read through the `paths` its `baseUrl` gives. */
|
|
32
|
+
function planned(configuration) {
|
|
33
|
+
const { options } = configuration.parsed;
|
|
34
|
+
const paths = baseUrlPaths(options);
|
|
35
|
+
return {
|
|
36
|
+
...configuration,
|
|
37
|
+
options: paths === undefined ? options : { ...options, paths },
|
|
38
|
+
build: paths === undefined
|
|
39
|
+
? { kind: "project" }
|
|
40
|
+
: {
|
|
41
|
+
kind: "configuration",
|
|
42
|
+
value: { extends: configuration.configFile, compilerOptions: { paths } },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The nested packages of a run: each directory below the target root, in no `node_modules`
|
|
48
|
+
* directory and none of `known`, that holds a `package.json` and lies at or above one of
|
|
49
|
+
* `files`, in path order.
|
|
50
|
+
*/
|
|
51
|
+
function nestedPackageDirs(host, targetRoot, files, known) {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const found = [];
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
for (const dir of ancestors(dirnamePath(file), targetRoot)) {
|
|
56
|
+
if (seen.has(dir) || relativePath(targetRoot, dir) === undefined) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
seen.add(dir);
|
|
60
|
+
const below = relativePath(targetRoot, dir) ?? "";
|
|
61
|
+
if (!inInstalledPackages(below) && !known.has(dir) && holdsManifest(host, dir)) {
|
|
62
|
+
found.push(dir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return found.sort(bytewise);
|
|
67
|
+
}
|
|
68
|
+
/** The manifests conventions are applied from: the target's, then each member's and nested package's. */
|
|
69
|
+
function conventionManifests(host, root, packages) {
|
|
70
|
+
const found = new Map();
|
|
71
|
+
if (holdsManifest(host, root)) {
|
|
72
|
+
found.set(root, { dir: root, path: MANIFEST });
|
|
73
|
+
}
|
|
74
|
+
for (const [dirs, nested] of [
|
|
75
|
+
[packages.members, false],
|
|
76
|
+
[packages.nested, true],
|
|
77
|
+
]) {
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
const below = relativePath(root, dir);
|
|
80
|
+
if (below !== undefined && !found.has(dir)) {
|
|
81
|
+
found.set(dir, { dir, path: `${below}/${MANIFEST}`, nested });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...found.values()];
|
|
86
|
+
}
|
|
87
|
+
/** The workspace one consumer sits in, read from its own root, the target's reused. */
|
|
88
|
+
function consumerWorkspace(host, consumer, target) {
|
|
89
|
+
try {
|
|
90
|
+
return { workspace: findWorkspace(host, consumer.path, () => undefined, target) };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!(error instanceof FailureExit)) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return { fault: error.failures[0].detail };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The plan of one run: the configurations discovery names, each built through the steps in
|
|
101
|
+
* order (the `baseUrl` paths, the workspace members read from source, the test runner's
|
|
102
|
+
* globals, the joined test files of a derived matrix), the package set and the convention
|
|
103
|
+
* rows applied per package. A nested package is one at or above a file a target
|
|
104
|
+
* configuration lists or a test file that joins it.
|
|
105
|
+
*/
|
|
106
|
+
export function planRun(engine, host, scope, config) {
|
|
107
|
+
const discovery = discoverProjects(engine, host, scope, config.analysis.configurations);
|
|
108
|
+
const root = resolvePath(host.workingDirectory(), scope.target.path);
|
|
109
|
+
const parse = (fileName, text) => engine.parseSourceFile(fileName, text);
|
|
110
|
+
let plans = discovery.configurations.map(planned);
|
|
111
|
+
const targets = plans.filter(isTargetConfiguration);
|
|
112
|
+
const manifest = readManifest(host, scope.target.path);
|
|
113
|
+
const workspace = workspaceRun(engine, host, root, targets, manifest);
|
|
114
|
+
const consumerWorkspaces = new Map(discovery.consumers.map((consumer) => [
|
|
115
|
+
consumer.path,
|
|
116
|
+
consumerWorkspace(host, consumer, workspace?.workspace),
|
|
117
|
+
]));
|
|
118
|
+
plans = plans.map((plan) => {
|
|
119
|
+
const build = workspace?.programs.get(plan.configFile);
|
|
120
|
+
return build === undefined ? plan : { ...plan, build };
|
|
121
|
+
});
|
|
122
|
+
plans = testGlobals(host, parse, plans);
|
|
123
|
+
const joins = discovery.mode === "derived"
|
|
124
|
+
? joinedTestFiles(host, root, targets, config.ts.test_files.map(globExpression))
|
|
125
|
+
: new Map();
|
|
126
|
+
plans = joinedTests(plans, joins);
|
|
127
|
+
const members = workspace?.memberDirs ?? [];
|
|
128
|
+
const nested = nestedPackageDirs(host, normalizePath(root), targets.flatMap((plan) => [
|
|
129
|
+
plan.configFile,
|
|
130
|
+
...plan.parsed.fileNames,
|
|
131
|
+
...(joins.get(plan.configFile) ?? []),
|
|
132
|
+
]), new Set(members));
|
|
133
|
+
const packages = {
|
|
134
|
+
manifest: workspace?.manifest ?? manifest,
|
|
135
|
+
members,
|
|
136
|
+
nested,
|
|
137
|
+
entries: [...(workspace?.entries ?? []), ...packageEntries(host, normalizePath(root), nested)],
|
|
138
|
+
published: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
|
|
139
|
+
};
|
|
140
|
+
const conventions = readConventions(host, parse, conventionManifests(host, root, packages), config.ts.disabled_conventions);
|
|
141
|
+
return { discovery, plans, packages, conventions, workspace, consumerWorkspaces, root };
|
|
142
|
+
}
|
|
143
|
+
/** How the snapshot builds each planned configuration other than by opening its project. */
|
|
144
|
+
export function programBuilds(host, plans) {
|
|
145
|
+
const builds = new Map();
|
|
146
|
+
for (const { configFile, build } of plans) {
|
|
147
|
+
if (build.kind === "program") {
|
|
148
|
+
builds.set(configFile, build);
|
|
149
|
+
}
|
|
150
|
+
else if (build.kind === "configuration") {
|
|
151
|
+
builds.set(configFile, writtenConfiguration(host, configFile, build.value));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return builds;
|
|
155
|
+
}
|