diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index 70bf71b..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,9 +0,0 @@ -#! /usr/bin/env node - -import { please } from '.'; - -please(`please ${process.argv.slice(2).join(' ')}`); - -process.on('SIGINT', function () { - process.exit(0); -}); diff --git a/src/colors.ts b/src/colors.ts deleted file mode 100644 index e2a63ef..0000000 --- a/src/colors.ts +++ /dev/null @@ -1,17 +0,0 @@ -const colors = [ - 'green.bold', - 'yellow.bold', - 'blue.bold', - 'magenta.bold', - 'cyan.bold', - 'white.bold', -]; - -export function getNColors(n: number) { - if (n < colors.length) { - return colors.slice(0, n); - } - return Array.from({ length: colors.length }, () => colors) - .flat() - .slice(0, n); -} diff --git a/src/const.ts b/src/const.ts deleted file mode 100644 index 288b828..0000000 --- a/src/const.ts +++ /dev/null @@ -1,13 +0,0 @@ -const HELP_FLAGS = ['-h', '--h', '-help', '--help']; - -const TARGET_EXAMPLE_PACKAGE_SCRIPTS = [ - 'dev', - 'run', - 'start', - 'preview', - 'tsc', - 'lint', - 'clean', -]; - -export { HELP_FLAGS, TARGET_EXAMPLE_PACKAGE_SCRIPTS }; diff --git a/src/help.ts b/src/help.ts deleted file mode 100644 index 891427c..0000000 --- a/src/help.ts +++ /dev/null @@ -1,56 +0,0 @@ -import chalk from 'chalk'; - -export function helpText(examples?: string[]): string { - let example1 = 'dev:@scope/package-1 start:@scope/package-2'; - let example2 = 'dev:@scope/package-1 start:@scope/package-2,@scope/package-3'; - let example3 = - 'dev:@scope/package-1,@scope/package-3 start:@scope/package-2,@scope/package-3'; - - if (examples && examples.length >= 3) { - [example1, example2, example3] = examples; - } - - const [arg1, arg2] = example1.split(' '); - - return ` - ${chalk.bold.white('A CLI for developing in monorepos - not building them')} - - ${chalk.bold.white.underline('Usage:\n')} - ${chalk.bold.gray('$')}${chalk.bold.white(` please ${example1}\n`)} - ${chalk.bold.gray('$')}${chalk.bold.white(` please ${example2}\n`)} - ${chalk.bold.gray('$')}${chalk.bold.white(` please ${example3}\n`)} - ${chalk.bold.white.underline('Advanced Usage:\n')} - ${chalk.bold.gray('$')}${chalk.bold.white(` please dev:{@scope/*}\n`)} - ${chalk.bold.gray('$')}${chalk.bold.white( - ` please dev:{@scope/*} run:package-1,package-2\n` - )} - - ${chalk.bold.white.underline('Details:\n')} - ${chalk.bold.white( - `Let's refer to to the yellow text below - - ${chalk.bold.gray('$')}${chalk.white(` please ${chalk.yellow(arg2)}`)} - - as an "argument" to please.\n` - )} - ${chalk.bold.white( - `Arguments to please take the following form\n - ${chalk.bold.magenta('LHS')}${chalk.bold.gray(':')}${chalk.bold.magenta( - 'RHS' - )} - ` - )} - ${chalk.bold.white( - `where the ${chalk.bold.magenta( - 'LHS' - )} is a package script and the ${chalk.bold.magenta( - 'RHS' - )} is a comma-separated list of packages - in your monorepo to run the LHS package script in. Packages on the RHS can - contain wildcards, but must be escaped with brackets if so. We use micromatch - (https://github.com/micromatch/micromatch) for matching. Refer to their documentation - for questions on matching behavior and available functionality. - ` - )} - `; -} diff --git a/src/index.test.ts b/src/index.test.ts deleted file mode 100644 index f6c67a7..0000000 --- a/src/index.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -test('please', () => { - expect(true).toBe(true); -}); -export {}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 81241c2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - getPackages, - collectMonorepoContextualExamples, - findWorkspaceRoot, -} from './pnpm'; - -import { HELP_FLAGS } from './const'; - -import { getNColors } from './colors'; - -import { helpText } from './help'; - -import { help, parse } from './parse'; - -import { findMatchingPackages } from './match'; - -import concurrently from 'concurrently'; - -type Name = `${string}`; -type Command = `pnpm --filter ${Name} run ${string}`; - -export async function please(args: string) { - const packages = await getPackages(); - const workspaceRoot = await findWorkspaceRoot(); - - const helpArgs = help(args); - - if (helpArgs.some((arg) => HELP_FLAGS.includes(arg))) { - console.log(helpText(collectMonorepoContextualExamples(packages))); - process.exit(0); - } - - const commands: [Name, Command][] = []; - - const cliArgs = parse(args); - - cliArgs.forEach(([packageScript, targetPackages]) => { - findMatchingPackages(packages, targetPackages).forEach( - (matchingPackage) => { - const { scripts = {}, name: packageName } = matchingPackage.manifest; - - if (!scripts[packageScript]) { - throw new Error( - `MISSING_PACKAGE_SCRIPT: "${packageName}" has no ${packageScript} script` - ); - } - - commands.push([ - `${packageName}:${packageScript}`, - `pnpm --filter ${packageName} run ${packageScript}`, - ]); - } - ); - }); - - const { length } = commands - .map((c) => c[0]) - .reduce((a, b) => (a.length > b.length ? a : b)); - - const { result } = concurrently( - commands.map(([name, command]) => ({ - command, - name: name.padEnd(length), - })), - { - cwd: workspaceRoot, - prefix: '{name}', - prefixColors: getNColors(commands.length), - } - ); - - result.then( - () => process.exit(0), - () => process.exit(1) - ); -} diff --git a/src/match.ts b/src/match.ts deleted file mode 100644 index ef27189..0000000 --- a/src/match.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Project } from '@pnpm/find-workspace-packages'; -import micromatch from 'micromatch'; -export function findMatchingPackages( - packages: Project[], - targetPackages: string[] -) { - const packageNames = packages.map(({ manifest }) => manifest.name || ''); - - let matches: string[] = []; - - targetPackages.forEach((targetPackage) => { - if (targetPackage.startsWith('{') && targetPackage.endsWith('}')) { - matches = [ - ...matches, - ...micromatch(packageNames, [targetPackage.slice(1, -1)]), - ]; - } else { - const match = packageNames.find((name) => name === targetPackage); - - if (!match) { - throw new Error( - `UNKNOWN_PACKAGE: cannot locate package "${targetPackage}" in monorepo.` - ); - } - - matches.push(match); - } - }); - //@ts-ignore - const uniqueMatches = [...new Set(matches)]; - - return packages.filter( - (pkg) => uniqueMatches.indexOf(pkg.manifest.name) > -1 - ); -} diff --git a/src/parse.test.ts b/src/parse.test.ts deleted file mode 100644 index 9580d43..0000000 --- a/src/parse.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { parse, help } from './parse'; - -[ - { - describe: 'Clean/simple input/usages', - cases: [ - { - it: 'can parse single-command single-package arguments', - in: 'please a:@a/a', - out: [['a', ['@a/a']]], - }, - { - it: 'can parse single kebab-case command + single-package arguments', - in: 'please a-a:@a/a', - out: [['a-a', ['@a/a']]], - }, - { - it: 'can parse single kebab-case command + single-package arguments', - in: 'please a-a:@a/a-a', - out: [['a-a', ['@a/a-a']]], - }, - { - it: 'can parse single snake-case command + single-package arguments', - in: 'please a_a:@a/a_a', - out: [['a_a', ['@a/a_a']]], - }, - { - it: 'can parse multi-single-package-commands', - in: 'please a:@a/a b:@b/b', - out: [ - ['a', ['@a/a']], - ['b', ['@b/b']], - ], - }, - { - it: 'can parse single-command + multi-package', - in: 'please a:@a/a,@b/b', - out: [['a', ['@a/a', '@b/b']]], - }, - { - it: 'can parse multi-command + multi-package', - in: 'please a:@a/a,@a/b b:@b/a,@b/b', - out: [ - ['a', ['@a/a', '@a/b']], - ['b', ['@b/a', '@b/b']], - ], - }, - ], - }, - { - describe: 'Wildcard packages', - cases: [ - { - it: 'can parse single-command single-package w/ wildcards', - in: 'please a:{@a/*}', - out: [['a', ['{@a/*}']]], - }, - { - it: 'can parse single-command multi-package w/ wildcards', - in: 'please a:{@a/*},{@b/*}', - out: [['a', ['{@a/*}', '{@b/*}']]], - }, - { - it: 'can parse single-command multi-package w/ wildcards', - in: 'please a:{@a/*},{@b/*},{@*/*}', - out: [['a', ['{@a/*}', '{@b/*}', '{@*/*}']]], - }, - { - it: 'can parse multi-command multi-package w/ wildcards', - in: 'please a:{@a/*},{@b/*},{@*/*} b:{@c/*},{@d/*},{@*/*}', - out: [ - ['a', ['{@a/*}', '{@b/*}', '{@*/*}']], - ['b', ['{@c/*}', '{@d/*}', '{@*/*}']], - ], - }, - { - it: 'can parse multi-command multi-package w/ wildcards and literal packages', - in: 'please a:{@a/*},@joe/package,{@*/*} b:{@c/*},@meeko/package,{@*/*}', - out: [ - ['a', ['{@a/*}', '@joe/package', '{@*/*}']], - ['b', ['{@c/*}', '@meeko/package', '{@*/*}']], - ], - }, - ], - }, - { - describe: 'Less-than-clean input', - cases: [ - { - it: 'can parse w/ whitespace after command', - in: 'please a: @a/a', - out: [['a', ['@a/a']]], - }, - { - it: 'can parse w/ whitespace command delimiter', - in: 'please a : @a/a', - out: [['a', ['@a/a']]], - }, - { - it: 'can parse w/ whitespace between packages', - in: 'please a: @a/a , @b/b ,@c/c', - out: [['a', ['@a/a', '@b/b', '@c/c']]], - }, - { - it: 'can parse w/ newlines after command && between packages', - in: 'please a:\n@a/a,\n@b/b,\n@c/c', - out: [['a', ['@a/a', '@b/b', '@c/c']]], - }, - ], - }, -].forEach((group) => { - describe('Please CLI Arg Parser', () => { - describe(group.describe, () => { - group?.cases?.forEach((example) => { - it(example.it, () => { - expect(example.out).toEqual(parse(example.in)); - }); - }); - }); - }); -}); - -describe('Please CLI - Flag Parsing', () => { - it('should parse syntactically valid help flags', () => { - expect(help(' --help ---children -c -f ')).toEqual([ - '--help', - '-c', - '-f', - ]); - }); - it('should parse syntactically valid help flags', () => { - expect(help(' -a -b -c --d --e --f')).toEqual([ - '-a', - '-b', - '-c', - '--d', - '--e', - '--f', - ]); - }); -}); diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index d73fcd1..0000000 --- a/src/parse.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as parser from './parser'; - -export function parse(input: string): [string, string[]][] { - let parsed; - - try { - parsed = parser.parse(input); - } catch (e) { - console.log('No valid (read parseable) arguments to please found'); - process.exit(1); - } - - const commands = parsed.children[0].children; - //@ts-ignore - return commands.reduce((result, command) => { - return [ - ...result, - [command.value, command.children.children.map((c: any) => c.value)], - ]; - }, []); -} - -const FLAG_PARSER = /(?:\s+)(-(-)?\w+)(?:\s*)/; -export function help(args: string) { - let input = args; - - const help: string[] = []; - - while (input) { - const matched = input.match(FLAG_PARSER) || []; - - const [match, flag] = matched; - - if (!match) { - break; - } - - help.push(flag); - - //@ts-ignore - input = input.slice(matched.index + match.length - 1); - } - - return help; -} diff --git a/src/parser/ast.js b/src/parser/ast.js deleted file mode 100644 index 250f0cf..0000000 --- a/src/parser/ast.js +++ /dev/null @@ -1,31 +0,0 @@ -function Node(ast, type, value, children) { - this.type = type; - this.value = value; - if (ast) { - this.ast = true; - } - if (children) { - this.children = children; - } -} - -function node(isASTNode, type, value, children) { - return new Node(isASTNode, type, value, children); -} - -const types = { - ARGUMENT: 'argument', - PACKAGE_LIST: 'package-list', - COMMAND: 'command', - SYM: 'symbol', - PACKAGE_LITERAL: 'package-literal', - PACKAGE_GLOB: 'package-glob', - ROOT: 'root', - WHITESPACE: 'whitespace', -}; - -module.exports = { - node, - Node, - types, -}; diff --git a/src/parser/parser.pegjs b/src/parser/parser.pegjs deleted file mode 100644 index 79e4569..0000000 --- a/src/parser/parser.pegjs +++ /dev/null @@ -1,57 +0,0 @@ -// JS Utilities - -{ - var ast = require('./ast') - var types = ast.types -} - -please - = a:identifier b: (_ argument)* { - return ast.node(1, types.ROOT, text(), [ - ast.node(1, types.COMMAND, 'please', b.map(b => b[1])) - ]) - } - -argument - = a:command b: (_ colon) c: (_ pkg_list) { - return ast.node(1, types.COMMAND, a.value, c[1]); - } - -pkg_list - = a:pkg_expr b: (_ lsep _ pkg_expr)* { - return ast.node(1, types.PACKAGE_LIST, text(), [a, ...b.map(b => b[3])]); - } - - - -lsep "list_separator" = a:',' { return ast.node(0, types.SYM, a); } -colon "colon" = a:':' { return ast.node(0, types.SYM, a); } - -pkg_expr - = a:'@' scope:[a-z0-9-~][a-z0-9-._~]* b: '/' pkg:[a-z0-9-~][a-z0-9-._~]* { - return ast.node(1, types.PACKAGE_LITERAL, text()) - } - / pkg:[a-z0-9-~][a-z0-9-._~]* { - return ast.node(1, types.PACKAGE_LITERAL, text()) - } - / pkg_glob - -pkg_glob - = a:'{' id:('\\}'/[^\}])* b:'}' { - return ast.node(1, types.PACKAGE_GLOB, text()) - } - -// Terminals - -command -= a:[A-Za-z_] b:[A-Za-z_0-9-]* { - return ast.node(1, types.COMMAND, text()); - } - -identifier -= a:[A-Za-z_] b:[A-Za-z_0-9]* { - return ast.node(1, types.ID, text()); - } - -_ "whitespace" - = a:[ \r\n\t]* { return ast.node(0, types.WHITESPACE, ' '); } diff --git a/src/pnpm.ts b/src/pnpm.ts deleted file mode 100644 index 84600cb..0000000 --- a/src/pnpm.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { findWorkspacePackages, Project } from '@pnpm/find-workspace-packages'; -import { findWorkspaceDir } from '@pnpm/find-workspace-dir'; - -import { TARGET_EXAMPLE_PACKAGE_SCRIPTS } from './const'; - -export async function findWorkspaceRoot() { - return await findWorkspaceDir(process.cwd()); -} - -export async function getPackages() { - const workspaceDir = await findWorkspaceRoot(); - if (workspaceDir == undefined) { - throw new Error('Unable to locate pnpm workspace root'); - } - const workspacePacakges = await findWorkspacePackages( - (await findWorkspaceDir(process.cwd())) || '' - ); - - return workspacePacakges.slice(1); -} - -export function getNRandomPackages(packages: Project[], n: number): Project[] { - return [...packages].sort(() => 0.5 - Math.random()).slice(0, n); -} - -export function filterPackagsByPackageScript( - packages: Project[], - packageScript: string -) { - return packages.filter((pkg) => { - const scripts = pkg.manifest.scripts || {}; - - if (scripts[packageScript]) { - return true; - } - return false; - }); -} - -export function groupByPackageScripts(packages: Project[]) { - return packages.reduce>((rollup, pkg) => { - const scripts = pkg.manifest.scripts || {}; - - Object.keys(scripts).forEach((script) => { - if (!rollup[script]) { - rollup[script] = 1; - } else { - rollup[script] = rollup[script] + 1; - } - }); - - return rollup; - }, {}); -} - -export function collectMonorepoContextualExamples(packages: Project[]) { - const packagesGroupedByScriptName = groupByPackageScripts(packages); - - const scriptNames = Object.entries(packagesGroupedByScriptName).sort( - (a, b) => b[1] - a[1] - ); - - const examplePackages = TARGET_EXAMPLE_PACKAGE_SCRIPTS.reduce< - Record - >((examples, targetPackageScript) => { - const hasTargeScript = scriptNames.find( - ([name]) => name === targetPackageScript - ); - const hasGreaterThanTwo = - packagesGroupedByScriptName[targetPackageScript] >= 2; - - if (!hasTargeScript || !hasGreaterThanTwo) { - return examples; - } - - examples[targetPackageScript] = getNRandomPackages( - filterPackagsByPackageScript(packages, targetPackageScript), - 2 - ); - - return examples; - }, {}); - - if (Object.keys(examplePackages).length >= 2) { - const examples = Object.keys(examplePackages).slice(0, 2); - - return [ - `${examples[0]}:${examplePackages[examples[0]][0].manifest.name} ${ - examples[1] - }:${examplePackages[examples[1]][0].manifest.name}`, - `${examples[0]}:${examplePackages[examples[0]][0].manifest.name} ${ - examples[1] - }:${examplePackages[examples[1]] - .map((pkg) => pkg.manifest.name) - .join(',')}`, - `${examples[0]}:${examplePackages[examples[0]] - .map((pkg) => pkg.manifest.name) - .join(',')} ${examples[1]}:${examplePackages[examples[1]] - .map((pkg) => pkg.manifest.name) - .join(',')}`, - ]; - } - - return []; -}