mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
e89118a0ab
Saves a click. These PRs still require approval as per repo settings. The commit description is empty so that the React PRs aren't mentioned again in commits. That'd be pretty noisy for the React PRs.
775 lines
25 KiB
JavaScript
775 lines
25 KiB
JavaScript
// @ts-check
|
|
|
|
const path = require('path')
|
|
const fsp = require('fs/promises')
|
|
const process = require('process')
|
|
const { pathToFileURL } = require('url')
|
|
const execa = require('execa')
|
|
const { Octokit } = require('octokit')
|
|
const SemVer = require('semver')
|
|
const yargs = require('yargs')
|
|
const {
|
|
replayLocalCommitsAsSigned,
|
|
upsertBranchRef,
|
|
} = require('./github-utils/signed-commit')
|
|
|
|
// Use this script to update Next's vendored copy of React and related packages:
|
|
//
|
|
// Basic usage (defaults to most recent React canary version):
|
|
// pnpm run sync-react
|
|
//
|
|
// Update package.json but skip installing the dependencies automatically:
|
|
// pnpm run sync-react --no-install
|
|
//
|
|
// Sync from a local checkout of React (requires having React built first):
|
|
// pnpm run sync-react --version /path/to/react/checkout/
|
|
// Sync from a React commit (can be a commit on a PR)
|
|
// pnpm run sync-react --version vp:///commit-sha
|
|
|
|
const repoOwner = 'vercel'
|
|
const repoName = 'next.js'
|
|
const pullRequestLabels = ['type: react-sync']
|
|
const pullRequestReviewers = ['eps1lon']
|
|
/**
|
|
* Set to `null` to automatically sync the React version of Pages Router with App Router React version.
|
|
* Set to a specific version to override the Pages Router React version e.g. `^19.0.0`.
|
|
*
|
|
* "Active" just refers to our current development practice. While we do support
|
|
* React 18 in pages router, we don't focus our development process on it considering
|
|
* it does not receive new features.
|
|
* @type {string | null}
|
|
*/
|
|
const activePagesRouterReact = '^19.0.0'
|
|
|
|
const defaultLatestChannel = 'canary'
|
|
const filesReferencingReactPeerDependencyVersion = [
|
|
'run-tests.js',
|
|
'packages/create-next-app/templates/index.ts',
|
|
'test/lib/next-modes/base.ts',
|
|
]
|
|
const libraryManifestsSupportingNextjsReact = [
|
|
'packages/third-parties/package.json',
|
|
'packages/next/package.json',
|
|
]
|
|
const appManifestsInstallingNextjsPeerDependencies = [
|
|
'examples/reproduction-template/package.json',
|
|
'test/.stats-app/package.json',
|
|
// TODO: These should use the usual test helpers that automatically install the right React version
|
|
'test/e2e/next-test/first-time-setup-js/package.json',
|
|
'test/e2e/next-test/first-time-setup-ts/package.json',
|
|
]
|
|
|
|
async function getSchedulerVersion(reactVersion) {
|
|
if (reactVersion.startsWith('file://')) {
|
|
return reactVersion
|
|
}
|
|
if (reactVersion.startsWith('vp:')) {
|
|
return reactVersion
|
|
}
|
|
|
|
const url = `https://registry.npmjs.org/react-dom/${reactVersion}`
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`${url}: ${response.status} ${response.statusText}\n${await response.text()}`
|
|
)
|
|
}
|
|
|
|
const manifest = await response.json()
|
|
|
|
return manifest.dependencies['scheduler']
|
|
}
|
|
|
|
/**
|
|
* @param {string} packageName
|
|
* @param {string} versionStr An NPM version or a file URL to a React checkout
|
|
* @returns {string}
|
|
*/
|
|
function getPackageVersion(packageName, versionStr) {
|
|
if (versionStr.startsWith('file://')) {
|
|
return new URL(packageName, versionStr).href
|
|
}
|
|
if (versionStr.startsWith('vp:')) {
|
|
const { pathname } = new URL(versionStr)
|
|
const [, commit, releaseChannel] = pathname.split('/')
|
|
return new URL(
|
|
`/react/commits/${commit}/${packageName}@${releaseChannel}`,
|
|
'https://vercel-packages.vercel.app'
|
|
).href
|
|
}
|
|
|
|
return `npm:${packageName}@${versionStr}`
|
|
}
|
|
|
|
async function sync({ channel, newVersionStr, noInstall }) {
|
|
const useExperimental = channel === 'experimental'
|
|
const cwd = process.cwd()
|
|
const pkgJson = JSON.parse(
|
|
await fsp.readFile(path.join(cwd, 'package.json'), 'utf-8')
|
|
)
|
|
const devDependencies = pkgJson.devDependencies
|
|
const pnpmOverrides = pkgJson.pnpm.overrides
|
|
const baseVersionStr = devDependencies[
|
|
useExperimental ? 'react-experimental-builtin' : 'react-builtin'
|
|
].replace(/^npm:react@/, '')
|
|
|
|
console.log(`Updating "react@${channel}" to ${newVersionStr}...`)
|
|
if (newVersionStr === baseVersionStr) {
|
|
console.log('Already up to date.')
|
|
return
|
|
}
|
|
|
|
const newSchedulerVersionStr = await getSchedulerVersion(newVersionStr)
|
|
console.log(`Updating "scheduler@${channel}" to ${newSchedulerVersionStr}...`)
|
|
|
|
for (const packageName of ['react', 'react-dom']) {
|
|
devDependencies[
|
|
`${packageName}${useExperimental ? '-experimental' : ''}-builtin`
|
|
] = getPackageVersion(packageName, newVersionStr)
|
|
|
|
if (!useExperimental) {
|
|
pnpmOverrides[packageName] = getPackageVersion(packageName, newVersionStr)
|
|
}
|
|
}
|
|
|
|
for (const packageName of [
|
|
'react-server-dom-turbopack',
|
|
'react-server-dom-webpack',
|
|
]) {
|
|
devDependencies[`${packageName}${useExperimental ? '-experimental' : ''}`] =
|
|
getPackageVersion(packageName, newVersionStr)
|
|
}
|
|
|
|
devDependencies[
|
|
`scheduler-${useExperimental ? 'experimental-' : ''}builtin`
|
|
] = getPackageVersion('scheduler', newSchedulerVersionStr)
|
|
if (!useExperimental) {
|
|
pnpmOverrides.scheduler = getPackageVersion(
|
|
'scheduler',
|
|
newSchedulerVersionStr
|
|
)
|
|
|
|
// TODO: Should be handled like the other React packages
|
|
devDependencies['react-is-builtin'] = newVersionStr.startsWith('file://')
|
|
? new URL('react-is', newVersionStr).href
|
|
: newVersionStr.startsWith('vp:')
|
|
? getPackageVersion('react-is', newVersionStr)
|
|
: `npm:react-is@${newVersionStr}`
|
|
pnpmOverrides['react-is'] = newVersionStr.startsWith('file://')
|
|
? new URL('react-is', newVersionStr).href
|
|
: newVersionStr.startsWith('vp:')
|
|
? getPackageVersion('react-is', newVersionStr)
|
|
: `npm:react-is@${newVersionStr}`
|
|
}
|
|
|
|
await fsp.writeFile(
|
|
path.join(cwd, 'package.json'),
|
|
JSON.stringify(pkgJson, null, 2) +
|
|
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
|
|
'\n'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @typedef {object} ReactVersionInfo
|
|
* @property {string} semverVersion - The semver version of React.
|
|
* @property {string} releaseLabel - The release label of React (e.g. "canary", "rc").
|
|
* @property {string} sha - The commit SHA of the React version.
|
|
* @property {string} dateString - The date string of the React version.
|
|
* @returns {ReactVersionInfo}
|
|
*/
|
|
function extractInfoFromReactVersion(versionStr) {
|
|
if (versionStr.startsWith('file://')) {
|
|
return {
|
|
dateString: new Date().toISOString().split('T')[0],
|
|
releaseLabel: 'local',
|
|
semverVersion: '0.0.0',
|
|
sha: 'local',
|
|
}
|
|
}
|
|
if (versionStr.startsWith('vp:')) {
|
|
const { pathname } = new URL(versionStr)
|
|
const [, commit] = pathname.split('/')
|
|
return {
|
|
dateString: new Date().toISOString().split('T')[0],
|
|
releaseLabel: 'vercel-packages',
|
|
semverVersion: '0.0.0',
|
|
sha: commit,
|
|
}
|
|
}
|
|
if (versionStr.startsWith('https:')) {
|
|
const url = new URL(versionStr)
|
|
if (url.hostname === 'vercel-packages.vercel.app') {
|
|
// e.g https://vercel-packages.vercel.app/react/commits/bc50ab4bffa17f507386554a8ef3c3ed4f37fe1b/react@canary
|
|
const [, , , commit] = url.pathname.split('/')
|
|
return {
|
|
dateString: new Date().toISOString().split('T')[0],
|
|
releaseLabel: `vercel-packages`,
|
|
semverVersion: '0.0.0',
|
|
sha: commit,
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Unsupported URL '${versionStr}'. Only vercel-packages.vercel.app URLs are supported.`
|
|
)
|
|
}
|
|
|
|
const match = versionStr.match(
|
|
/(?<semverVersion>.*)-(?<releaseLabel>.*)-(?<sha>.*)-(?<dateString>.*)$/
|
|
)
|
|
return match ? match.groups : null
|
|
}
|
|
|
|
async function getChangelogFromGitHub(baseSha, newSha) {
|
|
const pageSize = 50
|
|
let changelog = []
|
|
for (let currentPage = 1; ; currentPage++) {
|
|
const url = `https://api.github.com/repos/facebook/react/compare/${baseSha}...${newSha}?per_page=${pageSize}&page=${currentPage}`
|
|
const headers = new Headers()
|
|
// GITHUB_TOKEN is optional but helps in case of rate limiting during development.
|
|
if (process.env.GITHUB_TOKEN) {
|
|
headers.set('Authorization', `token ${process.env.GITHUB_TOKEN}`)
|
|
}
|
|
const response = await fetch(url, {
|
|
headers,
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`${response.url}: Failed to fetch commit log from GitHub:\n${response.statusText}\n${await response.text()}`
|
|
)
|
|
}
|
|
const data = await response.json()
|
|
|
|
const { commits } = data
|
|
for (const { commit, sha } of commits) {
|
|
const title = commit.message.split('\n')[0] || ''
|
|
const match =
|
|
// The "title" looks like "[Fiber][Float] preinitialized stylesheets should support integrity option (#26881)"
|
|
/\(#([0-9]+)\)$/.exec(title) ??
|
|
// or contains "Pull Request resolved: https://github.com/facebook/react/pull/12345" in the body if merged via ghstack (e.g. https://github.com/facebook/react/commit/0a0a5c02f138b37e93d5d93341b494d0f5d52373)
|
|
/^Pull Request resolved: https:\/\/github.com\/facebook\/react\/pull\/([0-9]+)$/m.exec(
|
|
commit.message
|
|
)
|
|
const prNum = match ? match[1] : ''
|
|
if (prNum) {
|
|
changelog.push(`- https://github.com/facebook/react/pull/${prNum}`)
|
|
} else {
|
|
changelog.push(
|
|
`- [${commit.message.split('\n')[0]} facebook/react@${sha.slice(0, 9)}](https://github.com/facebook/react/commit/${sha}) (${commit.author.name})`
|
|
)
|
|
}
|
|
}
|
|
|
|
if (commits.length < pageSize) {
|
|
// If the number of commits is less than the page size, we've reached
|
|
// the end. Otherwise we'll keep fetching until we run out.
|
|
break
|
|
}
|
|
}
|
|
|
|
changelog.reverse()
|
|
|
|
return changelog.length > 0 ? changelog.join('\n') : null
|
|
}
|
|
|
|
async function findHighestNPMReactVersion(versionLike) {
|
|
const { stdout, stderr } = await execa(
|
|
'npm',
|
|
['--silent', 'view', '--json', `react@${versionLike}`, 'version'],
|
|
{
|
|
// Avoid "Usage Error: This project is configured to use pnpm".
|
|
cwd: '/tmp',
|
|
}
|
|
)
|
|
if (stderr) {
|
|
console.error(stderr)
|
|
throw new Error(
|
|
`Failed to read highest react@${versionLike} version from npm.`
|
|
)
|
|
}
|
|
|
|
const result = JSON.parse(stdout)
|
|
|
|
return typeof result === 'string'
|
|
? result
|
|
: result.sort((a, b) => {
|
|
return SemVer.compare(b, a)
|
|
})[0]
|
|
}
|
|
|
|
async function main() {
|
|
const cwd = process.cwd()
|
|
const errors = []
|
|
const argv = await yargs(process.argv.slice(2))
|
|
.version(false)
|
|
.options('actor', {
|
|
type: 'string',
|
|
description:
|
|
'Required with `--create-pull`. The actor (GitHub username) that runs this script. Will be used for notifications but not commit attribution.',
|
|
})
|
|
.options('create-pull', {
|
|
default: false,
|
|
type: 'boolean',
|
|
description: 'Create a Pull Request in vercel/next.js',
|
|
})
|
|
.options('commit', {
|
|
default: false,
|
|
type: 'boolean',
|
|
description:
|
|
'Creates commits for each intermediate step. Useful to create better diffs for GitHub.',
|
|
})
|
|
.options('install', { default: true, type: 'boolean' })
|
|
.options('version', {
|
|
default: null,
|
|
type: 'string',
|
|
description:
|
|
'e.g. 19.3.0-canary-?-? or vp:///commit-sha for a build from a specific React commit (can be a commit on a PR)',
|
|
}).argv
|
|
let { actor, createPull, commit, install, version } = argv
|
|
if (version !== null && version.startsWith('/')) {
|
|
version = pathToFileURL(version).href
|
|
// Ensure trailing slash so that the URL is treated as a directory.
|
|
if (!version.endsWith('/')) {
|
|
version += '/'
|
|
}
|
|
}
|
|
|
|
async function commitEverything(message) {
|
|
await execa('git', ['add', '-A'])
|
|
await execa('git', [
|
|
'commit',
|
|
'--message',
|
|
message,
|
|
'--no-verify',
|
|
// Some steps can be empty, e.g. when we don't sync Pages router
|
|
'--allow-empty',
|
|
])
|
|
}
|
|
|
|
if (createPull && !actor) {
|
|
throw new Error(
|
|
`Pull Request cannot be created without a GitHub actor (received '${String(actor)}'). ` +
|
|
'Pass an actor via `--actor "some-actor"`.'
|
|
)
|
|
}
|
|
const githubToken = process.env.GITHUB_TOKEN
|
|
if (createPull && !githubToken) {
|
|
throw new Error(
|
|
`Environment variable 'GITHUB_TOKEN' not specified but required when --create-pull is specified.`
|
|
)
|
|
}
|
|
const releaseGithubToken = process.env.RELEASE_GITHUB_TOKEN
|
|
const releaseAppSlug = process.env.RELEASE_GITHUB_APP_SLUG
|
|
const releaseAppUserId = process.env.RELEASE_GITHUB_APP_USER_ID
|
|
if (createPull) {
|
|
if (!releaseGithubToken) {
|
|
throw new Error(
|
|
`Environment variable 'RELEASE_GITHUB_TOKEN' not specified but required when --create-pull is specified.`
|
|
)
|
|
}
|
|
if (!releaseAppSlug || !releaseAppUserId) {
|
|
throw new Error(
|
|
`Environment variables 'RELEASE_GITHUB_APP_SLUG' and 'RELEASE_GITHUB_APP_USER_ID' must be set when --create-pull is specified.`
|
|
)
|
|
}
|
|
|
|
// Set the git author up-front so all subsequent local commits made by
|
|
// this script (intermediate `--commit` steps and the final PR commit)
|
|
// succeed even on CI runners that don't have a default git identity.
|
|
// The values themselves are discarded by the GitHub REST API: the
|
|
// GPG-signed commits on the remote are attributed to the app token's
|
|
// identity regardless of local git config.
|
|
const botUserName = `${releaseAppSlug}[bot]`
|
|
const botUserEmail = `${releaseAppUserId}+${releaseAppSlug}[bot]@users.noreply.github.com`
|
|
await execa('git', ['config', 'user.name', botUserName])
|
|
await execa('git', ['config', 'user.email', botUserEmail])
|
|
}
|
|
|
|
let newVersionStr = version
|
|
if (
|
|
newVersionStr === null ||
|
|
// TODO: Fork arguments in GitHub workflow to ensure `--version ""` is considered a mistake
|
|
newVersionStr === ''
|
|
) {
|
|
newVersionStr = await findHighestNPMReactVersion(defaultLatestChannel)
|
|
console.log(
|
|
`--version was not provided. Using react@${defaultLatestChannel}: ${newVersionStr}`
|
|
)
|
|
}
|
|
|
|
const newVersionInfo = extractInfoFromReactVersion(newVersionStr)
|
|
if (!newVersionInfo) {
|
|
throw new Error(
|
|
`New react version does not match expected format: ${newVersionStr}
|
|
|
|
Choose a React canary version from npm: https://www.npmjs.com/package/react?activeTab=versions
|
|
|
|
Or, run this command with no arguments to use the most recently published version.
|
|
`
|
|
)
|
|
}
|
|
const {
|
|
sha: newSha,
|
|
dateString: newDateString,
|
|
releaseLabel,
|
|
} = newVersionInfo
|
|
|
|
const branchName =
|
|
releaseLabel === 'local'
|
|
? // left to user to name their local sync branch
|
|
`update/react/local`
|
|
: releaseLabel === 'vercel-packages'
|
|
? `update/react/remote/vercel-packages/${newSha}`
|
|
: `update/react/${newVersionStr}`
|
|
if (createPull) {
|
|
const { exitCode, all, command } = await execa(
|
|
'git',
|
|
[
|
|
'ls-remote',
|
|
'--exit-code',
|
|
'--heads',
|
|
'origin',
|
|
`refs/heads/${branchName}`,
|
|
],
|
|
{ reject: false }
|
|
)
|
|
|
|
if (exitCode === 2) {
|
|
console.log(
|
|
`No sync in progress in branch '${branchName}' according to '${command}'. Starting a new one.`
|
|
)
|
|
} else if (exitCode === 0) {
|
|
console.log(
|
|
`An existing sync already exists in branch '${branchName}'. Delete the branch to start a new sync.`
|
|
)
|
|
return
|
|
} else {
|
|
throw new Error(
|
|
`Failed to check if the branch already existed:\n${command}: ${all}`
|
|
)
|
|
}
|
|
}
|
|
|
|
const rootManifest = JSON.parse(
|
|
await fsp.readFile(path.join(cwd, 'package.json'), 'utf-8')
|
|
)
|
|
const baseVersionStr = rootManifest.devDependencies['react-builtin'].replace(
|
|
/^npm:react@/,
|
|
''
|
|
)
|
|
|
|
let experimentalNewVersionStr = `0.0.0-experimental-${newSha}-${newDateString}`
|
|
if (version !== null && version.startsWith('file://')) {
|
|
experimentalNewVersionStr = new URL('build/oss-experimental/', version).href
|
|
newVersionStr = new URL('build/oss-stable/', version).href
|
|
} else if (releaseLabel === 'vercel-packages') {
|
|
experimentalNewVersionStr = `vp:///${newSha}/experimental`
|
|
newVersionStr = `vp:///${newSha}/canary`
|
|
}
|
|
|
|
await sync({
|
|
newVersionStr: experimentalNewVersionStr,
|
|
noInstall: !install,
|
|
channel: 'experimental',
|
|
})
|
|
if (commit) {
|
|
await commitEverything('Update `react@experimental`')
|
|
}
|
|
await sync({
|
|
newVersionStr,
|
|
noInstall: !install,
|
|
channel: '<framework-stable>',
|
|
})
|
|
if (commit) {
|
|
await commitEverything('Update `react`')
|
|
}
|
|
|
|
const baseVersionInfo = extractInfoFromReactVersion(baseVersionStr)
|
|
if (!baseVersionInfo) {
|
|
throw new Error(
|
|
'Base react version does not match expected format: ' + baseVersionStr
|
|
)
|
|
}
|
|
|
|
const syncPagesRouterReact = activePagesRouterReact === null
|
|
const newActivePagesRouterReactVersion = syncPagesRouterReact
|
|
? newVersionStr
|
|
: activePagesRouterReact
|
|
const pagesRouterReactVersion = `^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ${newActivePagesRouterReactVersion}`
|
|
const highestPagesRouterReactVersion = await findHighestNPMReactVersion(
|
|
pagesRouterReactVersion
|
|
)
|
|
const { sha: baseSha, dateString: baseDateString } = baseVersionInfo
|
|
|
|
for (const fileName of filesReferencingReactPeerDependencyVersion) {
|
|
const filePath = path.join(cwd, fileName)
|
|
const previousSource = await fsp.readFile(filePath, 'utf-8')
|
|
const previousHighestVersionMatch = previousSource.match(
|
|
/const nextjsReactPeerVersion = "([^"]+)";/
|
|
)
|
|
if (previousHighestVersionMatch === null) {
|
|
errors.push(
|
|
new Error(
|
|
`${fileName}: Is this file still referencing the React peer dependency version?`
|
|
)
|
|
)
|
|
} else {
|
|
const updatedSource = previousSource.replace(
|
|
previousHighestVersionMatch[0],
|
|
`const nextjsReactPeerVersion = "${highestPagesRouterReactVersion}";`
|
|
)
|
|
if (updatedSource !== previousSource) {
|
|
await fsp.writeFile(filePath, updatedSource)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const fileName of appManifestsInstallingNextjsPeerDependencies) {
|
|
const packageJsonPath = path.join(cwd, fileName)
|
|
const packageJson = await fsp.readFile(packageJsonPath, 'utf-8')
|
|
const manifest = JSON.parse(packageJson)
|
|
if (manifest.dependencies['react']) {
|
|
manifest.dependencies['react'] = highestPagesRouterReactVersion
|
|
}
|
|
if (manifest.dependencies['react-dom']) {
|
|
manifest.dependencies['react-dom'] = highestPagesRouterReactVersion
|
|
}
|
|
await fsp.writeFile(
|
|
packageJsonPath,
|
|
JSON.stringify(manifest, null, 2) +
|
|
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
|
|
'\n'
|
|
)
|
|
}
|
|
|
|
if (commit) {
|
|
await commitEverything('Updated peer dependency references in apps')
|
|
}
|
|
|
|
for (const fileName of libraryManifestsSupportingNextjsReact) {
|
|
const packageJsonPath = path.join(cwd, fileName)
|
|
const packageJson = await fsp.readFile(packageJsonPath, 'utf-8')
|
|
const manifest = JSON.parse(packageJson)
|
|
// Need to specify last supported RC version to avoid breaking changes.
|
|
if (manifest.peerDependencies['react']) {
|
|
manifest.peerDependencies['react'] = pagesRouterReactVersion
|
|
}
|
|
if (manifest.peerDependencies['react-dom']) {
|
|
manifest.peerDependencies['react-dom'] = pagesRouterReactVersion
|
|
}
|
|
await fsp.writeFile(
|
|
packageJsonPath,
|
|
JSON.stringify(manifest, null, 2) +
|
|
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
|
|
'\n'
|
|
)
|
|
}
|
|
|
|
if (commit) {
|
|
await commitEverything('Updated peer dependency references in libraries')
|
|
}
|
|
|
|
// Install the updated dependencies and build the vendored React files.
|
|
if (!install) {
|
|
console.log('Skipping install step because --no-install flag was passed.')
|
|
} else {
|
|
console.log('Installing dependencies...')
|
|
|
|
const installSubprocess = execa('pnpm', [
|
|
'install',
|
|
// Pnpm freezes the lockfile by default in CI.
|
|
// However, we just changed versions so the lockfile is expected to be changed.
|
|
'--no-frozen-lockfile',
|
|
])
|
|
if (installSubprocess.stdout) {
|
|
installSubprocess.stdout.pipe(process.stdout)
|
|
}
|
|
try {
|
|
await installSubprocess
|
|
} catch (error) {
|
|
console.error(error)
|
|
throw new Error('Failed to install updated dependencies.')
|
|
}
|
|
|
|
if (commit) {
|
|
await commitEverything('Update lockfile')
|
|
}
|
|
|
|
console.log('Building vendored React files...\n')
|
|
const nccSubprocess = execa('pnpm', ['ncc-compiled'], {
|
|
cwd: path.join(cwd, 'packages', 'next'),
|
|
})
|
|
if (nccSubprocess.stdout) {
|
|
nccSubprocess.stdout.pipe(process.stdout)
|
|
}
|
|
try {
|
|
await nccSubprocess
|
|
} catch (error) {
|
|
console.error(error)
|
|
throw new Error('Failed to run ncc.')
|
|
}
|
|
|
|
if (commit) {
|
|
await commitEverything('ncc-compiled')
|
|
}
|
|
|
|
// Print extra newline after ncc output
|
|
console.log()
|
|
}
|
|
|
|
let prDescription = ''
|
|
if (newVersionInfo.releaseLabel === 'local') {
|
|
prDescription = "Can't generate a changelog for local builds"
|
|
} else {
|
|
if (syncPagesRouterReact) {
|
|
prDescription += `**breaking change for canary users: Bumps peer dependency of React from \`${baseVersionStr}\` to \`${pagesRouterReactVersion}\`**\n\n`
|
|
}
|
|
|
|
// Fetch the changelog from GitHub and print it to the console.
|
|
prDescription += `[diff facebook/react@${baseSha}...${newSha}](https://github.com/facebook/react/compare/${baseSha}...${newSha})\n\n`
|
|
try {
|
|
const changelog = await getChangelogFromGitHub(baseSha, newSha)
|
|
if (changelog === null) {
|
|
prDescription += `GitHub reported no changes between ${baseSha} and ${newSha}.`
|
|
} else {
|
|
prDescription += `<details>\n<summary>React upstream changes</summary>\n\n${changelog}\n\n</details>`
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
prDescription +=
|
|
'\nFailed to fetch changelog from GitHub. Changes were applied, anyway.\n'
|
|
}
|
|
}
|
|
|
|
if (!install) {
|
|
console.log(
|
|
`
|
|
To finish upgrading, complete the following steps:
|
|
|
|
- Install the updated dependencies: pnpm install
|
|
- Build the vendored React files: (inside packages/next dir) pnpm ncc-compiled
|
|
|
|
Or run this command again without the --no-install flag to do both automatically.
|
|
`
|
|
)
|
|
}
|
|
|
|
if (errors.length) {
|
|
// eslint-disable-next-line no-undef -- Defined in Node.js
|
|
throw new AggregateError(errors)
|
|
}
|
|
|
|
if (createPull) {
|
|
const octokit = new Octokit({ auth: githubToken })
|
|
const prTitle = `Upgrade React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\``
|
|
|
|
await execa('git', ['checkout', '-b', branchName])
|
|
// We didn't commit intermediate steps yet so now we need to commit to create a PR.
|
|
if (!commit) {
|
|
await commitEverything(prTitle)
|
|
}
|
|
|
|
// Branch protection on canary requires signed commits. Push each local
|
|
// commit to the remote as a GitHub-signed commit via the REST API
|
|
// instead of `git push` (which would push unsigned commits).
|
|
const baseRef = process.env.GITHUB_REF_NAME || 'canary'
|
|
const remoteBaseSha = (
|
|
await execa('git', ['rev-parse', `origin/${baseRef}`])
|
|
).stdout.trim()
|
|
const localCommitSha = (
|
|
await execa('git', ['rev-parse', 'HEAD'])
|
|
).stdout.trim()
|
|
|
|
const { headSha: finalSignedSha } = await replayLocalCommitsAsSigned({
|
|
token: releaseGithubToken,
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
fromBaseSha: remoteBaseSha,
|
|
toLocalSha: localCommitSha,
|
|
// commitEverything uses --allow-empty for steps that didn't change
|
|
// any files (e.g. when Pages Router doesn't need a sync); preserve
|
|
// that on the remote signed commits.
|
|
allowEmpty: true,
|
|
})
|
|
await upsertBranchRef({
|
|
token: releaseGithubToken,
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
branch: branchName,
|
|
sha: finalSignedSha,
|
|
})
|
|
const pullRequest = await octokit.rest.pulls.create({
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
head: branchName,
|
|
base: process.env.GITHUB_REF || 'canary',
|
|
draft: false,
|
|
title: prTitle,
|
|
body: prDescription,
|
|
})
|
|
console.log('Created pull request %s', pullRequest.data.html_url)
|
|
|
|
// Enable GitHub auto-merge with the squash method so the PR merges
|
|
// automatically once required checks pass. GitHub has no REST field for
|
|
// this, so it must be enabled via the GraphQL mutation on the PR node id.
|
|
// The commit title is left to GitHub's default (the PR title with the PR
|
|
// number), while an explicit empty commitBody keeps the squash commit
|
|
// description empty regardless of the repo's squash message setting.
|
|
await octokit.graphql(
|
|
`mutation ($pullRequestId: ID!) {
|
|
enablePullRequestAutoMerge(
|
|
input: {
|
|
pullRequestId: $pullRequestId
|
|
mergeMethod: SQUASH
|
|
commitBody: ""
|
|
}
|
|
) {
|
|
pullRequest {
|
|
autoMergeRequest {
|
|
enabledAt
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
{ pullRequestId: pullRequest.data.node_id }
|
|
)
|
|
|
|
await Promise.all([
|
|
actor
|
|
? octokit.rest.issues.addAssignees({
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
issue_number: pullRequest.data.number,
|
|
assignees: [actor],
|
|
})
|
|
: Promise.resolve(),
|
|
octokit.rest.pulls.requestReviewers({
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
pull_number: pullRequest.data.number,
|
|
reviewers: pullRequestReviewers,
|
|
}),
|
|
octokit.rest.issues.addLabels({
|
|
owner: repoOwner,
|
|
repo: repoName,
|
|
issue_number: pullRequest.data.number,
|
|
labels: pullRequestLabels,
|
|
}),
|
|
])
|
|
}
|
|
|
|
console.log(prDescription)
|
|
console.log(
|
|
`Successfully updated React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\``
|
|
)
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|