@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.
Files changed (276) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/mapper-connection.js +2 -1
  3. package/dist/bin/node-host.js +24 -5
  4. package/dist/src/analysis.js +181 -341
  5. package/dist/src/augmentations.js +106 -13
  6. package/dist/src/automatic-types.js +10 -29
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +2 -3
  10. package/dist/src/config.js +6 -82
  11. package/dist/src/configuration-files.js +28 -176
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +1 -1
  14. package/dist/src/consumers.js +3 -30
  15. package/dist/src/contextual-uses.js +56 -9
  16. package/dist/src/contract/config.generated.js +558 -0
  17. package/dist/src/contract/contract.generated.js +1244 -0
  18. package/dist/src/contract/report.generated.js +862 -0
  19. package/dist/src/convention-rows.js +73 -3
  20. package/dist/src/conventions.js +36 -66
  21. package/dist/src/dependencies.js +103 -40
  22. package/dist/src/edges.js +38 -122
  23. package/dist/src/element-access.js +4 -6
  24. package/dist/src/entry-point-gaps.js +2 -8
  25. package/dist/src/entry-points.js +11 -27
  26. package/dist/src/enum-group.js +3 -10
  27. package/dist/src/erased-stores.js +4 -6
  28. package/dist/src/exempt-classes.js +1 -63
  29. package/dist/src/exempt.js +21 -40
  30. package/dist/src/failure.js +36 -0
  31. package/dist/src/file-facts.js +2 -25
  32. package/dist/src/findings/completion.js +139 -72
  33. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  34. package/dist/src/findings/emitters.js +5 -5
  35. package/dist/src/findings/interfaces.js +4 -5
  36. package/dist/src/findings/intra-function.js +13 -10
  37. package/dist/src/findings/non-code-artifacts.js +7 -8
  38. package/dist/src/findings/reads-and-writes.js +16 -13
  39. package/dist/src/findings/self-check.js +10 -12
  40. package/dist/src/findings/unused-declarations.js +41 -32
  41. package/dist/src/findings/visibility-narrowing.js +19 -16
  42. package/dist/src/findings-pass.js +8 -15
  43. package/dist/src/framework-lifecycle.js +7 -7
  44. package/dist/src/fs/document.js +52 -0
  45. package/dist/src/fs/manifest.js +72 -0
  46. package/dist/src/fs/tree.js +88 -0
  47. package/dist/src/generated-file.js +7 -4
  48. package/dist/src/git-ignore.js +39 -26
  49. package/dist/src/graph.js +5 -3
  50. package/dist/src/html-entries.js +6 -33
  51. package/dist/src/in-operands.js +2 -1
  52. package/dist/src/index.js +1 -1
  53. package/dist/src/injection-container.js +1 -1
  54. package/dist/src/intra-function-parts.js +63 -43
  55. package/dist/src/inventory.js +2 -21
  56. package/dist/src/javascript-file.js +33 -0
  57. package/dist/src/json-document.js +198 -67
  58. package/dist/src/keyof-comparisons.js +4 -6
  59. package/dist/src/kinds.js +3 -366
  60. package/dist/src/load/classify.js +270 -0
  61. package/dist/src/load/diagnostics.js +80 -0
  62. package/dist/src/load/discover.js +277 -0
  63. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  64. package/dist/src/load/expected-module.js +136 -0
  65. package/dist/src/load/load.js +27 -0
  66. package/dist/src/load/module-lookup.js +37 -0
  67. package/dist/src/load/plan.js +313 -0
  68. package/dist/src/load/specifiers.js +115 -0
  69. package/dist/src/load/synthesize.js +140 -0
  70. package/dist/src/load/test-globals.js +207 -0
  71. package/dist/src/load/test-joins.js +82 -0
  72. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  73. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
  74. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
  75. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  76. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  77. package/dist/src/manifest.js +49 -54
  78. package/dist/src/matrix.js +76 -4
  79. package/dist/src/mdx-entries.js +3 -26
  80. package/dist/src/outside-tests.js +1 -1
  81. package/dist/src/partial.js +2 -1
  82. package/dist/src/paths.js +26 -15
  83. package/dist/src/position.js +2 -1
  84. package/dist/src/print.js +11 -79
  85. package/dist/src/product.js +13 -0
  86. package/dist/src/query.js +1 -0
  87. package/dist/src/ref.js +1 -0
  88. package/dist/src/references.js +48 -30
  89. package/dist/src/report-schema.js +51 -0
  90. package/dist/src/report.js +57 -41
  91. package/dist/src/reporters/annotations.js +15 -8
  92. package/dist/src/reporters/sarif.js +16 -10
  93. package/dist/src/reporters/template.js +4 -19
  94. package/dist/src/resolve.js +245 -729
  95. package/dist/src/roots.js +273 -87
  96. package/dist/src/run.js +66 -77
  97. package/dist/src/schema.js +320 -343
  98. package/dist/src/scope.js +38 -115
  99. package/dist/src/serialization-contract.js +595 -21
  100. package/dist/src/session.js +34 -29
  101. package/dist/src/source-files.js +1 -1
  102. package/dist/src/suppress-file.js +25 -28
  103. package/dist/src/suppress-inline.js +3 -2
  104. package/dist/src/suppress.js +33 -51
  105. package/dist/src/sweep.js +7 -9
  106. package/dist/src/symbol-keys.js +3 -5
  107. package/dist/src/template-field.js +9 -19
  108. package/dist/src/test-support.js +8 -11
  109. package/dist/src/text/compare.js +26 -0
  110. package/dist/src/text/glob.js +194 -0
  111. package/dist/src/text/named-files.js +54 -0
  112. package/dist/src/text/shell.js +159 -0
  113. package/dist/src/text/yaml.js +393 -0
  114. package/dist/src/type-errors.js +11 -232
  115. package/dist/src/unseen-loads.js +100 -0
  116. package/dist/src/unwrap.js +23 -0
  117. package/dist/src/usage.js +10 -0
  118. package/dist/src/verbs/analyze.js +166 -126
  119. package/dist/src/verbs/describe.js +9 -16
  120. package/dist/src/verbs/explain.js +10 -7
  121. package/dist/src/verbs/print-config.js +2 -1
  122. package/dist/src/verbs/print-projects.js +6 -4
  123. package/dist/src/verbs/print-retained.js +4 -2
  124. package/dist/src/verbs/print-roots.js +4 -2
  125. package/dist/src/verbs/verb.js +1 -5
  126. package/dist/src/verbs/version.js +14 -3
  127. package/dist/src/workflow-steps.js +62 -64
  128. package/jsr.json +1 -1
  129. package/package.json +4 -3
  130. package/src/analysis.ts +245 -502
  131. package/src/augmentations.ts +135 -17
  132. package/src/automatic-types.ts +11 -29
  133. package/src/build-scripts.ts +543 -0
  134. package/src/component-layer.ts +4 -2
  135. package/src/components.ts +2 -4
  136. package/src/config.ts +31 -302
  137. package/src/configuration-files.ts +34 -179
  138. package/src/configured-declarations.ts +18 -23
  139. package/src/conformance-record.ts +1 -1
  140. package/src/consumers.ts +6 -57
  141. package/src/contextual-uses.ts +64 -12
  142. package/src/contract/config.generated.ts +818 -0
  143. package/src/contract/contract.generated.ts +1308 -0
  144. package/src/contract/report.generated.ts +1237 -0
  145. package/src/convention-rows.ts +85 -3
  146. package/src/conventions.ts +46 -84
  147. package/src/dependencies.ts +118 -46
  148. package/src/edges.ts +41 -140
  149. package/src/element-access.ts +4 -7
  150. package/src/entry-point-gaps.ts +3 -9
  151. package/src/entry-points.ts +12 -38
  152. package/src/enum-group.ts +2 -11
  153. package/src/erased-stores.ts +3 -7
  154. package/src/exempt-classes.ts +1 -75
  155. package/src/exempt.ts +36 -50
  156. package/src/failure.ts +63 -0
  157. package/src/file-facts.ts +9 -24
  158. package/src/finding.ts +14 -9
  159. package/src/findings/boundary.ts +2 -0
  160. package/src/findings/completion.ts +181 -84
  161. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  162. package/src/findings/emitter.ts +8 -9
  163. package/src/findings/emitters.ts +7 -7
  164. package/src/findings/interfaces.ts +4 -6
  165. package/src/findings/intra-function.ts +13 -11
  166. package/src/findings/non-code-artifacts.ts +9 -11
  167. package/src/findings/reads-and-writes.ts +20 -16
  168. package/src/findings/self-check.ts +11 -17
  169. package/src/findings/unused-declarations.ts +45 -32
  170. package/src/findings/visibility-narrowing.ts +20 -17
  171. package/src/findings-pass.ts +10 -19
  172. package/src/framework-lifecycle.ts +7 -7
  173. package/src/fs/document.ts +70 -0
  174. package/src/fs/manifest.ts +92 -0
  175. package/src/fs/tree.ts +110 -0
  176. package/src/generated-file.ts +7 -4
  177. package/src/git-ignore.ts +50 -26
  178. package/src/graph.ts +5 -3
  179. package/src/host.ts +12 -5
  180. package/src/html-entries.ts +6 -33
  181. package/src/in-operands.ts +2 -1
  182. package/src/index.ts +1 -1
  183. package/src/injection-container.ts +1 -1
  184. package/src/intra-function-parts.ts +73 -44
  185. package/src/inventory.ts +2 -20
  186. package/src/javascript-file.ts +37 -0
  187. package/src/json-document.ts +241 -76
  188. package/src/keyof-comparisons.ts +3 -7
  189. package/src/kinds.ts +5 -413
  190. package/src/load/classify.ts +397 -0
  191. package/src/load/diagnostics.ts +144 -0
  192. package/src/load/discover.ts +455 -0
  193. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  194. package/src/load/expected-module.ts +188 -0
  195. package/src/load/load.ts +73 -0
  196. package/src/load/module-lookup.ts +50 -0
  197. package/src/load/plan.ts +504 -0
  198. package/src/load/specifiers.ts +168 -0
  199. package/src/load/synthesize.ts +211 -0
  200. package/src/load/test-globals.ts +251 -0
  201. package/src/load/test-joins.ts +98 -0
  202. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  203. package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
  204. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
  205. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  206. package/src/{workspace.ts → load/workspace.ts} +90 -134
  207. package/src/manifest.ts +81 -75
  208. package/src/matrix.ts +122 -4
  209. package/src/mdx-entries.ts +3 -26
  210. package/src/outside-tests.ts +1 -1
  211. package/src/partial.ts +3 -2
  212. package/src/paths.ts +27 -15
  213. package/src/position.ts +2 -1
  214. package/src/print.ts +12 -92
  215. package/src/product.ts +18 -0
  216. package/src/query.ts +11 -0
  217. package/src/ref.ts +1 -0
  218. package/src/references.ts +52 -31
  219. package/src/report-schema.ts +68 -0
  220. package/src/report.ts +101 -224
  221. package/src/reporters/annotations.ts +22 -11
  222. package/src/reporters/json.ts +1 -1
  223. package/src/reporters/reporter.ts +2 -4
  224. package/src/reporters/reporters.ts +1 -1
  225. package/src/reporters/sarif.ts +29 -16
  226. package/src/reporters/template.ts +4 -20
  227. package/src/reporters/text.ts +3 -3
  228. package/src/resolve.ts +283 -1114
  229. package/src/roots.ts +339 -104
  230. package/src/run.ts +75 -91
  231. package/src/schema.ts +391 -388
  232. package/src/scope.ts +46 -138
  233. package/src/serialization-contract.ts +763 -22
  234. package/src/session.ts +70 -54
  235. package/src/source-files.ts +1 -1
  236. package/src/suppress-file.ts +24 -33
  237. package/src/suppress-inline.ts +2 -2
  238. package/src/suppress.ts +43 -58
  239. package/src/sweep.ts +23 -9
  240. package/src/symbol-keys.ts +2 -5
  241. package/src/template-field.ts +15 -25
  242. package/src/test-support.ts +17 -13
  243. package/src/text/compare.ts +27 -0
  244. package/src/text/glob.ts +210 -0
  245. package/src/text/named-files.inputs.json +1522 -0
  246. package/src/text/named-files.ts +68 -0
  247. package/src/text/shell.ts +161 -0
  248. package/src/text/yaml.ts +423 -0
  249. package/src/type-errors.ts +18 -288
  250. package/src/unseen-loads.ts +134 -0
  251. package/src/unwrap.ts +34 -0
  252. package/src/usage.ts +10 -0
  253. package/src/verbs/analyze.ts +224 -162
  254. package/src/verbs/describe.ts +14 -15
  255. package/src/verbs/explain.ts +10 -11
  256. package/src/verbs/print-config.ts +2 -1
  257. package/src/verbs/print-projects.ts +6 -4
  258. package/src/verbs/print-retained.ts +4 -2
  259. package/src/verbs/print-roots.ts +4 -2
  260. package/src/verbs/verb.ts +2 -8
  261. package/src/verbs/version.ts +15 -3
  262. package/src/workflow-steps.ts +74 -64
  263. package/dist/src/base-url.js +0 -18
  264. package/dist/src/discover.js +0 -468
  265. package/dist/src/glob.js +0 -65
  266. package/dist/src/pnpm-workspace.js +0 -120
  267. package/dist/src/setup-failure.js +0 -23
  268. package/dist/src/test-joins.js +0 -101
  269. package/dist/src/version.js +0 -7
  270. package/src/base-url.ts +0 -24
  271. package/src/discover.ts +0 -653
  272. package/src/glob.ts +0 -67
  273. package/src/pnpm-workspace.ts +0 -135
  274. package/src/setup-failure.ts +0 -39
  275. package/src/test-joins.ts +0 -120
  276. 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
+ }