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