@cplieger/deadset-ts 4.0.1 → 5.0.0

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 (117) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +229 -36
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +10 -3
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +3 -0
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +2 -0
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +394 -70
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +10 -6
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +3 -0
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +2 -0
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
package/src/discover.ts CHANGED
@@ -4,6 +4,7 @@ import type { BuildConfiguration, ProjectConfiguration } from "./config.ts";
4
4
  import type { Host } from "./host.ts";
5
5
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
6
6
  import type { Scope } from "./scope.ts";
7
+ import { SetupError } from "./setup-failure.ts";
7
8
  import type { Engine, ProjectDiagnostics } from "./session.ts";
8
9
 
9
10
  /** Directory names discovery does not descend into. */
@@ -323,13 +324,16 @@ function derivedFrom(
323
324
  */
324
325
  export function consumerProjects(engine: Engine, host: Host, root: string): readonly string[] {
325
326
  const kind = host.kindOf(root);
327
+ if (kind === "absent") {
328
+ throw new SetupError([
329
+ {
330
+ setupClass: "missing-consumer",
331
+ detail: `the scope declares the consumer ${root}, and nothing is at that path: check the consumer out at ${root} and install its dependencies`,
332
+ },
333
+ ]);
334
+ }
326
335
  if (kind !== "directory") {
327
- throw new DiscoveryError(
328
- kind === "absent"
329
- ? `the consumer ${root} does not exist`
330
- : `the consumer ${root} is not a directory`,
331
- [],
332
- );
336
+ throw new DiscoveryError(`the consumer ${root} is not a directory`, []);
333
337
  }
334
338
  const found = derivedFrom(engine, host, configFilesUnder(host, root));
335
339
  if (found.length === 0) {
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Where the compiler writes what it compiles: the output extension each source
3
+ * extension becomes, and the output path one source path is written to. A path a
4
+ * manifest or a document names is read back to its source through this one rule,
5
+ * whether the path is the target's own or a workspace package's.
6
+ */
7
+
8
+ import { dirnamePath, joinPath, relativePath } from "./paths.ts";
9
+
10
+ /** The source extensions the compiler emits each output extension from. */
11
+ const EMITTED: readonly { readonly emitted: string; readonly sources: readonly string[] }[] = [
12
+ { emitted: ".d.ts", sources: [".ts", ".tsx"] },
13
+ { emitted: ".d.mts", sources: [".mts"] },
14
+ { emitted: ".d.cts", sources: [".cts"] },
15
+ { emitted: ".js", sources: [".ts", ".tsx"] },
16
+ { emitted: ".jsx", sources: [".tsx"] },
17
+ { emitted: ".mjs", sources: [".mts"] },
18
+ { emitted: ".cjs", sources: [".cts"] },
19
+ ];
20
+
21
+ /** Where one compiler configuration writes its output, and the directories it writes it from. */
22
+ export interface EmitLayout {
23
+ /** The directory every output is written below, absolute. */
24
+ readonly outDir?: string | undefined;
25
+ /** The directory declaration outputs are written below where it differs, absolute. */
26
+ readonly declarationDir?: string | undefined;
27
+ /**
28
+ * The directories an output path below the output directory is read below, in the
29
+ * order they are tried: the one the configuration declares, or the ones the
30
+ * compiler's default may be.
31
+ */
32
+ readonly sourceDirs: readonly string[];
33
+ }
34
+
35
+ /** One path with its extension replaced, where it carries the one named. */
36
+ function reExtended(path: string, emitted: string, source: string): string | undefined {
37
+ return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
38
+ }
39
+
40
+ /**
41
+ * The longest directory every path shares, which is where the compiler roots its
42
+ * output when the configuration declares no root directory and its directory is not
43
+ * the root.
44
+ */
45
+ export function commonDirectory(paths: readonly string[]): string {
46
+ const first = paths[0];
47
+ if (first === undefined) {
48
+ return "";
49
+ }
50
+ let common = dirnamePath(first).split("/");
51
+ for (const path of paths.slice(1)) {
52
+ const segments = dirnamePath(path).split("/");
53
+ let shared = 0;
54
+ while (
55
+ shared < common.length &&
56
+ shared < segments.length &&
57
+ common[shared] === segments[shared]
58
+ ) {
59
+ shared += 1;
60
+ }
61
+ common = common.slice(0, shared);
62
+ }
63
+ return common.join("/");
64
+ }
65
+
66
+ /**
67
+ * The source paths one written path may be compiled from, in the order they are
68
+ * tried: for each output extension the path carries, the path re-extended where it
69
+ * stands, then read below each source directory where it is below the directory that
70
+ * output is written to.
71
+ */
72
+ export function emittedFrom(path: string, layout: EmitLayout): readonly string[] {
73
+ const found: string[] = [];
74
+ for (const { emitted, sources } of EMITTED) {
75
+ if (!path.endsWith(emitted)) {
76
+ continue;
77
+ }
78
+ const emitDir = emitted.startsWith(".d.")
79
+ ? (layout.declarationDir ?? layout.outDir)
80
+ : layout.outDir;
81
+ const below = emitDir === undefined ? undefined : relativePath(emitDir, path);
82
+ const mapped = below === undefined ? [] : layout.sourceDirs.map((dir) => joinPath(dir, below));
83
+ for (const source of sources) {
84
+ for (const from of [path, ...mapped]) {
85
+ const candidate = reExtended(from, emitted, source);
86
+ if (candidate !== undefined) {
87
+ found.push(candidate);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return found;
93
+ }
@@ -17,18 +17,23 @@ export interface DeclinedConvention {
17
17
  readonly reason: string;
18
18
  }
19
19
 
20
- /** What is declined for a tool none of whose conventions is entered. */
21
- const EVERY_CONVENTION = "its configuration and every file it loads by its own convention";
20
+ /** What is declined for a tool whose own conventions are entered only through its configuration files. */
21
+ const EVERY_CONVENTION = "every file it loads by its own convention beyond its configuration files";
22
22
 
23
- /** Why a tool none of whose conventions is entered declines them. */
23
+ /** Why a tool whose own conventions are not read declines them. */
24
24
  const UNREAD =
25
- "No rule reads it, so a file only it loads is reported as never imported unless " +
26
- "ts.entry_files or roots.patterns names it.";
25
+ "No rule reads it beyond a configuration file the file-name form or a string enters, so a " +
26
+ "file only it loads is reported as never imported unless ts.entry_files or roots.patterns " +
27
+ "names it.";
27
28
 
28
- /** The tools none of whose conventions is entered. */
29
+ /** Why a configuration's file names that are not relative literals are declined. */
30
+ const NOT_RELATIVE =
31
+ "A string of a configuration file names a file only where it is a literal starting with ./ " +
32
+ "or ../, so a file named otherwise is reported as never imported.";
33
+
34
+ /** The tools whose own conventions are not read. */
29
35
  const UNREAD_TOOLS: readonly string[] = [
30
36
  "angular",
31
- "astro",
32
37
  "astro-db",
33
38
  "astro-markdoc",
34
39
  "astro-og-canvas",
@@ -61,7 +66,6 @@ const UNREAD_TOOLS: readonly string[] = [
61
66
  "esbuild",
62
67
  "eve",
63
68
  "execa",
64
- "expo",
65
69
  "expressive-code",
66
70
  "fast",
67
71
  "fumadocs",
@@ -98,7 +102,6 @@ const UNREAD_TOOLS: readonly string[] = [
98
102
  "nano-staged",
99
103
  "nest",
100
104
  "netlify",
101
- "next",
102
105
  "next-intl",
103
106
  "next-mdx",
104
107
  "nitro",
@@ -106,7 +109,6 @@ const UNREAD_TOOLS: readonly string[] = [
106
109
  "node-modules-inspector",
107
110
  "nodemon",
108
111
  "npm-package-json-lint",
109
- "nuxt",
110
112
  "nuxtjs-i18n",
111
113
  "nx",
112
114
  "nyc",
@@ -131,17 +133,14 @@ const UNREAD_TOOLS: readonly string[] = [
131
133
  "prettier",
132
134
  "prisma",
133
135
  "quasar",
134
- "qwik",
135
136
  "railway",
136
137
  "raycast",
137
138
  "react-cosmos",
138
139
  "react-email",
139
140
  "react-native",
140
- "react-router",
141
141
  "relay",
142
142
  "release-it",
143
143
  "remark",
144
- "remix",
145
144
  "rolldown",
146
145
  "rollup",
147
146
  "rsbuild",
@@ -157,11 +156,9 @@ const UNREAD_TOOLS: readonly string[] = [
157
156
  "sst",
158
157
  "starlight",
159
158
  "stencil",
160
- "storybook",
161
159
  "stylelint",
162
160
  "svelte",
163
161
  "sveltejs-package",
164
- "sveltekit",
165
162
  "svgo",
166
163
  "svgr",
167
164
  "swc",
@@ -184,7 +181,6 @@ const UNREAD_TOOLS: readonly string[] = [
184
181
  "typescript-content-mapper",
185
182
  "unbuild",
186
183
  "unocss",
187
- "unplugin-auto-import",
188
184
  "unplugin-icons",
189
185
  "unplugin-vue-components",
190
186
  "unplugin-vue-i18n",
@@ -215,10 +211,17 @@ const UNREAD_TOOLS: readonly string[] = [
215
211
  const PARTLY_ENTERED: readonly DeclinedConvention[] = [
216
212
  {
217
213
  tool: "eslint",
218
- convention: "the legacy .eslintrc configuration files",
214
+ convention: "a legacy configuration written in YAML or in JSON with comments",
215
+ reason:
216
+ "Only a configuration module and a strict JSON configuration file are read, so a " +
217
+ "dependency only such a file names is reported as unused.",
218
+ },
219
+ {
220
+ tool: "eslint",
221
+ convention: "a plugin or shareable configuration named without its package's prefix",
219
222
  reason:
220
- "Only the flat configuration's file names are entered, so a legacy configuration " +
221
- "file the program holds is reported as never imported.",
223
+ "A string uses a dependency only where it spells the dependency's name, so a " +
224
+ "dependency only a shortened name loads is reported as unused.",
222
225
  },
223
226
  {
224
227
  tool: "playwright",
@@ -229,9 +232,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
229
232
  },
230
233
  {
231
234
  tool: "playwright",
232
- convention: "the globalSetup and globalTeardown files",
233
- reason:
234
- "No rule enters them, so a setup file only the runner loads is reported as never imported.",
235
+ convention: "a globalSetup or globalTeardown file named without a leading ./ or ../",
236
+ reason: NOT_RELATIVE,
235
237
  },
236
238
  {
237
239
  tool: "playwright",
@@ -240,17 +242,73 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
240
242
  "Only the configuration object's own testDir and testMatch are read, so a test " +
241
243
  "directory a project entry moves is not entered.",
242
244
  },
245
+ {
246
+ tool: "expo",
247
+ convention: "an app directory the expo-router entry of an app configuration moves",
248
+ reason:
249
+ "The convention row reads the app directory at its default places only, so a route " +
250
+ "below a moved directory is reported as never imported unless ts.entry_files names it.",
251
+ },
252
+ {
253
+ tool: "next",
254
+ convention: "a page extension next.config adds through pageExtensions",
255
+ reason:
256
+ "The convention row roots the default extensions only, so a route file of another " +
257
+ "extension is reported as never imported unless ts.entry_files names it.",
258
+ },
259
+ {
260
+ tool: "nuxt",
261
+ convention: "the files of a layer below layers/ beyond its nuxt.config",
262
+ reason:
263
+ "The convention row reads the project's own directories, so a layer's pages, plugins " +
264
+ "and server routes are reported as never imported unless ts.entry_files names them.",
265
+ },
266
+ {
267
+ tool: "qwik",
268
+ convention: "the routes of @qwik.dev/router",
269
+ reason:
270
+ "No convention row holds for that package, so its route files are reported as " +
271
+ "never imported unless ts.entry_files names them.",
272
+ },
273
+ {
274
+ tool: "react-router",
275
+ convention: "a route module app/routes.ts names outside the routes directory",
276
+ reason:
277
+ "The convention row roots the routes directory, and a string of routes.ts names no " +
278
+ "file, so a route module elsewhere is reported as never imported unless " +
279
+ "ts.entry_files names it.",
280
+ },
281
+ {
282
+ tool: "storybook",
283
+ convention: "a story the stories member of .storybook/main names outside *.stories.* files",
284
+ reason:
285
+ "The convention row roots *.stories.* files, so a story file named otherwise is " +
286
+ "reported as never imported unless ts.entry_files names it.",
287
+ },
243
288
  {
244
289
  tool: "stryker",
245
- convention: "a configuration file named on the command line",
246
- reason: "Only the file names the tool looks for by default are entered.",
290
+ convention: "a configuration file named on a command line no package.json script writes",
291
+ reason:
292
+ "Only the file names the tool looks for by default and the files a script names are entered.",
293
+ },
294
+ {
295
+ tool: "unplugin-auto-import",
296
+ convention: "an import its generated declarations add for a component's template alone",
297
+ reason:
298
+ "Only a global declared as an alias of an export is read, so an export the generated " +
299
+ "declarations name only for templates is reported as unused.",
300
+ },
301
+ {
302
+ tool: "vite",
303
+ convention: "an entry point its configuration names without a leading ./ or ../, or computes",
304
+ reason: NOT_RELATIVE,
247
305
  },
248
306
  {
249
307
  tool: "vite",
250
- convention: "its configuration and every entry point it names",
308
+ convention: "the module scripts an index.html loads",
251
309
  reason:
252
- "Only a worker a call addresses by a literal is entered. No rule reads the " +
253
- "configuration, so a file only it names is reported as never imported.",
310
+ "No rule reads an HTML file, so a script only index.html loads is reported as never " +
311
+ "imported unless ts.entry_files names it.",
254
312
  },
255
313
  {
256
314
  tool: "vitest",
@@ -261,17 +319,15 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
261
319
  },
262
320
  {
263
321
  tool: "vitest",
264
- convention: "the mock files below a __mocks__ directory",
322
+ convention: "the setup files of a configuration that moves its root",
265
323
  reason:
266
- "No rule enters a file by its directory, so a mock only the runner's module mocking " +
267
- "loads is reported as never imported.",
324
+ "A string is read against the configuration file's directory, so a setup file named " +
325
+ "against a root the configuration moves is not entered.",
268
326
  },
269
327
  {
270
328
  tool: "vitest",
271
- convention: "the setup files of a configuration that moves its root",
272
- reason:
273
- "A configuration writing root reads its setup files against a directory its text " +
274
- "may not state, so none of them is entered.",
329
+ convention: "a setup file named without a leading ./ or ../",
330
+ reason: NOT_RELATIVE,
275
331
  },
276
332
  {
277
333
  tool: "vitest",
@@ -282,10 +338,8 @@ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
282
338
  },
283
339
  {
284
340
  tool: "webpack",
285
- convention: "its configuration and every entry point it names",
286
- reason:
287
- "Only a worker a call addresses by a literal is entered. No rule reads the " +
288
- "configuration, so a file only it names is reported as never imported.",
341
+ convention: "an entry point its configuration names without a leading ./ or ../, or computes",
342
+ reason: NOT_RELATIVE,
289
343
  },
290
344
  ];
291
345
 
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The files a tool or the platform enters without any import naming them: the
3
- * configuration files the test runner, the linter, the mutation tester and the
4
- * browser-test runner load by their file name, the test files the runner executes,
5
- * and a worker a call addresses by a string literal.
3
+ * configuration files a tool loads by their file name and the files their strings
4
+ * name, the test files the runner executes, and a worker a call addresses by a string
5
+ * literal.
6
6
  *
7
7
  * Each rule is read from the program and nothing else: a file name the tool
8
8
  * itself looks for, a literal written in a configuration file, or a literal
@@ -30,7 +30,9 @@ import {
30
30
  type SourceFile,
31
31
  } from "@typescript/native/unstable/ast";
32
32
  import { SymbolFlags } from "@typescript/native/unstable/sync";
33
+ import { configurationFiles, filesNamedBy, moduleStrings } from "./configuration-files.ts";
33
34
  import { globExpression } from "./glob.ts";
35
+ import type { Host } from "./host.ts";
34
36
  import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
35
37
  import { UNANSWERED } from "./query.ts";
36
38
  import type { ProjectView } from "./session.ts";
@@ -38,13 +40,15 @@ import type { SourceFiles } from "./source-files.ts";
38
40
 
39
41
  /** Which rule entered a file. */
40
42
  export type EntryRule =
41
- /** The test runner: its configuration, its setup files and the test files. */
43
+ /** A configuration module, by the form of its name. */
44
+ | "configuration-file"
45
+ /** A file a relative string of a configuration module or a JSON configuration file names. */
46
+ | "configuration-string"
47
+ /** The test runner: its workspace configuration, its setup files and the test files. */
42
48
  | "test-runner"
43
- /** The linter's flat configuration. */
44
- | "lint-configuration"
45
- /** The mutation tester's configuration, and the test-runner configuration it names. */
49
+ /** The mutation tester's configuration under a name the configuration form does not cover. */
46
50
  | "mutation-testing"
47
- /** The browser-test runner's configuration and the test files below its test directory. */
51
+ /** The test files below the browser-test runner's test directory. */
48
52
  | "browser-tests"
49
53
  /** A worker or service worker a call addresses by a string literal. */
50
54
  | "worker";
@@ -78,42 +82,28 @@ interface Convention {
78
82
  /** The extensions the test runner and the browser-test runner load a configuration from. */
79
83
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"] as const;
80
84
 
81
- /** The extensions the linter's flat configuration and the mutation tester's are loaded from. */
85
+ /** The extensions the mutation tester's configuration is loaded from. */
82
86
  const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"] as const;
83
87
 
88
+ /** The tool file names the configuration form does not cover. */
84
89
  const CONVENTIONS: readonly Convention[] = [
85
90
  {
86
91
  rule: "test-runner",
87
- stems: ["vitest.config", "vitest.workspace", "vitest.projects"],
92
+ stems: ["vitest.workspace", "vitest.projects"],
88
93
  extensions: MODULE_EXTENSIONS,
89
94
  testRunner: true,
90
95
  },
91
- {
92
- rule: "lint-configuration",
93
- stems: ["eslint.config"],
94
- extensions: MODULE_EXTENSIONS,
95
- testRunner: false,
96
- },
97
96
  {
98
97
  rule: "mutation-testing",
99
- stems: ["stryker.config", "stryker.conf", ".stryker.config", ".stryker.conf"],
98
+ stems: ["stryker.conf", ".stryker.config", ".stryker.conf"],
100
99
  extensions: SCRIPT_EXTENSIONS,
101
100
  testRunner: false,
102
101
  },
103
- {
104
- rule: "mutation-testing",
105
- stems: ["vitest.stryker.config"],
106
- extensions: MODULE_EXTENSIONS,
107
- testRunner: true,
108
- },
109
- {
110
- rule: "browser-tests",
111
- stems: ["playwright.config"],
112
- extensions: MODULE_EXTENSIONS,
113
- testRunner: false,
114
- },
115
102
  ];
116
103
 
104
+ /** The stem of the browser-test runner's configuration, whose test directory is read. */
105
+ const BROWSER_TEST_CONFIG = "playwright.config";
106
+
117
107
  /** The browser-test runner's own test-file pattern, which holds where a configuration names none. */
118
108
  const BROWSER_TEST_MATCH = "**/*.{spec,test}.{js,ts,jsx,tsx,cjs,cts,cjsx,ctsx,mjs,mts,mjsx,mtsx}";
119
109
 
@@ -256,6 +246,43 @@ function filesNamed(files: SourceFiles, dir: string, text: string): readonly Sou
256
246
  return files.named(resolvePath(dir, pathPart(text)));
257
247
  }
258
248
 
249
+ /** Whether one file name is the browser-test runner's configuration. */
250
+ function isBrowserTestConfig(name: string): boolean {
251
+ return MODULE_EXTENSIONS.some((ext) => name === `${BROWSER_TEST_CONFIG}.${ext}`);
252
+ }
253
+
254
+ /**
255
+ * The files the configuration files of one project enter: each configuration module,
256
+ * with what it exports, and each own file a relative string of a configuration module
257
+ * or a JSON configuration file names, read against that file's directory.
258
+ */
259
+ function configurationEntries(
260
+ host: Host,
261
+ targetRoot: string,
262
+ files: SourceFiles,
263
+ ): readonly EntryPoint[] {
264
+ const { modules, documents } = configurationFiles(host, targetRoot, files);
265
+ const found: EntryPoint[] = [];
266
+ const strings = (dir: string, texts: readonly string[]): void => {
267
+ for (const text of texts) {
268
+ for (const file of filesNamedBy(files, dir, text)) {
269
+ found.push({ file, rule: "configuration-string", source: text, exports: true });
270
+ }
271
+ }
272
+ };
273
+ for (const { file, form } of modules) {
274
+ found.push({ file, rule: "configuration-file", source: form, exports: true });
275
+ strings(
276
+ dirnamePath(file.fileName),
277
+ moduleStrings(file).map((one) => one.text),
278
+ );
279
+ }
280
+ for (const document of documents) {
281
+ strings(dirnamePath(document.path), document.strings);
282
+ }
283
+ return found;
284
+ }
285
+
259
286
  /**
260
287
  * The setup files one test-runner configuration names: every literal under
261
288
  * `test.setupFiles` and `test.globalSetup`, read against the configuration's own
@@ -467,8 +494,8 @@ function workersOf<Brand>(
467
494
  }
468
495
 
469
496
  /**
470
- * Every file of one project the lifted rules enter, in no particular order and
471
- * possibly more than once: a file two rules enter appears once per rule.
497
+ * Every file of one project the rules enter, in no particular order and possibly more
498
+ * than once: a file two rules enter appears once per rule.
472
499
  *
473
500
  * `testFiles` is the configuration's test-file patterns, matched against each
474
501
  * file's path below the target root; a file one of them matches is a test file the
@@ -478,19 +505,22 @@ export function entryPoints<Brand>(
478
505
  project: ProjectView<Brand>,
479
506
  files: SourceFiles,
480
507
  testFiles: readonly string[],
508
+ host: Host,
509
+ targetRoot: string,
481
510
  ): readonly EntryPoint[] {
482
- const found: EntryPoint[] = [];
511
+ const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files)];
483
512
  const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
484
513
  for (const [path, file] of files.byPath) {
485
- const named = conventionOf(basename(path));
514
+ const name = basename(path);
515
+ const named = conventionOf(name);
486
516
  if (named !== undefined) {
487
517
  found.push({ file, rule: named.convention.rule, source: named.spelled, exports: true });
488
518
  if (named.convention.testRunner) {
489
519
  found.push(...setupFilesOf(file, files));
490
520
  }
491
- if (named.convention.rule === "browser-tests") {
492
- found.push(...browserTestsOf(file, files));
493
- }
521
+ }
522
+ if (isBrowserTestConfig(name)) {
523
+ found.push(...browserTestsOf(file, files));
494
524
  }
495
525
  for (const { pattern, expression } of tests) {
496
526
  if (expression.test(path)) {
package/src/exempt.ts CHANGED
@@ -33,6 +33,8 @@ export interface Evidence {
33
33
  /** One clause naming the relation, then the thing it relates to. */
34
34
  readonly detail: string;
35
35
  readonly site: Position;
36
+ /** The component file, below the target root, whose markup holds the evidence. */
37
+ readonly whileLive?: string;
36
38
  }
37
39
 
38
40
  /** What a detector reads of one project, while the project's view is open. */
@@ -180,6 +182,7 @@ export function computeExemptions<Brand>(
180
182
  class: exemptionClass,
181
183
  detail: evidence.detail,
182
184
  site: evidence.site,
185
+ ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
183
186
  });
184
187
  }
185
188
  }
@@ -196,8 +199,8 @@ function compare(a: string, b: string): number {
196
199
 
197
200
  /**
198
201
  * Records as a run holds them: ordered by site, then class, then declaration, and one
199
- * per declaration, class and detail, the one at the first site. A fact found at many
200
- * sites, or by several projects, is one record.
202
+ * per declaration, class, detail and file it holds while live, the one at the first
203
+ * site. A fact found at many sites, or by several projects, is one record.
201
204
  */
202
205
  export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[] {
203
206
  const ordered = [...found].sort(
@@ -205,7 +208,7 @@ export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[]
205
208
  );
206
209
  const seen = new Set<string>();
207
210
  return ordered.filter((record) => {
208
- const fact = [record.id, record.class, record.detail].join("\u0000");
211
+ const fact = [record.id, record.class, record.detail, record.whileLive ?? ""].join("\u0000");
209
212
  if (seen.has(fact)) {
210
213
  return false;
211
214
  }
@@ -214,6 +217,21 @@ export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[]
214
217
  });
215
218
  }
216
219
 
220
+ /**
221
+ * The records that hold while the component files at `live` are live: every record that
222
+ * holds whatever is live, and each one a live file's markup holds, as a record of the run.
223
+ */
224
+ export function holdingWhile(
225
+ records: readonly Exemption[],
226
+ live: ReadonlySet<string>,
227
+ ): readonly Exemption[] {
228
+ return exemptionsOf(
229
+ records.flatMap(({ whileLive, ...record }) =>
230
+ whileLive === undefined || live.has(whileLive) ? [record] : [],
231
+ ),
232
+ );
233
+ }
234
+
217
235
  /** The records per declaration, each declaration's in the order given. */
218
236
  function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, readonly Exemption[]> {
219
237
  const grouped = new Map<string, Exemption[]>();