@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
package/src/manifest.ts CHANGED
@@ -7,45 +7,47 @@
7
7
 
8
8
  import type { Host } from "./host.ts";
9
9
  import { isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
+ import { isRecord } from "./json-document.ts";
11
+ import { MANIFEST, manifestAt, readJSON } from "./fs/manifest.ts";
12
+ import { scriptTokens } from "./text/shell.ts";
10
13
 
11
14
  /** The manifest's own name. */
12
- const MANIFEST = "package.json";
13
15
 
14
16
  /** The three members that name an entry point beside `exports` and `bin`. */
15
17
  const LEGACY_MEMBERS = ["main", "module", "types"] as const;
16
18
 
17
- /** What reaches one entry point the manifest names. */
19
+ /** What reaches one entry the manifest names. */
18
20
  type EntryRole =
19
21
  /** A consumer importing the package, which is a caller the analysis cannot see. */
20
22
  | "import"
21
23
  /** A shell running the package's command, which is a caller that exists. */
22
24
  | "run"
23
- /** A token of one of the package's scripts, which the package manager runs. */
25
+ /** A token of one of the package's scripts, or the whole of a command string the package runs. */
24
26
  | "script";
25
27
 
26
- /** One entry point the manifest names. */
28
+ /** One entry point the manifest names, or the whole of one command string the package runs. */
27
29
  export interface ManifestEntry {
28
30
  /**
29
- * The member that named it, spelled as a reader of the manifest reads one:
30
- * `main`, `bin["deadset-ts"]`, `exports["."]["import"]`. It is what a maintainer
31
- * edits, so it is what a root reports as the string that named it.
31
+ * The member that named it as a maintainer edits it, `main`, `bin["deadset-ts"]` or
32
+ * `exports["."]["import"]`: what a root reports as the string that named it.
32
33
  */
33
34
  readonly member: string;
34
- /** The target the member carries, resolved against the manifest's directory. */
35
+ /** The target the member carries, resolved against the manifest's directory, or that directory for a whole command string. */
35
36
  readonly path: string;
36
37
  readonly role: EntryRole;
37
38
  /**
38
- * Whether the member declares what a consumer may import, which is `exports` and
39
- * the three members that preceded it. A command is published too, and is not this:
40
- * `exports` is the surface a library's published API is read from.
39
+ * Whether the member declares what a consumer may import, the surface a library's published
40
+ * API is read from: `exports`, `browser` and the three members that preceded `exports`.
41
41
  */
42
42
  readonly published: boolean;
43
43
  /** The directory of the manifest that names it, which `path` was resolved against. */
44
44
  readonly dir: string;
45
- }
46
-
47
- function isRecord(value: unknown): value is Record<string, unknown> {
48
- return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ /**
46
+ * On the entry of a whole command string, which names no entry point and whose path is the
47
+ * manifest's directory, that string: rooting enters no file for it and holds each file
48
+ * `spelledFiles` reads it to name.
49
+ */
50
+ readonly command?: string;
49
51
  }
50
52
 
51
53
  /**
@@ -60,35 +62,45 @@ function isTarget(value: string, requireDot: boolean): boolean {
60
62
  return !requireDot || value.startsWith("./");
61
63
  }
62
64
 
63
- /** Where a script's command line splits into tokens: white space and the shell's operators. */
64
- const SCRIPT_SEPARATORS = /\s+|&&|\|\||;|\|/u;
65
-
66
- /** A token written inside one pair of quotes. */
67
- const QUOTED = /^(["'])(.*)\1$/su;
68
-
69
- /**
70
- * Every token one script's command line holds, each with one pair of enclosing quotes
71
- * removed. A token holding a wildcard is a pattern rather than a path, and names no
72
- * entry point.
73
- */
74
- export function scriptTokens(command: string): readonly string[] {
75
- return command
76
- .split(SCRIPT_SEPARATORS)
77
- .map((token) => token.replace(QUOTED, "$2"))
78
- .filter((token) => token !== "" && !token.includes("*"));
79
- }
80
-
81
65
  /** One member's spelling below another: `exports` then `exports["."]`. */
82
66
  function below(member: string, key: string): string {
83
67
  return `${member}[${JSON.stringify(key)}]`;
84
68
  }
85
69
 
86
70
  /**
87
- * Every target one `exports` value carries, at the member each is written under.
88
- *
89
- * The value nests strings, subpath or condition objects and alternative arrays to
90
- * any depth; a `null` blocks a subpath. Every level is walked, because each
91
- * condition names its own entry file, and a key's meaning is never read.
71
+ * The command strings one manifest runs, each at the member it is written under: each of its
72
+ * `scripts`, and the `exec` command of nodemon's configuration, in the manifest's `nodemonConfig`
73
+ * or the `nodemon.json` beside it, which nodemon runs the watched script with:
74
+ * https://github.com/remy/nodemon#config-files
75
+ */
76
+ export function manifestCommands(
77
+ host: Host,
78
+ dir: string,
79
+ manifest: Record<string, unknown>,
80
+ ): readonly { readonly member: string; readonly command: string }[] {
81
+ const scripts = manifest["scripts"];
82
+ const nodemon = {
83
+ nodemonConfig: manifest["nodemonConfig"],
84
+ "nodemon.json": readJSON(host, joinPath(dir, "nodemon.json")),
85
+ };
86
+ const written = [
87
+ ...Object.entries(isRecord(scripts) ? scripts : {}).map(
88
+ ([name, value]) => [below("scripts", name), value] as const,
89
+ ),
90
+ ...Object.entries(nodemon).map(
91
+ ([member, config]) =>
92
+ [below(member, "exec"), isRecord(config) ? config["exec"] : undefined] as const,
93
+ ),
94
+ ];
95
+ return written.flatMap(([member, command]) =>
96
+ typeof command === "string" ? [{ member, command }] : [],
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Every target one `exports` value carries, at the member each is written under. Every level of
102
+ * strings, subpath or condition objects and alternative arrays is walked, because each condition
103
+ * names its own entry file; a `null` blocks a subpath, and a key's meaning is never read.
92
104
  */
93
105
  function exportTargets(value: unknown, member: string, found: Omit<ManifestEntry, "dir">[]): void {
94
106
  if (typeof value === "string") {
@@ -111,41 +123,18 @@ function exportTargets(value: unknown, member: string, found: Omit<ManifestEntry
111
123
  }
112
124
  }
113
125
 
114
- /** The manifest at the target root, decoded, or undefined where there is none to read. */
115
- function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | undefined {
116
- let text: string;
117
- try {
118
- text = host.readFile(joinPath(targetRoot, MANIFEST));
119
- } catch {
120
- return undefined;
121
- }
122
- let value: unknown;
123
- try {
124
- value = JSON.parse(text);
125
- } catch {
126
- // A manifest this program cannot read names no entry point, which is the same
127
- // answer as an absent one. What a manifest's contents say is not refused here:
128
- // the package manager owns that refusal, and an analysis of a tree it rejects
129
- // is not the run's subject.
130
- return undefined;
131
- }
132
- return isRecord(value) ? value : undefined;
133
- }
134
-
135
126
  /** What one manifest declares about where its package is entered. */
136
127
  export interface Manifest {
137
128
  /**
138
- * Every entry point the manifest names, in the order the members are read: `main`,
139
- * `module`, `types`, then each `bin` command, then every target of `exports`, then
140
- * every token of each script, which names a file only where one is at its path. Each
141
- * path is absolute.
129
+ * Every entry the manifest names, by absolute path, in the order read: the entry points `main`,
130
+ * `module`, `types`, `browser` and each file it maps, each `bin` command, every `exports` target
131
+ * and per script each token, which names a file only where one is at its path; and per command
132
+ * string {@link manifestCommands} reads one entry of its whole text, which is no entry point.
142
133
  */
143
134
  readonly entries: readonly ManifestEntry[];
144
135
  /**
145
- * Whether an `exports` member is written at all. A package that declares one has
146
- * said exactly what a consumer may import, so its published API is what `exports`
147
- * reaches; a package that declares none publishes whatever the members that
148
- * preceded it name.
136
+ * Whether an `exports` member is written: then the published API is what `exports` reaches,
137
+ * and otherwise whatever the members that preceded it name.
149
138
  */
150
139
  readonly declaresExports: boolean;
151
140
  }
@@ -158,7 +147,7 @@ export interface Manifest {
158
147
  * it and there is one such configuration per project, while there is one manifest.
159
148
  */
160
149
  export function readManifest(host: Host, targetRoot: string): Manifest {
161
- const manifest = manifestOf(host, targetRoot);
150
+ const manifest = manifestAt(host, targetRoot);
162
151
  if (manifest === undefined) {
163
152
  return { entries: [], declaresExports: false };
164
153
  }
@@ -171,6 +160,24 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
171
160
  }
172
161
  }
173
162
 
163
+ // A `browser` string replaces `main` in a browser bundle, and an object replaces each
164
+ // module it maps with the file it names: https://github.com/defunctzombie/package-browser-field-spec
165
+ const browser = manifest["browser"];
166
+ if (typeof browser === "string" && isTarget(browser, false)) {
167
+ found.push({ member: "browser", path: browser, role: "import", published: true });
168
+ } else if (isRecord(browser)) {
169
+ for (const [replaced, value] of Object.entries(browser)) {
170
+ if (typeof value === "string" && isTarget(value, true)) {
171
+ found.push({
172
+ member: below("browser", replaced),
173
+ path: value,
174
+ role: "import",
175
+ published: true,
176
+ });
177
+ }
178
+ }
179
+ }
180
+
174
181
  const bin = manifest["bin"];
175
182
  if (typeof bin === "string" && isTarget(bin, false)) {
176
183
  found.push({ member: "bin", path: bin, role: "run", published: false });
@@ -190,16 +197,15 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
190
197
  if (typeof command !== "string") {
191
198
  continue;
192
199
  }
193
- for (const token of new Set(scriptTokens(command))) {
194
- found.push({
195
- member: below("scripts", name),
196
- path: token,
197
- role: "script",
198
- published: false,
199
- });
200
+ const member = below("scripts", name);
201
+ for (const path of new Set(scriptTokens(command))) {
202
+ found.push({ member, path, role: "script", published: false });
200
203
  }
201
204
  }
202
205
  }
206
+ for (const { member, command } of manifestCommands(host, targetRoot, manifest)) {
207
+ found.push({ member, path: ".", role: "script", published: false, command });
208
+ }
203
209
 
204
210
  return {
205
211
  entries: found.map((entry) => ({
package/src/matrix.ts CHANGED
@@ -30,6 +30,8 @@ export interface Configured {
30
30
  readonly testFiles: readonly string[];
31
31
  /** The paths of the test-support files, which only test code imports. */
32
32
  readonly supportFiles?: readonly string[] | undefined;
33
+ /** The paths of the files only tests reach that are no test-support file. */
34
+ readonly testUsedFiles?: readonly string[] | undefined;
33
35
  /** The ids of the ambient declarations, which describe code the target does not implement. */
34
36
  readonly ambient?: ReadonlySet<string> | undefined;
35
37
  /**
@@ -60,6 +62,11 @@ export interface Matrix {
60
62
  * whose project holds it, in the run's order.
61
63
  */
62
64
  readonly heldIn: readonly (readonly string[])[];
65
+ /**
66
+ * Per declaration of {@link Matrix.union}, whether a file only tests reach that is no
67
+ * test-support file holds it in some configuration: production code only tests use.
68
+ */
69
+ readonly testUsed: readonly boolean[];
63
70
  /** Each declaration's dependents, by id, as the first configuration holding it states them. */
64
71
  readonly dependents: ReadonlyMap<string, readonly string[]>;
65
72
  }
@@ -81,6 +88,17 @@ export interface MatrixCandidate extends Candidate {
81
88
  * shape nothing references would be.
82
89
  */
83
90
  readonly unreferencedTest: boolean;
91
+ /**
92
+ * Whether it is dead only because code only tests use is: production code only tests
93
+ * use holds it and the run that counts test references reaches it, or such a
94
+ * declaration reaches it through dead declarations.
95
+ */
96
+ readonly testUsed: boolean;
97
+ /**
98
+ * Whether test code the run that counts test references reaches, and that is no test of
99
+ * dead code, references it: a test that stays keeps it.
100
+ */
101
+ readonly keptByTests: boolean;
84
102
  }
85
103
 
86
104
  /** One run's sweep. */
@@ -158,6 +176,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
158
176
  }
159
177
  });
160
178
  symbols.sort((a, b) => byPosition(a.position, b.position));
179
+ const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
161
180
 
162
181
  return {
163
182
  configurations: per.map((one) => one.configuration),
@@ -173,6 +192,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
173
192
  new Set(per.flatMap((one) => [...(one.ambient ?? [])])),
174
193
  ),
175
194
  heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
195
+ testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
176
196
  dependents,
177
197
  };
178
198
  }
@@ -203,18 +223,32 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
203
223
 
204
224
  const union = matrix.union;
205
225
  const heldBy: (string | undefined)[] = union.symbols.map(() => undefined);
206
- const testReached = reachedWithTheTests(union, [
226
+ const held = [
207
227
  ...input.marked,
208
228
  ...(input.exempt ?? []).map((record) => record.id),
209
229
  ...(input.unanswered ?? []),
210
- ]);
211
- const judged = union.symbols.map((symbol, at): readonly Candidate[] | undefined => {
230
+ ];
231
+ const testReached = reachedWithTheTests(union, held);
232
+ // A test uses what it names: a file a test reads through a star re-export or a
233
+ // namespace keeps the exports nothing names at their own class.
234
+ const testNamed = reachedWithTheTests(union, held, false);
235
+ const everywhere = union.symbols.map((symbol, at): readonly Candidate[] | undefined => {
212
236
  const held = matrix.heldIn[at] ?? [];
213
237
  const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
214
238
  return held.length === 0 || found.some((candidate) => candidate === undefined)
215
239
  ? undefined
216
240
  : found.filter((candidate): candidate is Candidate => candidate !== undefined);
217
241
  });
242
+ const compiled = compiledReferences(
243
+ matrix,
244
+ new Set(
245
+ union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id])),
246
+ ),
247
+ input.mode,
248
+ );
249
+ const judged = everywhere.map((found, at) =>
250
+ compiled.has(union.symbols[at]?.id ?? "") ? undefined : found,
251
+ );
218
252
  // A configuration that does not hold a test's subject's caller finds the subject dead,
219
253
  // so a test is of dead code only where every production subject it names is dead in
220
254
  // every configuration that holds it.
@@ -227,6 +261,14 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
227
261
  union.subject[edge.to] !== true ||
228
262
  judged[edge.to] !== undefined,
229
263
  );
264
+ const admittedAt = (at: number): boolean =>
265
+ judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
266
+ const kept = keptByTests(union, testReached, admittedAt);
267
+ const testUsed = testUsedReach(
268
+ union,
269
+ union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true),
270
+ judged.map((one) => one !== undefined),
271
+ );
230
272
  const candidates: MatrixCandidate[] = [];
231
273
  union.symbols.forEach((symbol, at) => {
232
274
  const all = judged[at];
@@ -234,7 +276,7 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
234
276
  return;
235
277
  }
236
278
  const held = matrix.heldIn[at] ?? [];
237
- const admitted = all.every((candidate) => candidate.testOfDeadCode) && subjectsDead(at);
279
+ const admitted = admittedAt(at);
238
280
  const testRefs = union.made[at]?.test ?? 0;
239
281
  const heldByTests =
240
282
  heldBy[union.parent[at] ?? OUTSIDE] ??
@@ -255,6 +297,8 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
255
297
  configurations: held,
256
298
  heldByTests,
257
299
  unreferencedTest: unreferenced,
300
+ testUsed: testUsed[at] === true,
301
+ keptByTests: kept[at] === true,
258
302
  });
259
303
  });
