Skip to content

Commit

Permalink
feat: add concurrency option to run and exec (#1312)
Browse files Browse the repository at this point in the history
To speed things up, add a `concurrency` option to `aegir run` and
`aegir exec` - this will run commands up to the cocurrency limit
while still taking monorepo sibling dependencies into account.

---------

Co-authored-by: Cayman <caymannava@gmail.com>
  • Loading branch information
achingbrain and wemeetagain committed Dec 20, 2023
1 parent c99edc0 commit 4d3c319
Show file tree
Hide file tree
Showing 34 changed files with 463 additions and 19 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
"npm-package-json-lint": "^7.0.0",
"nyc": "^15.1.0",
"p-map": "^6.0.0",
"p-queue": "^7.3.4",
"p-retry": "^6.0.0",
"pascalcase": "^2.0.0",
"path": "^0.12.7",
Expand Down
5 changes: 5 additions & 0 deletions src/cmds/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export default {
type: 'boolean',
describe: 'Prefix output with the package name',
default: userConfig.exec.prefix
},
concurrency: {
type: 'number',
describe: 'How many commands to run at the same time',
default: userConfig.exec.concurrency
}
})
},
Expand Down
5 changes: 5 additions & 0 deletions src/cmds/release-rc.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export default {
type: 'boolean',
describe: 'Prefix output with the package name',
default: userConfig.releaseRc.prefix
},
concurrency: {
type: 'number',
describe: 'How many modules to release at the same time',
default: userConfig.releaseRc.concurrency
}
})
},
Expand Down
5 changes: 5 additions & 0 deletions src/cmds/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export default {
type: 'boolean',
describe: 'Prefix output with the package name',
default: userConfig.run.prefix
},
concurrency: {
type: 'number',
describe: 'How many scripts to run at the same time',
default: userConfig.run.concurrency
}
})
.positional('script', {
Expand Down
2 changes: 2 additions & 0 deletions src/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export default {

console.info(kleur.red(err.stack)) // eslint-disable-line no-console
}
}, {
concurrency: ctx.concurrency
})
}
}
4 changes: 4 additions & 0 deletions src/release-rc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ async function releaseMonorepoRcs (commit, ctx) {
}

versions[project.manifest.name] = `${project.manifest.version}-${commit}`
}, {
concurrency: ctx.concurrency
})

console.info('Will release the following packages:')
Expand Down Expand Up @@ -97,6 +99,8 @@ async function releaseMonorepoRcs (commit, ctx) {
})

console.info('')
}, {
concurrency: ctx.concurrency
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export default {
console.info(kleur.red(err.stack)) // eslint-disable-line no-console
}
}
}, {
concurrency: ctx.concurrency
})
}
}
17 changes: 16 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ interface ReleaseRcOptions {
* Prefix output with the package name
*/
prefix?: boolean

/**
* Release modules in parallel up to this limit
*/
concurrency?: number
}

