@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.10
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/README.md +1 -1
- package/dist/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/analysis.js +181 -341
- package/dist/src/augmentations.js +106 -13
- package/dist/src/automatic-types.js +10 -29
- package/dist/src/build-scripts.js +425 -0
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +2 -3
- package/dist/src/config.js +6 -82
- package/dist/src/configuration-files.js +28 -176
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +3 -30
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/contract/config.generated.js +558 -0
- package/dist/src/contract/contract.generated.js +1244 -0
- package/dist/src/contract/report.generated.js +862 -0
- package/dist/src/convention-rows.js +73 -3
- package/dist/src/conventions.js +36 -66
- package/dist/src/dependencies.js +103 -40
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +2 -8
- package/dist/src/entry-points.js +11 -27
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +1 -63
- package/dist/src/exempt.js +21 -40
- package/dist/src/failure.js +36 -0
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +139 -72
- package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
- package/dist/src/findings/emitters.js +5 -5
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +13 -10
- package/dist/src/findings/non-code-artifacts.js +7 -8
- package/dist/src/findings/reads-and-writes.js +16 -13
- package/dist/src/findings/self-check.js +10 -12
- package/dist/src/findings/unused-declarations.js +41 -32
- package/dist/src/findings/visibility-narrowing.js +19 -16
- package/dist/src/findings-pass.js +8 -15
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +72 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/generated-file.js +7 -4
- package/dist/src/git-ignore.js +39 -26
- package/dist/src/graph.js +5 -3
- package/dist/src/html-entries.js +6 -33
- package/dist/src/in-operands.js +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/intra-function-parts.js +63 -43
- package/dist/src/inventory.js +2 -21
- package/dist/src/javascript-file.js +33 -0
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +3 -366
- package/dist/src/load/classify.js +270 -0
- package/dist/src/load/diagnostics.js +80 -0
- package/dist/src/load/discover.js +277 -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 +27 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +313 -0
- package/dist/src/load/specifiers.js +115 -0
- package/dist/src/load/synthesize.js +140 -0
- package/dist/src/load/test-globals.js +207 -0
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
- package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
- package/dist/src/{workspace.js → load/workspace.js} +62 -124
- package/dist/src/manifest.js +49 -54
- package/dist/src/matrix.js +76 -4
- package/dist/src/mdx-entries.js +3 -26
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +11 -79
- package/dist/src/product.js +13 -0
- package/dist/src/query.js +1 -0
- package/dist/src/ref.js +1 -0
- package/dist/src/references.js +48 -30
- package/dist/src/report-schema.js +51 -0
- package/dist/src/report.js +57 -41
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +16 -10
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +245 -729
- package/dist/src/roots.js +273 -87
- package/dist/src/run.js +66 -77
- package/dist/src/schema.js +320 -343
- package/dist/src/scope.js +38 -115
- package/dist/src/serialization-contract.js +595 -21
- package/dist/src/session.js +34 -29
- package/dist/src/source-files.js +1 -1
- package/dist/src/suppress-file.js +25 -28
- package/dist/src/suppress-inline.js +3 -2
- package/dist/src/suppress.js +33 -51
- package/dist/src/sweep.js +7 -9
- package/dist/src/symbol-keys.js +3 -5
- package/dist/src/template-field.js +9 -19
- package/dist/src/test-support.js +8 -11
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/named-files.js +54 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/text/yaml.js +393 -0
- package/dist/src/type-errors.js +11 -232
- package/dist/src/unseen-loads.js +100 -0
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +166 -126
- package/dist/src/verbs/describe.js +9 -16
- package/dist/src/verbs/explain.js +10 -7
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +6 -4
- package/dist/src/verbs/print-retained.js +4 -2
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +14 -3
- package/dist/src/workflow-steps.js +62 -64
- package/jsr.json +1 -1
- package/package.json +4 -3
- package/src/analysis.ts +245 -502
- package/src/augmentations.ts +135 -17
- package/src/automatic-types.ts +11 -29
- package/src/build-scripts.ts +543 -0
- package/src/component-layer.ts +4 -2
- package/src/components.ts +2 -4
- package/src/config.ts +31 -302
- package/src/configuration-files.ts +34 -179
- package/src/configured-declarations.ts +18 -23
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +6 -57
- package/src/contextual-uses.ts +64 -12
- package/src/contract/config.generated.ts +818 -0
- package/src/contract/contract.generated.ts +1308 -0
- package/src/contract/report.generated.ts +1237 -0
- package/src/convention-rows.ts +85 -3
- package/src/conventions.ts +46 -84
- package/src/dependencies.ts +118 -46
- package/src/edges.ts +41 -140
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +3 -9
- package/src/entry-points.ts +12 -38
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +1 -75
- package/src/exempt.ts +36 -50
- package/src/failure.ts +63 -0
- package/src/file-facts.ts +9 -24
- package/src/finding.ts +14 -9
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +181 -84
- package/src/findings/dependencies-and-module-machinery.ts +4 -7
- package/src/findings/emitter.ts +8 -9
- package/src/findings/emitters.ts +7 -7
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +13 -11
- package/src/findings/non-code-artifacts.ts +9 -11
- package/src/findings/reads-and-writes.ts +20 -16
- package/src/findings/self-check.ts +11 -17
- package/src/findings/unused-declarations.ts +45 -32
- package/src/findings/visibility-narrowing.ts +20 -17
- package/src/findings-pass.ts +10 -19
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +92 -0
- package/src/fs/tree.ts +110 -0
- package/src/generated-file.ts +7 -4
- package/src/git-ignore.ts +50 -26
- package/src/graph.ts +5 -3
- package/src/host.ts +12 -5
- package/src/html-entries.ts +6 -33
- package/src/in-operands.ts +2 -1
- package/src/index.ts +1 -1
- package/src/injection-container.ts +1 -1
- package/src/intra-function-parts.ts +73 -44
- package/src/inventory.ts +2 -20
- package/src/javascript-file.ts +37 -0
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +5 -413
- package/src/load/classify.ts +397 -0
- package/src/load/diagnostics.ts +144 -0
- package/src/load/discover.ts +455 -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 +73 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +504 -0
- package/src/load/specifiers.ts +168 -0
- package/src/load/synthesize.ts +211 -0
- package/src/load/test-globals.ts +251 -0
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
- package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
- package/src/{workspace.ts → load/workspace.ts} +90 -134
- package/src/manifest.ts +81 -75
- package/src/matrix.ts +122 -4
- package/src/mdx-entries.ts +3 -26
- package/src/outside-tests.ts +1 -1
- package/src/partial.ts +3 -2
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +12 -92
- package/src/product.ts +18 -0
- package/src/query.ts +11 -0
- package/src/ref.ts +1 -0
- package/src/references.ts +52 -31
- package/src/report-schema.ts +68 -0
- package/src/report.ts +101 -224
- package/src/reporters/annotations.ts +22 -11
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -4
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +29 -16
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +283 -1114
- package/src/roots.ts +339 -104
- package/src/run.ts +75 -91
- package/src/schema.ts +391 -388
- package/src/scope.ts +46 -138
- package/src/serialization-contract.ts +763 -22
- package/src/session.ts +70 -54
- package/src/source-files.ts +1 -1
- package/src/suppress-file.ts +24 -33
- package/src/suppress-inline.ts +2 -2
- package/src/suppress.ts +43 -58
- package/src/sweep.ts +23 -9
- package/src/symbol-keys.ts +2 -5
- package/src/template-field.ts +15 -25
- package/src/test-support.ts +17 -13
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/named-files.inputs.json +1522 -0
- package/src/text/named-files.ts +68 -0
- package/src/text/shell.ts +161 -0
- package/src/text/yaml.ts +423 -0
- package/src/type-errors.ts +18 -288
- package/src/unseen-loads.ts +134 -0
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +224 -162
- package/src/verbs/describe.ts +14 -15
- package/src/verbs/explain.ts +10 -11
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +6 -4
- package/src/verbs/print-retained.ts +4 -2
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +2 -8
- package/src/verbs/version.ts +15 -3
- package/src/workflow-steps.ts +74 -64
- package/dist/src/base-url.js +0 -18
- package/dist/src/discover.js +0 -468
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/dist/src/setup-failure.js +0 -23
- package/dist/src/test-joins.js +0 -101
- package/dist/src/version.js +0 -7
- package/src/base-url.ts +0 -24
- package/src/discover.ts +0 -653
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
- package/src/setup-failure.ts +0 -39
- package/src/test-joins.ts +0 -120
- package/src/version.ts +0 -7
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module specifiers of the JavaScript files a configuration does not check, read for what
|
|
3
|
+
* the compiler leaves unresolved. Unchecked JavaScript reports no unfound module, and a
|
|
4
|
+
* JavaScript file may name a file that is absent on purpose, so an unresolved one is no failure
|
|
5
|
+
* of the program: a relative one is noted, and only a configuration whose imported dependencies
|
|
6
|
+
* are none of them installed fails, because its program is not the one the project runs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isBinaryExpression,
|
|
11
|
+
isCallExpression,
|
|
12
|
+
isParenthesizedExpression,
|
|
13
|
+
isStringLiteralLikeNode,
|
|
14
|
+
isTryStatement,
|
|
15
|
+
SyntaxKind,
|
|
16
|
+
type Node,
|
|
17
|
+
type SourceFile,
|
|
18
|
+
type StringLiteralLikeNode,
|
|
19
|
+
} from "@typescript/native/ast";
|
|
20
|
+
import type { Program } from "@typescript/native/sync";
|
|
21
|
+
import { packageOfSpecifier } from "../dependencies.ts";
|
|
22
|
+
import type { Failure } from "../failure.ts";
|
|
23
|
+
import { readManifestAt } from "../fs/manifest.ts";
|
|
24
|
+
import type { Host } from "../host.ts";
|
|
25
|
+
import { JAVASCRIPT_FILE } from "../javascript-file.ts";
|
|
26
|
+
import { ancestors, dirnamePath, relativePath, resolvePath } from "../paths.ts";
|
|
27
|
+
import { renderPosition, type Position } from "../position.ts";
|
|
28
|
+
import { batched, type Engine, type ProjectView } from "../session.ts";
|
|
29
|
+
import { dependencySpecifiers } from "./module-lookup.ts";
|
|
30
|
+
|
|
31
|
+
/** A relative specifier: `.`, `..`, or one starting `./` or `../`. */
|
|
32
|
+
const RELATIVE = /^\.\.?(?:\/|$)/u;
|
|
33
|
+
|
|
34
|
+
/** One relative specifier of a JavaScript file that resolves to no file. */
|
|
35
|
+
export interface UnresolvedSpecifier {
|
|
36
|
+
readonly specifier: string;
|
|
37
|
+
readonly position: Position;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the specifiers of one project's unchecked JavaScript files say. */
|
|
41
|
+
interface SpecifierReading {
|
|
42
|
+
readonly unresolved: readonly UnresolvedSpecifier[];
|
|
43
|
+
/** The failure of a project whose imported dependencies are none of them installed. */
|
|
44
|
+
readonly uninstalled: Failure | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether one specifier is loaded optionally: written inside a `try` block, or as a call whose
|
|
49
|
+
* result an `||` or a `??` falls back from, so the program is written for its absence.
|
|
50
|
+
*/
|
|
51
|
+
function optional(node: StringLiteralLikeNode): boolean {
|
|
52
|
+
const call = node.parent;
|
|
53
|
+
let operand: Node = call;
|
|
54
|
+
let parent = call.parent as Node | undefined;
|
|
55
|
+
while (parent !== undefined && isParenthesizedExpression(parent)) {
|
|
56
|
+
operand = parent;
|
|
57
|
+
parent = parent.parent;
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
isCallExpression(call) &&
|
|
61
|
+
parent !== undefined &&
|
|
62
|
+
isBinaryExpression(parent) &&
|
|
63
|
+
parent.left === operand &&
|
|
64
|
+
(parent.operatorToken.kind === SyntaxKind.BarBarToken ||
|
|
65
|
+
parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken)
|
|
66
|
+
) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
for (let at: Node | undefined = node; at !== undefined; at = at.parent as Node | undefined) {
|
|
70
|
+
const above = at.parent as Node | undefined;
|
|
71
|
+
if (above !== undefined && isTryStatement(above) && above.tryBlock === at) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a manifest at or above `dir`, no higher than `root`, declares the package `name`. A
|
|
80
|
+
* manifest that does not parse or cannot be read ends the search, because what it declares is
|
|
81
|
+
* unanswered and a manifest above it is another package's.
|
|
82
|
+
*/
|
|
83
|
+
function declared(host: Host, dir: string, root: string, name: string): boolean {
|
|
84
|
+
for (const at of ancestors(dir, root)) {
|
|
85
|
+
const read = readManifestAt(host, at);
|
|
86
|
+
if (read.kind === "malformed" || read.kind === "unread") {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (read.kind === "read" && dependencySpecifiers(read.manifest, name).length > 0) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What a project's specifiers are read against. */
|
|
97
|
+
interface SpecifierContext {
|
|
98
|
+
/** Whether the project's options check its JavaScript files, which then report their own. */
|
|
99
|
+
readonly checked: boolean;
|
|
100
|
+
/** The target root, absolute. */
|
|
101
|
+
readonly root: string;
|
|
102
|
+
/** The directories a configuration of the run emits into, whose paths name build output. */
|
|
103
|
+
readonly outputs: readonly string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The specifiers of every JavaScript file of `project`, where its options do not check
|
|
108
|
+
* JavaScript, each asked of the compiler in batched round trips. A relative one naming a path
|
|
109
|
+
* below one of the run's output directories names build output, which a build writes later.
|
|
110
|
+
*/
|
|
111
|
+
export function readSpecifiers<Brand>(
|
|
112
|
+
engine: Engine,
|
|
113
|
+
host: Host,
|
|
114
|
+
project: ProjectView<Brand>,
|
|
115
|
+
{ checked, root, outputs }: SpecifierContext,
|
|
116
|
+
): SpecifierReading {
|
|
117
|
+
if (checked) {
|
|
118
|
+
return { unresolved: [], uninstalled: undefined };
|
|
119
|
+
}
|
|
120
|
+
const usages: { readonly file: SourceFile; readonly node: StringLiteralLikeNode }[] = [];
|
|
121
|
+
for (const file of project.ownSourceFiles()) {
|
|
122
|
+
if (JAVASCRIPT_FILE.test(file.fileName)) {
|
|
123
|
+
for (const node of file.imports) {
|
|
124
|
+
if (isStringLiteralLikeNode(node) && !optional(node)) {
|
|
125
|
+
usages.push({ file, node });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const answers = batched<
|
|
131
|
+
(typeof usages)[number],
|
|
132
|
+
ReturnType<Program["getResolvedModuleFromModuleSpecifier"]>
|
|
133
|
+
>(engine, usages, ({ file, node }) =>
|
|
134
|
+
project.program.getResolvedModuleFromModuleSpecifier.gen(node, file.fileName),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const unresolved: UnresolvedSpecifier[] = [];
|
|
138
|
+
let missing: { readonly file: SourceFile; readonly node: StringLiteralLikeNode } | undefined;
|
|
139
|
+
let installed = false;
|
|
140
|
+
for (const [at, usage] of usages.entries()) {
|
|
141
|
+
const resolved = (answers[at]?.resolvedFileName ?? "") !== "";
|
|
142
|
+
const specifier = usage.node.text;
|
|
143
|
+
const name = packageOfSpecifier(specifier);
|
|
144
|
+
if (RELATIVE.test(specifier)) {
|
|
145
|
+
const named = resolvePath(dirnamePath(usage.file.fileName), specifier);
|
|
146
|
+
if (!resolved && !outputs.some((dir) => relativePath(dir, named) !== undefined)) {
|
|
147
|
+
unresolved.push({
|
|
148
|
+
specifier,
|
|
149
|
+
position: renderPosition(usage.file, root, usage.node.getStart()),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} else if (name !== undefined && declared(host, dirnamePath(usage.file.fileName), root, name)) {
|
|
153
|
+
installed ||= resolved;
|
|
154
|
+
missing ??= resolved ? undefined : usage;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (installed || missing === undefined) {
|
|
158
|
+
return { unresolved, uninstalled: undefined };
|
|
159
|
+
}
|
|
160
|
+
const { path, line } = renderPosition(missing.file, root, missing.node.getStart());
|
|
161
|
+
return {
|
|
162
|
+
unresolved,
|
|
163
|
+
uninstalled: {
|
|
164
|
+
failureClass: "missing-module",
|
|
165
|
+
detail: `${path}:${String(line)}: ${path} imports ${JSON.stringify(missing.node.text)}, a declared dependency, and no dependency the configuration's JavaScript files import is installed: run the package manager's install`,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The programs a run forms for the JavaScript files no compiler configuration lists: one per
|
|
3
|
+
* package, over the files git would track that are no build output, each written as a
|
|
4
|
+
* configuration only the snapshot holds, with the module system of the runtime its package
|
|
5
|
+
* is written for.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CONVENTION_ROWS } from "../convention-rows.ts";
|
|
9
|
+
import type { Conventions } from "../conventions.ts";
|
|
10
|
+
import { holdsManifest, MANIFEST, readManifestAt, type ManifestRead } from "../fs/manifest.ts";
|
|
11
|
+
import { filesBelow } from "../fs/tree.ts";
|
|
12
|
+
import type { Host } from "../host.ts";
|
|
13
|
+
import { JAVASCRIPT_FILE, minified, minifiedByName } from "../javascript-file.ts";
|
|
14
|
+
import { isRecord } from "../json-document.ts";
|
|
15
|
+
import { ancestors, dirnamePath, joinPath, relativePath } from "../paths.ts";
|
|
16
|
+
import type { Engine } from "../session.ts";
|
|
17
|
+
import { bytewise } from "../text/compare.ts";
|
|
18
|
+
import type { Configuration, Origin } from "./discover.ts";
|
|
19
|
+
import { writtenPath } from "./workspace-programs.ts";
|
|
20
|
+
|
|
21
|
+
/** The name a package's program is written beside, made free by {@link writtenPath}. */
|
|
22
|
+
const GROUP_CONFIGURATION = "jsconfig.json";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The options of a formed program before its runtime is chosen: unchecked JavaScript, no
|
|
26
|
+
* emit, no installed package's JavaScript, which is not the target's code and grows the
|
|
27
|
+
* program with the depth of `node_modules`, and every type package visible.
|
|
28
|
+
*/
|
|
29
|
+
const FORMED_OPTIONS = {
|
|
30
|
+
allowJs: true,
|
|
31
|
+
checkJs: false,
|
|
32
|
+
noEmit: true,
|
|
33
|
+
skipLibCheck: true,
|
|
34
|
+
maxNodeModuleJsDepth: 0,
|
|
35
|
+
target: "esnext",
|
|
36
|
+
jsx: "preserve",
|
|
37
|
+
resolveJsonModule: true,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
interface CandidateContext {
|
|
41
|
+
/** The files a configuration of the run lists or a test file joins it with, absolute. */
|
|
42
|
+
readonly covered: ReadonlySet<string>;
|
|
43
|
+
/** The output and declaration directories of every configuration of the run, absolute. */
|
|
44
|
+
readonly outputs: readonly string[];
|
|
45
|
+
/** Whether git ignores one absolute path. */
|
|
46
|
+
readonly ignored: (path: string) => boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether one file is minified build output; one that cannot be read is left to the compiler. */
|
|
50
|
+
function minifiedFile(host: Host, file: string): boolean {
|
|
51
|
+
if (minifiedByName(file)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return minified(file, host.readFile(file));
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The JavaScript files below `root` a program may be formed over: none in a skipped
|
|
63
|
+
* directory, none git ignores, none minified, none `context` covers and none below one of
|
|
64
|
+
* its output directories. A convention row's generated directory leaves out more, once the
|
|
65
|
+
* rows are read: {@link formedGroups}.
|
|
66
|
+
*/
|
|
67
|
+
export function candidateFiles(
|
|
68
|
+
host: Host,
|
|
69
|
+
root: string,
|
|
70
|
+
{ covered, outputs, ignored }: CandidateContext,
|
|
71
|
+
): readonly string[] {
|
|
72
|
+
return filesBelow(host, root, (name) => JAVASCRIPT_FILE.test(name))
|
|
73
|
+
.map((path) => joinPath(root, path))
|
|
74
|
+
.filter(
|
|
75
|
+
(file) =>
|
|
76
|
+
!covered.has(file) &&
|
|
77
|
+
!outputs.some((dir) => relativePath(dir, file) !== undefined) &&
|
|
78
|
+
!ignored(file) &&
|
|
79
|
+
!minifiedFile(host, file),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface Group {
|
|
84
|
+
/** The package's directory, absolute. */
|
|
85
|
+
readonly dir: string;
|
|
86
|
+
readonly manifest: ManifestRead;
|
|
87
|
+
/** Its files, absolute, in ascending order. */
|
|
88
|
+
readonly files: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The candidates below `root` grouped by package, in directory order, less every file below a
|
|
93
|
+
* directory an applied row of `conventions` names as generated: each file belongs to the
|
|
94
|
+
* nearest directory at or above it, up to `root`, that holds a `package.json`, a manifest that
|
|
95
|
+
* does not parse included, and to `root` where none does.
|
|
96
|
+
*/
|
|
97
|
+
export function formedGroups(
|
|
98
|
+
host: Host,
|
|
99
|
+
root: string,
|
|
100
|
+
candidates: readonly string[],
|
|
101
|
+
conventions: Conventions,
|
|
102
|
+
): readonly Group[] {
|
|
103
|
+
const generated = conventions.generated.map(({ dir, below }) => joinPath(dir, below));
|
|
104
|
+
const groups = new Map<string, string[]>();
|
|
105
|
+
for (const file of candidates) {
|
|
106
|
+
if (generated.some((dir) => relativePath(dir, file) !== undefined)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const dir = packageOf(host, root, file);
|
|
110
|
+
groups.set(dir, [...(groups.get(dir) ?? []), file]);
|
|
111
|
+
}
|
|
112
|
+
return [...groups]
|
|
113
|
+
.sort(([a], [b]) => bytewise(a, b))
|
|
114
|
+
.map(([dir, files]) => ({
|
|
115
|
+
dir,
|
|
116
|
+
manifest: readManifestAt(host, dir),
|
|
117
|
+
files: files.sort(bytewise),
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The directory whose `package.json` groups one file: the nearest at or above it, up to `root`. */
|
|
122
|
+
export function packageOf(host: Host, root: string, file: string): string {
|
|
123
|
+
return ancestors(dirnamePath(file), root).find((at) => holdsManifest(host, at)) ?? root;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type Runtime = "bundler" | "node";
|
|
127
|
+
|
|
128
|
+
const RUNTIME_OPTIONS = {
|
|
129
|
+
bundler: { module: "preserve", moduleResolution: "bundler" },
|
|
130
|
+
node: { module: "nodenext", moduleResolution: "nodenext" },
|
|
131
|
+
} as const satisfies Record<Runtime, Readonly<Record<string, string>>>;
|
|
132
|
+
|
|
133
|
+
/** The manifest members that say a package is written for Node. */
|
|
134
|
+
const NODE_MEMBERS = ["main", "exports", "bin", "type"] as const;
|
|
135
|
+
|
|
136
|
+
/** Whether one manifest says its package runs under Node, `engines.node` included; its scripts are not read. */
|
|
137
|
+
function writtenForNode(manifest: Readonly<Record<string, unknown>>): boolean {
|
|
138
|
+
const { engines } = manifest;
|
|
139
|
+
return (
|
|
140
|
+
NODE_MEMBERS.some((member) => manifest[member] !== undefined) ||
|
|
141
|
+
(isRecord(engines) && engines["node"] !== undefined)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The manifests, by path as `conventions` names them, an applied row of a tool that bundles
|
|
147
|
+
* applies through.
|
|
148
|
+
*/
|
|
149
|
+
export function bundlingManifests(conventions: Conventions): ReadonlySet<string> {
|
|
150
|
+
return new Set(
|
|
151
|
+
conventions.applied
|
|
152
|
+
.filter((one) => CONVENTION_ROWS.some((row) => row.name === one.name && row.bundles === true))
|
|
153
|
+
.map((one) => one.manifest),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface FormedProgram {
|
|
158
|
+
readonly configuration: Configuration;
|
|
159
|
+
readonly group: Group;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface Forming {
|
|
163
|
+
/** The tree's root, absolute, which the manifest paths of `bundling` are read below. */
|
|
164
|
+
readonly root: string;
|
|
165
|
+
readonly origin: Origin;
|
|
166
|
+
/** The manifests a bundling row applies through, as {@link bundlingManifests} names them. */
|
|
167
|
+
readonly bundling: ReadonlySet<string>;
|
|
168
|
+
/** The name one group's program is given, by the group's manifest path, `undefined` for none. */
|
|
169
|
+
readonly idOf: (manifest: string | undefined) => string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Each group's program, given the module system of the runtime its package is written for: a
|
|
174
|
+
* bundler's where a row of a tool that bundles applies through the group's manifest or the
|
|
175
|
+
* root's, else Node's where its manifest says the package runs under Node, under which a file's
|
|
176
|
+
* extension and its package's `type` decide its module format, else a bundler's, which reads a
|
|
177
|
+
* file with no module syntax as a classic script, as a browser loads it.
|
|
178
|
+
*/
|
|
179
|
+
export function formedPrograms(
|
|
180
|
+
engine: Engine,
|
|
181
|
+
host: Host,
|
|
182
|
+
groups: readonly Group[],
|
|
183
|
+
{ root, origin, bundling, idOf }: Forming,
|
|
184
|
+
): readonly FormedProgram[] {
|
|
185
|
+
return groups.map((group) => {
|
|
186
|
+
const manifest = group.manifest.kind === "absent" ? undefined : joinPath(group.dir, MANIFEST);
|
|
187
|
+
const below = manifest === undefined ? undefined : relativePath(root, manifest);
|
|
188
|
+
const runtime: Runtime =
|
|
189
|
+
!bundling.has(MANIFEST) &&
|
|
190
|
+
(below === undefined || !bundling.has(below)) &&
|
|
191
|
+
group.manifest.kind === "read" &&
|
|
192
|
+
writtenForNode(group.manifest.manifest)
|
|
193
|
+
? "node"
|
|
194
|
+
: "bundler";
|
|
195
|
+
const configFile = writtenPath(host, joinPath(group.dir, GROUP_CONFIGURATION));
|
|
196
|
+
const written = {
|
|
197
|
+
compilerOptions: { ...FORMED_OPTIONS, ...RUNTIME_OPTIONS[runtime] },
|
|
198
|
+
files: group.files,
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
group,
|
|
202
|
+
configuration: {
|
|
203
|
+
id: idOf(manifest),
|
|
204
|
+
configFile,
|
|
205
|
+
origin,
|
|
206
|
+
parsed: engine.parseConfigJson(written, configFile),
|
|
207
|
+
written,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The globals a test runner injects into every file it runs, which no import names: a
|
|
3
|
+
* Vitest configuration whose `test.globals` is `true` gives each test and each file it
|
|
4
|
+
* loads `describe`, `it`, `expect` and `vi` (https://vitest.dev/config/globals). A program
|
|
5
|
+
* analyzed without the runner's declarations of them reads every use as an error, so a
|
|
6
|
+
* configuration under such a runner is read with them; and a file that calls one is
|
|
7
|
+
* written for the runner, as one that imports it is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
isAsExpression,
|
|
12
|
+
isBinaryExpression,
|
|
13
|
+
isCallExpression,
|
|
14
|
+
isExportAssignment,
|
|
15
|
+
isExpressionStatement,
|
|
16
|
+
isIdentifier,
|
|
17
|
+
isObjectLiteralExpression,
|
|
18
|
+
isParenthesizedExpression,
|
|
19
|
+
isPropertyAccessExpression,
|
|
20
|
+
isPropertyAssignment,
|
|
21
|
+
isSatisfiesExpression,
|
|
22
|
+
SyntaxKind,
|
|
23
|
+
type Node,
|
|
24
|
+
type SourceFile,
|
|
25
|
+
} from "@typescript/native/ast";
|
|
26
|
+
import type { ParseFile } from "../configuration-files.ts";
|
|
27
|
+
import type { Host } from "../host.ts";
|
|
28
|
+
import { isRecord } from "../json-document.ts";
|
|
29
|
+
import { ancestors, dirnamePath, joinPath } from "../paths.ts";
|
|
30
|
+
import { UNANSWERED } from "../query.ts";
|
|
31
|
+
import type { ProjectView } from "../session.ts";
|
|
32
|
+
import { filesIn } from "../fs/tree.ts";
|
|
33
|
+
import { holdsManifest, INSTALLED_PACKAGES } from "../fs/manifest.ts";
|
|
34
|
+
import { isTargetConfiguration } from "./discover.ts";
|
|
35
|
+
import type { ProgramPlan } from "./plan.ts";
|
|
36
|
+
|
|
37
|
+
/** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
|
|
38
|
+
export const GLOBALS_RUNNER = "vitest";
|
|
39
|
+
const GLOBALS_TYPES = "vitest/globals";
|
|
40
|
+
|
|
41
|
+
/** The configuration files the runner reads its settings from, by name. */
|
|
42
|
+
const RUNNER_CONFIG = /^(?:vitest|vite)\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u;
|
|
43
|
+
|
|
44
|
+
/** The globals `vitest/globals` declares that a test or a helper calls or reads a member of. */
|
|
45
|
+
const GLOBAL_NAMES: ReadonlySet<string> = new Set([
|
|
46
|
+
"afterAll",
|
|
47
|
+
"afterEach",
|
|
48
|
+
"assert",
|
|
49
|
+
"assertType",
|
|
50
|
+
"beforeAll",
|
|
51
|
+
"beforeEach",
|
|
52
|
+
"bench",
|
|
53
|
+
"describe",
|
|
54
|
+
"expect",
|
|
55
|
+
"expectTypeOf",
|
|
56
|
+
"it",
|
|
57
|
+
"onTestFailed",
|
|
58
|
+
"onTestFinished",
|
|
59
|
+
"suite",
|
|
60
|
+
"test",
|
|
61
|
+
"vi",
|
|
62
|
+
"vitest",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** The expression one wrapper that changes no value holds, and a call's first argument. */
|
|
66
|
+
function configObject(node: Node): Node | undefined {
|
|
67
|
+
let at: Node | undefined = node;
|
|
68
|
+
while (at !== undefined) {
|
|
69
|
+
if (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
70
|
+
at = at.expression;
|
|
71
|
+
} else if (isCallExpression(at)) {
|
|
72
|
+
at = at.arguments[0];
|
|
73
|
+
} else {
|
|
74
|
+
return at;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The value one object literal writes for one member as `name: value`. */
|
|
81
|
+
function member(object: Node | undefined, name: string): Node | undefined {
|
|
82
|
+
if (object === undefined || !isObjectLiteralExpression(object)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
for (const property of object.properties) {
|
|
86
|
+
if (
|
|
87
|
+
isPropertyAssignment(property) &&
|
|
88
|
+
isIdentifier(property.name) &&
|
|
89
|
+
property.name.text === name
|
|
90
|
+
) {
|
|
91
|
+
return configObject(property.initializer);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether one runner configuration's default export sets `test.globals` to `true`. */
|
|
98
|
+
function setsGlobals(file: SourceFile): boolean {
|
|
99
|
+
for (const statement of file.statements) {
|
|
100
|
+
let exported: Node | undefined;
|
|
101
|
+
if (isExportAssignment(statement)) {
|
|
102
|
+
exported = statement.expression;
|
|
103
|
+
} else if (isExpressionStatement(statement) && isBinaryExpression(statement.expression)) {
|
|
104
|
+
const { left, operatorToken, right } = statement.expression;
|
|
105
|
+
if (
|
|
106
|
+
operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
107
|
+
isPropertyAccessExpression(left) &&
|
|
108
|
+
isIdentifier(left.expression) &&
|
|
109
|
+
left.expression.text === "module" &&
|
|
110
|
+
isIdentifier(left.name) &&
|
|
111
|
+
left.name.text === "exports"
|
|
112
|
+
) {
|
|
113
|
+
exported = right;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const object = exported === undefined ? undefined : configObject(exported);
|
|
117
|
+
const globals = member(member(object, "test"), "globals");
|
|
118
|
+
if (globals?.kind === SyntaxKind.TrueKeyword) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Whether the runner's configuration in one manifest's directory injects the globals. */
|
|
126
|
+
function injectsGlobals(host: Host, parse: ParseFile, dir: string): boolean {
|
|
127
|
+
return filesIn(host, dir, (name) => RUNNER_CONFIG.test(name)).some((name) => {
|
|
128
|
+
const path = joinPath(dir, name);
|
|
129
|
+
try {
|
|
130
|
+
return setsGlobals(parse(path, host.readFile(path)));
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether a file declaring the globals is installed where the configuration resolves packages from. */
|
|
138
|
+
function globalsInstalled(host: Host, configFile: string): boolean {
|
|
139
|
+
return ancestors(dirnamePath(configFile)).some(
|
|
140
|
+
(dir) =>
|
|
141
|
+
host.kindOf(joinPath(dir, INSTALLED_PACKAGES, GLOBALS_RUNNER, "globals.d.ts")) === "file",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The directory of the manifest nearest one configuration file, at or above its own. */
|
|
146
|
+
function manifestDir(host: Host, configFile: string): string | undefined {
|
|
147
|
+
return ancestors(dirnamePath(configFile)).find((dir) => holdsManifest(host, dir));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function typesOf(value: unknown): readonly string[] | undefined {
|
|
151
|
+
return Array.isArray(value) && value.every((one) => typeof one === "string") ? value : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The plans with the globals' declarations added to the `types` of each target configuration
|
|
156
|
+
* whose nearest manifest's runner configuration injects them and that can resolve them.
|
|
157
|
+
*/
|
|
158
|
+
export function testGlobals(
|
|
159
|
+
host: Host,
|
|
160
|
+
parse: ParseFile,
|
|
161
|
+
plans: readonly ProgramPlan[],
|
|
162
|
+
): readonly ProgramPlan[] {
|
|
163
|
+
const injects = new Map<string, boolean>();
|
|
164
|
+
return plans.map((plan): ProgramPlan => {
|
|
165
|
+
const dir = manifestDir(host, plan.configFile);
|
|
166
|
+
if (
|
|
167
|
+
!isTargetConfiguration(plan) ||
|
|
168
|
+
dir === undefined ||
|
|
169
|
+
!globalsInstalled(host, plan.configFile)
|
|
170
|
+
) {
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
if (!injects.has(dir)) {
|
|
174
|
+
injects.set(dir, injectsGlobals(host, parse, dir));
|
|
175
|
+
}
|
|
176
|
+
const base = typesOf(plan.options.types) ?? [];
|
|
177
|
+
const { build } = plan;
|
|
178
|
+
if (injects.get(dir) !== true) {
|
|
179
|
+
return plan;
|
|
180
|
+
}
|
|
181
|
+
if (build.kind === "program") {
|
|
182
|
+
const listed = typesOf(build.params.compilerOptions.types) ?? base;
|
|
183
|
+
return listed.includes(GLOBALS_TYPES)
|
|
184
|
+
? plan
|
|
185
|
+
: {
|
|
186
|
+
...plan,
|
|
187
|
+
build: {
|
|
188
|
+
kind: "program",
|
|
189
|
+
params: {
|
|
190
|
+
...build.params,
|
|
191
|
+
compilerOptions: {
|
|
192
|
+
...build.params.compilerOptions,
|
|
193
|
+
types: [...listed, GLOBALS_TYPES],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const written = build.kind === "configuration" ? build.value : { extends: plan.configFile };
|
|
200
|
+
const held = written["compilerOptions"];
|
|
201
|
+
const options = isRecord(held) ? held : {};
|
|
202
|
+
const listed = typesOf(options["types"]) ?? base;
|
|
203
|
+
return listed.includes(GLOBALS_TYPES)
|
|
204
|
+
? plan
|
|
205
|
+
: {
|
|
206
|
+
...plan,
|
|
207
|
+
build: {
|
|
208
|
+
kind: "configuration",
|
|
209
|
+
value: {
|
|
210
|
+
...written,
|
|
211
|
+
compilerOptions: { ...options, types: [...listed, GLOBALS_TYPES] },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Whether one file names a global the runner injects, which is how a file written for the
|
|
220
|
+
* runner's globals reads: a call of one of the names or a read of a member of it that
|
|
221
|
+
* resolves to a declaration outside the project's own files, the runner's declaration
|
|
222
|
+
* file. A name the file binds, imported, declared or a parameter, resolves to its own.
|
|
223
|
+
*/
|
|
224
|
+
export function namesTestGlobals<Brand>(project: ProjectView<Brand>, file: SourceFile): boolean {
|
|
225
|
+
const named: Node[] = [];
|
|
226
|
+
const visit = (node: Node): void => {
|
|
227
|
+
const callee = isCallExpression(node)
|
|
228
|
+
? node.expression
|
|
229
|
+
: isPropertyAccessExpression(node)
|
|
230
|
+
? node.expression
|
|
231
|
+
: undefined;
|
|
232
|
+
if (callee !== undefined && isIdentifier(callee) && GLOBAL_NAMES.has(callee.text)) {
|
|
233
|
+
named.push(callee);
|
|
234
|
+
}
|
|
235
|
+
node.forEachChild(visit);
|
|
236
|
+
};
|
|
237
|
+
file.forEachChild(visit);
|
|
238
|
+
if (named.length === 0) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const own = project.ownPaths();
|
|
242
|
+
return project
|
|
243
|
+
.symbolsAt(named.map((node) => project.handle(node)))
|
|
244
|
+
.some(
|
|
245
|
+
(symbol) =>
|
|
246
|
+
symbol === UNANSWERED ||
|
|
247
|
+
(symbol !== undefined &&
|
|
248
|
+
symbol.declarations.length > 0 &&
|
|
249
|
+
symbol.declarations.every((handle) => !own.has(handle.path))),
|
|
250
|
+
);
|
|
251
|
+
}
|