Skip to content

Commit

Permalink
feat: dedupe injected dependencies (#7416)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
zkochan committed Dec 14, 2023
1 parent e79ab38 commit 672c559
Show file tree
Hide file tree
Showing 13 changed files with 435 additions and 12 deletions.
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`)
}
})

0 comments on commit 672c559

Please sign in to comment.