@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.8

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 (193) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +24 -5
  3. package/dist/src/analysis.js +37 -56
  4. package/dist/src/automatic-types.js +10 -29
  5. package/dist/src/build-scripts.js +17 -29
  6. package/dist/src/component-layer.js +4 -2
  7. package/dist/src/components.js +2 -3
  8. package/dist/src/config.js +6 -82
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/configured-declarations.js +15 -20
  11. package/dist/src/consumers.js +2 -2
  12. package/dist/src/contract/config.generated.js +544 -0
  13. package/dist/src/contract/contract.generated.js +13 -0
  14. package/dist/src/contract/report.generated.js +3 -2
  15. package/dist/src/conventions.js +20 -60
  16. package/dist/src/dependencies.js +25 -43
  17. package/dist/src/discover.js +14 -40
  18. package/dist/src/edges.js +38 -122
  19. package/dist/src/element-access.js +4 -6
  20. package/dist/src/entry-point-gaps.js +2 -8
  21. package/dist/src/entry-points.js +3 -10
  22. package/dist/src/enum-group.js +3 -10
  23. package/dist/src/erased-stores.js +4 -6
  24. package/dist/src/exempt-classes.js +0 -4
  25. package/dist/src/exempt.js +6 -22
  26. package/dist/src/file-facts.js +2 -25
  27. package/dist/src/findings/completion.js +5 -4
  28. package/dist/src/findings/emitters.js +1 -1
  29. package/dist/src/findings/intra-function.js +1 -1
  30. package/dist/src/findings/non-code-artifacts.js +2 -2
  31. package/dist/src/findings/reads-and-writes.js +2 -1
  32. package/dist/src/findings/self-check.js +3 -2
  33. package/dist/src/findings/unused-declarations.js +1 -1
  34. package/dist/src/findings/visibility-narrowing.js +2 -2
  35. package/dist/src/findings-pass.js +7 -12
  36. package/dist/src/framework-lifecycle.js +7 -7
  37. package/dist/src/fs/document.js +59 -0
  38. package/dist/src/fs/manifest.js +43 -0
  39. package/dist/src/fs/tree.js +88 -0
  40. package/dist/src/git-ignore.js +34 -25
  41. package/dist/src/html-entries.js +6 -33
  42. package/dist/src/in-operands.js +2 -1
  43. package/dist/src/injection-container.js +1 -1
  44. package/dist/src/intra-function-parts.js +10 -13
  45. package/dist/src/inventory.js +2 -21
  46. package/dist/src/json-document.js +198 -67
  47. package/dist/src/keyof-comparisons.js +4 -6
  48. package/dist/src/kinds.js +0 -7
  49. package/dist/src/manifest.js +5 -43
  50. package/dist/src/mdx-entries.js +3 -26
  51. package/dist/src/partial.js +2 -1
  52. package/dist/src/paths.js +26 -15
  53. package/dist/src/position.js +2 -1
  54. package/dist/src/print.js +11 -79
  55. package/dist/src/product.js +13 -0
  56. package/dist/src/references.js +14 -27
  57. package/dist/src/report-schema.js +3 -225
  58. package/dist/src/report.js +22 -31
  59. package/dist/src/reporters/sarif.js +4 -2
  60. package/dist/src/reporters/template.js +4 -19
  61. package/dist/src/resolve.js +245 -724
  62. package/dist/src/roots.js +7 -17
  63. package/dist/src/run.js +55 -61
  64. package/dist/src/schema.js +320 -343
  65. package/dist/src/scope.js +34 -103
  66. package/dist/src/serialization-contract.js +5 -9
  67. package/dist/src/session.js +2 -1
  68. package/dist/src/suppress-file.js +21 -25
  69. package/dist/src/suppress.js +3 -3
  70. package/dist/src/symbol-keys.js +3 -5
  71. package/dist/src/template-field.js +9 -19
  72. package/dist/src/test-globals.js +6 -28
  73. package/dist/src/test-joins.js +6 -6
  74. package/dist/src/text/compare.js +26 -0
  75. package/dist/src/text/glob.js +194 -0
  76. package/dist/src/text/shell.js +159 -0
  77. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  78. package/dist/src/type-errors.js +14 -38
  79. package/dist/src/unseen-loads.js +2 -9
  80. package/dist/src/unwrap.js +23 -0
  81. package/dist/src/usage.js +10 -0
  82. package/dist/src/verbs/analyze.js +25 -26
  83. package/dist/src/verbs/describe.js +4 -4
  84. package/dist/src/verbs/explain.js +8 -6
  85. package/dist/src/verbs/print-projects.js +4 -3
  86. package/dist/src/verbs/print-retained.js +2 -1
  87. package/dist/src/verbs/print-roots.js +2 -1
  88. package/dist/src/verbs/version.js +2 -1
  89. package/dist/src/workflow-steps.js +9 -46
  90. package/dist/src/workspace-manifests.js +2 -2
  91. package/dist/src/workspace-programs.js +5 -3
  92. package/dist/src/workspace-resolution.js +13 -17
  93. package/dist/src/workspace.js +28 -116
  94. package/jsr.json +1 -1
  95. package/package.json +1 -1
  96. package/src/analysis.ts +55 -76
  97. package/src/automatic-types.ts +11 -29
  98. package/src/build-scripts.ts +17 -38
  99. package/src/component-layer.ts +4 -2
  100. package/src/components.ts +2 -4
  101. package/src/config.ts +31 -296
  102. package/src/configuration-files.ts +27 -170
  103. package/src/configured-declarations.ts +18 -23
  104. package/src/consumers.ts +4 -4
  105. package/src/contract/config.generated.ts +798 -0
  106. package/src/contract/contract.generated.ts +25 -0
  107. package/src/contract/report.generated.ts +3 -2
  108. package/src/conventions.ts +26 -74
  109. package/src/dependencies.ts +32 -50
  110. package/src/discover.ts +15 -40
  111. package/src/edges.ts +41 -140
  112. package/src/element-access.ts +4 -7
  113. package/src/entry-point-gaps.ts +3 -9
  114. package/src/entry-points.ts +2 -13
  115. package/src/enum-group.ts +2 -11
  116. package/src/erased-stores.ts +3 -7
  117. package/src/exempt-classes.ts +0 -5
  118. package/src/exempt.ts +8 -30
  119. package/src/file-facts.ts +6 -24
  120. package/src/findings/completion.ts +5 -6
  121. package/src/findings/emitter.ts +2 -2
  122. package/src/findings/emitters.ts +1 -1
  123. package/src/findings/intra-function.ts +1 -1
  124. package/src/findings/non-code-artifacts.ts +4 -4
  125. package/src/findings/reads-and-writes.ts +2 -1
  126. package/src/findings/self-check.ts +3 -2
  127. package/src/findings/unused-declarations.ts +1 -1
  128. package/src/findings/visibility-narrowing.ts +2 -2
  129. package/src/findings-pass.ts +9 -15
  130. package/src/framework-lifecycle.ts +7 -7
  131. package/src/fs/document.ts +74 -0
  132. package/src/fs/manifest.ts +52 -0
  133. package/src/fs/tree.ts +110 -0
  134. package/src/git-ignore.ts +38 -25
  135. package/src/host.ts +12 -5
  136. package/src/html-entries.ts +6 -33
  137. package/src/in-operands.ts +2 -1
  138. package/src/injection-container.ts +1 -1
  139. package/src/intra-function-parts.ts +10 -14
  140. package/src/inventory.ts +2 -20
  141. package/src/json-document.ts +241 -76
  142. package/src/keyof-comparisons.ts +3 -7
  143. package/src/kinds.ts +0 -8
  144. package/src/manifest.ts +5 -46
  145. package/src/mdx-entries.ts +3 -26
  146. package/src/partial.ts +2 -1
  147. package/src/paths.ts +27 -15
  148. package/src/position.ts +2 -1
  149. package/src/print.ts +12 -92
  150. package/src/product.ts +18 -0
  151. package/src/references.ts +14 -29
  152. package/src/report-schema.ts +3 -301
  153. package/src/report.ts +24 -34
  154. package/src/reporters/sarif.ts +4 -4
  155. package/src/reporters/template.ts +4 -20
  156. package/src/resolve.ts +283 -1114
  157. package/src/roots.ts +7 -20
  158. package/src/run.ts +57 -75
  159. package/src/schema.ts +391 -388
  160. package/src/scope.ts +40 -126
  161. package/src/serialization-contract.ts +5 -10
  162. package/src/session.ts +2 -1
  163. package/src/suppress-file.ts +21 -29
  164. package/src/suppress.ts +3 -3
  165. package/src/symbol-keys.ts +2 -5
  166. package/src/template-field.ts +15 -25
  167. package/src/test-globals.ts +9 -27
  168. package/src/test-joins.ts +6 -8
  169. package/src/text/compare.ts +27 -0
  170. package/src/text/glob.ts +210 -0
  171. package/src/text/shell.ts +161 -0
  172. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  173. package/src/type-errors.ts +25 -36
  174. package/src/unseen-loads.ts +1 -12
  175. package/src/unwrap.ts +34 -0
  176. package/src/usage.ts +10 -0
  177. package/src/verbs/analyze.ts +32 -42
  178. package/src/verbs/describe.ts +4 -4
  179. package/src/verbs/explain.ts +8 -10
  180. package/src/verbs/print-projects.ts +4 -3
  181. package/src/verbs/print-retained.ts +2 -1
  182. package/src/verbs/print-roots.ts +2 -1
  183. package/src/verbs/verb.ts +2 -2
  184. package/src/verbs/version.ts +2 -1
  185. package/src/workflow-steps.ts +9 -44
  186. package/src/workspace-manifests.ts +2 -2
  187. package/src/workspace-programs.ts +5 -3
  188. package/src/workspace-resolution.ts +21 -17
  189. package/src/workspace.ts +35 -121
  190. package/dist/src/glob.js +0 -65
  191. package/dist/src/pnpm-workspace.js +0 -120
  192. package/src/glob.ts +0 -67
  193. package/src/pnpm-workspace.ts +0 -135
