Skip to content

Commit 05f062c

Browse files
committed
feat(install): add --restore flag to rebuild the lockfile from component graphs
bit install does not normally use the per-component dependency graphs stored on each Version object. If a user deletes pnpm-lock.yaml and runs bit install, pnpm re-resolves every dep from the manifest specifiers and drifts to whatever the registry now considers latest — losing the tag-time version pins that the graph feature is supposed to guarantee. --restore opts into the same code path bit import uses: fetch the dep graph for every component in the bitmap, merge them, and hand the merged graph to the package manager, which then seeds pnpm-lock.yaml from it. If no workspace component has a stored graph, we fall back to a regular install with a warning. Adds e2e coverage that deletes the lockfile + node_modules, bumps the registry so fresh resolve would drift, runs bit install --restore, and asserts the components stay locked to their graph versions.
1 parent a56c80e commit 05f062c

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

e2e/harmony/deps-graph.e2e.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,4 +543,68 @@ chai.use(chaiFs);
543543
expect(lockfile.packages).to.have.property('@pnpm.e2e/bar@100.0.0');
544544
});
545545
});
546+
// `bit install --restore` seeds the lockfile from the dependency graphs stored on
547+
// every bitmap entry, the same way `bit import` does for the components it writes.
548+
// This lets a user recover from a deleted pnpm-lock.yaml without re-resolving from
549+
// manifest specifiers (which would drift to whatever the registry considers latest).
550+
describe('bit install --restore rebuilds the lockfile from workspace component graphs', function () {
551+
let randomStr: string;
552+
let lockfileAfterRestore: any;
553+
before(async () => {
554+
randomStr = generateRandomStr(4);
555+
const name = `@ci/${randomStr}.{name}`;
556+
helper.scopeHelper.setWorkspaceWithRemoteScope();
557+
npmCiRegistry = new NpmCiRegistry(helper);
558+
npmCiRegistry.configureCustomNameInPackageJsonHarmony(name);
559+
await npmCiRegistry.init();
560+
helper.command.setConfig('registry', npmCiRegistry.getRegistryUrl());
561+
helper.env.setCustomNewEnv(
562+
undefined,
563+
undefined,
564+
{ policy: { peers: [] } },
565+
false,
566+
'custom-env/env',
567+
'custom-env/env'
568+
);
569+
helper.fs.createFile('comp1', 'comp1.js', 'require("@pnpm.e2e/foo"); // eslint-disable-line');
570+
helper.command.addComponent('comp1');
571+
helper.extensions.addExtensionToVariant('comp1', `${helper.scopes.remote}/custom-env/env`, {});
572+
helper.fs.createFile('comp2', 'comp2.js', 'require("@pnpm.e2e/bar"); // eslint-disable-line');
573+
helper.command.addComponent('comp2');
574+
helper.extensions.addExtensionToVariant('comp2', `${helper.scopes.remote}/custom-env/env`, {});
575+
helper.extensions.workspaceJsonc.addKeyValToDependencyResolver('rootComponents', true);
576+
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.0.0', distTag: 'latest' });
577+
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.0.0', distTag: 'latest' });
578+
helper.command.install('--add-missing-deps');
579+
helper.command.tagAllComponents('--skip-tests');
580+
helper.command.export();
581+
582+
helper.scopeHelper.reInitWorkspace();
583+
helper.scopeHelper.addRemoteScope();
584+
helper.extensions.workspaceJsonc.addKeyValToDependencyResolver('rootComponents', true);
585+
helper.command.import(`${helper.scopes.remote}/comp1@latest ${helper.scopes.remote}/comp2@latest`);
586+
587+
// bump registry and blow away the lockfile + node_modules, then restore from graphs.
588+
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' });
589+
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' });
590+
helper.fs.deletePath('pnpm-lock.yaml');
591+
helper.fs.deletePath('node_modules');
592+
helper.command.runCmd('bit install --restore');
593+
lockfileAfterRestore = yaml.load(fs.readFileSync(path.join(helper.scopes.localPath, 'pnpm-lock.yaml'), 'utf8'));
594+
});
595+
after(() => {
596+
npmCiRegistry.destroy();
597+
helper.command.delConfig('registry');
598+
helper.scopeHelper.destroy();
599+
});
600+
it('should mark the regenerated lockfile as restoredFromModel', () => {
601+
expect(lockfileAfterRestore.bit.restoredFromModel).to.eq(true);
602+
});
603+
it('should keep both components locked to the versions stored in their graphs', () => {
604+
expect(lockfileAfterRestore.packages).to.have.property('@pnpm.e2e/foo@100.0.0');
605+
expect(lockfileAfterRestore.packages).to.have.property('@pnpm.e2e/bar@100.0.0');
606+
expect(lockfileAfterRestore.packages).to.not.have.property('@pnpm.e2e/foo@100.1.0');
607+
expect(lockfileAfterRestore.packages).to.not.have.property('@pnpm.e2e/bar@100.1.0');
608+
});
609+
});
546610
});

