diff --git a/.changeset/fuzzy-adults-suffer.md b/.changeset/fuzzy-adults-suffer.md new file mode 100644 index 0000000000..a9093f17e9 --- /dev/null +++ b/.changeset/fuzzy-adults-suffer.md @@ -0,0 +1,5 @@ +--- +"@pnpm/package-requester": patch +--- + +Always fetch the bundled manifest. diff --git a/.changeset/tender-grapes-leave.md b/.changeset/tender-grapes-leave.md new file mode 100644 index 0000000000..f924815141 --- /dev/null +++ b/.changeset/tender-grapes-leave.md @@ -0,0 +1,6 @@ +--- +"@pnpm/plugin-commands-installation": minor +"supi": patch +--- + +Adding --fix-lockfile for the install command to support autofix broken lockfile diff --git a/.changeset/three-knives-flow.md b/.changeset/three-knives-flow.md new file mode 100644 index 0000000000..89f23073c7 --- /dev/null +++ b/.changeset/three-knives-flow.md @@ -0,0 +1,5 @@ +--- +"@pnpm/resolve-dependencies": patch +--- + +`requiresBuild` fields should be updated when a full resolution is forced. diff --git a/packages/package-requester/src/packageRequester.ts b/packages/package-requester/src/packageRequester.ts index eb9f98ce5b..c20be06f6f 100644 --- a/packages/package-requester/src/packageRequester.ts +++ b/packages/package-requester/src/packageRequester.ts @@ -234,7 +234,7 @@ async function resolveAndFetch ( } const fetchResult = ctx.fetchPackageToStore({ - fetchRawManifest: updated || (manifest == null), + fetchRawManifest: true, force: forceFetch, lockfileDir: options.lockfileDir, pkg: { diff --git a/packages/plugin-commands-installation/src/install.ts b/packages/plugin-commands-installation/src/install.ts index 17d3122417..5a86464ae7 100644 --- a/packages/plugin-commands-installation/src/install.ts +++ b/packages/plugin-commands-installation/src/install.ts @@ -67,6 +67,7 @@ export function rcOptionsTypes () { export const cliOptionsTypes = () => ({ ...rcOptionsTypes(), ...pick(['force'], allTypes), + 'fix-lockfile': Boolean, recursive: Boolean, }) @@ -131,6 +132,10 @@ For options that may be used with `-r`, see "pnpm help recursive"', description: `The directory in which the ${WANTED_LOCKFILE} of the package will be created. Several projects may share a single lockfile.`, name: '--lockfile-dir ', }, + { + description: 'Fix broken lockfile entries automatically', + name: '--fix-lockfile', + }, { description: 'The directory in which dependencies will be installed (instead of node_modules)', name: '--modules-dir ', @@ -286,6 +291,7 @@ export type InstallCommandOptions = Pick Promise.all(Object.values(resolvedPackagesByDepPath).map(async ({ finishing }) => finishing?.())) diff --git a/packages/resolve-dependencies/src/resolveDependencies.ts b/packages/resolve-dependencies/src/resolveDependencies.ts index c7bfc30b92..107277eb22 100644 --- a/packages/resolve-dependencies/src/resolveDependencies.ts +++ b/packages/resolve-dependencies/src/resolveDependencies.ts @@ -526,7 +526,9 @@ function getInfoFromLockfile ( dependencyLockfile, depPath, pkgId: packageIdFromSnapshot(depPath, dependencyLockfile, registries), - resolution: pkgSnapshotToResolution(depPath, dependencyLockfile, registries), + // resolution may not exist if lockfile is broken, and an unexpected error will be thrown + // if resolution does not exist, return undefined so it can be autofixed later + resolution: dependencyLockfile.resolution && pkgSnapshotToResolution(depPath, dependencyLockfile, registries), } } else { return { diff --git a/packages/supi/src/install/extendInstallOptions.ts b/packages/supi/src/install/extendInstallOptions.ts index c90a5722db..beda2fb141 100644 --- a/packages/supi/src/install/extendInstallOptions.ts +++ b/packages/supi/src/install/extendInstallOptions.ts @@ -21,6 +21,7 @@ export interface StrictInstallOptions { useLockfile: boolean linkWorkspacePackagesDepth: number lockfileOnly: boolean + fixLockfile: boolean ignorePackageManifest: boolean preferFrozenLockfile: boolean saveWorkspaceProtocol: boolean diff --git a/packages/supi/src/install/index.ts b/packages/supi/src/install/index.ts index 93b16a320d..2ce8c66491 100644 --- a/packages/supi/src/install/index.ts +++ b/packages/supi/src/install/index.ts @@ -194,7 +194,8 @@ export async function mutateModules ( let needsFullResolution = !maybeOpts.ignorePackageManifest && ( !equals(ctx.wantedLockfile.overrides ?? {}, overrides ?? {}) || !equals((ctx.wantedLockfile.neverBuiltDependencies ?? []).sort(), (neverBuiltDependencies ?? []).sort()) || - ctx.wantedLockfile.packageExtensionsChecksum !== packageExtensionsChecksum) + ctx.wantedLockfile.packageExtensionsChecksum !== packageExtensionsChecksum) || + opts.fixLockfile if (needsFullResolution) { ctx.wantedLockfile.overrides = overrides ctx.wantedLockfile.neverBuiltDependencies = neverBuiltDependencies @@ -206,6 +207,7 @@ export async function mutateModules ( !ctx.lockfileHadConflicts && !opts.lockfileOnly && !opts.update && + !opts.fixLockfile && installsOnly && ( frozenLockfile || @@ -725,6 +727,23 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { workspacePackages: opts.workspacePackages, }) const projectsToResolve = await Promise.all(projects.map(async (project) => _toResolveImporter(project))) + + // Ignore some field when fixing lockfile, so this filed can be regenereated + // and make sure it's up-to-date + if ( + opts.fixLockfile && + (ctx.wantedLockfile.packages != null) && + !isEmpty(ctx.wantedLockfile.packages) + ) { + ctx.wantedLockfile.packages = Object.entries(ctx.wantedLockfile.packages).reduce((pre, [depPath, snapshot]) => ({ + ...pre, + [depPath]: { + name: snapshot.name, + version: snapshot.version, + }, + }), {}) + } + let { dependenciesGraph, dependenciesByProjectId, diff --git a/packages/supi/test/install/fixLockfile.ts b/packages/supi/test/install/fixLockfile.ts new file mode 100644 index 0000000000..bfafc16af0 --- /dev/null +++ b/packages/supi/test/install/fixLockfile.ts @@ -0,0 +1,61 @@ +import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants' +import { prepareEmpty } from '@pnpm/prepare' +import { install } from 'supi' +import writeYamlFile from 'write-yaml-file' +import readYamlFile from 'read-yaml-file' +import { Lockfile, PackageSnapshots } from '@pnpm/lockfile-file' +import { testDefaults } from '../utils' + +test('fix broken lockfile with --fix-lockfile', async () => { + prepareEmpty() + + await writeYamlFile(WANTED_LOCKFILE, { + dependencies: { + '@types/semver': '5.3.31', + }, + devDependencies: { + fsevents: '2.3.2', + }, + lockfileVersion: LOCKFILE_VERSION, + packages: { + '/@types/semver/5.3.31': { + // resolution: { + // integrity: 'sha1-uZnX2TX0P1IHsBsA094ghS9Mp18=', + // }, + }, + '/core-js-pure/3.16.2': { + resolution: { + integrity: 'sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==', + }, + // requiresBuild: true, + // dev: true + }, + }, + specifiers: { + '@types/semver': '^5.3.31', + fsevents: '^2.3.2', + }, + }, { lineWidth: 1000 }) + + await install({ + dependencies: { + '@types/semver': '^5.3.31', + }, + devDependencies: { + 'core-js-pure': '^3.16.2', + }, + }, await testDefaults({ fixLockfile: true })) + + const lockfile: Lockfile = await readYamlFile(WANTED_LOCKFILE) + expect(Object.keys(lockfile.packages as PackageSnapshots).length).toBe(2) + expect(lockfile.packages?.['/@types/semver/5.3.31']).toBeTruthy() + expect(lockfile.packages?.['/@types/semver/5.3.31']?.resolution).toEqual({ + integrity: 'sha1-uZnX2TX0P1IHsBsA094ghS9Mp18=', + }) + expect(lockfile.packages?.['/core-js-pure/3.16.2']).toBeTruthy() + expect(lockfile.packages?.['/core-js-pure/3.16.2']?.resolution).toEqual({ + integrity: 'sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==', + }) + expect(lockfile.packages?.['/core-js-pure/3.16.2']?.requiresBuild).toBeTruthy() + expect(lockfile.packages?.['/core-js-pure/3.16.2']?.dev).toBeTruthy() +}) \ No newline at end of file