Skip to content

Commit fc46e08

Browse files
committed
fix(ci): align vendored Vitest dependencies before install
1 parent ac38d99 commit fc46e08

6 files changed

Lines changed: 200 additions & 62 deletions

File tree

.github/actions/clone/action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ runs:
4747
ref: ${{ steps.upstream-versions.outputs.ROLLDOWN_VITE_HASH }}
4848
persist-credentials: false
4949

50+
# Match the manifests used by sync-remote when it generated the root lockfile.
51+
# setup-node installs with --frozen-lockfile, so this must run before it.
52+
- name: Align vendored Vitest dependencies
53+
shell: bash
54+
run: node packages/tools/src/vendored-vitest.mjs
55+
5056
# Disable autocrlf to preserve LF line endings on Windows
5157
# This prevents prettier/eslint from failing with "Delete ␍" errors
5258
- name: Configure git for LF line endings

.typos.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[default]
2+
# Vitest v4 used this misspelled directory; the migrator must recognize it.
3+
extend-ignore-re = ['\.vitest-attachements\b']
4+
15
[default.extend-words]
26
ratatui = "ratatui"
37
PUNICODE = "PUNICODE"

packages/tools/src/__tests__/sync-remote-deps.spec.ts

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,114 @@
1-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1+
import { spawnSync } from 'node:child_process';
2+
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
23
import { tmpdir } from 'node:os';
34
import { join } from 'node:path';
45

56
import * as semver from 'semver';
67
import { describe, expect, test } from 'vitest';
8+
import * as yaml from 'yaml';
79

10+
import { VITEST_VERSION } from '../../../cli/src/utils/constants.ts';
811
import {
9-
alignVendoredVitestDependencies,
1012
mergePnpmWorkspaces,
1113
syncCargoOxcVersions,
1214
syncViteDevtoolsDependencies,
1315
} from '../sync-remote-deps.ts';
16+
import { alignVendoredVitestDependencies } from '../vendored-vitest.mjs';
1417