260
304
 
@@ -266,6 +310,80 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
266
310
  };
267
311
  }
268
312
 
313
+ /**
314
+ * Every declaration of `dead`, those dead in every configuration that holds them, that a
315
+ * configuration's graph references from a judged declaration outside `dead` or already
316
+ * kept, until none joins: deleting one would break that configuration's compile. A
317
+ * production run counts no reference a test file made.
318
+ */
319
+ function compiledReferences(
320
+ matrix: Matrix,
321
+ dead: ReadonlySet<string>,
322
+ mode: SweepInput["mode"],
323
+ ): ReadonlySet<string> {
324
+ const kept = new Set<string>();
325
+ for (let changed = true; changed;) {
326
+ changed = false;
327
+ for (const graph of matrix.graphs) {
328
+ graph.out.forEach((edges, from) => {
329
+ const id = graph.symbols[from]?.id ?? "";
330
+ if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
331
+ return;
332
+ }
333
+ for (const edge of edges) {
334
+ const to = graph.symbols[edge.to]?.id ?? "";
335
+ if ((!mode.production || !edge.test) && dead.has(to) && !kept.has(to)) {
336
+ kept.add(to);
337
+ changed = true;
338
+ }
339
+ }
340
+ });
341
+ }
342
+ }
343
+ return kept;
344
+ }
345
+
346
+ /** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
347
+ function testUsedReach(
348
+ union: Graph,
349
+ seeds: readonly boolean[],
350
+ dead: readonly boolean[],
351
+ ): readonly boolean[] {
352
+ // A file is no candidate, and its top level is what reaches what it alone calls.
353
+ const reached = seeds.map((seed, at) => seed && dead[at] === true);
354
+ const queue = seeds.flatMap((one, at) => (one ? [at] : []));
355
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
356
+ for (const edge of union.out[at] ?? []) {
357
+ if (dead[edge.to] === true && reached[edge.to] !== true) {
358
+ reached[edge.to] = true;
359
+ queue.push(edge.to);
360
+ }
361
+ }
362
+ }
363
+ return reached;
364
+ }
365
+
366
+ /** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
367
+ function keptByTests(
368
+ union: Graph,
369
+ reached: readonly boolean[],
370
+ admitted: (at: number) => boolean,
371
+ ): readonly boolean[] {
372
+ const kept = union.symbols.map(() => false);
373
+ union.out.forEach((edges, from) => {
374
+ const testCode = union.test[from] === true || union.support[from] === true;
375
+ if (!testCode || reached[from] !== true || admitted(from)) {
376
+ return;
377
+ }
378
+ for (const edge of edges) {
379
+ if (edge.test) {
380
+ kept[edge.to] = true;
381
+ }
382
+ }
383
+ });
384
+ return kept;
385
+ }
386
+
269
387
  /**
270
388
  * The dead components of one sweep over the union, with the file declarations at `files`
271
389
  * dead as well: a file that is deleted whole takes the declarations it holds and every
@@ -9,6 +9,7 @@ import { resolveRelativeSpecifier } from "./configuration-files.ts";
9
9
  import type { Host } from "./host.ts";
10
10
  import { dirnamePath, joinPath, resolvePath } from "./paths.ts";
11
11
  import type { SourceFiles } from "./source-files.ts";
12
+ import { filesBelow } from "./fs/tree.ts";
12
13
 
13
14
  /** One file a document imports, the specifier that named it, and the export names it reads. */