scopes/workspace/install/install.cmd.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type InstallCmdOptions = {
2222
noOptional: boolean;
2323
recurringInstall: boolean;
2424
lockfileOnly: boolean;
25+
restore: boolean;
2526
allowScripts?: string;
2627
disallowScripts?: string;
2728
};
@@ -69,6 +70,11 @@ automatically imports components, compiles components, links to node_modules, an
6970
],
7071
['', 'no-optional [noOptional]', 'do not install optional dependencies (works with pnpm only)'],
7172
['', 'lockfile-only', 'dependencies are not written to node_modules. Only the lockfile is updated'],
73+
[
74+
'',
75+
'restore',
76+
'reconstruct the lockfile from each workspace component\'s stored dependency graph before installing',
77+
],
7278
['', 'allow-scripts [pkgNames]', 'a comma separated list of package names that are allowed to run installation scripts'],
7379
['', 'disallow-scripts [pkgNames]', 'a comma separated list of package names that are NOT allowed to run installation scripts'],
7480
] as CommandOptions;
@@ -134,6 +140,7 @@ automatically imports components, compiles components, links to node_modules, an
134140
updateAll: options.update,
135141
recurringInstall: options.recurringInstall,
136142
lockfileOnly: options.lockfileOnly,
143+
restoreFromDependenciesGraph: options.restore,
137144
showExternalPackageManagerPrompt: true,
138145
allowScripts: this._parseAllowScriptsFlags(options.allowScripts, options.disallowScripts),
139146
};

scopes/workspace/install/install.main.runtime.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ export type WorkspaceInstallOptions = {
108108
writeConfigFiles?: boolean;
109109
skipPrune?: boolean;
110110
dependenciesGraph?: DependenciesGraph;
111+
/**
112+
* When true, attempt to reconstruct the lockfile from each workspace component's
113+
* stored dependency graph before running the package manager. Graphs are fetched for
114+
* every component listed in the bitmap, merged, and handed to the package manager the
115+
* same way `bit import` does for the components it writes.
116+
*/
117+
restoreFromDependenciesGraph?: boolean;
111118
allowScripts?: Record<string, boolean>;
112119
};
113120

@@ -392,11 +399,12 @@ export class InstallMain {
392399
}
393400
);
394401

402+
const dependenciesGraph = await this.resolveDependenciesGraph(options);
395403
const pmInstallOptions: PackageManagerInstallOptions = {
396404
...calcManifestsOpts,
397405
autoInstallPeers: this.dependencyResolver.config.autoInstallPeers,
398406
dedupePeers: this.dependencyResolver.config.dedupePeers,
399-
dependenciesGraph: options?.dependenciesGraph,
407+
dependenciesGraph,
400408
includeOptionalDeps: options?.includeOptionalDeps,
401409
neverBuiltDependencies: this.dependencyResolver.config.neverBuiltDependencies,
402410
allowScripts: this.dependencyResolver.getAllowedScripts(),
@@ -516,6 +524,22 @@ export class InstallMain {
516524
return nonLoadedEnvs.length > 0;
517525
}
518526

527+
private async resolveDependenciesGraph(
528+
options?: ModulesInstallOptions
529+
): Promise<DependenciesGraph | undefined> {
530+
if (options?.dependenciesGraph) return options.dependenciesGraph;
531+
if (!options?.restoreFromDependenciesGraph) return undefined;
532+
const graph = await this.workspace.scope.getDependenciesGraphByComponentIds(this.workspace.listIds());
533+
if (!graph) {
534+
this.logger.console(
535+
chalk.yellow(
536+
'--restore was requested but no workspace component has a stored dependency graph. Falling back to a regular install.'
537+
)
538+
);
539+
}
540+
return graph;
541+
}
542+
519543
/**
520544
* This function is very important to fix some issues that might happen during the installation process.
521545
* The case is the following:

0 commit comments

Comments
 (0)