@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.2

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 (98) hide show
  1. package/dist/src/analysis.js +45 -32
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +68 -10
  6. package/dist/src/convention-rows.js +48 -0
  7. package/dist/src/conventions.js +80 -0
  8. package/dist/src/dependencies.js +121 -46
  9. package/dist/src/discover.js +1 -1
  10. package/dist/src/edges.js +1 -1
  11. package/dist/src/exempt-classes.js +9 -22
  12. package/dist/src/exempt.js +3 -15
  13. package/dist/src/findings/completion.js +41 -11
  14. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  15. package/dist/src/findings/emitters.js +0 -4
  16. package/dist/src/findings/self-check.js +14 -10
  17. package/dist/src/html-entries.js +29 -14
  18. package/dist/src/indexed-access.js +377 -81
  19. package/dist/src/intra-function-parts.js +57 -11
  20. package/dist/src/inventory.js +3 -12
  21. package/dist/src/kinds.js +6 -6
  22. package/dist/src/references.js +62 -23
  23. package/dist/src/report.js +3 -0
  24. package/dist/src/roots.js +96 -7
  25. package/dist/src/run.js +0 -2
  26. package/dist/src/schema.js +1 -1
  27. package/dist/src/scope.js +2 -2
  28. package/dist/src/session.js +6 -2
  29. package/dist/src/source-files.js +1 -0
  30. package/dist/src/suppress-inline.js +1 -1
  31. package/dist/src/suppress.js +15 -16
  32. package/dist/src/sweep.js +29 -17
  33. package/dist/src/symbol-keys.js +114 -0
  34. package/dist/src/test-joins.js +101 -0
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +21 -15
  37. package/dist/src/workspace-programs.js +1 -4
  38. package/dist/src/workspace-resolution.js +0 -2
  39. package/dist/src/workspace.js +0 -3
  40. package/jsr.json +1 -1
  41. package/package.json +2 -1
  42. package/src/analysis.ts +55 -47
  43. package/src/calls.ts +3 -3
  44. package/src/component-files.ts +7 -7
  45. package/src/component-layer.ts +2 -2
  46. package/src/components.ts +1 -1
  47. package/src/config.ts +4 -4
  48. package/src/configuration-files.ts +246 -28
  49. package/src/conformance-record.ts +2 -2
  50. package/src/contextual-uses.ts +99 -13
  51. package/src/convention-rows.ts +76 -0
  52. package/src/conventions.ts +102 -0
  53. package/src/dependencies.ts +148 -56
  54. package/src/discover.ts +3 -3
  55. package/src/edges.ts +3 -3
  56. package/src/entry-points.ts +1 -1
  57. package/src/exempt-classes.ts +10 -24
  58. package/src/exempt.ts +5 -29
  59. package/src/file-facts.ts +1 -1
  60. package/src/finding.ts +5 -1
  61. package/src/findings/completion.ts +53 -11
  62. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  63. package/src/findings/emitters.ts +3 -8
  64. package/src/findings/self-check.ts +17 -14
  65. package/src/findings-pass.ts +2 -2
  66. package/src/graph.ts +2 -2
  67. package/src/html-entries.ts +34 -16
  68. package/src/indexed-access.ts +476 -102
  69. package/src/intra-function-parts.ts +84 -12
  70. package/src/inventory.ts +5 -33
  71. package/src/json-document.ts +1 -1
  72. package/src/kinds.ts +7 -6
  73. package/src/manifest.ts +1 -1
  74. package/src/namespace-objects.ts +1 -1
  75. package/src/pnpm-workspace.ts +1 -1
  76. package/src/references.ts +93 -33
  77. package/src/report.ts +8 -4
  78. package/src/roots.ts +129 -9
  79. package/src/run.ts +0 -2
  80. package/src/schema.ts +2 -2
  81. package/src/scope.ts +2 -2
  82. package/src/session.ts +7 -3
  83. package/src/source-files.ts +3 -0
  84. package/src/stores.ts +1 -1
  85. package/src/suppress-inline.ts +3 -3
  86. package/src/suppress.ts +21 -24
  87. package/src/sweep.ts +29 -17
  88. package/src/symbol-keys.ts +172 -0
  89. package/src/template-field.ts +1 -1
  90. package/src/test-joins.ts +120 -0
  91. package/src/value-reach.ts +1 -1
  92. package/src/verbs/verb.ts +1 -4
  93. package/src/version.ts +1 -1
  94. package/src/workflow-steps.ts +23 -16
  95. package/src/workspace-programs.ts +3 -10
  96. package/src/workspace-resolution.ts +0 -3
  97. package/src/workspace-run.ts +1 -1
  98. package/src/workspace.ts +1 -9
