UNPKG

next

Version:

The React Framework

342 lines (341 loc) • 14.5 kB
import { readFile } from 'fs/promises'; import { createRequire } from 'module'; import { join } from 'path'; import { resetEnv } from '@next/env'; import semver from 'next/dist/compiled/semver'; import loadConfig from '../../server/config'; import { PHASE_INFO } from '../../shared/lib/constants'; import { getPendingFutureDefaults } from './future-defaults'; export async function prepareUpgrade(directory, targetRequest = 'security') { if (targetRequest !== 'security' && targetRequest !== 'latest' && targetRequest !== 'experimental-future') { throw new Error(`Unsupported agent upgrade type ${JSON.stringify(targetRequest)}. Expected "security", "latest", or "experimental-future".`); } // Resolve from the app: the invoking canary is only the upgrade tooling. const requireFromApp = createRequire(join(directory, 'package.json')); const installedNext = JSON.parse(await readFile(requireFromApp.resolve('next/package.json'), 'utf8')); const installedVersion = installedNext.version; if (!semver.valid(installedVersion)) { throw new Error('Could not determine the installed Next.js version.'); } const { upgrade } = await getUpgradeAssessment(installedVersion, targetRequest); if (upgrade.status !== 'ready' || targetRequest !== 'experimental-future') { return upgrade; } const config = await loadConfig(PHASE_INFO, directory, { silent: true }).finally(resetEnv); const pendingFutureDefaults = getPendingFutureDefaults(directory, config, upgrade.targetVersion); if (upgrade.targetVersion === installedVersion && pendingFutureDefaults.length === 0) { return { status: 'unaffected', reason: `Next.js ${installedVersion} is current and no applicable Future Defaults are pending.` }; } return { ...upgrade, futureDefaults: pendingFutureDefaults }; } // TODO: Cache assessments briefly by installed version, policy, and resolved // target to avoid repeating nudge lookups. New advisories can affect the same // target, so expire cached results and refresh metadata before execution. export async function getUpgradeAssessment(installedVersion, policy, onlyIfAffected = false) { if (!semver.valid(installedVersion)) { throw new Error('The running Next.js version is not valid semver.'); } const channel = getPrereleaseChannel(installedVersion); if (semver.prerelease(installedVersion) && !channel) { throw new Error('Agent upgrades are not available for this prerelease version of Next.js.'); } if (channel && (policy === 'security' || onlyIfAffected)) { return { affected: null, reference: null, upgrade: { status: 'blocked', reason: `The installed Next.js version (${installedVersion}) is a ${channel} prerelease. Security advisories target stable versions, and prereleases do not reliably follow stable version ordering, so an advisory could be a false positive. To upgrade to the latest ${channel === 'canary' ? 'canary' : 'stable'} release, run this command from the app's directory:\n\nnpx next@canary upgrade --agent=latest` } }; } if (channel && channel !== 'canary' && policy === 'experimental-future') { return { affected: null, reference: null, upgrade: { status: 'blocked', reason: `Future Defaults upgrades are not supported for Next.js ${installedVersion}. To upgrade to the latest stable release, run this command from the app's directory:\n\nnpx next@canary upgrade --agent=latest` } }; } // Prerelease version ordering does not establish which security fixes it // contains, but stable promotion targets still need advisory validation. let snapshot = null; if (channel !== 'canary') { try { snapshot = { ranges: await readNpmAdvisories([ installedVersion ]), reference: NPM_ADVISORIES }; } catch { return { affected: null, reference: null, upgrade: { status: 'unknown', reason: 'Could not check for security updates. Please try again.' } }; } } const affected = snapshot && !channel ? snapshot.ranges.some((range)=>semver.satisfies(installedVersion, range)) : null; const assessment = { affected, reference: (snapshot == null ? void 0 : snapshot.reference) ?? null }; // A dismissed release reminder still checks advisories, but does not need // target metadata unless an advisory applies. if ((policy === 'security' || onlyIfAffected) && !affected) { return { ...assessment, upgrade: { status: 'unaffected', reason: `No security update is needed for Next.js ${installedVersion}.` } }; } // Keep a confirmed advisory even when target metadata cannot be read. try { let targetVersion; let references; if (policy === 'security' && snapshot) { const registryURL = `${NPM_REGISTRY}next`; const registry = (await fetchJSON(registryURL)).value; const releases = parseReleases(registry); references = [ snapshot.reference, registryURL ]; const candidates = securityCandidates(installedVersion, releases); const ranges = [ ...snapshot.ranges, ...candidates.length ? await readNpmAdvisories(candidates.map(({ version })=>version)) : [] ]; try { targetVersion = selectSecurityTarget(installedVersion, releases, ranges).version; } catch (error) { return { ...assessment, upgrade: { status: 'blocked', reason: error.message } }; } } else { const release = await fetchLatestRelease(installedVersion); if (!release) { throw new Error(channel === 'canary' ? 'Could not determine the latest Next.js version on the canary dist-tag.' : 'Could not determine the latest stable Next.js version.'); } targetVersion = policy === 'experimental-future' && semver.gt(installedVersion, release.version) ? installedVersion : release.version; references = [ release.reference ]; const releaseKind = channel === 'canary' ? 'canary release' : 'stable release'; if (policy === 'latest' && semver.lt(targetVersion, installedVersion)) { return { ...assessment, upgrade: { status: 'unaffected', reason: `Next.js ${installedVersion} is newer than the latest ${releaseKind} ${targetVersion}.` } }; } const targetRanges = snapshot && targetVersion !== installedVersion ? await readNpmAdvisories([ targetVersion ]) : snapshot == null ? void 0 : snapshot.ranges; if (targetRanges == null ? void 0 : targetRanges.some((range)=>semver.satisfies(targetVersion, range))) { return { ...assessment, upgrade: { status: 'blocked', reason: `Next.js ${targetVersion} is affected by an active advisory.` } }; } if (policy === 'latest' && semver.eq(targetVersion, installedVersion)) { return { ...assessment, upgrade: { status: 'unaffected', reason: `Next.js ${installedVersion} is already the latest ${releaseKind}.` } }; } } return { ...assessment, upgrade: { status: 'ready', installedVersion, targetVersion, references, futureDefaults: [] } }; } catch (error) { return { ...assessment, upgrade: { status: 'unknown', reason: error.message } }; } } const NPM_REGISTRY = 'https://registry.npmjs.org/'; const NPM_ADVISORIES = `${NPM_REGISTRY}-/npm/v1/security/advisories/bulk`; async function fetchJSON(url, init = undefined) { try { const response = await fetch(url, { ...init, headers: { Accept: 'application/json', ...init == null ? void 0 : init.headers }, signal: AbortSignal.timeout(10000), redirect: 'error' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return { value: await response.json() }; } catch (error) { throw new Error('Could not fetch upgrade metadata. Please try again.', { cause: error }); } } export function getPrereleaseChannel(version) { var _semver_prerelease; const channel = (_semver_prerelease = semver.prerelease(version)) == null ? void 0 : _semver_prerelease[0]; return channel === 'canary' || channel === 'rc' || channel === 'beta' || channel === 'preview' ? channel : null; } function parseReleases(value) { const data = value; if (!(data == null ? void 0 : data.versions)) { throw new Error('Could not determine a safe Next.js version.'); } return Object.entries(data.versions).flatMap(([version, metadata])=>{ if (!semver.valid(version) || semver.prerelease(version)) { return []; } if (metadata.version !== version) { throw new Error('Could not determine a safe Next.js version.'); } return [ { version } ]; }); } export function getLatestUpgradeVersion(version, targetVersion) { const channel = getPrereleaseChannel(version); const targetChannel = getPrereleaseChannel(targetVersion); if (!semver.valid(version) || !semver.valid(targetVersion) || semver.prerelease(version) && !channel || semver.prerelease(targetVersion) && !targetChannel || (channel === 'canary' ? targetChannel !== 'canary' : targetChannel !== null) || !semver.gt(targetVersion, version)) { return null; } // Stable releases replacing prereleases still warrant a reminder, even // within the same minor. Patches and consecutive canaries remain explicit. if (!(channel && channel !== 'canary') && semver.major(targetVersion) === semver.major(version) && semver.minor(targetVersion) === semver.minor(version)) { return null; } return targetVersion; } async function fetchLatestRelease(installedVersion) { const channel = getPrereleaseChannel(installedVersion); const releaseChannel = channel === 'canary' ? 'canary' : 'latest'; const reference = `${NPM_REGISTRY}next/${releaseChannel}`; const { value } = await fetchJSON(reference); const release = value; if (!release || !semver.valid(release.version) || channel !== 'canary' && semver.prerelease(release.version) || getPrereleaseChannel(release.version) !== (channel === 'canary' ? 'canary' : null)) { return null; } return { version: release.version, reference }; } // TODO: Record whether each advisory lookup succeeded or failed in upgrade telemetry. async function readNpmAdvisories(versions) { let value; try { value = (await fetchJSON(NPM_ADVISORIES, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ next: versions }) })).value; } catch (error) { throw new Error('Could not check for security updates. Please try again.', { cause: error }); } if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('Could not check for security updates.'); } const data = value; if (Object.keys(data).some((name)=>name !== 'next')) { throw new Error('Could not check for security updates.'); } // The bulk endpoint omits packages with no known vulnerabilities. if (!('next' in data)) { return []; } if (!Array.isArray(data.next)) { throw new Error('Could not check for security updates.'); } return data.next.map((finding)=>{ if (!finding || typeof finding.vulnerable_versions !== 'string' || !finding.vulnerable_versions.trim()) { throw new Error('Could not check for security updates.'); } const range = finding.vulnerable_versions.replace(/,\s*/g, ' '); if (!semver.validRange(range)) { throw new Error('Could not check for security updates.'); } return range; }); } function securityCandidates(source, releases) { // Only the newest stable release of each eligible major is considered. const latest = new Map(); for (const release of releases){ const major = semver.major(release.version); const previous = latest.get(major); if (!previous || semver.gt(release.version, previous.version)) { latest.set(major, release); } } return [ ...latest.entries() ].sort(([a], [b])=>a - b).filter(([major, release])=>major >= semver.major(source) && semver.gt(release.version, source)).map(([, release])=>release); } function selectSecurityTarget(source, releases, ranges) { if (!ranges.some((range)=>semver.satisfies(source, range))) { return; } for (const candidate of securityCandidates(source, releases)){ if (!ranges.some((range)=>semver.satisfies(candidate.version, range))) { return candidate; } } throw new Error('No safe Next.js update is currently available.'); } //# sourceMappingURL=prepare-upgrade.js.map