Skip to content

Commit

Permalink
feat: resolve peer dependencies from workspace root (#5882)
Browse files Browse the repository at this point in the history
partially reverts #4469
  • Loading branch information
zkochan committed Jan 5, 2023
1 parent 5b311d1 commit 1fad508
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 10 deletions.
9 changes: 9 additions & 0 deletions .changeset/clean-seals-boil.md
@@ -0,0 +1,9 @@
---
"@pnpm/plugin-commands-installation": minor
"@pnpm/resolve-dependencies": minor
"@pnpm/core": minor
"@pnpm/config": minor
"pnpm": minor
---

When the `resolve-peers-from-workspace-root` setting is set to `true`, pnpm will use dependencies installed in the root of the workspace to resolve peer dependencies in any of the workspace's projects [#5882](https://github.com/pnpm/pnpm/pull/5882).
1 change: 1 addition & 0 deletions config/config/src/Config.ts
Expand Up @@ -87,6 +87,7 @@ export interface Config {
resolutionMode?: 'highest' | 'time-based'
registrySupportsTimeField?: boolean
failedToLoadBuiltInConfig: boolean
resolvePeersFromWorkspaceRoot?: boolean

// proxy
httpProxy?: string
Expand Down
1 change: 1 addition & 0 deletions config/config/src/index.ts
Expand Up @@ -92,6 +92,7 @@ export const types = Object.assign({
'recursive-install': Boolean,
reporter: String,
'resolution-mode': ['highest', 'time-based'],
'resolve-peers-from-workspace-root': Boolean,
'aggregate-output': Boolean,
'save-peer': Boolean,
'save-workspace-protocol': Boolean,
Expand Down
2 changes: 2 additions & 0 deletions pkg-manager/core/src/install/extendInstallOptions.ts
Expand Up @@ -104,6 +104,7 @@ export interface StrictInstallOptions {
allowNonAppliedPatches: boolean
preferSymlinkedExecutables: boolean
resolutionMode: 'highest' | 'time-based'
resolvePeersFromWorkspaceRoot: boolean

publicHoistPattern: string[] | undefined
hoistPattern: string[] | undefined
Expand Down Expand Up @@ -204,6 +205,7 @@ const defaults = async (opts: InstallOptions) => {
modulesCacheMaxAge: 7 * 24 * 60,
resolveSymlinksInInjectedDirs: false,
dedupeDirectDeps: false,
resolvePeersFromWorkspaceRoot: false,
} as StrictInstallOptions
}

Expand Down
1 change: 1 addition & 0 deletions pkg-manager/core/src/install/index.ts
Expand Up @@ -834,6 +834,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
workspacePackages: opts.workspacePackages,
patchedDependencies: opts.patchedDependencies,
lockfileIncludeTarballUrl: opts.lockfileIncludeTarballUrl,
resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot,
}
)
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
Expand Down
76 changes: 76 additions & 0 deletions pkg-manager/core/test/install/peerDependencies.ts
Expand Up @@ -233,6 +233,82 @@ test('strict-peer-dependencies: error is thrown when cannot resolve peer depende
})
})

