mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
59b76e42ba
https://github.com/vercel/next.js/pull/98330 revealed theat `@next/third-parties` never worked with prereleases after 16.0. Now we [update `peerDependencies` similar to what we do for preview builds already](https://github.com/vercel/next.js/blob/27f679224e2660971377f855ced51093462734d8/scripts/create-preview-tarballs.js#L130-L132) (we should unify both eventually to go through the same flow). Fixes ``` npm error code ERESOLVE npm error ERESOLVE unable to resolve dependency tree npm error npm error While resolving: undefined@undefined npm error Found: next@16.4.0-canary.22 npm error node_modules/next npm error next@"16.4.0-canary.22" from the root project npm error npm error Could not resolve dependency: npm error peer next@"^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0" from @next/third-parties@16.4.0-canary.22 npm error node_modules/@next/third-parties npm error @next/third-parties@"16.4.0-canary.22" from the root project ``` -- https://github.com/vercel/next.js/actions/runs/34291905918/attempts/2
372 lines
11 KiB
JavaScript
Executable File
372 lines
11 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// @ts-check
|
|
|
|
const path = require('path')
|
|
const execa = require('execa')
|
|
const semver = require('semver')
|
|
const { Sema } = require('async-sema')
|
|
const fs = require('fs/promises')
|
|
const {
|
|
getGitHubToken,
|
|
getGitHubTokenMissingMessage,
|
|
} = require('./release-github-auth')
|
|
|
|
const cwd = process.cwd()
|
|
const dryRun = process.argv.includes('--dry-run')
|
|
const maxPublishAttempts = 4
|
|
const publishRetryDelaySeconds = 15
|
|
|
|
;(async function () {
|
|
if (dryRun) {
|
|
console.log('Dry run: not publishing to npm')
|
|
}
|
|
const publishSema = new Sema(2)
|
|
|
|
const { version } = JSON.parse(
|
|
await fs.readFile(path.join(cwd, 'lerna.json'), 'utf-8')
|
|
)
|
|
const parsedVersion = semver.parse(version)
|
|
if (parsedVersion === null) {
|
|
throw new Error(`Invalid version in lerna.json: ${version}`)
|
|
}
|
|
const prereleaseChannel = parsedVersion.prerelease[0]
|
|
const isPrerelease = prereleaseChannel != null
|
|
console.log(`Publishing ${version}`)
|
|
|
|
let npmDistTag = isPrerelease ? String(prereleaseChannel) : 'latest'
|
|
|
|
try {
|
|
if (!isPrerelease) {
|
|
const res = await fetch(
|
|
`https://registry.npmjs.org/-/package/next/dist-tags`
|
|
)
|
|
const tags = await res.json()
|
|
|
|
if (semver.lt(version, tags.latest)) {
|
|
// If the current version is less than the latest, it means this
|
|
// is a backport release. Since NPM sets the 'latest' tag by default
|
|
// during publishing, when users install `next@latest`, they might
|
|
// get the backported version instead of the actual "latest" version.
|
|
// Therefore, we explicitly set the tag as 'backport' for backports.
|
|
// But force @latest tag if we accidentally tagged a prerelase as latest
|
|
if (!semver.prerelease(tags.latest)) {
|
|
npmDistTag = 'backport'
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Failed to fetch Next.js dist tags from the NPM registry.')
|
|
throw error
|
|
}
|
|
|
|
console.log(`Publishing as "${npmDistTag}" dist tag...`)
|
|
|
|
const publish = async (label, args, attempt = 1) => {
|
|
let output = ''
|
|
try {
|
|
await publishSema.acquire()
|
|
const child = execa('pnpm', args, { stdio: 'pipe' })
|
|
const handleData = (type) => (chunk) => {
|
|
process[type].write(chunk)
|
|
output += chunk.toString()
|
|
}
|
|
child.stdout?.on('data', handleData('stdout'))
|
|
child.stderr?.on('data', handleData('stderr'))
|
|
// Return here to avoid retry logic
|
|
return await child
|
|
} catch (err) {
|
|
console.error(
|
|
`Failed to publish ${label} (attempt ${attempt} of ${maxPublishAttempts})`,
|
|
err
|
|
)
|
|
|
|
if (
|
|
output.includes('cannot publish over the previously published versions')
|
|
) {
|
|
console.error('Ignoring already published error', label)
|
|
return
|
|
}
|
|
|
|
if (attempt >= maxPublishAttempts) {
|
|
throw err
|
|
}
|
|
} finally {
|
|
publishSema.release()
|
|
}
|
|
// Recursive call need to be outside of the publishSema
|
|
console.log(`retrying ${label} in ${publishRetryDelaySeconds}s`)
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, publishRetryDelaySeconds * 1000)
|
|
)
|
|
await publish(label, args, attempt + 1)
|
|
}
|
|
|
|
// Copy binaries to package folders, update version, and publish
|
|
const nativePackagesDir = path.join(cwd, 'crates/next-napi-bindings/npm')
|
|
const platforms = (await fs.readdir(nativePackagesDir)).filter(
|
|
(name) => !name.startsWith('.')
|
|
)
|
|
|
|
const nativeResults = await Promise.allSettled(
|
|
platforms.map(async (platform) => {
|
|
const binaryName = `next-swc.${platform}.node`
|
|
try {
|
|
await fs.cp(
|
|
path.join(cwd, 'packages/next-swc/native', binaryName),
|
|
path.join(nativePackagesDir, platform, binaryName)
|
|
)
|
|
} catch (error) {
|
|
if (dryRun) {
|
|
console.warn(
|
|
`Binary ${binaryName} not found, but ignoring due to dry run`
|
|
)
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
|
|
const pkgDir = path.join(nativePackagesDir, platform)
|
|
const pkg = JSON.parse(
|
|
await fs.readFile(path.join(pkgDir, 'package.json'), {
|
|
encoding: 'utf-8',
|
|
})
|
|
)
|
|
pkg.version = version
|
|
await fs.writeFile(
|
|
path.join(pkgDir, 'package.json'),
|
|
JSON.stringify(pkg, null, 2)
|
|
)
|
|
await publish(platform, [
|
|
'publish',
|
|
pkgDir,
|
|
'--access',
|
|
'public',
|
|
'--no-git-checks',
|
|
'--ignore-scripts',
|
|
'--tag',
|
|
npmDistTag,
|
|
...(dryRun ? ['--dry-run'] : []),
|
|
])
|
|
})
|
|
)
|
|
|
|
// Update name/version of wasm packages and publish
|
|
const pkgDirectory = 'crates/wasm'
|
|
const wasmDir = path.join(cwd, pkgDirectory)
|
|
const wasmResults = await Promise.allSettled(
|
|
['web', 'nodejs'].map(async (wasmTarget) => {
|
|
const pkgDir = path.join(wasmDir, `pkg-${wasmTarget}`)
|
|
const wasmPkg = JSON.parse(
|
|
await fs.readFile(path.join(pkgDir, 'package.json'), {
|
|
encoding: 'utf-8',
|
|
})
|
|
)
|
|
wasmPkg.name = `@next/swc-wasm-${wasmTarget}`
|
|
wasmPkg.version = version
|
|
wasmPkg.repository = {
|
|
type: 'git',
|
|
url: 'https://github.com/vercel/next.js',
|
|
directory: pkgDirectory,
|
|
}
|
|
await fs.writeFile(
|
|
path.join(pkgDir, 'package.json'),
|
|
JSON.stringify(wasmPkg, null, 2)
|
|
)
|
|
await publish(`wasm-${wasmTarget}`, [
|
|
'publish',
|
|
pkgDir,
|
|
'--access',
|
|
'public',
|
|
'--no-git-checks',
|
|
'--ignore-scripts',
|
|
'--tag',
|
|
npmDistTag,
|
|
...(dryRun ? ['--dry-run'] : []),
|
|
])
|
|
})
|
|
)
|
|
|
|
if (nativeResults.some((item) => item.status === 'rejected')) {
|
|
console.error(
|
|
`Not all native packages published successfully`,
|
|
nativeResults
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (wasmResults.some((item) => item.status === 'rejected')) {
|
|
console.error(`Not all wasm packages published successfully`, wasmResults)
|
|
if (process.env.BAIL_ON_NATIVE_WASM_PUBLISH_FAILURE === 'true') {
|
|
process.exit(1)
|
|
} else {
|
|
console.warn(
|
|
'Continuing with release even though some wasm packages failed to publish because BAIL_ON_NATIVE_WASM_PUBLISH_FAILURE is not set to true'
|
|
)
|
|
}
|
|
}
|
|
|
|
// Update optional dependencies versions
|
|
const nextPkg = JSON.parse(
|
|
await fs.readFile(path.join(cwd, 'packages/next/package.json'), {
|
|
encoding: 'utf-8',
|
|
})
|
|
)
|
|
for (const platform of platforms) {
|
|
const optionalDependencies = nextPkg.optionalDependencies || {}
|
|
optionalDependencies['@next/swc-' + platform] = version
|
|
nextPkg.optionalDependencies = optionalDependencies
|
|
}
|
|
// These props are only needed for development and build so we strip
|
|
// before publishing to reduce the total metadata size. The max is
|
|
// 100 MB for all versions of a pkg and we are currently at 30MB. Run:
|
|
// curl -w '%{size_download}' -so /dev/null https://registry.npmjs.org/next | awk '{print $1/1000000 " MB"}'
|
|
for (const field of ['devDependencies', 'taskr', 'scripts']) {
|
|
delete nextPkg[field]
|
|
}
|
|
await fs.writeFile(
|
|
path.join(cwd, 'packages/next/package.json'),
|
|
JSON.stringify(nextPkg, null, 2)
|
|
)
|
|
|
|
if (isPrerelease) {
|
|
// Lerna does not update peerDependencies at version time, and a static
|
|
// range like "^16.0.0" never satisfies a fresh prerelease version.
|
|
// Append the release version to any peer dependency on packages published
|
|
// from this repo so the published prerelease accepts its own version while
|
|
// keeping the stable ranges.
|
|
// Stable releases keep their wide ranges untouched so a published package
|
|
// keeps working with newer Next.js minors.
|
|
// TODO: Use `|| workspace:*` once pnpm supports that. Only a lone `workspace:*`
|
|
// works at the moment
|
|
const pnpmListJson = await execa('pnpm', [
|
|
'--silent',
|
|
'--recursive',
|
|
'--filter',
|
|
'./packages/**',
|
|
'list',
|
|
'--depth',
|
|
'-1',
|
|
'--json',
|
|
])
|
|
const workspacePackages = JSON.parse(pnpmListJson.stdout)
|
|
const publishedPackageNames = new Set(
|
|
workspacePackages
|
|
.filter((workspacePackage) => !workspacePackage.private)
|
|
.map((workspacePackage) => workspacePackage.name)
|
|
)
|
|
|
|
for (const workspacePackage of workspacePackages) {
|
|
if (workspacePackage.private) {
|
|
continue
|
|
}
|
|
|
|
const packageJsonPath = path.join(workspacePackage.path, 'package.json')
|
|
const manifest = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
|
|
const peerDependencies = manifest.peerDependencies ?? {}
|
|
let updated = false
|
|
|
|
for (const dependencyName of Object.keys(peerDependencies)) {
|
|
if (publishedPackageNames.has(dependencyName)) {
|
|
peerDependencies[dependencyName] =
|
|
`${peerDependencies[dependencyName]} || ${version}`
|
|
updated = true
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
await fs.writeFile(packageJsonPath, JSON.stringify(manifest, null, 2))
|
|
console.log(
|
|
`Appended ${version} to peer dependencies in ${workspacePackage.name}`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
await publish('workspace', [
|
|
'--filter',
|
|
'./packages/**',
|
|
'publish',
|
|
'--recursive',
|
|
'--access',
|
|
'public',
|
|
'--no-git-checks',
|
|
'--ignore-scripts',
|
|
'--report-summary',
|
|
'--tag',
|
|
npmDistTag,
|
|
...(dryRun ? ['--dry-run'] : []),
|
|
])
|
|
|
|
if (dryRun) {
|
|
console.log('Dry run: skipping GitHub release un-draft')
|
|
return
|
|
}
|
|
|
|
const githubToken = getGitHubToken()
|
|
|
|
if (!githubToken) {
|
|
throw new Error(getGitHubTokenMissingMessage())
|
|
}
|
|
|
|
if (isPrerelease) {
|
|
try {
|
|
const ghHeaders = {
|
|
Accept: 'application/vnd.github+json',
|
|
Authorization: `Bearer ${githubToken}`,
|
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
}
|
|
const tag = `v${version}`
|
|
|
|
let release
|
|
let releasesData
|
|
|
|
// The release might take a minute to show up in
|
|
// the list so retry a bit
|
|
for (let i = 0; i < 6; i++) {
|
|
try {
|
|
const releaseUrlRes = await fetch(
|
|
`https://api.github.com/repos/vercel/next.js/releases`,
|
|
{
|
|
headers: ghHeaders,
|
|
}
|
|
)
|
|
releasesData = await releaseUrlRes.json()
|
|
|
|
release = releasesData.find((release) => release.tag_name === tag)
|
|
} catch (err) {
|
|
console.log(`Fetching release failed`, err)
|
|
}
|
|
if (!release) {
|
|
console.log(`Retrying in 10s...`)
|
|
await new Promise((resolve) => setTimeout(resolve, 10 * 1000))
|
|
}
|
|
}
|
|
|
|
if (!release) {
|
|
console.log(`Failed to find release`, releasesData)
|
|
return
|
|
}
|
|
|
|
const undraftRes = await fetch(release.url, {
|
|
headers: ghHeaders,
|
|
method: 'PATCH',
|
|
body: JSON.stringify({
|
|
draft: false,
|
|
name: tag,
|
|
}),
|
|
})
|
|
|
|
if (undraftRes.ok) {
|
|
console.log(`un-drafted ${prereleaseChannel} release successfully`)
|
|
} else {
|
|
console.log(`Failed to undraft`, await undraftRes.text())
|
|
}
|
|
} catch (err) {
|
|
console.error(`Failed to undraft release`, err)
|
|
}
|
|
}
|
|
})().catch((err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|