rsnext/scripts/sync-react.js
Shu Ding 9c0e520896
Re-land "Vendor react@experimental under an experimentalReact flag" (#48041)
Reverts vercel/next.js#48038

fix NEXT-926

---

The root cause was that when copying the package.json, I removed all
fields except for a few (such as `exports`) but missed the `browser`
field. That caused the client bundle to resolve to the Node.js version
of React DOM, and then we had the `async_hooks` error. Added it back in
99c9b9e51f8b0d4e4503ece9d07bce09161f3341.

I reproduced the error with next-site earlier and confirmed that this
fix is good.
2023-04-08 17:16:24 +02:00

210 lines
6.1 KiB
JavaScript

// @ts-check
const path = require('path')
const { readJson, writeJson } = require('fs-extra')
const execa = require('execa')
/** @type {any} */
const fetch = require('node-fetch')
// 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
async function sync(channel = 'next') {
const noInstall = readBoolArg(process.argv, 'no-install')
const useExperimental = channel === 'experimental'
let newVersionStr = readStringArg(process.argv, 'version')
if (newVersionStr === null) {
const { stdout, stderr } = await execa(
'npm',
[
'view',
useExperimental ? 'react@experimental' : 'react@next',
'version',
],
{
// Avoid "Usage Error: This project is configured to use pnpm".
cwd: '/tmp',
}
)
if (stderr) {
console.error(stderr)
throw new Error(`Failed to read latest React canary version from npm.`)
}
newVersionStr = stdout.trim()
}
const newVersionInfo = extractInfoFromReactCanaryVersion(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 cwd = process.cwd()
const pkgJson = await readJson(path.join(cwd, 'package.json'))
const devDependencies = pkgJson.devDependencies
const baseVersionStr = devDependencies[
useExperimental ? 'react-experimental-builtin' : 'react-builtin'
].replace(/^npm:react@/, '')
const baseVersionInfo = extractInfoFromReactCanaryVersion(baseVersionStr)
if (!baseVersionInfo) {
throw new Error(
'Base react version does not match expected format: ' + baseVersionStr
)
}
const { sha: newSha, dateString: newDateString } = newVersionInfo
const { sha: baseSha, dateString: baseDateString } = baseVersionInfo
console.log(`Updating "react@${channel}" to ${newSha}...\n`)
if (newSha === baseSha) {
console.log('Already up to date.')
return
}
for (const [dep, version] of Object.entries(devDependencies)) {
if (version.endsWith(`${baseSha}-${baseDateString}`)) {
devDependencies[dep] = version.replace(
`${baseSha}-${baseDateString}`,
`${newSha}-${newDateString}`
)
}
}
await writeJson(path.join(cwd, 'package.json'), pkgJson, { spaces: 2 })
console.log('Successfully updated React dependencies in package.json.\n')
// Install the updated dependencies and build the vendored React files.
if (noInstall) {
console.log('Skipping install step because --no-install flag was passed.\n')
} else {
console.log('Installing dependencies...\n')
const installSubprocess = execa('pnpm', ['install'])
if (installSubprocess.stdout) {
installSubprocess.stdout.pipe(process.stdout)
}
try {
await installSubprocess
} catch (error) {
console.error(error)
throw new Error('Failed to install updated dependencies.')
}
console.log('Building vendored React files...\n')
const nccSubprocess = execa('pnpm', ['taskr', 'copy_vendor_react'], {
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.')
}
// Print extra newline after ncc output
console.log()
}
console.log(`Successfully updated React from ${baseSha} to ${newSha}.\n`)
// Fetch the changelog from GitHub and print it to the console.
try {
const changelog = await getChangelogFromGitHub(baseSha, newSha)
if (changelog === null) {
console.log(
`GitHub reported no changes between ${baseSha} and ${newSha}.`
)
} else {
console.log('Includes the following upstream changes:\n\n' + changelog)
}
} catch (error) {
console.error(error)
console.log(
'\nFailed to fetch changelog from GitHub. Changes were applied, anyway.\n'
)
}
if (noInstall) {
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 taskr ncc
Or run this command again without the --no-install flag to do both automatically.
`
)
}
}
function readBoolArg(argv, argName) {
return argv.indexOf('--' + argName) !== -1
}
function readStringArg(argv, argName) {
const argIndex = argv.indexOf('--' + argName)
return argIndex === -1 ? null : argv[argIndex + 1]
}
function extractInfoFromReactCanaryVersion(reactCanaryVersion) {
const match = reactCanaryVersion.match(
/(?<semverVersion>.*)-(?<releaseChannel>.*)-(?<sha>.*)-(?<dateString>.*)$/
)
return match ? match.groups : null
}
async function getChangelogFromGitHub(baseSha, newSha) {
const pageSize = 50
let changelog = []
for (let currentPage = 0; ; currentPage++) {
const response = await fetch(
`https://api.github.com/repos/facebook/react/compare/${baseSha}...${newSha}?per_page=${pageSize}&page=${currentPage}`
)
if (!response.ok) {
throw new Error('Failed to fetch commit log from GitHub.')
}
const data = await response.json()
const { commits } = data
for (const { commit, sha } of commits) {
changelog.push(
`- ${sha.slice(0, 9)} ${commit.message.split('\n')[0]} (${
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
}
sync('next')
.then(() => sync('experimental'))
.catch((error) => {
console.error(error)
process.exit(1)
})