test('peer dependency is resolved from the dependencies of the workspace root project', async () => {
const projects = preparePackages([
{
location: '.',
package: { name: 'root' },
},
{
location: 'pkg',
package: {},
},
])
const allProjects = [
{
buildIndex: 0,
manifest: {
name: 'root',
version: '1.0.0',

dependencies: {
ajv: '4.10.0',
},
},
rootDir: process.cwd(),
},
{
buildIndex: 0,
manifest: {
name: 'pkg',
version: '1.0.0',

dependencies: {
'ajv-keywords': '1.5.0',
},
},
rootDir: path.resolve('pkg'),
},
]
const reporter = jest.fn()
await mutateModules([
{
mutation: 'install',
rootDir: process.cwd(),
},
{
mutation: 'install',
rootDir: path.resolve('pkg'),
},
], await testDefaults({ allProjects, reporter, resolvePeersFromWorkspaceRoot: true }))

expect(reporter).not.toHaveBeenCalledWith(expect.objectContaining({
name: 'pnpm:peer-dependency-issues',
}))

{
const lockfile = await projects.root.readLockfile()
expect(lockfile.importers.pkg?.dependencies?.['ajv-keywords']).toBe('1.5.0_ajv@4.10.0')
}

allProjects[1].manifest.dependencies['is-positive'] = '1.0.0'
await mutateModules([
{
mutation: 'install',
rootDir: process.cwd(),
},
{
mutation: 'install',
rootDir: path.resolve('pkg'),
},
], await testDefaults({ allProjects, reporter, resolvePeersFromWorkspaceRoot: true }))

{
const lockfile = await projects.root.readLockfile()
expect(lockfile.importers.pkg?.dependencies?.['ajv-keywords']).toBe('1.5.0_ajv@4.10.0')
}
})

test('warning is reported when cannot resolve peer dependency for non-top-level dependency', async () => {
prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/abc-parent-with-ab', version: '1.0.0', distTag: 'latest' })
Expand Down
9 changes: 8 additions & 1 deletion pkg-manager/plugin-commands-installation/src/installDeps.ts
Expand Up @@ -143,13 +143,20 @@ when running add/update with the --workspace option')
})
}

let allProjectsGraph = selectedProjectsGraph
if (!allProjectsGraph[opts.workspaceDir]) {
allProjectsGraph = {
...allProjectsGraph,
...selectProjectByDir(allProjects, opts.workspaceDir),
}
}
await recursive(allProjects,
params,
{
...opts,
forceHoistPattern,
forcePublicHoistPattern,
allProjectsGraph: selectedProjectsGraph,
allProjectsGraph,
selectedProjectsGraph,
workspaceDir: opts.workspaceDir,
},
Expand Down
13 changes: 13 additions & 0 deletions pkg-manager/plugin-commands-installation/src/recursive.ts
Expand Up @@ -246,6 +246,19 @@ export async function recursive (
} as MutatedProject)
}
}))
if (!opts.selectedProjectsGraph[opts.workspaceDir] && manifestsByPath[opts.workspaceDir] != null) {
const localConfig = await memReadLocalConfig(opts.workspaceDir)
const modulesDir = localConfig.modulesDir ?? opts.modulesDir
const { manifest, writeProjectManifest } = manifestsByPath[opts.workspaceDir]
writeProjectManifests.push(writeProjectManifest)
mutatedImporters.push({
buildIndex: 0,
manifest,
modulesDir,
mutation: 'install',
rootDir: opts.workspaceDir,
} as MutatedProject)
}
if ((mutatedImporters.length === 0) && cmdFullName === 'update' && opts.depth === 0) {
throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES',
'None of the specified packages were found in the dependencies of any of the projects.')
Expand Down
1 change: 1 addition & 0 deletions pkg-manager/resolve-dependencies/src/index.ts
Expand Up @@ -207,6 +207,7 @@ export async function resolveDependencies (
lockfileDir: opts.lockfileDir,
projects: projectsToLink,
virtualStoreDir: opts.virtualStoreDir,
resolvePeersFromWorkspaceRoot: Boolean(opts.resolvePeersFromWorkspaceRoot),
})

