Skip to content

Commit 2208cf1

Browse files
feat(mono-pub): add ignoreDependencies option to resolve cycles (#31)
* feat(mono-pub): add ignoreDependencies option to resolve cycles * fix(mono-pub): fix semgrep issues and tests
1 parent eb4cc7b commit 2208cf1

File tree

5 files changed

+119
-6
lines changed

5 files changed

+119
-6
lines changed

packages/mono-pub/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ export default async function publish(
3232
plugins: Array<MonoPubPlugin>,
3333
options: MonoPubOptions = {}
3434
) {
35-
const { stdout = process.stdout, stderr = process.stderr, ...restOptions } = options
35+
const { stdout = process.stdout, stderr = process.stderr, ignoreDependencies, ...restOptions } = options
3636
const logger = getLogger({ stdout, stderr })
3737
const context: MonoPubContext = {
3838
cwd: process.cwd(),
3939
env: process.env,
40+
ignoreDependencies: ignoreDependencies || {},
4041
...restOptions,
4142
logger,
4243
}

packages/mono-pub/src/types/config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import type { Signale } from 'signale'
22

3+
export type IgnoringDependencies = Record<string, Array<string>>
4+
35
export type MonoPubContext = {
6+
/** Path to start scanning from, process.cwd() by default */
47
cwd: string
8+
/** Plugins shared environment, process.env by default */
59
env: Record<string, string | undefined>
10+
/** Context-specific logger which can be used by plugins to display progress */
611
logger: Signale<'info' | 'error' | 'log' | 'success'>
12+
/**
13+
* List of dependencies per package, which should not affect its version bump.
14+
* Might be helpful to break cyclic dependencies, so proper release order can be resolved
15+
* */
16+
ignoreDependencies: IgnoringDependencies
717
}
818

919
export type MonoPubOptions = Partial<
10-
Pick<MonoPubContext, 'cwd' | 'env'> & {
20+
Pick<MonoPubContext, 'cwd' | 'env' | 'ignoreDependencies'> & {
1121
stdout: NodeJS.WriteStream
1222
stderr: NodeJS.WriteStream
1323
}

packages/mono-pub/src/utils/deps.spec.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import { getDependencies, getExecutionOrder, patchPackageDeps } from './deps'
66
import { getNewVersion, versionToString } from './versions'
77

88
import type { DirResult } from 'tmp'
9-
import type { BasePackageInfo, LatestPackagesReleases, PackageVersion } from '@/types'
9+
import type {
10+
BasePackageInfo,
11+
LatestPackagesReleases,
12+
PackageVersion,
13+
PackageInfoWithDependencies,
14+
IgnoringDependencies,
15+
} from '@/types'
1016

1117
function writePackageJson(obj: Record<string, unknown>, packagePath: string, cwd: string) {
1218
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
@@ -149,6 +155,87 @@ describe('Dependencies utils', () => {
149155
const batches = getExecutionOrder(Object.values(deps), { batching: true })
150156
expect(batches).toEqual([[pkg1Info, pkg4Info], [pkg2Info], [pkg3Info]])
151157
})
158+
describe('Should respect ignoreDependencies', () => {
159+
const cycleLength = 6
160+
let packages: Array<PackageInfoWithDependencies> = []
161+
beforeEach(() => {
162+
packages = Array.from({ length: cycleLength }, (_, i) => {
163+
const packageName = `package${i}`
164+
const prevPackageName = `package${(i - 1 + cycleLength) % cycleLength}`
165+
166+
return {
167+
name: packageName,
168+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
169+
location: path.join(tmpDir.name, 'packages', packageName, 'package.json'),
170+
dependsOn: [
171+
{
172+
name: prevPackageName,
173+
type: Math.random() > 0.5 ? 'dep' : 'devDep',
174+
value: versionToString(getRandomVersion()),
175+
},
176+
],
177+
}
178+
})
179+
})
180+
181+
it('Simple cyclic graph test', () => {
182+
expect(() => {
183+
getExecutionOrder(packages)
184+
}).toThrow('The release cannot be done because of cyclic dependencies')
185+
186+
for (let i = 0; i < cycleLength; i++) {
187+
const ignoreDependencies = {
188+
[packages[i].name]: [packages[(i - 1 + cycleLength) % cycleLength].name],
189+
}
190+
191+
const expectedNonBatchedOrder = Array.from(
192+
{ length: cycleLength },
193+
(_, idx) => packages[(i + idx) % cycleLength].name
194+
)
195+
196+
const nonBatchedOrder = getExecutionOrder(packages, {
197+
ignoreDependencies,
198+
}).map((pkg) => pkg.name)
199+
200+
expect(nonBatchedOrder).toEqual(expectedNonBatchedOrder)
201+
202+
const expectedBatchedOrder = expectedNonBatchedOrder.map((name) => [name])
203+
const batchedOrder = getExecutionOrder(packages, {
204+
ignoreDependencies,
205+
batching: true,
206+
}).map((batch) => batch.map((pkg) => pkg.name))
207+
208+
expect(batchedOrder).toEqual(expectedBatchedOrder)
209+
}
210+
})
211+
it('Advanced test', () => {
212+
const ignoreDependencies: IgnoringDependencies = {}
213+
const firstBatchExpected: Array<string> = []
214+
const secondBatchExpected: Array<string> = []
215+
216+
for (let i = 0; i < cycleLength; i++) {
217+
const packageName = packages[i].name
218+
219+
if (i % 2 === 0) {
220+
const prevPackageName = packages[(i + cycleLength - 1) % cycleLength].name
221+
ignoreDependencies[packageName] = [prevPackageName]
222+
firstBatchExpected.push(packageName)
223+
} else {
224+
secondBatchExpected.push(packageName)
225+
}
226+
}
227+
228+
const batchedOrder = getExecutionOrder(packages, {
229+
ignoreDependencies,
230+
batching: true,
231+
}).map((batch) => batch.map((pkg) => pkg.name))
232+
233+
expect(batchedOrder).toEqual([
234+
expect.objectContaining(firstBatchExpected),
235+
expect.objectContaining(secondBatchExpected),
236+
])
237+
})
238+
})
152239
})
153240
describe('patchPackageDeps', () => {
154241
it('Should patch package with new version or version from latest release', async () => {

packages/mono-pub/src/utils/deps.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import get from 'lodash/get'
33
import set from 'lodash/set'
44
import { versionToString, getVersionCriteria } from '@/utils/versions'
55

6-
import type { BasePackageInfo, PackageInfoWithDependencies, DependencyInfo, LatestPackagesReleases } from '@/types'
6+
import type {
7+
BasePackageInfo,
8+
PackageInfoWithDependencies,
9+
DependencyInfo,
10+
LatestPackagesReleases,
11+
IgnoringDependencies,
12+
} from '@/types'
713

814
export async function getDependencies(
915
packages: Array<BasePackageInfo>
@@ -40,6 +46,7 @@ type ExecutionOrder<T extends boolean | undefined> = T extends true
4046

4147
type TaskPlanningOptions<T extends boolean | undefined> = {
4248
batching?: T
49+
ignoreDependencies?: IgnoringDependencies
4350
}
4451

4552
export function getExecutionOrder<T extends boolean | undefined = undefined>(
@@ -48,12 +55,15 @@ export function getExecutionOrder<T extends boolean | undefined = undefined>(
4855
): ExecutionOrder<T> {
4956
const batches: Array<Array<BasePackageInfo>> = []
5057
const pkgMap = Object.fromEntries(packages.map((pkg) => [pkg.name, pkg]))
58+
const ignoreDependencies = options?.ignoreDependencies || {}
5159

5260
const dependencies = new Map<string, Array<string>>()
5361
for (const pkg of packages) {
62+
const packageIgnoreList = ignoreDependencies[pkg.name] || []
63+
5464
dependencies.set(
5565
pkg.name,
56-
pkg.dependsOn.map((dep) => dep.name)
66+
pkg.dependsOn.map((dep) => dep.name).filter((name) => !packageIgnoreList.includes(name))
5767
)
5868
}
5969

packages/mono-pub/src/utils/plugins.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ describe('CombinedPlugin', () => {
173173
packages = Object.values(await getDependencies([pkg3Info, pkg2Info, pkg1Info]))
174174

175175
eventLog = []
176-
ctx = { cwd: process.cwd(), env: {}, logger: getLogger({ stdout: process.stdout, stderr: process.stderr }) }
176+
ctx = {
177+
cwd: process.cwd(),
178+
env: {},
179+
logger: getLogger({ stdout: process.stdout, stderr: process.stderr }),
180+
ignoreDependencies: {},
181+
}
177182
chain = {
178183
getter: getFakeVersionGetter(eventLog),
179184
extractor: getFakeExtractor(eventLog),

0 commit comments

Comments
 (0)