diff --git a/.github/actions/clone/action.yml b/.github/actions/clone/action.yml index cdcea6bfce..3a847d54c8 100644 --- a/.github/actions/clone/action.yml +++ b/.github/actions/clone/action.yml @@ -36,6 +36,7 @@ runs: run: | node -e "console.log('ROLLDOWN_HASH=' + require('./packages/tools/.upstream-versions.json').rolldown.hash)" >> $GITHUB_OUTPUT node -e "console.log('ROLLDOWN_VITE_HASH=' + require('./packages/tools/.upstream-versions.json')['vite'].hash)" >> $GITHUB_OUTPUT + node -e "console.log('OXC_NODE_HASH=' + require('./packages/tools/.upstream-versions.json')['oxc-node'].hash)" >> $GITHUB_OUTPUT - name: Output ecosystem ci project hash shell: bash @@ -62,6 +63,19 @@ runs: ref: ${{ steps.upstream-versions.outputs.ROLLDOWN_VITE_HASH }} persist-credentials: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: oxc-project/oxc-node + path: oxc-node + ref: ${{ steps.upstream-versions.outputs.OXC_NODE_HASH }} + persist-credentials: false + + # oxc-node compiles into the native binding, and Cargo.lock records the + # patched manifest, so patch it before any cargo command reads it. + - name: Patch vendored oxc-node + shell: bash + run: node packages/tools/src/patch-oxc-node.ts + # Match the manifests used by sync-remote when it generated the root lockfile. # The later dependency installation uses --frozen-lockfile, so alignment must run first. - name: Align vendored Vite's Vitest dependencies diff --git a/.github/actions/compute-native-cache-input-hash/action.yml b/.github/actions/compute-native-cache-input-hash/action.yml index f89222f2b6..445bb83df5 100644 --- a/.github/actions/compute-native-cache-input-hash/action.yml +++ b/.github/actions/compute-native-cache-input-hash/action.yml @@ -30,6 +30,8 @@ runs: ${{ hashFiles( 'packages/tools/.upstream-versions.json', + 'packages/tools/src/patch-oxc-node.ts', + 'packages/tools/src/cargo-toml.ts', 'rust-toolchain.toml', 'Cargo.lock', 'Cargo.toml', diff --git a/.github/scripts/__tests__/upgrade-deps.spec.ts b/.github/scripts/__tests__/upgrade-deps.spec.ts index 1569855728..14f9bb88e0 100644 --- a/.github/scripts/__tests__/upgrade-deps.spec.ts +++ b/.github/scripts/__tests__/upgrade-deps.spec.ts @@ -50,7 +50,16 @@ test.each([false, true])( vi.spyOn(process, 'cwd').mockReturnValue(tempDir); vi.spyOn(console, 'log').mockImplementation(() => {}); vi.stubEnv('UPGRADE_DEPS_META_DIR', metaDir); + // Each run also covers one side of the oxc-node pin guard: a release that does not + // contain the pinned commit leaves the pin alone. + const pinnedOxcNode = JSON.parse( + readFileSync(join(tempDir, 'packages/tools/.upstream-versions.json'), 'utf8'), + )['oxc-node'].hash; vi.stubGlobal('fetch', async (url: string) => { + if (url.startsWith('https://api.github.com/repos/oxc-project/oxc-node/compare/')) { + expect(url).toContain(`/compare/${pinnedOxcNode}...`); + return Response.json({ status: failInstall ? 'diverged' : 'ahead' }); + } if (url.startsWith('https://api.github.com/repos/')) { return Response.json([{ name: 'v1.2.3', commit: { sha: 'a'.repeat(40) } }]); } @@ -121,6 +130,11 @@ test.each([false, true])( expect(versions['@oxlint/plugins'].new).toBe('1.88.0'); expect(versions['@oxlint/plugins'].new).toBe(versions.oxlint.new); expect(versions['oxlint-tsgolint'].new).toBe('7.0.2004'); + expect(versions['oxc-node']).toEqual( + failInstall + ? { old: pinnedOxcNode, new: pinnedOxcNode } + : { old: pinnedOxcNode, new: 'a'.repeat(40), tag: 'v1.2.3' }, + ); if (!failInstall) { expect(versions['yuku-parser']).toEqual({ old: '^0.9.3', new: '0.11.2' }); } diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts index bb6c2d1a21..71285474f7 100644 --- a/.github/scripts/upgrade-deps.ts +++ b/.github/scripts/upgrade-deps.ts @@ -48,6 +48,9 @@ type UpstreamVersions = { vite: { hash: string; }; + 'oxc-node': { + hash: string; + }; }; type PnpmWorkspaceVersions = { @@ -106,21 +109,22 @@ function recordChange( } // ============ GitHub API ============ +function fetchGitHubRepo(owner: string, repo: string, endpoint: string): Promise { + return fetch(`https://api.github.com/repos/${owner}/${repo}/${endpoint}`, { + headers: { + Authorization: `token ${process.env.GITHUB_TOKEN}`, + Accept: 'application/vnd.github.v3+json', + }, + }); +} + async function getLatestTag( owner: string, repo: string, options: LatestTagOptions = {}, ): Promise { const perPage = options.stableOnly ? 100 : 1; - const res = await fetch( - `https://api.github.com/repos/${owner}/${repo}/tags?per_page=${perPage}`, - { - headers: { - Authorization: `token ${process.env.GITHUB_TOKEN}`, - Accept: 'application/vnd.github.v3+json', - }, - }, - ); + const res = await fetchGitHubRepo(owner, repo, `tags?per_page=${perPage}`); if (!res.ok) { throw new Error(`Failed to fetch tags for ${owner}/${repo}: ${res.status} ${res.statusText}`); } @@ -141,6 +145,25 @@ async function getLatestTag( return { sha: latest.commit.sha, tag: latest.name }; } +// Whether `head` contains every commit of `base`, so moving a pin from `base` to `head` +// drops nothing. +async function containsCommit( + owner: string, + repo: string, + base: string, + head: string, +): Promise { + if (base === head) { + return true; + } + const res = await fetchGitHubRepo(owner, repo, `compare/${base}...${head}`); + if (!res.ok) { + throw new Error(`Failed to compare ${owner}/${repo} ${base}...${head}: ${res.status}`); + } + const { status } = (await res.json()) as { status?: unknown }; + return status === 'ahead' || status === 'identical'; +} + // ============ npm Registry ============ async function fetchNpmLatest(packageName: string): Promise { const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`); @@ -197,14 +220,24 @@ async function updateUpstreamVersions(): Promise { const oldRolldownHash = data.rolldown.hash; const oldViteHash = data.vite.hash; - const [rolldown, vite] = await Promise.all([ + const oldOxcNodeHash = data['oxc-node'].hash; + const [rolldown, vite, oxcNode] = await Promise.all([ getLatestTag('rolldown', 'rolldown'), getLatestTag('vitejs', 'vite', { stableOnly: true }), + // Vendored into the native binding; `sync-remote` re-applies its patch. + getLatestTag('oxc-project', 'oxc-node', { stableOnly: true }), ]); data.rolldown.hash = rolldown.sha; data.vite.hash = vite.sha; recordChange('rolldown', oldRolldownHash, rolldown.sha, rolldown.tag); recordChange('vite', oldViteHash, vite.sha, vite.tag); + // A pin to an unreleased fix on oxc-node's main stays until a release contains it. + if (await containsCommit('oxc-project', 'oxc-node', oldOxcNodeHash, oxcNode.sha)) { + data['oxc-node'].hash = oxcNode.sha; + recordChange('oxc-node', oldOxcNodeHash, oxcNode.sha, oxcNode.tag); + } else { + recordChange('oxc-node', oldOxcNodeHash, oldOxcNodeHash); + } fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); console.log('Updated .upstream-versions.json'); diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 50088f681c..247e2622f9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -32,7 +32,9 @@ jobs: - name: Output rolldown hash id: upstream-versions shell: bash - run: node -e "console.log('ROLLDOWN_HASH=' + require('./packages/tools/.upstream-versions.json').rolldown.hash)" >> $GITHUB_OUTPUT + run: | + node -e "console.log('ROLLDOWN_HASH=' + require('./packages/tools/.upstream-versions.json').rolldown.hash)" >> $GITHUB_OUTPUT + node -e "console.log('OXC_NODE_HASH=' + require('./packages/tools/.upstream-versions.json')['oxc-node'].hash)" >> $GITHUB_OUTPUT - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -41,6 +43,25 @@ jobs: ref: ${{ steps.upstream-versions.outputs.ROLLDOWN_HASH }} persist-credentials: false + # The binding's `oxc-node` path dependency must resolve against the + # patched manifest recorded in Cargo.lock. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: oxc-project/oxc-node + path: oxc-node + ref: ${{ steps.upstream-versions.outputs.OXC_NODE_HASH }} + persist-credentials: false + + # Run the patch script with native TypeScript support instead of unpinned `npx tsx`. + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .node-version + package-manager-cache: false + + - name: Patch vendored oxc-node + shell: bash + run: node packages/tools/src/patch-oxc-node.ts + - name: Clear git origin for nested checkout shell: bash run: git remote remove origin diff --git a/.gitignore b/.gitignore index c904cdf975..9c4ece5b47 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist rolldown rolldown-vite vite +oxc-node /crates/vp_global_cli/vp .void/ .direnv diff --git a/Cargo.lock b/Cargo.lock index 8db7bc4ff6..ed41b934b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4228,6 +4228,25 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "oxc-node" +version = "0.0.0" +dependencies = [ + "idna", + "memchr", + "napi", + "napi-build", + "napi-derive", + "oxc", + "oxc_resolver", + "oxc_sourcemap", + "phf 0.14.0", + "serde_json", + "simdutf8", + "tracing", + "tracing-subscriber", +] + [[package]] name = "oxc_allocator" version = "0.154.0" @@ -8309,6 +8328,7 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "oxc-node", "petgraph", "pretty_assertions", "rolldown_binding", @@ -8362,6 +8382,7 @@ dependencies = [ "shell-escape", "tempfile", "toml", + "url", "vt_pty_terminal_test", "vt_pty_terminal_test_client", "vt_snapshot_test", @@ -8435,6 +8456,7 @@ dependencies = [ "thiserror 2.0.21", "tokio", "tracing", + "url", "uuid", "vp_cli_help", "vp_command", diff --git a/Cargo.toml b/Cargo.toml index ddddc9472c..bff72dcfb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,9 @@ members = ["bench", "crates/*", "packages/cli/binding"] # It needs a separate release profile and a crate-local build-std config. # Cargo ignores `panic` in per-package profile overrides. # See crates/vp_trampoline/Cargo.toml. -exclude = ["crates/vp_trampoline"] +# oxc-node is vendored upstream source (see packages/tools/src/patch-oxc-node.ts); +# excluding it keeps it out of workspace lints, like the nested rolldown workspace. +exclude = ["crates/vp_trampoline", "oxc-node"] [workspace.metadata.cargo-shear] ignored = [ @@ -354,6 +356,13 @@ oxc_resolver = { version = "11.24.3", features = ["yarn_pnp"] } oxc_resolver_napi = { version = "11.24.3", default-features = false, features = ["yarn_pnp"] } oxc_sourcemap = "9.0.0" +# vendored oxc-node, compiled into the native binding for `vpx