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: resolve peer dependencies from workspace root #5882

Merged
merged 4 commits into from Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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'])
})