Skip to content

Commit eb4cc7b

Browse files
feat(mono-pub)!: ADR-1 split prepare into prepareAll and prepareSingle (#30)
* feat(mono-pub): add getExecutionOrder util * feat(mono-pub)!: ADR-1 split prepare into prepareAll and prepareSingle BREAKING-CHANGE: plugin chain behaviour change, prepare step is removed and replaced by prepareSingle and prepareAll * refactor: changed docs and fixed tests * fix: PR review fixes
1 parent 2a6f706 commit eb4cc7b

File tree

10 files changed

+283
-68
lines changed

10 files changed

+283
-68
lines changed

bin/publish.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ const git = require('@mono-pub/git')
44
const github = require('@mono-pub/github')
55
const npm = require('@mono-pub/npm')
66
const commitAnalyzer = require('@mono-pub/commit-analyzer')
7+
const { getExecutionOrder } = require('mono-pub/utils')
78

9+
/** @type {import('mono-pub').MonoPubPlugin} */
810
const builder = {
911
name: '@mono-pub/local-builder',
10-
async prepare(_, ctx) {
11-
await execa('yarn', ['build'], { cwd: ctx.cwd })
12+
async prepareAll({ foundPackages }, ctx) {
13+
const batches = getExecutionOrder(foundPackages, { batching: true })
14+
for (const batch of batches) {
15+
await execa('yarn', ['build', ...batch.map((pkg) => `--filter=${pkg.name}`)], { cwd: ctx.cwd })
16+
}
1217
},
1318
}
1419

packages/mono-pub/README.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ The mono-pub plugins handle this out of the box (See [`@mono-pub/github`](https:
6767
We divide the publishing process into several steps,
6868
allowing you to control most of the process yourself through pre-made plugins or using your own.
6969

70-
| Step | Description |
71-
|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
72-
| Setup | Sets up the plugin, checks all the conditions necessary for work. |
73-
| Get Last Release | Obtains latest released version for each package (Usually from tags analysis) |
74-
| Extract commits | Extracts commits relevant to specific package that happened since last release |
75-
| Get Release Type | Based on the received data and updated dependencies, calculates the release type according to semantic versioning |
76-
| Prepare | Performs any action that prepares all packages for publication after all versions are patched (You can build packages here, omit devDeps, you name it). |
77-
| Publish | Publishes individual package to destination |
78-
| Post-Publish | Performs any actions on successful publishing (You can generate new tag, publish release notes, send web hooks and every other side effect here) |
70+
| Step | Description |
71+
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
72+
| Setup | Sets up the plugin, checks all the conditions necessary for work. |
73+
| Get Last Release | Obtains latest released version for each package (Usually from tags analysis) |
74+
| Extract commits | Extracts commits relevant to specific package that happened since last release |
75+
| Get Release Type | Based on the received data and updated dependencies, calculates the release type according to semantic versioning |
76+
| Prepare | Performs any action that prepares all or individual packages for publication after all versions are patched (You can build packages here, omit devDeps, you name it). |
77+
| Publish | Publishes individual package to destination |
78+
| Post-Publish | Performs any actions on successful publishing (You can generate new tag, publish release notes, send web hooks and every other side effect here) |
7979

8080
> All plugins must implement MonoPubPlugin interface, containing plugin name and implementation of one or more specified steps.
8181
> Detailed interface description can be found [here](https://github.com/SavelevMatthew/mono-pub/blob/main/packages/mono-pub/src/types/plugins.ts)
@@ -114,10 +114,12 @@ const git = require('@mono-pub/git')
114114
const npm = require('@mono-pub/npm')
115115
const commitAnalyzer = require('@mono-pub/commit-analyzer')
116116

117+
118+
/** @type {import('mono-pub').MonoPubPlugin} */
117119
const builder = {
118120
name: '@mono-pub/local-builder',
119-
async prepare(_, ctx) {
120-
await execa('yarn', ['build'], { cwd: ctx.cwd })
121+
async prepareSingle({ targetPackage }) {
122+
await execa('yarn', ['build'], { cwd: targetPackage.location })
121123
},
122124
}
123125

@@ -161,11 +163,16 @@ const git = require('@mono-pub/git')
161163
const github = require('@mono-pub/github')
162164
const npm = require('@mono-pub/npm')
163165
const commitAnalyzer = require('@mono-pub/commit-analyzer')
166+
const { getExecutionOrder } = require('mono-pub/utils')
164167
168+
/** @type {import('mono-pub').MonoPubPlugin} */
165169
const builder = {
166-
name: '@mono-pub/local-builder',
167-
async prepare(_, ctx) {
168-
await execa('yarn', ['build'], { cwd: ctx.cwd })
170+
name: '@mono-pub/local-turborepo-builder',
171+
async prepareAll({ foundPackages }, ctx) {
172+
const batches = getExecutionOrder(foundPackages, { batching: true })
173+
for (const batch of batches) {
174+
await execa('yarn', ['build', ...batch.map(pkg => `--filter=${pkg.name}`)], { cwd: ctx.cwd })
175+
}
169176
},
170177
}
171178

packages/mono-pub/src/index.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import isEqual from 'lodash/isEqual'
33
import { getAllPackages } from '@/utils/path'
44
import getLogger from '@/logger'
55
import { CombinedPlugin } from '@/utils/plugins'
6-
import { getDependencies, getReleaseOrder, patchPackageDeps } from '@/utils/deps'
7-
import { getNewVersion, versionToString } from '@/utils/versions'
6+
import { getDependencies, getExecutionOrder, patchPackageDeps } from '@/utils/deps'
7+
import { getNewVersion, versionToString, isPackageChanged } from '@/utils/versions'
88

99
import type {
1010
MonoPubPlugin,
@@ -66,19 +66,19 @@ export default async function publish(
6666
)
6767
logger.log('Calculating release order based on packages dependencies and devDependencies...')
6868
let packagesWithDeps: Record<string, PackageInfoWithDependencies> = {}
69-
let releaseOrder: Array<string> = []
69+
let releaseOrder: Array<BasePackageInfo> = []
7070

7171
try {
7272
packagesWithDeps = await getDependencies(packages)
73-
releaseOrder = getReleaseOrder(packagesWithDeps)
73+
releaseOrder = getExecutionOrder(Object.values(packagesWithDeps))
7474
} catch (err) {
7575
if (err instanceof Error) {
7676
logger.error(err.message)
7777
}
7878
throw err
7979
}
8080

81-
logger.success(`Packages release order: [${releaseOrder.map((pkg) => `"${pkg}"`).join(', ')}]`)
81+
logger.success(`Packages release order: [${releaseOrder.map((pkg) => `"${pkg.name}"`).join(', ')}]`)
8282

8383
logger.success(
8484
`Found ${plugins.length} plugins to form release chain: [${plugins
@@ -107,7 +107,7 @@ export default async function publish(
107107
const releaseTypes: Record<string, ReleaseType> = {}
108108
const newVersions: Record<string, LatestReleasedVersion> = {}
109109

110-
for (const pkgName of releaseOrder) {
110+
for (const { name: pkgName } of releaseOrder) {
111111
const scopedLogger = scopedContexts[pkgName].logger
112112

113113
const latestRelease = get(latestReleases, pkgName, null)
@@ -150,13 +150,28 @@ export default async function publish(
150150
await patchPackageDeps(pkg, newVersions, latestReleases)
151151
}
152152

153-
await releaseChain.prepare(packages, context)
153+
const foundPackages = Object.values(packagesWithDeps)
154+
const changedPackages = foundPackages.filter(({ name }) => {
155+
const newVersion = newVersions[name]
156+
const releaseType = releaseTypes[name]
157+
const oldVersion = latestReleases[name]
158+
159+
return isPackageChanged(newVersion, oldVersion, releaseType)
160+
})
161+
162+
await releaseChain.prepareAll(
163+
{
164+
foundPackages,
165+
changedPackages,
166+
},
167+
context
168+
)
154169

155-
for (const packageName of releaseOrder) {
170+
for (const { name: packageName } of releaseOrder) {
156171
const newVersion = newVersions[packageName]
157172
const releaseType = releaseTypes[packageName]
158173
const oldVersion = latestReleases[packageName]
159-
if (releaseType === 'none' || !newVersion || isEqual(newVersion, oldVersion)) {
174+
if (!isPackageChanged(newVersion, oldVersion, releaseType)) {
160175
continue
161176
}
162177

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@ import type {
66
PackageInfoWithLatestRelease,
77
ReleaseType,
88
ReleasedPackageInfo,
9+
PackageInfoWithDependencies,
910
} from './packages'
1011

1112
type Awaitable<T> = T | Promise<T>
1213

14+
export type PrepareAllInfo = {
15+
/** All packages found by filter */
16+
foundPackages: Array<PackageInfoWithDependencies>
17+
/** Packages, which will actually be published */
18+
changedPackages: Array<PackageInfoWithDependencies>
19+
}
20+
21+
export type PrepareSingleInfo = PrepareAllInfo & {
22+
targetPackage: BasePackageInfo
23+
}
24+
1325
export interface MonoPubPlugin {
1426
/**
1527
* Name of plugin
@@ -51,12 +63,29 @@ export interface MonoPubPlugin {
5163

5264
/**
5365
* Prepares packages for publishing. Usually includes build process.
66+
* Most suitable for scenarios in monorepos with existing orchestrator, such as TurboRepo,
67+
* where multiple packages can be built at once.
5468
* NOTE: This step is triggered once for all packages, not for each package individually
55-
* @param packages {Array<BasePackageInfo>} List of packages containing its name and location (absolute path to package.json)
69+
* NOTE: You can get execution order with or without batches by using getExecutionOrder util from mono-pub/utils
70+
* @param info {PrepareAllInfo} Information about all packages found, as well as packages that will be published immediately.
71+
* List of packages containing its name and location (absolute path to package.json)
72+
* @param ctx {MonoPubContext} Execution context. Used to obtain cwd, env and logger
73+
* @return {void}
74+
*/
75+
prepareAll?(info: PrepareAllInfo, ctx: MonoPubContext): Awaitable<void>
76+
77+
/**
78+
* Prepares individual package for publishing. Usually includes build process.
79+
* Most suitable for scenarios in monorepos without, where you can just execute "yarn build" one by one.
80+
* Order of execution is controlled for mono-pub, so you can ensure,
81+
* that all package dependencies are built before package itself
82+
* NOTE: This step is triggered once for all packages, not for each package individually
83+
* @param info {PrepareSingleInfo} Information about all packages found, packages that will be directly published,
84+
* and the current package being prepared
5685
* @param ctx {MonoPubContext} Execution context. Used to obtain cwd, env and logger
5786
* @return {void}
5887
*/
59-
prepare?(packages: Array<BasePackageInfo>, ctx: MonoPubContext): Awaitable<void>
88+
prepareSingle?(info: PrepareSingleInfo, ctx: MonoPubContext): Awaitable<void>
6089

6190
/**
6291
* Publishes package to a specific registry

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs'
22
import path from 'path'
33
import { dirSync } from 'tmp'
44
import { faker } from '@faker-js/faker'
5-
import { getDependencies, getReleaseOrder, patchPackageDeps } from './deps'
5+
import { getDependencies, getExecutionOrder, patchPackageDeps } from './deps'
66
import { getNewVersion, versionToString } from './versions'
77

88
import type { DirResult } from 'tmp'
@@ -28,6 +28,7 @@ describe('Dependencies utils', () => {
2828
let pkg1Info: BasePackageInfo
2929
let pkg2Info: BasePackageInfo
3030
let pkg3Info: BasePackageInfo
31+
let pkg4Info: BasePackageInfo
3132
beforeEach(() => {
3233
tmpDir = dirSync({ unsafeCleanup: true })
3334
pkg1Info = {
@@ -43,7 +44,13 @@ describe('Dependencies utils', () => {
4344
location: path.join(tmpDir.name, 'packages/pkg3', 'package.json'),
4445
}
4546

47+
pkg4Info = {
48+
name: '@scope/pkg4',
49+
location: path.join(tmpDir.name, 'packages/pkg4', 'package.json'),
50+
}
51+
4652
writePackageJson({ name: pkg1Info.name, version: '0.0.0-development' }, 'packages/pkg1', tmpDir.name)
53+
writePackageJson({ name: pkg4Info.name, version: '0.0.0-development' }, 'packages/pkg4', tmpDir.name)
4754
writePackageJson(
4855
{
4956
name: pkg2Info.name,
@@ -78,13 +85,17 @@ describe('Dependencies utils', () => {
7885
})
7986
describe('getDependencies', () => {
8087
it('Should determine deps of packages correctly', async () => {
81-
const deps = await getDependencies([pkg1Info, pkg2Info, pkg3Info])
88+
const deps = await getDependencies([pkg1Info, pkg2Info, pkg3Info, pkg4Info])
8289
expect(deps).toEqual(
8390
expect.objectContaining({
8491
[pkg1Info.name]: {
8592
...pkg1Info,
8693
dependsOn: [],
8794
},
95+
[pkg4Info.name]: {
96+
...pkg4Info,
97+
dependsOn: [],
98+
},
8899
[pkg2Info.name]: {
89100
...pkg2Info,
90101
dependsOn: expect.arrayContaining([{ name: pkg1Info.name, type: 'dep', value: 'workspace:^' }]),
@@ -99,6 +110,7 @@ describe('Dependencies utils', () => {
99110
})
100111
)
101112
expect(deps[pkg1Info.name].dependsOn).toHaveLength(0)
113+
expect(deps[pkg4Info.name].dependsOn).toHaveLength(0)
102114
expect(deps[pkg2Info.name].dependsOn).toHaveLength(1)
103115
expect(deps[pkg3Info.name].dependsOn).toHaveLength(2)
104116
})
@@ -118,20 +130,25 @@ describe('Dependencies utils', () => {
118130
)
119131
})
120132
})
121-
describe('getReleaseOrder', () => {
133+
describe('getExecutionOrder', () => {
122134
it('Should determine release order from leafs to root of deps tree', async () => {
123135
const deps = await getDependencies([pkg3Info, pkg2Info, pkg1Info])
124-
const order = getReleaseOrder(deps)
125-
expect(order).toEqual([pkg1Info.name, pkg2Info.name, pkg3Info.name])
136+
const order = getExecutionOrder(Object.values(deps))
137+
expect(order).toEqual([pkg1Info, pkg2Info, pkg3Info])
126138
})
127139
it('Should throw if cyclic deps found', async () => {
128140
const deps = await getDependencies([pkg3Info, pkg2Info, pkg1Info])
129141
deps[pkg1Info.name].dependsOn.push({ name: pkg3Info.name, value: '1.0.0', type: 'dep' })
130142

131143
expect(() => {
132-
getReleaseOrder(deps)
144+
getExecutionOrder(Object.values(deps))
133145
}).toThrow('The release cannot be done because of cyclic dependencies')
134146
})
147+
it('Can batch tasks, which can be executed together', async () => {
148+
const deps = await getDependencies([pkg3Info, pkg2Info, pkg1Info, pkg4Info])
149+
const batches = getExecutionOrder(Object.values(deps), { batching: true })
150+
expect(batches).toEqual([[pkg1Info, pkg4Info], [pkg2Info], [pkg3Info]])
151+
})
135152
})
136153
describe('patchPackageDeps', () => {
137154
it('Should patch package with new version or version from latest release', async () => {

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

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,39 +34,57 @@ export async function getDependencies(
3434
return result
3535
}
3636

37-
export function getReleaseOrder(packages: Record<string, PackageInfoWithDependencies>): Array<string> {
38-
const order: Array<string> = []
37+
type ExecutionOrder<T extends boolean | undefined> = T extends true
38+
? Array<Array<BasePackageInfo>>
39+
: Array<BasePackageInfo>
3940

40-
const deps = new Map<string, Array<string>>()
41-
for (const pkg of Object.values(packages)) {
42-
deps.set(
41+
type TaskPlanningOptions<T extends boolean | undefined> = {
42+
batching?: T
43+
}
44+
45+
export function getExecutionOrder<T extends boolean | undefined = undefined>(
46+
packages: Array<PackageInfoWithDependencies>,
47+
options?: TaskPlanningOptions<T>
48+
): ExecutionOrder<T> {
49+
const batches: Array<Array<BasePackageInfo>> = []
50+
const pkgMap = Object.fromEntries(packages.map((pkg) => [pkg.name, pkg]))
51+
52+
const dependencies = new Map<string, Array<string>>()
53+
for (const pkg of packages) {
54+
dependencies.set(
4355
pkg.name,
4456
pkg.dependsOn.map((dep) => dep.name)
4557
)
4658
}
4759

48-
while (deps.size > 0) {
49-
const toRelease: Array<string> = []
50-
for (const [key, value] of deps) {
51-
if (value.length === 0) {
52-
toRelease.push(key)
53-
deps.delete(key)
60+
while (dependencies.size > 0) {
61+
const batch: Array<BasePackageInfo> = []
62+
for (const [pkgName, pkgDeps] of dependencies) {
63+
if (pkgDeps.length === 0) {
64+
batch.push({ name: pkgName, location: pkgMap[pkgName].location })
65+
dependencies.delete(pkgName)
5466
}
5567
}
56-
if (toRelease.length === 0) {
68+
69+
if (batch.length === 0) {
5770
throw new Error('The release cannot be done because of cyclic dependencies')
58-
} else {
59-
order.push(...toRelease)
60-
for (const [key, value] of deps) {
61-
deps.set(
62-
key,
63-
value.filter((pkg) => !toRelease.includes(pkg))
64-
)
65-
}
6671
}
72+
73+
batches.push(batch)
74+
const includedPackages = batch.map((pkg) => pkg.name)
75+
for (const [pkgName, pkgDeps] of dependencies) {
76+
dependencies.set(
77+
pkgName,
78+
pkgDeps.filter((depName) => !includedPackages.includes(depName))
79+
)
80+
}
81+
}
82+
83+
if (options?.batching) {
84+
return batches as ExecutionOrder<T>
6785
}
6886

69-
return order
87+
return batches.flat() as ExecutionOrder<T>
7088
}
7189

7290
export async function patchPackageDeps(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export { getExecutionOrder } from './deps'
12
export { versionToString } from './versions'

0 commit comments

Comments
 (0)