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
21 changes: 18 additions & 3 deletions .github/workflows/upgrade-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
106 changes: 105 additions & 1 deletion packages/tools/src/__tests__/sync-remote-deps.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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" }');
});
});
128 changes: 128 additions & 0 deletions packages/tools/src/sync-remote-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,130 @@ export function mergeWorkspaceYaml(
return mainDoc.toString(stringifyOptions);
}

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// 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<string, string> {
const versions = new Map<string, string>();
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,
// 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 mainVersions = parseCargoOxcVersions(section);
const rolldownVersions = parseCargoOxcVersions(rolldownCargoSrc);
const mainUmbrella = mainVersions.get('oxc');
const rolldownUmbrella = rolldownVersions.get('oxc');

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 = rolldownVersions.get(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 };
}

// 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: {
Expand Down Expand Up @@ -833,6 +957,10 @@ export async function syncRemote() {

execCommand('pnpm install --no-frozen-lockfile', rootDir);

// Keep the root Cargo.toml oxc pins in lockstep with the vendored rolldown.
log('Syncing Cargo.toml oxc versions with rolldown...');
syncCargoOxcWithRolldown(rootDir);

// Merge package.json exports
log('Merging package.json exports...');

Expand Down
Loading