Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions scripts/__tests__/vitest-invocation-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,31 @@ describe('parseVitestArgv', () => {
expect(parsed.afterDoubleDash).toEqual(['--run', 'packages/fields/src/a.test.ts']);
});

it('keeps EVERY value of a repeated flag, in order, next to the last-wins scalar', () => {
// objectui#7329. The root `test:integration` script really is
// `vitest run --project dom --project dom-heavy`; `flags` is last-wins, so it
// answers `dom-heavy` and drops `dom` with no symptom. A reader asking "which
// projects does this command run" reads `flagValues` instead.
const parsed = parseVitestArgv(argvFor('run', '--project', 'dom', '--project', 'dom-heavy'));

expect(parsed.flagValues['--project']).toEqual(['dom', 'dom-heavy']);
// Backward compatibility, the whole reason `flags` was not turned into arrays:
// the guard's own `--root` / `--changed` readers still see one scalar, the last.
expect(parsed.flags['--project']).toBe('dom-heavy');
// Neither value leaked into the file filters.
expect(parsed.positionals).toEqual([]);
});

it('lists a flag that appears once as a one-element array, so readers need no fallback', () => {
const parsed = parseVitestArgv(argvFor('run', '--project', 'a', '--shard=1/4', '--watch'));

expect(parsed.flagValues['--project']).toEqual(['a']);
// Both other spellings are recorded too: `--flag=value` and a bare boolean.
expect(parsed.flagValues['--shard']).toEqual(['1/4']);
expect(parsed.flagValues['--watch']).toEqual([true]);
expect(parsed.flagValues['--reporter']).toBeUndefined();
});

it('treats a leading subcommand as the subcommand, not a filter', () => {
expect(parseVitestArgv(argvFor('run')).positionals).toEqual([]);
expect(parseVitestArgv(argvFor('list')).subcommand).toBe('list');
Expand Down
39 changes: 34 additions & 5 deletions scripts/vitest-invocation-guard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,24 @@ const CONCRETE_TEST_PATH = /[\\/].*\.(test|spec)\.(c|m)?[jt]sx?$/;
/**
* Split a `process.argv`-shaped array into the parts this guard reasons about.
*
* Flags come back twice, because two readers want two different answers:
*
* - `flags` is LAST-WINS — one scalar per flag, the final occurrence. That is
* what this guard's own readers want (`--changed` for existence, `--root`
* for the one root that actually takes effect), and it is the shape they
* have always had.
* - `flagValues` is the lossless sibling — every occurrence of every flag, in
* argv order. A repeated flag is legal and meaningful in this repo: the root
* `test:integration` script is `vitest run --project dom --project
* dom-heavy`, which `flags` alone reports as `dom-heavy` with `dom` silently
* dropped (objectui#7329). A reader asking "which projects does this command
* run" reads `flagValues['--project']`.
*
* Every flag is in `flagValues`, including one that appears once (as a
* one-element array), so a reader never needs a scalar-or-array fallback.
*
* @param {string[]} argv full `process.argv` (node binary + script + args)
* @returns {{ subcommand: string | null, positionals: string[], afterDoubleDash: string[], flags: Record<string, string | true> }}
* @returns {{ subcommand: string | null, positionals: string[], afterDoubleDash: string[], flags: Record<string, string | true>, flagValues: Record<string, Array<string | true>> }}
*/
export function parseVitestArgv(argv) {
const args = argv.slice(2);
Expand All @@ -250,8 +266,21 @@ export function parseVitestArgv(argv) {
const afterDoubleDash = [];
/** @type {Record<string, string | true>} */
const flags = {};
/** @type {Record<string, Array<string | true>>} */
const flagValues = {};
let subcommand = null;

/**
* Record one occurrence of a flag into both views.
*
* @param {string} name the flag as written, e.g. `--project`
* @param {string | true} value its value, or `true` for a bare boolean flag
*/
const record = (name, value) => {
flags[name] = value;
(flagValues[name] ??= []).push(value);
};

for (let i = 0; i < args.length; i += 1) {
const token = args[i];

Expand All @@ -263,16 +292,16 @@ export function parseVitestArgv(argv) {
if (token.startsWith('-')) {
const eq = token.indexOf('=');
if (eq !== -1) {
flags[token.slice(0, eq)] = token.slice(eq + 1);
record(token.slice(0, eq), token.slice(eq + 1));
continue;
}
const next = args[i + 1];
if (VALUE_FLAGS.has(token) && next !== undefined && !next.startsWith('-')) {
flags[token] = next;
record(token, next);
i += 1;
continue;
}
flags[token] = true;
record(token, true);
continue;
}

Expand All @@ -284,7 +313,7 @@ export function parseVitestArgv(argv) {
positionals.push(token);
}

return { subcommand, positionals, afterDoubleDash, flags };
return { subcommand, positionals, afterDoubleDash, flags, flagValues };
}

/**
Expand Down
Loading