Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update lockfile patching for different versions #36959

Merged
merged 3 commits into from
May 17, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 154 additions & 68 deletions packages/next/lib/patch-incorrect-lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { promises } from 'fs'
import '../server/node-polyfill-fetch'
import * as Log from '../build/output/log'
import findUp from 'next/dist/compiled/find-up'
import { execSync } from 'child_process'
// @ts-ignore no-json types
import nextPkgJson from 'next/package.json'
import type { UnwrapPromise } from './coalesced-function'
import { isCI } from '../telemetry/ci-info'

/**
* Attempts to patch npm package-lock.json when it
Expand All @@ -10,93 +15,174 @@ import findUp from 'next/dist/compiled/find-up'
* node_modules install instead of pulling fresh package data
*/
export async function patchIncorrectLockfile(dir: string) {
if (process.env.NEXT_IGNORE_INCORRECT_LOCKFILE) {
return
}
const lockfilePath = await findUp('package-lock.json', { cwd: dir })

if (!lockfilePath) {
// if no lockfile present there is no action to take
return
}
const content = await promises.readFile(lockfilePath, 'utf8')
// maintain current line ending
const endingNewline = content.endsWith('\r\n')
? '\r\n'
: content.endsWith('\n')
? '\n'
: ''

const lockfileParsed = JSON.parse(content)
const lockfileVersion = parseInt(lockfileParsed?.lockfileVersion, 10)
const expectedSwcPkgs = Object.keys(nextPkgJson.optionalDependencies || {})

const foundSwcPkgs = new Set()
const nextPkg = lockfileParsed.packages?.['node_modules/next']
const patchDependency = (
pkg: string,
pkgData: UnwrapPromise<ReturnType<typeof fetchPkgInfo>>
) => {
lockfileParsed.dependencies[pkg] = {
version: nextPkgJson.version,
resolved: pkgData.tarball,
integrity: pkgData.integrity,
optional: true,
}
}

// if we don't find next in the package-lock we can't continue
if (!nextPkg) {
return
const patchPackage = (
pkg: string,
pkgData: UnwrapPromise<ReturnType<typeof fetchPkgInfo>>
) => {
lockfileParsed.packages[pkg] = {
version: nextPkgJson.version,
resolved: pkgData.tarball,
integrity: pkgData.integrity,
cpu: pkgData.cpu,
optional: true,
os: pkgData.os,
engines: pkgData.engines,
}
}
const nextVersion = nextPkg.version
const packageKeys = Object.keys(lockfileParsed.dependencies || {})

const expectedSwcPkgs = Object.keys(nextPkg?.optionalDependencies).filter(
(pkg) => pkg.startsWith('@next/swc-')
)
try {
const supportedVersions = [1, 2, 3]

packageKeys.forEach((pkgKey) => {
const swcIndex = pkgKey.indexOf('@next/swc-')
if (swcIndex > -1) {
foundSwcPkgs.add(pkgKey.substring(swcIndex))
if (!supportedVersions.includes(lockfileVersion)) {
// bail on unsupported version
return
}
})
// v1 only uses dependencies
// v2 uses dependencies and packages
// v3 only uses packages
const shouldPatchDependencies =
lockfileVersion === 1 || lockfileVersion === 2
const shouldPatchPackages = lockfileVersion === 2 || lockfileVersion === 3

// if swc package keys are missing manually populate them
// so installs on different platforms can succeed
// user will need to run npm i after to ensure it's corrected
if (foundSwcPkgs.size !== expectedSwcPkgs.length) {
Log.warn(`Found lockfile missing swc dependencies, patching..`)
if (
(shouldPatchDependencies && !lockfileParsed.dependencies) ||
(shouldPatchPackages && !lockfileParsed.packages)
) {
// invalid lockfile so bail
return
}
const missingSwcPkgs = []
let pkgPrefix: string | undefined

try {
// populate fields for each missing swc pkg
for (const pkg of expectedSwcPkgs) {
if (!foundSwcPkgs.has(pkg)) {
const res = await fetch(`https://registry.npmjs.org/${pkg}`)

if (!res.ok) {
throw new Error(
`Failed to fetch registry info for ${pkg}, got status ${res.status}`
)
}
const data = await res.json()
const version = data.versions[nextVersion]

if (!version) {
throw new Error(
`Failed to find matching version for ${pkg} at ${nextVersion}`
)
}
if (lockfileParsed.dependencies) {
lockfileParsed.dependencies[pkg] = {
version: nextVersion,
resolved: version.dist.tarball,
integrity: version.dist.integrity,
optional: true,
}
}
lockfileParsed.packages[`node_modules/${pkg}`] = {
version: nextVersion,
resolved: version.dist.tarball,
integrity: version.dist.integrity,
cpu: version.cpu,
optional: true,
os: version.os,
engines: version.engines,
}
if (shouldPatchPackages) {
pkgPrefix = ''
for (const pkg of Object.keys(lockfileParsed.packages)) {
if (pkg.endsWith('node_modules/next')) {
pkgPrefix = pkg.substring(0, pkg.length - 4)
}
}

await promises.writeFile(
lockfilePath,
`${JSON.stringify(lockfileParsed, null, 2)}\n`
)
Log.warn(
'Lockfile was successfully patched, please run "npm install" to ensure @next/swc dependencies are downloaded'
)
} catch (err) {
Log.error(
`Failed to patch lockfile, please try uninstalling and reinstalling next in this workspace`
)
console.error(err)
if (!pkgPrefix) {
// unable to locate the next package so bail
return
}
}

for (const pkg of expectedSwcPkgs) {
if (
(shouldPatchDependencies && !lockfileParsed.dependencies[pkg]) ||
(shouldPatchPackages && !lockfileParsed.packages[`${pkgPrefix}${pkg}`])
) {
missingSwcPkgs.push(pkg)
}
}
if (missingSwcPkgs.length === 0) {
return
}
Log.warn(
`Found lockfile missing swc dependencies,`,
isCI ? 'run next locally to automatically patch' : 'patching...'
)

if (isCI) {
// no point in updating in CI as the user can't save the patch
return
}
const pkgsData = await Promise.all(
missingSwcPkgs.map((pkg) => fetchPkgInfo(pkg))
)

for (let i = 0; i < pkgsData.length; i++) {
const pkg = missingSwcPkgs[i]
const pkgData = pkgsData[i]

if (shouldPatchDependencies) {
patchDependency(pkg, pkgData)
}
if (shouldPatchPackages) {
patchPackage(`${pkgPrefix}${pkg}`, pkgData)
}
}

await promises.writeFile(
lockfilePath,
JSON.stringify(lockfileParsed, null, 2) + endingNewline
)
Log.warn(
'Lockfile was successfully patched, please run "npm install" to ensure @next/swc dependencies are downloaded'
)
} catch (err) {
Log.error(
`Failed to patch lockfile, please try uninstalling and reinstalling next in this workspace`
)
console.error(err)
}
}
let registry: string | undefined

async function fetchPkgInfo(pkg: string) {
if (!registry) {
try {
const output = execSync('npm config get registry').toString().trim()
if (output.startsWith('http')) {
registry = output

if (!registry.endsWith('/')) {
registry += '/'
}
}
} catch (_) {
registry = `https://registry.npmjs.org/`
}
}
const res = await fetch(`${registry}${pkg}`)

if (!res.ok) {
throw new Error(
`Failed to fetch registry info for ${pkg}, got status ${res.status}`
)
}
const data = await res.json()
const versionData = data.versions[nextPkgJson.version]

return {
os: versionData.os,
cpu: versionData.cpu,
engines: versionData.engines,
tarball: versionData.dist.tarball,
integrity: versionData.dist.integrity,
}
}