Files
vercel__next.js/scripts/patch-preview-tarball.mjs
T
Tobias Koppers 9c939729d1 Fix preview tarball script to include @next/swc-linux-x64-gnu (#91099)
### What?

Adds `@next/swc-linux-x64-gnu` to the preview tarball patching script
(`scripts/patch-preview-tarball.mjs`):

1. Adds the package to the `PACKAGES_TO_PATCH` list so its tarball URL
is fetched.
2. Adds the package as an explicit dependency in the patched
`package.json`, in addition to the overrides/resolutions.

### Why?

The `@next/swc-linux-x64-gnu` native binary package is an optional
dependency of `next` and may not be installed automatically depending on
the platform/package manager. When testing preview tarballs (e.g., in CI
or on Linux environments), the SWC binary needs to be explicitly present
for Next.js to function correctly. Without this change, preview tarball
testing on Linux x64 could fail because the native binary isn't resolved
from the preview build.

Adding it as a direct dependency (not just an override/resolution)
ensures the package manager actually installs it, since optional
dependencies from overridden tarballs may not be auto-resolved.

### How?

- Added `'@next/swc-linux-x64-gnu'` to the `PACKAGES_TO_PATCH` array so
the script fetches its tarball URL.
- After writing overrides/resolutions, the script now also adds
`@next/swc-linux-x64-gnu` as an explicit entry in `pkg.dependencies`
pointing to the tarball URL.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:32:12 +01:00

241 lines
5.8 KiB
JavaScript

import { execSync } from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import { parseArgs } from 'node:util'
const BASE_URL = 'https://vercel-packages.vercel.app/next/commits'
const PACKAGES_TO_PATCH = [
'next',
'@next/mdx',
'@next/env',
'@next/bundle-analyzer',
'@next/swc-linux-x64-gnu',
]
// --- Argument parsing ---
function parseAndValidateArgs() {
const { values } = parseArgs({
options: {
project: { type: 'string' },
commit: { type: 'string' },
branch: { type: 'string' },
},
strict: true,
})
if (!values.project) {
console.error(
'Usage: node scripts/patch-preview-tarball.mjs --project <path> [--commit <sha> | --branch <name>]'
)
process.exit(1)
}
if (values.commit && values.branch) {
console.error('Error: --commit and --branch are mutually exclusive.')
process.exit(1)
}
return {
project: path.resolve(values.project),
commit: values.commit,
branch: values.branch,
}
}
// --- Resolve commit SHA ---
function resolveCommitSha({ commit, branch }) {
if (commit) {
if (!/^[0-9a-f]{7,40}$/i.test(commit)) {
console.error(`Error: Invalid commit SHA: ${commit}`)
process.exit(1)
}
return commit
}
if (branch) {
const encoded = encodeURIComponent(branch)
try {
const sha = execSync(
`gh api "repos/vercel/next.js/branches/${encoded}" --jq '.commit.sha'`,
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
).trim()
if (!sha) {
console.error(
`Error: Could not resolve branch '${branch}' to a commit SHA.`
)
process.exit(1)
}
return sha
} catch (err) {
console.error(
`Error: Failed to look up branch '${branch}' via GitHub API.`
)
console.error(err.stderr?.toString() || err.message)
process.exit(1)
}
}
// Fallback: local HEAD
try {
return execSync('git rev-parse HEAD', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
}).trim()
} catch (err) {
console.error('Error: Failed to resolve local HEAD commit.')
console.error(err.stderr?.toString() || err.message)
process.exit(1)
}
}
// --- URL construction ---
function buildTarballUrls(commitSha) {
const urls = new Map()
for (const pkg of PACKAGES_TO_PATCH) {
urls.set(pkg, `${BASE_URL}/${commitSha}/${pkg}`)
}
return urls
}
// --- Tarball verification ---
async function verifyTarballExists(url) {
try {
const res = await fetch(url, { method: 'HEAD', redirect: 'follow' })
return res.ok
} catch {
return false
}
}
// --- Workspace root finding ---
async function fileExists(filePath) {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
async function findWorkspaceRoot(projectPath) {
for (const ev of ['NPM_CONFIG_WORKSPACE_DIR', 'npm_config_workspace_dir']) {
if (process.env[ev]) {
return process.env[ev]
}
}
try {
const canonicalPath = await fs.realpath(projectPath)
let currentDir = canonicalPath
while (currentDir !== path.parse(currentDir).root) {
if (await fileExists(path.join(currentDir, 'pnpm-workspace.yaml'))) {
return currentDir
}
const packageJsonPath = path.join(currentDir, 'package.json')
if (await fileExists(packageJsonPath)) {
const content = await fs.readFile(packageJsonPath, 'utf8')
const pkg = JSON.parse(content)
if (pkg.workspaces) {
return currentDir
}
}
currentDir = path.dirname(currentDir)
}
return null
} catch {
return null
}
}
// --- Patch package.json ---
async function patchPackageJson(projectPath, tarballUrls) {
const root = await findWorkspaceRoot(projectPath)
const packageJsonPath = root
? path.join(root, 'package.json')
: path.join(projectPath, 'package.json')
if (!(await fileExists(packageJsonPath))) {
console.error(`Error: package.json not found at ${packageJsonPath}`)
process.exit(1)
}
const content = await fs.readFile(packageJsonPath, 'utf8')
const pkg = JSON.parse(content)
const entries = Array.from(tarballUrls.entries())
// npm/pnpm overrides
pkg.overrides = pkg.overrides || {}
for (const [name, url] of entries) {
pkg.overrides[name] = url
}
// yarn resolutions
pkg.resolutions = pkg.resolutions || {}
for (const [name, url] of entries) {
pkg.resolutions[name] = url
}
// Add @next/swc-linux-x64-gnu to dependencies
pkg.dependencies = pkg.dependencies || {}
pkg.dependencies['@next/swc-linux-x64-gnu'] = tarballUrls.get(
'@next/swc-linux-x64-gnu'
)
await fs.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n')
console.log(`Patched ${packageJsonPath}`)
console.log('Packages overridden:')
for (const [name, url] of entries) {
console.log(` ${name} -> ${url}`)
}
return packageJsonPath
}
// --- Main ---
async function main() {
const { project, commit, branch } = parseAndValidateArgs()
const sha = resolveCommitSha({ commit, branch })
console.log(`Resolved commit SHA: ${sha}`)
const tarballUrls = buildTarballUrls(sha)
const nextUrl = tarballUrls.get('next')
console.log(`Verifying preview tarball exists: ${nextUrl}`)
const exists = await verifyTarballExists(nextUrl)
if (!exists) {
console.error(
`Preview tarball not found for commit ${sha}.\n` +
`The "Deploy preview tarball" job may not have completed yet, or the commit may not have a build.\n` +
`Check: https://github.com/vercel/next.js/actions/workflows/build_and_deploy.yml`
)
process.exit(1)
}
console.log('Preview tarball verified.')
await patchPackageJson(project, tarballUrls)
console.log(
'\nDone! Run your package manager install command to apply changes.'
)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})