test-inventory 0.4.1 → 0.6.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 (51) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +4 -1
  3. package/dist/ast.d.ts +3 -3
  4. package/dist/ast.js +4 -4
  5. package/dist/bin.js +1 -0
  6. package/dist/cli.d.ts +4 -2
  7. package/dist/cli.js +27 -7
  8. package/dist/config-dependencies.d.ts +62 -0
  9. package/dist/config-dependencies.js +231 -0
  10. package/dist/config-runs.d.ts +113 -0
  11. package/dist/config-runs.js +807 -0
  12. package/dist/config.d.ts +90 -0
  13. package/dist/config.js +652 -0
  14. package/dist/dependency-hash.d.ts +242 -0
  15. package/dist/dependency-hash.js +1458 -0
  16. package/dist/diagnostics.js +6 -1
  17. package/dist/extract.d.ts +20 -0
  18. package/dist/extract.js +20 -4
  19. package/dist/filter.d.ts +6 -0
  20. package/dist/filter.js +18 -1
  21. package/dist/frameworks/bindings.d.ts +22 -0
  22. package/dist/frameworks/bindings.js +19 -9
  23. package/dist/frameworks/common.d.ts +5 -0
  24. package/dist/frameworks/common.js +11 -0
  25. package/dist/frameworks/playwright.d.ts +2 -1
  26. package/dist/frameworks/playwright.js +11 -2
  27. package/dist/frameworks/vitest.d.ts +2 -1
  28. package/dist/frameworks/vitest.js +9 -3
  29. package/dist/hash.d.ts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/inventory.d.ts +2 -2
  32. package/dist/inventory.js +2 -1
  33. package/dist/json.js +2 -0
  34. package/dist/model.d.ts +9 -1
  35. package/dist/options.d.ts +2 -2
  36. package/dist/records.d.ts +58 -2
  37. package/dist/records.js +196 -28
  38. package/dist/scan-file.d.ts +13 -1
  39. package/dist/scan-file.js +16 -4
  40. package/dist/scan-source.js +1 -1
  41. package/dist/scan.d.ts +2 -1
  42. package/dist/scan.js +131 -18
  43. package/dist/types.d.ts +83 -7
  44. package/dist/values.d.ts +5 -0
  45. package/dist/values.js +5 -0
  46. package/dist/vitest-config.d.ts +6 -4
  47. package/dist/vitest-config.js +32 -28
  48. package/dist/vitest-defaults.d.ts +6 -0
  49. package/dist/vitest-defaults.js +16 -0
  50. package/package.json +10 -8
  51. package/schema/v1.json +200 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # test-inventory
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#61](https://github.com/Huldoser/test-inventory/pull/61) [`926352f`](https://github.com/Huldoser/test-inventory/commit/926352fd67caf8d26e17e346b9e916400e28a4e3) - Each test now has a `dependencyHash` of everything it runs in the project, beside the unchanged `bodyHash`: its own call
8
+ with its options, the hooks, `test.use()` and options of its file and of every describe around it, the fixtures it,
9
+ those hooks, conditions and `test.use()` values ask for, with the fixtures those ask for, `auto` ones and overrides of
10
+ Playwright's own, through every `extend()` and `mergeTests()`, the file's other top-level code, the config and the
11
+ configs it builds on, setup files, global setup and teardown, the Playwright setup projects the test runs after, and the
12
+ snapshot files and screenshots it compares against, and the functions, classes, constants and modules all of these use,
13
+ followed by scope and through imports, re-exports, barrels, `require()`, `import()` and Vitest's `vi.mock()` with a
14
+ fixed path, with the code each module runs when it loads. A helper that declares the test is in it with the arguments of
15
+ the call that declares it, so two calls give two hashes. Formatting and comments don't change it, nor does moving the
16
+ test. Installed packages are left out, and types too, except for type tests.
17
+
18
+ `dependencies` lists the files that went into the hash. When the test reaches something the scan can't follow, a local
19
+ file that is missing or doesn't parse, an import with a path worked out at run time or a pattern, a value only a
20
+ declaration file describes, or a setup file the config names in a way the scan can't work out,
21
+ `hasUnfollowedDependencies` is `true` and the new `dependency-not-followed` warning says where, once per place.
22
+
23
+ - [#65](https://github.com/Huldoser/test-inventory/pull/65) [`31a9bee`](https://github.com/Huldoser/test-inventory/commit/31a9beef6f2912a03c4ae70ccd797b9b40cfe3e8) - `--related <file>` and the `related` filter of `filterInventory()` keep the tests a change to a file can affect: those
24
+ whose `dependencies` include it, and those that reach code the scan can't follow, which a change may affect unseen.
25
+ `--related -` reads the files from standard input, one per line, so `git diff --name-only origin/main... | npx
26
+ test-inventory --framework playwright --related -` lists the tests a pull request can affect. Paths are taken from the
27
+ current folder, as git writes them, also when `--root` is a folder in the repository.
28
+
29
+ ## 0.5.0
30
+
31
+ ### Minor Changes
32
+
33
+ - [#51](https://github.com/Huldoser/test-inventory/pull/51) [`acec7e7`](https://github.com/Huldoser/test-inventory/commit/acec7e7fd9bae8301ba3559c3986264ea35c3056) - Each file record has a new `config` key: the config the file uses, as the runner started at the root would pick it.
34
+ That is the outermost config at or above the file that runs it, and for Vitest `projects` the project that does,
35
+ read from Playwright's `testDir`, `testMatch`, `testIgnore` and `.gitignore` files, and from Vitest's `include`,
36
+ `exclude`, `includeSource`, `typecheck.include` and `benchmark.include` with each version's defaults. Before, the
37
+ nearest config was taken, which is wrong for a root config whose `include` covers packages that have their own
38
+ `vite.config.ts`. A file with tests that no config runs gets the new `file-not-run` warning.
39
+
40
+ The globals check follows the same choice, so a test in a folder project without a config of its own gets Vitest's
41
+ defaults, and a test the root config runs takes the root's `test.globals`. Files named by `benchmark.include` are
42
+ marked `isBenchmark`, in place of the default `*.bench.*` and `*.benchmark.*` names. The config reader now reads
43
+ regular expressions and paths built from `__dirname`, `__filename` or `import.meta` with `path.join()`,
44
+ `path.resolve()`, `path.dirname()`, `fileURLToPath()` and `new URL()`.
45
+
46
+ - [#50](https://github.com/Huldoser/test-inventory/pull/50) [`3e55445`](https://github.com/Huldoser/test-inventory/commit/3e55445d0b195941813368b7beeea239ccf8792f) - The output lists the configs the scanned files use in a new top-level `configs` key, and as `config` lines in JSON
47
+ Lines. A file uses the nearest `playwright.config.*`, `vitest.config.*` or `vite.config.*` in its folder or a folder
48
+ above, up to the root, as the runner started in that folder would. A config is read as code, never run, and `extends`
49
+ lists the files it takes settings from through imports, such as a shared base config.
50
+
51
+ Vitest's globals now come from `test.globals` in the config a file uses, read through `defineConfig()`,
52
+ `mergeConfig()`, spreads, constants and imported base configs. Before, any `globals: true` in a Vitest or Vite config
53
+ in the file's folder or a folder above counted, also one Vitest doesn't read for the file, as in a `vite.config.ts`
54
+ next to a `vitest.config.ts`. So `globals-not-enabled` can now appear where it didn't, and go away where a base config
55
+ turns globals on. A `test.globals` the scan can't work out without running the config, and a config it can't read,
56
+ get the new `config-value-unknown` warning.
57
+
58
+ - [#55](https://github.com/Huldoser/test-inventory/pull/55) [`a714612`](https://github.com/Huldoser/test-inventory/commit/a714612dcaf01eaa40d218bae44e7d27e68dbce6) - Run settings now come also from the config a file uses: Playwright's `timeout`, `retries` and `repeatEach`, and
59
+ Vitest's `testTimeout`, `retry` and `repeats`, where the file sets none. New `timeoutSource`, `retriesSource` and `repeatsSource`
60
+ keys say where each value comes from: `test`, `suite`, `file` or `config`, or `null` when nothing sets it. A test
61
+ takes the settings of the projects that run it, after their `grep`. When those projects disagree, the config sets the
62
+ value under a condition, as in `process.env.CI ? 2 : 0`, or the scan can't work out the projects, the value is `null`
63
+ with its source `config`. For a value under a condition, `timeoutConfigText`, `retriesConfigText` and
64
+ `repeatsConfigText` hold the config's text for it, so `retries: process.env.CI ? 2 : 0` reads as written. Playwright's `fullyParallel` and Vitest's `sequence.concurrent` make tests
65
+ `isParallel` where the file sets no mode, when every project that runs the test sets it.
66
+
67
+ A `.only` in a Playwright file whose config sets `forbidOnly` gets the new `forbidden-only` error, since the run then
68
+ fails before any test runs. When `forbidOnly` depends on a condition, as in `!!process.env.CI`, it is a warning that
69
+ quotes the condition.
70
+
71
+ - [#59](https://github.com/Huldoser/test-inventory/pull/59) [`aeb3eb1`](https://github.com/Huldoser/test-inventory/commit/aeb3eb10896ba79b82fb34e5b3dd08cb601a251d) - Vitest tags the config defines in `test.tags` now apply to the tests that carry them, as Vitest applies them. Their
72
+ `skip`, `todo`, `fails` and `only` are merged with the test's own and those a describe passes down, and Vitest's order,
73
+ `only` over `skip` over `todo`, gives the state, with the new `stateSource` value `config` when a tag sets it. Their
74
+ `concurrent`, `timeout`, `retry`, `repeats` and `meta` set those keys where the test sets none: from Vitest 5 over a
75
+ describe's options, and in Vitest 4.1 under them. The lowest `priority` wins between tags, and a Vitest 5 project's
76
+ own `tags` replace those of the config it extends.
77
+
78
+ A tag the config doesn't define fails the file in Vitest unless `strictTags` is off, also when there is no config, so
79
+ it now gets the new `undefined-tag` error, and the file's tests are `notLoaded`; in a function or under a condition it
80
+ is a warning, as that code may not run. The `config-may-change-state` warning now shows only when
81
+ the scan can't work out the config's tags.
82
+
83
+ - [#54](https://github.com/Huldoser/test-inventory/pull/54) [`c610789`](https://github.com/Huldoser/test-inventory/commit/c61078957ce5150fde089a45a444cf5bef41cfe5) - Each test record has a new `projects` key: the names of the projects that run the test, of the config its file uses.
84
+ For Playwright, those whose `testDir`, `testMatch` and `testIgnore` include the file, less those whose `grep` or
85
+ `grepInvert` leaves the test's title and tags out; for Vitest, those whose files include it, with each browser
86
+ instance as a project, named as Vitest names it, such as `unit (chromium)`. It is `[]` when no project runs the test,
87
+ and `null` without a config or when the scan can't tell. Config records list their projects in `projects`, each with
88
+ its `name`, `testDir` and Playwright's `dependencies`.
89
+
90
+ - [#53](https://github.com/Huldoser/test-inventory/pull/53) [`b9471ec`](https://github.com/Huldoser/test-inventory/commit/b9471ecd36f9f6157c21b013c7fa6cc9584250cd) - Glob patterns are optional, in `scan()` and on the command line: without them, the test files are those the
91
+ Playwright or Vitest configs in the root and the folders below it run, each with the config it uses. A config whose
92
+ files the scan can't work out gets a `config-value-unknown` warning that says to give patterns, and a scan without
93
+ patterns and without a config says so in `no-files-matched`. An empty list of patterns is still an error.
94
+
95
+ ### Patch Changes
96
+
97
+ - [#58](https://github.com/Huldoser/test-inventory/pull/58) [`1a34d74`](https://github.com/Huldoser/test-inventory/commit/1a34d74e76faead734afe87c0babc2a23fdbe4b7) - oxc-parser is updated from 0.154.0 to 0.156.0. The scan output doesn't change.
98
+
3
99
  ## 0.4.1
4
100
 
5
101
  ### Patch Changes
package/README.md CHANGED
@@ -25,6 +25,8 @@ Node.js 22.12 or newer, on Linux, macOS or Windows. Playwright 1.42 or newer, Vi
25
25
  npx test-inventory "tests/**/*.spec.ts" --framework playwright --output inventory.json
26
26
  ```
27
27
 
28
+ Without the glob, the test files are those the Playwright or Vitest configs in the project run.
29
+
28
30
  For this spec from a trading app's UI tests, `tests/alerts.spec.ts`:
29
31
 
30
32
  <!-- code: trading-web/tests/alerts.spec.ts -->
@@ -94,7 +96,8 @@ in Claude Code, Cursor, VS Code and other MCP clients ask it which tests a proje
94
96
 
95
97
  ## Limits
96
98
 
97
- - Config files are not read, so `grep`, projects and Vitest tag options in them do not change the output.
99
+ - Config files are read as code, never run, and only in part: settings computed at run time, as from `process.env`,
100
+ can't be worked out, and settings such as `workers` or `use` are not read.
98
101
  - Values imported from other files are not worked out. A tag or title that comes from an import gets a diagnostic
99
102
  that names the module. A custom `test` object is followed through as many files as it takes, through relative
100
103
  paths, tsconfig path aliases, `#` imports in package.json and workspace packages; one that can't be followed gets
package/dist/ast.d.ts CHANGED
@@ -11,10 +11,10 @@ export interface ReturnValue {
11
11
  }
12
12
  /**
13
13
  * The values a function returns; null when it may return no value, as with `return;` or no `return` at all, and for
14
- * an async function or a generator, which returns a promise or an iterator whatever its `return` says. A `return` in a
15
- * function inside it is that function's.
14
+ * an async function or a generator, which returns a promise or an iterator whatever its `return` says. With `awaited`,
15
+ * an async function's values are those its promise resolves to. A `return` in a function inside it is that function's.
16
16
  */
17
- export declare function returnValues(fn: FunctionNode): ReturnValue[] | null;
17
+ export declare function returnValues(fn: FunctionNode, awaited?: boolean): ReturnValue[] | null;
18
18
  export declare function isStringLiteral(node: Node): node is StringLiteral;
19
19
  /** `import.meta.vitest`, which in-source Vitest tests read their functions from. */
20
20
  export declare function isImportMetaVitest(expression: Expression): boolean;
package/dist/ast.js CHANGED
@@ -34,11 +34,11 @@ export function isFunctionNode(node) {
34
34
  }
35
35
  /**
36
36
  * The values a function returns; null when it may return no value, as with `return;` or no `return` at all, and for
37
- * an async function or a generator, which returns a promise or an iterator whatever its `return` says. A `return` in a
38
- * function inside it is that function's.
37
+ * an async function or a generator, which returns a promise or an iterator whatever its `return` says. With `awaited`,
38
+ * an async function's values are those its promise resolves to. A `return` in a function inside it is that function's.
39
39
  */
40
- export function returnValues(fn) {
41
- if (fn.async || fn.generator)
40
+ export function returnValues(fn, awaited = false) {
41
+ if ((fn.async && !awaited) || fn.generator)
42
42
  return null;
43
43
  // Only declarations of overloads have no body.
44
44
  const body = fn.body;
package/dist/bin.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { run } from "./cli.js";
3
3
  process.exitCode = await run(process.argv.slice(2), {
4
4
  stdout: process.stdout,
5
+ stdin: process.stdin,
5
6
  stderr: process.stderr,
6
7
  cwd: process.cwd(),
7
8
  });
package/dist/cli.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { Writable } from 'node:stream';
1
+ import type { Readable, Writable } from 'node:stream';
2
2
  export interface Io {
3
3
  stdout: Writable;
4
4
  stderr: Writable;
5
5
  cwd: string;
6
+ /** Read for `--related -`. */
7
+ stdin?: Readable;
6
8
  }
7
- export declare const HELP = "Usage: test-inventory <glob...> --framework <playwright|vitest> [options]\n\nLists the tests in the matching files as JSON, read from the source code without running them.\n\nOptions:\n --framework <name> playwright or vitest (required)\n --root <dir> project root; paths in the output are relative to it (default: current directory)\n --ignore <glob> leave out matching files; can be repeated (node_modules is always left out)\n --framework-version <x.y.z> apply the rules of this framework version instead of the one in the project\n --comments <mode> comments above tests: all, non-active or none (default: non-active)\n --state <state> keep tests in this state, such as skip or fixme; can be repeated\n --tag <tag> keep tests with this tag, such as @smoke; can be repeated\n --grep <regex> keep tests whose full name matches, ignoring case\n --only keep tests that use .only, or are inside a describe that does\n --without-reason keep tests that don't run and give no reason\n --fields <keys> write only these keys of each test, separated by commas; id is always kept\n --format <format> json or jsonl, one record per line (default: json)\n -o, --output <file> write to a file instead of standard output\n --pretty indent the JSON\n -h, --help show this help\n -v, --version show the version\n\nExit codes: 0 when the scan finishes, even with diagnostics; 2 for invalid arguments, an output that cannot be\nwritten, or a crash. 1 is reserved for a future release.\n\nExamples:\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --output inventory.json\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --state skip --fields fullName,reason\n";
9
+ export declare const HELP = "Usage: test-inventory [glob...] --framework <playwright|vitest> [options]\n\nLists the tests in the matching files as JSON, read from the source code without running them. Without globs, the\ntest files are those the Playwright or Vitest configs in the root and the folders below it run.\n\nOptions:\n --framework <name> playwright or vitest (required)\n --root <dir> project root; paths in the output are relative to it (default: current directory)\n --ignore <glob> leave out matching files; can be repeated (node_modules is always left out)\n --framework-version <x.y.z> apply the rules of this framework version instead of the one in the project\n --comments <mode> comments above tests: all, non-active or none (default: non-active)\n --state <state> keep tests in this state, such as skip or fixme; can be repeated\n --tag <tag> keep tests with this tag, such as @smoke; can be repeated\n --grep <regex> keep tests whose full name matches, ignoring case\n --only keep tests that use .only, or are inside a describe that does\n --without-reason keep tests that don't run and give no reason\n --related <file> keep tests a change to this file can affect; can be repeated; - reads files, one per\n line, from standard input, as git diff --name-only writes them\n --fields <keys> write only these keys of each test, separated by commas; id is always kept\n --format <format> json or jsonl, one record per line (default: json)\n -o, --output <file> write to a file instead of standard output\n --pretty indent the JSON\n -h, --help show this help\n -v, --version show the version\n\nExit codes: 0 when the scan finishes, even with diagnostics; 2 for invalid arguments, an output that cannot be\nwritten, or a crash. 1 is reserved for a future release.\n\nExamples:\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --output inventory.json\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --state skip --fields fullName,reason\n";
8
10
  /** Runs the command line with the given arguments and streams. Resolves to the exit code. */
9
11
  export declare function run(argv: string[], io: Io): Promise<number>;
package/dist/cli.js CHANGED
@@ -5,12 +5,13 @@ import { parseArgs } from 'node:util';
5
5
  import { readFailure } from "./files.js";
6
6
  import { checkFields, filterInventory, matcher, pickTestFields } from "./filter.js";
7
7
  import { writeInventory, writeInventoryLines } from "./json.js";
8
- import { OptionsError } from "./options.js";
8
+ import { OptionsError, toPosixPath } from "./options.js";
9
9
  import { scan } from "./scan.js";
10
10
  import { toolVersion } from "./version.js";
11
- export const HELP = `Usage: test-inventory <glob...> --framework <playwright|vitest> [options]
11
+ export const HELP = `Usage: test-inventory [glob...] --framework <playwright|vitest> [options]
12
12
 
13
- Lists the tests in the matching files as JSON, read from the source code without running them.
13
+ Lists the tests in the matching files as JSON, read from the source code without running them. Without globs, the
14
+ test files are those the Playwright or Vitest configs in the root and the folders below it run.
14
15
 
15
16
  Options:
16
17
  --framework <name> playwright or vitest (required)
@@ -23,6 +24,8 @@ Options:
23
24
  --grep <regex> keep tests whose full name matches, ignoring case
24
25
  --only keep tests that use .only, or are inside a describe that does
25
26
  --without-reason keep tests that don't run and give no reason
27
+ --related <file> keep tests a change to this file can affect; can be repeated; - reads files, one per
28
+ line, from standard input, as git diff --name-only writes them
26
29
  --fields <keys> write only these keys of each test, separated by commas; id is always kept
27
30
  --format <format> json or jsonl, one record per line (default: json)
28
31
  -o, --output <file> write to a file instead of standard output
@@ -48,6 +51,7 @@ const FLAGS = {
48
51
  'filter.states': '--state',
49
52
  'filter.tags': '--tag',
50
53
  'filter.grep': '--grep',
54
+ 'filter.related': '--related',
51
55
  fields: '--fields',
52
56
  };
53
57
  const FORMATS = ['json', 'jsonl'];
@@ -82,6 +86,13 @@ async function writeOutput(io, file, write) {
82
86
  throw error;
83
87
  }
84
88
  }
89
+ /** All of a stream's text, as `git diff --name-only` writes to it. */
90
+ async function text(stream) {
91
+ const chunks = [];
92
+ for await (const chunk of stream)
93
+ chunks.push(Buffer.from(chunk));
94
+ return Buffer.concat(chunks).toString('utf8');
95
+ }
85
96
  /** ESLint's exit codes. 1 is reserved for a future release. */
86
97
  const FINISHED = 0;
87
98
  const FAILED = 2;
@@ -108,6 +119,7 @@ export async function run(argv, io) {
108
119
  grep: { type: 'string' },
109
120
  only: { type: 'boolean' },
110
121
  'without-reason': { type: 'boolean' },
122
+ related: { type: 'string', multiple: true },
111
123
  fields: { type: 'string' },
112
124
  format: { type: 'string' },
113
125
  output: { type: 'string', short: 'o' },
@@ -129,8 +141,6 @@ export async function run(argv, io) {
129
141
  io.stdout.write(`${toolVersion}\n`);
130
142
  return FINISHED;
131
143
  }
132
- if (positionals.length === 0)
133
- return fail('give at least one glob pattern for the test files.');
134
144
  if (values.framework === undefined)
135
145
  return fail('--framework is required: playwright or vitest.');
136
146
  const format = values.format ?? 'json';
@@ -145,6 +155,16 @@ export async function run(argv, io) {
145
155
  only: values.only,
146
156
  withoutReason: values['without-reason'],
147
157
  };
158
+ const root = path.resolve(io.cwd, values.root ?? '.');
159
+ if (values.related) {
160
+ const read = values.related.includes('-') && io.stdin ? await text(io.stdin) : '';
161
+ const named = [
162
+ ...values.related.filter((file) => file !== '-'),
163
+ ...read.split(/\r?\n/).filter((line) => line.trim()),
164
+ ];
165
+ // Paths from the current folder, as git writes them there, to the root the output's paths start from.
166
+ filter.related = named.map((file) => toPosixPath(path.relative(root, path.resolve(io.cwd, file.trim()))));
167
+ }
148
168
  const fields = values.fields?.split(',').map((field) => field.trim());
149
169
  let inventory;
150
170
  let result;
@@ -154,9 +174,9 @@ export async function run(argv, io) {
154
174
  if (fields !== undefined)
155
175
  checkFields(fields);
156
176
  inventory = await scan({
157
- patterns: positionals,
177
+ patterns: positionals.length > 0 ? positionals : undefined,
158
178
  framework: values.framework,
159
- root: path.resolve(io.cwd, values.root ?? '.'),
179
+ root,
160
180
  ignore: values.ignore ?? [],
161
181
  frameworkVersion: values['framework-version'],
162
182
  comments: values.comments,
@@ -0,0 +1,62 @@
1
+ import type { ConfigRuns, Placement } from './config-runs.ts';
2
+ import type { ModuleReader, ModuleSource, Span } from './model.ts';
3
+ import { SourceText } from './source.ts';
4
+ import type { Framework } from './types.ts';
5
+ /**
6
+ * Something in a config the scan can't follow: where it is written, or the config and the path it names, which the
7
+ * warning points at.
8
+ */
9
+ export type ConfigGap = {
10
+ message: string;
11
+ /** Not reported, as the config reader reports it already: the tests are only marked. */
12
+ isQuiet: boolean;
13
+ } & ({
14
+ relativeFilePath: string;
15
+ source: SourceText;
16
+ span: Span;
17
+ } | {
18
+ module: ModuleSource;
19
+ text: string | null;
20
+ });
21
+ /** What the config a file uses runs with each of its tests: modules to take whole, and what can't be followed. */
22
+ export interface RunsWith {
23
+ /** The config the file uses, which builds on others through imports. */
24
+ config: ModuleSource | null;
25
+ /** Its setup files, the configs that list its project, and the files of setup projects. */
26
+ modules: ModuleSource[];
27
+ gaps: ConfigGap[];
28
+ /** Where the config puts snapshots elsewhere than the runner's default, which the scan doesn't follow. */
29
+ moved: ConfigGap | null;
30
+ }
31
+ /**
32
+ * Finds what each file's config makes its tests run besides the file: the config itself, which brings in the configs it
33
+ * builds on through imports, the setup files and global setup and teardown of each project that runs the file, the
34
+ * config that lists a Vitest project, whose global setup runs too, and for Playwright the files of the setup projects
35
+ * the file's projects depend on. Read once per config and set of projects.
36
+ */
37
+ export declare class ConfigDependencies {
38
+ private readonly framework;
39
+ private readonly runs;
40
+ private readonly readerFor;
41
+ /** The files setup projects may run. */
42
+ private readonly candidates;
43
+ private readonly found;
44
+ private readonly ids;
45
+ constructor(framework: Framework, runs: ConfigRuns, readerFor: (fromFile: string) => ModuleReader, candidates: readonly string[]);
46
+ of(placement: Placement): RunsWith;
47
+ private idOf;
48
+ private read;
49
+ /** Whether Playwright's `snapshotDir` is the testDir of every project that runs the file, its default. */
50
+ private isDefault;
51
+ /** The first setting of the config or a project running the file that puts snapshots elsewhere; null for none. */
52
+ private moved;
53
+ /** The files of the setup projects the file's projects depend on, and the ones those depend on. */
54
+ private setupProjects;
55
+ /**
56
+ * A setup file a config names, found from `root` as the runners find it; null for a name that isn't a file there,
57
+ * which is a package, whose code is left out as installed code is.
58
+ */
59
+ private setupFile;
60
+ /** A file the scan reads code from, by its absolute path, as the modules a test imports are read. */
61
+ private module;
62
+ }
@@ -0,0 +1,231 @@
1
+ import path from 'node:path';
2
+ import { settingAt } from "./config.js";
3
+ import { SourceText } from "./source.js";
4
+ const NOTHING = { config: null, modules: [], gaps: [], moved: null };
5
+ /** The keys that put snapshots elsewhere than the runner's default, by framework. */
6
+ const SNAPSHOT_KEYS = {
7
+ playwright: [
8
+ ['snapshotPathTemplate'],
9
+ ['snapshotDir'],
10
+ ['expect', 'toHaveScreenshot', 'pathTemplate'],
11
+ ['expect', 'toMatchAriaSnapshot', 'pathTemplate'],
12
+ ],
13
+ vitest: [
14
+ ['test', 'resolveSnapshotPath'],
15
+ ['test', 'snapshotEnvironment'],
16
+ ['test', 'browser', 'expect', 'toMatchScreenshot', 'resolveScreenshotPath'],
17
+ ['test', 'browser', 'expect', 'toMatchScreenshot', 'screenshotDirectory'],
18
+ ],
19
+ };
20
+ /** The keys of a config that name setup files, by framework; Vitest's are a project's own. */
21
+ const SETUP_KEYS = {
22
+ playwright: [['globalSetup'], ['globalTeardown']],
23
+ vitest: [
24
+ ['test', 'setupFiles'],
25
+ ['test', 'globalSetup'],
26
+ ],
27
+ };
28
+ const UNKNOWN_PROJECTS = "The projects that run the file can't be worked out, so dependencyHash doesn't cover their setup.";
29
+ /**
30
+ * Finds what each file's config makes its tests run besides the file: the config itself, which brings in the configs it
31
+ * builds on through imports, the setup files and global setup and teardown of each project that runs the file, the
32
+ * config that lists a Vitest project, whose global setup runs too, and for Playwright the files of the setup projects
33
+ * the file's projects depend on. Read once per config and set of projects.
34
+ */
35
+ export class ConfigDependencies {
36
+ framework;
37
+ runs;
38
+ readerFor;
39
+ /** The files setup projects may run. */
40
+ candidates;
41
+ found = new Map();
42
+ ids = new Map();
43
+ constructor(framework, runs, readerFor, candidates) {
44
+ this.framework = framework;
45
+ this.runs = runs;
46
+ this.readerFor = readerFor;
47
+ this.candidates = candidates;
48
+ }
49
+ of(placement) {
50
+ const { config } = placement;
51
+ if (config === null)
52
+ return NOTHING;
53
+ // Projects without a name are told apart by identity, as two may have different setup.
54
+ const ids = (placement.projects ?? []).map((project) => this.idOf(project));
55
+ const key = `${config.file}\0${placement.projects === null}\0${ids.join('\0')}`;
56
+ let found = this.found.get(key);
57
+ if (!found) {
58
+ found = this.read(config, placement);
59
+ this.found.set(key, found);
60
+ }
61
+ return found;
62
+ }
63
+ idOf(project) {
64
+ let id = this.ids.get(project);
65
+ if (id === undefined) {
66
+ id = this.ids.size;
67
+ this.ids.set(project, id);
68
+ }
69
+ return id;
70
+ }
71
+ read(config, placement) {
72
+ const own = this.module(config.file);
73
+ if (!own) {
74
+ // The config reader reports a config it can't read.
75
+ const message = "The config can't be read, so dependencyHash doesn't cover it.";
76
+ const span = { start: 0, end: 0 };
77
+ const gap = { relativeFilePath: config.record.relativeFilePath, source: new SourceText(''), span, message };
78
+ return { config: null, modules: [], gaps: [{ ...gap, isQuiet: true }], moved: null };
79
+ }
80
+ const modules = new Set();
81
+ const gaps = [];
82
+ const places = new Set();
83
+ /** The setup files a key of `settings` names, found from `root`, as written in the config `module`. */
84
+ const setup = (module, settings, root, keys) => {
85
+ const key = keys.join('.');
86
+ const found = written(settings, keys);
87
+ for (const item of found ? listOf(found.setting) : []) {
88
+ if (item.kind === 'value' && typeof item.value === 'string') {
89
+ const read = this.setupFile(root, item.value);
90
+ if (read && !('reason' in read))
91
+ modules.add(read);
92
+ else if (read) {
93
+ const message = `${key} names '${item.value}', which ${read.reason}, so dependencyHash doesn't cover it.`;
94
+ gaps.push({ module, text: item.value, message, isQuiet: false });
95
+ }
96
+ }
97
+ else if (item.kind === 'unknown') {
98
+ const message = `${key} can't be worked out, so dependencyHash doesn't cover the files it names.`;
99
+ // A Vitest 3 workspace file, which the scan doesn't read, has no place in the config.
100
+ if (!item.where)
101
+ gaps.push({ module, text: null, message, isQuiet: true });
102
+ // One gap per place, as both setup keys may come from the same spread.
103
+ else if (!places.has(item.where.node)) {
104
+ places.add(item.where.node);
105
+ const { frame, node } = item.where;
106
+ const at = { relativeFilePath: frame.relativeFilePath, source: frame.parsed.source, span: node };
107
+ gaps.push({ ...at, message, isQuiet: found.isInherited });
108
+ }
109
+ }
110
+ }
111
+ };
112
+ const { projects } = placement;
113
+ if (projects === null)
114
+ gaps.push({ module: own, text: null, message: UNKNOWN_PROJECTS, isQuiet: true });
115
+ const folder = path.dirname(config.file);
116
+ if (this.framework === 'playwright') {
117
+ for (const keys of SETUP_KEYS.playwright)
118
+ setup(own, config.settings, folder, keys);
119
+ this.setupProjects(config, own, projects ?? [], modules, gaps);
120
+ }
121
+ else {
122
+ // A file no project runs still runs with the config's own setup when the runner is given it.
123
+ const running = projects?.length
124
+ ? projects
125
+ : [{ settings: placement.settings, root: folder, container: null }];
126
+ // Vitest runs the global setup of the config it starts with for every project, extending it or not.
127
+ setup(own, config.settings, folder, ['test', 'globalSetup']);
128
+ for (const project of running) {
129
+ // A file several projects run may be run by one from another config, which the file runs with too; the config
130
+ // reader has read that config, so it can be read.
131
+ const written = 'config' in project ? this.module(project.config.file) : own;
132
+ modules.add(written);
133
+ for (const keys of SETUP_KEYS.vitest)
134
+ setup(written, project.settings, project.root, keys);
135
+ const { container } = project;
136
+ const outer = container && this.module(container.file);
137
+ if (container && outer) {
138
+ modules.add(outer);
139
+ setup(outer, container.settings, path.dirname(container.file), ['test', 'globalSetup']);
140
+ }
141
+ }
142
+ }
143
+ return { config: own, modules: [...modules], gaps, moved: this.moved(own, config, placement) };
144
+ }
145
+ /** Whether Playwright's `snapshotDir` is the testDir of every project that runs the file, its default. */
146
+ isDefault(config, placement, keys, found) {
147
+ if (keys[0] !== 'snapshotDir' || found.kind !== 'value' || typeof found.value !== 'string')
148
+ return false;
149
+ const folder = path.resolve(path.dirname(config.file), found.value);
150
+ const projects = placement.projects ?? [];
151
+ return projects.length > 0 && projects.every((project) => project.testDir === folder);
152
+ }
153
+ /** The first setting of the config or a project running the file that puts snapshots elsewhere; null for none. */
154
+ moved(own, config, placement) {
155
+ const settings = [config.settings, ...(placement.projects ?? []).map((project) => project.settings)];
156
+ for (const keys of SNAPSHOT_KEYS[this.framework]) {
157
+ for (const setting of settings) {
158
+ const found = settingAt(setting, keys);
159
+ if (found === null || this.isDefault(config, placement, keys, found))
160
+ continue;
161
+ const message = `${keys.join('.')} puts snapshots where the scan doesn't look, so dependencyHash doesn't cover them.`;
162
+ if (found.kind === 'unknown' && found.where) {
163
+ const { frame, node } = found.where;
164
+ return {
165
+ relativeFilePath: frame.relativeFilePath,
166
+ source: frame.parsed.source,
167
+ span: node,
168
+ message,
169
+ isQuiet: false,
170
+ };
171
+ }
172
+ const text = found.kind === 'value' && typeof found.value === 'string' ? found.value : null;
173
+ return { module: own, text, message, isQuiet: false };
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ /** The files of the setup projects the file's projects depend on, and the ones those depend on. */
179
+ setupProjects(config, own, projects, modules, gaps) {
180
+ const names = new Set();
181
+ const pending = projects.flatMap((project) => project.dependencies);
182
+ for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
183
+ if (names.has(name))
184
+ continue;
185
+ names.add(name);
186
+ const project = this.runs.projectNamed(config, name);
187
+ if (!project) {
188
+ // The config reader reports a project it can't work out.
189
+ const message = `The files of the setup project '${name}' can't be worked out, so dependencyHash doesn't cover them.`;
190
+ gaps.push({ module: own, text: null, message, isQuiet: true });
191
+ continue;
192
+ }
193
+ pending.push(...project.about.dependencies);
194
+ const files = this.candidates.filter((file) => project.runs(file)).map((file) => this.module(file));
195
+ for (const file of files.filter((item) => item !== null))
196
+ modules.add(file);
197
+ }
198
+ }
199
+ /**
200
+ * A setup file a config names, found from `root` as the runners find it; null for a name that isn't a file there,
201
+ * which is a package, whose code is left out as installed code is.
202
+ */
203
+ setupFile(root, file) {
204
+ // `path.join(__dirname, 'setup.ts')` gives an absolute path.
205
+ const relative = path.isAbsolute(file) ? path.relative(root, file).replaceAll('\\', '/') : file;
206
+ const isPath = /^\.\.?[\\/]/.test(relative);
207
+ const read = this.readerFor(path.join(root, 'config'))(isPath ? relative : `./${relative}`);
208
+ return 'reason' in read && !isPath ? null : read;
209
+ }
210
+ /** A file the scan reads code from, by its absolute path, as the modules a test imports are read. */
211
+ module(file) {
212
+ const read = this.readerFor(file)(`./${path.basename(file)}`);
213
+ return 'reason' in read ? null : read;
214
+ }
215
+ }
216
+ /**
217
+ * The setting under a path of keys, and whether it comes from an object around it that the scan can't work out
218
+ * rather than from the key itself; null when the config doesn't set it.
219
+ */
220
+ function written(setting, keys) {
221
+ const found = settingAt(setting, keys);
222
+ if (found?.kind !== 'unknown')
223
+ return found && { setting: found, isInherited: false };
224
+ // An unknown setting is set by the object around the key when the key itself isn't written there.
225
+ const around = settingAt(setting, keys.slice(0, -1));
226
+ const isWritten = around.kind === 'object' && around.keys.get(keys.at(-1)) === found;
227
+ return { setting: found, isInherited: !isWritten };
228
+ }
229
+ function listOf(setting) {
230
+ return setting.kind === 'list' ? setting.items : [setting];
231
+ }