@@ -45,6 +45,24 @@ export interface HelperReading extends MoveReading {
45
45
  readonly package: string;
46
46
  }
47
47
 
48
+ /** One member of a configuration file whose strings name the packages its tool loads. */
49
+ export interface PackageKey {
50
+ /** The member's name, matched at any depth of the objects the row reads. */
51
+ readonly key: string;
52
+ /** Globs naming the configuration files, matched in the manifest's directory alone. */
53
+ readonly files: readonly string[];
54
+ }
55
+
56
+ /** One configuration property whose strings name files the test runner loads beside the tests. */
57
+ export interface TestFileReading extends MoveReading {
58
+ /**
59
+ * The properties that move the directory the strings are read against, the first one the
60
+ * file writes deciding, read against the manifest's directory, which stands where none is
61
+ * written. A value that is not one string literal leaves the file's strings unread.
62
+ */
63
+ readonly roots: readonly string[];
64
+ }
65
+
48
66
  /** One directory a row's globs name that a configuration property moves. */
49
67
  export interface DirectoryMove {
50
68
  /** The placeholder the row's globs and later moves write as `<id>`. */
@@ -83,6 +101,13 @@ export interface ConventionRow {
83
101
  * string there names no file the tool runs or loads.
84
102
  */
85
103
  readonly selections?: readonly MoveReading[];
104
+ /** The properties whose strings name a file the test runner loads beside the tests, a test file. */
105
+ readonly testFiles?: readonly TestFileReading[];
106
+ /**
107
+ * The members whose strings name a package the tool loads: the package itself, a
108
+ * subpath of it, or the package followed by `:` and a name.
109
+ */
110
+ readonly packageKeys?: readonly PackageKey[];
86
111
  /** The members of a `package.json` the tool reads its configuration from. */
87
112
  readonly manifestKeys?: readonly string[];
88
113
  /** The settings that make the tool's output import a runtime helper package. */
@@ -131,8 +156,46 @@ const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
131
156
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
132
157
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
133
158
 
159
+ /**
160
+ * The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
161
+ * resolves `setupFiles` against that root (https://vitest.dev/config/setupfiles).
162
+ */
163
+ const VITEST_ROOTS = ["test.root", "root"] as const;
164
+
165
+ /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
166
+ const ANGULAR_ENTRIES = [
167
+ "src/{main,main.server,server,polyfills,test}.ts",
168
+ "server.ts",
169
+ "projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
170
+ "projects/*/server.ts",
171
+ ];
172
+
173
+ /** The `angular.json` members whose strings name the packages a build loads. */
174
+ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
175
+ "builder",
176
+ "polyfills",
177
+ "styles",
178
+ "scripts",
179
+ ].map((key) => ({ key, files: ["angular.json"] }));
180
+
134
181
  /** Every row, ordered by name and then by range. */
135
182
  export const CONVENTION_ROWS: readonly ConventionRow[] = [
183
+ {
184
+ name: "angular",
185
+ package: "@angular/build",
186
+ range: ">=17.0.0 <22.0.0",
187
+ entries: ANGULAR_ENTRIES,
188
+ moves: [],
189
+ packageKeys: ANGULAR_PACKAGE_KEYS,
190
+ },
191
+ {
192
+ name: "angular-devkit",
193
+ package: "@angular-devkit/build-angular",
194
+ range: ">=12.0.0 <22.0.0",
195
+ entries: ANGULAR_ENTRIES,
196
+ moves: [],
197
+ packageKeys: ANGULAR_PACKAGE_KEYS,
198
+ },
136
199
  {
137
200
  name: "astro",
138
201
  package: "astro",
@@ -564,6 +627,18 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
564
627
  },
565
628
  ],
566
629
  },
