Skip to content

Commit

Permalink
Update lockfile patching for different versions (#36959)
Browse files Browse the repository at this point in the history
This ensures different lockfile versions are handled and we skip patching when the version isn't supported. This also adds an env variable to allow skipping this check if desired. 

## Bug

- [x] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

Closes: #36816
  • Loading branch information
ijjk authored May 17, 2022
1 parent 16bcb07 commit c947abb
Showing 1 changed file with 154 additions and 68 deletions.
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,
}
}

0 comments on commit c947abb

Please sign in to comment.