From 31bf9c9af6f79d85520a68886b0599025ebbe02c Mon Sep 17 00:00:00 2001 From: SavelevMatthew Date: Tue, 3 Jun 2025 19:49:28 +0500 Subject: [PATCH 1/2] feat(mono-pub): add ignoreDependencies option to resolve cycles --- packages/mono-pub/src/index.ts | 3 +- packages/mono-pub/src/types/config.ts | 12 +++- packages/mono-pub/src/utils/deps.spec.ts | 88 +++++++++++++++++++++++- packages/mono-pub/src/utils/deps.ts | 14 +++- 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/packages/mono-pub/src/index.ts b/packages/mono-pub/src/index.ts index 7c96872..43ac0c8 100644 --- a/packages/mono-pub/src/index.ts +++ b/packages/mono-pub/src/index.ts @@ -32,11 +32,12 @@ export default async function publish( plugins: Array, options: MonoPubOptions = {} ) { - const { stdout = process.stdout, stderr = process.stderr, ...restOptions } = options + const { stdout = process.stdout, stderr = process.stderr, ignoreDependencies, ...restOptions } = options const logger = getLogger({ stdout, stderr }) const context: MonoPubContext = { cwd: process.cwd(), env: process.env, + ignoreDependencies: ignoreDependencies || {}, ...restOptions, logger, } diff --git a/packages/mono-pub/src/types/config.ts b/packages/mono-pub/src/types/config.ts index 74096a8..4f90112 100644 --- a/packages/mono-pub/src/types/config.ts +++ b/packages/mono-pub/src/types/config.ts @@ -1,13 +1,23 @@ import type { Signale } from 'signale' +export type IgnoringDependencies = Record> + export type MonoPubContext = { + /** Path to start scanning from, process.cwd() by default */ cwd: string + /** Plugins shared environment, process.env by default */ env: Record + /** Context-specific logger which can be used by plugins to display progress */ logger: Signale<'info' | 'error' | 'log' | 'success'> + /** + * List of dependencies per package, which should not affect its version bump. + * Might be helpful to break cyclic dependencies, so proper release order can be resolved + * */ + ignoreDependencies: IgnoringDependencies } export type MonoPubOptions = Partial< - Pick & { + Pick & { stdout: NodeJS.WriteStream stderr: NodeJS.WriteStream } diff --git a/packages/mono-pub/src/utils/deps.spec.ts b/packages/mono-pub/src/utils/deps.spec.ts index fcfd8bb..cace6b1 100644 --- a/packages/mono-pub/src/utils/deps.spec.ts +++ b/packages/mono-pub/src/utils/deps.spec.ts @@ -6,7 +6,13 @@ import { getDependencies, getExecutionOrder, patchPackageDeps } from './deps' import { getNewVersion, versionToString } from './versions' import type { DirResult } from 'tmp' -import type { BasePackageInfo, LatestPackagesReleases, PackageVersion } from '@/types' +import type { + BasePackageInfo, + LatestPackagesReleases, + PackageVersion, + PackageInfoWithDependencies, + IgnoringDependencies, +} from '@/types' function writePackageJson(obj: Record, packagePath: string, cwd: string) { // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal @@ -149,6 +155,86 @@ describe('Dependencies utils', () => { const batches = getExecutionOrder(Object.values(deps), { batching: true }) expect(batches).toEqual([[pkg1Info, pkg4Info], [pkg2Info], [pkg3Info]]) }) + describe('Should respect ignoreDependencies', () => { + const cycleLength = 6 + let packages: Array = [] + beforeEach(() => { + packages = Array.from({ length: cycleLength }, (_, i) => { + const packageName = `package${i}` + const prevPackageName = `package${(i - 1 + cycleLength) % cycleLength}` + + return { + name: packageName, + location: path.join(tmpDir.name, 'packages', packageName, 'package.json'), + dependsOn: [ + { + name: prevPackageName, + type: Math.random() > 0.5 ? 'dep' : 'devDep', + value: versionToString(getRandomVersion()), + }, + ], + } + }) + }) + + it('Simple cyclic graph test', () => { + expect(() => { + getExecutionOrder(packages) + }).toThrow('The release cannot be done because of cyclic dependencies') + + for (let i = 0; i < cycleLength; i++) { + const ignoreDependencies = { + [packages[i].name]: [packages[(i - 1 + cycleLength) % cycleLength].name], + } + + const expectedNonBatchedOrder = Array.from( + { length: cycleLength }, + (_, idx) => packages[(i + idx) % cycleLength].name + ) + + const nonBatchedOrder = getExecutionOrder(packages, { + ignoreDependencies, + }).map((pkg) => pkg.name) + + expect(nonBatchedOrder).toEqual(expectedNonBatchedOrder) + + const expectedBatchedOrder = expectedNonBatchedOrder.map((name) => [name]) + const batchedOrder = getExecutionOrder(packages, { + ignoreDependencies, + batching: true, + }).map((batch) => batch.map((pkg) => pkg.name)) + + expect(batchedOrder).toEqual(expectedBatchedOrder) + } + }) + it('Advanced test', () => { + const ignoreDependencies: IgnoringDependencies = {} + const firstBatchExpected: Array = [] + const secondBatchExpected: Array = [] + + for (let i = 0; i < cycleLength; i++) { + const packageName = packages[i].name + + if (i % 2 === 0) { + const prevPackageName = packages[(i + cycleLength - 1) % cycleLength].name + ignoreDependencies[packageName] = [prevPackageName] + firstBatchExpected.push(packageName) + } else { + secondBatchExpected.push(packageName) + } + } + + const batchedOrder = getExecutionOrder(packages, { + ignoreDependencies, + batching: true, + }).map((batch) => batch.map((pkg) => pkg.name)) + + expect(batchedOrder).toEqual([ + expect.objectContaining(firstBatchExpected), + expect.objectContaining(secondBatchExpected), + ]) + }) + }) }) describe('patchPackageDeps', () => { it('Should patch package with new version or version from latest release', async () => { diff --git a/packages/mono-pub/src/utils/deps.ts b/packages/mono-pub/src/utils/deps.ts index c78069f..7ad1b8a 100644 --- a/packages/mono-pub/src/utils/deps.ts +++ b/packages/mono-pub/src/utils/deps.ts @@ -3,7 +3,13 @@ import get from 'lodash/get' import set from 'lodash/set' import { versionToString, getVersionCriteria } from '@/utils/versions' -import type { BasePackageInfo, PackageInfoWithDependencies, DependencyInfo, LatestPackagesReleases } from '@/types' +import type { + BasePackageInfo, + PackageInfoWithDependencies, + DependencyInfo, + LatestPackagesReleases, + IgnoringDependencies, +} from '@/types' export async function getDependencies( packages: Array @@ -40,6 +46,7 @@ type ExecutionOrder = T extends true type TaskPlanningOptions = { batching?: T + ignoreDependencies?: IgnoringDependencies } export function getExecutionOrder( @@ -48,12 +55,15 @@ export function getExecutionOrder( ): ExecutionOrder { const batches: Array> = [] const pkgMap = Object.fromEntries(packages.map((pkg) => [pkg.name, pkg])) + const ignoreDependencies = options?.ignoreDependencies || {} const dependencies = new Map>() for (const pkg of packages) { + const packageIgnoreList = ignoreDependencies[pkg.name] || [] + dependencies.set( pkg.name, - pkg.dependsOn.map((dep) => dep.name) + pkg.dependsOn.map((dep) => dep.name).filter((name) => !packageIgnoreList.includes(name)) ) } From 1c2f199b4c3227a7ca768fbcee64f4b54c3788f0 Mon Sep 17 00:00:00 2001 From: SavelevMatthew Date: Tue, 3 Jun 2025 19:56:34 +0500 Subject: [PATCH 2/2] fix(mono-pub): fix semgrep issues and tests --- packages/mono-pub/src/utils/deps.spec.ts | 1 + packages/mono-pub/src/utils/plugins.spec.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/mono-pub/src/utils/deps.spec.ts b/packages/mono-pub/src/utils/deps.spec.ts index cace6b1..c3d5471 100644 --- a/packages/mono-pub/src/utils/deps.spec.ts +++ b/packages/mono-pub/src/utils/deps.spec.ts @@ -165,6 +165,7 @@ describe('Dependencies utils', () => { return { name: packageName, + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal location: path.join(tmpDir.name, 'packages', packageName, 'package.json'), dependsOn: [ { diff --git a/packages/mono-pub/src/utils/plugins.spec.ts b/packages/mono-pub/src/utils/plugins.spec.ts index eb78fb1..93d2e6b 100644 --- a/packages/mono-pub/src/utils/plugins.spec.ts +++ b/packages/mono-pub/src/utils/plugins.spec.ts @@ -173,7 +173,12 @@ describe('CombinedPlugin', () => { packages = Object.values(await getDependencies([pkg3Info, pkg2Info, pkg1Info])) eventLog = [] - ctx = { cwd: process.cwd(), env: {}, logger: getLogger({ stdout: process.stdout, stderr: process.stderr }) } + ctx = { + cwd: process.cwd(), + env: {}, + logger: getLogger({ stdout: process.stdout, stderr: process.stderr }), + ignoreDependencies: {}, + } chain = { getter: getFakeVersionGetter(eventLog), extractor: getFakeExtractor(eventLog),