From ed6ad0b92a05511301c0414b6f89b766a197d8da Mon Sep 17 00:00:00 2001 From: Uzhanin Egor Date: Fri, 22 Mar 2024 13:52:58 +0300 Subject: [PATCH] feat: run-many command --- .eslintrc.cjs | 5 +- package.json | 8 +- pnpm-lock.yaml | 89 ++++++++++ src/commands/index.ts | 8 +- src/commands/migrate/migrate.command.ts | 2 +- src/commands/run-many/run-many.command.ts | 154 ++++++++++++++++++ src/commands/run/run.command.ts | 16 +- src/commands/show/show.command.ts | 2 +- src/logger/logger.consts.ts | 3 +- src/logger/logger.service.ts | 56 ++++++- src/pkg-manager/index.ts | 1 - .../managers/abstract.pkg-manager.ts | 2 + src/pkg-manager/managers/bun.pkg-manager.ts | 7 +- src/pkg-manager/pkg-manager.consts.ts | 1 + src/pkg-manager/pkg-manager.types.ts | 2 +- 15 files changed, 329 insertions(+), 27 deletions(-) create mode 100644 src/commands/run-many/run-many.command.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6698075..9bf7869 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -20,6 +20,9 @@ module.exports = configure({ rules: { 'import/extensions': 'warn', 'import/no-unresolved': 'warn' - } + }, + extends: [ + 'plugin:oxlint/recommended' + ] } }) diff --git a/package.json b/package.json index 05a57a4..4dddd7e 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "scripts": { "build": "unbuild", "dev": "unbuild --stub", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint": "oxlint . && eslint .", + "lint:fix": "oxlint . && eslint . --fix", "prepublishOnly": "nr build", "release": "bumpp && npm publish", "test": "vitest", @@ -61,8 +61,10 @@ "@types/which": "^3.0.3", "bumpp": "^9.2.1", "eslint": "^8.56.0", + "eslint-plugin-oxlint": "^0.2.7", "esno": "^4.0.0", "lint-staged": "^15.2.0", + "oxlint": "^0.2.14", "pnpm": "^8.14.0", "prettier": "^3.2.5", "rimraf": "^5.0.5", @@ -77,7 +79,7 @@ "pre-commit": "pnpm install && pnpm lint-staged && pnpm typecheck" }, "lint-staged": { - "*": "eslint . --fix" + "*": "oxlint . && eslint . --fix" }, "dependencies": { "@neodx/fs": "^0.0.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19fb88d..ea10a1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,12 +81,18 @@ importers: eslint: specifier: ^8.56.0 version: 8.56.0 + eslint-plugin-oxlint: + specifier: ^0.2.7 + version: 0.2.7 esno: specifier: ^4.0.0 version: 4.0.0 lint-staged: specifier: ^15.2.0 version: 15.2.0 + oxlint: + specifier: ^0.2.14 + version: 0.2.14 pnpm: specifier: ^8.14.0 version: 8.14.0 @@ -1229,6 +1235,70 @@ packages: - encoding dev: false + /@oxlint/darwin-arm64@0.2.14: + resolution: {integrity: sha512-ddCNJDIpwdtDy2EveF76jXmHOs63b2AU8vvzHZ4cw6xtHxXgfGSOTAckWfJLKXq+s5LDROMVL2DXIcuizaXYBw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@oxlint/darwin-x64@0.2.14: + resolution: {integrity: sha512-EvHOIjDpNgMXlDGk7Sr/2C0VszHj7riV+VhjGXyZwVCNFfT6jR2CfJNagTUp9AICkyESPY7UtOHYcsEsEKCtAw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@oxlint/linux-arm64-gnu@0.2.14: + resolution: {integrity: sha512-ptX6gC9wLCI3EMgYJd8cUIbcwTZv7aubuFnCg68bop2SdYAtYZIQQfazsOB/+a1pt/ISkheL19rGeaedmRqaPQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@oxlint/linux-arm64-musl@0.2.14: + resolution: {integrity: sha512-F7hzgZB65C+K1ZJkUG8vgDzo+AD/DVU2cUU7o0pv6/cn1BGjcERiyJECzrPXsnYSgeaT88eXoUgnDZNf5mH7Lw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@oxlint/linux-x64-gnu@0.2.14: + resolution: {integrity: sha512-mEkPz/GXOkPC3HNwN5SHGaT+ajk+twTuPHiI6TRKSbNCMHvi4kV9Ftana+t2hV5IfpbieFhnOsO0iH/iUOdKTA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@oxlint/linux-x64-musl@0.2.14: + resolution: {integrity: sha512-0jmm/NcD78etW+G9jHAuFjQdl9GW2bXxlOR8/y2jYyYTuCtSuqhzJCK2kWoAcBynacDIsDC+fCZF7qHOPrDHDA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@oxlint/win32-arm64@0.2.14: + resolution: {integrity: sha512-P5vaQFjtVnshk94PMaW/kot/FKH9y8fjhd7vwGp6eRO32QOkSMbG7jOtuvlFOb4E9lGyQaihJ9yVbP9vmkvzvw==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@oxlint/win32-x64@0.2.14: + resolution: {integrity: sha512-ORXoR8DpAOWX/3YDBwOCg+yUORu6cK/GUYEq22BpUXn/HRljiFbEBoBWadOhfow/K+/j9OGUxPeepe8zthN2iA==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3174,6 +3244,10 @@ packages: object.fromentries: 2.0.7 dev: false + /eslint-plugin-oxlint@0.2.7: + resolution: {integrity: sha512-cEdPDbxsK3/xtnW7PFPb/lOmeMFJvedeJuaG/7PByVuHjxjy8yOk8l1I6aAjlgFpWNhxjBrPU8cq/AEyfoZ03Q==} + dev: true + /eslint-plugin-prettier@5.1.3(eslint@8.56.0)(prettier@3.2.5): resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -5293,6 +5367,21 @@ packages: engines: {node: '>=0.10.0'} dev: false + /oxlint@0.2.14: + resolution: {integrity: sha512-LFOnbhtpitIqtHqyBDYAvT5g3ckm3fZ6xAMU8zkaDwC4ofC29ZpxAPuF42ZFJOw1UnufJPUutECSwMco2ho/Fg==} + engines: {node: '>=14.*'} + hasBin: true + optionalDependencies: + '@oxlint/darwin-arm64': 0.2.14 + '@oxlint/darwin-x64': 0.2.14 + '@oxlint/linux-arm64-gnu': 0.2.14 + '@oxlint/linux-arm64-musl': 0.2.14 + '@oxlint/linux-x64-gnu': 0.2.14 + '@oxlint/linux-x64-musl': 0.2.14 + '@oxlint/win32-arm64': 0.2.14 + '@oxlint/win32-x64': 0.2.14 + dev: true + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} diff --git a/src/commands/index.ts b/src/commands/index.ts index 46c050a..9ed25a0 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,5 +1,11 @@ import { MigrateCommand } from '@/commands/migrate/migrate.command' import { RunCommand } from '@/commands/run/run.command' +import { RunManyCommand } from '@/commands/run-many/run-many.command' import { ShowCommand } from '@/commands/show/show.command' -export const Commands = [MigrateCommand, RunCommand, ShowCommand] +export const Commands = [ + RunCommand, + MigrateCommand, + ShowCommand, + RunManyCommand +] diff --git a/src/commands/migrate/migrate.command.ts b/src/commands/migrate/migrate.command.ts index 38301e6..9aa5b25 100644 --- a/src/commands/migrate/migrate.command.ts +++ b/src/commands/migrate/migrate.command.ts @@ -11,7 +11,7 @@ import { resolve } from 'node:path' import { ConfigService } from '@/config' import { LoggerService } from '@/logger' import type { AbstractPackageManager } from '@/pkg-manager' -import { InjectPackageManager } from '@/pkg-manager' +import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator' import { ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts' import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types' import type { TargetOptions } from '@/resolver/targets/targets-resolver.schema' diff --git a/src/commands/run-many/run-many.command.ts b/src/commands/run-many/run-many.command.ts new file mode 100644 index 0000000..ff36fd9 --- /dev/null +++ b/src/commands/run-many/run-many.command.ts @@ -0,0 +1,154 @@ +import { + compact, + concurrent, + concurrently, + isEmpty, + isTypeOfBoolean +} from '@neodx/std' +import { Inject } from '@nestjs/common' +import { + CliUtilityService, + Command, + CommandRunner, + Option +} from 'nest-commander' +import { cpus } from 'node:os' +import { RunCommand } from '@/commands/run/run.command' +import { LoggerService } from '@/logger' +import type { AbstractPackageManager } from '@/pkg-manager' +import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator' +import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types' +import { invariant } from '@/shared/misc' + +export interface RunManyCommandOptions { + all?: boolean + exclude?: string[] + parallel?: number + projects?: string[] + targets?: string[] +} + +@Command({ + name: 'run-many', + // TODO: add descriptions + description: '' +}) +export class RunManyCommand extends CommandRunner { + constructor( + @InjectPackageManager() private readonly manager: AbstractPackageManager, + @Inject(LoggerService) private readonly logger: LoggerService, + @Inject(RunCommand) private readonly runner: RunCommand, + private readonly utilityService: CliUtilityService + ) { + super() + } + + private readonly DEFAULT_CONCURRENT_THREADS = 1 as const + + public async run(_: string[], opts: RunManyCommandOptions) { + await this.manager.computeWorkspaceProjects() + + const timeEnd = this.logger.time() + const { projects } = this.manager + + const computeProjectsToRun = (): WorkspaceProject[] => { + if (opts.all) return projects + + const isExcluded = ({ name }: WorkspaceProject) => + !opts.exclude?.includes(name) + const isIncluded = ({ name }: WorkspaceProject) => + opts.projects?.includes(name) + + if (opts.exclude) return projects.filter(isExcluded) + + if (opts.projects) return projects.filter(isIncluded) + + this.logger.error( + 'Invalid options provided: Either "all", "exclude", or "projects" should be specified.' + ) + process.exit(1) + } + const projectsToRun = computeProjectsToRun() + + invariant(opts.targets && !isEmpty(opts.targets), 'Targets are required.') + + const targetsToRun = opts.targets as string[] + const threadsConcurrency = isTypeOfBoolean(opts.parallel) + ? cpus().length + : opts.parallel ?? this.DEFAULT_CONCURRENT_THREADS + + await concurrently( + projectsToRun, + async (projectMeta) => { + const targetsConcurrency = Math.min(targetsToRun.length, cpus().length) + + const splitTargets = concurrent(async (target) => { + const project = projectMeta.name + this.logger.log( + `${this.logger.greaterSignPrefix} gx run ${project}:${target}\n` + ) + + this.logger.mute() + try { + await this.runner.run([target, project], {}) + } catch {} + + this.logger.unmute() + }, targetsConcurrency) + + await splitTargets(targetsToRun) + }, + threadsConcurrency + ) + + const projectNames = projectsToRun.map(({ name }) => name) + + const joinCommas = (arr: string[]) => arr.join(', ') + + timeEnd( + `Successfully ran targets ${joinCommas(targetsToRun)} for projects ${joinCommas(projectNames)}` + ) + } + + @Option({ + flags: '--all [boolean]', + name: 'all' + }) + public parseAll(all: string) { + return this.utilityService.parseBoolean(all) + } + + @Option({ + flags: '--parallel [boolean]', + name: 'parallel' + }) + public parseParallel(parallel: string) { + return this.utilityService.parseInt(parallel) + } + + @Option({ + flags: '-e --exclude [string]', + name: 'exclude' + }) + public parseExclude(exclude: string) { + return serializeCommas(exclude) + } + + @Option({ + flags: '-p --projects [string]', + name: 'projects' + }) + public parseProjects(projects: string) { + return serializeCommas(projects) + } + + @Option({ + flags: '-t --targets [string]', + name: 'targets' + }) + public parseTargets(targets: string) { + return serializeCommas(targets) + } +} + +const serializeCommas = (str: string) => compact(str.split(', ')) diff --git a/src/commands/run/run.command.ts b/src/commands/run/run.command.ts index b461293..65ac131 100644 --- a/src/commands/run/run.command.ts +++ b/src/commands/run/run.command.ts @@ -12,12 +12,14 @@ import { dirname } from 'node:path' import { buildTargetInfoPrompt } from '@/commands/run/run.prompts' import { LoggerService } from '@/logger' import type { AbstractPackageManager } from '@/pkg-manager' -import { InjectPackageManager } from '@/pkg-manager' import { PackageManager, ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts' +import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator' import { ResolverService } from '@/resolver/resolver.service' import { invariant } from '@/shared/misc' import { createIndependentTargetCommand } from './utils/independent-target-command' +// TODO: comment everything + export interface RunCommandOptions { args?: string parallel?: boolean @@ -54,7 +56,7 @@ export class RunCommand extends CommandRunner { invariant(target, 'Please specify a target. It cannot be empty.') const timeEnd = this.logger.time() - let projectCwd = process.cwd() + let projectCwd = options.cwd ?? process.cwd() if (project) { const projectMeta = this.manager.projects.find( @@ -73,11 +75,13 @@ export class RunCommand extends CommandRunner { const { targets, type: targetType } = await this.resolver.resolveProjectTargets(projectCwd) + const hasTarget = isObject(targets) && hasOwn(targets, target) - invariant( - isObject(targets) && hasOwn(targets, target), - `Could not find target ${target} in project ${project}.` - ) + if (!hasTarget) { + return this.logger.error( + `Could not find target ${target} in project ${project}.` + ) + } if (targetType === 'package-scripts') { const command = this.manager.createRunCommand({ diff --git a/src/commands/show/show.command.ts b/src/commands/show/show.command.ts index 0504a56..17d5505 100644 --- a/src/commands/show/show.command.ts +++ b/src/commands/show/show.command.ts @@ -8,7 +8,7 @@ import { } from 'nest-commander' import { LoggerService } from '@/logger' import type { AbstractPackageManager } from '@/pkg-manager' -import { InjectPackageManager } from '@/pkg-manager' +import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator' export interface ShowCommandOptions { json?: boolean diff --git a/src/logger/logger.consts.ts b/src/logger/logger.consts.ts index c542879..9fa7b9d 100644 --- a/src/logger/logger.consts.ts +++ b/src/logger/logger.consts.ts @@ -1,4 +1,5 @@ import chalk from 'chalk' export const ERROR_PREFIX = chalk.inverse(chalk.bold(chalk.red(' ERROR '))) -export const LIBRARY_PREFIX = `${chalk.cyan('>')} ${chalk.inverse(chalk.bold(chalk.cyanBright(' GX ')))}` +export const GREATER_SIGN_PREFIX = `${chalk.cyan('>')}` +export const LIBRARY_PREFIX = `${GREATER_SIGN_PREFIX} ${chalk.inverse(chalk.bold(chalk.cyanBright(' GX ')))}` diff --git a/src/logger/logger.service.ts b/src/logger/logger.service.ts index 70932c8..6755a1b 100644 --- a/src/logger/logger.service.ts +++ b/src/logger/logger.service.ts @@ -2,23 +2,29 @@ import { serializeJson } from '@neodx/fs' import { isTypeOfString } from '@neodx/std' import { Injectable } from '@nestjs/common' import chalk from 'chalk' -import { ERROR_PREFIX, LIBRARY_PREFIX } from '@/logger/logger.consts' +import { + ERROR_PREFIX, + GREATER_SIGN_PREFIX, + LIBRARY_PREFIX +} from '@/logger/logger.consts' @Injectable() export class LoggerService { - public get errorPrefix() { - return ERROR_PREFIX - } + private isSilent: boolean - public get libraryPrefix() { - return LIBRARY_PREFIX + constructor() { + this.isSilent = false } public warn(msg: string) { + if (this.isSilent) return + console.warn(chalk.bold(chalk.yellow(msg))) } public error(msg: unknown) { + if (this.isSilent) return + if (isTypeOfString(msg)) { console.error(`\n ${this.errorPrefix} ${chalk.bold(chalk.red(msg))}`) } else if (msg instanceof Error && msg.stack) { @@ -29,6 +35,8 @@ export class LoggerService { } public info(msg: unknown) { + if (this.isSilent) return + if (isTypeOfString(msg)) { console.info(`\n${this.libraryPrefix} ${chalk.bold(msg)}`) } else console.info(msg) @@ -47,14 +55,44 @@ export class LoggerService { } public log(msg: string) { - console.log(msg) + if (!this.isSilent) { + console.log(msg) + } } public debug(msg: string) { - console.debug(msg) + if (!this.isSilent) { + console.debug(msg) + } } public fatal(msg: string) { - console.error(msg) + if (!this.isSilent) { + console.error(msg) + } + } + + public mute() { + this.isSilent = true + + return () => { + this.isSilent = false + } + } + + public unmute() { + this.isSilent = false + } + + public get errorPrefix() { + return ERROR_PREFIX + } + + public get libraryPrefix() { + return LIBRARY_PREFIX + } + + public get greaterSignPrefix() { + return GREATER_SIGN_PREFIX } } diff --git a/src/pkg-manager/index.ts b/src/pkg-manager/index.ts index d004054..56f3759 100644 --- a/src/pkg-manager/index.ts +++ b/src/pkg-manager/index.ts @@ -1,4 +1,3 @@ export { AbstractPackageManager } from './managers/abstract.pkg-manager' export { PACKAGE_MANAGER } from './pkg-manager.consts' -export { InjectPackageManager } from './pkg-manager.decorator' export { PackageManagerModule } from './pkg-manager.module' diff --git a/src/pkg-manager/managers/abstract.pkg-manager.ts b/src/pkg-manager/managers/abstract.pkg-manager.ts index 16ef06c..eca795a 100644 --- a/src/pkg-manager/managers/abstract.pkg-manager.ts +++ b/src/pkg-manager/managers/abstract.pkg-manager.ts @@ -19,6 +19,8 @@ export abstract class AbstractPackageManager { private command: string ) { this.resolver = options.resolver + + this.computeWorkspaceProjects() } public async exec(args: string | string[], options?: ExecaOptions) { diff --git a/src/pkg-manager/managers/bun.pkg-manager.ts b/src/pkg-manager/managers/bun.pkg-manager.ts index 8cf59d9..a3d5d10 100644 --- a/src/pkg-manager/managers/bun.pkg-manager.ts +++ b/src/pkg-manager/managers/bun.pkg-manager.ts @@ -3,7 +3,10 @@ import { isTypeOfString } from '@neodx/std' import { dirname, resolve } from 'node:path' import * as process from 'process' import { AbstractPackageManager } from '@/pkg-manager/managers/abstract.pkg-manager' -import { PackageManager } from '@/pkg-manager/pkg-manager.consts' +import { + PackageManager, + UNNAMED_PROJECT +} from '@/pkg-manager/pkg-manager.consts' import type { PackageManagerFactoryOptions } from '@/pkg-manager/pkg-manager.factory' import type { RunCommandOptions } from '@/pkg-manager/pkg-manager.types' import type { PackageJson } from '@/shared/json' @@ -34,7 +37,7 @@ export class BunPackageManager extends AbstractPackageManager { projectPatterns.map(async (pattern) => { const scopedPkgJson = await readJson(pattern) - const workspaceName = scopedPkgJson.name ?? null + const workspaceName = scopedPkgJson.name ?? UNNAMED_PROJECT const workspaceDir = dirname(pattern) const { targets, type } = await this.resolver.resolveProjectTargets(workspaceDir) diff --git a/src/pkg-manager/pkg-manager.consts.ts b/src/pkg-manager/pkg-manager.consts.ts index 3983d1e..fb644da 100644 --- a/src/pkg-manager/pkg-manager.consts.ts +++ b/src/pkg-manager/pkg-manager.consts.ts @@ -45,3 +45,4 @@ export const packageManagerMatchers = [ ] satisfies PackageManagerMatcher[] export const ROOT_PROJECT = '$$root' as const +export const UNNAMED_PROJECT = '$$unnamed' as const diff --git a/src/pkg-manager/pkg-manager.types.ts b/src/pkg-manager/pkg-manager.types.ts index a97ec93..b260b64 100644 --- a/src/pkg-manager/pkg-manager.types.ts +++ b/src/pkg-manager/pkg-manager.types.ts @@ -2,7 +2,7 @@ import type { TargetType } from '@/resolver/resolver.types' import type { TargetOptions } from '@/resolver/targets/targets-resolver.schema' export interface WorkspaceProject { - name: string | null + name: string location: string targets: TargetOptions type: TargetType