Skip to content

Commit

Permalink
feat: run lerna scripts based on scope
Browse files Browse the repository at this point in the history
  • Loading branch information
just-paja committed Mar 22, 2023
1 parent 0944202 commit 07e6737
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 15 deletions.
10 changes: 10 additions & 0 deletions packages/isolate/cli.mjs
@@ -1,5 +1,15 @@
import childProcess from 'child_process'

export const log = (message, { padding = 0, newline = true } = {}) => {
if (padding) {
process.stdout.write(Array(padding).fill(' ').join(''))
}
process.stdout.write(message)
if (newline) {
process.stdout.write('\n')
}
}

export async function execute(cmd, options) {
return await new Promise((resolve, reject) => {
childProcess.exec(cmd, options, (err, stdout, stderr) => {
Expand Down
70 changes: 55 additions & 15 deletions packages/isolate/isolate.mjs
Expand Up @@ -9,11 +9,9 @@ import { findRoot } from './paths.mjs'
import { join, relative } from 'path'
import { JobRunner } from './JobRunner.mjs'
import { IsolatedProject } from './IsolatedProject.mjs'

function log(message) {
process.stdout.write(message)
process.stdout.write('\n')
}
import { runScopeCommand } from './runner.mjs'
import { printPackages, printScopes } from './scopes.mjs'
import { log } from './cli.mjs'

async function isolatePackages(packages, options) {
const root = findRoot()
Expand Down Expand Up @@ -49,15 +47,6 @@ function resolvePackages(available, packageList) {
return available
}

async function printPackages() {
const root = findRoot()
const project = new IsolatedProject(root)
const packages = await project.getPackageNames()
for (const pkgName of packages) {
log(pkgName)
}
}

async function cleanPackages() {
const baseDir = findRoot()
const formatPath = (...args) => join(baseDir, ...args)
Expand Down Expand Up @@ -97,7 +86,58 @@ yargs(hideBin(process.argv))
},
async argv => await isolatePackages(argv.packages, {})
)
.command('list', 'list packages', printPackages)
.command(
'run [scope] [pkg]',
'run scripts on project scope',
y => {
y.positional('scope', {
describe: 'project scope, like "@foo" or "foo"',
})
.positional('pkg', {
describe: 'package name',
})
.option('all', {
alias: 'a',
boolean: true,
default: false,
})
.option('script', {
alias: 's',
default: 'dev',
describe: 'run this npm script',
string: true,
})
},
runScopeCommand
)
.command(
'packages',
'work with packages',
y => {
y.option('scope', {
alias: 's',
describe: 'filter packages from this scope',
string: true,
}).option('with-script', {
alias: 'w',
describe: 'filter scopes supporting this npm script',
string: true,
})
},
printPackages
)
.command(
'scopes',
'work with project scopes',
y => {
y.option('with-script', {
alias: 'w',
describe: 'filter scopes supporting this npm script',
string: true,
})
},
printScopes
)
.command('clean', 'clean artifacts', cleanPackages)
.demandCommand()
.parse()
84 changes: 84 additions & 0 deletions packages/isolate/runner.mjs
@@ -0,0 +1,84 @@
import { findRoot } from './paths.mjs'
import { getPackageNames, getScopes } from './scopes.mjs'
import { join } from 'path'
import { log } from './cli.mjs'
import { spawn } from 'child_process'

const serializeFilter = filter => {
return JSON.stringify(filter)
}

const printAvailableScopes = async script => {
const scopes = await getScopes({ withScript: script })
log(`Available "${script}" project scopes:`)
scopes.map(s => log(s, { padding: 2 }))
}

const printAvailablePackages = async (scope, script) => {
const scopes = await getPackageNames({ scope, withScript: script })
log(`Available "${script}" packages${scope ? ` from scope "${scope}"` : ''}:`)
scopes.map(s => log(s, { padding: 2 }))
}

export const runScopeCommand = async ({ all, scope, pkg, script } = {}) => {
if (!pkg && !all) {
return printAvailablePackages(scope, script)
}
if (!scope && !all) {
return printAvailableScopes(script)
}
const packages = await getPackageNames({
exact: pkg,
scope,
withScript: script,
})

if (!packages.length) {
log(
`No packages after filtering for ${serializeFilter({
scope,
pkg,
script,
})}`
)
process.exit(1)
}

log(`Starting ${packages.length} projects\n`)
for (const pack of packages) {
log(`* ${pack}\n`)
}
const baseDir = await findRoot()
const lerna = join(baseDir, 'node_modules', '.bin', 'lerna')
const noPrefix = packages.length <= 1
const lernaArgs = [
'run',
script,
noPrefix && '--no-prefix',
'--stream',
'--parallel',
...packages.map(p => ['--scope', p]).flat(),
].filter(Boolean)
return await runCommand({ baseDir, binary: lerna, args: lernaArgs })
}

const runCommand = async ({ baseDir, binary, args }) => {
const cmd = await spawn(binary, args, {
cwd: baseDir,
env: process.env,
})

const terminate = signal => () => {
cmd.kill(signal)
}

process.on('exit', terminate('SIGINT'))
process.on('SIGTERM', terminate('SIGTERM'))
process.on('SIGINT', terminate('SIGINT'))

cmd.stdout.on('data', data => log(data))
cmd.stderr.on('data', data => process.stderr.write(data))
cmd.on('close', code => {
log(`Lerna terminated with code ${code}\n`)
})
}
57 changes: 57 additions & 0 deletions packages/isolate/scopes.mjs
@@ -0,0 +1,57 @@
import { findRoot } from './paths.mjs'
import { IsolatedProject } from './IsolatedProject.mjs'
import { log } from './cli.mjs'

const extractProjectScope = p => p.name.split('/')[0]
const extractPackageName = p => p.name.split('/')[1]
const filterUnique = (item, index, src) => src.indexOf(item) === index

export const padScope = scope => {
if (!scope) {
return null
}
return scope.startsWith('@') ? scope : `@${scope}`
}

export const getPackages = async ({ exact, scope, withScript } = {}) => {
const root = await findRoot()
const project = new IsolatedProject(root)
let packages = await project.getPackages()
if (scope) {
const projectScope = padScope(scope)
packages = packages.filter(p => p.name.startsWith(`${projectScope}/`))
}
if (withScript) {
packages = packages.filter(p => p.scripts[withScript])
}
if (exact) {
packages = packages.filter(p => extractPackageName(p).startsWith(exact))
}
return packages
}

export const getPackageNames = async ({ noScope = false, ...args } = {}) => {
const packages = await getPackages(args)
if (noScope) {
return packages.map(extractPackageName)
}
return packages.map(p => p.name)
}

export const getScopes = async args => {
const packages = await getPackages(args)
return packages.map(extractProjectScope).filter(filterUnique)
}

export const printScopes = async args => {
const scopes = await getScopes(args)
scopes.map(log)
}

export const printPackages = async args => {
const packages = await getPackageNames({
...args,
noScope: Boolean(args.scope),
})
packages.map(log)
}

0 comments on commit 07e6737

Please sign in to comment.