diff --git a/packages/isolate/cli.mjs b/packages/isolate/cli.mjs index ab36c59..b327b30 100644 --- a/packages/isolate/cli.mjs +++ b/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) => { diff --git a/packages/isolate/isolate.mjs b/packages/isolate/isolate.mjs index 6cba7c1..d0fd514 100755 --- a/packages/isolate/isolate.mjs +++ b/packages/isolate/isolate.mjs @@ -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() @@ -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) @@ -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() diff --git a/packages/isolate/runner.mjs b/packages/isolate/runner.mjs new file mode 100644 index 0000000..63b1b9a --- /dev/null +++ b/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`) + }) +} diff --git a/packages/isolate/scopes.mjs b/packages/isolate/scopes.mjs new file mode 100644 index 0000000..9fbdf55 --- /dev/null +++ b/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) +}