diff --git a/.changeset/long-spiders-smoke.md b/.changeset/long-spiders-smoke.md new file mode 100644 index 00000000000..e39130bdd98 --- /dev/null +++ b/.changeset/long-spiders-smoke.md @@ -0,0 +1,15 @@ +--- +"@pnpm/resolve-dependencies": minor +"@pnpm/package-requester": minor +"@pnpm/store-controller-types": minor +"@pnpm/lockfile.settings-checker": minor +"@pnpm/resolver-base": minor +"@pnpm/npm-resolver": minor +"@pnpm/core": minor +"@pnpm/lockfile.types": minor +"@pnpm/config": minor +"@pnpm/deps.status": minor +"pnpm": minor +--- + +A new setting, `inject-workspace-packages`, has been added to allow hard-linking all local workspace dependencies instead of symlinking them. Previously, this behavior was achievable via the [`dependenciesMeta[].injected`](https://pnpm.io/package_json#dependenciesmetainjected) setting, which remains supported [#8836](https://github.com/pnpm/pnpm/pull/8836). diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index efac84218f2..b16326c1a4c 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -148,6 +148,7 @@ export interface Config { reporter?: string aggregateOutput: boolean linkWorkspacePackages: boolean | 'deep' + injectWorkspacePackages?: boolean preferWorkspacePackages: boolean reverse: boolean sort: boolean diff --git a/config/config/src/index.ts b/config/config/src/index.ts index 401d55844a8..a6cb38d3f66 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -147,6 +147,7 @@ export async function getConfig (opts: { 'hoist-workspace-packages': true, 'ignore-workspace-cycles': false, 'ignore-workspace-root-check': false, + 'inject-workspace-packages': false, 'link-workspace-packages': false, 'lockfile-include-tarball-url': false, 'manage-package-manager-versions': true, diff --git a/config/config/src/types.ts b/config/config/src/types.ts index f606b2e755b..00e3987e563 100644 --- a/config/config/src/types.ts +++ b/config/config/src/types.ts @@ -42,6 +42,7 @@ export const types = Object.assign({ 'ignore-workspace-cycles': Boolean, 'ignore-workspace-root-check': Boolean, 'include-workspace-root': Boolean, + 'inject-workspace-packages': Boolean, 'legacy-dir-filtering': Boolean, 'link-workspace-packages': [Boolean, 'deep'], lockfile: Boolean, diff --git a/deps/status/src/checkDepsStatus.ts b/deps/status/src/checkDepsStatus.ts index 42cff42f894..c6d5a658324 100644 --- a/deps/status/src/checkDepsStatus.ts +++ b/deps/status/src/checkDepsStatus.ts @@ -46,6 +46,7 @@ export type CheckDepsStatusOptions = Pick Record getWorkspacePackages: () => WorkspacePackages | undefined @@ -359,6 +364,7 @@ async function assertWantedLockfileUpToDate ( const outdatedLockfileSettingName = getOutdatedLockfileSetting(wantedLockfile, { autoInstallPeers: config.autoInstallPeers, + injectWorkspacePackages: config.injectWorkspacePackages, excludeLinksFromLockfile: config.excludeLinksFromLockfile, peersSuffixMaxLength: config.peersSuffixMaxLength, overrides: createOverridesMapFromParsed(parseOverrides(rootManifestOptions?.overrides ?? {}, config.catalogs)), diff --git a/lockfile/fs/src/lockfileFormatConverters.ts b/lockfile/fs/src/lockfileFormatConverters.ts index c5f1bd84b52..a33ac1347b9 100644 --- a/lockfile/fs/src/lockfileFormatConverters.ts +++ b/lockfile/fs/src/lockfileFormatConverters.ts @@ -65,6 +65,9 @@ export function convertToLockfileFile (lockfile: Lockfile, opts: NormalizeLockfi if (newLockfile.settings?.peersSuffixMaxLength === 1000) { newLockfile.settings = omit(['peersSuffixMaxLength'], newLockfile.settings) } + if (newLockfile.settings?.injectWorkspacePackages === false) { + delete newLockfile.settings.injectWorkspacePackages + } return normalizeLockfile(newLockfile, opts) } diff --git a/lockfile/settings-checker/src/getOutdatedLockfileSetting.ts b/lockfile/settings-checker/src/getOutdatedLockfileSetting.ts index ed05cbbe30b..14cb38af235 100644 --- a/lockfile/settings-checker/src/getOutdatedLockfileSetting.ts +++ b/lockfile/settings-checker/src/getOutdatedLockfileSetting.ts @@ -9,6 +9,7 @@ export type ChangedField = | 'settings.autoInstallPeers' | 'settings.excludeLinksFromLockfile' | 'settings.peersSuffixMaxLength' + | 'settings.injectWorkspacePackages' | 'pnpmfileChecksum' export function getOutdatedLockfileSetting ( @@ -22,6 +23,7 @@ export function getOutdatedLockfileSetting ( excludeLinksFromLockfile, peersSuffixMaxLength, pnpmfileChecksum, + injectWorkspacePackages, }: { overrides?: Record packageExtensionsChecksum?: string @@ -31,6 +33,7 @@ export function getOutdatedLockfileSetting ( excludeLinksFromLockfile?: boolean peersSuffixMaxLength?: number pnpmfileChecksum?: string + injectWorkspacePackages?: boolean } ): ChangedField | null { if (!equals(lockfile.overrides ?? {}, overrides ?? {})) { @@ -60,5 +63,8 @@ export function getOutdatedLockfileSetting ( if (lockfile.pnpmfileChecksum !== pnpmfileChecksum) { return 'pnpmfileChecksum' } + if (Boolean(lockfile.settings?.injectWorkspacePackages) !== Boolean(injectWorkspacePackages)) { + return 'settings.injectWorkspacePackages' + } return null } diff --git a/lockfile/types/src/index.ts b/lockfile/types/src/index.ts index 9d39ad8d0e9..06b788e9201 100644 --- a/lockfile/types/src/index.ts +++ b/lockfile/types/src/index.ts @@ -9,6 +9,7 @@ export interface LockfileSettings { autoInstallPeers?: boolean excludeLinksFromLockfile?: boolean peersSuffixMaxLength?: number + injectWorkspacePackages?: boolean } export interface Lockfile { diff --git a/pkg-manager/core/src/getPeerDependencyIssues.ts b/pkg-manager/core/src/getPeerDependencyIssues.ts index 11ec071e623..f02711b1529 100644 --- a/pkg-manager/core/src/getPeerDependencyIssues.ts +++ b/pkg-manager/core/src/getPeerDependencyIssues.ts @@ -46,7 +46,7 @@ export async function getPeerDependencyIssues ( const projectsToResolve = Object.values(ctx.projects).map((project) => ({ ...project, updatePackageManifest: false, - wantedDependencies: getWantedDependencies(project.manifest), + wantedDependencies: getWantedDependencies(project.manifest, opts), })) const preferredVersions = getPreferredVersionsFromLockfileAndManifests( ctx.wantedLockfile.packages, diff --git a/pkg-manager/core/src/install/extendInstallOptions.ts b/pkg-manager/core/src/install/extendInstallOptions.ts index cd1fb0a7e5e..31e8d7567dd 100644 --- a/pkg-manager/core/src/install/extendInstallOptions.ts +++ b/pkg-manager/core/src/install/extendInstallOptions.ts @@ -155,6 +155,7 @@ export interface StrictInstallOptions { peersSuffixMaxLength: number prepareExecutionEnv?: PrepareExecutionEnv returnListOfDepsRequiringBuild?: boolean + injectWorkspacePackages?: boolean } export type InstallOptions = diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index eaf39944b4d..2eeb9d35a45 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -361,6 +361,7 @@ export async function mutateModules ( if (!opts.ignorePackageManifest) { const outdatedLockfileSettingName = getOutdatedLockfileSetting(ctx.wantedLockfile, { autoInstallPeers: opts.autoInstallPeers, + injectWorkspacePackages: opts.injectWorkspacePackages, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, overrides: overridesMap, @@ -384,6 +385,7 @@ export async function mutateModules ( autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, + injectWorkspacePackages: opts.injectWorkspacePackages, } ctx.wantedLockfile.overrides = overridesMap ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum @@ -395,6 +397,7 @@ export async function mutateModules ( autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, peersSuffixMaxLength: opts.peersSuffixMaxLength, + injectWorkspacePackages: opts.injectWorkspacePackages, } } if ( @@ -976,6 +979,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot, supportedArchitectures: opts.supportedArchitectures, peersSuffixMaxLength: opts.peersSuffixMaxLength, + injectWorkspacePackages: opts.injectWorkspacePackages, } ) if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) { @@ -1355,6 +1359,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { includeDirect: opts.includeDirect, updateWorkspaceDependencies: false, nodeExecPath: opts.nodeExecPath, + injectWorkspacePackages: opts.injectWorkspacePackages, } for (const project of allProjectsLocatedInsideWorkspace) { if (!newProjects.some(({ rootDir }) => rootDir === project.rootDir)) { diff --git a/pkg-manager/core/test/install/injectLocalPackages.ts b/pkg-manager/core/test/install/injectLocalPackages.ts index 3b8eed2463a..4804bd24bed 100644 --- a/pkg-manager/core/test/install/injectLocalPackages.ts +++ b/pkg-manager/core/test/install/injectLocalPackages.ts @@ -212,6 +212,204 @@ test('inject local packages', async () => { } }) +test('inject local packages using the injectWorkspacePackages setting', async () => { + const project1Manifest = { + name: 'project-1', + version: '1.0.0', + dependencies: { + 'is-negative': '1.0.0', + }, + devDependencies: { + '@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0', + }, + peerDependencies: { + 'is-positive': '>=1.0.0', + }, + } + const project2Manifest = { + name: 'project-2', + version: '1.0.0', + dependencies: { + 'project-1': 'workspace:1.0.0', + }, + devDependencies: { + 'is-positive': '1.0.0', + }, + } + const project3Manifest = { + name: 'project-3', + version: '1.0.0', + dependencies: { + 'project-2': 'workspace:1.0.0', + }, + devDependencies: { + 'is-positive': '2.0.0', + }, + } + const projects = preparePackages([ + { + location: 'project-1', + package: project1Manifest, + }, + { + location: 'project-2', + package: project2Manifest, + }, + { + location: 'project-3', + package: project3Manifest, + }, + ]) + + const importers: MutatedProject[] = [ + { + mutation: 'install', + rootDir: path.resolve('project-1') as ProjectRootDir, + }, + { + mutation: 'install', + rootDir: path.resolve('project-2') as ProjectRootDir, + }, + { + mutation: 'install', + rootDir: path.resolve('project-3') as ProjectRootDir, + }, + ] + const allProjects: ProjectOptions[] = [ + { + buildIndex: 0, + manifest: project1Manifest, + rootDir: path.resolve('project-1') as ProjectRootDir, + }, + { + buildIndex: 0, + manifest: project2Manifest, + rootDir: path.resolve('project-2') as ProjectRootDir, + }, + { + buildIndex: 0, + manifest: project3Manifest, + rootDir: path.resolve('project-3') as ProjectRootDir, + }, + ] + await mutateModules(importers, testDefaults({ + autoInstallPeers: false, + allProjects, + injectWorkspacePackages: true, + })) + + projects['project-1'].has('is-negative') + projects['project-1'].has('@pnpm.e2e/dep-of-pkg-with-1-dep') + projects['project-1'].hasNot('is-positive') + + projects['project-2'].has('is-positive') + projects['project-2'].has('project-1') + + projects['project-3'].has('is-positive') + projects['project-3'].has('project-2') + + expect(fs.readdirSync('node_modules/.pnpm').length).toBe(8) + + const rootModules = assertProject(process.cwd()) + { + const lockfile = rootModules.readLockfile() + expect(lockfile.settings.injectWorkspacePackages).toBe(true) + expect(lockfile.importers['project-2'].dependenciesMeta).not.toEqual({ + 'project-1': { + injected: true, + }, + }) + expect(lockfile.packages['project-1@file:project-1']).toEqual({ + resolution: { + directory: 'project-1', + type: 'directory', + }, + peerDependencies: { + 'is-positive': '>=1.0.0', + }, + }) + expect(lockfile.snapshots['project-1@file:project-1(is-positive@1.0.0)']).toEqual({ + dependencies: { + 'is-negative': '1.0.0', + 'is-positive': '1.0.0', + }, + }) + expect(lockfile.packages['project-2@file:project-2']).toEqual({ + resolution: { + directory: 'project-2', + type: 'directory', + }, + }) + expect(lockfile.snapshots['project-2@file:project-2(is-positive@2.0.0)']).toEqual({ + dependencies: { + 'project-1': 'file:project-1(is-positive@2.0.0)', + }, + transitivePeerDependencies: ['is-positive'], + }) + + const modulesState = rootModules.readModulesManifest() + expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(2) + expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`) + expect(modulesState?.injectedDeps?.['project-1'][1]).toContain(`node_modules${path.sep}.pnpm`) + } + + rimraf('node_modules') + rimraf('project-1/node_modules') + rimraf('project-2/node_modules') + rimraf('project-3/node_modules') + + await mutateModules(importers, testDefaults({ + autoInstallPeers: false, + allProjects, + frozenLockfile: true, + injectWorkspacePackages: true, + })) + + projects['project-1'].has('is-negative') + projects['project-1'].has('@pnpm.e2e/dep-of-pkg-with-1-dep') + projects['project-1'].hasNot('is-positive') + + projects['project-2'].has('is-positive') + projects['project-2'].has('project-1') + + projects['project-3'].has('is-positive') + projects['project-3'].has('project-2') + + expect(fs.readdirSync('node_modules/.pnpm').length).toBe(8) + + // The injected project is updated when one of its dependencies needs to be updated + allProjects[0].manifest.dependencies!['is-negative'] = '2.0.0' + await mutateModules(importers, testDefaults({ autoInstallPeers: false, allProjects, injectWorkspacePackages: true })) + { + const lockfile = rootModules.readLockfile() + expect(lockfile.settings.injectWorkspacePackages).toBe(true) + expect(lockfile.importers['project-2'].dependenciesMeta).not.toEqual({ + 'project-1': { + injected: true, + }, + }) + expect(lockfile.packages['project-1@file:project-1']).toEqual({ + resolution: { + directory: 'project-1', + type: 'directory', + }, + peerDependencies: { + 'is-positive': '>=1.0.0', + }, + }) + expect(lockfile.snapshots['project-1@file:project-1(is-positive@1.0.0)']).toEqual({ + dependencies: { + 'is-negative': '2.0.0', + 'is-positive': '1.0.0', + }, + }) + const modulesState = rootModules.readModulesManifest() + expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(2) + expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`) + expect(modulesState?.injectedDeps?.['project-1'][1]).toContain(`node_modules${path.sep}.pnpm`) + } +}) + test('inject local packages declared via file protocol', async () => { const project1Manifest = { name: 'project-1', diff --git a/pkg-manager/package-requester/src/packageRequester.ts b/pkg-manager/package-requester/src/packageRequester.ts index af82d520154..0fedacc9b2a 100644 --- a/pkg-manager/package-requester/src/packageRequester.ts +++ b/pkg-manager/package-requester/src/packageRequester.ts @@ -188,6 +188,7 @@ async function resolveAndFetch ( registry: options.registry, workspacePackages: options.workspacePackages, updateToLatest: options.updateToLatest, + injectWorkspacePackages: options.injectWorkspacePackages, }), { priority: options.downloadPriority }) manifest = resolveResult.manifest diff --git a/pkg-manager/resolve-dependencies/src/index.ts b/pkg-manager/resolve-dependencies/src/index.ts index 86a8a056ce7..1a4da9b931e 100644 --- a/pkg-manager/resolve-dependencies/src/index.ts +++ b/pkg-manager/resolve-dependencies/src/index.ts @@ -127,6 +127,7 @@ export async function resolveDependencies ( workspacePackages: opts.workspacePackages, updateToLatest: opts.updateToLatest, noDependencySelectors: importers.every(({ wantedDependencies }) => wantedDependencies.length === 0), + injectWorkspacePackages: opts.injectWorkspacePackages, }) const projectsToResolve = await Promise.all(importers.map(async (project) => _toResolveImporter(project))) const { diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts index dcf792fcf48..5082cf3111b 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts @@ -153,6 +153,7 @@ export interface ResolutionContext { pendingNodes: PendingNode[] wantedLockfile: Lockfile currentLockfile: Lockfile + injectWorkspacePackages?: boolean linkWorkspacePackagesDepth: number lockfileDir: string storeController: StoreController @@ -1261,6 +1262,7 @@ async function resolveDependency ( return err }, updateToLatest: options.updateToLatest, + injectWorkspacePackages: ctx.injectWorkspacePackages, }) } catch (err: any) { // eslint-disable-line const wantedDependencyDetails = { diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts index 1facf920b5c..a559fa3dd19 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts @@ -118,6 +118,7 @@ export interface ResolveDependenciesOptions { preferWorkspacePackages?: boolean resolutionMode?: 'highest' | 'time-based' | 'lowest-direct' resolvePeersFromWorkspaceRoot?: boolean + injectWorkspacePackages?: boolean linkWorkspacePackagesDepth?: number lockfileDir: string storeController: StoreController @@ -165,6 +166,7 @@ export async function resolveDependencyTree ( force: opts.force, forceFullResolution: opts.forceFullResolution, ignoreScripts: opts.ignoreScripts, + injectWorkspacePackages: opts.injectWorkspacePackages, linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? -1, lockfileDir: opts.lockfileDir, nodeVersion: opts.nodeVersion, diff --git a/resolving/npm-resolver/src/index.ts b/resolving/npm-resolver/src/index.ts index cfcb14e4763..55bdc93f407 100644 --- a/resolving/npm-resolver/src/index.ts +++ b/resolving/npm-resolver/src/index.ts @@ -118,6 +118,7 @@ export type ResolveFromNpmOptions = { preferredVersions?: PreferredVersions preferWorkspacePackages?: boolean updateToLatest?: boolean + injectWorkspacePackages?: boolean } & ({ projectDir?: string workspacePackages?: undefined @@ -143,6 +144,7 @@ async function resolveNpm ( projectDir: opts.projectDir, registry: opts.registry, workspacePackages: opts.workspacePackages, + injectWorkspacePackages: opts.injectWorkspacePackages, }) if (resolvedFromWorkspace != null) { return resolvedFromWorkspace @@ -173,7 +175,7 @@ async function resolveNpm ( wantedDependency, projectDir: opts.projectDir, lockfileDir: opts.lockfileDir, - hardLinkLocalPackages: wantedDependency.injected, + hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected, }) } catch { // ignore @@ -190,7 +192,7 @@ async function resolveNpm ( wantedDependency, projectDir: opts.projectDir, lockfileDir: opts.lockfileDir, - hardLinkLocalPackages: wantedDependency.injected, + hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected, }) } catch { // ignore @@ -207,7 +209,7 @@ async function resolveNpm ( ...resolveFromLocalPackage(matchedPkg, spec.normalizedPref, { projectDir: opts.projectDir, lockfileDir: opts.lockfileDir, - hardLinkLocalPackages: wantedDependency.injected, + hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected, }), latest: meta['dist-tags'].latest, } @@ -218,7 +220,7 @@ async function resolveNpm ( ...resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec.normalizedPref, { projectDir: opts.projectDir, lockfileDir: opts.lockfileDir, - hardLinkLocalPackages: wantedDependency.injected, + hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected, }), latest: meta['dist-tags'].latest, } @@ -249,6 +251,7 @@ function tryResolveFromWorkspace ( projectDir?: string registry: string workspacePackages?: WorkspacePackages + injectWorkspacePackages?: boolean } ): ResolveResult | null { if (!wantedDependency.pref?.startsWith('workspace:')) { @@ -267,7 +270,7 @@ function tryResolveFromWorkspace ( return tryResolveFromWorkspacePackages(opts.workspacePackages, spec, { wantedDependency, projectDir: opts.projectDir, - hardLinkLocalPackages: wantedDependency.injected, + hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected, lockfileDir: opts.lockfileDir, }) } diff --git a/resolving/resolver-base/src/index.ts b/resolving/resolver-base/src/index.ts index f48a27317dd..553a5461cb8 100644 --- a/resolving/resolver-base/src/index.ts +++ b/resolving/resolver-base/src/index.ts @@ -83,6 +83,7 @@ export interface ResolveOptions { registry: string workspacePackages?: WorkspacePackages updateToLatest?: boolean + injectWorkspacePackages?: boolean } export type WantedDependency = { diff --git a/store/store-controller-types/src/index.ts b/store/store-controller-types/src/index.ts index 40789b8ec61..dc7cdb3b1fb 100644 --- a/store/store-controller-types/src/index.ts +++ b/store/store-controller-types/src/index.ts @@ -132,6 +132,7 @@ export interface RequestPackageOptions { supportedArchitectures?: SupportedArchitectures onFetchError?: OnFetchError updateToLatest?: boolean + injectWorkspacePackages?: boolean } export type BundledManifestFunction = () => Promise