vibecodedshit 0.0.0-stage → 0.2.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.
- package/LICENSE +95 -0
- package/README.md +64 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +8 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +121 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +25 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +9 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { inDir, jsManager, requirementsFiles } from './lockfiles.js';
|
|
2
|
+
const JS_INSTALL = {
|
|
3
|
+
npm: { locked: ['npm', 'ci'], unlocked: ['npm', 'install'] },
|
|
4
|
+
pnpm: { locked: ['pnpm', 'install', '--frozen-lockfile'], unlocked: ['pnpm', 'install'] },
|
|
5
|
+
yarn: { locked: ['yarn', 'install', '--immutable'], unlocked: ['yarn', 'install'] },
|
|
6
|
+
bun: { locked: ['bun', 'install', '--frozen-lockfile'], unlocked: ['bun', 'install'] },
|
|
7
|
+
};
|
|
8
|
+
/** pip installs into `.venv`, so later steps find the packages the same way as with uv, poetry, pdm and pipenv. */
|
|
9
|
+
function pip(args) {
|
|
10
|
+
return ['sh', '-c', 'python -m venv .venv && exec .venv/bin/pip install "$@"', 'sh', ...args];
|
|
11
|
+
}
|
|
12
|
+
/** Dependencies from the lockfile, or from the manifest when there is none. The only step with the network on. */
|
|
13
|
+
export const install = {
|
|
14
|
+
name: 'install',
|
|
15
|
+
network: true,
|
|
16
|
+
command(ctx) {
|
|
17
|
+
if (ctx.group === 'typescript_javascript') {
|
|
18
|
+
if (!ctx.tracked.has(inDir(ctx, 'package.json')))
|
|
19
|
+
return null;
|
|
20
|
+
const { manager, locked } = jsManager(ctx);
|
|
21
|
+
return { argv: [...JS_INSTALL[manager][locked ? 'locked' : 'unlocked']] };
|
|
22
|
+
}
|
|
23
|
+
const has = (name) => ctx.tracked.has(inDir(ctx, name));
|
|
24
|
+
if (has('uv.lock'))
|
|
25
|
+
return { argv: ['uv', 'sync', '--frozen'] };
|
|
26
|
+
if (has('poetry.lock'))
|
|
27
|
+
return { argv: ['poetry', 'install', '--sync'] };
|
|
28
|
+
if (has('pdm.lock'))
|
|
29
|
+
return { argv: ['pdm', 'sync'] };
|
|
30
|
+
if (has('Pipfile.lock'))
|
|
31
|
+
return { argv: ['pipenv', 'sync'] };
|
|
32
|
+
const requirements = requirementsFiles(ctx);
|
|
33
|
+
if (requirements.length > 0)
|
|
34
|
+
return { argv: pip(requirements.flatMap((file) => ['-r', file])) };
|
|
35
|
+
if (has('Pipfile'))
|
|
36
|
+
return { argv: ['pipenv', 'install'] };
|
|
37
|
+
if (has('pyproject.toml') || has('setup.py'))
|
|
38
|
+
return { argv: pip(['.']) };
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const KNIP_CONFIG = '/opt/health/knip.json';
|
|
2
|
+
/**
|
|
3
|
+
* Knip in the installed work copy, for 4.4 and 5.5. The repository's own knip config is not used
|
|
4
|
+
* (an empty config baked into the image), framework plugins stay enabled. DOTENV_CONFIG_QUIET keeps
|
|
5
|
+
* dotenv 17 from printing its banner into the report. Network off.
|
|
6
|
+
*/
|
|
7
|
+
export const knip = {
|
|
8
|
+
name: 'knip',
|
|
9
|
+
network: false,
|
|
10
|
+
command() {
|
|
11
|
+
return {
|
|
12
|
+
argv: [
|
|
13
|
+
'health-capture',
|
|
14
|
+
'/out/knip.json',
|
|
15
|
+
'env',
|
|
16
|
+
'DOTENV_CONFIG_QUIET=true',
|
|
17
|
+
'knip',
|
|
18
|
+
'--reporter',
|
|
19
|
+
'json',
|
|
20
|
+
'--no-progress',
|
|
21
|
+
'--no-config-hints',
|
|
22
|
+
'--config',
|
|
23
|
+
KNIP_CONFIG,
|
|
24
|
+
],
|
|
25
|
+
report: 'knip.json',
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const ESLINT_CONFIG = '/opt/health/eslint.config.mjs';
|
|
3
|
+
const RUFF_CONFIG = '/opt/health/ruff.toml';
|
|
4
|
+
/** The methodology's pinned ESLint and ruff rulesets, baked into the image. The repository's own lint config is never used. Network off. */
|
|
5
|
+
export const lint = {
|
|
6
|
+
name: 'lint',
|
|
7
|
+
network: false,
|
|
8
|
+
command(ctx) {
|
|
9
|
+
const files = ctx.files.map((file) => path.posix.relative(ctx.dir, file));
|
|
10
|
+
if (files.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
if (ctx.group === 'typescript_javascript') {
|
|
13
|
+
return {
|
|
14
|
+
argv: ['health-capture', '/out/eslint.json', 'health-eslint', '-c', ESLINT_CONFIG, '-f', 'json', '--no-error-on-unmatched-pattern', ...files],
|
|
15
|
+
tool: 'eslint',
|
|
16
|
+
report: 'eslint.json',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
argv: ['health-capture', '/out/ruff.json', 'ruff', 'check', '--config', RUFF_CONFIG, '--no-cache', '--output-format', 'json', ...files],
|
|
21
|
+
tool: 'ruff',
|
|
22
|
+
report: 'ruff.json',
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { chooseJsLockfile } from '../../measure/deps/lockfiles.js';
|
|
3
|
+
export function inDir(ctx, name) {
|
|
4
|
+
return path.posix.join(ctx.dir, name);
|
|
5
|
+
}
|
|
6
|
+
const MANAGER = {
|
|
7
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
8
|
+
'yarn.lock': 'yarn',
|
|
9
|
+
'bun.lock': 'bun',
|
|
10
|
+
'bun.lockb': 'bun',
|
|
11
|
+
'npm-shrinkwrap.json': 'npm',
|
|
12
|
+
'package-lock.json': 'npm',
|
|
13
|
+
};
|
|
14
|
+
/** The package manager of the lockfile `chooseJsLockfile` picks (the one D5 reads); npm when there is none. */
|
|
15
|
+
export function jsManager(ctx) {
|
|
16
|
+
const chosen = chooseJsLockfile(Object.keys(MANAGER).filter((name) => ctx.tracked.has(inDir(ctx, name))));
|
|
17
|
+
return chosen === undefined ? { manager: 'npm', locked: false } : { manager: MANAGER[chosen] ?? 'npm', locked: true };
|
|
18
|
+
}
|
|
19
|
+
/** `requirements*.txt` files directly in the directory, sorted. */
|
|
20
|
+
export function requirementsFiles(ctx) {
|
|
21
|
+
return [...ctx.tracked]
|
|
22
|
+
.filter((file) => path.posix.dirname(file) === ctx.dir && /^requirements.*\.txt$/.test(path.posix.basename(file)))
|
|
23
|
+
.map((file) => path.posix.basename(file))
|
|
24
|
+
.sort();
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { logicGlobs, pythonTargets } from '../test-runners/detect.js';
|
|
2
|
+
import { RUNNERS } from '../test-runners/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* One run of the invocation's test runner with the report and coverage flags of the methodology, so the
|
|
5
|
+
* results and the coverage come from a single instrumented run (1.1 to 1.3, and 6.3's test time).
|
|
6
|
+
* The reports are read from /out, never stdout; the exit code is not the pass/fail source. Network off.
|
|
7
|
+
*/
|
|
8
|
+
export const test = {
|
|
9
|
+
name: 'test',
|
|
10
|
+
network: false,
|
|
11
|
+
command(ctx) {
|
|
12
|
+
const invocation = ctx.invocation;
|
|
13
|
+
if (!invocation)
|
|
14
|
+
return null;
|
|
15
|
+
return { argv: RUNNERS[invocation.runner].argv(invocation, logicGlobs(ctx.files, ctx.dir), pythonTargets(ctx.files, ctx.dir)) };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { inDir } from './lockfiles.js';
|
|
3
|
+
const JS_FILE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
|
|
4
|
+
function pythonTool(ctx) {
|
|
5
|
+
const has = (name) => ctx.tracked.has(inDir(ctx, name));
|
|
6
|
+
const pyproject = ctx.read(inDir(ctx, 'pyproject.toml')) ?? '';
|
|
7
|
+
if (has('pyrightconfig.json') || /^\s*\[tool\.pyright\]/m.test(pyproject))
|
|
8
|
+
return 'pyright';
|
|
9
|
+
const setupCfg = ctx.read(inDir(ctx, 'setup.cfg')) ?? '';
|
|
10
|
+
if (has('mypy.ini') || has('.mypy.ini') || /^\s*\[tool\.mypy\]/m.test(pyproject) || /^\s*\[mypy\]/m.test(setupCfg))
|
|
11
|
+
return 'mypy';
|
|
12
|
+
return 'pyright';
|
|
13
|
+
}
|
|
14
|
+
/** tsc or pyright (mypy when only mypy is configured) with the repository's own config. Network off. */
|
|
15
|
+
export const typecheck = {
|
|
16
|
+
name: 'typecheck',
|
|
17
|
+
network: false,
|
|
18
|
+
command(ctx) {
|
|
19
|
+
if (ctx.group === 'typescript_javascript') {
|
|
20
|
+
const base = ['health-capture', '/out/tsc.txt', 'health-tsc', '--noEmit', '--pretty', 'false'];
|
|
21
|
+
const project = ['tsconfig.json', 'jsconfig.json'].find((name) => ctx.tracked.has(inDir(ctx, name)));
|
|
22
|
+
if (project)
|
|
23
|
+
return { argv: [...base, '-p', project], tool: 'tsc', report: 'tsc.txt' };
|
|
24
|
+
const files = ctx.files.filter((file) => JS_FILE.test(file)).map((file) => path.posix.relative(ctx.dir, file));
|
|
25
|
+
if (files.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
// No tsconfig or jsconfig: the methodology's default config.
|
|
28
|
+
const defaults = ['--allowJs', '--checkJs', '--strict', 'false', '--target', 'esnext', '--module', 'esnext', '--moduleResolution', 'bundler', '--jsx', 'preserve', '--skipLibCheck'];
|
|
29
|
+
return { argv: [...base, ...defaults, ...files], tool: 'tsc', report: 'tsc.txt' };
|
|
30
|
+
}
|
|
31
|
+
if (pythonTool(ctx) === 'mypy') {
|
|
32
|
+
return {
|
|
33
|
+
argv: ['health-capture', '/out/mypy.txt', 'health-venv', 'mypy', '--no-error-summary', '--no-pretty', '--no-color-output', '.'],
|
|
34
|
+
tool: 'mypy',
|
|
35
|
+
report: 'mypy.txt',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { argv: ['health-capture', '/out/pyright.json', 'health-venv', 'pyright', '--outputjson'], tool: 'pyright', report: 'pyright.json' };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { toRepoPath } from '../reports.js';
|
|
2
|
+
const num = (value) => (typeof value === 'number' ? value : 0);
|
|
3
|
+
/** `coverage json` (pytest-cov `--cov-report=json`): `files.<path>.summary`. Null when the text is not that report. */
|
|
4
|
+
export function parseCoveragePy(text, stepDir) {
|
|
5
|
+
let doc;
|
|
6
|
+
try {
|
|
7
|
+
doc = JSON.parse(text);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (!doc || typeof doc.files !== 'object' || doc.files === null)
|
|
13
|
+
return null;
|
|
14
|
+
const files = new Map();
|
|
15
|
+
for (const [file, entry] of Object.entries(doc.files)) {
|
|
16
|
+
const summary = entry?.summary ?? {};
|
|
17
|
+
files.set(toRepoPath(file, stepDir), {
|
|
18
|
+
lines: { total: num(summary.num_statements), covered: num(summary.covered_lines) },
|
|
19
|
+
branches: { total: num(summary.num_branches), covered: num(summary.covered_branches) },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { toRepoPath } from '../reports.js';
|
|
2
|
+
function range(loc, hits) {
|
|
3
|
+
const start = loc?.start?.line;
|
|
4
|
+
const end = loc?.end?.line;
|
|
5
|
+
if (typeof start !== 'number' || typeof end !== 'number' || typeof hits !== 'number')
|
|
6
|
+
return null;
|
|
7
|
+
return { start, end, hits };
|
|
8
|
+
}
|
|
9
|
+
/** The istanbul `json` reporter (`coverage-final.json`, also c8 and the v8 provider): the statement and function ranges of each file. Null when the text is not that report. */
|
|
10
|
+
export function parseCoverageRanges(text, stepDir) {
|
|
11
|
+
let doc;
|
|
12
|
+
try {
|
|
13
|
+
doc = JSON.parse(text);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc))
|
|
19
|
+
return null;
|
|
20
|
+
const files = new Map();
|
|
21
|
+
for (const [file, entry] of Object.entries(doc)) {
|
|
22
|
+
if (typeof entry !== 'object' || entry === null || typeof entry.statementMap !== 'object')
|
|
23
|
+
return null;
|
|
24
|
+
const ranges = [];
|
|
25
|
+
for (const [id, loc] of Object.entries(entry.statementMap)) {
|
|
26
|
+
const found = range(loc, entry.s?.[id]);
|
|
27
|
+
if (found)
|
|
28
|
+
ranges.push(found);
|
|
29
|
+
}
|
|
30
|
+
for (const [id, fn] of Object.entries(entry.fnMap ?? {})) {
|
|
31
|
+
const found = range(fn.loc, entry.f?.[id]);
|
|
32
|
+
if (found)
|
|
33
|
+
ranges.push(found);
|
|
34
|
+
}
|
|
35
|
+
files.set(toRepoPath(file, stepDir), ranges);
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The code lines of a file that ran: a line counts when the innermost statement or function range holding it
|
|
41
|
+
* has a hit. A line inside no range counts when anything in the file ran. Lines are 1-based.
|
|
42
|
+
*/
|
|
43
|
+
export function coveredLines(codeLines, ranges) {
|
|
44
|
+
const anyHit = ranges.some((r) => r.hits > 0);
|
|
45
|
+
const covered = new Set();
|
|
46
|
+
for (const line of codeLines) {
|
|
47
|
+
let best = null;
|
|
48
|
+
let hit = false;
|
|
49
|
+
for (const r of ranges) {
|
|
50
|
+
if (r.start > line || r.end < line)
|
|
51
|
+
continue;
|
|
52
|
+
const span = r.end - r.start;
|
|
53
|
+
if (best === null || span < best.end - best.start) {
|
|
54
|
+
best = r;
|
|
55
|
+
hit = r.hits > 0;
|
|
56
|
+
}
|
|
57
|
+
else if (span === best.end - best.start && r.hits > 0) {
|
|
58
|
+
hit = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (best === null ? anyHit : hit)
|
|
62
|
+
covered.add(line);
|
|
63
|
+
}
|
|
64
|
+
return covered;
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { toRepoPath } from '../reports.js';
|
|
2
|
+
function part(value) {
|
|
3
|
+
return { total: typeof value?.total === 'number' ? value.total : 0, covered: typeof value?.covered === 'number' ? value.covered : 0 };
|
|
4
|
+
}
|
|
5
|
+
/** The istanbul `json-summary` reporter (also c8 and the v8 provider): one entry per file next to `total`. Null when the text is not that report. */
|
|
6
|
+
export function parseCoverageSummary(text, stepDir) {
|
|
7
|
+
let doc;
|
|
8
|
+
try {
|
|
9
|
+
doc = JSON.parse(text);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (typeof doc !== 'object' || doc === null || typeof doc.total !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const files = new Map();
|
|
17
|
+
for (const [file, entry] of Object.entries(doc)) {
|
|
18
|
+
if (file === 'total' || typeof entry !== 'object' || entry === null)
|
|
19
|
+
continue;
|
|
20
|
+
files.set(toRepoPath(file, stepDir), { lines: part(entry.lines), branches: part(entry.branches) });
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jest --json` and `vitest --reporter=json` share one shape. Passed and failed assertions count as
|
|
3
|
+
* such; pending, skipped, todo and disabled are skipped. A file that failed without any assertion
|
|
4
|
+
* (it did not load) is one failed case. Null when the text is not a results report.
|
|
5
|
+
*/
|
|
6
|
+
export function parseJestJson(text) {
|
|
7
|
+
let doc;
|
|
8
|
+
try {
|
|
9
|
+
doc = JSON.parse(text);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (!doc || !Array.isArray(doc.testResults))
|
|
15
|
+
return null;
|
|
16
|
+
const counts = { passed: 0, failed: 0, errored: 0, skipped: 0 };
|
|
17
|
+
for (const file of doc.testResults) {
|
|
18
|
+
const assertions = file.assertionResults ?? [];
|
|
19
|
+
if (assertions.length === 0 && file.status === 'failed')
|
|
20
|
+
counts.failed += 1;
|
|
21
|
+
for (const assertion of assertions) {
|
|
22
|
+
if (assertion.status === 'passed')
|
|
23
|
+
counts.passed += 1;
|
|
24
|
+
else if (assertion.status === 'failed')
|
|
25
|
+
counts.failed += 1;
|
|
26
|
+
else
|
|
27
|
+
counts.skipped += 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return counts;
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JUnit XML from pytest and `node --test`. Every `<testcase>` is one case: `<error>` is errored
|
|
3
|
+
* (a pytest collection error is a testcase with an error), `<failure>` is failed, `<skipped>` is
|
|
4
|
+
* skipped except pytest's `xfail`, which is left out entirely. Null when there is no testsuite.
|
|
5
|
+
*/
|
|
6
|
+
export function parseJunit(text) {
|
|
7
|
+
if (!/<testsuites?\b/.test(text))
|
|
8
|
+
return null;
|
|
9
|
+
const counts = { passed: 0, failed: 0, errored: 0, skipped: 0 };
|
|
10
|
+
for (const match of text.matchAll(/<testcase\b[^>]*?(?:\/>|>([\s\S]*?)<\/testcase>)/g)) {
|
|
11
|
+
const body = match[1] ?? '';
|
|
12
|
+
if (/<error\b/.test(body))
|
|
13
|
+
counts.errored += 1;
|
|
14
|
+
else if (/<failure\b/.test(body))
|
|
15
|
+
counts.failed += 1;
|
|
16
|
+
else if (/<skipped\b/.test(body)) {
|
|
17
|
+
if (!/<skipped\b[^>]*type="pytest\.xfail"/.test(body))
|
|
18
|
+
counts.skipped += 1;
|
|
19
|
+
}
|
|
20
|
+
else
|
|
21
|
+
counts.passed += 1;
|
|
22
|
+
}
|
|
23
|
+
return counts;
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** `mocha --reporter json`: `passes`, `failures` and `pending` arrays. Null when the text is not a mocha report. */
|
|
2
|
+
export function parseMochaJson(text) {
|
|
3
|
+
let doc;
|
|
4
|
+
try {
|
|
5
|
+
doc = JSON.parse(text);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
if (!doc || typeof doc.stats !== 'object' || !Array.isArray(doc.passes) || !Array.isArray(doc.failures))
|
|
11
|
+
return null;
|
|
12
|
+
return {
|
|
13
|
+
passed: doc.passes.length,
|
|
14
|
+
failed: doc.failures.length,
|
|
15
|
+
errored: 0,
|
|
16
|
+
skipped: Array.isArray(doc.pending) ? doc.pending.length : 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runnerInScript, scriptArgs } from './script.js';
|
|
3
|
+
const DEPENDENCY_ORDER = ['vitest', 'jest', 'mocha'];
|
|
4
|
+
const PYTHON_MANIFESTS = /^(?:pyproject\.toml|setup\.py|setup\.cfg|Pipfile|tox\.ini|requirements.*\.txt)$/;
|
|
5
|
+
function isUnder(dir, parent) {
|
|
6
|
+
return parent === '.' || dir === parent || dir.startsWith(`${parent}/`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The runner a package.json declares: the one its `test` script names, else the first of vitest,
|
|
10
|
+
* jest, mocha among the dependencies. Null when it declares none (Playwright and Cypress are never run).
|
|
11
|
+
*/
|
|
12
|
+
export function jsRunner(packageJson) {
|
|
13
|
+
if (packageJson === null)
|
|
14
|
+
return null;
|
|
15
|
+
let pkg;
|
|
16
|
+
try {
|
|
17
|
+
pkg = JSON.parse(packageJson);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const script = typeof pkg.scripts?.test === 'string' ? pkg.scripts.test : '';
|
|
23
|
+
const named = runnerInScript(script);
|
|
24
|
+
if (named)
|
|
25
|
+
return { runner: named, args: scriptArgs(script, named) };
|
|
26
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
27
|
+
const runner = DEPENDENCY_ORDER.find((name) => name in deps);
|
|
28
|
+
return runner ? { runner, args: [] } : null;
|
|
29
|
+
}
|
|
30
|
+
function pythonHasTests(input, root) {
|
|
31
|
+
const underRoot = (file) => file.group === 'python' && isUnder(path.posix.dirname(file.path), root.dir);
|
|
32
|
+
if (input.scope.files.some((file) => file.role === 'test' && underRoot(file)))
|
|
33
|
+
return true;
|
|
34
|
+
const inRoot = (name) => path.posix.join(root.dir, name);
|
|
35
|
+
if (input.tracked.has(inRoot('tox.ini')) || input.tracked.has(inRoot('noxfile.py')))
|
|
36
|
+
return true;
|
|
37
|
+
for (const file of input.tracked) {
|
|
38
|
+
if (path.posix.dirname(file) !== root.dir || !PYTHON_MANIFESTS.test(path.posix.basename(file)))
|
|
39
|
+
continue;
|
|
40
|
+
if (/pytest/.test(input.read(file) ?? ''))
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Which test runs to make. TypeScript/JavaScript: the install root when its package.json declares a
|
|
47
|
+
* runner, otherwise each workspace package under it that does. Python: one pytest run per install root
|
|
48
|
+
* that has a pytest dependency, a test file or a tox/nox config (pytest runs unittest suites too).
|
|
49
|
+
* At most one invocation per directory.
|
|
50
|
+
*/
|
|
51
|
+
export function detectInvocations(input) {
|
|
52
|
+
const invocations = [];
|
|
53
|
+
for (const root of input.roots) {
|
|
54
|
+
if (root.group === 'python') {
|
|
55
|
+
if (pythonHasTests(input, root))
|
|
56
|
+
invocations.push({ dir: root.dir, group: 'python', runner: 'pytest', args: [] });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const manifest = (dir) => path.posix.join(dir, 'package.json');
|
|
60
|
+
const own = jsRunner(input.read(manifest(root.dir)));
|
|
61
|
+
if (own) {
|
|
62
|
+
invocations.push({ dir: root.dir, group: root.group, ...own });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
for (const dir of [...input.scope.packages].sort()) {
|
|
66
|
+
if (dir === root.dir || !isUnder(dir, root.dir))
|
|
67
|
+
continue;
|
|
68
|
+
const member = jsRunner(input.read(manifest(dir)));
|
|
69
|
+
if (member)
|
|
70
|
+
invocations.push({ dir, group: root.group, ...member });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return invocations;
|
|
74
|
+
}
|
|
75
|
+
const JS_EXTENSIONS = '{ts,tsx,mts,cts,js,jsx,mjs,cjs}';
|
|
76
|
+
/** Directory-level globs of the logic files beneath `dir`, relative to it (a file at the top is named exactly). */
|
|
77
|
+
export function logicGlobs(logicFiles, dir) {
|
|
78
|
+
const globs = new Set();
|
|
79
|
+
for (const file of logicFiles) {
|
|
80
|
+
const rel = path.posix.relative(dir, file);
|
|
81
|
+
const [top] = rel.split('/');
|
|
82
|
+
if (!top)
|
|
83
|
+
continue;
|
|
84
|
+
globs.add(top === rel ? rel : `${top}/**/*.${JS_EXTENSIONS}`);
|
|
85
|
+
}
|
|
86
|
+
return [...globs].sort();
|
|
87
|
+
}
|
|
88
|
+
/** `--cov` targets for pytest: each top-level directory with Python logic, and top-level modules by name. */
|
|
89
|
+
export function pythonTargets(logicFiles, dir) {
|
|
90
|
+
const targets = new Set();
|
|
91
|
+
for (const file of logicFiles) {
|
|
92
|
+
const rel = path.posix.relative(dir, file);
|
|
93
|
+
const [top] = rel.split('/');
|
|
94
|
+
if (!top)
|
|
95
|
+
continue;
|
|
96
|
+
targets.add(top === rel ? top.replace(/\.py$/, '') : top);
|
|
97
|
+
}
|
|
98
|
+
return [...targets].sort();
|
|
99
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jest } from './jest.js';
|
|
2
|
+
import { mocha } from './mocha.js';
|
|
3
|
+
import { nodeTest } from './node-test.js';
|
|
4
|
+
import { pytest } from './pytest.js';
|
|
5
|
+
import { vitest } from './vitest.js';
|
|
6
|
+
export const RUNNERS = { vitest, jest, mocha, 'node-test': nodeTest, pytest };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseCoverageRanges } from '../test-reports/coverage-ranges.js';
|
|
2
|
+
import { parseCoverageSummary } from '../test-reports/coverage-summary.js';
|
|
3
|
+
import { parseJestJson } from '../test-reports/jest-json.js';
|
|
4
|
+
export const jest = {
|
|
5
|
+
results: 'results.json',
|
|
6
|
+
coverage: 'coverage/coverage-summary.json',
|
|
7
|
+
ranges: 'coverage/coverage-final.json',
|
|
8
|
+
argv: (invocation, globs) => [
|
|
9
|
+
'npx',
|
|
10
|
+
'--no-install',
|
|
11
|
+
'jest',
|
|
12
|
+
'--ci',
|
|
13
|
+
'--json',
|
|
14
|
+
'--outputFile=/out/results.json',
|
|
15
|
+
'--coverage',
|
|
16
|
+
'--coverageProvider=v8',
|
|
17
|
+
'--coverageReporters=json-summary',
|
|
18
|
+
'--coverageReporters=json',
|
|
19
|
+
'--coverageDirectory=/out/coverage',
|
|
20
|
+
...globs.map((glob) => `--collectCoverageFrom=${glob}`),
|
|
21
|
+
'--coverageThreshold={}',
|
|
22
|
+
...invocation.args,
|
|
23
|
+
],
|
|
24
|
+
parseResults: parseJestJson,
|
|
25
|
+
parseCoverage: parseCoverageSummary,
|
|
26
|
+
parseRanges: parseCoverageRanges,
|
|
27
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseCoverageRanges } from '../test-reports/coverage-ranges.js';
|
|
2
|
+
import { parseCoverageSummary } from '../test-reports/coverage-summary.js';
|
|
3
|
+
import { parseMochaJson } from '../test-reports/mocha-json.js';
|
|
4
|
+
/** mocha has no coverage of its own: c8 wraps it. `--all` reports files no test loads. */
|
|
5
|
+
export const mocha = {
|
|
6
|
+
results: 'results.json',
|
|
7
|
+
coverage: 'coverage/coverage-summary.json',
|
|
8
|
+
ranges: 'coverage/coverage-final.json',
|
|
9
|
+
argv: (invocation, globs) => [
|
|
10
|
+
'c8',
|
|
11
|
+
'--reporter=json-summary',
|
|
12
|
+
'--reporter=json',
|
|
13
|
+
'--report-dir=/out/coverage',
|
|
14
|
+
'--all',
|
|
15
|
+
...globs.flatMap((glob) => ['--include', glob]),
|
|
16
|
+
'npx',
|
|
17
|
+
'--no-install',
|
|
18
|
+
'mocha',
|
|
19
|
+
'--reporter',
|
|
20
|
+
'json',
|
|
21
|
+
'--reporter-option',
|
|
22
|
+
'output=/out/results.json',
|
|
23
|
+
...invocation.args,
|
|
24
|
+
],
|
|
25
|
+
parseResults: parseMochaJson,
|
|
26
|
+
parseCoverage: parseCoverageSummary,
|
|
27
|
+
parseRanges: parseCoverageRanges,
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseCoverageRanges } from '../test-reports/coverage-ranges.js';
|
|
2
|
+
import { parseCoverageSummary } from '../test-reports/coverage-summary.js';
|
|
3
|
+
import { parseJunit } from '../test-reports/junit.js';
|
|
4
|
+
export const nodeTest = {
|
|
5
|
+
results: 'junit.xml',
|
|
6
|
+
coverage: 'coverage/coverage-summary.json',
|
|
7
|
+
ranges: 'coverage/coverage-final.json',
|
|
8
|
+
argv: (invocation, globs) => [
|
|
9
|
+
'c8',
|
|
10
|
+
'--reporter=json-summary',
|
|
11
|
+
'--reporter=json',
|
|
12
|
+
'--report-dir=/out/coverage',
|
|
13
|
+
'--all',
|
|
14
|
+
...globs.flatMap((glob) => ['--include', glob]),
|
|
15
|
+
'node',
|
|
16
|
+
'--test',
|
|
17
|
+
'--test-reporter=junit',
|
|
18
|
+
'--test-reporter-destination=/out/junit.xml',
|
|
19
|
+
...invocation.args,
|
|
20
|
+
],
|
|
21
|
+
parseResults: parseJunit,
|
|
22
|
+
parseCoverage: parseCoverageSummary,
|
|
23
|
+
parseRanges: parseCoverageRanges,
|
|
24
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseCoveragePy } from '../test-reports/coverage-py.js';
|
|
2
|
+
import { parseJunit } from '../test-reports/junit.js';
|
|
3
|
+
/** pytest also runs unittest suites. `health-pytest` is `python -m pytest` in the repository's environment, with the image's pytest and pytest-cov when it has none. */
|
|
4
|
+
export const pytest = {
|
|
5
|
+
results: 'junit.xml',
|
|
6
|
+
coverage: 'coverage.json',
|
|
7
|
+
argv: (_invocation, _globs, targets) => [
|
|
8
|
+
'health-pytest',
|
|
9
|
+
'--continue-on-collection-errors',
|
|
10
|
+
'--junitxml=/out/junit.xml',
|
|
11
|
+
...targets.map((target) => `--cov=${target}`),
|
|
12
|
+
'--cov-branch',
|
|
13
|
+
'--cov-report=json:/out/coverage.json',
|
|
14
|
+
'--cov-fail-under=0',
|
|
15
|
+
],
|
|
16
|
+
parseResults: parseJunit,
|
|
17
|
+
parseCoverage: parseCoveragePy,
|
|
18
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const SEGMENT = /&&|\|\||;|\|/;
|
|
2
|
+
/** Where each runner appears in a `test` script. */
|
|
3
|
+
const RUNNER_PATTERN = {
|
|
4
|
+
vitest: /\bvitest\b/,
|
|
5
|
+
jest: /\bjest\b/,
|
|
6
|
+
mocha: /\bmocha\b/,
|
|
7
|
+
'node-test': /\bnode\b(?:\s+\S+)*?\s--test\b/,
|
|
8
|
+
};
|
|
9
|
+
/** The runner a script names; when it names several, the first one in the text. */
|
|
10
|
+
export function runnerInScript(script) {
|
|
11
|
+
let best = null;
|
|
12
|
+
for (const [runner, pattern] of Object.entries(RUNNER_PATTERN)) {
|
|
13
|
+
const index = pattern.exec(script)?.index;
|
|
14
|
+
if (index !== undefined && (best === null || index < best.index))
|
|
15
|
+
best = { runner, index };
|
|
16
|
+
}
|
|
17
|
+
return best?.runner ?? null;
|
|
18
|
+
}
|
|
19
|
+
const DROP_FLAG = /^(?:--watch\S*|-w|--reporter(?:=.*)?|-R|--outputFile(?:=.*)?|--json|--ci|--coverage(?:\..*)?(?:=.*)?|--collectCoverage\S*|--run)$/;
|
|
20
|
+
const VALUE_FLAG = new Set(['--reporter', '-R', '--outputFile']);
|
|
21
|
+
/**
|
|
22
|
+
* The arguments after the runner in the first segment of the script that names it (a segment ends at
|
|
23
|
+
* `&&`, `||`, `;` or `|`), without watch, reporter, output and coverage options. Positional arguments
|
|
24
|
+
* and `--config`, `--project`, `--dir` stay. A leading `run` (vitest) and a `--test` flag (node) are the runner itself.
|
|
25
|
+
*/
|
|
26
|
+
export function scriptArgs(script, runner) {
|
|
27
|
+
for (const segment of script.split(SEGMENT)) {
|
|
28
|
+
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
29
|
+
const at = tokens.findIndex((token) => (runner === 'node-test' ? token === '--test' : new RegExp(`(?:^|/)${runner}$`).test(token)));
|
|
30
|
+
if (at < 0)
|
|
31
|
+
continue;
|
|
32
|
+
const rest = tokens.slice(at + 1);
|
|
33
|
+
const kept = [];
|
|
34
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
35
|
+
const token = rest[i];
|
|
36
|
+
if (i === 0 && runner === 'vitest' && (token === 'run' || token === 'watch' || token === 'dev'))
|
|
37
|
+
continue;
|
|
38
|
+
if (DROP_FLAG.test(token)) {
|
|
39
|
+
if (VALUE_FLAG.has(token) && rest[i + 1] !== undefined && !rest[i + 1].startsWith('-'))
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
kept.push(token);
|
|
44
|
+
}
|
|
45
|
+
return kept;
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|