@@ -28,6 +28,31 @@ export const FORMATS = [
28
28
  "template",
29
29
  ] as const satisfies readonly Format[];
30
30
 
31
+ /** Each analyzed language's row: its analyzer, the names that mark it and the directories it skips. */
32
+ export const LANGUAGE_ROWS = {
33
+ go: {
34
+ analyzer: "deadset-go",
35
+ markers: ["go.mod"],
36
+ skippedDirectories: ["node_modules", "testdata", "vendor", ".*", "_*"],
37
+ },
38
+ ts: {
39
+ analyzer: "deadset-ts",
40
+ markers: ["tsconfig*.json"],
41
+ skippedDirectories: ["node_modules", ".git"],
42
+ },
43
+ } as const satisfies Readonly<
44
+ Partial<
45
+ Record<
46
+ Language,
47
+ {
48
+ readonly analyzer: string;
49
+ readonly markers: readonly string[];
50
+ readonly skippedDirectories: readonly string[];
51
+ }
52
+ >
53
+ >
54
+ >;
55
+
31
56
  export const EXIT_CLEAN = 0;
32
57
  export const EXIT_FINDINGS = 1;
33
58
  export const EXIT_USAGE = 2;
@@ -589,7 +589,7 @@ export const REPORT_SCHEMAS = {
589
589
  additionalProperties: false,
590
590
  required: ["edge", "side", "symbol", "state"],
591
591
  properties: {
592
- edge: { type: "string", pattern: "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$" },
592
+ edge: { $ref: "finding.schema.json#/$defs/edge_identifier" },
593
593
  side: { type: "string", enum: ["provides", "used_by"] },
594
594
  symbol: { type: "string" },
595
595
  state: { type: "string", enum: ["live", "dead", "absent"] },
@@ -969,7 +969,7 @@ export const REPORT_SCHEMAS = {
969
969
  uniqueItems: true,
970
970
  items: { type: "string", minLength: 1 },
971
971
  },
972
- edge: { type: "string", pattern: "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$" },
972
+ edge: { $ref: "#/$defs/edge_identifier" },
973
973
  sides: {
974
974
  type: "array",
975
975
  minItems: 1,
@@ -1125,6 +1125,7 @@ export const REPORT_SCHEMAS = {
1125
1125
  },
1126
1126
  ],
1127
1127
  $defs: {
1128
+ edge_identifier: { type: "string", pattern: "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$" },
1128
1129
  position: {
1129
1130
  type: "object",
1130
1131
  additionalProperties: false,
@@ -8,7 +8,6 @@
8
8
 
9
9
  import {
10
10
  isArrayLiteralExpression,
11
- isAsExpression,
12
11
  isBinaryExpression,
13
12
  isCallExpression,
14
13
  isClassDeclaration,
@@ -22,27 +21,20 @@ import {
22
21
  isNamedExports,
23
22
  isNamespaceImport,
24
23
  isNoSubstitutionTemplateLiteral,
25
- isNonNullExpression,
26
24
  isNumericLiteral,
27
25
  isObjectBindingPattern,
28
26
  isObjectLiteralExpression,
29
- isParenthesizedExpression,
30
27
  isPropertyAccessExpression,
31
28
  isPropertyAssignment,
32
- isSatisfiesExpression,
33
29
  isShorthandPropertyAssignment,
34
30
  isSpreadAssignment,
35
31
  isStringLiteral,
36
32
  isVariableStatement,
37
33
  SyntaxKind,
38
- type AsExpression,
39
34
  type Identifier,
40
35
  type Node,
41
36
  type ObjectBindingPattern,
42
- type NonNullExpression,
43
37
  type ObjectLiteralExpression,
44
- type ParenthesizedExpression,
45
- type SatisfiesExpression,
46
38
  type SourceFile,
47
39
  } from "@typescript/native/ast";
48
40
  import { lineOf } from "./component-files.ts";
@@ -60,13 +52,25 @@ import {
60
52
  type TestFileReading,
61
53
  } from "./convention-rows.ts";
62
54
  import { documentStrings } from "./configuration-files.ts";
63
- import { globExpression } from "./glob.ts";
64
55
  import type { Host } from "./host.ts";
65
- import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
56
+ import {
57
+ ancestors,
58
+ dirnamePath,
59
+ joinPath,
60
+ normalizePath,
61
+ relativePath,
62
+ resolvePath,
63
+ } from "./paths.ts";
66
64
  import type { SetupFailure } from "./setup-failure.ts";
67
65
  import type { SourceFiles } from "./source-files.ts";
68
66
  import { satisfies } from "./version-range.ts";
69
- import { yamlDocument } from "./yaml-document.ts";
67
+ import { bytewise } from "./text/compare.ts";
68
+ import { isRecord } from "./json-document.ts";
69
+ import { unwrapped } from "./unwrap.ts";
70
+ import { filesIn } from "./fs/tree.ts";
71
+ import { INSTALLED_PACKAGES, MANIFEST, readJSON } from "./fs/manifest.ts";
72
+ import { globExpression } from "./text/glob.ts";
73
+ import { yamlDocument } from "./text/yaml.ts";
70
74
 
71
75
  /** One manifest the analysis reads. */
72
76
  export interface ConventionManifest {
@@ -171,18 +175,6 @@ const DEPENDENCY_SECTIONS = [
171
175
  "optionalDependencies",
172
176
  ] as const;
173
177
 
174
- function isRecord(value: unknown): value is Record<string, unknown> {
175
- return typeof value === "object" && value !== null && !Array.isArray(value);
176
- }
177
-
178
- function readJSON(host: Host, path: string): unknown {
179
- try {
180
- return JSON.parse(host.readFile(path));
181
- } catch {
182
- return undefined;
183
- }
184
- }
185
-
186
178
  function declares(
187
179
  manifest: unknown,
188
180
  name: string,
@@ -203,38 +195,15 @@ function declares(
203
195
  * none, or `undefined` where no `node_modules` holds it.
204
196
  */
205
197
  function installedVersion(host: Host, dir: string, name: string): string | undefined {
206
- for (let at = normalizePath(dir); ; at = dirnamePath(at)) {
207
- const path = joinPath(at, "node_modules", name, "package.json");
198
+ for (const at of ancestors(dir)) {
199
+ const path = joinPath(at, INSTALLED_PACKAGES, name, MANIFEST);
208
200
  if (host.kindOf(path) === "file") {
209
201
  const manifest = readJSON(host, path);
210
202
  const version = isRecord(manifest) ? manifest["version"] : undefined;
211
203
  return typeof version === "string" ? version : "";
212
204
  }
213
- if (dirnamePath(at) === at) {
214
- return undefined;
215
- }
216
- }
217
- }
218
-
219
- /** Whether one node wraps an expression without changing its value. */
220
- function isWrapper(
221
- node: Node,
222
- ): node is ParenthesizedExpression | AsExpression | SatisfiesExpression | NonNullExpression {
223
- return (
224
- isParenthesizedExpression(node) ||
225
- isAsExpression(node) ||
226
- isSatisfiesExpression(node) ||
227
- isNonNullExpression(node)
228
- );
229
- }
230
-
231
- /** The expression a wrapper that changes no value holds. */
232
- function unwrapped(node: Node): Node {
233
- let at = node;
234
- while (isWrapper(at)) {
235
- at = at.expression;
236
205
  }
237
- return at;
206
+ return undefined;
238
207
  }
239
208
 
240
209
  /** The key one property is written with, where it is written as a name or as literal text. */
@@ -823,16 +792,7 @@ function expand(
823
792
  /** The configuration files in one directory that one glob names, by name. */
824
793
  function configurationFiles(host: Host, dir: string, glob: string): readonly string[] {
825
794
  const expression = globExpression(glob);
826
- let entries;
827
- try {
828
- entries = host.readDirectory(dir);
829
- } catch {
830
- return [];
831
- }
832
- return entries
833
- .filter((entry) => !entry.directory && expression.test(entry.name))
834
- .map((entry) => entry.name)
835
- .sort();
795
+ return filesIn(host, dir, (name) => expression.test(name));
836
796
  }
837
797
 
838
798
  /** What one move resolved to: its directories, or the failure its property met. */
@@ -948,7 +908,7 @@ function shortNameUses(
948
908
  }
949
909
  }
950
910
  }
951
- return [...used].filter((name) => declared.has(name)).sort(compare);
911
+ return [...used].filter((name) => declared.has(name)).sort(bytewise);
952
912
  }
953
913
 
954
914
  /**
@@ -1008,7 +968,7 @@ function prefixedNameUses(
1008
968
  }
1009
969
  }
1010
970
  }
1011
- return [...used].sort(compare);
971
+ return [...used].sort(bytewise);
1012
972
  }
1013
973
 
1014
974
  /** The declared dependencies an applied row's helper settings use. */
@@ -1092,14 +1052,6 @@ function packageKeyUses(
1092
1052
  );
1093
1053
  }
1094
1054
 
1095
- /** Two strings ordered bytewise. */
1096
- function compare(a: string, b: string): number {
1097
- if (a === b) {
1098
- return 0;
1099
- }
1100
- return a < b ? -1 : 1;
1101
- }
1102
-
1103
1055
  /**
1104
1056
  * What the rows decide over the manifests the analysis reads. A row `disabled` names
1105
1057
  * is skipped before anything is read for it.
@@ -1119,7 +1071,7 @@ export function readConventions(
1119
1071
  const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
1120
1072
  const testFiles = new Set<string>();
1121
1073
  for (const manifest of manifests) {
1122
- const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
1074
+ const declared = readJSON(host, joinPath(manifest.dir, MANIFEST));
1123
1075
  const versions = new Map<string, string | undefined>();
1124
1076
  const missing = new Set<string>();
1125
1077
  for (const row of rows) {
@@ -1222,16 +1174,16 @@ export function readConventions(
1222
1174
  const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
1223
1175
  unique.sort(
1224
1176
  (a, b) =>
1225
- compare(a.name, b.name) ||
1226
- compare(a.manifest, b.manifest) ||
1227
- compare(JSON.stringify(a), JSON.stringify(b)),
1177
+ bytewise(a.name, b.name) ||
1178
+ bytewise(a.manifest, b.manifest) ||
1179
+ bytewise(JSON.stringify(a), JSON.stringify(b)),
1228
1180
  );
1229
1181
  return {
1230
1182
  applied: unique,
1231
1183
  globs,
1232
1184
  generated,
1233
1185
  failures,
1234
- uses: [...uses].sort(compare),
1186
+ uses: [...uses].sort(bytewise),
1235
1187
  selected,
1236
1188
  testFiles,
1237
1189
  };
@@ -42,17 +42,25 @@ import {
42
42
  import type { FindingPosition } from "./finding.ts";
43
43
  import type { Host } from "./host.ts";
44
44
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
45
- import { scriptTokens } from "./manifest.ts";
46
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
45
+ import {
46
+ ancestors,
47
+ dirnamePath,
48
+ isAbsolutePath,
49
+ joinPath,
50
+ relativePath,
51
+ resolvePath,
52
+ } from "./paths.ts";
47
53
  import type { DependencySection, Module } from "./ref.ts";
48
54
  import { UNANSWERED } from "./query.ts";
49
55
  import type { Handle, ProjectView } from "./session.ts";
50
56
  import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
51
57
  import { workflowCommands, workflowTokens } from "./workflow-steps.ts";
58
+ import { bytewise } from "./text/compare.ts";
59
+ import { isRecord } from "./json-document.ts";
60
+ import { inInstalledPackages, INSTALLED_PACKAGES, MANIFEST, readDocument } from "./fs/manifest.ts";
61
+ import { commandsOf, scriptTokens } from "./text/shell.ts";
52
62
 
53
- const MANIFEST = "package.json";
54
- const MODULES_DIR = "node_modules";
55
- const MODULES_SEGMENT = `/${MODULES_DIR}/`;
63
+ const MODULES_SEGMENT = `/${INSTALLED_PACKAGES}/`;
56
64
  const TYPES_SCOPE = "@types/";
57
65
 
58
66
  /** A script token naming a JavaScript file by a path no expansion changes. */
@@ -160,25 +168,6 @@ export interface Dependencies {
160
168
  readonly heldBy: ReadonlyMap<string, readonly string[]>;
161
169
  }
162
170
 
163
- function isRecord(value: unknown): value is Record<string, unknown> {
164
- return typeof value === "object" && value !== null && !Array.isArray(value);
165
- }
166
-
167
- /** One JSON document's decoded value, or undefined where it cannot be read or parsed. */
168
- function readJSON(host: Host, path: string): { text: string; value: unknown } | undefined {
169
- let text: string;
170
- try {
171
- text = host.readFile(path);
172
- } catch {
173
- return undefined;
174
- }
175
- try {
176
- return { text, value: JSON.parse(text) };
177
- } catch {
178
- return undefined;
179
- }
180
- }
181
-
182
171
  /**
183
172
  * The package a bare module specifier names: its first segment, or its first two
184
173
  * where it starts with a scope. A relative, absolute or scheme-prefixed specifier and
@@ -429,7 +418,7 @@ export function projectNeeds<Brand>(
429
418
  }
430
419
  for (const { strings, manifestDirs } of configuration.documents) {
431
420
  for (const manifestDir of manifestDirs) {
432
- const names = declaredAt(joinPath(manifestDir, "package.json"));
421
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
433
422
  for (const text of strings) {
434
423
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
435
424
  }
@@ -444,7 +433,7 @@ export function projectNeeds<Brand>(
444
433
  );
445
434
  for (const { file, manifestDirs } of [...configuration.outside, ...ranFiles]) {
446
435
  for (const manifestDir of manifestDirs) {
447
- const names = declaredAt(joinPath(manifestDir, "package.json"));
436
+ const names = declaredAt(joinPath(manifestDir, MANIFEST));
448
437
  for (const { text } of moduleStrings(file)) {
449
438
  use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
450
439
  }
@@ -467,8 +456,8 @@ function scriptRunFiles(
467
456
  const root = resolvePath(host.workingDirectory(), targetRoot);
468
457
  const dirs = new Set<string>([root]);
469
458
  for (const name of files.byName.keys()) {
470
- for (let dir = dirnamePath(name); !dirs.has(dir); dir = dirnamePath(dir)) {
471
- if (relativePath(root, dir) === undefined) {
459
+ for (const dir of ancestors(dirnamePath(name), root)) {
460
+ if (dirs.has(dir)) {
472
461
  break;
473
462
  }
474
463
  dirs.add(dir);
@@ -478,7 +467,7 @@ function scriptRunFiles(
478
467
  [root, new Set(workflowTokens(host, root))],
479
468
  ]);
480
469
  for (const dir of dirs) {
481
- const read = readJSON(host, joinPath(dir, MANIFEST));
470
+ const read = readDocument(host, joinPath(dir, MANIFEST));
482
471
  if (read !== undefined && isRecord(read.value)) {
483
472
  const own = scriptTokensOf(read.value);
484
473
  tokens.set(dir, dir === root ? new Set([...own, ...(tokens.get(root) ?? [])]) : own);
@@ -491,7 +480,8 @@ function scriptRunFiles(
491
480
  const below = path === undefined ? undefined : relativePath(root, path);
492
481
  if (
493
482
  path !== undefined &&
494
- below?.split("/").includes(MODULES_DIR) === false &&
483
+ below !== undefined &&
484
+ !inInstalledPackages(below) &&
495
485
  !files.byName.has(path) &&
496
486
  host.kindOf(path) === "file"
497
487
  ) {
@@ -797,8 +787,8 @@ function lineAndColumn(text: string, offset: number): { line: number; column: nu
797
787
 
798
788
  /** The installed copy of one dependency, read where the package manager puts it for the target. */
799
789
  function installedOf(host: Host, root: string, name: string): InstalledDependency | undefined {
800
- for (let dir = root; ; dir = dirnamePath(dir)) {
801
- const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
790
+ for (const dir of ancestors(root)) {
791
+ const read = readDocument(host, joinPath(dir, INSTALLED_PACKAGES, name, MANIFEST));
802
792
  if (read !== undefined && isRecord(read.value)) {
803
793
  const bin = read.value["bin"];
804
794
  const peers = read.value["peerDependencies"];
@@ -815,10 +805,8 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
815
805
  : [],
816
806
  };
817
807
  }
818
- if (dirnamePath(dir) === dir) {
819
- return undefined;
820
- }
821
808
  }
809
+ return undefined;
822
810
  }
823
811
 
824
812
  /** Every dependency the manifest text at `path` declares, in the order the text writes them. */
@@ -904,9 +892,6 @@ const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
904
892
  const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
905
893
  const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
906
894
 
907
- /** The commands one script string runs, split at the shell's list and pipeline operators. */
908
- const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
909
-
910
895
  /**
911
896
  * The command strings one manifest runs: each of its `scripts`, and the `exec` command of
912
897
  * nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
@@ -919,7 +904,10 @@ function manifestCommands(
919
904
  manifest: Record<string, unknown>,
920
905
  ): readonly string[] {
921
906
  const scripts = manifest["scripts"];
922
- const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
907
+ const nodemon = [
908
+ manifest["nodemonConfig"],
909
+ readDocument(host, joinPath(dir, "nodemon.json"))?.value,
910
+ ];
923
911
  return [
924
912
  ...(isRecord(scripts) ? Object.values(scripts) : []),
925
913
  ...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
@@ -934,7 +922,7 @@ function manifestCommands(
934
922
  function preloadsOf(scripts: readonly string[]): ReadonlySet<string> {
935
923
  const found = new Set<string>();
936
924
  for (const script of scripts) {
937
- for (const command of script.split(COMMAND_SEPARATORS)) {
925
+ for (const command of commandsOf(script)) {
938
926
  const tokens = scriptTokens(command);
939
927
  let at = 0;
940
928
  while (
@@ -1076,7 +1064,7 @@ export function dependenciesOf(
1076
1064
  const peers = new Map<string, readonly string[]>();
1077
1065
  const uninstalledHere = new Set(nested);
1078
1066
  for (const [dir, path] of manifests) {
1079
- const read = readJSON(host, joinPath(dir, MANIFEST));
1067
+ const read = readDocument(host, joinPath(dir, MANIFEST));
1080
1068
  if (read === undefined || !isRecord(read.value)) {
1081
1069
  continue;
1082
1070
  }
@@ -1154,10 +1142,7 @@ export function dependenciesOf(
1154
1142
  const heldBy = new Map<string, readonly string[]>();
1155
1143
  for (const [key, held] of holders) {
1156
1144
  if (!live.has(key) && !ran.has(key)) {
1157
- heldBy.set(
1158
- key,
1159
- [...held].sort((a, b) => (a < b ? -1 : 1)),
1160
- );
1145
+ heldBy.set(key, [...held].sort(bytewise));
1161
1146
  }
1162
1147
  }
1163
1148
  const users = new Map<string, number>();
@@ -1169,10 +1154,7 @@ export function dependenciesOf(
1169
1154
  for (const id of candidates) {
1170
1155
  const sole = [...(uses.get(id) ?? [])].filter((one) => users.get(one) === 1 && names.has(one));
1171
1156
  if (sole.length > 0) {
1172
- lastUses.set(
1173
- id,
1174
- sole.sort((a, b) => (a < b ? -1 : 1)),
1175
- );
1157
+ lastUses.set(id, sole.sort(bytewise));
1176
1158
  }
1177
1159
  }
1178
1160
  const owners = new Map<string, Set<string>>();
@@ -1184,7 +1166,7 @@ export function dependenciesOf(
1184
1166
  }
1185
1167
  }
1186
1168
  const fileLastUses = new Map<string, string[]>();
1187
- for (const [one, held] of [...owners].sort(([a], [b]) => (a < b ? -1 : 1))) {
1169
+ for (const [one, held] of [...owners].sort(([a], [b]) => bytewise(a, b))) {
1188
1170
  const paths = new Set([...held].map((owner) => owner.replace(/:\d+:\d+$/u, "")));
1189
1171
  const [path] = paths;
1190
1172
  if (paths.size === 1 && path !== undefined && path !== PROJECT_OWNER) {
package/src/discover.ts CHANGED
@@ -8,12 +8,17 @@ import type { Scope } from "./scope.ts";
8
8
  import { SetupError } from "./setup-failure.ts";
9
9
  import type { Engine, ProjectDiagnostics } from "./session.ts";
10
10
  import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
11
+ import { filesBelow } from "./fs/tree.ts";
12
+ import { LANGUAGE, message } from "./product.ts";
13
+ import { LANGUAGE_ROWS } from "./contract/contract.generated.ts";
14
+ import { globExpression } from "./text/glob.ts";
11
15
 
12
- /** Directory names discovery does not descend into. */
13
- export const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set(["node_modules", ".git"]);
16
+ const MARKERS = LANGUAGE_ROWS[LANGUAGE].markers.map(globExpression);
14
17
 
15
- /** A compiler configuration file discovery recognises under a root. */
16
- const CONFIG_FILE = /^tsconfig.*\.json$/u;
18
+ /** Whether one file name is a compiler configuration's: one of the language row's markers. */
19
+ export function isConfigFile(name: string): boolean {
20
+ return MARKERS.some((marker) => marker.test(name));
21
+ }
17
22
 
18
23
  /**
19
24
  * A run that cannot read what it was asked to analyze. It ends the run with the
@@ -218,39 +223,9 @@ interface Discovery {
218
223
  readonly fromScope: readonly string[];
219
224
  }
220
225
 
221
- /**
222
- * The configuration files below `root`, in ascending path order, skipping an
223
- * ignored directory. A directory that cannot be read is skipped: a tree the
224
- * analysis may not enter names no project.
225
- */
226
- function configFilesUnder(host: Host, root: string): string[] {
227
- const found: string[] = [];
228
- const pending = [root];
229
- while (pending.length > 0) {
230
- const dir = pending.pop();
231
- if (dir === undefined) {
232
- break;
233
- }
234
- let entries: readonly { readonly name: string; readonly directory: boolean }[];
235
- try {
236
- entries = host.readDirectory(dir);
237
- } catch {
238
- continue;
239
- }
240
- for (const entry of [...entries].sort((a, b) => (a.name < b.name ? -1 : 1))) {
241
- const path = joinPath(dir, entry.name);
242
- if (entry.directory) {
243
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
244
- pending.push(path);
245
- }
246
- continue;
247
- }
248
- if (CONFIG_FILE.test(entry.name)) {
249
- found.push(path);
250
- }
251
- }
252
- }
253
- return found.sort();
226
+ /** The configuration files below `root`, by absolute path in ascending order. */
227
+ function configFilesUnder(host: Host, root: string): readonly string[] {
228
+ return filesBelow(host, root, isConfigFile).map((path) => joinPath(root, path));
254
229
  }
255
230
 
256
231
  /**
@@ -329,7 +304,7 @@ export function consumerProjects(engine: Engine, host: Host, root: string): read
329
304
  if (kind !== "directory") {
330
305
  throw new DiscoveryError(`the consumer ${root} is not a directory`, []);
331
306
  }
332
- const found = derivedFrom(engine, host, configFilesUnder(host, root), () => true);
307
+ const found = derivedFrom(engine, host, [...configFilesUnder(host, root)], () => true);
333
308
  if (found.length === 0) {
334
309
  throw new DiscoveryError(
335
310
  `the consumer ${root} holds no compiler configuration, so none of its references can be read`,
@@ -384,7 +359,7 @@ function readConfiguration(
384
359
  ? { references: referencesOf(parsed) }
385
360
  : { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
386
361
  } catch (error: unknown) {
387
- return { reason: error instanceof Error ? error.message : String(error) };
362
+ return { reason: message(error) };
388
363
  }
389
364
  }
390
365
 
@@ -529,7 +504,7 @@ export function discoverProjects(
529
504
  configurations: readonly BuildConfiguration[] = [],
530
505
  ): Discovery {
531
506
  const declared = configurations.filter(
532
- (entry): entry is ProjectConfiguration => entry.shape === "project",
507
+ (entry): entry is ProjectConfiguration => "project" in entry,
533
508
  );
534
509
  if (declared.length > 0) {
535
510
  return declaredProjects(engine, host, scope.target.path, declared);