feat: add disallow-workspace-cycles option to error on cyclic dependencies (#7144)

* feat: add `no-cyclic` option to error on cyclic dependencies

* refactor: rename no-cyclic to disallow-workspace-cycles
This commit is contained in:
Jannis Morgenstern
2023-10-03 18:25:22 +02:00
committed by GitHub
parent b0df222db0
commit 832e288263
8 changed files with 128 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
---
"@pnpm/config": minor
"@pnpm/core": minor
"@pnpm/plugin-commands-installation": minor
"pnpm": minor
---
Add `disallow-workspace-cycles` option to error instead of warn about cyclic dependencies

View File

@@ -160,6 +160,7 @@ export interface Config {
dedupePeerDependents?: boolean
patchesDir?: string
ignoreWorkspaceCycles?: boolean
disallowWorkspaceCycles?: boolean
packGzipLevel?: number
registries: Registries

View File

@@ -45,6 +45,7 @@ export const types = Object.assign({
'dedupe-direct-deps': Boolean,
dev: [null, true],
dir: String,
'disallow-workspace-cycles': Boolean,
'enable-modules-dir': Boolean,
'enable-pre-post-scripts': Boolean,
'exclude-links-from-lockfile': Boolean,
@@ -193,6 +194,7 @@ export async function getConfig (
'deploy-all-files': false,
'dedupe-peer-dependents': true,
'dedupe-direct-deps': false,
'disallow-workspace-cycles': false,
'enable-modules-dir': true,
'exclude-links-from-lockfile': false,
'extend-node-path': true,

View File

@@ -105,6 +105,7 @@ export interface StrictInstallOptions {
resolutionMode: 'highest' | 'time-based' | 'lowest-direct'
resolvePeersFromWorkspaceRoot: boolean
ignoreWorkspaceCycles: boolean
disallowWorkspaceCycles: boolean
publicHoistPattern: string[] | undefined
hoistPattern: string[] | undefined
@@ -223,6 +224,7 @@ const defaults = (opts: InstallOptions) => {
resolvePeersFromWorkspaceRoot: true,
extendNodePath: true,
ignoreWorkspaceCycles: false,
disallowWorkspaceCycles: false,
excludeLinksFromLockfile: false,
} as StrictInstallOptions
}

View File

@@ -98,6 +98,7 @@ export type ImportCommandOptions = Pick<Config,
| 'selectedProjectsGraph'
| 'workspaceDir'
| 'ignoreWorkspaceCycles'
| 'disallowWorkspaceCycles'
| 'sharedWorkspaceLockfile'
> & CreateStoreControllerOptions & Omit<InstallOptions, 'storeController' | 'lockfileOnly' | 'preferredVersions'>
@@ -139,6 +140,11 @@ export async function handler (
const cyclicDependenciesInfo = sequencedGraph.cycles.length > 0
? `: ${sequencedGraph.cycles.map(deps => deps.join(', ')).join('; ')}`
: ''
if (opts.disallowWorkspaceCycles) {
throw new PnpmError('DISALLOW_WORKSPACE_CYCLES', `There are cyclic workspace dependencies${cyclicDependenciesInfo}`)
}
logger.warn({
message: `There are cyclic workspace dependencies${cyclicDependenciesInfo}`,
prefix: opts.workspaceDir,

View File

@@ -293,6 +293,7 @@ export type InstallCommandOptions = Pick<Config,
| 'extraEnv'
| 'resolutionMode'
| 'ignoreWorkspaceCycles'
| 'disallowWorkspaceCycles'
> & CreateStoreControllerOptions & {
argv: {
original: string[]

View File

@@ -81,6 +81,7 @@ export type InstallDepsOptions = Pick<Config,
| 'workspaceDir'
| 'extraEnv'
| 'ignoreWorkspaceCycles'
| 'disallowWorkspaceCycles'
> & CreateStoreControllerOptions & {
argv: {
original: string[]
@@ -157,6 +158,11 @@ when running add/update with the --workspace option')
const cyclicDependenciesInfo = sequencedGraph.cycles.length > 0
? `: ${sequencedGraph.cycles.map(deps => deps.join(', ')).join('; ')}`
: ''
if (opts.disallowWorkspaceCycles) {
throw new PnpmError('DISALLOW_WORKSPACE_CYCLES', `There are cyclic workspace dependencies${cyclicDependenciesInfo}`)
}
logger.warn({
message: `There are cyclic workspace dependencies${cyclicDependenciesInfo}`,
prefix: opts.workspaceDir,

View File

@@ -0,0 +1,102 @@
import { install } from '@pnpm/plugin-commands-installation'
import { readProjects } from '@pnpm/filter-workspace-packages'
import { preparePackages } from '@pnpm/prepare'
import { DEFAULT_OPTS } from './utils'
import type { PnpmError } from '@pnpm/error'
test('should error if disallow-workspace-cycles is set', async () => {
preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: { 'project-2': 'workspace:*' },
},
{
name: 'project-2',
version: '2.0.0',
devDependencies: { 'project-1': 'workspace:*' },
},
])
const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), [])
let err!: PnpmError
try {
await install.handler({
...DEFAULT_OPTS,
allProjects,
dir: process.cwd(),
recursive: true,
selectedProjectsGraph,
workspaceDir: process.cwd(),
disallowWorkspaceCycles: true,
})
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err.code).toBe('ERR_PNPM_DISALLOW_WORKSPACE_CYCLES')
})
test('should not error if disallow-workspace-cycles is not set', async () => {
preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: { 'project-2': 'workspace:*' },
},
{
name: 'project-2',
version: '2.0.0',
devDependencies: { 'project-1': 'workspace:*' },
},
])
const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), [])
let err!: PnpmError
try {
await install.handler({
...DEFAULT_OPTS,
allProjects,
dir: process.cwd(),
recursive: true,
selectedProjectsGraph,
workspaceDir: process.cwd(),
})
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err).toBeUndefined()
})
test('should not error if there are no cyclic dependencies', async () => {
preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: { 'project-2': 'workspace:*' },
},
{
name: 'project-2',
version: '2.0.0',
},
])
const { allProjects, selectedProjectsGraph } = await readProjects(process.cwd(), [])
let err!: PnpmError
try {
await install.handler({
...DEFAULT_OPTS,
allProjects,
dir: process.cwd(),
recursive: true,
selectedProjectsGraph,
workspaceDir: process.cwd(),
disallowWorkspaceCycles: true,
})
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err).toBeUndefined()
})