Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dedupe injected dependencies #7416

Merged
merged 15 commits into from Dec 14, 2023
8 changes: 8 additions & 0 deletions .changeset/soft-ghosts-kick.md
@@ -0,0 +1,8 @@
---
"@pnpm/resolve-dependencies": minor
"@pnpm/core": minor
"@pnpm/config": minor
"pnpm": minor
---

A new setting added for symlinking [injected dependencies](https://pnpm.io/package_json#dependenciesmetainjected) from the workspace, if their dependencies use the same peer dependencies as the dependent package. The setting is called `dedupe-injected-deps` [#7416](https://github.com/pnpm/pnpm/pull/7416).
1 change: 1 addition & 0 deletions config/config/src/Config.ts
Expand Up @@ -184,6 +184,7 @@ export interface Config {
gitBranchLockfile?: boolean
globalDir?: string
lockfile?: boolean
dedupeInjectedDeps?: boolean
}

export interface ConfigWithDeprecatedSettings extends Config {
Expand Down
2 changes: 2 additions & 0 deletions config/config/src/index.ts
Expand Up @@ -52,6 +52,7 @@ export const types = Object.assign({
'deploy-all-files': Boolean,
'dedupe-peer-dependents': Boolean,
'dedupe-direct-deps': Boolean,
'dedupe-injected-deps': Boolean,
dev: [null, true],
dir: String,
'disallow-workspace-cycles': Boolean,
Expand Down Expand Up @@ -204,6 +205,7 @@ export async function getConfig (
'deploy-all-files': false,
'dedupe-peer-dependents': true,
'dedupe-direct-deps': false,
'dedupe-injected-deps': false,
'disallow-workspace-cycles': false,
'enable-modules-dir': true,
'exclude-links-from-lockfile': false,
Expand Down
1 change: 1 addition & 0 deletions pkg-manager/core/src/install/extendInstallOptions.ts
Expand Up @@ -123,6 +123,7 @@ export interface StrictInstallOptions {
allProjects: ProjectOptions[]
resolveSymlinksInInjectedDirs: boolean
dedupeDirectDeps: boolean
dedupeInjectedDeps: boolean
dedupePeerDependents: boolean
extendNodePath: boolean
excludeLinksFromLockfile: boolean
Expand Down
1 change: 1 addition & 0 deletions pkg-manager/core/src/install/index.ts
Expand Up @@ -1010,6 +1010,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
currentLockfile: ctx.currentLockfile,
defaultUpdateDepth: opts.depth,
dedupeDirectDeps: opts.dedupeDirectDeps,
dedupeInjectedDeps: opts.dedupeInjectedDeps,
dedupePeerDependents: opts.dedupePeerDependents,
dryRun: opts.lockfileOnly,
engineStrict: opts.engineStrict,
Expand Down
261 changes: 261 additions & 0 deletions pkg-manager/core/test/install/injectLocalPackages.ts
Expand Up @@ -1780,3 +1780,264 @@ test('do not relink injected dependency on install when disableRelinkLocalDirDep

expect(newInode).toEqual(getInode())
})

test('injected local packages are deduped', 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',
},
dependenciesMeta: {
'project-1': {
injected: true,
},
},
}
const project3Manifest = {
name: 'project-3',
version: '1.0.0',
dependencies: {
'project-2': 'workspace:1.0.0',
},
devDependencies: {
'is-positive': '2.0.0',
},
dependenciesMeta: {
'project-2': {
injected: true,
},
},
}
const project4Manifest = {
name: 'project-4',
version: '1.0.0',
dependencies: {
'project-2': 'workspace:1.0.0',
},
devDependencies: {
'is-positive': '1.0.0',
},
dependenciesMeta: {
'project-2': {
injected: true,
},
},
}
const project5Manifest = {
name: 'project-5',
version: '1.0.0',
dependencies: {
'project-4': 'workspace:1.0.0',
},
devDependencies: {
'is-positive': '1.0.0',
},
dependenciesMeta: {
'project-4': {
injected: true,
},
},
}
const projects = preparePackages([
{
location: 'project-1',
package: project1Manifest,
},
{
location: 'project-2',
package: project2Manifest,
},
{
location: 'project-3',
package: project3Manifest,
},
{
location: 'project-4',
package: project4Manifest,
},
{
location: 'project-5',
package: project5Manifest,
},
])

const importers: MutatedProject[] = [
{
mutation: 'install',
rootDir: path.resolve('project-1'),
},
{
mutation: 'install',
rootDir: path.resolve('project-2'),
},
{
mutation: 'install',
rootDir: path.resolve('project-3'),
},
{
mutation: 'install',
rootDir: path.resolve('project-4'),
},
{
mutation: 'install',
rootDir: path.resolve('project-5'),
},
]
const allProjects: ProjectOptions[] = [
{
buildIndex: 0,
manifest: project1Manifest,
rootDir: path.resolve('project-1'),
},
{
buildIndex: 0,
manifest: project2Manifest,
rootDir: path.resolve('project-2'),
},
{
buildIndex: 0,
manifest: project3Manifest,
rootDir: path.resolve('project-3'),
},
{
buildIndex: 0,
manifest: project4Manifest,
rootDir: path.resolve('project-4'),
},
{
buildIndex: 0,
manifest: project5Manifest,
rootDir: path.resolve('project-5'),
},
]
const workspacePackages = {
'project-1': {
'1.0.0': {
dir: path.resolve('project-1'),
manifest: project1Manifest,
},
},
'project-2': {
'1.0.0': {
dir: path.resolve('project-2'),
manifest: project2Manifest,
},
},
'project-3': {
'1.0.0': {
dir: path.resolve('project-3'),
manifest: project3Manifest,
},
},
'project-4': {
'1.0.0': {
dir: path.resolve('project-4'),
manifest: project4Manifest,
},
},
}
await mutateModules(importers, await testDefaults({
autoInstallPeers: true,
allProjects,
dedupeInjectedDeps: true,
workspacePackages,
}))

await projects['project-1'].has('is-negative')
await projects['project-1'].has('@pnpm.e2e/dep-of-pkg-with-1-dep')
await projects['project-1'].has('is-positive')

await projects['project-2'].has('is-positive')
await projects['project-2'].has('project-1')

await projects['project-3'].has('is-positive')
await projects['project-3'].has('project-2')

expect(fs.readdirSync('node_modules/.pnpm').length).toBe(7)

const rootModules = assertProject(process.cwd())
{
const lockfile = await rootModules.readLockfile()
expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['file:project-1(is-positive@1.0.0)']).toBeFalsy()
expect(lockfile.packages['file:project-2(is-positive@2.0.0)']).toEqual({
resolution: {
directory: 'project-2',
type: 'directory',
},
id: 'file:project-2',
name: 'project-2',
dependencies: {
'project-1': 'file:project-1(is-positive@2.0.0)',
},
transitivePeerDependencies: ['is-positive'],
dev: false,
})

const modulesState = await rootModules.readModulesManifest()
expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(1)
expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`)
}

await rimraf('node_modules')
await rimraf('project-1/node_modules')
await rimraf('project-2/node_modules')
await rimraf('project-3/node_modules')

await mutateModules(importers, await testDefaults({
autoInstallPeers: true,
allProjects,
dedupeInjectedDeps: true,
frozenLockfile: true,
workspacePackages,
}))

await projects['project-1'].has('is-negative')
await projects['project-1'].has('@pnpm.e2e/dep-of-pkg-with-1-dep')
await projects['project-1'].has('is-positive')

await projects['project-2'].has('is-positive')
await projects['project-2'].has('project-1')

await projects['project-3'].has('is-positive')
await projects['project-3'].has('project-2')

expect(fs.readdirSync('node_modules/.pnpm').length).toBe(7)

// 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, await testDefaults({ autoInstallPeers: true, allProjects, dedupeInjectedDeps: true, workspacePackages }))
{
const lockfile = await rootModules.readLockfile()
expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({
'project-1': {
injected: true,
},
})
expect(lockfile.packages['file:project-1(is-positive@1.0.0)']).toBeFalsy()
const modulesState = await rootModules.readModulesManifest()
expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(1)
expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`)
}
})