630
+ {
631
+ name: "vitepress",
632
+ package: "vitepress",
633
+ range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
634
+ entries: [
635
+ `.vitepress/config.${ANY_MODULE}`,
636
+ `.vitepress/theme/index.${ANY_MODULE}`,
637
+ `**/*.data.${ANY_MODULE}`,
638
+ "**/*.vue",
639
+ ],
640
+ moves: [],
641
+ },
567
642
  {
568
643
  name: "vitest",
569
644
  package: "vitest",
@@ -585,5 +660,6 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
585
660
  { property: "test.coverage.include", files: [VITEST_CONFIG] },
586
661
  { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
587
662
  ],
663
+ testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
588
664
  },
589
665
  ];
@@ -53,7 +53,9 @@ import {
53
53
  type HelperReading,
54
54
  type MoveReading,
55
55
  type OptionsCall,
56
+ type PackageKey,
56
57
  type ShortNameReading,
58
+ type TestFileReading,
57
59
  } from "./convention-rows.ts";
58
60
  import { documentStrings } from "./configuration-files.ts";
59
61
  import { globExpression } from "./glob.ts";
@@ -118,6 +120,8 @@ export interface Conventions {
118
120
  readonly uses: readonly string[];
119
121
  /** Per configuration file, absolute, the strings its applied rows' selection keys hold. */
120
122
  readonly selected: ReadonlyMap<string, Selected>;
123
+ /** The files, absolute, an applied row's test-file properties name. */
124
+ readonly testFiles: ReadonlySet<string>;
121
125
  }
122
126
 
123
127
  /** The strings one configuration file holds at selection keys, which root no file. */
@@ -607,6 +611,43 @@ function holdsValue(held: Node, value: true | string): boolean {
607
611
  );
608
612
  }
609
613
 
614
+ /** The directory one file's test-file strings are read against, as {@link TestFileReading} states it. */
615
+ function testFileRoot(file: SourceFile, dir: string, reading: TestFileReading): string | undefined {
616
+ for (const property of reading.roots) {
617
+ const values = valuesAt(file, property, reading.call);
618
+ if (values.length > 0) {
619
+ const texts = new Set(values.map(literalText));
620
+ const [text] = texts;
621
+ return texts.size === 1 && text !== undefined ? resolvePath(dir, text) : undefined;
622
+ }
623
+ }
624
+ return dir;
625
+ }
626
+
627
+ /** The files an applied row's test-file properties name. */
628
+ function testFilesNamed(
629
+ host: Host,
630
+ parse: ParseSource,
631
+ dir: string,
632
+ readings: readonly TestFileReading[],
633
+ into: Set<string>,
634
+ ): void {
635
+ for (const reading of readings) {
636
+ for (const { file } of readingFiles(host, parse, dir, reading.files)) {
637
+ const at = testFileRoot(file, dir, reading);
638
+ if (at === undefined) {
639
+ continue;
640
+ }
641
+ for (const node of stringsAt(file, reading.property, reading.call)) {
642
+ const text = literalText(node);
643
+ if (text !== undefined && text !== "") {
644
+ into.add(resolvePath(at, text));
645
+ }
646
+ }
647
+ }
648
+ }
649
+ }
650
+
610
651
  /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