interface DependencyCheckOptions {
Expand All @@ -365,18 +370,28 @@ interface ExecOptions {
* Prefix output with the package name
*/
prefix?: boolean

/**
* Run commands in parallel up to this limit
*/
concurrency?: number
}

interface RunOptions {
/**
* If false, the command will continue to be run in other packages
* If false, the script will continue to be run in other packages
*/
bail?: boolean

/**
* Prefix output with the package name
*/
prefix?: boolean

/**
* Run scripts in parallel up to this limit
*/
concurrency?: number
}

export type {
Expand Down
50 changes: 32 additions & 18 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import fs from 'fs-extra'
import kleur from 'kleur'
import Listr from 'listr'
import { minimatch } from 'minimatch'
import PQueue from 'p-queue'
import lockfile from 'proper-lockfile'
import { readPackageUpSync } from 'read-pkg-up'
import stripBom from 'strip-bom'
Expand Down Expand Up @@ -313,14 +314,15 @@ export function findBinary (bin) {
* @property {string} dir
* @property {string[]} siblingDependencies
* @property {string[]} dependencies
* @property {boolean} run
*/

/**
* @param {string} projectDir
* @param {(project: Project) => Promise<void>} fn
* @param {object} [opts]
* @param {number} [opts.concurrency]
*/
export async function everyMonorepoProject (projectDir, fn) {
export async function everyMonorepoProject (projectDir, fn, opts) {
const manifest = fs.readJSONSync(path.join(projectDir, 'package.json'))
const workspaces = manifest.workspaces

Expand All @@ -334,27 +336,40 @@ export async function everyMonorepoProject (projectDir, fn) {
checkForCircularDependencies(projects)

/**
* @param {Project} project
* @type {Map<string, number>} Track the number of outstanding dependencies of each project
*
* This is mutated (decremented and deleted) as tasks are run for dependencies
*/
async function run (project) {
if (project.run) {
return
}
const inDegree = new Map()
for (const [name, project] of Object.entries(projects)) {
inDegree.set(name, project.siblingDependencies.length)
}

for (const siblingDep of project.siblingDependencies) {
await run(projects[siblingDep])
}
const queue = new PQueue({
concurrency: opts?.concurrency ?? os.availableParallelism?.() ?? os.cpus().length
})

if (project.run) {
return
while (inDegree.size) {
/** @type {string[]} */
const toRun = []

for (const [name, d] of inDegree) {
// when there are no more dependencies
// the project can be added to the queue
// and removed from the tracker
if (d === 0) {
toRun.push(name)
inDegree.delete(name)
}
}

project.run = true
await fn(project)
}
await Promise.all(toRun.map((name) => queue.add(() => fn(projects[name]))))

for (const project of Object.values(projects)) {
await run(project)
// decrement projects whose dependencies were just run
for (const [name, d] of inDegree) {
const decrement = projects[name].siblingDependencies.filter(dep => toRun.includes(dep)).length
inDegree.set(name, d - decrement)
}
}
}

Expand Down Expand Up @@ -390,7 +405,6 @@ export function parseProjects (projectDir, workspaces) {
manifest: pkg,
dir: subProjectDir,
siblingDependencies: [],
run: false,
dependencies: [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.devDependencies ?? {}),
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/projects/a-large-monorepo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "a-large-monorepo",
"version": "1.0.0",
"description": "",
"homepage": "https://github.com/ipfs/aegir#readme",
"scripts": {
"docs": "aegir docs"
},
"workspaces": [
"packages/*"
]
}
17 changes: 17 additions & 0 deletions test/fixtures/projects/a-large-monorepo/packages/a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "a",
"version": "1.0.0",
"description": "",
"homepage": "https://github.com/ipfs/aegir#readme",
"exports": {
".": {
"import": "./src/index.js"
}
},
"scripts": {
"test": "echo very test"
},
"type": "module",
"author": "",
"license": "ISC"
}
16 changes: 16 additions & 0 deletions test/fixtures/projects/a-large-monorepo/packages/a/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @typedef {import('./types').ExportedButNotInExports} ExportedButNotInExports
*/

/**
* @typedef {object} AnExportedInterface
* @property {() => void} AnExportedInterface.aMethod
*/

export const useHerp = () => {

}

export const useDerp = () => {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ExportedButNotInExports {
aMethod(): void
}
39 changes: 39 additions & 0 deletions test/fixtures/projects/a-large-monorepo/packages/a/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"compilerOptions": {
"strict": true,
// project options
"outDir": "dist",
"allowJs": true,
"checkJs": true,
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2021", "ES2021.Promise", "ES2021.String", "ES2020.BigInt", "DOM", "DOM.Iterable"],
"noEmit": false,
"noEmitOnError": true,
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true,
"incremental": true,
"composite": true,
"isolatedModules": true,
"removeComments": false,
"sourceMap": true,
// module resolution
"esModuleInterop": true,
"moduleResolution": "node",
// linter checks
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
// advanced
"verbatimModuleSyntax": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"stripInternal": true,
"resolveJsonModule": true
},
"include": [
"src"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"entryPoints": ["./src/index.js"]
}
20 changes: 20 additions & 0 deletions test/fixtures/projects/a-large-monorepo/packages/b/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "b",
"version": "1.0.0",
"description": "",
"homepage": "https://github.com/ipfs/aegir#readme",
"exports": {
".": {
"import": "./src/index.js"
}
},
"dependencies": {
"a": "1.0.0"
},
"scripts": {
"test": "echo very test"
},
"type": "module",
"author": "",
"license": "ISC"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @param {string} arg
* @returns {boolean}
*/
export function garply (arg) {
return true
}
18 changes: 18 additions & 0 deletions test/fixtures/projects/a-large-monorepo/packages/b/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @typedef {import('./types.js').ExportedButNotInExports} ExportedButNotInExports
*/

/**
* @typedef {object} AnExportedInterface
* @property {() => void} AnExportedInterface.aMethod
*/

export const useHerp = () => {

}

export const useDerp = () => {

}

export { garply } from './dir/index.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ExportedButNotInExports {
aMethod(): void
}
Loading

0 comments on commit 4d3c319

Please sign in to comment.