mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: resolve peer dependencies from workspace root (#5882)
partially reverts #4469
This commit is contained in:
9
.changeset/clean-seals-boil.md
Normal file
9
.changeset/clean-seals-boil.md
Normal file
@@ -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).
|
||||
@@ -87,6 +87,7 @@ export interface Config {
|
||||
resolutionMode?: 'highest' | 'time-based'
|
||||
registrySupportsTimeField?: boolean
|
||||
failedToLoadBuiltInConfig: boolean
|
||||
resolvePeersFromWorkspaceRoot?: boolean
|
||||
|
||||
// proxy
|
||||
httpProxy?: string
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface StrictInstallOptions {
|
||||
allowNonAppliedPatches: boolean
|
||||
preferSymlinkedExecutables: boolean
|
||||
resolutionMode: 'highest' | 'time-based'
|
||||
resolvePeersFromWorkspaceRoot: boolean
|
||||
|
||||
publicHoistPattern: string[] | undefined
|
||||
hoistPattern: string[] | undefined
|
||||
@@ -204,6 +205,7 @@ const defaults = async (opts: InstallOptions) => {
|
||||
modulesCacheMaxAge: 7 * 24 * 60,
|
||||
resolveSymlinksInInjectedDirs: false,
|
||||
dedupeDirectDeps: false,
|
||||
resolvePeersFromWorkspaceRoot: false,
|
||||
} as StrictInstallOptions
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
@@ -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 }: {
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user