611
652
  function selectedStrings(
612
653
  host: Host,
@@ -873,6 +914,60 @@ function helperUses(
873
914
  .map((reading) => reading.package);
874
915
  }
875
916
 
917
+ /**
918
+ * The dependencies one manifest declares that an applied row's package keys use: each
919
+ * whose name a string under a member of a key's name equals, or begins followed by `/` or
920
+ * by `:` and a name. A member's own name is no such string.
921
+ */
922
+ function packageKeyUses(
923
+ host: Host,
924
+ parse: ParseSource,
925
+ dir: string,
926
+ manifest: unknown,
927
+ keys: readonly PackageKey[],
928
+ ): readonly string[] {
929
+ const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
930
+ const held = isRecord(manifest) ? manifest[section] : undefined;
931
+ return isRecord(held) ? Object.keys(held) : [];
932
+ });
933
+ const strings: string[] = [];
934
+ const collect = (node: Node): void => {
935
+ const text = literalText(node);
936
+ if (text !== undefined) {
937
+ strings.push(text);
938
+ } else if (isPropertyAssignment(node)) {
939
+ collect(node.initializer);
940
+ } else {
941
+ node.forEachChild(collect);
942
+ }
943
+ };
944
+ for (const { key, files } of keys) {
945
+ const visit = (node: Node): void => {
946
+ if (isPropertyAssignment(node) && keyOf(node) === key) {
947
+ collect(node.initializer);
948
+ }
949
+ node.forEachChild(visit);
950
+ };
951
+ for (const { file } of readingFiles(host, parse, dir, files)) {
952
+ const values = declaredValues(file);
953
+ for (const one of defaultExports(file)) {
954
+ const object = objectOf(one, values, true);
955
+ if (object !== undefined) {
956
+ visit(object);
957
+ }
958
+ }
959
+ }
960
+ }
961
+ return declared.filter((name) =>
962
+ strings.some(
963
+ (text) =>
964
+ text === name ||
965
+ text.startsWith(`${name}/`) ||
966
+ (text.startsWith(`${name}:`) && text.length > name.length + 1),
967
+ ),
968
+ );
969
+ }
970
+
876
971
  /** Two strings ordered bytewise. */
