rsnext/packages/next/lib/patch-incorrect-lockfile.ts
Tim Neutkens 4cd8b23032
Enable @typescript-eslint/no-use-before-define for functions (#39602)
Follow-up to the earlier enabling of classes/variables etc.

Bug

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

Feature

 Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
 Related issues linked using fixes #number
 Integration tests added
 Documentation added
 Telemetry added. In case of a feature if it's used or not.
 Errors have helpful link attached, see contributing.md

Documentation / Examples

 Make sure the linting passes by running pnpm lint
 The examples guidelines are followed from our contributing doc

Co-authored-by: Steven <steven@ceriously.com>
2022-08-15 10:29:51 -04:00

189 lines
5.1 KiB
TypeScript

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'
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,
}
}
/**
* Attempts to patch npm package-lock.json when it
* fails to include optionalDependencies for other platforms
* this can occur when the package-lock is rebuilt from a current
* 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 patchDependency = (
pkg: string,
pkgData: UnwrapPromise<ReturnType<typeof fetchPkgInfo>>
) => {
lockfileParsed.dependencies[pkg] = {
version: nextPkgJson.version,
resolved: pkgData.tarball,
integrity: pkgData.integrity,
optional: true,
}
}
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,
}
}
try {
const supportedVersions = [1, 2, 3]
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 (
(shouldPatchDependencies && !lockfileParsed.dependencies) ||
(shouldPatchPackages && !lockfileParsed.packages)
) {
// invalid lockfile so bail
return
}
const missingSwcPkgs = []
let pkgPrefix: string | undefined
if (shouldPatchPackages) {
pkgPrefix = ''
for (const pkg of Object.keys(lockfileParsed.packages)) {
if (pkg.endsWith('node_modules/next')) {
pkgPrefix = pkg.substring(0, pkg.length - 4)
}
}
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)
}
}