From c6f1d94e0a557c25846617a600ea5040a27781c9 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 15 Jun 2026 10:22:26 +0800 Subject: [PATCH 1/3] fix(ci): keep upgrade-deps green when rolldown bumps oxc The daily upgrade bumps the rolldown hash to a commit built against a newer oxc release, but nothing updated vp's root Cargo.toml oxc pins, so the vendored rolldown crates failed to compile (e.g. oxc_ast::template_element changing arity between 0.134 and 0.135). With the native binding missing, the unguarded "Format code" step (vp fmt) then crashed and hard-failed the whole job, so no PR was created. - sync-remote now rewrites the root Cargo.toml oxc pins to match rolldown/Cargo.toml (same-version family follows rolldown's umbrella oxc version; independently-pinned crates follow their explicit pin; [patch.*] left untouched), with a unit test. - "Format code" is now continue-on-error so a broken build still produces a PR, matching the existing build steps. --- .github/workflows/upgrade-deps.yml | 21 ++- .../src/__tests__/sync-remote-deps.spec.ts | 106 ++++++++++++- packages/tools/src/sync-remote-deps.ts | 139 ++++++++++++++++++ 3 files changed, 262 insertions(+), 4 deletions(-) diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index 43978a16ee..e220537f09 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -82,8 +82,9 @@ jobs: - Upgrade script: `./.github/scripts/upgrade-deps.ts` - Sync-remote tool: `pnpm tool sync-remote` (source in `packages/tools/src/sync-remote-deps.ts`) — clones rolldown/vite into the - working tree and merges their pnpm-workspace catalogs into the root - `pnpm-workspace.yaml`. + working tree, merges their pnpm-workspace catalogs into the root + `pnpm-workspace.yaml`, and syncs the root `Cargo.toml` oxc crate + versions to match `rolldown/Cargo.toml`. - Build-upstream action: `./.github/actions/build-upstream/action.yml` - Package manager: `pnpm`. Do NOT downgrade any dep — we want the latest. @@ -105,7 +106,15 @@ jobs: then re-run `pnpm tool sync-remote` until it exits 0. Finish with `pnpm install --no-frozen-lockfile`. 2. Re-run the steps in `./.github/actions/build-upstream/action.yml`; fix any - non-zero exits. + non-zero exits. If the Rust build fails inside a vendored `rolldown` + crate with an oxc API mismatch (e.g. `oxc_ast::template_element` taking + a different number of arguments, or any `oxc_*` type/signature error), + the root `Cargo.toml` oxc pins are out of sync with the bumped rolldown. + `pnpm tool sync-remote` should already reconcile them; if any `oxc_*` + entry in the root `Cargo.toml [workspace.dependencies]` still differs + from `rolldown/Cargo.toml`, bump it to match (the oxc same-version + family follows rolldown's umbrella `oxc` version), run `cargo update`, + and rebuild. 3. If the rolldown hash changed, follow `.claude/agents/cargo-workspace-merger.md` to resync the workspace. 4. Compare tsdown CLI options with `vp pack` and sync new/removed options per @@ -182,6 +191,12 @@ jobs: pnpm dedupe - name: Format code + # `pnpm fmt` runs `vp fmt`, which loads the freshly built NAPI binding. When + # `build-upstream` fails (e.g. an upstream rolldown/oxc desync the in-workflow + # fixup could not resolve) the binding is never produced and `vp fmt` aborts. + # Keep going so the PR is still created with the broken state surfaced for review, + # matching the `continue-on-error` on the build steps above. + continue-on-error: true run: pnpm fmt - name: Enhance PR description with Claude diff --git a/packages/tools/src/__tests__/sync-remote-deps.spec.ts b/packages/tools/src/__tests__/sync-remote-deps.spec.ts index 626e171d8b..f530b59045 100644 --- a/packages/tools/src/__tests__/sync-remote-deps.spec.ts +++ b/packages/tools/src/__tests__/sync-remote-deps.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from '@voidzero-dev/vite-plus-test'; import * as semver from 'semver'; -import { mergePnpmWorkspaces } from '../sync-remote-deps.ts'; +import { mergePnpmWorkspaces, syncCargoOxcVersions } from '../sync-remote-deps.ts'; describe('mergePnpmWorkspaces() minimumReleaseAgeExclude', () => { test('drops versioned upstream entries already covered by a glob or bare pattern', () => { @@ -80,3 +80,107 @@ describe('mergePnpmWorkspaces() minimumReleaseAgeExclude', () => { expect(result.minimumReleaseAgeExclude).toEqual(['@oxc-parser/*', 'oxc-parser']); }); }); + +// Reproduces the upstream-upgrade build break: when the bumped rolldown hash +// pins a newer oxc release (e.g. 0.135.0 / oxc_index 5), the vendored rolldown +// crates fail to compile against vp's stale `Cargo.toml` oxc pin (0.134.0). The +// root `Cargo.toml` oxc versions must follow rolldown's `Cargo.toml`. +describe('syncCargoOxcVersions()', () => { + const mainCargo = `[workspace] +members = ["crates/*"] + +[workspace.dependencies] +serde = "1" + +# oxc crates with the same version +oxc = { version = "0.134.0", features = [ + "ast_visit", + "transformer", +] } +oxc_allocator = { version = "0.134.0", features = ["pool"] } +oxc_ast = "0.134.0" +oxc_parser = "0.134.0" +oxc_span = "0.134.0" +oxc_traverse = "0.134.0" + +# oxc crates in their own repos +oxc_index = { version = "4", features = ["rayon", "serde"] } +oxc_resolver = { version = "11.21.0", features = ["yarn_pnp"] } +oxc_sourcemap = "7" + +[profile.release] +lto = true +`; + + const rolldownCargo = `[workspace] +members = ["crates/*"] + +[workspace.dependencies] +# oxc crates with the same version +oxc = { version = "0.135.0", features = [ + "ast_visit", + "transformer", +] } +oxc_allocator = { version = "0.135.0", features = ["pool"] } +oxc_traverse = { version = "0.135.0" } + +# oxc crates in their own repos +oxc_index = { version = "5", features = ["rayon", "serde"] } +oxc_resolver = { version = "11.21.0", features = ["yarn_pnp"] } +oxc_sourcemap = { version = "7" } +`; + + test('bumps the oxc same-version family and oxc_index to match rolldown', () => { + const { content, changes } = syncCargoOxcVersions(mainCargo, rolldownCargo); + + // Same-version family follows rolldown's umbrella `oxc` version, including + // crates rolldown does not declare explicitly (oxc_ast/oxc_parser/oxc_span). + expect(content).toContain('oxc = { version = "0.135.0"'); + expect(content).toContain('oxc_allocator = { version = "0.135.0"'); + expect(content).toContain('oxc_ast = "0.135.0"'); + expect(content).toContain('oxc_parser = "0.135.0"'); + expect(content).toContain('oxc_span = "0.135.0"'); + expect(content).toContain('oxc_traverse = "0.135.0"'); + // Independently-versioned crate follows rolldown's own pin. + expect(content).toContain('oxc_index = { version = "5"'); + // Unchanged crates stay put. + expect(content).toContain('oxc_resolver = { version = "11.21.0"'); + expect(content).toContain('oxc_sourcemap = "7"'); + // Features and unrelated entries are preserved. + expect(content).toContain('"ast_visit",'); + expect(content).toContain('serde = "1"'); + + const changed = Object.fromEntries(changes.map((c) => [c.key, c.to])); + expect(changed).toMatchObject({ + oxc: '0.135.0', + oxc_allocator: '0.135.0', + oxc_ast: '0.135.0', + oxc_parser: '0.135.0', + oxc_span: '0.135.0', + oxc_traverse: '0.135.0', + oxc_index: '5', + }); + // No spurious changes for already-matching crates. + expect(changes.find((c) => c.key === 'oxc_resolver')).toBeUndefined(); + expect(changes.find((c) => c.key === 'oxc_sourcemap')).toBeUndefined(); + }); + + test('is a no-op when versions already match', () => { + const { content, changes } = syncCargoOxcVersions(mainCargo, mainCargo); + expect(content).toBe(mainCargo); + expect(changes).toEqual([]); + }); + + test('only rewrites entries inside [workspace.dependencies]', () => { + const withPatch = `${mainCargo} +[patch.crates-io] +# pinned override, must not be touched by the oxc sync +oxc_ast = { git = "https://example.com/oxc", rev = "abc" } +`; + const { content } = syncCargoOxcVersions(withPatch, rolldownCargo); + // The dependency entry is bumped... + expect(content).toContain('oxc_ast = "0.135.0"'); + // ...but the [patch] git override is left intact. + expect(content).toContain('oxc_ast = { git = "https://example.com/oxc", rev = "abc" }'); + }); +}); diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts index b2c1278daa..7e4803b36e 100755 --- a/packages/tools/src/sync-remote-deps.ts +++ b/packages/tools/src/sync-remote-deps.ts @@ -743,6 +743,124 @@ export function mergeWorkspaceYaml( return mainDoc.toString(stringifyOptions); } +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Read a crate's version from a Cargo.toml `[workspace.dependencies]` entry. +// Handles both the bare `key = "X"` form and the inline-table +// `key = { version = "X", features = [...] }` form (which may span lines). +function readCargoCrateVersion(src: string, key: string): string | undefined { + const escaped = escapeRegExp(key); + const bare = src.match(new RegExp(`^\\s*${escaped}\\s*=\\s*"([^"]+)"`, 'm')); + if (bare) { + return bare[1]; + } + const tableStart = new RegExp(`^\\s*${escaped}\\s*=\\s*\\{`, 'm').exec(src); + if (!tableStart) { + return undefined; + } + // The inline table ends at the first `}` (oxc tables only nest `[...]` + // feature arrays, never `{...}`), so version lookup stays within this entry. + const after = src.slice(tableStart.index); + const close = after.indexOf('}'); + const body = close >= 0 ? after.slice(0, close) : after; + const version = body.match(/version\s*=\s*"([^"]+)"/); + return version ? version[1] : undefined; +} + +// Replace the version of a single crate entry in-place, preserving features, +// formatting, and surrounding text. Only the first matching entry is rewritten. +function replaceCargoCrateVersion(src: string, key: string, newVersion: string): string { + const escaped = escapeRegExp(key); + const bareRe = new RegExp(`^(\\s*${escaped}\\s*=\\s*")[^"]+(")`, 'm'); + if (bareRe.test(src)) { + return src.replace(bareRe, `$1${newVersion}$2`); + } + const tableStart = new RegExp(`^\\s*${escaped}\\s*=\\s*\\{`, 'm').exec(src); + if (!tableStart) { + return src; + } + const start = tableStart.index; + const close = src.indexOf('}', start); + const end = close >= 0 ? close : src.length; + const table = src + .slice(start, end) + .replace(/(version\s*=\s*")[^"]+(")/, `$1${newVersion}$2`); + return src.slice(0, start) + table + src.slice(end); +} + +export interface CargoOxcChange { + key: string; + from: string; + to: string; +} + +// Keep vp's root `Cargo.toml` oxc pins in lockstep with the vendored rolldown +// source. When the daily upgrade bumps the rolldown hash to a commit built +// against a newer oxc release, rolldown's crates no longer compile against vp's +// stale oxc pin (e.g. `oxc_ast::template_element` gaining an argument between +// 0.134 and 0.135). For every oxc-family crate in the root +// `[workspace.dependencies]`, follow the version rolldown's `Cargo.toml` +// declares; crates rolldown does not pin explicitly but that currently track +// the umbrella `oxc` version (oxc_ast/oxc_parser/oxc_span, ...) follow rolldown's +// `oxc` version. Crates with an independent pin rolldown does not declare are +// left untouched. Only the `[workspace.dependencies]` section is edited so that +// `[patch.*]` overrides are never rewritten. +export function syncCargoOxcVersions( + mainCargoSrc: string, + rolldownCargoSrc: string, +): { content: string; changes: CargoOxcChange[] } { + const changes: CargoOxcChange[] = []; + + const header = '[workspace.dependencies]'; + const headerIdx = mainCargoSrc.indexOf(header); + if (headerIdx < 0) { + return { content: mainCargoSrc, changes }; + } + const afterHeader = headerIdx + header.length; + const nextHeader = mainCargoSrc.slice(afterHeader).search(/\n\[[^\]\n]+\]/); + const sectionEnd = nextHeader < 0 ? mainCargoSrc.length : afterHeader + nextHeader; + + let section = mainCargoSrc.slice(headerIdx, sectionEnd); + + const mainUmbrella = readCargoCrateVersion(section, 'oxc'); + const rolldownUmbrella = readCargoCrateVersion(rolldownCargoSrc, 'oxc'); + + // Collect every oxc-prefixed dependency key declared in the section. + const keys = new Set(); + const keyRe = /^\s*(oxc[\w-]*)\s*=/gm; + for (let m = keyRe.exec(section); m; m = keyRe.exec(section)) { + keys.add(m[1]); + } + + for (const key of keys) { + const current = readCargoCrateVersion(section, key); + if (!current) { + continue; + } + // Prefer rolldown's explicit pin; otherwise, if this crate currently tracks + // the umbrella `oxc` version, follow rolldown's umbrella bump. Independent + // pins rolldown does not declare are skipped. + let target = readCargoCrateVersion(rolldownCargoSrc, key); + if (!target && mainUmbrella && current === mainUmbrella) { + target = rolldownUmbrella; + } + if (!target || target === current) { + continue; + } + section = replaceCargoCrateVersion(section, key, target); + changes.push({ key, from: current, to: target }); + } + + if (changes.length === 0) { + return { content: mainCargoSrc, changes }; + } + + const content = mainCargoSrc.slice(0, headerIdx) + section + mainCargoSrc.slice(sectionEnd); + return { content, changes }; +} + export async function syncRemote() { const { values } = parseArgs({ options: { @@ -833,6 +951,27 @@ export async function syncRemote() { execCommand('pnpm install --no-frozen-lockfile', rootDir); + // Keep the root Cargo.toml oxc pins in lockstep with the vendored rolldown, + // so the bumped rolldown source compiles against the same oxc release. + log('Syncing Cargo.toml oxc versions with rolldown...'); + const mainCargoPath = join(rootDir, 'Cargo.toml'); + const rolldownCargoPath = join(rootDir, ROLLDOWN_DIR, 'Cargo.toml'); + if (existsSync(mainCargoPath) && existsSync(rolldownCargoPath)) { + const { content: cargoContent, changes: cargoChanges } = syncCargoOxcVersions( + readFileSync(mainCargoPath, 'utf-8'), + readFileSync(rolldownCargoPath, 'utf-8'), + ); + if (cargoChanges.length > 0) { + writeFileSync(mainCargoPath, cargoContent, 'utf-8'); + for (const change of cargoChanges) { + log(` ${change.key}: ${change.from} -> ${change.to}`); + } + log('✓ Cargo.toml oxc versions updated; run `cargo update` to refresh Cargo.lock'); + } else { + log('✓ Cargo.toml oxc versions already in sync'); + } + } + // Merge package.json exports log('Merging package.json exports...'); From 1936db8371e99fd0e121f64d263333ab1a94963f Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 15 Jun 2026 10:32:30 +0800 Subject: [PATCH 2/3] refactor(tools): single-pass oxc version parsing in sync-remote Parse each Cargo.toml's oxc pins once into a Map instead of rescanning the source (and recompiling regexes) per crate, and extract the Cargo sync wiring out of syncRemote into a flat helper. No behavior change. --- packages/tools/src/sync-remote-deps.ts | 99 ++++++++++++-------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts index 7e4803b36e..0c088b950b 100755 --- a/packages/tools/src/sync-remote-deps.ts +++ b/packages/tools/src/sync-remote-deps.ts @@ -747,26 +747,18 @@ function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -// Read a crate's version from a Cargo.toml `[workspace.dependencies]` entry. -// Handles both the bare `key = "X"` form and the inline-table -// `key = { version = "X", features = [...] }` form (which may span lines). -function readCargoCrateVersion(src: string, key: string): string | undefined { - const escaped = escapeRegExp(key); - const bare = src.match(new RegExp(`^\\s*${escaped}\\s*=\\s*"([^"]+)"`, 'm')); - if (bare) { - return bare[1]; - } - const tableStart = new RegExp(`^\\s*${escaped}\\s*=\\s*\\{`, 'm').exec(src); - if (!tableStart) { - return undefined; - } - // The inline table ends at the first `}` (oxc tables only nest `[...]` - // feature arrays, never `{...}`), so version lookup stays within this entry. - const after = src.slice(tableStart.index); - const close = after.indexOf('}'); - const body = close >= 0 ? after.slice(0, close) : after; - const version = body.match(/version\s*=\s*"([^"]+)"/); - return version ? version[1] : undefined; +// Parse every `oxc*` crate version from a Cargo.toml in a single pass, keyed by +// crate name. Handles both the bare `oxc_x = "X"` form and the inline-table +// `oxc = { version = "X", features = [...] }` form (which may span lines, but +// always lists `version` before its `[...]` feature array, so `[^}]*?` reaches +// it without crossing the closing `}`). +function parseCargoOxcVersions(src: string): Map { + const versions = new Map(); + const re = /^\s*(oxc[\w-]*)\s*=\s*(?:"([^"]+)"|\{[^}]*?version\s*=\s*"([^"]+)")/gm; + for (let m = re.exec(src); m; m = re.exec(src)) { + versions.set(m[1], m[2] ?? m[3]); + } + return versions; } // Replace the version of a single crate entry in-place, preserving features, @@ -824,25 +816,16 @@ export function syncCargoOxcVersions( let section = mainCargoSrc.slice(headerIdx, sectionEnd); - const mainUmbrella = readCargoCrateVersion(section, 'oxc'); - const rolldownUmbrella = readCargoCrateVersion(rolldownCargoSrc, 'oxc'); - - // Collect every oxc-prefixed dependency key declared in the section. - const keys = new Set(); - const keyRe = /^\s*(oxc[\w-]*)\s*=/gm; - for (let m = keyRe.exec(section); m; m = keyRe.exec(section)) { - keys.add(m[1]); - } + const mainVersions = parseCargoOxcVersions(section); + const rolldownVersions = parseCargoOxcVersions(rolldownCargoSrc); + const mainUmbrella = mainVersions.get('oxc'); + const rolldownUmbrella = rolldownVersions.get('oxc'); - for (const key of keys) { - const current = readCargoCrateVersion(section, key); - if (!current) { - continue; - } + for (const [key, current] of mainVersions) { // Prefer rolldown's explicit pin; otherwise, if this crate currently tracks // the umbrella `oxc` version, follow rolldown's umbrella bump. Independent // pins rolldown does not declare are skipped. - let target = readCargoCrateVersion(rolldownCargoSrc, key); + let target = rolldownVersions.get(key); if (!target && mainUmbrella && current === mainUmbrella) { target = rolldownUmbrella; } @@ -861,6 +844,31 @@ export function syncCargoOxcVersions( return { content, changes }; } +// Rewrite the root Cargo.toml oxc pins in place to match the freshly cloned +// rolldown, so the bumped rolldown source compiles against the same oxc release. +function syncCargoOxcWithRolldown(rootDir: string): void { + const mainCargoPath = join(rootDir, 'Cargo.toml'); + const rolldownCargoPath = join(rootDir, ROLLDOWN_DIR, 'Cargo.toml'); + if (!existsSync(mainCargoPath) || !existsSync(rolldownCargoPath)) { + return; + } + + const { content, changes } = syncCargoOxcVersions( + readFileSync(mainCargoPath, 'utf-8'), + readFileSync(rolldownCargoPath, 'utf-8'), + ); + if (changes.length === 0) { + log('✓ Cargo.toml oxc versions already in sync'); + return; + } + + writeFileSync(mainCargoPath, content, 'utf-8'); + for (const change of changes) { + log(` ${change.key}: ${change.from} -> ${change.to}`); + } + log('✓ Cargo.toml oxc versions updated; run `cargo update` to refresh Cargo.lock'); +} + export async function syncRemote() { const { values } = parseArgs({ options: { @@ -951,26 +959,9 @@ export async function syncRemote() { execCommand('pnpm install --no-frozen-lockfile', rootDir); - // Keep the root Cargo.toml oxc pins in lockstep with the vendored rolldown, - // so the bumped rolldown source compiles against the same oxc release. + // Keep the root Cargo.toml oxc pins in lockstep with the vendored rolldown. log('Syncing Cargo.toml oxc versions with rolldown...'); - const mainCargoPath = join(rootDir, 'Cargo.toml'); - const rolldownCargoPath = join(rootDir, ROLLDOWN_DIR, 'Cargo.toml'); - if (existsSync(mainCargoPath) && existsSync(rolldownCargoPath)) { - const { content: cargoContent, changes: cargoChanges } = syncCargoOxcVersions( - readFileSync(mainCargoPath, 'utf-8'), - readFileSync(rolldownCargoPath, 'utf-8'), - ); - if (cargoChanges.length > 0) { - writeFileSync(mainCargoPath, cargoContent, 'utf-8'); - for (const change of cargoChanges) { - log(` ${change.key}: ${change.from} -> ${change.to}`); - } - log('✓ Cargo.toml oxc versions updated; run `cargo update` to refresh Cargo.lock'); - } else { - log('✓ Cargo.toml oxc versions already in sync'); - } - } + syncCargoOxcWithRolldown(rootDir); // Merge package.json exports log('Merging package.json exports...'); From cb09d30c02c2165b028875175b7fc89cfc2f0132 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 15 Jun 2026 10:45:31 +0800 Subject: [PATCH 3/3] style(tools): apply formatter to replaceCargoCrateVersion --- packages/tools/src/sync-remote-deps.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts index 0c088b950b..029f16e1f2 100755 --- a/packages/tools/src/sync-remote-deps.ts +++ b/packages/tools/src/sync-remote-deps.ts @@ -776,9 +776,7 @@ function replaceCargoCrateVersion(src: string, key: string, newVersion: string): const start = tableStart.index; const close = src.indexOf('}', start); const end = close >= 0 ? close : src.length; - const table = src - .slice(start, end) - .replace(/(version\s*=\s*")[^"]+(")/, `$1${newVersion}$2`); + const table = src.slice(start, end).replace(/(version\s*=\s*")[^"]+(")/, `$1${newVersion}$2`); return src.slice(0, start) + table + src.slice(end); }