877
972
  function compare(a: string, b: string): number {
878
973
  if (a === b) {
@@ -898,6 +993,7 @@ export function readConventions(
898
993
  const failures: ConventionFailure[] = [];
899
994
  const uses = new Set<string>();
900
995
  const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
996
+ const testFiles = new Set<string>();
901
997
  for (const manifest of manifests) {
902
998
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
903
999
  const versions = new Map<string, string | undefined>();
@@ -959,6 +1055,10 @@ export function readConventions(
959
1055
  for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
960
1056
  uses.add(name);
961
1057
  }
1058
+ const keys = row.packageKeys ?? [];
1059
+ for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
1060
+ uses.add(name);
1061
+ }
962
1062
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
963
1063
  uses.add(name);
964
1064
  }
@@ -966,6 +1066,7 @@ export function readConventions(
966
1066
  uses.add(name);
967
1067
  }
968
1068
  selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
1069
+ testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
969
1070
  const excludes = [
970
1071
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
971
1072
  ].map(globExpression);
@@ -996,6 +1097,7 @@ export function readConventions(
996
1097
  failures,
997
1098
  uses: [...uses].sort(compare),
998
1099
  selected,
1100
+ testFiles,
999
1101
  };
1000
1102
  }
1001
1103
 
@@ -1,19 +1,22 @@
1
1
  /**
2
- * The dependencies the target's manifest and its workspace members' manifests declare, and
3
- * what the projects' own files need from outside the target. A file needs a package it
4
- * imports, re-exports from, augments or references the types of, by name or through the
5
- * file the specifier resolves to; a compiler configuration needs one it names, and a
6
- * configuration file a tool loads needs each dependency its strings spell. A package one
7
- * dependency pulls in for itself is that dependency's need, not the target's.
2
+ * The dependencies the target's manifest and its workspace members' manifests declare, and what the
3
+ * projects' own files need from outside the target. A file needs a package it imports, re-exports
4
+ * from, augments, references the types of or asks the runtime to resolve, by name or through the
5
+ * file the specifier resolves to; a compiler configuration needs one it names, and a configuration
6
+ * file a tool loads needs each dependency its strings spell. A package one dependency pulls in for
7
+ * itself is that dependency's need, not the target's.
8
8
  */
9
9
 
10
10
  import {
11
+ isCallExpression,
11
12
  isExportDeclaration,
12
13
  isExternalModuleReference,
13
14
  isIdentifier,
14
15
  isImportDeclaration,
15
16
  isImportEqualsDeclaration,
17
+ isMetaProperty,
16
18
  isNamespaceExport,
19
+ isPropertyAccessExpression,
17
20
  isShorthandPropertyAssignment,
18
21
  isStringLiteral,
19
22
  isNoSubstitutionTemplateLiteral,
@@ -32,11 +35,13 @@ import {
32
35
  import type { FindingPosition } from "./finding.ts";
33
36
  import type { Host } from "./host.ts";
34
37
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
38
+ import { scriptTokens } from "./manifest.ts";
35
39
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
36
40
  import type { DependencySection, Module } from "./ref.ts";
37
41
  import { UNANSWERED } from "./query.ts";
38
42
  import type { Handle, ProjectView } from "./session.ts";
39
43
  import { sourceFilesOf } from "./source-files.ts";
44
+ import { workflowTokens } from "./workflow-steps.ts";
40
45
 
41
46
  const MANIFEST = "package.json";
42
47
  const MODULES_DIR = "node_modules";
@@ -54,7 +59,7 @@ const SECTIONS: readonly (readonly [string, DependencySection])[] = [
54
59
  const PROJECT_OWNER = "";
55
60
 
56
61
  /** One dependency a manifest the run reads declares. */
57
- export interface DeclaredDependency {
62
+ interface DeclaredDependency {
58
63
  readonly name: string;
59
64
  readonly section: DependencySection;
60
65
  /** Where the manifest writes the dependency's key, below the target root. */
@@ -68,16 +73,14 @@ export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
68
73
  return {
69
74
  packages: new Set(packages),
70
75
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
71
- unanswered: false,
76
+ unanswered: [],
72
77
  };
73
78
  }
74
79
 
75
80
  /** What the installed copy of one declared dependency states about itself. */
76
- export interface InstalledDependency {
77
- /** Whether its manifest declares a command. */
78
- readonly command: boolean;
79
- /** The peer dependencies its manifest requires: every peer not marked optional. */
80
- readonly peers: readonly string[];
81
+ interface InstalledDependency {
82
+ /** The commands its manifest's `bin` declares, by name. */
83
+ readonly commands: readonly string[];
81
84
  }
82
85
 
83
86
  /** What one project's own files and configuration need from outside the target. */
@@ -91,20 +94,29 @@ export interface ProjectNeeds {
91
94
  */
92
95
  readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
93
96
  /**
94
- * Whether the checker left a module specifier unanswered, so the project may use any
95
- * dependency the manifest declares.
97
+ * The files writing a module specifier the checker left unanswered, each of which may
98
+ * use any dependency a manifest at or above it declares.
96
99
  */
97
- readonly unanswered: boolean;
100
+ readonly unanswered: readonly string[];
98
101
  }
99
102
 
100
103
  /** The run's dependency evidence. */
101
104
  export interface Dependencies {
102
105
  /** Every declared dependency, manifest by manifest, in the order each writes them. */
103
106
  readonly declared: readonly DeclaredDependency[];
104
- /** The installed copy of each declared dependency the run found, by name. */
105
- readonly installed: ReadonlyMap<string, InstalledDependency>;
106
107
  /** Every package some project needs. */
107
108
  readonly needed: ReadonlySet<string>;
109
+ /**
110
+ * The declared dependencies whose command a script entry of the manifest declaring them,
111
+ * or a workflow step for the target's manifest, runs, each by {@link declarationKey}.
112
+ */
113
+ readonly ran: ReadonlySet<string>;
114
+ /**
115
+ * The manifests, by their path below the target root, whose directory holds source only
116
+ * in configurations the run could not build, so a dependency they declare that nothing
117
+ * needs is reported at `possible`.
118
+ */
119
+ readonly unbuilt: ReadonlySet<string>;
108
120
  /**
109
121
  * Per deletion candidate, the declared dependencies whose last use in the target is
110
122
  * written inside it, sorted bytewise; a candidate holding no last use has no entry.
@@ -325,9 +337,11 @@ export function projectNeeds<Brand>(
325
337
  };
326
338
  use([PROJECT_OWNER], packages);
327
339
 
328
- let unanswered = false;
340
+ const unanswered: string[] = [];
329
341
  for (const file of project.ownSourceFiles()) {
330
- unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
342
+ if (fileNeeds(project, held, file, programPackages, use)) {
343
+ unanswered.push(file.fileName);
344
+ }
331
345
  }
332
346
 
333
347
  const beside = new Map<string, readonly string[]>();
@@ -352,16 +366,20 @@ export function projectNeeds<Brand>(
352
366
  use(ownersOf(file, held, node), dependenciesNamed(text, names));
353
367
  }
354
368
  }
355
- for (const document of configuration.documents) {
356
- const names = declaredAt(document.path);
357
- for (const text of document.strings) {
358
- use([PROJECT_OWNER], dependenciesNamed(text, names));
369
+ for (const { strings, manifestDirs } of configuration.documents) {
370
+ for (const manifestDir of manifestDirs) {
371
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
372
+ for (const text of strings) {
373
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
374
+ }
359
375
  }
360
376
  }
361
- for (const { file, manifestDir } of configuration.outside) {
362
- const names = declaredAt(joinPath(manifestDir, "package.json"));
363
- for (const { text } of moduleStrings(file)) {
364
- use([PROJECT_OWNER], dependenciesNamed(text, names));
377
+ for (const { file, manifestDirs } of configuration.outside) {
378
+ for (const manifestDir of manifestDirs) {
379
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
380
+ for (const { text } of moduleStrings(file)) {
381
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
382
+ }
365
383
  }
366
384
  }
367
385
  return { packages, uses, unanswered };
@@ -491,9 +509,43 @@ function fileNeeds<Brand>(
491
509
  for (const reference of file.typeReferenceDirectives) {
492
510
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
493
511
  }
512
+ for (const specifier of resolvedSpecifiers(file)) {
513
+ const named = packageOfSpecifier(specifier.text);
514
+ if (named !== undefined) {
515
+ use(ownersOf(file, held, specifier), [named]);
516
+ }
517
+ }
494
518
  return resolved.slice(0, specifiers.length).includes(UNANSWERED);
495
519
  }
496
520
 
521
+ /**
522
+ * The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
523
+ * or `require.resolve`, each of which needs the package it names installed.
524
+ */
525
+ function resolvedSpecifiers(file: SourceFile): readonly Texted[] {
526
+ const found: Texted[] = [];
527
+ if (!file.text.includes(".resolve(")) {
528
+ return found;
529
+ }
530
+ const visit = (node: Node): void => {
531
+ if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
532
+ const callee = node.expression;
533
+ const [argument] = node.arguments;
534
+ const receiver = callee.expression;
535
+ const resolving =
536
+ callee.name.text === "resolve" &&
537
+ ((isMetaProperty(receiver) && receiver.name.text === "meta") ||
538
+ (isIdentifier(receiver) && receiver.text === "require"));
539
+ if (resolving && argument !== undefined && isLiteralText(argument)) {
540
+ found.push(argument);
541
+ }
542
+ }
543
+ node.forEachChild(visit);
544
+ };
545
+ visit(file);
546
+ return found;
547
+ }
548
+
497
549
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
498
550
  function valueEnd(text: string, at: number): number {
499
551
  const first = text.charAt(at);
@@ -585,21 +637,17 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
585
637
  for (let dir = root; ; dir = dirnamePath(dir)) {
586
638
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
587
639
  if (read !== undefined && isRecord(read.value)) {
588
- const manifest = read.value;
589
- const bin = manifest["bin"];
590
- const peers = manifest["peerDependencies"];
591
- const meta = isRecord(manifest["peerDependenciesMeta"])
592
- ? manifest["peerDependenciesMeta"]
593
- : {};
640
+ const bin = read.value["bin"];
641
+ // A string `bin` names one command, the package's name without its scope.
594
642
  return {
595
- command:
596
- (typeof bin === "string" && bin !== "") || (isRecord(bin) && Object.keys(bin).length > 0),
597
- peers: isRecord(peers)
598
- ? Object.keys(peers).filter((peer) => {
599
- const about = meta[peer];
600
- return !(isRecord(about) && about["optional"] === true);
601
- })
602
- : [],
643
+ commands:
644
+ typeof bin === "string"
645
+ ? bin === ""
646
+ ? []
647
+ : [name.slice(name.indexOf("/") + 1)]
648
+ : isRecord(bin)
649
+ ? Object.keys(bin)
650
+ : [],
603
651
  };
604
652
  }
605
653
  if (dirnamePath(dir) === dir) {
@@ -639,12 +687,37 @@ function declaredIn(
639
687
  return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
640
688
  }
641
689
 
690
+ /** The key one declared dependency is known by: its manifest's path and its name. */
691
+ export function declarationKey(dependency: DeclaredDependency): string {
692
+ return `${dependency.position.path}\u0000${dependency.name}`;
693
+ }
694
+
695
+ /** Every token of every value of one manifest's `scripts` member. */
696
+ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string> {
697
+ const scripts = manifest["scripts"];
698
+ const found = new Set<string>();
699
+ if (isRecord(scripts)) {
700
+ for (const command of Object.values(scripts)) {
701
+ if (typeof command === "string") {
702
+ scriptTokens(command).forEach((token) => found.add(token));
703
+ }
704
+ }
705
+ }
706
+ return found;
707
+ }
708
+
709
+ /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
710
+ function isInside(path: string, dir: string): boolean {
711
+ return dir === "" || path.startsWith(`${dir}/`);
712
+ }
713
+
642
714
  /**
643
715
  * The run's dependency evidence: the target's manifest and each workspace member's in
644
716
  * `members` below the target root, read with the position of every dependency each
645
- * declares, the installed copy of each found from its manifest's directory, what the
646
- * projects need, and the last uses each deletion candidate holds. A manifest the
647
- * program cannot read declares nothing.
717
+ * declares, what the projects and the commands its scripts and workflow steps run need,
718
+ * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
719
+ * in configurations the run could not build. A manifest the program cannot read declares
720
+ * nothing.
648
721
  */
649
722
  export function dependenciesOf(
650
723
  host: Host,
@@ -652,30 +725,43 @@ export function dependenciesOf(
652
725
  perProject: readonly ProjectNeeds[],
653
726
  candidates: readonly string[],
654
727
  members: readonly string[] = [],
728
+ unbuiltMembers: readonly string[] = [],
655
729
  ): Dependencies {
656
730
  const root = resolvePath(host.workingDirectory(), targetRoot);
657
731
  const scopeOf = packageScope(host, targetRoot);
658
732
  const manifests = new Map<string, string>([[root, MANIFEST]]);
659
- for (const dir of members) {
733
+ const unbuilt = new Set<string>();
734
+ for (const dir of [...members, ...unbuiltMembers]) {
660
735
  const below = relativePath(root, dir);
661
736
  if (below !== undefined && !manifests.has(dir)) {
662
737
  manifests.set(dir, `${below}/${MANIFEST}`);
663
738
  }
664
739
  }
740
+ for (const dir of unbuiltMembers) {
741
+ const path = manifests.get(dir);
742
+ if (path !== undefined && dir !== root) {
743
+ unbuilt.add(path);
744
+ }
745
+ }
746
+ const workflow = new Set(workflowTokens(host, targetRoot));
665
747
  const declared: DeclaredDependency[] = [];
666
- const installed = new Map<string, InstalledDependency>();
748
+ const ran = new Set<string>();
667
749
  for (const [dir, path] of manifests) {
668
750
  const read = readJSON(host, joinPath(dir, MANIFEST));
669
751
  if (read === undefined || !isRecord(read.value)) {
670
752
  continue;
671
753
  }
754
+ const tokens = scriptTokensOf(read.value);
672
755
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
673
756
  declared.push(dependency);
674
- if (!installed.has(dependency.name)) {
675
- const found = installedOf(host, dir, dependency.name);
676
- if (found !== undefined) {
677
- installed.set(dependency.name, found);
678
- }
757
+ const found = installedOf(host, dir, dependency.name);
758
+ if (found === undefined) {
759
+ continue;
760
+ }
761
+ const runs = (command: string): boolean =>
762
+ tokens.has(command) || (dir === root && workflow.has(command));
763
+ if (found.commands.some(runs)) {
764
+ ran.add(declarationKey(dependency));
679
765
  }
680
766
  }
681
767
  }
@@ -685,9 +771,15 @@ export function dependenciesOf(
685
771
  const needed = new Set<string>();
686
772
  const uses = new Map<string, Set<string>>();
687
773
  for (const project of perProject) {
688
- // A specifier the checker left unanswered may resolve to any declared dependency,
689
- // and what the configuration needs is never deleted, so it is needed there.
690
- const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
774
+ // A specifier the checker left unanswered may resolve to any dependency a manifest at
775
+ // or above its file declares, and what the configuration needs is never deleted.
776
+ const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
777
+ const unknown = declared
778
+ .filter((dependency) => {
779
+ const dir = dirnamePath(dependency.position.path);
780
+ return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
781
+ })
782
+ .map((dependency) => dependency.name);
691
783
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
692
784
  for (const [id, used] of [...project.uses, ...held]) {
693
785
  const merged = uses.get(id) ?? new Set<string>();
@@ -711,5 +803,5 @@ export function dependenciesOf(
711
803
  );
712
804
  }
713
805
  }
714
- return { declared, installed, needed, lastUses };
806
+ return { declared, needed, ran, unbuilt, lastUses };
715
807
  }
package/src/discover.ts CHANGED
@@ -34,7 +34,7 @@ export class DiscoveryError extends Error {
34
34
  const ERROR_CATEGORY: number = DiagnosticCategory.Error;
35
35
 
36
36
  /** Whether one diagnostic is an error, which is what fails a run closed. */
37
- export function isError(diagnostic: Diagnostic): boolean {
37
+ function isError(diagnostic: Diagnostic): boolean {
38
38
  return diagnostic.category === ERROR_CATEGORY;
39
39
  }
40
40
 
@@ -113,7 +113,7 @@ export function projectErrors(sets: ProjectDiagnostics): ProjectErrors {
113
113
  }
114
114
 
115
115
  /** One project a run analyzes. */
116
- export interface DiscoveredProject {
116
+ interface DiscoveredProject {
117
117
  /**
118
118
  * The name a run gives the project: the identifier the build matrix declares for
119
119
  * it, or, for a project discovery derived, its configuration file's path below the
@@ -162,7 +162,7 @@ export function isGuess(discovery: Discovery, configFile: string): boolean {
162
162
  }
163
163
 
164
164
  /** Every project discovery found, and where each came from. */
165
- export interface Discovery {
165
+ interface Discovery {
166
166
  /** The projects, in discovery order, each once. */
167
167
  readonly projects: readonly DiscoveredProject[];
168
168
  /**
package/src/edges.ts CHANGED
@@ -39,7 +39,7 @@ const REFERENCE_WANT =
39
39
  "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
40
40
 
41
41
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
42
- export class EdgesError extends ConfigError {
42
+ class EdgesError extends ConfigError {
43
43
  constructor(at: string, text: string, want: string) {
44
44
  super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
45
45
  this.name = "EdgesError";
@@ -174,7 +174,7 @@ export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide
174
174
  }
175
175
 
176
176
  /** One side's verdict, as an evaluation names it. */
177
- export type EdgeState = "live" | "dead" | "absent";
177
+ type EdgeState = "live" | "dead" | "absent";
178
178
 
179
179
  /** One edge evaluation: one side of this language, its state, and the finding it holds. */
180
180
  export interface EdgeEvaluation<F extends Finding = Finding> extends EdgeSide {
@@ -184,7 +184,7 @@ export interface EdgeEvaluation<F extends Finding = Finding> extends EdgeSide {
184
184
  }
185
185
 
186
186
  /** The findings a report publishes, and one evaluation per declared side of this language. */
187
- export interface Evaluated<F extends Finding> {
187
+ interface Evaluated<F extends Finding> {
188
188
  readonly findings: readonly F[];
189
189
  /** Ordered by edge, then by side, each compared bytewise. */
190
190
  readonly evaluations: readonly EdgeEvaluation<F>[];
@@ -64,7 +64,7 @@ export type EntryRule =
64
64
  | "workflow-step";
65
65
 
66
66
  /** One file a rule enters. */
67
- export interface EntryPoint {
67
+ interface EntryPoint {
68
68
  readonly file: SourceFile;
69
69
  readonly rule: EntryRule;
70
70
  /**