2023-03-01 21:15:02 -05:00
// @ts-check
const path = require ( 'path' )
2023-10-18 03:31:19 +08:00
const fsp = require ( 'fs/promises' )
2024-08-23 02:53:10 +02:00
const process = require ( 'process' )
2026-02-20 20:06:34 -08:00
const { pathToFileURL } = require ( 'url' )
2023-03-01 21:15:02 -05:00
const execa = require ( 'execa' )
2024-08-28 17:33:57 +02:00
const { Octokit } = require ( 'octokit' )
2025-04-10 14:39:55 +02:00
const SemVer = require ( 'semver' )
2024-08-23 02:53:10 +02:00
const yargs = require ( 'yargs' )
2026-05-05 10:50:55 +02:00
const {
replayLocalCommitsAsSigned ,
upsertBranchRef ,
} = require ( './github-utils/signed-commit' )
2023-03-01 21:15:02 -05:00
2026-02-20 20:06:34 -08:00
// 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/
2026-02-25 09:49:03 +01:00
// Sync from a React commit (can be a commit on a PR)
// pnpm run sync-react --version vp:///commit-sha
2023-03-07 22:04:08 +01:00
2024-08-28 17:33:57 +02:00
const repoOwner = 'vercel'
const repoName = 'next.js'
const pullRequestLabels = [ 'type: react-sync' ]
const pullRequestReviewers = [ 'eps1lon' ]
2024-12-05 19:21:07 +01:00
/**
* 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`.
2025-04-10 14:39:55 +02:00
*
* "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.
2024-12-05 19:21:07 +01:00
* @type {string | null}
*/
2025-04-10 14:39:55 +02:00
const activePagesRouterReact = '^19.0.0'
2024-08-28 17:33:57 +02:00
2024-12-18 22:09:36 +01:00
const defaultLatestChannel = 'canary'
2024-08-23 00:20:10 +02:00
const filesReferencingReactPeerDependencyVersion = [
'run-tests.js' ,
'packages/create-next-app/templates/index.ts' ,
'test/lib/next-modes/base.ts' ,
]
2024-10-22 20:05:19 +02:00
const libraryManifestsSupportingNextjsReact = [
2024-10-15 05:21:04 +11:00
'packages/third-parties/package.json' ,
2024-10-22 20:05:19 +02:00
'packages/next/package.json' ,
]
const appManifestsInstallingNextjsPeerDependencies = [
2024-08-23 00:20:10 +02:00
'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' ,
]
2024-12-18 22:09:36 +01:00
async function getSchedulerVersion ( reactVersion ) {
2026-02-20 20:06:34 -08:00
if ( reactVersion . startsWith ( 'file://' ) ) {
return reactVersion
}
2026-02-25 09:49:03 +01:00
if ( reactVersion . startsWith ( 'vp:' ) ) {
return reactVersion
}
2026-02-20 20:06:34 -08:00
2024-12-18 22:09:36 +01:00
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' ]
}
2026-02-20 20:06:34 -08:00
/**
* @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
}
2026-02-25 09:49:03 +01:00
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
}
2026-02-20 20:06:34 -08:00
return ` npm: ${ packageName } @ ${ versionStr } `
}
2023-03-01 21:15:02 -05:00
2024-12-18 22:09:36 +01:00
async function sync ( { channel , newVersionStr , noInstall } ) {
2023-04-25 16:29:02 +02:00
const useExperimental = channel === 'experimental'
2023-03-01 21:15:02 -05:00
const cwd = process . cwd ( )
2023-10-18 03:31:19 +08:00
const pkgJson = JSON . parse (
await fsp . readFile ( path . join ( cwd , 'package.json' ) , 'utf-8' )
)
2023-03-01 21:15:02 -05:00
const devDependencies = pkgJson . devDependencies
2024-07-30 18:53:18 +02:00
const pnpmOverrides = pkgJson . pnpm . overrides
2023-04-25 16:29:02 +02:00
const baseVersionStr = devDependencies [
useExperimental ? 'react-experimental-builtin' : 'react-builtin'
] . replace ( /^npm:react@/ , '' )
2023-03-01 21:15:02 -05:00
2024-12-18 22:09:36 +01:00
console . log ( ` Updating "react@ ${ channel } " to ${ newVersionStr } ... ` )
if ( newVersionStr === baseVersionStr ) {
2023-03-01 21:15:02 -05:00
console . log ( 'Already up to date.' )
return
}
2024-12-18 22:09:36 +01:00
const newSchedulerVersionStr = await getSchedulerVersion ( newVersionStr )
console . log ( ` Updating "scheduler@ ${ channel } " to ${ newSchedulerVersionStr } ... ` )
2026-02-20 20:06:34 -08:00
for ( const packageName of [ 'react' , 'react-dom' ] ) {
devDependencies [
` ${ packageName } ${ useExperimental ? '-experimental' : '' } -builtin `
] = getPackageVersion ( packageName , newVersionStr )
if ( ! useExperimental ) {
pnpmOverrides [ packageName ] = getPackageVersion ( packageName , newVersionStr )
2023-03-01 21:15:02 -05:00
}
}
2026-02-20 20:06:34 -08:00
for ( const packageName of [
'react-server-dom-turbopack' ,
'react-server-dom-webpack' ,
] ) {
devDependencies [ ` ${ packageName } ${ useExperimental ? '-experimental' : '' } ` ] =
getPackageVersion ( packageName , newVersionStr )
2024-05-22 23:49:56 +02:00
}
2026-02-20 20:06:34 -08:00
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
2026-02-25 09:49:03 +01:00
: newVersionStr . startsWith ( 'vp:' )
? getPackageVersion ( 'react-is' , newVersionStr )
: ` npm:react-is@ ${ newVersionStr } `
2026-02-20 20:06:34 -08:00
pnpmOverrides [ 'react-is' ] = newVersionStr . startsWith ( 'file://' )
? new URL ( 'react-is' , newVersionStr ) . href
2026-02-25 09:49:03 +01:00
: newVersionStr . startsWith ( 'vp:' )
? getPackageVersion ( 'react-is' , newVersionStr )
: ` npm:react-is@ ${ newVersionStr } `
2026-02-20 20:06:34 -08:00
}
2023-10-18 03:31:19 +08:00
await fsp . writeFile (
path . join ( cwd , 'package.json' ) ,
2024-05-22 23:49:56 +02:00
JSON . stringify ( pkgJson , null , 2 ) +
// Prettier would add a newline anyway so do it manually to skip the additional `pnpm prettier-write`
'\n'
2023-10-18 03:31:19 +08:00
)
2024-08-23 02:53:10 +02:00
}
2026-02-20 20:06:34 -08:00
/**
* @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' ,
}
}
2026-02-25 09:49:03 +01:00
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. `
)
}
2026-02-20 20:06:34 -08:00
const match = versionStr . match (
2024-08-23 02:53:10 +02:00
/(?<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 } `
2026-02-20 20:06:34 -08:00
const headers = new Headers ( )
2024-08-23 02:53:10 +02:00
// GITHUB_TOKEN is optional but helps in case of rate limiting during development.
if ( process . env . GITHUB _TOKEN ) {
2026-02-20 20:06:34 -08:00
headers . set ( 'Authorization' , ` token ${ process . env . GITHUB _TOKEN } ` )
2024-08-23 02:53:10 +02:00
}
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 ( ) } `
2024-08-23 00:20:10 +02:00
)
2024-08-23 02:53:10 +02:00
}
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
2024-08-23 00:20:10 +02:00
)
2024-08-23 02:53:10 +02:00
const prNum = match ? match [ 1 ] : ''
if ( prNum ) {
changelog . push ( ` - https://github.com/facebook/react/pull/ ${ prNum } ` )
2024-08-23 00:20:10 +02:00
} else {
2024-08-23 02:53:10 +02:00
changelog . push (
` - [ ${ commit . message . split ( '\n' ) [ 0 ] } facebook/react@ ${ sha . slice ( 0 , 9 ) } ](https://github.com/facebook/react/commit/ ${ sha } ) ( ${ commit . author . name } ) `
)
2024-08-23 00:20:10 +02:00
}
}
2024-08-23 02:53:10 +02:00
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
}
2025-04-10 14:39:55 +02:00
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 ]
}
2026-08-21 09:53:32 +02:00
/**
* Assigns `actor` to the given Pull Request if they can be assigned.
* On scheduled runs `github.actor` often resolves to a bot like
* `github-actions[bot]`, which cannot be assigned. User tokens silently
* ignore non-assignable assignees but GitHub App tokens fail the whole
* request with 403, so check assignability first and skip instead.
* @param {InstanceType<typeof Octokit>} octokit
* @param {string | undefined} actor
* @param {number} pullRequestNumber
*/
async function assignActorIfAssignable ( octokit , actor , pullRequestNumber ) {
if ( actor === undefined ) {
return null
}
try {
await octokit . rest . issues . checkUserCanBeAssigned ( {
owner : repoOwner ,
repo : repoName ,
assignee : actor ,
} )
} catch ( error ) {
if ( error instanceof Error && 'status' in error && error . status === 404 ) {
console . warn (
` ' ${ actor } ' cannot be assigned in ${ repoOwner } / ${ repoName } . Skipping assignment. `
)
return null
}
throw error
}
return octokit . rest . issues . addAssignees ( {
owner : repoOwner ,
repo : repoName ,
issue _number : pullRequestNumber ,
assignees : [ actor ] ,
} )
}
2024-08-23 02:53:10 +02:00
async function main ( ) {
const cwd = process . cwd ( )
const errors = [ ]
2024-08-27 10:01:31 +02:00
const argv = await yargs ( process . argv . slice ( 2 ) )
. version ( false )
2024-08-28 17:33:57 +02:00
. options ( 'actor' , {
type : 'string' ,
description :
2026-08-05 07:24:09 +02:00
'Required with `--create-pull`. The actor (GitHub username) that runs this script. Will be assigned to the Pull Request for notifications, but neither the commits nor the Pull Request are attributed to them.' ,
2024-08-28 17:33:57 +02:00
} )
. 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.' ,
} )
2024-08-27 10:01:31 +02:00
. options ( 'install' , { default : true , type : 'boolean' } )
2026-02-25 09:49:03 +01:00
. 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
2026-02-20 20:06:34 -08:00
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 += '/'
}
}
2024-08-28 17:33:57 +02:00
async function commitEverything ( message ) {
await execa ( 'git' , [ 'add' , '-A' ] )
2024-12-05 19:21:07 +01:00
await execa ( 'git' , [
'commit' ,
'--message' ,
message ,
'--no-verify' ,
// Some steps can be empty, e.g. when we don't sync Pages router
'--allow-empty' ,
] )
2024-08-28 17:33:57 +02:00
}
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"`.'
)
}
2026-05-05 10:50:55 +02:00
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
2026-08-05 07:24:09 +02:00
// identity regardless of local git config. The same app token opens the
// Pull Request, so the PR author matches the commit author.
2026-05-05 10:50:55 +02:00
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 ] )
}
2024-08-23 02:53:10 +02:00
let newVersionStr = version
2024-08-28 17:33:57 +02:00
if (
newVersionStr === null ||
// TODO: Fork arguments in GitHub workflow to ensure `--version ""` is considered a mistake
newVersionStr === ''
) {
2025-04-10 14:39:55 +02:00
newVersionStr = await findHighestNPMReactVersion ( defaultLatestChannel )
2024-08-28 17:33:57 +02:00
console . log (
2024-12-18 22:09:36 +01:00
` --version was not provided. Using react@ ${ defaultLatestChannel } : ${ newVersionStr } `
2024-08-28 17:33:57 +02:00
)
2024-08-23 02:53:10 +02:00
}
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.
`
2024-08-23 00:20:10 +02:00
)
2024-08-23 02:53:10 +02:00
}
2026-02-25 09:49:03 +01:00
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 } `
2024-08-28 17:33:57 +02:00
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 } `
)
}
}
2024-08-23 02:53:10 +02:00
const rootManifest = JSON . parse (
await fsp . readFile ( path . join ( cwd , 'package.json' ) , 'utf-8' )
)
const baseVersionStr = rootManifest . devDependencies [ 'react-builtin' ] . replace (
/^npm:react@/ ,
''
)
2026-02-20 20:06:34 -08:00
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
2026-02-25 09:49:03 +01:00
} else if ( releaseLabel === 'vercel-packages' ) {
experimentalNewVersionStr = ` vp:/// ${ newSha } /experimental `
newVersionStr = ` vp:/// ${ newSha } /canary `
2026-02-20 20:06:34 -08:00
}
2024-08-23 02:53:10 +02:00
await sync ( {
2026-02-20 20:06:34 -08:00
newVersionStr : experimentalNewVersionStr ,
2024-08-27 10:01:31 +02:00
noInstall : ! install ,
2024-08-23 02:53:10 +02:00
channel : 'experimental' ,
} )
2024-08-28 17:33:57 +02:00
if ( commit ) {
await commitEverything ( 'Update `react@experimental`' )
}
2024-08-23 02:53:10 +02:00
await sync ( {
newVersionStr ,
2024-08-27 10:01:31 +02:00
noInstall : ! install ,
2024-12-18 22:09:36 +01:00
channel : '<framework-stable>' ,
2024-08-23 02:53:10 +02:00
} )
2024-08-28 17:33:57 +02:00
if ( commit ) {
2024-12-18 22:09:36 +01:00
await commitEverything ( 'Update `react`' )
2024-08-28 17:33:57 +02:00
}
2024-08-23 02:53:10 +02:00
const baseVersionInfo = extractInfoFromReactVersion ( baseVersionStr )
if ( ! baseVersionInfo ) {
throw new Error (
'Base react version does not match expected format: ' + baseVersionStr
2024-08-23 00:20:10 +02:00
)
2024-08-23 02:53:10 +02:00
}
2024-08-23 00:20:10 +02:00
2025-04-10 14:39:55 +02:00
const syncPagesRouterReact = activePagesRouterReact === null
const newActivePagesRouterReactVersion = syncPagesRouterReact
2024-12-05 19:21:07 +01:00
? newVersionStr
2025-04-10 14:39:55 +02:00
: activePagesRouterReact
const pagesRouterReactVersion = ` ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ${ newActivePagesRouterReactVersion } `
const highestPagesRouterReactVersion = await findHighestNPMReactVersion (
pagesRouterReactVersion
)
2024-08-23 02:53:10 +02:00
const { sha : baseSha , dateString : baseDateString } = baseVersionInfo
2024-12-05 19:21:07 +01:00
2025-10-13 12:10:10 +02:00
for ( const fileName of filesReferencingReactPeerDependencyVersion ) {
const filePath = path . join ( cwd , fileName )
const previousSource = await fsp . readFile ( filePath , 'utf-8' )
2025-10-13 12:29:19 -07:00
const previousHighestVersionMatch = previousSource . match (
/const nextjsReactPeerVersion = "([^"]+)";/
2025-10-13 12:10:10 +02:00
)
2025-10-13 12:29:19 -07:00
if ( previousHighestVersionMatch === null ) {
2025-10-13 12:10:10 +02:00
errors . push (
new Error (
2025-10-13 12:29:19 -07:00
` ${ fileName } : Is this file still referencing the React peer dependency version? `
2024-12-05 19:21:07 +01:00
)
2025-10-13 12:10:10 +02:00
)
} else {
2025-10-13 12:29:19 -07:00
const updatedSource = previousSource . replace (
previousHighestVersionMatch [ 0 ] ,
` const nextjsReactPeerVersion = " ${ highestPagesRouterReactVersion } "; `
)
if ( updatedSource !== previousSource ) {
await fsp . writeFile ( filePath , updatedSource )
}
2024-08-23 00:20:10 +02:00
}
}
2024-08-23 02:53:10 +02:00
for ( const fileName of appManifestsInstallingNextjsPeerDependencies ) {
const packageJsonPath = path . join ( cwd , fileName )
const packageJson = await fsp . readFile ( packageJsonPath , 'utf-8' )
const manifest = JSON . parse ( packageJson )
2024-10-15 05:21:04 +11:00
if ( manifest . dependencies [ 'react' ] ) {
2025-04-10 14:39:55 +02:00
manifest . dependencies [ 'react' ] = highestPagesRouterReactVersion
2024-10-15 05:21:04 +11:00
}
if ( manifest . dependencies [ 'react-dom' ] ) {
2025-04-10 14:39:55 +02:00
manifest . dependencies [ 'react-dom' ] = highestPagesRouterReactVersion
2024-10-15 05:21:04 +11:00
}
2024-08-23 02:53:10 +02:00
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'
)
}
2024-08-28 17:33:57 +02:00
if ( commit ) {
2024-10-22 20:05:19 +02:00
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 )
2024-12-05 21:19:20 +01:00
// Need to specify last supported RC version to avoid breaking changes.
2024-10-22 20:05:19 +02:00
if ( manifest . peerDependencies [ 'react' ] ) {
2025-04-10 14:39:55 +02:00
manifest . peerDependencies [ 'react' ] = pagesRouterReactVersion
2024-10-22 20:05:19 +02:00
}
if ( manifest . peerDependencies [ 'react-dom' ] ) {
2025-04-10 14:39:55 +02:00
manifest . peerDependencies [ 'react-dom' ] = pagesRouterReactVersion
2024-10-22 20:05:19 +02:00
}
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' )
2024-08-28 17:33:57 +02:00
}
2023-03-01 21:15:02 -05:00
// Install the updated dependencies and build the vendored React files.
2024-08-27 10:01:31 +02:00
if ( ! install ) {
2024-12-18 22:09:36 +01:00
console . log ( 'Skipping install step because --no-install flag was passed.' )
2023-03-01 21:15:02 -05:00
} else {
2024-12-18 22:09:36 +01:00
console . log ( 'Installing dependencies...' )
2023-03-01 21:15:02 -05:00
2024-08-28 17:33:57 +02:00
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' ,
] )
2023-03-01 21:15:02 -05:00
if ( installSubprocess . stdout ) {
installSubprocess . stdout . pipe ( process . stdout )
}
try {
await installSubprocess
} catch ( error ) {
console . error ( error )
throw new Error ( 'Failed to install updated dependencies.' )
}
2024-08-28 17:33:57 +02:00
if ( commit ) {
await commitEverything ( 'Update lockfile' )
}
2023-03-01 21:15:02 -05:00
console . log ( 'Building vendored React files...\n' )
2024-05-22 23:49:56 +02:00
const nccSubprocess = execa ( 'pnpm' , [ 'ncc-compiled' ] , {
2023-03-01 21:15:02 -05:00
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.' )
}
2024-08-28 17:33:57 +02:00
if ( commit ) {
await commitEverything ( 'ncc-compiled' )
}
2023-03-01 21:15:02 -05:00
// Print extra newline after ncc output
console . log ( )
}
2024-12-05 19:21:07 +01:00
let prDescription = ''
2026-02-20 20:06:34 -08:00
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 `
}
2024-08-23 01:39:34 +02:00
2026-02-20 20:06:34 -08:00
// 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'
2023-03-01 21:15:02 -05:00
}
}
2024-08-27 10:01:31 +02:00
if ( ! install ) {
2023-03-01 21:15:02 -05:00
console . log (
`
To finish upgrading, complete the following steps:
- Install the updated dependencies: pnpm install
2024-05-22 23:49:56 +02:00
- Build the vendored React files: (inside packages/next dir) pnpm ncc-compiled
2023-03-01 21:15:02 -05:00
Or run this command again without the --no-install flag to do both automatically.
`
)
}
2024-06-05 18:50:32 +02:00
2024-08-23 00:20:10 +02:00
if ( errors . length ) {
// eslint-disable-next-line no-undef -- Defined in Node.js
throw new AggregateError ( errors )
}
2024-08-28 17:33:57 +02:00
if ( createPull ) {
2026-08-05 07:24:09 +02:00
const octokit = new Octokit ( { auth : releaseGithubToken } )
2024-08-28 17:33:57 +02:00
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 ) {
2026-05-05 10:50:55 +02:00
await commitEverything ( prTitle )
2024-08-28 17:33:57 +02:00
}
2026-05-05 10:50:55 +02:00
// 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 ( )
[cd] add ad-hoc preview release cut from canary (#95086)
Add a `preview` release type so `@preview` npm releases can be cut
**ad-hoc from `canary`** via the `Trigger Release` workflow, retiring
the dedicated long-lived preview branch. The GitHub environment has been
updated accordingly.
A preview cut produces **two commits**, pushed in a single update:
1. **Bump-to-preview** — all packages set to the computed preview
version, tagged `v…-preview.N`. This tag's build publishes `@preview`
(relies on #95085).
2. **Revert-to-canary** — restores the `-canary.N` versions so `canary`
keeps advancing its own line and isn't stranded on a preview version.
```
B canary HEAD before cut (e.g. 16.3.0-canary.61)
│
P v16.3.0-preview.N ← tag here; tag build publishes @preview
│
R canary HEAD after cut (versions restored to 16.3.0-canary.61)
```
### Preview version numbering
`base = max(canary major.minor.patch, npm @preview major.minor.patch)`:
- base still on the published preview line → continue it (`n + 1`)
- canary advanced to a higher base (or no `@preview` published) → reset
to `.0`
So if `canary` jumps to `18.0.0-canary.x` since the last
`16.3.0-preview.x`, the next cut is `18.0.0-preview.0`, not a
continuation.
## Test plan
The dry mode of `start-release.js` was extended to intercept the GH
requests and log the relevant data.
```console
$ node ./scripts/start-release.js --release-type preview --dry-run
Dry run: keeping commits locally, skipping git push and GitHub release creation
Running pnpm release-preview...
[...]
> lerna version 16.3.0-preview.4 --force-publish -y --no-push --allow-branch '**'
[...]
Creating GitHub-signed release commit(s) for v16.3.0-preview.4 from local commits 0f5c1e865cc34edb257a16823ed6525d9cd58bfc..0062348c411776271fe7eac64e97b8bad2775b77
[dry-run] GitHub API POST /repos/vercel/next.js/git/blobs [...]
[dry-run] GitHub API POST /repos/vercel/next.js/git/commits {"message":"v16.3.0-preview.4","tree":"0000000000000000000000000000000000000016","parents":["0f5c1e865cc34edb257a16823ed6525d9cd58bfc"]}
[dry-run] GitHub API POST /repos/vercel/next.js/git/commits {"message":"Restore canary version 16.3.0-canary.61 after v16.3.0-preview.4 preview release","tree":"000000000000000000000000000000000000002d","parents":["0000000000000000000000000000000000000017"]}
[dry-run] GitHub API POST /repos/vercel/next.js/git/refs {"ref":"refs/tags/v16.3.0-preview.4","sha":"0000000000000000000000000000000000000017"}
[dry-run] GitHub API PATCH /repos/vercel/next.js/git/refs/heads/sebbie/preview-release-from-canary {"sha":"000000000000000000000000000000000000002e","force":false}
Dry run: skipping local branch sync; would set sebbie/preview-release-from-canary to 000000000000000000000000000000000000002e and tag v16.3.0-preview.4 at 0000000000000000000000000000000000000017
Created GitHub-signed release tag v16.3.0-preview.4 at 0000000000000000000000000000000000000017; branch sebbie/preview-release-from-canary now at 000000000000000000000000000000000000002e
Dry run: skipping GitHub release creation
Release process is finished
```
2026-06-24 16:33:37 +02:00
const { headSha : finalSignedSha } = await replayLocalCommitsAsSigned ( {
2026-05-05 10:50:55 +02:00
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 ,
} )
2024-08-28 17:33:57 +02:00
const pullRequest = await octokit . rest . pulls . create ( {
owner : repoOwner ,
repo : repoName ,
head : branchName ,
2025-05-21 21:03:19 +02:00
base : process . env . GITHUB _REF || 'canary' ,
2024-08-28 17:33:57 +02:00
draft : false ,
title : prTitle ,
body : prDescription ,
} )
console . log ( 'Created pull request %s' , pullRequest . data . html _url )
2026-07-31 17:03:23 -04:00
// 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 }
)
2026-08-21 09:53:32 +02:00
const finalizeResults = await Promise . allSettled ( [
assignActorIfAssignable ( octokit , actor , pullRequest . data . number ) ,
2024-08-28 17:33:57 +02:00
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 ,
} ) ,
] )
2026-08-21 09:53:32 +02:00
const failures = finalizeResults . filter (
( result ) => result . status === 'rejected'
)
if ( failures . length > 0 ) {
// eslint-disable-next-line no-undef -- Defined in Node.js
throw new AggregateError (
failures . map ( ( failure ) => failure . reason ) ,
` ${ failures . length } of ${ finalizeResults . length } requests to finalize the Pull Request failed. `
)
}
2024-08-28 17:33:57 +02:00
}
console . log ( prDescription )
2024-06-05 18:50:32 +02:00
console . log (
2024-08-23 01:39:34 +02:00
` Successfully updated React from \` ${ baseSha } - ${ baseDateString } \` to \` ${ newSha } - ${ newDateString } \` `
2024-06-05 18:50:32 +02:00
)
2023-03-01 21:15:02 -05:00
}
2024-08-23 02:53:10 +02:00
main ( ) . catch ( ( error ) => {
console . error ( error )
process . exit ( 1 )
} )