@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.11

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 +5 -4
  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
@@ -1,47 +1,34 @@
1
1
  /**
2
2
  * The workspace a target sits in: the packages `pnpm-workspace.yaml` or a manifest's
3
- * `workspaces` declares in the nearest directory at or above the target root, up to
4
- * its repository root, where its patterns name the package the target is in, each
5
- * with the compiler configurations in its directory. A
6
- * member is the workspace root or a directory the patterns name that holds a named
7
- * manifest, never one below `node_modules`.
3
+ * `workspaces` declares in the nearest directory at or above the target root, up to its
4
+ * repository root, where its patterns name the package the target is in, each with the
5
+ * compiler configurations in its directory.
8
6
  */
9
- import { DiscoveryError } from "./discover.js";
7
+ import { configFilesIn } from "./discover.js";
8
+ import { failureExit } from "../failure.js";
10
9
  import { commonDirectory } from "./emit-map.js";
11
- import { dirnamePath, joinPath, relativePath } from "./paths.js";
12
- import { pnpmPackages } from "./pnpm-workspace.js";
13
- const MANIFEST = "package.json";
10
+ import { ancestors, dirnamePath, joinPath, relativePath } from "../paths.js";
11
+ import { bytewise } from "../text/compare.js";
12
+ import { isRecord } from "../json-document.js";
13
+ import { directoriesIn, repositoryOf } from "../fs/tree.js";
14
+ import { holdsManifest, inInstalledPackages, INSTALLED_PACKAGES, MANIFEST, manifestAt, } from "../fs/manifest.js";
15
+ import { braceExpanded, pathMatches, segmentMatches } from "../text/glob.js";
16
+ import { yamlKey } from "../text/yaml.js";
14
17
  const PNPM_WORKSPACE = "pnpm-workspace.yaml";
15
- const CONFIG_FILE = /^tsconfig.*\.json$/u;
16
- function isRecord(value) {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
18
- }
19
- /** One manifest, decoded, or `undefined` where there is none to read. */
20
- function manifestAt(host, dir) {
21
- let text;
22
- try {
23
- text = host.readFile(joinPath(dir, MANIFEST));
24
- }
25
- catch {
26
- return undefined;
27
- }
28
- try {
29
- const value = JSON.parse(text);
30
- return isRecord(value) ? value : undefined;
31
- }
32
- catch {
33
- return undefined;
34
- }
35
- }
36
18
  /** The patterns one directory declares a workspace with, or `undefined` where it declares none. */