for (const { id, manifest } of projectsToLink) {
Expand Down
Expand Up @@ -77,6 +77,7 @@ export interface ResolveDependenciesOptions {
preferredVersions?: PreferredVersions
preferWorkspacePackages?: boolean
resolutionMode?: 'highest' | 'time-based'
resolvePeersFromWorkspaceRoot?: boolean
updateMatching?: (pkgName: string) => boolean
linkWorkspacePackagesDepth?: number
lockfileDir: string
Expand Down
30 changes: 21 additions & 9 deletions pkg-manager/resolve-dependencies/src/resolvePeers.ts
Expand Up @@ -46,19 +46,22 @@ export interface GenericDependenciesGraph<T extends PartialResolvedPackage> {
[depPath: string]: T & GenericDependenciesGraphNode
}

export interface ProjectToResolve {
directNodeIdsByAlias: { [alias: string]: string }
// only the top dependencies that were already installed
// to avoid warnings about unresolved peer dependencies
topParents: Array<{ name: string, version: string }>
rootDir: string // is only needed for logging
id: string
}

export function resolvePeers<T extends PartialResolvedPackage> (
opts: {
projects: Array<{
directNodeIdsByAlias: { [alias: string]: string }
// only the top dependencies that were already installed
// to avoid warnings about unresolved peer dependencies
topParents: Array<{ name: string, version: string }>
rootDir: string // is only needed for logging
id: string
}>
projects: ProjectToResolve[]
dependenciesTree: DependenciesTree<T>
virtualStoreDir: string
lockfileDir: string
resolvePeersFromWorkspaceRoot?: boolean
}
): {
dependenciesGraph: GenericDependenciesGraph<T>
Expand All @@ -68,11 +71,15 @@ export function resolvePeers<T extends PartialResolvedPackage> (
const depGraph: GenericDependenciesGraph<T> = {}
const pathsByNodeId = {}
const _createPkgsByName = createPkgsByName.bind(null, opts.dependenciesTree)
const rootPkgsByName = opts.resolvePeersFromWorkspaceRoot ? getRootPkgsByName(opts.dependenciesTree, opts.projects) : {}
const peerDependencyIssuesByProjects: PeerDependencyIssuesByProjects = {}

for (const { directNodeIdsByAlias, topParents, rootDir, id } of opts.projects) {
const peerDependencyIssues: Pick<PeerDependencyIssues, 'bad' | 'missing'> = { bad: {}, missing: {} }
const pkgsByName = _createPkgsByName({ directNodeIdsByAlias, topParents })
const pkgsByName = {
...rootPkgsByName,
..._createPkgsByName({ directNodeIdsByAlias, topParents }),
}

resolvePeersOfChildren(directNodeIdsByAlias, pkgsByName, {
dependenciesTree: opts.dependenciesTree,
Expand Down Expand Up @@ -108,6 +115,11 @@ export function resolvePeers<T extends PartialResolvedPackage> (
}
}

function getRootPkgsByName<T extends PartialResolvedPackage> (dependenciesTree: DependenciesTree<T>, projects: ProjectToResolve[]) {
const rootProject = projects.length > 1 ? projects.find(({ id }) => id === '.') : null
return rootProject == null ? {} : createPkgsByName(dependenciesTree, rootProject)
}

function createPkgsByName<T extends PartialResolvedPackage> (
dependenciesTree: DependenciesTree<T>,
{ directNodeIdsByAlias, topParents }: {
Expand Down
29 changes: 29 additions & 0 deletions pnpm/test/monorepo/index.ts
Expand Up @@ -1717,3 +1717,32 @@ packages/alfa test: Done
packages/alfa test: OK`
)
})

test('peer dependencies are resolved from the root of the workspace when a new dependency is added to a workspace project', async () => {
const projects = preparePackages([
{
location: '.',
package: {
name: 'project-1',
version: '1.0.0',

dependencies: {
ajv: '4.10.4',
},
},
},
{
name: 'project-2',
version: '1.0.0',
},
])

await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })

process.chdir('project-2')

await execPnpm(['add', 'ajv-keywords@1.5.0', '--strict-peer-dependencies', '--config.resolve-peers-from-workspace-root=true'])

const lockfile = await projects['project-1'].readLockfile()
expect(lockfile.packages).toHaveProperty(['/ajv-keywords/1.5.0_ajv@4.10.4'])
})

0 comments on commit 1fad508

Please sign in to comment.