1518
describe('vendored Vitest v5 bridge', () => {
19+
test('can be imported from stdin without running the bootstrap', () => {
20+
const root = mkdtempSync(join(tmpdir(), 'vp-vendored-vitest-import-'));
21+
try {
22+
const url = new URL('../vendored-vitest.mjs', import.meta.url).href;
23+
const result = spawnSync(process.execPath, ['--input-type=module', '-'], {
24+
cwd: root,
25+
encoding: 'utf8',
26+
input: `import ${JSON.stringify(url)};`,
27+
});
28+
expect(result.stderr).toBe('');
29+
expect(result.status).toBe(0);
30+
} finally {
31+
rmSync(root, { recursive: true, force: true });
32+
}
33+
});
34+
35+
test('keeps the bootstrap runtime pin in sync with the root catalog', () => {
36+
const workspace = yaml.parse(
37+
readFileSync(new URL('../../../../pnpm-workspace.yaml', import.meta.url), 'utf8'),
38+
);
39+
expect(workspace.catalog.vitest).toBe(VITEST_VERSION);
40+
});
41+
42+
test.each(['4.1.10', '^5.0.0', '5.1.0-beta.1'])(
43+
'rejects an unsupported bootstrap runtime pin %s before changing manifests',
44+
(version) => {
45+
const root = mkdtempSync(join(tmpdir(), 'vp-vendored-vitest-bootstrap-'));
46+
try {
47+
const constantsDir = join(root, 'packages/cli/src/utils');
48+
mkdirSync(constantsDir, { recursive: true });
49+
writeFileSync(
50+
join(constantsDir, 'constants.ts'),
51+
`export const VITEST_VERSION = '${version}';\n`,
52+
);
53+
const script = join(root, 'vendored-vitest.mjs');
54+
copyFileSync(new URL('../vendored-vitest.mjs', import.meta.url), script);
55+
const result = spawnSync(process.execPath, [script], { cwd: root, encoding: 'utf8' });
56+
expect(result.status).not.toBe(0);
57+
expect(result.stderr).toContain('VITEST_VERSION must declare an exact stable v5 version');
58+
} finally {
59+
rmSync(root, { recursive: true, force: true });
60+
}
61+
},
62+
);
63+
64+
test.each(['5.0.0', '5.1.2'])(
65+
'runs before dependency installation with the selected %s runtime pin',
66+
(version) => {
67+
const root = mkdtempSync(join(tmpdir(), 'vp-vendored-vitest-bootstrap-'));
68+
try {
69+
const constantsDir = join(root, 'packages/cli/src/utils');
70+
mkdirSync(constantsDir, { recursive: true });
71+
writeFileSync(
72+
join(constantsDir, 'constants.ts'),
73+
`export const VITEST_VERSION = '${version}';\n`,
74+
);
75+
// Copy the entry point outside the repo so it cannot resolve node_modules.
76+
const script = join(root, 'vendored-vitest.mjs');
77+
copyFileSync(new URL('../vendored-vitest.mjs', import.meta.url), script);
78+
for (const vendor of ['vite', 'rolldown']) {
79+
mkdirSync(join(root, vendor, 'packages', vendor), { recursive: true });
80+
writeFileSync(
81+
join(root, vendor, 'packages', vendor, 'package.json'),
82+
JSON.stringify({
83+
dependencies: { '@vitest/utils': '4.1.10' },
84+
devDependencies: { vitest: 'catalog:' },
85+
optionalDependencies: { '@vitest/spy': '^4.1.10' },
86+
}),
87+
);
88+
}
89+
const run = () => spawnSync(process.execPath, [script], { cwd: root, encoding: 'utf8' });
90+
const first = run();
91+
expect(first.stderr).toBe('');
92+
expect(first.status).toBe(0);
93+
const manifests = ['vite', 'rolldown'].map((vendor) =>
94+
join(root, vendor, 'packages', vendor, 'package.json'),
95+
);
96+
const sources = manifests.map((file) => readFileSync(file, 'utf8'));
97+
for (const source of sources) {
98+
expect(JSON.parse(source)).toEqual({
99+
dependencies: { '@vitest/utils': version },
100+
devDependencies: { vitest: 'catalog:' },
101+
optionalDependencies: { '@vitest/spy': version },
102+
});
103+
}
104+
expect(run().status).toBe(0);
105+
expect(manifests.map((file) => readFileSync(file, 'utf8'))).toEqual(sources);
106+
} finally {
107+
rmSync(root, { recursive: true, force: true });
108+
}
109+
},
110+
);
111+
16112
test('resolves the reviewed Vitest major catalog conflict', () => {
17113
const merged = mergePnpmWorkspaces(
18114
{ catalog: { vitest: '5.0.0' } },

packages/tools/src/sync-remote-deps.ts

Lines changed: 6 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { execSync, spawnSync } from 'node:child_process';
2-
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
33
import { join } from 'node:path';
44
import { parseArgs } from 'node:util';
55

66
import upstreamVersions from '../.upstream-versions.json' with { type: 'json' };
7+
import {
8+
alignVendoredVitestDependencies,
9+
REMOVED_VITEST_PACKAGES,
10+
VITEST_EXACT_VERSION_PACKAGES,
11+
} from './vendored-vitest.mjs';
712

813
interface PnpmWorkspace {
914
packages?: string[];
@@ -370,65 +375,6 @@ const OXC_PACKAGES = new Set([
370375
'oxlint',
371376
'oxlint-tsgolint',
372377
]);
373-
// These official packages share the runner version selected by upgrade-deps.ts.
374-
// Community packages, such as browser-webdriverio, have independent versions.
375-
const VITEST_EXACT_VERSION_PACKAGES = new Set([
376-
'vitest',
377-
'@vitest/browser',
378-
'@vitest/browser-playwright',
379-
'@vitest/browser-preview',
380-
'@vitest/coverage-v8',
381-
'@vitest/coverage-istanbul',
382-
'@vitest/mocker',
383-
'@vitest/pretty-format',
384-
'@vitest/snapshot',
385-
'@vitest/spy',
386-
'@vitest/ui',
387-
'@vitest/utils',
388-
'@vitest/web-worker',
389-
]);
390-
const REMOVED_VITEST_PACKAGES = new Set(['@vitest/runner', '@vitest/expect']);
391-
392-
export function alignVendoredVitestDependencies(rootDir: string, version: string): void {
393-
for (const vendor of [VITE_DIR, ROLLDOWN_DIR]) {
394-
const packagesDir = join(rootDir, vendor, 'packages');
395-
const dirs = [
396-
join(rootDir, vendor),
397-
...readdirSync(packagesDir, { withFileTypes: true })
398-
.filter((entry) => entry.isDirectory())
399-
.map((entry) => join(packagesDir, entry.name)),
400-
];
401-
for (const dir of dirs) {
402-
const file = join(dir, 'package.json');
403-
if (!existsSync(file)) {
404-
continue;
405-
}
406-
const source = readFileSync(file, 'utf8');
407-
const pkg = JSON.parse(source) as Record<string, Record<string, string>>;
408-
let changed = false;
409-
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
410-
for (const name of Object.keys(pkg[field] ?? {})) {
411-
if (REMOVED_VITEST_PACKAGES.has(name)) {
412-
throw new Error(`Migrate removed ${name} use in ${file} before synchronizing Vitest`);
413-
}
414-
if (!VITEST_EXACT_VERSION_PACKAGES.has(name)) {
415-
continue;
416-
}
417-
// Only the default catalog is aligned during sync; named catalogs can still use v4.
418-
if (pkg[field][name] !== version && pkg[field][name] !== 'catalog:') {
419-
pkg[field][name] = version;
420-
changed = true;
421-
}
422-
}
423-
}
424-
if (changed) {
425-
writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
426-
log(`Aligned Vitest dependencies in ${file} to ${version}`);
427-
}
428-
}
429-
}
430-
}
431-
432378
// These packages should always use the highest version
433379
function syncedPackages(packageName: string): boolean {
434380
if (OXC_PACKAGES.has(packageName) || packageName === 'tinybench') {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const VITEST_EXACT_VERSION_PACKAGES: Set<string>;
2+
export const REMOVED_VITEST_PACKAGES: Set<string>;
3+
export function alignVendoredVitestDependencies(rootDir: string, version: string): void;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
// This module runs before dependency installation and Node.js setup in CI.
6+
// Keep it as plain JavaScript with only built-in imports.
7+
// These official packages share the runner version selected by upgrade-deps.ts.
8+
// Community packages, such as browser-webdriverio, have independent versions.
9+
export const VITEST_EXACT_VERSION_PACKAGES = new Set([
10+
'vitest',
11+
'@vitest/browser',
12+
'@vitest/browser-playwright',
13+
'@vitest/browser-preview',
14+
'@vitest/coverage-v8',
15+
'@vitest/coverage-istanbul',
16+
'@vitest/mocker',
17+
'@vitest/pretty-format',
18+
'@vitest/snapshot',
19+
'@vitest/spy',
20+
'@vitest/ui',
21+
'@vitest/utils',
22+
'@vitest/web-worker',
23+
]);
24+
export const REMOVED_VITEST_PACKAGES = new Set(['@vitest/runner', '@vitest/expect']);
25+
26+
/**
27+
* @param {string} rootDir
28+
* @param {string} version
29+
*/
30+
export function alignVendoredVitestDependencies(rootDir, version) {
31+
for (const vendor of ['vite', 'rolldown']) {
32+
const packagesDir = join(rootDir, vendor, 'packages');
33+
const dirs = [
34+
join(rootDir, vendor),
35+
...readdirSync(packagesDir, { withFileTypes: true })
36+
.filter((entry) => entry.isDirectory())
37+
.map((entry) => join(packagesDir, entry.name)),
38+
];
39+
for (const dir of dirs) {
40+
const file = join(dir, 'package.json');
41+
if (!existsSync(file)) {
42+
continue;
43+
}
44+
const source = readFileSync(file, 'utf8');
45+
const pkg = JSON.parse(source);
46+
let changed = false;
47+
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
48+
for (const name of Object.keys(pkg[field] ?? {})) {
49+
if (REMOVED_VITEST_PACKAGES.has(name)) {
50+
throw new Error(`Migrate removed ${name} use in ${file} before synchronizing Vitest`);
51+
}
52+
if (!VITEST_EXACT_VERSION_PACKAGES.has(name)) {
53+
continue;
54+
}
55+
// Only the default catalog is aligned during sync; named catalogs can still use v4.
56+
if (pkg[field][name] !== version && pkg[field][name] !== 'catalog:') {
57+
pkg[field][name] = version;
58+
changed = true;
59+
}
60+
}
61+
}
62+
if (changed) {
63+
writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
64+
}
65+
}
66+
}
67+
}
68+
69+
if (
70+
process.argv[1] &&
71+
existsSync(process.argv[1]) &&
72+
fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
73+
) {
74+
const rootDir = process.cwd();
75+
// upgrade-deps.ts keeps this exact runtime pin in sync with the root catalog.
76+
// Read the declaration without loading TypeScript or an installed YAML parser.
77+
const constants = readFileSync(join(rootDir, 'packages/cli/src/utils/constants.ts'), 'utf8');
78+
const version = constants.match(/^export const VITEST_VERSION = '(5\.\d+\.\d+)';\r?$/m)?.[1];
79+
if (!version) {
80+
throw new Error('VITEST_VERSION must declare an exact stable v5 version');
81+
}
82+
alignVendoredVitestDependencies(rootDir, version);
83+
}

0 commit comments

Comments
 (0)