diff --git a/.gitignore b/.gitignore index 83d4faa21795..804c8e216ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ package.tgz # This is the Yarn build state; it's local to each clone /.yarn/build-state.yml +# This is the Yarn install state cache, it can be rebuilt anytime +/.yarn/install-state.gz + # Those files are meant to be local-only /packages/*/bundles diff --git a/.pnp.js b/.pnp.js index 676d33f2ba1d..ad2675e44cd7 100755 --- a/.pnp.js +++ b/.pnp.js @@ -5822,6 +5822,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/got", "npm:8.3.5"], ["@types/is-ci", "npm:2.0.0"], ["@types/micromatch", "npm:3.1.0"], + ["@types/node", "npm:13.7.0"], ["@types/semver", "npm:7.1.0"], ["@types/tar", "npm:4.0.0"], ["@types/tmp", "npm:0.0.33"], diff --git a/.yarn/versions/1e50b08a.yml b/.yarn/versions/1e50b08a.yml new file mode 100644 index 000000000000..2ff67a0bd2cd --- /dev/null +++ b/.yarn/versions/1e50b08a.yml @@ -0,0 +1,29 @@ +releases: + "@yarnpkg/cli": prerelease + "@yarnpkg/core": prerelease + "@yarnpkg/plugin-dlx": prerelease + "@yarnpkg/plugin-essentials": prerelease + "@yarnpkg/plugin-npm-cli": prerelease + "@yarnpkg/plugin-pack": prerelease + "@yarnpkg/plugin-patch": prerelease + "@yarnpkg/plugin-version": prerelease + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-exec" + - "@yarnpkg/plugin-file" + - "@yarnpkg/plugin-git" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-http" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-link" + - "@yarnpkg/plugin-node-modules" + - "@yarnpkg/plugin-npm" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/doctor" diff --git a/packages/gatsby/content/advanced/questions-and-answers.md b/packages/gatsby/content/advanced/questions-and-answers.md index f972f47df1d1..48248f56d6d5 100644 --- a/packages/gatsby/content/advanced/questions-and-answers.md +++ b/packages/gatsby/content/advanced/questions-and-answers.md @@ -49,16 +49,18 @@ Lockfiles should **always** be kept within the repository. Continuous integratio If you're using Zero-Installs: ```gitignore -.yarn/unplugged -.yarn/build-state.yml +.yarn/* +!.yarn/cache +!.yarn/releases +!.yarn/plugins ``` If you're not using Zero-Installs: ```gitignore -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml +.yarn/* +!.yarn/releases +!.yarn/plugins .pnp.* ``` diff --git a/packages/plugin-dlx/sources/commands/dlx.ts b/packages/plugin-dlx/sources/commands/dlx.ts index 18c6c9536bc5..83b73d351fe0 100644 --- a/packages/plugin-dlx/sources/commands/dlx.ts +++ b/packages/plugin-dlx/sources/commands/dlx.ts @@ -1,5 +1,5 @@ import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; -import {Configuration, Project, ThrowReport} from '@yarnpkg/core'; +import {Configuration, Project} from '@yarnpkg/core'; import {scriptUtils, structUtils} from '@yarnpkg/core'; import {Filename, PortablePath, npath, ppath, toFilename, xfs} from '@yarnpkg/fslib'; import {Command, Usage} from 'clipanion'; @@ -64,10 +64,7 @@ export default class DlxCommand extends BaseCommand { if (workspace === null) throw new WorkspaceRequiredError(project.cwd, tmpDir); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); return await scriptUtils.executeWorkspaceAccessibleBinary(workspace, command, this.args, { cwd: this.context.cwd, diff --git a/packages/plugin-essentials/sources/commands/bin.ts b/packages/plugin-essentials/sources/commands/bin.ts index b4f1c0d42faa..29637f089edc 100644 --- a/packages/plugin-essentials/sources/commands/bin.ts +++ b/packages/plugin-essentials/sources/commands/bin.ts @@ -1,7 +1,7 @@ -import {BaseCommand} from '@yarnpkg/cli'; -import {Configuration, Project, ThrowReport, StreamReport} from '@yarnpkg/core'; -import {scriptUtils, structUtils} from '@yarnpkg/core'; -import {Command, Usage, UsageError} from 'clipanion'; +import {BaseCommand} from '@yarnpkg/cli'; +import {Configuration, Project, StreamReport} from '@yarnpkg/core'; +import {scriptUtils, structUtils} from '@yarnpkg/core'; +import {Command, Usage, UsageError} from 'clipanion'; // eslint-disable-next-line arca/no-default-export export default class BinCommand extends BaseCommand { @@ -37,10 +37,7 @@ export default class BinCommand extends BaseCommand { const configuration = await Configuration.find(this.context.cwd, this.context.plugins); const {project, locator} = await Project.find(configuration, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); if (this.name) { const binaries = await scriptUtils.getPackageAccessibleBinaries(locator, {project}); diff --git a/packages/plugin-essentials/sources/commands/run.ts b/packages/plugin-essentials/sources/commands/run.ts index 019c7aae6e22..1a9a9175189c 100644 --- a/packages/plugin-essentials/sources/commands/run.ts +++ b/packages/plugin-essentials/sources/commands/run.ts @@ -1,9 +1,9 @@ -import {BaseCommand} from '@yarnpkg/cli'; -import {Configuration, Project, Workspace, ThrowReport} from '@yarnpkg/core'; -import {scriptUtils, structUtils} from '@yarnpkg/core'; -import {Command, Usage, UsageError} from 'clipanion'; +import {BaseCommand} from '@yarnpkg/cli'; +import {Configuration, Project, Workspace} from '@yarnpkg/core'; +import {scriptUtils, structUtils} from '@yarnpkg/core'; +import {Command, Usage, UsageError} from 'clipanion'; -import {pluginCommands} from '../pluginCommands'; +import {pluginCommands} from '../pluginCommands'; // eslint-disable-next-line arca/no-default-export export default class RunCommand extends BaseCommand { @@ -67,10 +67,7 @@ export default class RunCommand extends BaseCommand { const configuration = await Configuration.find(this.context.cwd, this.context.plugins); const {project, workspace, locator} = await Project.find(configuration, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); const effectiveLocator = this.topLevel ? project.topLevelWorkspace.anchoredLocator diff --git a/packages/plugin-essentials/sources/commands/why.ts b/packages/plugin-essentials/sources/commands/why.ts index efaef70ccd79..3628d4b2c19d 100644 --- a/packages/plugin-essentials/sources/commands/why.ts +++ b/packages/plugin-essentials/sources/commands/why.ts @@ -1,10 +1,10 @@ -import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; -import {Configuration, LocatorHash, Package, ThrowReport} from '@yarnpkg/core'; -import {IdentHash, Project} from '@yarnpkg/core'; -import {miscUtils, structUtils} from '@yarnpkg/core'; -import {Command, Usage} from 'clipanion'; -import {Writable} from 'stream'; -import {asTree} from 'treeify'; +import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; +import {Configuration, LocatorHash, Package} from '@yarnpkg/core'; +import {IdentHash, Project} from '@yarnpkg/core'; +import {miscUtils, structUtils} from '@yarnpkg/core'; +import {Command, Usage} from 'clipanion'; +import {Writable} from 'stream'; +import {asTree} from 'treeify'; type TreeNode = {[key: string]: TreeNode}; @@ -42,10 +42,7 @@ export default class WhyCommand extends BaseCommand { if (!workspace) throw new WorkspaceRequiredError(project.cwd, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); const identHash = structUtils.parseIdent(this.package).identHash; diff --git a/packages/plugin-npm-cli/sources/commands/npm/publish.ts b/packages/plugin-npm-cli/sources/commands/npm/publish.ts index b36ce7c255f8..3a255377be5b 100644 --- a/packages/plugin-npm-cli/sources/commands/npm/publish.ts +++ b/packages/plugin-npm-cli/sources/commands/npm/publish.ts @@ -1,11 +1,11 @@ -import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; -import {Configuration, MessageName, Project, ReportError, StreamReport, Workspace, ThrowReport} from '@yarnpkg/core'; -import {miscUtils, structUtils} from '@yarnpkg/core'; -import {npmConfigUtils, npmHttpUtils} from '@yarnpkg/plugin-npm'; -import {packUtils} from '@yarnpkg/plugin-pack'; -import {Command, Usage, UsageError} from 'clipanion'; -import {createHash} from 'crypto'; -import ssri from 'ssri'; +import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; +import {Configuration, MessageName, Project, ReportError, StreamReport, Workspace} from '@yarnpkg/core'; +import {miscUtils, structUtils} from '@yarnpkg/core'; +import {npmConfigUtils, npmHttpUtils} from '@yarnpkg/plugin-npm'; +import {packUtils} from '@yarnpkg/plugin-pack'; +import {Command, Usage, UsageError} from 'clipanion'; +import {createHash} from 'crypto'; +import ssri from 'ssri'; // eslint-disable-next-line arca/no-default-export export default class NpmPublishCommand extends BaseCommand { @@ -47,10 +47,7 @@ export default class NpmPublishCommand extends BaseCommand { if (workspace.manifest.name === null || workspace.manifest.version === null) throw new UsageError(`Workspaces must have valid names and versions to be published on an external registry`); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); // We store it so that TS knows that it's non-null const ident = workspace.manifest.name; diff --git a/packages/plugin-pack/sources/commands/pack.ts b/packages/plugin-pack/sources/commands/pack.ts index c5878ac9987d..daaaacfbab7d 100644 --- a/packages/plugin-pack/sources/commands/pack.ts +++ b/packages/plugin-pack/sources/commands/pack.ts @@ -60,10 +60,7 @@ export default class PackCommand extends BaseCommand { report: new ThrowReport(), }); } else { - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); } } diff --git a/packages/plugin-patch/sources/commands/patch.ts b/packages/plugin-patch/sources/commands/patch.ts index fc42eb92a26b..5c3069500180 100644 --- a/packages/plugin-patch/sources/commands/patch.ts +++ b/packages/plugin-patch/sources/commands/patch.ts @@ -1,9 +1,9 @@ -import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; -import {Cache, Configuration, Project, ThrowReport, structUtils, StreamReport, MessageName} from '@yarnpkg/core'; -import {npath} from '@yarnpkg/fslib'; -import {Command, Usage, UsageError} from 'clipanion'; +import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; +import {Cache, Configuration, Project, structUtils, StreamReport, MessageName} from '@yarnpkg/core'; +import {npath} from '@yarnpkg/fslib'; +import {Command, Usage, UsageError} from 'clipanion'; -import * as patchUtils from '../patchUtils'; +import * as patchUtils from '../patchUtils'; // eslint-disable-next-line arca/no-default-export export default class PatchCommand extends BaseCommand { @@ -25,10 +25,7 @@ export default class PatchCommand extends BaseCommand { if (!workspace) throw new WorkspaceRequiredError(project.cwd, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); let locator = structUtils.parseLocator(this.package); diff --git a/packages/plugin-patch/sources/commands/patchCommit.ts b/packages/plugin-patch/sources/commands/patchCommit.ts index 4354c229789a..3d417b8f601b 100644 --- a/packages/plugin-patch/sources/commands/patchCommit.ts +++ b/packages/plugin-patch/sources/commands/patchCommit.ts @@ -1,9 +1,9 @@ -import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; -import {Cache, Configuration, Project, ThrowReport, structUtils, execUtils, miscUtils} from '@yarnpkg/core'; -import {npath, xfs, Filename, ppath} from '@yarnpkg/fslib'; -import {Command, Usage, UsageError} from 'clipanion'; +import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli'; +import {Cache, Configuration, Project, structUtils, execUtils, miscUtils} from '@yarnpkg/core'; +import {npath, xfs, Filename, ppath} from '@yarnpkg/fslib'; +import {Command, Usage, UsageError} from 'clipanion'; -import * as patchUtils from '../patchUtils'; +import * as patchUtils from '../patchUtils'; // eslint-disable-next-line arca/no-default-export export default class PatchCommitCommand extends BaseCommand { @@ -27,10 +27,7 @@ export default class PatchCommitCommand extends BaseCommand { if (!workspace) throw new WorkspaceRequiredError(project.cwd, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); const folderPath = ppath.resolve(this.context.cwd, npath.toPortablePath(this.patchFolder)); const metaPath = ppath.join(folderPath, `.yarn-patch.json` as Filename); diff --git a/packages/plugin-version/sources/commands/version/check.tsx b/packages/plugin-version/sources/commands/version/check.tsx index 14eff6e12721..42720c0690a8 100644 --- a/packages/plugin-version/sources/commands/version/check.tsx +++ b/packages/plugin-version/sources/commands/version/check.tsx @@ -1,16 +1,16 @@ -import {WorkspaceRequiredError} from '@yarnpkg/cli'; -import {CommandContext, Configuration, MessageName, Project, StreamReport, Workspace, structUtils, ThrowReport} from '@yarnpkg/core'; -import {ppath} from '@yarnpkg/fslib'; -import {ScrollableItems} from '@yarnpkg/libui/sources/components/ScrollableItems'; -import {FocusRequest} from '@yarnpkg/libui/sources/hooks/useFocusRequest'; -import {useListInput} from '@yarnpkg/libui/sources/hooks/useListInput'; -import {renderForm} from '@yarnpkg/libui/sources/misc/renderForm'; -import {Command, Usage, UsageError} from 'clipanion'; -import {Box, Color} from 'ink'; -import React, {useCallback, useState} from 'react'; -import semver from 'semver'; - -import * as versionUtils from '../../versionUtils'; +import {WorkspaceRequiredError} from '@yarnpkg/cli'; +import {CommandContext, Configuration, MessageName, Project, StreamReport, Workspace, structUtils} from '@yarnpkg/core'; +import {ppath} from '@yarnpkg/fslib'; +import {ScrollableItems} from '@yarnpkg/libui/sources/components/ScrollableItems'; +import {FocusRequest} from '@yarnpkg/libui/sources/hooks/useFocusRequest'; +import {useListInput} from '@yarnpkg/libui/sources/hooks/useListInput'; +import {renderForm} from '@yarnpkg/libui/sources/misc/renderForm'; +import {Command, Usage, UsageError} from 'clipanion'; +import {Box, Color} from 'ink'; +import React, {useCallback, useState} from 'react'; +import semver from 'semver'; + +import * as versionUtils from '../../versionUtils'; type Releases = Map>; @@ -53,10 +53,7 @@ export default class VersionApplyCommand extends Command { if (!workspace) throw new WorkspaceRequiredError(project.cwd, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); const versionFile = await versionUtils.openVersionFile(project); if (versionFile === null || versionFile.releaseRoots.size === 0) @@ -273,10 +270,7 @@ export default class VersionApplyCommand extends Command { if (!workspace) throw new WorkspaceRequiredError(project.cwd, this.context.cwd); - await project.resolveEverything({ - lockfileOnly: true, - report: new ThrowReport(), - }); + await project.restoreInstallState(); const report = await StreamReport.start({ configuration, diff --git a/packages/yarnpkg-core/package.json b/packages/yarnpkg-core/package.json index b7b5bcbb196a..a93fd724246a 100644 --- a/packages/yarnpkg-core/package.json +++ b/packages/yarnpkg-core/package.json @@ -37,6 +37,7 @@ "@types/got": "^8.3.4", "@types/is-ci": "^2.0.0", "@types/micromatch": "^3.1.0", + "@types/node": "^13.7.0", "@types/semver": "^7.1.0", "@types/tar": "^4.0.0", "@types/tmp": "^0.0.33", diff --git a/packages/yarnpkg-core/sources/Configuration.ts b/packages/yarnpkg-core/sources/Configuration.ts index fdbca9a2b976..1922bc606f21 100644 --- a/packages/yarnpkg-core/sources/Configuration.ts +++ b/packages/yarnpkg-core/sources/Configuration.ts @@ -195,6 +195,11 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} = type: SettingsType.STRING, default: DEFAULT_LOCK_FILENAME, }, + installStatePath: { + description: `Path of the file where the install state will be persisted`, + type: SettingsType.ABSOLUTE_PATH, + default: `./.yarn/install-state.gz`, + }, rcFilename: { description: `Name of the files where the configuration can be found`, type: SettingsType.STRING, diff --git a/packages/yarnpkg-core/sources/Project.ts b/packages/yarnpkg-core/sources/Project.ts index 40de5ce460c7..a71af3affda7 100644 --- a/packages/yarnpkg-core/sources/Project.ts +++ b/packages/yarnpkg-core/sources/Project.ts @@ -8,6 +8,9 @@ import Logic from import pLimit from 'p-limit'; import semver from 'semver'; import {tmpNameSync} from 'tmp'; +import {promisify} from 'util'; +import v8 from 'v8'; +import zlib from 'zlib'; import {Cache} from './Cache'; import {Configuration, FormatType} from './Configuration'; @@ -39,8 +42,16 @@ import {LinkType} from // the Package type; no more no less. const LOCKFILE_VERSION = 4; +// Same thing but must be bumped when the members of the Project class changes (we +// don't recommend our users to check-in this file, so it's fine to bump it even +// between patch or minor releases). +const INSTALL_STATE_VERSION = 1; + const MULTIPLE_KEYS_REGEXP = / *, */g; +const gzip = promisify(zlib.gzip); +const gunzip = promisify(zlib.gunzip); + export type InstallOptions = { cache: Cache, fetcher?: Fetcher, @@ -80,6 +91,8 @@ export class Project { public originalPackages: Map = new Map(); public optionalBuilds: Set = new Set(); + public lockFileChecksum: string | null = null; + static async find(configuration: Configuration, startingCwd: PortablePath): Promise<{project: Project, workspace: Workspace | null, locator: Locator}> { if (!configuration.projectCwd) throw new UsageError(`No project found in ${startingCwd}`); @@ -152,11 +165,17 @@ export class Project { this.storedDescriptors = new Map(); this.storedPackages = new Map(); + this.lockFileChecksum = null; + const lockfilePath = ppath.join(this.cwd, this.configuration.get(`lockfileFilename`)); const defaultLanguageName = this.configuration.get(`defaultLanguageName`); if (xfs.existsSync(lockfilePath)) { const content = await xfs.readFilePromise(lockfilePath, `utf8`); + + // We store the salted checksum of the lockfile in order to invalidate the install state when needed + this.lockFileChecksum = hashUtils.makeHash(`${INSTALL_STATE_VERSION}`, content); + const parsed: any = parseSyml(content); // Protects against v1 lockfiles @@ -1487,8 +1506,43 @@ export class Project { }); } + async persistInstallStateFile() { + const {accessibleLocators, optionalBuilds, storedDescriptors, storedResolutions, storedPackages, lockFileChecksum} = this; + const installState = {accessibleLocators, optionalBuilds, storedDescriptors, storedResolutions, storedPackages, lockFileChecksum}; + const serializedState = await gzip(v8.serialize(installState)); + + const installStatePath = this.configuration.get(`installStatePath`); + + await xfs.mkdirpPromise(ppath.dirname(installStatePath)); + await xfs.writeFilePromise(installStatePath, serializedState as Buffer); + } + + async restoreInstallState() { + const installStatePath = this.configuration.get(`installStatePath`); + if (!xfs.existsSync(installStatePath)) + return await this.applyLightResolution(); + + const serializedState = await xfs.readFilePromise(installStatePath); + const installState = v8.deserialize(await gunzip(serializedState) as Buffer); + + if (installState.lockFileChecksum !== this.lockFileChecksum) + return await this.applyLightResolution(); + + Object.assign(this, installState); + } + + async applyLightResolution() { + await this.resolveEverything({ + lockfileOnly: true, + report: new ThrowReport(), + }); + + await this.persistInstallStateFile(); + } + async persist() { await this.persistLockfile(); + await this.persistInstallStateFile(); for (const workspace of this.workspacesByCwd.values()) { await workspace.persistManifest(); @@ -1632,14 +1686,10 @@ function applyVirtualResolutionMutations({ return result; }; - let iterationCount = 0; - const resolvePeerDependenciesImpl = (parentLocator: Locator, first: boolean, optional: boolean) => { if (accessibleLocators.has(parentLocator.locatorHash)) return; - iterationCount += 1; - accessibleLocators.add(parentLocator.locatorHash); if (!optional) diff --git a/packages/yarnpkg-core/tests/Project.test.ts b/packages/yarnpkg-core/tests/Project.test.ts index 0483ccc51eda..2565cc4eead1 100644 --- a/packages/yarnpkg-core/tests/Project.test.ts +++ b/packages/yarnpkg-core/tests/Project.test.ts @@ -1,7 +1,7 @@ -import {Cache, Configuration, Project, ThrowReport, structUtils} from '@yarnpkg/core'; -import {Filename, PortablePath, ppath, xfs} from '@yarnpkg/fslib'; -import LinkPlugin from '@yarnpkg/plugin-link'; -import PnpPlugin from '@yarnpkg/plugin-pnp'; +import {Cache, Configuration, Project, ThrowReport, structUtils, LocatorHash, Package} from '@yarnpkg/core'; +import {Filename, PortablePath, ppath, xfs} from '@yarnpkg/fslib'; +import LinkPlugin from '@yarnpkg/plugin-link'; +import PnpPlugin from '@yarnpkg/plugin-pnp'; const getConfiguration = (p: PortablePath) => { return new Configuration(p, p, new Map([ @@ -66,4 +66,76 @@ describe(`Project`, () => { } }); }); + + it(`should generate the exact same structure with a full resolveEverything as hydrateVirtualPackages`, async () => { + await xfs.mktempPromise(async dir => { + await xfs.mkdirpPromise(ppath.join(dir, `foo` as Filename)); + await xfs.writeFilePromise(ppath.join(dir, `foo` as Filename, `package.json` as Filename), JSON.stringify({ + name: `foo`, + peerDependencies: { + [`bar`]: `*`, + }, + }, null, 2)); + + await xfs.mkdirpPromise(ppath.join(dir, `bar` as PortablePath)); + await xfs.writeFilePromise(ppath.join(dir, `bar` as Filename, `package.json` as Filename), JSON.stringify({ + name: `bar`, + }, null, 2)); + + await xfs.writeFilePromise(ppath.join(dir, `package.json` as Filename), JSON.stringify({ + dependencies: { + [`foo`]: `portal:./foo`, + [`bar`]: `portal:./bar`, + }, + })); + + let project1: Project; + let project2: Project; + + // First we install the project; this will generate the lockfile yada yada + { + const configuration = await getConfiguration(dir); + const {project} = await Project.find(configuration, dir); + const cache = await Cache.find(configuration); + + await project.install({cache, report: new ThrowReport()}); + + project1 = project; + } + + // Then we setup the project except that this time we only call `hydrateVirtualPackages` + { + const configuration = await getConfiguration(dir); + const {project} = await Project.find(configuration, dir); + + await project.restoreInstallState(); + + project2 = project; + } + + // We remove the "version" field from all pkgs, because they contain + // invalid data when calling the resolution through the lockfileOnly + // mode (which is what hydrateVirtualPackage does, in part). + // + // This discrepancy is expected, because we don't want to store the real + // version number inside the lockfile for developer experience purposes + // (otherwise our users would need to run `yarn install` again each time + // they change the version from one of their workspaces / portals). + const clean = (registry: Map) => { + return new Map([...registry.values()].map(pkg => { + return [pkg.locatorHash, {...pkg, version: null}]; + })); + }; + + expect({ + packages: clean(project1.storedPackages), + descriptors: project1.storedDescriptors, + resolutions: project1.storedResolutions, + }).toEqual({ + packages: clean(project2.storedPackages), + descriptors: project2.storedDescriptors, + resolutions: project2.storedResolutions, + }); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 06ed0e4568f1..42a741706690 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4875,6 +4875,7 @@ __metadata: "@types/got": ^8.3.4 "@types/is-ci": ^2.0.0 "@types/micromatch": ^3.1.0 + "@types/node": ^13.7.0 "@types/semver": ^7.1.0 "@types/tar": ^4.0.0 "@types/tmp": ^0.0.33