@orkestrel/scaffold 0.0.108 → 0.0.109
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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
- package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
- package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
- package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
- package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
- package/dist/agents/templates/brief.md +1 -1
- package/dist/bin/main.js +51 -46
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +23 -21
- package/dist/host/agents/orchestration.md +43 -18
- package/dist/host/agents/platforms/windows.md +3 -3
- package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
- package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
- package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
- package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
- package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
- package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
- package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
- package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
- package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
- package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
- package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
- package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
- package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
- package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
- package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
- package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
- package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
- package/dist/host/agents/templates/brief.md +1 -1
- package/dist/host/agents/transports/claude-cli.md +3 -3
- package/dist/host/agents/transports/codex.md +2 -7
- package/dist/host/agents/transports/cursor.md +2 -3
- package/dist/host/claude/AGENTS.md +2 -2
- package/dist/host/claude/agents/analyst.md +3 -3
- package/dist/host/claude/agents/astra.md +3 -3
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +1 -1
- package/dist/host/claude/agents/distiller.md +1 -1
- package/dist/host/claude/agents/grok.md +1 -1
- package/dist/host/claude/agents/opus.md +2 -2
- package/dist/host/claude/agents/orkestrel.md +57 -90
- package/dist/host/claude/agents/researcher.md +1 -1
- package/dist/host/claude/agents/reviewer.md +4 -0
- package/dist/host/claude/agents/scout.md +1 -1
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +14 -21
- package/dist/host/claude/rules/architecture.md +34 -44
- package/dist/host/claude/rules/browser.md +3 -4
- package/dist/host/claude/rules/campaign.md +2 -2
- package/dist/host/claude/rules/documentation.md +25 -12
- package/dist/host/claude/rules/names.md +9 -4
- package/dist/host/claude/rules/patterns.md +6 -7
- package/dist/host/claude/rules/portability.md +1 -1
- package/dist/host/claude/rules/quality.md +4 -5
- package/dist/host/claude/rules/styles.md +2 -2
- package/dist/host/claude/rules/tests.md +57 -114
- package/dist/host/claude/rules/typescript.md +7 -9
- package/dist/host/claude/rules/workspace.md +21 -19
- package/dist/host/claude/rules/writing.md +2 -5
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +9 -3
- package/dist/host/codex/agents/astra.toml +3 -4
- package/dist/host/codex/agents/builder.toml +1 -1
- package/dist/host/codex/agents/opus.toml +3 -4
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/verifier.toml +1 -1
- package/dist/host/configs/helpers.ts +21 -1
- package/dist/host/configs/policy.ts +299 -15
- package/dist/host/dotfiles/oxlintrc.json +137 -3
- package/dist/host/guides/agent.md +111 -134
- package/dist/host/guides/capture.md +801 -0
- package/dist/host/guides/probe.md +27 -8
- package/dist/host/guides/scaffold.md +96 -18
- package/dist/host/guides/study.md +534 -440
- package/dist/host/guides/test.md +111 -44
- package/dist/host/manifest.json +160 -240
- package/dist/host/tests/config.test.ts +971 -187
- package/dist/host/tests/policy.test.ts +127 -1
- package/dist/host/tests/setupPolicy.ts +893 -33
- package/dist/src/core/index.cjs +214 -86
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +122 -38
- package/dist/src/core/index.d.ts +122 -38
- package/dist/src/core/index.js +209 -85
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +20 -27
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +21 -28
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
- package/dist/host/guides/supervisor.md +0 -4890
|
@@ -1,27 +1,65 @@
|
|
|
1
1
|
// Let Vitest select each gate's tests, then compare its identities with each config/mode universe.
|
|
2
2
|
// Run from the checkout root:
|
|
3
3
|
// node .agents/skills/orkestrel-harden/scripts/discovery.ts [--config vite.config.ts] [--projects a,b] [--json]
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
4
|
+
// Each value is a separate argument: `--flag=value` and any flag the script does not read exit 64.
|
|
5
|
+
// Roots are the package scripts no `npm run` invokes; `npm test` runs the root `test`, which stays a
|
|
6
|
+
// root. Follow root chains and forwarded arguments through `npm run [-s|--silent] NAME`,
|
|
7
|
+
// `npm run-script`, `npm test`, and `npm t`. Read a command past an environment prefix, `cross-env`,
|
|
8
|
+
// and a redirection. A gate is a Vitest run; list, bench, watch, dev, `--watch` without `=false`, and
|
|
9
|
+
// the mergeReports, listTags, and clearCache invocations are not gates. A test file counts as gated
|
|
10
|
+
// by a command only when `node` runs it as an argument (`node FILE`, `node --test FILE`).
|
|
11
|
+
// Collect an unfiltered full listing for each config/mode/root/dir, including the base config. Pass
|
|
12
|
+
// gate arguments verbatim except the reporter, output-file, coverage, color, cache, silent, UI,
|
|
13
|
+
// watch, run-mode, inspector, and standalone options; use filesOnly for file selection and full
|
|
14
|
+
// collection with --staticParse=false for name, tag, line, and shard selection, so runtime titles,
|
|
15
|
+
// setup modules, and the worker sequencer participate. Compare file/project/name/line/column
|
|
16
|
+
// identities without renaming projects. Preserve duplicate names within a listing and merge across
|
|
17
|
+
// units. Exit 2 naming the Vitest version when the option table lacks a required option.
|
|
18
|
+
// With --projects, list only the named projects: the universe, each gate listing, each project
|
|
19
|
+
// reading, the units enumerated, and the empty-project rescan carry the names, and a gate that
|
|
20
|
+
// reaches none of them is not listed. An exact name never lists a project outside the set, and the
|
|
21
|
+
// refusal for a name that matches none lists no other project either. A name holding `*` or opening
|
|
22
|
+
// with `!` is a Vitest pattern the script cannot match beforehand, so a gate that names projects
|
|
23
|
+
// keeps its own and a listing for such a name can reach projects beyond the names. Exit 64 for a
|
|
24
|
+
// name no project in the config matches, and skip the undiscovered scan.
|
|
25
|
+
// Flag ungated projects, empty chained projects, and undiscovered files under tests/ (the test
|
|
26
|
+
// and spec names, any extension). Report marker counts for the forms that need a ruling (`.todo`,
|
|
27
|
+
// `.skipIf`, `.runIf`, `.fails`, a `retry` option, a timeout option or a numeric last argument),
|
|
28
|
+
// read as call forms outside comments and strings; `vitest/no-focused-tests` and `vitest/no-disabled-tests` refuse `.only` and `.skip`. Report empty
|
|
29
|
+
// standalone workbenches without a flag: an empty project no chain containing " > " names.
|
|
13
30
|
// Retry when test files change during collection; refuse another change during that retry.
|
|
14
|
-
// Exit 0 with no flag, 3 with a flag, 2 for missing Vitest,
|
|
15
|
-
// CLI/help/argument/listing/output failures, repeated
|
|
16
|
-
// exit 64 on usage.
|
|
31
|
+
// Exit 0 with no flag, 3 with a flag, 2 for missing Vitest, an option table without a required
|
|
32
|
+
// option, unsupported related commands, CLI/help/argument/listing/output failures, repeated
|
|
33
|
+
// test-file changes, or filesystem/JSON errors; exit 64 on usage.
|
|
17
34
|
import { spawnSync } from 'node:child_process';
|
|
18
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
35
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, } from 'node:fs';
|
|
19
36
|
import { tmpdir } from 'node:os';
|
|
20
37
|
import { basename, join, relative, resolve } from 'node:path';
|
|
21
|
-
|
|
22
|
-
const VITEST = 'node_modules/vitest/vitest.mjs';
|
|
23
|
-
|
|
24
|
-
const
|
|
38
|
+
/** Names the Vitest entry the census runs. */
|
|
39
|
+
export const VITEST = 'node_modules/vitest/vitest.mjs';
|
|
40
|
+
/** Lists the marker names the census counts, in report order. */
|
|
41
|
+
export const MARKERS = [
|
|
42
|
+
'.todo',
|
|
43
|
+
'.skipIf',
|
|
44
|
+
'.runIf',
|
|
45
|
+
'.fails',
|
|
46
|
+
'retry',
|
|
47
|
+
'timeout',
|
|
48
|
+
];
|
|
49
|
+
/** Lists the options the scraped Vitest help table must hold before a listing runs. */
|
|
50
|
+
export const REQUIRED_OPTIONS = [
|
|
51
|
+
'--reporter',
|
|
52
|
+
'--project',
|
|
53
|
+
'--config',
|
|
54
|
+
'--filesOnly',
|
|
55
|
+
'--staticParse',
|
|
56
|
+
];
|
|
57
|
+
/** Lists the flags this script reads, value flags and switches alike. */
|
|
58
|
+
export const FLAGS = ['--config', '--projects', '--json'];
|
|
59
|
+
/** Holds the usage line printed on a usage refusal. */
|
|
60
|
+
export const USAGE = 'usage: discovery.ts [--config vite.config.ts] [--projects a,b] [--json]';
|
|
61
|
+
/** Names the options a gate listing drops, because they change the run and not the selection. */
|
|
62
|
+
export const OMITTED = new Set([
|
|
25
63
|
'reporter',
|
|
26
64
|
'outputFile',
|
|
27
65
|
'coverage',
|
|
@@ -35,10 +73,171 @@ const OMITTED = new Set([
|
|
|
35
73
|
'inspectBrk',
|
|
36
74
|
'standalone',
|
|
37
75
|
]);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
76
|
+
/** Bounds one listing, so an option that waits for input exits 2 naming its gate. */
|
|
77
|
+
export const LISTING_TIMEOUT = 600_000;
|
|
78
|
+
/** Caps the characters of a name an error message echoes. */
|
|
79
|
+
export const NAME_LIMIT = 80;
|
|
80
|
+
/** Names the directories a tree walk skips by default, so build output and notes stay unread. */
|
|
81
|
+
export const SKIPPED_DIRECTORIES = new Set([
|
|
82
|
+
'node_modules',
|
|
83
|
+
'.git',
|
|
84
|
+
'dist',
|
|
85
|
+
'tmp',
|
|
86
|
+
'.orkestrel',
|
|
87
|
+
'.idea',
|
|
88
|
+
]);
|
|
89
|
+
/** Names the only directories the undiscovered scan skips. */
|
|
90
|
+
export const UNDISCOVERED_SKIPS = new Set(['node_modules', '.git']);
|
|
91
|
+
/** Matches the test file names the modification snapshot watches. */
|
|
92
|
+
export const TEST_FILE_PATTERN = /\.test\.[cm]?[jt]sx?$/u;
|
|
93
|
+
/** Matches the test and spec file names the undiscovered scan reports. */
|
|
94
|
+
export const DISCOVERABLE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
95
|
+
/** Matches a test file path a `node` command can run as an argument. */
|
|
96
|
+
export const TEST_ARGUMENT_PATTERN = /^(?:tests|src|app)\/[A-Za-z0-9_./-]+\.test\.[cm]?ts$/u;
|
|
97
|
+
/** Matches one command token, keeping a quoted run, an `&`-form redirection, or a separator whole. */
|
|
98
|
+
export const COMMAND_TOKEN_PATTERN = /\d*[<>]&[\d-]+|&>>?|(?:[^\s"';&|]+|"[^"]*"|'[^']*')+|[;&|]+/gu;
|
|
99
|
+
/** Matches a redirection operator that takes the next token as its target. */
|
|
100
|
+
export const REDIRECT_OPERATOR_PATTERN = /^(?:\d*>>?|\d*<|&>>?)$/u;
|
|
101
|
+
/** Matches a redirection that carries its target or duplicates a descriptor. */
|
|
102
|
+
export const REDIRECT_TARGET_PATTERN = /^(?:(?:\d*>>?|\d*<|&>>?)[^\s<>&]\S*|\d*[<>]&[\d-]+)$/u;
|
|
103
|
+
/** Matches a leading `NAME=value` environment assignment. */
|
|
104
|
+
export const ENVIRONMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
105
|
+
/** Matches a `--watch` switch that is not turned off in its own token. */
|
|
106
|
+
export const WATCH_PATTERN = /^(?:--watch|--watch=(?!false$).*|-w)$/u;
|
|
107
|
+
/** Matches the reporting and cache-maintenance switches that make an invocation no gate. */
|
|
108
|
+
export const MAINTENANCE_PATTERN = /^(?:--mergeReports|--merge-reports|--listTags|--list-tags|--clearCache|--clear-cache)(?:=|$)/u;
|
|
109
|
+
/** Matches the options and filters that make a gate select tests by name, tag, line, or shard. */
|
|
110
|
+
export const FULL_PATTERN = /^(?:-t|--testNamePattern|--test-name-pattern|--tagsFilter|--tags-filter|--shard|--(?:no-)?allowOnly|--(?:no-)?allow-only|--(?:no-)?strictTags|--(?:no-)?strict-tags)(?:=|$)/u;
|
|
111
|
+
/** Matches a numeric literal token such as a timeout argument. */
|
|
112
|
+
export const NUMBER_PATTERN = /^\d[\d_]*(?:\.\d+)?(?:e\d+)?$/iu;
|
|
113
|
+
/** Matches the quoted string literals the scanner keeps whole. */
|
|
114
|
+
export const STRING_PATTERN = /^(?:'(?:\\[\s\S]|[^'\\\r\n])*'|"(?:\\[\s\S]|[^"\\\r\n])*")/u;
|
|
115
|
+
/** Matches a regular expression literal opening at a slash. */
|
|
116
|
+
export const REGEX_PATTERN = /^\/(?:\\.|\[(?:\\.|[^\]\\\r\n])*\]|[^/\\[\r\n])+\/[a-z]*/u;
|
|
117
|
+
/** Matches an identifier or a number as one scanner word. */
|
|
118
|
+
export const WORD_PATTERN = /^(?:[\p{ID_Start}_$][\p{ID_Continue}$]*|\d[\w.]*)/u;
|
|
119
|
+
/** Names the keywords after which a slash opens a regular expression. */
|
|
120
|
+
export const REGEX_KEYWORDS = new Set([
|
|
121
|
+
'return',
|
|
122
|
+
'typeof',
|
|
123
|
+
'case',
|
|
124
|
+
'throw',
|
|
125
|
+
'in',
|
|
126
|
+
'of',
|
|
127
|
+
'void',
|
|
128
|
+
'delete',
|
|
129
|
+
'yield',
|
|
130
|
+
'await',
|
|
131
|
+
'else',
|
|
132
|
+
'do',
|
|
133
|
+
'instanceof',
|
|
134
|
+
'new',
|
|
135
|
+
]);
|
|
136
|
+
/** Lists the tokens that open a bracket group. */
|
|
137
|
+
export const OPENERS = ['(', '[', '{'];
|
|
138
|
+
/** Lists the tokens that close a bracket group. */
|
|
139
|
+
export const CLOSERS = [')', ']', '}'];
|
|
140
|
+
/** Names the calls whose first argument list precedes the registration call. */
|
|
141
|
+
export const ARGUMENT_MODIFIERS = new Set(['each', 'skipIf', 'runIf', 'for']);
|
|
142
|
+
/** Names the registration modifiers that need a ruling when a test file carries them. */
|
|
143
|
+
export const CHAIN_MARKERS = new Set(['todo', 'skipIf', 'runIf', 'fails']);
|
|
144
|
+
/** Names the identifiers that open a test registration. */
|
|
145
|
+
export const REGISTRARS = new Set(['it', 'test', 'describe', 'suite']);
|
|
146
|
+
/**
|
|
147
|
+
* Normalizes a host path to forward slashes.
|
|
148
|
+
*
|
|
149
|
+
* @param path - A host path.
|
|
150
|
+
* @returns The same path with every backslash replaced by a slash.
|
|
151
|
+
*/
|
|
152
|
+
export function normalizeSlashes(path) {
|
|
153
|
+
return path.split('\\').join('/');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reads the value that follows a flag.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
|
|
160
|
+
* and the `--flag=value` form is not read.
|
|
161
|
+
*
|
|
162
|
+
* @param argv - The arguments after the script path.
|
|
163
|
+
* @param name - The flag, such as `--config`.
|
|
164
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
165
|
+
* another flag.
|
|
166
|
+
*/
|
|
167
|
+
export function readOption(argv, name) {
|
|
168
|
+
const index = argv.indexOf(name);
|
|
169
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
170
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Names the value flags a caller gave with no value.
|
|
174
|
+
*
|
|
175
|
+
* @param argv - The arguments after the script path.
|
|
176
|
+
* @param names - The flags that take a value.
|
|
177
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
178
|
+
*/
|
|
179
|
+
export function readMissingFlags(argv, names) {
|
|
180
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Names the flags a caller gave that the script does not read.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* Reads the arguments before a bare `--`. A `--flag=value` argument is unknown, because the script
|
|
187
|
+
* reads `--flag value` pairs only.
|
|
188
|
+
*
|
|
189
|
+
* @param argv - The arguments after the script path.
|
|
190
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
191
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
192
|
+
*/
|
|
193
|
+
export function readUnknownFlags(argv, flags) {
|
|
194
|
+
const end = argv.indexOf('--');
|
|
195
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Lists every regular file beneath a directory, skipping the named directories at any depth.
|
|
199
|
+
*
|
|
200
|
+
* @param directory - The directory to walk.
|
|
201
|
+
* @param skipped - The directory names to skip. Default: {@link SKIPPED_DIRECTORIES}.
|
|
202
|
+
* @returns Each file's forward-slash path and modification time, in walk order.
|
|
203
|
+
* @throws Thrown when `directory` does not exist or cannot be read; check `existsSync` first.
|
|
204
|
+
*/
|
|
205
|
+
export function listFiles(directory, skipped = SKIPPED_DIRECTORIES) {
|
|
206
|
+
const files = [];
|
|
207
|
+
const pending = [directory];
|
|
208
|
+
while (pending.length > 0) {
|
|
209
|
+
const current = pending.pop();
|
|
210
|
+
if (current === undefined)
|
|
211
|
+
break;
|
|
212
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
213
|
+
if (skipped.has(entry.name))
|
|
214
|
+
continue;
|
|
215
|
+
const path = join(current, entry.name);
|
|
216
|
+
if (entry.isDirectory())
|
|
217
|
+
pending.push(path);
|
|
218
|
+
else if (entry.isFile()) {
|
|
219
|
+
files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return files;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Describes a name for an error message, JSON-encoded and length-capped.
|
|
227
|
+
*
|
|
228
|
+
* @param name - The untrusted name to echo.
|
|
229
|
+
* @returns The encoded name, cut where it exceeds the cap.
|
|
230
|
+
*/
|
|
231
|
+
export function describeName(name) {
|
|
232
|
+
return JSON.stringify(name.length > NAME_LIMIT ? `${name.slice(0, NAME_LIMIT)}…` : name);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Reads the Vitest option table the installed CLI prints for its list command.
|
|
236
|
+
*
|
|
237
|
+
* @returns Each option keyed by its long, kebab, negated, and short spellings.
|
|
238
|
+
* @throws Thrown when the help command fails.
|
|
239
|
+
*/
|
|
240
|
+
export function readCLIOptions() {
|
|
42
241
|
const help = spawnSync(process.execPath, [VITEST, 'list', '--help', '--expand-help'], {
|
|
43
242
|
encoding: 'utf8',
|
|
44
243
|
windowsHide: true,
|
|
@@ -60,8 +259,40 @@ function readCLIOptions() {
|
|
|
60
259
|
];
|
|
61
260
|
}));
|
|
62
261
|
}
|
|
63
|
-
|
|
64
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Names the required options an option table lacks.
|
|
264
|
+
*
|
|
265
|
+
* @param options - The table `readCLIOptions` returned.
|
|
266
|
+
* @returns Each required option the table does not hold, in required order.
|
|
267
|
+
*/
|
|
268
|
+
export function readMissingOptions(options) {
|
|
269
|
+
return REQUIRED_OPTIONS.filter((name) => !options.has(name));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Reads the installed Vitest version.
|
|
273
|
+
*
|
|
274
|
+
* @returns The version string, or `unknown` when the manifest names none.
|
|
275
|
+
*/
|
|
276
|
+
export function readVitestVersion() {
|
|
277
|
+
const parsed = JSON.parse(readFileSync('node_modules/vitest/package.json', 'utf8'));
|
|
278
|
+
return typeof parsed === 'object' &&
|
|
279
|
+
parsed !== null &&
|
|
280
|
+
'version' in parsed &&
|
|
281
|
+
typeof parsed.version === 'string'
|
|
282
|
+
? parsed.version
|
|
283
|
+
: 'unknown';
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Skips the options of a `node` command so the first non-option token is the runner.
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* An option that takes a value consumes the token after it, so that token is never read as a runner.
|
|
290
|
+
*
|
|
291
|
+
* @param command - The command tokens.
|
|
292
|
+
* @param index - The index to start reading options at.
|
|
293
|
+
* @returns The index of the first non-option token, or -1 for an eval, print, or check command.
|
|
294
|
+
*/
|
|
295
|
+
export function skipRunnerOptions(command, index) {
|
|
65
296
|
const skipped = [...command.entries()].reduce((state, [position, option]) => {
|
|
66
297
|
if (state.found !== undefined || position < state.next)
|
|
67
298
|
return state;
|
|
@@ -90,7 +321,13 @@ function skipRunnerOptions(command, index) {
|
|
|
90
321
|
}, { next: index, found: undefined });
|
|
91
322
|
return skipped.found ?? skipped.next;
|
|
92
323
|
}
|
|
93
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Finds the Vitest runner in a command.
|
|
326
|
+
*
|
|
327
|
+
* @param command - The command tokens, past any environment prefix.
|
|
328
|
+
* @returns The runner's index, or -1 when the command does not run Vitest.
|
|
329
|
+
*/
|
|
330
|
+
export function findRunner(command) {
|
|
94
331
|
const executable = basename(command[0] ?? '').replace(/\.(?:exe|cmd)$/u, '');
|
|
95
332
|
const start = executable === 'npm'
|
|
96
333
|
? command[1] === 'exec'
|
|
@@ -106,9 +343,18 @@ function findRunner(command) {
|
|
|
106
343
|
? index
|
|
107
344
|
: -1;
|
|
108
345
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Keeps the tokens of a gate command that select tests and drops the run-mode options.
|
|
348
|
+
*
|
|
349
|
+
* @remarks
|
|
350
|
+
* The first `--` ends the options, so the rest of the arguments pass verbatim. A runner option that
|
|
351
|
+
* takes a value consumes the argument after it, so a step below `skip` reads nothing.
|
|
352
|
+
*
|
|
353
|
+
* @param argv - The tokens after the Vitest runner and verb.
|
|
354
|
+
* @param options - The option table `readCLIOptions` returned.
|
|
355
|
+
* @returns The tokens to pass to a listing.
|
|
356
|
+
*/
|
|
357
|
+
export function readSelection(argv, options) {
|
|
112
358
|
return argv.reduce((state, token, index) => {
|
|
113
359
|
if (state.closed || index < state.skip)
|
|
114
360
|
return state;
|
|
@@ -133,7 +379,14 @@ function readSelection(argv, options) {
|
|
|
133
379
|
return { ...state, skip: index + (valued ? 2 : 1) };
|
|
134
380
|
}, { selected: [], skip: 0, closed: false }).selected;
|
|
135
381
|
}
|
|
136
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Reads every value a named flag carries before the first `--`.
|
|
384
|
+
*
|
|
385
|
+
* @param argv - The tokens to read.
|
|
386
|
+
* @param names - The flag spellings to read.
|
|
387
|
+
* @returns Each value of the flag in either the `--flag value` or the `--flag=value` form.
|
|
388
|
+
*/
|
|
389
|
+
export function readArguments(argv, names) {
|
|
137
390
|
const end = argv.indexOf('--');
|
|
138
391
|
return (end < 0 ? argv : argv.slice(0, end)).flatMap((token, index) => {
|
|
139
392
|
const equals = token.indexOf('=');
|
|
@@ -143,7 +396,12 @@ function readArguments(argv, names) {
|
|
|
143
396
|
return value !== undefined && !value.startsWith('-') ? [value] : [];
|
|
144
397
|
});
|
|
145
398
|
}
|
|
146
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Reads the script table of the package manifest in the working directory.
|
|
401
|
+
*
|
|
402
|
+
* @returns Each script name with its command text.
|
|
403
|
+
*/
|
|
404
|
+
export function readScripts() {
|
|
147
405
|
const parsed = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
148
406
|
if (typeof parsed !== 'object' || parsed === null)
|
|
149
407
|
return {};
|
|
@@ -152,96 +410,311 @@ function readScripts() {
|
|
|
152
410
|
return {};
|
|
153
411
|
return Object.fromEntries(Object.entries(scripts).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : []));
|
|
154
412
|
}
|
|
155
|
-
|
|
156
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Drops the redirections from a command's tokens.
|
|
415
|
+
*
|
|
416
|
+
* @param tokens - The raw command tokens, quotes still attached.
|
|
417
|
+
* @returns The tokens without a redirection operator, its target, or a descriptor duplication.
|
|
418
|
+
*/
|
|
419
|
+
export function stripRedirections(tokens) {
|
|
420
|
+
return tokens.filter((token, index) => {
|
|
421
|
+
const previous = tokens[index - 1];
|
|
422
|
+
return (!(previous !== undefined && REDIRECT_OPERATOR_PATTERN.test(previous)) &&
|
|
423
|
+
!REDIRECT_OPERATOR_PATTERN.test(token) &&
|
|
424
|
+
!REDIRECT_TARGET_PATTERN.test(token));
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Drops the leading `cross-env` word and `NAME=value` assignments from a command's tokens.
|
|
429
|
+
*
|
|
430
|
+
* @param tokens - The command tokens.
|
|
431
|
+
* @returns The tokens from the first word that is neither, empty when the command is only a prefix.
|
|
432
|
+
*/
|
|
433
|
+
export function stripEnvironment(tokens) {
|
|
434
|
+
const start = tokens.findIndex((token) => token !== 'cross-env' && !ENVIRONMENT_PATTERN.test(token));
|
|
435
|
+
return start < 0 ? [] : tokens.slice(start);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Splits a script's text into its commands.
|
|
439
|
+
*
|
|
440
|
+
* @param text - The script text, with `;`, `&`, and `|` separating commands.
|
|
441
|
+
* @returns The commands as tokens, past redirections and environment prefixes, quotes removed.
|
|
442
|
+
*/
|
|
443
|
+
export function readCommands(text) {
|
|
444
|
+
const tokens = [...text.matchAll(COMMAND_TOKEN_PATTERN)].map((match) => match[0]);
|
|
157
445
|
// A separator token ends a command, so the commands are the runs of tokens between the cuts.
|
|
158
446
|
const cuts = tokens.flatMap((token, index) => (/^[;&|]+$/u.test(token) ? [index] : []));
|
|
159
|
-
return [-1, ...cuts].map((cut, at) => tokens.slice(cut + 1, cuts[at] ?? tokens.length).map((token) => token.replace(/["']/gu, '')));
|
|
447
|
+
return [-1, ...cuts].map((cut, at) => stripEnvironment(stripRedirections(tokens.slice(cut + 1, cuts[at] ?? tokens.length))).map((token) => token.replace(/["']/gu, '')));
|
|
160
448
|
}
|
|
161
|
-
|
|
449
|
+
/**
|
|
450
|
+
* Lists the script calls the commands make.
|
|
451
|
+
*
|
|
452
|
+
* @remarks
|
|
453
|
+
* Reads `npm run [-s|--silent] NAME`, `npm run-script NAME`, `npm test`, and `npm t`; the last two
|
|
454
|
+
* are shorthand, which runs a script a person also starts directly. Arguments after `--` forward.
|
|
455
|
+
*
|
|
456
|
+
* @param commands - The commands to read.
|
|
457
|
+
* @returns One invocation per script call.
|
|
458
|
+
*/
|
|
459
|
+
export function listInvocations(commands) {
|
|
162
460
|
return commands.flatMap((command) => {
|
|
163
461
|
const index = command.indexOf('npm');
|
|
164
|
-
|
|
165
|
-
if (index < 0 || command[index + 1] !== 'run' || name === undefined)
|
|
462
|
+
if (index < 0)
|
|
166
463
|
return [];
|
|
167
|
-
const
|
|
168
|
-
|
|
464
|
+
const verb = command[index + 1];
|
|
465
|
+
const separator = command.indexOf('--', index + 2);
|
|
466
|
+
const args = separator < 0 ? [] : command.slice(separator + 1);
|
|
467
|
+
if (verb === 'test' || verb === 't') {
|
|
468
|
+
return [{ name: 'test', args, ancestors: [], shorthand: true }];
|
|
469
|
+
}
|
|
470
|
+
if (verb !== 'run' && verb !== 'run-script')
|
|
471
|
+
return [];
|
|
472
|
+
const name = (separator < 0 ? command.slice(index + 2) : command.slice(index + 2, separator)).find((token) => !token.startsWith('-'));
|
|
473
|
+
return name === undefined ? [] : [{ name, args, ancestors: [], shorthand: false }];
|
|
169
474
|
});
|
|
170
475
|
}
|
|
171
|
-
|
|
476
|
+
/**
|
|
477
|
+
* Walks the scripts a root reaches, last-pushed first, each script once per forwarded arguments.
|
|
478
|
+
*
|
|
479
|
+
* @param commands - The commands of every script, by name.
|
|
480
|
+
* @param root - The root script the chain starts at.
|
|
481
|
+
* @param pending - The invocations left to visit.
|
|
482
|
+
* @param visited - The name and argument keys already visited.
|
|
483
|
+
* @returns One visit per script reached, in visit order.
|
|
484
|
+
*/
|
|
485
|
+
export function walkInvocations(commands, root, pending, visited) {
|
|
486
|
+
const invocation = pending.at(-1);
|
|
487
|
+
if (invocation === undefined)
|
|
488
|
+
return [];
|
|
489
|
+
const rest = pending.slice(0, -1);
|
|
490
|
+
const { name, args, ancestors } = invocation;
|
|
491
|
+
const key = JSON.stringify([name, args]);
|
|
492
|
+
if (visited.has(key) || ancestors.includes(name)) {
|
|
493
|
+
return walkInvocations(commands, root, rest, visited);
|
|
494
|
+
}
|
|
495
|
+
const seen = new Set([...visited, key]);
|
|
496
|
+
const script = commands[name];
|
|
497
|
+
if (script === undefined)
|
|
498
|
+
return walkInvocations(commands, root, rest, seen);
|
|
499
|
+
const expanded = script.map((command, index) => index === script.length - 1 ? [...command, ...args] : command);
|
|
500
|
+
const children = listInvocations(expanded).map((entry) => ({
|
|
501
|
+
...entry,
|
|
502
|
+
ancestors: [...ancestors, name],
|
|
503
|
+
}));
|
|
504
|
+
return [
|
|
505
|
+
{ chain: root === name ? name : `${root} > ${name}`, commands: expanded },
|
|
506
|
+
...walkInvocations(commands, root, [...rest, ...children], seen),
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Checks whether a command's options name a reporting or cache-maintenance invocation.
|
|
511
|
+
*
|
|
512
|
+
* @param before - The tokens before the first `--`.
|
|
513
|
+
* @returns True if a maintenance switch is present and not turned off.
|
|
514
|
+
*/
|
|
515
|
+
export function isMaintenance(before) {
|
|
516
|
+
return before.some((token, position) => MAINTENANCE_PATTERN.test(token) &&
|
|
517
|
+
!token.endsWith('=false') &&
|
|
518
|
+
before[position + 1] !== 'false');
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Checks whether a command's options start watch mode.
|
|
522
|
+
*
|
|
523
|
+
* @param before - The tokens before the first `--`.
|
|
524
|
+
* @returns True if `--watch` or `-w` is present and not turned off.
|
|
525
|
+
*/
|
|
526
|
+
export function isWatching(before) {
|
|
527
|
+
return before.some((token, position) => WATCH_PATTERN.test(token) && before[position + 1] !== 'false');
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Reads the gate a command makes, or none when the command does not run a test selection.
|
|
531
|
+
*
|
|
532
|
+
* @param command - The command tokens.
|
|
533
|
+
* @param chain - The root chain label.
|
|
534
|
+
* @param config - The base config path, forward-slashed and relative.
|
|
535
|
+
* @param options - The option table `readCLIOptions` returned.
|
|
536
|
+
* @returns The gate, or undefined for a command that is no Vitest run or no gate.
|
|
537
|
+
* @throws Thrown for a `related` command, which `vitest list` cannot select.
|
|
538
|
+
*/
|
|
539
|
+
export function readGate(command, chain, config, options) {
|
|
540
|
+
const index = findRunner(command);
|
|
541
|
+
if (index < 0)
|
|
542
|
+
return undefined;
|
|
543
|
+
const verb = command[index + 1];
|
|
544
|
+
if (verb === 'list' || verb === 'bench' || verb === 'watch' || verb === 'dev')
|
|
545
|
+
return undefined;
|
|
546
|
+
const tokens = command.slice(index + (verb === 'run' ? 2 : 1));
|
|
547
|
+
const separator = tokens.indexOf('--');
|
|
548
|
+
const before = separator < 0 ? tokens : tokens.slice(0, separator);
|
|
549
|
+
if (isMaintenance(before) || isWatching(before))
|
|
550
|
+
return undefined;
|
|
551
|
+
if (verb === 'related') {
|
|
552
|
+
throw new Error(`${chain}: Vitest list exposes no related command or --related option`);
|
|
553
|
+
}
|
|
554
|
+
const flags = readSelection(tokens, options);
|
|
555
|
+
const mode = readArguments(flags, ['--mode'])[0];
|
|
556
|
+
const directory = readArguments(flags, ['--root', '-r'])[0];
|
|
557
|
+
const dir = readArguments(flags, ['--dir'])[0];
|
|
558
|
+
const configured = readArguments(flags, ['--config', '-c'])[0];
|
|
559
|
+
const path = configured ?? (directory === undefined && dir === undefined ? config : undefined);
|
|
560
|
+
return {
|
|
561
|
+
config: path === undefined ? undefined : normalizeSlashes(relative(process.cwd(), resolve(path))),
|
|
562
|
+
mode: mode === 'test' ? undefined : mode,
|
|
563
|
+
...(directory === undefined ? {} : { root: directory }),
|
|
564
|
+
...(dir === undefined ? {} : { dir }),
|
|
565
|
+
projects: readArguments(flags, ['--project']),
|
|
566
|
+
chain,
|
|
567
|
+
args: configured === undefined && path !== undefined ? ['--config', path, ...flags] : flags,
|
|
568
|
+
full: before.some((flag) => FULL_PATTERN.test(flag) || /:\d+(?:-\d+)?$/u.test(flag)),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Reads the test files a `node` command runs as its arguments.
|
|
573
|
+
*
|
|
574
|
+
* @param command - The command tokens.
|
|
575
|
+
* @returns The test file arguments of `node FILE` or `node --test FILE...`; none for any other runner.
|
|
576
|
+
*/
|
|
577
|
+
export function readFileCredits(command) {
|
|
578
|
+
if (basename(command[0] ?? '').replace(/\.exe$/u, '') !== 'node')
|
|
579
|
+
return [];
|
|
580
|
+
const first = skipRunnerOptions(command, 1);
|
|
581
|
+
if (first < 0)
|
|
582
|
+
return [];
|
|
583
|
+
const candidates = command.includes('--test')
|
|
584
|
+
? command.slice(first)
|
|
585
|
+
: command.slice(first, first + 1);
|
|
586
|
+
return candidates.filter((argument) => TEST_ARGUMENT_PATTERN.test(argument));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Reads the gates and node-run test files every root script chain reaches.
|
|
590
|
+
*
|
|
591
|
+
* @param scripts - The package scripts by name.
|
|
592
|
+
* @param config - The base config path, forward-slashed and relative.
|
|
593
|
+
* @param options - The option table `readCLIOptions` returned.
|
|
594
|
+
* @returns The gates in visit order, and the earliest chain that runs each test file with `node`.
|
|
595
|
+
*/
|
|
596
|
+
export function readGates(scripts, config, options) {
|
|
172
597
|
const commands = Object.fromEntries(Object.entries(scripts).map(([name, text]) => [name, readCommands(text)]));
|
|
598
|
+
// A shorthand call (`npm test`) runs a script a person also starts directly, so it stays a root.
|
|
173
599
|
const invoked = new Set(Object.values(commands)
|
|
174
600
|
.flatMap(listInvocations)
|
|
601
|
+
.filter((entry) => !entry.shorthand)
|
|
175
602
|
.map((entry) => entry.name));
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
visited.add(key);
|
|
190
|
-
const script = commands[name];
|
|
191
|
-
if (script === undefined)
|
|
192
|
-
continue;
|
|
193
|
-
const expanded = script.map((command, index) => index === script.length - 1 ? [...command, ...args] : command);
|
|
194
|
-
for (const entry of listInvocations(expanded)) {
|
|
195
|
-
pending.push({ ...entry, ancestors: [...ancestors, name] });
|
|
196
|
-
}
|
|
197
|
-
const gate = root === name ? name : `${root} > ${name}`;
|
|
198
|
-
for (const command of expanded) {
|
|
199
|
-
const index = findRunner(command);
|
|
200
|
-
if (index >= 0) {
|
|
201
|
-
const verb = command[index + 1];
|
|
202
|
-
if (verb === 'list' || verb === 'bench')
|
|
203
|
-
continue;
|
|
204
|
-
const tokens = command.slice(index + (['run', 'watch', 'dev'].includes(verb ?? '') ? 2 : 1));
|
|
205
|
-
const separator = tokens.indexOf('--');
|
|
206
|
-
const before = separator < 0 ? tokens : tokens.slice(0, separator);
|
|
207
|
-
if (before.some((token, position) => /^(?:--mergeReports|--merge-reports|--listTags|--list-tags|--clearCache|--clear-cache)(?:=|$)/u.test(token) &&
|
|
208
|
-
!token.endsWith('=false') &&
|
|
209
|
-
before[position + 1] !== 'false'))
|
|
210
|
-
continue;
|
|
211
|
-
if (verb === 'related')
|
|
212
|
-
throw new Error(`${gate}: Vitest list exposes no related command or --related option`);
|
|
213
|
-
const flags = readSelection(tokens, options);
|
|
214
|
-
const mode = readArguments(flags, ['--mode'])[0];
|
|
215
|
-
const directory = readArguments(flags, ['--root', '-r'])[0];
|
|
216
|
-
const dir = readArguments(flags, ['--dir'])[0];
|
|
217
|
-
const configured = readArguments(flags, ['--config', '-c'])[0];
|
|
218
|
-
const path = configured ?? (directory === undefined && dir === undefined ? config : undefined);
|
|
219
|
-
units.push({
|
|
220
|
-
config: path === undefined
|
|
221
|
-
? undefined
|
|
222
|
-
: relative(process.cwd(), resolve(path)).split('\\').join('/'),
|
|
223
|
-
mode: mode === 'test' ? undefined : mode,
|
|
224
|
-
...(directory === undefined ? {} : { root: directory }),
|
|
225
|
-
...(dir === undefined ? {} : { dir }),
|
|
226
|
-
projects: readArguments(flags, ['--project']),
|
|
227
|
-
chain: gate,
|
|
228
|
-
args: configured === undefined && path !== undefined ? ['--config', path, ...flags] : flags,
|
|
229
|
-
full: before.some((flag) => /^(?:-t|--testNamePattern|--test-name-pattern|--tagsFilter|--tags-filter|--shard|--(?:no-)?allowOnly|--(?:no-)?allow-only|--(?:no-)?strictTags|--(?:no-)?strict-tags)(?:=|$)/u.test(flag) || /:\d+(?:-\d+)?$/u.test(flag)),
|
|
230
|
-
});
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
for (const argument of command) {
|
|
234
|
-
if (/^(?:tests|src|app)\/[A-Za-z0-9_./-]+\.test\.[cm]?ts$/u.test(argument) &&
|
|
235
|
-
!files.has(argument))
|
|
236
|
-
files.set(argument, gate);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
603
|
+
const visits = Object.keys(scripts)
|
|
604
|
+
.filter((name) => !invoked.has(name))
|
|
605
|
+
.flatMap((root) => walkInvocations(commands, root, [{ name: root, args: [], ancestors: [], shorthand: false }], new Set()));
|
|
606
|
+
const units = visits.flatMap((visit) => visit.commands.flatMap((command) => {
|
|
607
|
+
const gate = readGate(command, visit.chain, config, options);
|
|
608
|
+
return gate === undefined ? [] : [gate];
|
|
609
|
+
}));
|
|
610
|
+
// A Map keeps the last entry of a key, so the reversed credits leave the earliest chain.
|
|
611
|
+
const files = new Map(visits
|
|
612
|
+
.flatMap((visit) => visit.commands.flatMap((command) => findRunner(command) >= 0
|
|
613
|
+
? []
|
|
614
|
+
: readFileCredits(command).map((file) => [file, visit.chain])))
|
|
615
|
+
.toReversed());
|
|
241
616
|
return { units, files };
|
|
242
617
|
}
|
|
243
|
-
|
|
244
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Checks whether a project name is a literal name and not a Vitest pattern.
|
|
620
|
+
*
|
|
621
|
+
* @param name - The project name or pattern.
|
|
622
|
+
* @returns True if the name holds no `*` and does not open with `!`.
|
|
623
|
+
*/
|
|
624
|
+
export function isConcreteName(name) {
|
|
625
|
+
return !name.includes('*') && !name.startsWith('!');
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Checks whether a name matches a wildcard pattern the way Vitest matches a project filter.
|
|
629
|
+
*
|
|
630
|
+
* @param pattern - The pattern, where `*` matches any run of characters.
|
|
631
|
+
* @param name - The name to test.
|
|
632
|
+
* @returns True if the whole name matches, ignoring case.
|
|
633
|
+
*/
|
|
634
|
+
export function matchesWildcard(pattern, name) {
|
|
635
|
+
const body = pattern
|
|
636
|
+
.split('*')
|
|
637
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'))
|
|
638
|
+
.join('.*');
|
|
639
|
+
return new RegExp(`^${body}$`, 'iu').test(name);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Checks whether a gate's project filters reach a project name.
|
|
643
|
+
*
|
|
644
|
+
* @remarks
|
|
645
|
+
* A browser instance name such as `web (chromium)` is also tested by its base name, because Vitest
|
|
646
|
+
* keeps every instance of a project whose base name a positive filter matches and drops them all
|
|
647
|
+
* when a negative filter matches it.
|
|
648
|
+
*
|
|
649
|
+
* @param filters - The gate's `--project` values, where a leading `!` negates.
|
|
650
|
+
* @param name - The concrete project name.
|
|
651
|
+
* @returns True if no negative filter matches and some positive filter does, or none is positive.
|
|
652
|
+
*/
|
|
653
|
+
export function matchesProjectFilter(filters, name) {
|
|
654
|
+
const names = [name, name.replace(/ \([^)]*\)$/u, '')];
|
|
655
|
+
const negative = filters
|
|
656
|
+
.filter((filter) => filter.startsWith('!'))
|
|
657
|
+
.map((filter) => filter.slice(1));
|
|
658
|
+
const positive = filters.filter((filter) => !filter.startsWith('!'));
|
|
659
|
+
if (negative.some((pattern) => names.some((candidate) => matchesWildcard(pattern, candidate)))) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
return (positive.length === 0 ||
|
|
663
|
+
positive.some((pattern) => names.some((candidate) => matchesWildcard(pattern, candidate))));
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Replaces the `--project` flags of a gate's arguments.
|
|
667
|
+
*
|
|
668
|
+
* @param args - The gate arguments.
|
|
669
|
+
* @param projects - The project names to pass in place of the existing flags.
|
|
670
|
+
* @returns The arguments with one `--project NAME` pair per name, before any bare `--`.
|
|
671
|
+
*/
|
|
672
|
+
export function replaceProjects(args, projects) {
|
|
673
|
+
const end = args.indexOf('--');
|
|
674
|
+
const head = end < 0 ? args : args.slice(0, end);
|
|
675
|
+
const tail = end < 0 ? [] : args.slice(end);
|
|
676
|
+
const kept = head.filter((token, index) => token !== '--project' && !token.startsWith('--project=') && head[index - 1] !== '--project');
|
|
677
|
+
return [...kept, ...projects.flatMap((project) => ['--project', project]), ...tail];
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Narrows the gates to the named projects, so no listing runs for a project outside the names.
|
|
681
|
+
*
|
|
682
|
+
* @remarks
|
|
683
|
+
* A gate with no project filter lists the named projects alone. A gate that names projects lists the
|
|
684
|
+
* named ones its filters reach and is dropped when it reaches none. A pattern among the names
|
|
685
|
+
* (`*`, or a leading `!`) cannot be matched beforehand, so a gate that names projects keeps them.
|
|
686
|
+
*
|
|
687
|
+
* @param gates - The gates the root chains reach.
|
|
688
|
+
* @param named - The `--projects` names, empty for no narrowing.
|
|
689
|
+
* @returns The gates to list, each with its narrowed arguments.
|
|
690
|
+
*/
|
|
691
|
+
export function narrowGates(gates, named) {
|
|
692
|
+
if (named.length === 0)
|
|
693
|
+
return gates;
|
|
694
|
+
const concrete = named.filter(isConcreteName);
|
|
695
|
+
const patterned = concrete.length < named.length;
|
|
696
|
+
return gates.flatMap((gate) => {
|
|
697
|
+
if (gate.projects.length === 0)
|
|
698
|
+
return [{ ...gate, args: replaceProjects(gate.args, named) }];
|
|
699
|
+
if (patterned)
|
|
700
|
+
return [gate];
|
|
701
|
+
const reached = concrete.filter((name) => matchesProjectFilter(gate.projects, name));
|
|
702
|
+
return reached.length === 0 ? [] : [{ ...gate, args: replaceProjects(gate.args, reached) }];
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Lists the tests one Vitest listing collects.
|
|
707
|
+
*
|
|
708
|
+
* @param args - The arguments to pass after `vitest list`.
|
|
709
|
+
* @param full - True to collect each test with its name and location; false for files only.
|
|
710
|
+
* @param cache - The listings already read, keyed by their arguments.
|
|
711
|
+
* @returns The tests and whether the project filter matched, or undefined when Vitest refuses.
|
|
712
|
+
*/
|
|
713
|
+
export function listCollected(args, full, cache) {
|
|
714
|
+
const parameters = [
|
|
715
|
+
...(full ? ['--includeTaskLocation', '--staticParse=false'] : ['--filesOnly']),
|
|
716
|
+
...args,
|
|
717
|
+
];
|
|
245
718
|
const key = JSON.stringify(parameters);
|
|
246
719
|
const cached = cache.get(key);
|
|
247
720
|
if (cached !== undefined)
|
|
@@ -257,9 +730,11 @@ function listCollected(args, full, cache) {
|
|
|
257
730
|
});
|
|
258
731
|
if (args.includes('--project') &&
|
|
259
732
|
result.status === 1 &&
|
|
260
|
-
result.stderr.includes('No projects matched the filter "')
|
|
261
|
-
|
|
262
|
-
|
|
733
|
+
(result.stderr.includes('No projects matched the filter "') ||
|
|
734
|
+
result.stderr.includes('The filter matched no projects:'))) {
|
|
735
|
+
const unmatched = { entries: [], matched: false };
|
|
736
|
+
cache.set(key, unmatched);
|
|
737
|
+
return unmatched;
|
|
263
738
|
}
|
|
264
739
|
if (result.status !== 0 || !existsSync(listing)) {
|
|
265
740
|
const diagnostic = result.stderr || result.error?.message || result.stdout;
|
|
@@ -287,7 +762,7 @@ function listCollected(args, full, cache) {
|
|
|
287
762
|
}
|
|
288
763
|
return [
|
|
289
764
|
{
|
|
290
|
-
file: relative(process.cwd(), record.file)
|
|
765
|
+
file: normalizeSlashes(relative(process.cwd(), record.file)),
|
|
291
766
|
projectName: record.projectName ?? '',
|
|
292
767
|
name: typeof record.name === 'string' ? record.name : undefined,
|
|
293
768
|
line: typeof location.line === 'number' ? location.line : undefined,
|
|
@@ -295,8 +770,9 @@ function listCollected(args, full, cache) {
|
|
|
295
770
|
},
|
|
296
771
|
];
|
|
297
772
|
});
|
|
298
|
-
|
|
299
|
-
|
|
773
|
+
const listed = { entries: collected, matched: true };
|
|
774
|
+
cache.set(key, listed);
|
|
775
|
+
return listed;
|
|
300
776
|
}
|
|
301
777
|
catch (error) {
|
|
302
778
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -306,86 +782,340 @@ function listCollected(args, full, cache) {
|
|
|
306
782
|
rmSync(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
307
783
|
}
|
|
308
784
|
}
|
|
309
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Lists the test and spec files beneath a directory.
|
|
787
|
+
*
|
|
788
|
+
* @param directory - The directory to walk; a missing directory holds none.
|
|
789
|
+
* @returns The forward-slash paths of every test or spec file, skipping only `node_modules` and `.git`.
|
|
790
|
+
*/
|
|
791
|
+
export function listTestFiles(directory) {
|
|
310
792
|
if (!existsSync(directory))
|
|
311
793
|
return [];
|
|
312
|
-
return listFiles(directory)
|
|
794
|
+
return listFiles(directory, UNDISCOVERED_SKIPS)
|
|
313
795
|
.map((file) => file.path)
|
|
314
|
-
.filter((path) =>
|
|
796
|
+
.filter((path) => DISCOVERABLE_PATTERN.test(path));
|
|
315
797
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
798
|
+
/**
|
|
799
|
+
* Scans source text into tokens that tell code from comments and strings.
|
|
800
|
+
*
|
|
801
|
+
* @remarks
|
|
802
|
+
* Comments are dropped. A string, a template (whole, with its expressions), and a regular expression
|
|
803
|
+
* literal each become one token. A slash opens a regular expression after an operator, an opening
|
|
804
|
+
* bracket, or a keyword, and divides after a word, a literal, or a closing bracket. The scanner
|
|
805
|
+
* pairs each opening bracket with its closing bracket as it reads.
|
|
806
|
+
*/
|
|
807
|
+
export class CodeScanner {
|
|
808
|
+
#text;
|
|
809
|
+
#tokens = [];
|
|
810
|
+
#closes = new Map();
|
|
811
|
+
#opened = [];
|
|
812
|
+
#position = 0;
|
|
813
|
+
#last = undefined;
|
|
814
|
+
constructor(text) {
|
|
815
|
+
this.#text = text;
|
|
816
|
+
}
|
|
817
|
+
scan() {
|
|
818
|
+
while (this.#position < this.#text.length) {
|
|
819
|
+
const token = this.#read();
|
|
820
|
+
if (token === undefined)
|
|
821
|
+
continue;
|
|
822
|
+
if (token.kind === 'punct' && OPENERS.includes(token.text)) {
|
|
823
|
+
this.#opened.push(this.#tokens.length);
|
|
824
|
+
}
|
|
825
|
+
if (token.kind === 'punct' && CLOSERS.includes(token.text)) {
|
|
826
|
+
const open = this.#opened.pop();
|
|
827
|
+
if (open !== undefined)
|
|
828
|
+
this.#closes.set(open, this.#tokens.length);
|
|
829
|
+
}
|
|
830
|
+
this.#tokens.push(token);
|
|
831
|
+
}
|
|
832
|
+
return { tokens: this.#tokens, closes: this.#closes };
|
|
833
|
+
}
|
|
834
|
+
#read() {
|
|
835
|
+
const rest = this.#text.slice(this.#position);
|
|
836
|
+
const skipped = /^(?:\s+|\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$))/u.exec(rest);
|
|
837
|
+
if (skipped !== null) {
|
|
838
|
+
this.#position += skipped[0].length;
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
if (rest.startsWith('`'))
|
|
842
|
+
return this.#take('template', this.#readTemplate());
|
|
843
|
+
const quoted = STRING_PATTERN.exec(rest);
|
|
844
|
+
if (quoted !== null)
|
|
845
|
+
return this.#take('string', quoted[0]);
|
|
846
|
+
if (rest.startsWith('/') && this.#expectsRegex()) {
|
|
847
|
+
const regex = REGEX_PATTERN.exec(rest);
|
|
848
|
+
if (regex !== null)
|
|
849
|
+
return this.#take('regex', regex[0]);
|
|
850
|
+
}
|
|
851
|
+
const word = WORD_PATTERN.exec(rest);
|
|
852
|
+
if (word !== null)
|
|
853
|
+
return this.#take('word', word[0]);
|
|
854
|
+
return this.#take('punct', rest.slice(0, 1));
|
|
855
|
+
}
|
|
856
|
+
#take(kind, text) {
|
|
857
|
+
if (kind !== 'template')
|
|
858
|
+
this.#position += text.length;
|
|
859
|
+
const token = { kind, text };
|
|
860
|
+
this.#last = token;
|
|
861
|
+
return token;
|
|
862
|
+
}
|
|
863
|
+
#expectsRegex() {
|
|
864
|
+
const last = this.#last;
|
|
865
|
+
if (last === undefined)
|
|
866
|
+
return true;
|
|
867
|
+
if (last.kind === 'punct')
|
|
868
|
+
return !CLOSERS.includes(last.text);
|
|
869
|
+
if (last.kind === 'word')
|
|
870
|
+
return REGEX_KEYWORDS.has(last.text);
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
#readTemplate() {
|
|
874
|
+
const start = this.#position;
|
|
875
|
+
this.#position += 1;
|
|
876
|
+
while (this.#position < this.#text.length) {
|
|
877
|
+
const character = this.#text[this.#position];
|
|
878
|
+
if (character === '\\') {
|
|
879
|
+
this.#position += 2;
|
|
880
|
+
}
|
|
881
|
+
else if (character === '`') {
|
|
882
|
+
this.#position += 1;
|
|
883
|
+
return this.#text.slice(start, this.#position);
|
|
884
|
+
}
|
|
885
|
+
else if (this.#text.startsWith('${', this.#position)) {
|
|
886
|
+
this.#position += 2;
|
|
887
|
+
this.#skipExpression();
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
this.#position += 1;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return this.#text.slice(start, this.#position);
|
|
894
|
+
}
|
|
895
|
+
#skipExpression() {
|
|
896
|
+
while (this.#position < this.#text.length) {
|
|
897
|
+
const token = this.#read();
|
|
898
|
+
if (token?.kind !== 'punct')
|
|
899
|
+
continue;
|
|
900
|
+
if (token.text === '}')
|
|
901
|
+
return;
|
|
902
|
+
if (token.text === '{')
|
|
903
|
+
this.#skipExpression();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
319
906
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
907
|
+
/**
|
|
908
|
+
* Checks whether a token is a punctuation mark with the given text.
|
|
909
|
+
*
|
|
910
|
+
* @param token - The token, or undefined past the end.
|
|
911
|
+
* @param text - The punctuation text.
|
|
912
|
+
* @returns True if the token is that punctuation mark.
|
|
913
|
+
*/
|
|
914
|
+
export function isPunct(token, text) {
|
|
915
|
+
return token?.kind === 'punct' && token.text === text;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Reads the modifier chain after a registrar, up to its registration call.
|
|
919
|
+
*
|
|
920
|
+
* @param scan - The scanned source.
|
|
921
|
+
* @param at - The index after the registrar or the last chain element.
|
|
922
|
+
* @param names - The modifier names read so far; an empty name marks a consumed argument group.
|
|
923
|
+
* @returns The names and the index of the registration call's opening parenthesis, or undefined when
|
|
924
|
+
* the tokens are no registration call.
|
|
925
|
+
*/
|
|
926
|
+
export function readChain(scan, at, names) {
|
|
927
|
+
const token = scan.tokens[at];
|
|
928
|
+
if (token === undefined)
|
|
338
929
|
return undefined;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (rescans === undefined)
|
|
930
|
+
const last = names.at(-1) ?? '';
|
|
931
|
+
if (isPunct(token, '.')) {
|
|
932
|
+
const name = scan.tokens[at + 1];
|
|
933
|
+
return name?.kind === 'word' ? readChain(scan, at + 2, [...names, name.text]) : undefined;
|
|
934
|
+
}
|
|
935
|
+
if (token.kind === 'template' && ARGUMENT_MODIFIERS.has(last)) {
|
|
936
|
+
return readChain(scan, at + 1, [...names, '']);
|
|
937
|
+
}
|
|
938
|
+
if (!isPunct(token, '('))
|
|
349
939
|
return undefined;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
]);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
940
|
+
if (!ARGUMENT_MODIFIERS.has(last))
|
|
941
|
+
return { names, call: at };
|
|
942
|
+
const close = scan.closes.get(at);
|
|
943
|
+
return close === undefined ? undefined : readChain(scan, close + 1, [...names, '']);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Splits a token range into its top-level arguments, jumping over bracket groups.
|
|
947
|
+
*
|
|
948
|
+
* @param scan - The scanned source.
|
|
949
|
+
* @param start - The index the current argument starts at.
|
|
950
|
+
* @param at - The index being read.
|
|
951
|
+
* @param end - The index the range ends at, excluded.
|
|
952
|
+
* @returns One span per non-empty argument.
|
|
953
|
+
*/
|
|
954
|
+
export function splitArguments(scan, start, at, end) {
|
|
955
|
+
if (at >= end)
|
|
956
|
+
return start < end ? [{ start, end }] : [];
|
|
957
|
+
const token = scan.tokens[at];
|
|
958
|
+
if (token?.kind === 'punct' && OPENERS.includes(token.text)) {
|
|
959
|
+
return splitArguments(scan, start, (scan.closes.get(at) ?? at) + 1, end);
|
|
960
|
+
}
|
|
961
|
+
if (isPunct(token, ',')) {
|
|
962
|
+
return [{ start, end: at }, ...splitArguments(scan, at + 1, at + 1, end)];
|
|
963
|
+
}
|
|
964
|
+
return splitArguments(scan, start, at + 1, end);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Reads the property keys at the top level of an object literal's token range.
|
|
968
|
+
*
|
|
969
|
+
* @param scan - The scanned source.
|
|
970
|
+
* @param at - The index being read.
|
|
971
|
+
* @param end - The index the range ends at, excluded.
|
|
972
|
+
* @returns Each key that follows `{` or `,` and precedes `:`, `,`, or `}`, quotes removed.
|
|
973
|
+
*/
|
|
974
|
+
export function readKeys(scan, at, end) {
|
|
975
|
+
const token = scan.tokens[at];
|
|
976
|
+
if (token === undefined || at >= end)
|
|
977
|
+
return [];
|
|
978
|
+
if (token.kind === 'punct' && OPENERS.includes(token.text)) {
|
|
979
|
+
return readKeys(scan, (scan.closes.get(at) ?? at) + 1, end);
|
|
980
|
+
}
|
|
981
|
+
const previous = scan.tokens[at - 1];
|
|
982
|
+
const following = scan.tokens[at + 1];
|
|
983
|
+
const spelled = token.kind === 'word'
|
|
984
|
+
? token.text
|
|
985
|
+
: token.kind === 'string'
|
|
986
|
+
? token.text.slice(1, -1)
|
|
987
|
+
: undefined;
|
|
988
|
+
const isKey = spelled !== undefined &&
|
|
989
|
+
(isPunct(previous, '{') || isPunct(previous, ',')) &&
|
|
990
|
+
(isPunct(following, ':') || isPunct(following, ',') || isPunct(following, '}'));
|
|
991
|
+
return [...(isKey ? [spelled] : []), ...readKeys(scan, at + 1, end)];
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Reads the retry and timeout markers a registration's arguments carry.
|
|
995
|
+
*
|
|
996
|
+
* @param scan - The scanned source.
|
|
997
|
+
* @param spans - The registration call's top-level arguments.
|
|
998
|
+
* @returns A `timeout` entry for a numeric last argument after the title, and an entry for each
|
|
999
|
+
* `retry` or `timeout` key of an object-literal argument.
|
|
1000
|
+
*/
|
|
1001
|
+
export function readArgumentMarkers(scan, spans) {
|
|
1002
|
+
const last = spans.at(-1);
|
|
1003
|
+
const lastToken = last === undefined ? undefined : scan.tokens[last.start];
|
|
1004
|
+
const numeric = spans.length >= 2 &&
|
|
1005
|
+
last !== undefined &&
|
|
1006
|
+
last.end - last.start === 1 &&
|
|
1007
|
+
lastToken?.kind === 'word' &&
|
|
1008
|
+
NUMBER_PATTERN.test(lastToken.text);
|
|
1009
|
+
const keys = spans.flatMap((span) => isPunct(scan.tokens[span.start], '{') && scan.closes.get(span.start) === span.end - 1
|
|
1010
|
+
? readKeys(scan, span.start + 1, span.end - 1)
|
|
1011
|
+
: []);
|
|
1012
|
+
return [
|
|
1013
|
+
...(numeric ? ['timeout'] : []),
|
|
1014
|
+
...keys.filter((key) => key === 'retry' || key === 'timeout'),
|
|
1015
|
+
];
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Reads the markers one registration carries.
|
|
1019
|
+
*
|
|
1020
|
+
* @param scan - The scanned source.
|
|
1021
|
+
* @param index - The index of the registrar identifier.
|
|
1022
|
+
* @returns The chain markers (`.todo`, `.skipIf`, `.runIf`, `.fails`) and the argument markers.
|
|
1023
|
+
*/
|
|
1024
|
+
export function readHits(scan, index) {
|
|
1025
|
+
const chain = readChain(scan, index + 1, []);
|
|
1026
|
+
if (chain === undefined)
|
|
1027
|
+
return [];
|
|
1028
|
+
const close = scan.closes.get(chain.call);
|
|
1029
|
+
const spans = close === undefined ? [] : splitArguments(scan, chain.call + 1, chain.call + 1, close);
|
|
1030
|
+
return [
|
|
1031
|
+
...chain.names.filter((name) => CHAIN_MARKERS.has(name)).map((name) => `.${name}`),
|
|
1032
|
+
...readArgumentMarkers(scan, spans),
|
|
1033
|
+
];
|
|
358
1034
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1035
|
+
/**
|
|
1036
|
+
* Counts the markers a scanned test file carries, as call forms outside comments and strings.
|
|
1037
|
+
*
|
|
1038
|
+
* @param scan - The scanned source.
|
|
1039
|
+
* @returns Each marker with a nonzero count, in marker order.
|
|
1040
|
+
*/
|
|
1041
|
+
export function readMarkers(scan) {
|
|
1042
|
+
const hits = scan.tokens.flatMap((token, index) => token.kind === 'word' && REGISTRARS.has(token.text) && !isPunct(scan.tokens[index - 1], '.')
|
|
1043
|
+
? readHits(scan, index)
|
|
1044
|
+
: []);
|
|
1045
|
+
return Object.fromEntries(MARKERS.map((marker) => [marker, hits.filter((hit) => hit === marker).length]).filter(([, count]) => count > 0));
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Counts the markers of a test file on disk.
|
|
1049
|
+
*
|
|
1050
|
+
* @param file - The test file path.
|
|
1051
|
+
* @returns Each marker with a nonzero count.
|
|
1052
|
+
*/
|
|
1053
|
+
export function countMarkers(file) {
|
|
1054
|
+
return readMarkers(new CodeScanner(readFileSync(file, 'utf8')).scan());
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Runs one read per item in order and stops at the first refusal, so no later read spawns.
|
|
1058
|
+
*
|
|
1059
|
+
* @param items - The items to read.
|
|
1060
|
+
* @param read - The read for one item; undefined refuses.
|
|
1061
|
+
* @returns The readings in order, or undefined once any read refuses.
|
|
1062
|
+
*/
|
|
1063
|
+
export function collectResults(items, read) {
|
|
1064
|
+
return items.reduce((results, item) => {
|
|
1065
|
+
if (results === undefined)
|
|
363
1066
|
return undefined;
|
|
364
|
-
const
|
|
365
|
-
return
|
|
1067
|
+
const result = read(item);
|
|
1068
|
+
return result === undefined ? undefined : [...results, result];
|
|
366
1069
|
}, []);
|
|
367
1070
|
}
|
|
368
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Checks whether two units run under the same config, mode, root, and dir.
|
|
1073
|
+
*
|
|
1074
|
+
* @param left - The first unit.
|
|
1075
|
+
* @param right - The second unit.
|
|
1076
|
+
* @returns True if every field is equal.
|
|
1077
|
+
*/
|
|
1078
|
+
export function isSameUnit(left, right) {
|
|
369
1079
|
return (left.config === right.config &&
|
|
370
1080
|
left.mode === right.mode &&
|
|
371
1081
|
left.root === right.root &&
|
|
372
1082
|
left.dir === right.dir);
|
|
373
1083
|
}
|
|
374
|
-
|
|
1084
|
+
/**
|
|
1085
|
+
* Builds the arguments that select a unit's config, mode, root, and dir.
|
|
1086
|
+
*
|
|
1087
|
+
* @param unit - The unit.
|
|
1088
|
+
* @returns The arguments to pass to `vitest list`.
|
|
1089
|
+
*/
|
|
1090
|
+
export function readUnitArguments(unit) {
|
|
1091
|
+
return [
|
|
1092
|
+
...(unit.config === undefined ? [] : ['--config', unit.config]),
|
|
1093
|
+
...(unit.mode === undefined ? [] : ['--mode', unit.mode]),
|
|
1094
|
+
...(unit.root === undefined ? [] : ['--root', unit.root]),
|
|
1095
|
+
...(unit.dir === undefined ? [] : ['--dir', unit.dir]),
|
|
1096
|
+
];
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Reads one unit's universe, gate selections, and project readings.
|
|
1100
|
+
*
|
|
1101
|
+
* @param unit - The unit to read.
|
|
1102
|
+
* @param named - The `--projects` names; the universe and every listing carry them when present.
|
|
1103
|
+
* @param gates - The narrowed gates.
|
|
1104
|
+
* @param cache - The listings already read.
|
|
1105
|
+
* @returns The unit's reading, or undefined when a listing refuses.
|
|
1106
|
+
*/
|
|
1107
|
+
export function readUnit(unit, named, gates, cache) {
|
|
375
1108
|
const reached = gates.units.filter((gate) => isSameUnit(gate, unit));
|
|
376
1109
|
const parameters = readUnitArguments(unit);
|
|
377
|
-
const
|
|
378
|
-
|
|
1110
|
+
const universeArguments = [...parameters, ...named.flatMap((name) => ['--project', name])];
|
|
1111
|
+
const universe = listCollected(universeArguments, true, cache);
|
|
1112
|
+
if (universe === undefined)
|
|
379
1113
|
return undefined;
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (named.length > 0 && scope === undefined)
|
|
384
|
-
return undefined;
|
|
385
|
-
const selected = collectReadings(reached, (gate) => {
|
|
386
|
-
const selection = JSON.stringify(gate.args) === JSON.stringify(parameters)
|
|
1114
|
+
const collected = universe.entries;
|
|
1115
|
+
const selected = collectResults(reached, (gate) => {
|
|
1116
|
+
const selection = JSON.stringify(gate.args) === JSON.stringify(universeArguments)
|
|
387
1117
|
? collected
|
|
388
|
-
: listCollected(gate.args, gate.full, cache);
|
|
1118
|
+
: listCollected(gate.args, gate.full, cache)?.entries;
|
|
389
1119
|
if (selection === undefined) {
|
|
390
1120
|
console.error(`discovery: gate ${gate.chain}: vitest list refused ${gate.args.join(' ')}`);
|
|
391
1121
|
return undefined;
|
|
@@ -401,7 +1131,8 @@ function readUnit(unit, named, gates, cache) {
|
|
|
401
1131
|
});
|
|
402
1132
|
if (selected === undefined)
|
|
403
1133
|
return undefined;
|
|
404
|
-
const
|
|
1134
|
+
const candidates = named.length === 0 ? reached.flatMap((gate) => gate.projects) : named;
|
|
1135
|
+
const projects = collectResults([...new Set(candidates)].filter(isConcreteName), (project) => {
|
|
405
1136
|
if (selected.some(({ gate, identities }) => gate.projects.length === 1 && gate.projects[0] === project && identities.size > 0)) {
|
|
406
1137
|
return { project, populated: true, empty: false };
|
|
407
1138
|
}
|
|
@@ -410,21 +1141,18 @@ function readUnit(unit, named, gates, cache) {
|
|
|
410
1141
|
return undefined;
|
|
411
1142
|
return {
|
|
412
1143
|
project,
|
|
413
|
-
populated: selection.length > 0,
|
|
414
|
-
empty: selection.length === 0 &&
|
|
1144
|
+
populated: selection.entries.length > 0,
|
|
1145
|
+
empty: selection.entries.length === 0 &&
|
|
415
1146
|
(reached.some((gate) => gate.projects.includes(project)) || named.includes(project)) &&
|
|
416
1147
|
(named.length === 0 || named.includes(project)),
|
|
417
1148
|
};
|
|
418
1149
|
});
|
|
419
|
-
if (
|
|
1150
|
+
if (projects === undefined)
|
|
420
1151
|
return undefined;
|
|
421
1152
|
return {
|
|
422
1153
|
listing: {
|
|
423
1154
|
...unit,
|
|
424
|
-
collected: collected
|
|
425
|
-
.filter((entry) => scope === undefined ||
|
|
426
|
-
scope.some((chosen) => chosen.file === entry.file && chosen.projectName === entry.projectName))
|
|
427
|
-
.map((entry) => ({
|
|
1155
|
+
collected: collected.map((entry) => ({
|
|
428
1156
|
...entry,
|
|
429
1157
|
gate: selected.find(({ gate, identities }) => identities.has(JSON.stringify([
|
|
430
1158
|
entry.file,
|
|
@@ -432,25 +1160,93 @@ function readUnit(unit, named, gates, cache) {
|
|
|
432
1160
|
...(gate.full ? [entry.name, entry.line, entry.column] : []),
|
|
433
1161
|
])))?.gate.chain,
|
|
434
1162
|
})),
|
|
435
|
-
empty:
|
|
1163
|
+
empty: projects.filter(({ empty }) => empty).map(({ project }) => project),
|
|
436
1164
|
},
|
|
437
|
-
populated:
|
|
1165
|
+
populated: projects.filter(({ populated }) => populated).map(({ project }) => project),
|
|
438
1166
|
};
|
|
439
1167
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1168
|
+
/**
|
|
1169
|
+
* Reads the listing of every unit the narrowed gates reach.
|
|
1170
|
+
*
|
|
1171
|
+
* @param config - The base config path.
|
|
1172
|
+
* @param named - The `--projects` names.
|
|
1173
|
+
* @param gates - The narrowed gates.
|
|
1174
|
+
* @returns One listing per unit, or undefined when a listing refuses.
|
|
1175
|
+
*/
|
|
1176
|
+
export function collectListings(config, named, gates) {
|
|
1177
|
+
const base = { config, mode: undefined };
|
|
1178
|
+
// A gate joins the units unless the base unit or an earlier gate already names the same unit.
|
|
1179
|
+
const units = [
|
|
1180
|
+
base,
|
|
1181
|
+
...gates.units
|
|
1182
|
+
.filter((gate, index) => ![base, ...gates.units.slice(0, index)].some((unit) => isSameUnit(unit, gate)))
|
|
1183
|
+
.map((gate) => ({
|
|
1184
|
+
config: gate.config,
|
|
1185
|
+
mode: gate.mode,
|
|
1186
|
+
...(gate.root === undefined ? {} : { root: gate.root }),
|
|
1187
|
+
...(gate.dir === undefined ? {} : { dir: gate.dir }),
|
|
1188
|
+
})),
|
|
446
1189
|
];
|
|
1190
|
+
// A listing is a memo keyed by its arguments: each key is written once, on its first miss.
|
|
1191
|
+
const cache = new Map();
|
|
1192
|
+
const results = collectResults(units, (unit) => readUnit(unit, named, gates, cache));
|
|
1193
|
+
if (results === undefined)
|
|
1194
|
+
return undefined;
|
|
1195
|
+
const listings = results.map(({ listing }) => listing);
|
|
1196
|
+
const populated = new Set(results.flatMap((result) => result.populated));
|
|
1197
|
+
// A project empty under one unit may still list tests under another, so each unit is asked for it.
|
|
1198
|
+
const rescans = collectResults([...new Set(listings.flatMap((listing) => listing.empty))]
|
|
1199
|
+
.filter((name) => !populated.has(name))
|
|
1200
|
+
.flatMap((name) => units.map((unit) => ({ name, unit }))), ({ name, unit }) => {
|
|
1201
|
+
const selection = listCollected([...readUnitArguments(unit), '--project', name], false, cache);
|
|
1202
|
+
return selection === undefined ? undefined : { name, listed: selection.entries.length > 0 };
|
|
1203
|
+
});
|
|
1204
|
+
if (rescans === undefined)
|
|
1205
|
+
return undefined;
|
|
1206
|
+
const resolved = new Set([
|
|
1207
|
+
...populated,
|
|
1208
|
+
...rescans.filter(({ listed }) => listed).map(({ name }) => name),
|
|
1209
|
+
]);
|
|
1210
|
+
return listings.map((listing) => ({
|
|
1211
|
+
...listing,
|
|
1212
|
+
empty: listing.empty.filter((name) => !resolved.has(name)),
|
|
1213
|
+
}));
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Names the `--projects` entries the base config's projects do not match.
|
|
1217
|
+
*
|
|
1218
|
+
* @param unit - The base unit.
|
|
1219
|
+
* @param named - The `--projects` names.
|
|
1220
|
+
* @param cache - The listings already read.
|
|
1221
|
+
* @returns The names Vitest matched to no project, or undefined when a listing refuses.
|
|
1222
|
+
*/
|
|
1223
|
+
export function readUnmatched(unit, named, cache) {
|
|
1224
|
+
const matches = collectResults(named, (name) => {
|
|
1225
|
+
const listed = listCollected([...readUnitArguments(unit), '--project', name], false, cache);
|
|
1226
|
+
return listed === undefined ? undefined : { name, matched: listed.matched };
|
|
1227
|
+
});
|
|
1228
|
+
return matches?.filter(({ matched }) => !matched).map(({ name }) => name);
|
|
447
1229
|
}
|
|
448
|
-
|
|
1230
|
+
/**
|
|
1231
|
+
* Identifies a collected test by its file, project, name, and location.
|
|
1232
|
+
*
|
|
1233
|
+
* @param entry - The collected test.
|
|
1234
|
+
* @returns A key two listings share for the same test.
|
|
1235
|
+
*/
|
|
1236
|
+
export function identifyCollected(entry) {
|
|
449
1237
|
return JSON.stringify([entry.file, entry.projectName, entry.name, entry.line, entry.column]);
|
|
450
1238
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
1239
|
+
/**
|
|
1240
|
+
* Merges the tests of several listings into one list.
|
|
1241
|
+
*
|
|
1242
|
+
* @remarks
|
|
1243
|
+
* The same test listed twice in one listing is two occurrences, and the same occurrence listed by
|
|
1244
|
+
* two listings is one test: the last listing's fields win, and the gate is the last one any named.
|
|
1245
|
+
*
|
|
1246
|
+
* @param listings - The listings to merge.
|
|
1247
|
+
* @returns The merged tests.
|
|
1248
|
+
*/
|
|
1249
|
+
export function mergeCollected(listings) {
|
|
454
1250
|
const keyed = listings.flatMap((listing) => {
|
|
455
1251
|
const positions = new Map([...Map.groupBy(listing.collected, identifyCollected).values()].flatMap((group) => group.map((entry, position) => [entry, position + 1])));
|
|
456
1252
|
return listing.collected.map((entry) => ({
|
|
@@ -470,7 +1266,13 @@ function mergeCollected(listings) {
|
|
|
470
1266
|
];
|
|
471
1267
|
});
|
|
472
1268
|
}
|
|
473
|
-
|
|
1269
|
+
/**
|
|
1270
|
+
* Reads the modification times of every test file the census can collect.
|
|
1271
|
+
*
|
|
1272
|
+
* @param gates - The narrowed gates, whose roots and dirs add watched directories.
|
|
1273
|
+
* @returns A JSON text two reads share when no test file changed.
|
|
1274
|
+
*/
|
|
1275
|
+
export function readSnapshot(gates) {
|
|
474
1276
|
const bases = [
|
|
475
1277
|
process.cwd(),
|
|
476
1278
|
...gates.units.flatMap((gate) => [gate.root, gate.dir].filter((path) => path !== undefined).map((path) => resolve(path))),
|
|
@@ -479,12 +1281,22 @@ function readSnapshot(gates) {
|
|
|
479
1281
|
// A later listing of the same path replaces the earlier modification time at the earlier position.
|
|
480
1282
|
const files = new Map([...roots].flatMap((root) => existsSync(root)
|
|
481
1283
|
? listFiles(root)
|
|
482
|
-
.filter((file) =>
|
|
1284
|
+
.filter((file) => TEST_FILE_PATTERN.test(file.path))
|
|
483
1285
|
.map((file) => [file.path, file.modified])
|
|
484
1286
|
: []));
|
|
485
1287
|
return JSON.stringify([...files].sort(([left], [right]) => left.localeCompare(right)));
|
|
486
1288
|
}
|
|
487
|
-
|
|
1289
|
+
/**
|
|
1290
|
+
* Reads the listings, repeating once when a test file changed during the first read.
|
|
1291
|
+
*
|
|
1292
|
+
* @param config - The base config path.
|
|
1293
|
+
* @param named - The `--projects` names.
|
|
1294
|
+
* @param gates - The narrowed gates.
|
|
1295
|
+
* @param attempt - The attempt number, zero for the first.
|
|
1296
|
+
* @returns One listing per unit, or undefined when a listing refuses.
|
|
1297
|
+
* @throws Thrown when test files changed during both attempts.
|
|
1298
|
+
*/
|
|
1299
|
+
export function collectStableListings(config, named, gates, attempt = 0) {
|
|
488
1300
|
const before = readSnapshot(gates);
|
|
489
1301
|
const listings = collectListings(config, named, gates);
|
|
490
1302
|
if (before === readSnapshot(gates))
|
|
@@ -493,7 +1305,18 @@ function collectStableListings(config, named, gates, attempt = 0) {
|
|
|
493
1305
|
throw new Error('Test files changed during both census attempts');
|
|
494
1306
|
return collectStableListings(config, named, gates, attempt + 1);
|
|
495
1307
|
}
|
|
496
|
-
|
|
1308
|
+
/**
|
|
1309
|
+
* Runs the census and prints its report.
|
|
1310
|
+
*
|
|
1311
|
+
* @param argv - The arguments after the script path.
|
|
1312
|
+
* @returns The exit code: 0 no flag, 3 a flag, 2 a tool failure, 64 usage.
|
|
1313
|
+
*/
|
|
1314
|
+
export function main(argv) {
|
|
1315
|
+
const unknown = readUnknownFlags(argv, FLAGS);
|
|
1316
|
+
if (unknown.length > 0) {
|
|
1317
|
+
console.error(`discovery: ${unknown.map(describeName).join(', ')} is not a flag this script reads; give each value as its own argument. ${USAGE}`);
|
|
1318
|
+
return 64;
|
|
1319
|
+
}
|
|
497
1320
|
const missing = readMissingFlags(argv, ['--config', '--projects']);
|
|
498
1321
|
if (missing.length > 0) {
|
|
499
1322
|
console.error(`discovery: ${missing.join(', ')} given with no value`);
|
|
@@ -501,16 +1324,38 @@ function main(argv) {
|
|
|
501
1324
|
}
|
|
502
1325
|
const config = readOption(argv, '--config') ?? 'vite.config.ts';
|
|
503
1326
|
if (!existsSync(config) || !existsSync('package.json')) {
|
|
504
|
-
console.error(
|
|
1327
|
+
console.error(USAGE);
|
|
505
1328
|
return 64;
|
|
506
1329
|
}
|
|
507
1330
|
if (!existsSync(VITEST)) {
|
|
508
1331
|
console.error(`discovery: ${VITEST} is missing; run npm ci first`);
|
|
509
1332
|
return 2;
|
|
510
1333
|
}
|
|
511
|
-
const canonical = relative(process.cwd(), resolve(config))
|
|
512
|
-
const
|
|
513
|
-
const
|
|
1334
|
+
const canonical = normalizeSlashes(relative(process.cwd(), resolve(config)));
|
|
1335
|
+
const options = readCLIOptions();
|
|
1336
|
+
const absent = readMissingOptions(options);
|
|
1337
|
+
if (absent.length > 0) {
|
|
1338
|
+
console.error(`discovery: Vitest ${readVitestVersion()} lists no ${absent.join(', ')} option; the help layout this script reads has changed`);
|
|
1339
|
+
return 2;
|
|
1340
|
+
}
|
|
1341
|
+
const named = [
|
|
1342
|
+
...new Set((readOption(argv, '--projects') ?? '').split(',').filter((name) => name !== '')),
|
|
1343
|
+
];
|
|
1344
|
+
const read = readGates(readScripts(), canonical, options);
|
|
1345
|
+
if (named.length > 0) {
|
|
1346
|
+
const base = { config: canonical, mode: undefined };
|
|
1347
|
+
const cache = new Map();
|
|
1348
|
+
const unmatched = readUnmatched(base, named, cache);
|
|
1349
|
+
if (unmatched === undefined) {
|
|
1350
|
+
console.error('discovery: vitest list failed');
|
|
1351
|
+
return 2;
|
|
1352
|
+
}
|
|
1353
|
+
if (unmatched.length > 0) {
|
|
1354
|
+
console.error(`discovery: --projects ${unmatched.map(describeName).join(', ')} matches no project in ${describeName(canonical)}; run without --projects to list every project that collects tests, since naming them here would collect the projects outside the set`);
|
|
1355
|
+
return 64;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const gates = { ...read, units: narrowGates(read.units, named) };
|
|
514
1359
|
const listings = collectStableListings(canonical, named, gates);
|
|
515
1360
|
if (listings === undefined) {
|
|
516
1361
|
console.error('discovery: vitest list failed');
|
|
@@ -594,10 +1439,12 @@ function main(argv) {
|
|
|
594
1439
|
}
|
|
595
1440
|
return flagged ? 3 : 0;
|
|
596
1441
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1442
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
1443
|
+
try {
|
|
1444
|
+
process.exitCode = main(process.argv.slice(2));
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
console.error(`discovery: ${error instanceof Error ? error.message : String(error)}`);
|
|
1448
|
+
process.exitCode = 2;
|
|
1449
|
+
}
|
|
603
1450
|
}
|