37
19
  function declarationAt(host, dir) {
38
20
  const pnpm = joinPath(dir, PNPM_WORKSPACE);
39
21
  if (host.kindOf(pnpm) === "file") {
40
- const read = pnpmPackages(host.readFile(pnpm));
41
- if (read.kind === "refused") {
42
- throw new DiscoveryError(`${pnpm}: ${read.reason}; the workspace's packages are read from a sequence of strings`, []);
22
+ const read = yamlKey(host.readFile(pnpm), "packages");
23
+ if (read.kind === "absent") {
24
+ return { file: pnpm, patterns: [] };
43
25
  }
44
- return { file: pnpm, patterns: read.kind === "listed" ? read.patterns : [] };
26
+ const listed = read.kind === "value" ? read.value : undefined;
27
+ if (!Array.isArray(listed) || !listed.every((entry) => typeof entry === "string")) {
28
+ const reason = read.kind === "refused" ? read.reason : "packages is not a sequence of strings";
29
+ throw failureExit("no-program", `${pnpm}: ${reason}; the workspace's packages are read from a sequence of strings: correct the declaration`);
30
+ }
31
+ return { file: pnpm, patterns: listed };
45
32
  }
46
33
  const manifest = manifestAt(host, dir);
47
34
  if (manifest === undefined || !Object.hasOwn(manifest, "workspaces")) {
@@ -50,7 +37,7 @@ function declarationAt(host, dir) {
50
37
  const declared = manifest["workspaces"];
51
38
  const list = isRecord(declared) ? declared["packages"] : declared;
52
39
  if (!Array.isArray(list) || !list.every((entry) => typeof entry === "string")) {
53
- throw new DiscoveryError(`${joinPath(dir, MANIFEST)}: workspaces is neither an array of strings nor an object whose packages is one`, []);
40
+ throw failureExit("no-program", `${joinPath(dir, MANIFEST)}: workspaces is neither an array of strings nor an object whose packages is one: correct the declaration`);
54
41
  }
55
42
  return { file: joinPath(dir, MANIFEST), patterns: list };
56
43
  }
@@ -62,20 +49,6 @@ function declaresWorkspace(host, dir) {
62
49
  const manifest = manifestAt(host, dir);
63
50
  return manifest !== undefined && Object.hasOwn(manifest, "workspaces");
64
51
  }
65
- /** A pattern with every brace list expanded into its alternatives. */
66
- function braceExpanded(pattern) {
67
- const open = pattern.indexOf("{");
68
- const close = open < 0 ? -1 : pattern.indexOf("}", open);
69
- if (close < 0) {
70
- return [pattern];
71
- }
72
- const before = pattern.slice(0, open);
73
- const after = pattern.slice(close + 1);
74
- return pattern
75
- .slice(open + 1, close)
76
- .split(",")
77
- .flatMap((member) => braceExpanded(before + member + after));
78
- }
79
52
  /**
80
53
  * The segments of one pattern below the workspace root, `.` and empty segments
81
54
  * dropped and `..` taken back, or `undefined` where it leaves the root.
@@ -96,52 +69,6 @@ function segmentsOf(pattern) {
96
69
  }
97
70
  return segments;
98
71
  }
99
- /**
100
- * Whether one directory name matches one pattern segment: `*` stands for any run of
101
- * characters and `?` for one, and neither matches a leading dot.
102
- */
103
- function segmentMatches(segment, name) {
104
- if (name.startsWith(".") && !segment.startsWith(".")) {
105
- return false;
106
- }
107
- const source = Array.from(segment)
108
- .map((character) => {
109
- if (character === "*") {
110
- return ".*";
111
- }
112
- if (character === "?") {
113
- return ".";
114
- }
115
- return character.replaceAll(/[.+^${}()|[\]\\/]/gu, (c) => `\\${c}`);
116
- })
117
- .join("");
118
- return new RegExp(`^${source}$`, "u").test(name);
119
- }
120
- /** Whether one path's segments match one pattern's, `**` standing for any run of segments. */
121
- function pathMatches(pattern, path) {
122
- const [head, ...rest] = pattern;
123
- if (head === undefined) {
124
- return path.length === 0;
125
- }
126
- if (head === "**") {
127
- return (pathMatches(rest, path) ||
128
- (path.length > 0 && !(path[0] ?? "").startsWith(".") && pathMatches(pattern, path.slice(1))));
129
- }
130
- const [name, ...below] = path;
131
- return name !== undefined && segmentMatches(head, name) && pathMatches(rest, below);
132
- }
133
- function subdirectories(host, dir) {
134
- try {
135
- return host
136
- .readDirectory(dir)
137
- .filter((entry) => entry.directory && entry.name !== "node_modules" && entry.name !== ".git")
138
- .map((entry) => entry.name)
139
- .sort();
140
- }
141
- catch {
142
- return [];
143
- }
144
- }
145
72
  /**
146
73
  * Every directory below `dir` one pattern names. `**` does not descend into a
147
74
  * directory that declares a workspace of its own, whose packages are that
@@ -155,7 +82,7 @@ function expand(host, dir, pattern, into) {
155
82
  }
156
83
  if (head === "**") {
157
84
  expand(host, dir, rest, into);
158
- for (const name of subdirectories(host, dir)) {
85
+ for (const name of directoriesIn(host, dir)) {
159
86
  if (name.startsWith(".")) {
160
87
  continue;
161
88
  }
@@ -166,12 +93,12 @@ function expand(host, dir, pattern, into) {
166
93
  }
167
94
  if (!head.includes("*") && !head.includes("?")) {
168
95
  const child = joinPath(dir, head);
169
- if (head !== "node_modules" && host.kindOf(child) === "directory") {
96
+ if (head !== INSTALLED_PACKAGES && host.kindOf(child) === "directory") {
170
97
  expand(host, child, rest, into);
171
98
  }
172
99
  return;
173
100
  }
174
- for (const name of subdirectories(host, dir)) {
101
+ for (const name of directoriesIn(host, dir)) {
175
102
  if (segmentMatches(head, name)) {
176
103
  expand(host, joinPath(dir, name), rest, into);
177
104
  }
@@ -208,10 +135,10 @@ function namesDirectory(root, patterns, dir) {
208
135
  if (below === undefined) {
209
136
  return false;
210
137
  }
211
- const path = below.split("/");
212
- if (path.includes("node_modules")) {
138
+ if (inInstalledPackages(below)) {
213
139
  return false;
214
140
  }
141
+ const path = below.split("/");
215
142
  let named = false;
216
143
  for (const written of patterns) {
217
144
  const negated = written.startsWith("!");
@@ -238,18 +165,7 @@ function layoutOf(configFile, parsed) {
238
165
  return { outDir: parsed.outDir, declarationDir: parsed.declarationDir, sourceDirs };
239
166
  }
240
167
  function configurationsOf(host, dir, parse) {
241
- let names;
242
- try {
243
- names = host
244
- .readDirectory(dir)
245
- .filter((entry) => !entry.directory && CONFIG_FILE.test(entry.name))
246
- .map((entry) => entry.name)
247
- .sort();
248
- }
249
- catch {
250
- return [];
251
- }
252
- return names.flatMap((name) => {
168
+ return configFilesIn(host, dir).flatMap((name) => {
253
169
  const configFile = joinPath(dir, name);
254
170
  const parsed = parse(configFile);
255
171
  return parsed === undefined
@@ -267,31 +183,27 @@ function configurationsOf(host, dir, parse) {
267
183
  * highest directory below it, at or above the target root, that holds a manifest.
268
184
  */
269
185
  function nearestDeclaration(host, targetRoot) {
270
- let dir = targetRoot;
271
186
  let highestPackage;
272
- for (;;) {
187
+ for (const dir of ancestors(targetRoot, repositoryOf(host, targetRoot)?.root)) {
273
188
  const declared = declarationAt(host, dir);
274
189
  if (declared !== undefined) {
275
190
  return { root: dir, ...declared, highestPackage };
276
191
  }
277
- if (host.kindOf(joinPath(dir, MANIFEST)) === "file") {
192
+ if (holdsManifest(host, dir)) {
278
193
  highestPackage = dir;
279
194
  }
280
- const up = dirnamePath(dir);
281
- if (up === dir || host.kindOf(joinPath(dir, ".git")) !== "absent") {
282
- return undefined;
283
- }
284
- dir = up;
285
195
  }
196
+ return undefined;
286
197
  }
287
198
  /**
288
199
  * The workspace the target sits in, or `undefined` where none holds it: no directory
289
200
  * from the target root up to its repository root declares one with a member beside
290
201
  * its root, or the nearest one does not name the highest package below it, at or
291
202
  * above the target root, among its members. `parse` reads one compiler configuration,
292
- * or answers `undefined` where it does not read.
203
+ * or answers `undefined` where it does not read. `known`, a workspace already read, is the
204
+ * answer where the nearest declaration is its own, so its members are not listed again.
293
205
  */
294
- export function findWorkspace(host, targetRoot, parse) {
206
+ export function findWorkspace(host, targetRoot, parse, known) {
295
207
  const declared = nearestDeclaration(host, targetRoot);
296
208
  if (declared === undefined) {
297
209
  return undefined;
@@ -300,6 +212,9 @@ export function findWorkspace(host, targetRoot, parse) {
300
212
  !namesDirectory(declared.root, declared.patterns, declared.highestPackage)) {
301
213
  return undefined;
302
214
  }
215
+ if (known?.root === declared.root) {
216
+ return known;
217
+ }
303
218
  const members = [];
304
219
  for (const dir of memberDirectories(host, declared.root, declared.patterns)) {
305
220
  const manifest = manifestAt(host, dir);
@@ -320,7 +235,10 @@ export function findWorkspace(host, targetRoot, parse) {
320
235
  if (members.length < 2) {
321
236
  return undefined;
322
237
  }
323
- members.sort((a, b) => a.dir === declared.root ? -1 : b.dir === declared.root ? 1 : a.dir < b.dir ? -1 : 1);
238
+ members.sort((a, b) => a.dir === declared.root ? -1 : b.dir === declared.root ? 1 : bytewise(a.dir, b.dir));
239
+ return assembled(host, declared.root, members);
240
+ }
241
+ function assembled(host, root, members) {
324
242
  const named = new Map();
325
243
  for (const member of members) {
326
244
  named.set(member.name, [...(named.get(member.name) ?? []), member]);
@@ -332,11 +250,12 @@ export function findWorkspace(host, targetRoot, parse) {
332
250
  return scopes.get(dir);
333
251
  }
334
252
  const up = dirnamePath(dir);
335
- const found = host.kindOf(joinPath(dir, MANIFEST)) === "file" ? dir : up === dir ? undefined : scopeOf(up);
253
+ const found = holdsManifest(host, dir) ? dir : up === dir ? undefined : scopeOf(up);
336
254
  scopes.set(dir, found);
337
255
  return found;
338
256
  };
339
257
  return {
258
+ root,
340
259
  members,
341
260
  named,
342
261
  memberOf: (path) => {
@@ -345,6 +264,25 @@ export function findWorkspace(host, targetRoot, parse) {
345
264
  },
346
265
  };
347
266
  }
267
+ /**
268
+ * The workspace with the programs the run forms put first among each member's configurations,
269
+ * by the member's directory: a formed program has no file on disk to list, so its file list
270
+ * is what makes a member's JavaScript its source.
271
+ */
272
+ export function withFormed(host, workspace, formed) {
273
+ return assembled(host, workspace.root, workspace.members.map((member) => {
274
+ const own = formed.get(member.dir) ?? [];
275
+ return own.length === 0
276
+ ? member
277
+ : {
278
+ ...member,
279
+ configurations: [
280
+ ...own.map((layout) => ({ fileNames: new Set(layout.fileNames) })),
281
+ ...member.configurations,
282
+ ],
283
+ };
284
+ }));
285
+ }
348
286
  /**
349
287
  * Whether one configuration holds one file only for the references it makes: the file
350
288
  * belongs to a workspace package, the root included, and the configuration does not
@@ -5,13 +5,12 @@
5
5
  * directory's manifest, {@link packageEntries} those of the packages below the target root.
6
6
  */
7
7
  import { isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
8
+ import { isRecord } from "./json-document.js";
9
+ import { MANIFEST, manifestAt, readJSON } from "./fs/manifest.js";
10
+ import { scriptTokens } from "./text/shell.js";
8
11
  /** The manifest's own name. */
9
- const MANIFEST = "package.json";
10
12
  /** The three members that name an entry point beside `exports` and `bin`. */
11
13
  const LEGACY_MEMBERS = ["main", "module", "types"];
12
- function isRecord(value) {
13
- return typeof value === "object" && value !== null && !Array.isArray(value);
14
- }
15
14
  /**
16
15
  * Whether one member's value names a file of this package rather than another
17
16
  * package or a URL. A target written with no leading dot is a path too, which is
@@ -23,31 +22,32 @@ function isTarget(value, requireDot) {
23
22
  }
24
23
  return !requireDot || value.startsWith("./");
25
24
  }
26
- /** Where a script's command line splits into tokens: white space and the shell's operators. */
27
- const SCRIPT_SEPARATORS = /\s+|&&|\|\||;|\|/u;
28
- /** A token written inside one pair of quotes. */
29
- const QUOTED = /^(["'])(.*)\1$/su;
30
- /**
31
- * Every token one script's command line holds, each with one pair of enclosing quotes
32
- * removed. A token holding a wildcard is a pattern rather than a path, and names no
33
- * entry point.
34
- */
35
- export function scriptTokens(command) {
36
- return command
37
- .split(SCRIPT_SEPARATORS)
38
- .map((token) => token.replace(QUOTED, "$2"))
39
- .filter((token) => token !== "" && !token.includes("*"));
40
- }
41
25
  /** One member's spelling below another: `exports` then `exports["."]`. */
42
26
  function below(member, key) {
43
27
  return `${member}[${JSON.stringify(key)}]`;
44
28
  }
45
29
  /**
46
- * Every target one `exports` value carries, at the member each is written under.
47
- *
48
- * The value nests strings, subpath or condition objects and alternative arrays to
49
- * any depth; a `null` blocks a subpath. Every level is walked, because each
50
- * condition names its own entry file, and a key's meaning is never read.
30
+ * The command strings one manifest runs, each at the member it is written under: each of its
31
+ * `scripts`, and the `exec` command of nodemon's configuration, in the manifest's `nodemonConfig`
32
+ * or the `nodemon.json` beside it, which nodemon runs the watched script with:
33
+ * https://github.com/remy/nodemon#config-files
34
+ */
35
+ export function manifestCommands(host, dir, manifest) {
36
+ const scripts = manifest["scripts"];
37
+ const nodemon = {
38
+ nodemonConfig: manifest["nodemonConfig"],
39
+ "nodemon.json": readJSON(host, joinPath(dir, "nodemon.json")),
40
+ };
41
+ const written = [
42
+ ...Object.entries(isRecord(scripts) ? scripts : {}).map(([name, value]) => [below("scripts", name), value]),
43
+ ...Object.entries(nodemon).map(([member, config]) => [below(member, "exec"), isRecord(config) ? config["exec"] : undefined]),
44
+ ];
45
+ return written.flatMap(([member, command]) => typeof command === "string" ? [{ member, command }] : []);
46
+ }
47
+ /**
48
+ * Every target one `exports` value carries, at the member each is written under. Every level of
49
+ * strings, subpath or condition objects and alternative arrays is walked, because each condition
50
+ * names its own entry file; a `null` blocks a subpath, and a key's meaning is never read.
51
51
  */
52
52
  function exportTargets(value, member, found) {
53
53
  if (typeof value === "string") {
@@ -69,28 +69,6 @@ function exportTargets(value, member, found) {
69
69
  exportTargets(held, below(member, key), found);
70
70
  }
71
71
  }
72
- /** The manifest at the target root, decoded, or undefined where there is none to read. */
73
- function manifestOf(host, targetRoot) {
74
- let text;
75
- try {
76
- text = host.readFile(joinPath(targetRoot, MANIFEST));
77
- }
78
- catch {
79
- return undefined;
80
- }
81
- let value;
82
- try {
83
- value = JSON.parse(text);
84
- }
85
- catch {
86
- // A manifest this program cannot read names no entry point, which is the same
87
- // answer as an absent one. What a manifest's contents say is not refused here:
88
- // the package manager owns that refusal, and an analysis of a tree it rejects
89
- // is not the run's subject.
90
- return undefined;
91
- }
92
- return isRecord(value) ? value : undefined;
93
- }
94
72
  /**
95
73
  * What the manifest in `targetRoot` declares about where its package is entered.
96
74
  *
@@ -99,7 +77,7 @@ function manifestOf(host, targetRoot) {
99
77
  * it and there is one such configuration per project, while there is one manifest.
100
78
  */
101
79
  export function readManifest(host, targetRoot) {
102
- const manifest = manifestOf(host, targetRoot);
80
+ const manifest = manifestAt(host, targetRoot);
103
81
  if (manifest === undefined) {
104
82
  return { entries: [], declaresExports: false };
105
83
  }
@@ -110,6 +88,24 @@ export function readManifest(host, targetRoot) {
110
88
  found.push({ member, path: value, role: "import", published: true });
111
89
  }
112
90
  }
91
+ // A `browser` string replaces `main` in a browser bundle, and an object replaces each
92
+ // module it maps with the file it names: https://github.com/defunctzombie/package-browser-field-spec
93
+ const browser = manifest["browser"];
94
+ if (typeof browser === "string" && isTarget(browser, false)) {
95
+ found.push({ member: "browser", path: browser, role: "import", published: true });
96
+ }
97
+ else if (isRecord(browser)) {
98
+ for (const [replaced, value] of Object.entries(browser)) {
99
+ if (typeof value === "string" && isTarget(value, true)) {
100
+ found.push({
101
+ member: below("browser", replaced),
102
+ path: value,
103
+ role: "import",
104
+ published: true,
105
+ });
106
+ }
107
+ }
108
+ }
113
109
  const bin = manifest["bin"];
114
110
  if (typeof bin === "string" && isTarget(bin, false)) {
115
111
  found.push({ member: "bin", path: bin, role: "run", published: false });
@@ -128,16 +124,15 @@ export function readManifest(host, targetRoot) {
128
124
  if (typeof command !== "string") {
129
125
  continue;
130
126
  }
131
- for (const token of new Set(scriptTokens(command))) {
132
- found.push({
133
- member: below("scripts", name),
134
- path: token,
135
- role: "script",
136
- published: false,
137
- });
127
+ const member = below("scripts", name);
128
+ for (const path of new Set(scriptTokens(command))) {
129
+ found.push({ member, path, role: "script", published: false });
138
130
  }
139
131
  }
140
132
  }
133
+ for (const { member, command } of manifestCommands(host, targetRoot, manifest)) {
134
+ found.push({ member, path: ".", role: "script", published: false, command });
135
+ }
141
136
  return {
142
137
  entries: found.map((entry) => ({
143
138
  ...entry,
@@ -70,11 +70,13 @@ export function matrixOf(per) {
70
70
  }
71
71
  });
72
72
  symbols.sort((a, b) => byPosition(a.position, b.position));
73
+ const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
73
74
  return {
74
75
  configurations: per.map((one) => one.configuration),
75
76
  graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles, one.ambient)),
76
77
  union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))], [...new Set(per.flatMap((one) => one.supportFiles ?? []))], new Set(per.flatMap((one) => [...(one.ambient ?? [])]))),
77
78
  heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
79
+ testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
78
80
  dependents,
79
81
  };
80
82
  }
@@ -101,18 +103,24 @@ export function sweepMatrix(matrix, input) {
101
103
  ]));
102
104
  const union = matrix.union;
103
105
  const heldBy = union.symbols.map(() => undefined);
104
- const testReached = reachedWithTheTests(union, [
106
+ const held = [
105
107
  ...input.marked,
106
108
  ...(input.exempt ?? []).map((record) => record.id),
107
109
  ...(input.unanswered ?? []),
108
- ]);
109
- const judged = union.symbols.map((symbol, at) => {
110
+ ];
111
+ const testReached = reachedWithTheTests(union, held);
112
+ // A test uses what it names: a file a test reads through a star re-export or a
113
+ // namespace keeps the exports nothing names at their own class.
114
+ const testNamed = reachedWithTheTests(union, held, false);
115
+ const everywhere = union.symbols.map((symbol, at) => {
110
116
  const held = matrix.heldIn[at] ?? [];
111
117
  const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
112
118
  return held.length === 0 || found.some((candidate) => candidate === undefined)
113
119
  ? undefined
114
120
  : found.filter((candidate) => candidate !== undefined);
115
121
  });
122
+ const compiled = compiledReferences(matrix, new Set(union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id]))), input.mode);
123
+ const judged = everywhere.map((found, at) => compiled.has(union.symbols[at]?.id ?? "") ? undefined : found);
116
124
  // A configuration that does not hold a test's subject's caller finds the subject dead,
117
125
  // so a test is of dead code only where every production subject it names is dead in
118
126
  // every configuration that holds it.
@@ -121,6 +129,9 @@ export function sweepMatrix(matrix, input) {
121
129
  union.ambient[edge.to] === true ||
122
130
  union.subject[edge.to] !== true ||
123
131
  judged[edge.to] !== undefined);
132
+ const admittedAt = (at) => judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
133
+ const kept = keptByTests(union, testReached, admittedAt);
134
+ const testUsed = testUsedReach(union, union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true), judged.map((one) => one !== undefined));
124
135
  const candidates = [];
125
136
  union.symbols.forEach((symbol, at) => {
126
137
  const all = judged[at];
@@ -128,7 +139,7 @@ export function sweepMatrix(matrix, input) {
128
139
  return;
129
140
  }
130
141
  const held = matrix.heldIn[at] ?? [];
131
- const admitted = all.every((candidate) => candidate.testOfDeadCode) && subjectsDead(at);
142
+ const admitted = admittedAt(at);
132
143
  const testRefs = union.made[at]?.test ?? 0;
133
144
  const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
134
145
  (union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
@@ -148,6 +159,8 @@ export function sweepMatrix(matrix, input) {
148
159
  configurations: held,
149
160
  heldByTests,
150
161
  unreferencedTest: unreferenced,
162
+ testUsed: testUsed[at] === true,
163
+ keptByTests: kept[at] === true,
151
164
  });
152
165
  });
153
166
  const liveUnder = liveAnywhere(union.symbols, per);
@@ -157,6 +170,65 @@ export function sweepMatrix(matrix, input) {
157
170
  components: deadComponents(matrix, { liveUnder, candidates }, new Set()),
158
171
  };
159
172
  }
173
+ /**
174
+ * Every declaration of `dead`, those dead in every configuration that holds them, that a
175
+ * configuration's graph references from a judged declaration outside `dead` or already
176
+ * kept, until none joins: deleting one would break that configuration's compile. A
177
+ * production run counts no reference a test file made.
178
+ */
179
+ function compiledReferences(matrix, dead, mode) {
180
+ const kept = new Set();
181
+ for (let changed = true; changed;) {
182
+ changed = false;
183
+ for (const graph of matrix.graphs) {
184
+ graph.out.forEach((edges, from) => {
185
+ const id = graph.symbols[from]?.id ?? "";
186
+ if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
187
+ return;
188
+ }
189
+ for (const edge of edges) {
190
+ const to = graph.symbols[edge.to]?.id ?? "";
191
+ if ((!mode.production || !edge.test) && dead.has(to) && !kept.has(to)) {
192
+ kept.add(to);
193
+ changed = true;
194
+ }
195
+ }
196
+ });
197
+ }
198
+ }
199
+ return kept;
200
+ }
201
+ /** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
202
+ function testUsedReach(union, seeds, dead) {
203
+ // A file is no candidate, and its top level is what reaches what it alone calls.
204
+ const reached = seeds.map((seed, at) => seed && dead[at] === true);
205
+ const queue = seeds.flatMap((one, at) => (one ? [at] : []));
206
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
207
+ for (const edge of union.out[at] ?? []) {
208
+ if (dead[edge.to] === true && reached[edge.to] !== true) {
209
+ reached[edge.to] = true;
210
+ queue.push(edge.to);
211
+ }
212
+ }
213
+ }
214
+ return reached;
215
+ }
216
+ /** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
217
+ function keptByTests(union, reached, admitted) {
218
+ const kept = union.symbols.map(() => false);
219
+ union.out.forEach((edges, from) => {
220
+ const testCode = union.test[from] === true || union.support[from] === true;
221
+ if (!testCode || reached[from] !== true || admitted(from)) {
222
+ return;
223
+ }
224
+ for (const edge of edges) {
225
+ if (edge.test) {
226
+ kept[edge.to] = true;
227
+ }
228
+ }
229
+ });
230
+ return kept;
231
+ }
160
232
  /**
161
233
  * The dead components of one sweep over the union, with the file declarations at `files`
162
234
  * dead as well: a file that is deleted whole takes the declarations it holds and every
@@ -5,8 +5,7 @@
5
5
  */
6
6
  import { resolveRelativeSpecifier } from "./configuration-files.js";
7
7
  import { dirnamePath, joinPath, resolvePath } from "./paths.js";
8
- /** The directories no document is read below. */
9
- const SKIPPED = new Set(["node_modules", ".git"]);
8
+ import { filesBelow } from "./fs/tree.js";
10
9
  /** The extensions of a document whose imports an MDX compiler reads. */
11
10
  const DOCUMENT = /\.mdx?$/u;
12
11
  /** A fence line, its marker run and what follows it captured. */
@@ -100,29 +99,6 @@ function clauseNames(clause) {
100
99
  }
101
100
  return names;
102
101
  }
103
- /** Every document below `dir`, outside every skipped directory, by absolute path. */
104
- function documents(host, dir) {
105
- let entries;
106
- try {
107
- entries = host.readDirectory(dir);
108
- }
109
- catch {
110
- return [];
111
- }
112
- const found = [];
113
- for (const entry of entries) {
114
- const path = joinPath(dir, entry.name);
115
- if (entry.directory) {
116
- if (!SKIPPED.has(entry.name)) {
117
- found.push(...documents(host, path));
118
- }
119
- }
120
- else if (DOCUMENT.test(entry.name)) {
121
- found.push(path);
122
- }
123
- }
124
- return found.sort();
125
- }
126
102
  /** The relative specifiers one document's import lines name, its fenced code blocks skipped. */
127
103
  function importedBy(text) {
128
104
  const found = [];
@@ -165,7 +141,8 @@ function documentImports(host, root) {
165
141
  if (known !== undefined) {
166
142
  return known;
167
143
  }
168
- const found = documents(host, root).flatMap((path) => {
144
+ const found = filesBelow(host, root, (name) => DOCUMENT.test(name)).flatMap((below) => {
145
+ const path = joinPath(root, below);
169
146
  try {
170
147
  return [{ path, imports: importedBy(host.readFile(path)) }];
171
148
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { isCallExpression, isExportDeclaration, isImportDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
10
10
  import { RESOLVED_EXTENSIONS } from "./configuration-files.js";
11
- import { emittedFrom } from "./emit-map.js";
11
+ import { emittedFrom } from "./load/emit-map.js";
12
12
  import { namesImported } from "./module-declarations.js";
13
13
  import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.js";
14
14
  import { renderPosition } from "./position.js";
@@ -1,4 +1,5 @@
1
1
  import { relativePath } from "./paths.js";
2
+ import { bytewise } from "./text/compare.js";
2
3
  /** A count with its noun, so a line reads for one as well as for several. */
3
4
  function counted(n, one, many) {
4
5
  return `${String(n)} ${n === 1 ? one : many}`;
@@ -10,7 +11,7 @@ function counted(n, one, many) {
10
11
  */
11
12
  export function partialNotes(partial) {
12
13
  const notes = [...partial.notBuilt]
13
- .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
14
+ .sort((a, b) => bytewise(a.id, b.id))
14
15
  .map((one) => `the derived configuration ${one.id} was not built and is not analyzed: ${one.error}`);
15
16
  if (partial.unanswered.length > 0) {
16
17
  const byProject = new Map();