14
15
  interface DocumentEntry {
@@ -18,9 +19,6 @@ interface DocumentEntry {
18
19
  readonly names: readonly string[] | undefined;
19
20
  }
20
21
 
21
- /** The directories no document is read below. */
22
- const SKIPPED: ReadonlySet<string> = new Set(["node_modules", ".git"]);
23
-
24
22
  /** The extensions of a document whose imports an MDX compiler reads. */
25
23
  const DOCUMENT = /\.mdx?$/u;
26
24
 
@@ -129,28 +127,6 @@ function clauseNames(clause: string): readonly string[] | undefined {
129
127
  return names;
130
128
  }
131
129
 
132
- /** Every document below `dir`, outside every skipped directory, by absolute path. */
133
- function documents(host: Host, dir: string): readonly string[] {
134
- let entries;
135
- try {
136
- entries = host.readDirectory(dir);
137
- } catch {
138
- return [];
139
- }
140
- const found: string[] = [];
141
- for (const entry of entries) {
142
- const path = joinPath(dir, entry.name);
143
- if (entry.directory) {
144
- if (!SKIPPED.has(entry.name)) {
145
- found.push(...documents(host, path));
146
- }
147
- } else if (DOCUMENT.test(entry.name)) {
148
- found.push(path);
149
- }
150
- }
151
- return found.sort();
152
- }
153
-
154
130
  /** The relative specifiers one document's import lines name, its fenced code blocks skipped. */
155
131
  function importedBy(
156
132
  text: string,
@@ -205,7 +181,8 @@ function documentImports(host: Host, root: string): readonly DocumentImports[] {
205
181
  if (known !== undefined) {
206
182
  return known;
207
183
  }
208
- const found = documents(host, root).flatMap((path) => {
184
+ const found = filesBelow(host, root, (name) => DOCUMENT.test(name)).flatMap((below) => {
185
+ const path = joinPath(root, below);
209
186
  try {
210
187
  return [{ path, imports: importedBy(host.readFile(path)) }];
211
188
  } catch {
@@ -20,7 +20,7 @@ import {
20
20
  type SourceFile,
21
21
  } from "@typescript/native/ast";
22
22
  import { RESOLVED_EXTENSIONS, type ParseFile } from "./configuration-files.ts";
23
- import { emittedFrom } from "./emit-map.ts";
23
+ import { emittedFrom } from "./load/emit-map.ts";
24
24
  import type { Host } from "./host.ts";
25
25
  import type { Inventory } from "./inventory.ts";
26
26
  import { namesImported } from "./module-declarations.ts";
package/src/partial.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { NotBuilt } from "./discover.ts";
1
+ import type { NotBuilt } from "./load/discover.ts";
2
2
  import { relativePath } from "./paths.ts";
3
3
  import type { UnansweredQuestion } from "./query.ts";
4
+ import { bytewise } from "./text/compare.ts";
4
5
 
5
6
  /** What a run read only in part. */
6
7
  interface Partial {
@@ -24,7 +25,7 @@ function counted(n: number, one: string, many: string): string {
24
25
  */
25
26
  export function partialNotes(partial: Partial): readonly string[] {
26
27
  const notes = [...partial.notBuilt]
27
- .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
28
+ .sort((a, b) => bytewise(a.id, b.id))
28
29
  .map(
29
30
  (one) =>
30
31
  `the derived configuration ${one.id} was not built and is not analyzed: ${one.error}`,
package/src/paths.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  /**
2
- * Filesystem paths, as the supported platform spells them: a solidus separates
3
- * segments and a leading solidus makes a path absolute.
4
- *
5
- * They are written here rather than taken from the platform's own library because
6
- * every module below the command line stays free of the platform, and because the
7
- * supported platform set is one, so a second spelling would be a rule nothing
8
- * exercises.
2
+ * Filesystem paths as the supported platform spells them: a solidus between segments, a
3
+ * leading one for an absolute path. They are written here rather than taken from the
4
+ * platform's library so every module below the command line stays free of the platform.
9
5
  */
10
6
 
11
7
  const SEPARATOR = "/";
@@ -67,6 +63,25 @@ export function dirnamePath(path: string): string {
67
63
  return cut === 0 ? SEPARATOR : normalized.slice(0, cut);
68
64
  }
69
65
 
66
+ /**
67
+ * `dir` and every directory above it, nearest first, normalized. With `top`, the list ends
68
+ * at `top` and is empty where `dir` is neither `top` nor below it; without, it ends at the
69
+ * root of the path.
70
+ */
71
+ export function ancestors(dir: string, top?: string): readonly string[] {
72
+ const end = top === undefined ? undefined : normalizePath(top);
73
+ const found: string[] = [];
74
+ for (let at = normalizePath(dir); ; at = dirnamePath(at)) {
75
+ if (end !== undefined && at !== end && relativePath(end, at) === undefined) {
76
+ return [];
77
+ }
78
+ found.push(at);
79
+ if (at === end || dirnamePath(at) === at) {
80
+ return found;
81
+ }
82
+ }
83
+ }
84
+
70
85
  /**
71
86
  * The extension of one path's last segment, from its last full stop, or the empty string
72
87
  * where it has none. A segment that only starts with a full stop has none.
@@ -101,14 +116,11 @@ export function queriedModulePath(specifier: string): string | undefined {
101
116
  }
102
117
 
103
118
  /**
104
- * One path as it reads below a root, or undefined when it is not below the root.
105
- * Both are normalized first, so a root written with a trailing separator or a `.`
106
- * segment reaches the same answer, and the comparison is then lexical, so a root
107
- * and a path spelled through different symbolic links name nothing in common. The
108
- * root itself is not below itself.
109
- *
110
- * The caller resolves both against the same directory before calling: a relative
111
- * root and an absolute path are two spellings this cannot reconcile.
119
+ * One path as it reads below a root, or undefined when it is not strictly below the root:
120
+ * the root itself is not below itself. Both are normalized, then compared lexically, so a
121
+ * root and a path spelled through different symbolic links name nothing in common. The
122
+ * caller resolves both against the same directory first: a relative root and an absolute
123
+ * path cannot be reconciled.
112
124
  */
113
125
  export function relativePath(root: string, path: string): string | undefined {
114
126
  const at = normalizePath(root);
package/src/position.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { SourceFile } from "@typescript/native/ast";
2
2
  import { relativePath } from "./paths.ts";
3
+ import { bytewise } from "./text/compare.ts";
3
4
 
4
5
  /**
5
6
  * Where one declaration is written, as every report renders it.
@@ -87,7 +88,7 @@ export function positionKey(position: Position): string {
87
88
  */
88
89
  export function byPosition(a: Position, b: Position): number {
89
90
  if (a.path !== b.path) {
90
- return a.path < b.path ? -1 : 1;
91
+ return bytewise(a.path, b.path);
91
92
  }
92
93
  if (a.line !== b.line) {
93
94
  return a.line - b.line;