Skip to content

Commit ed6ad0b

Browse files
committed
feat: run-many command
1 parent fdc29fc commit ed6ad0b

15 files changed

+329
-27
lines changed

.eslintrc.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ module.exports = configure({
2020
rules: {
2121
'import/extensions': 'warn',
2222
'import/no-unresolved': 'warn'
23-
}
23+
},
24+
extends: [
25+
'plugin:oxlint/recommended'
26+
]
2427
}
2528
})

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
"scripts": {
4545
"build": "unbuild",
4646
"dev": "unbuild --stub",
47-
"lint": "eslint .",
48-
"lint:fix": "eslint . --fix",
47+
"lint": "oxlint . && eslint .",
48+
"lint:fix": "oxlint . && eslint . --fix",
4949
"prepublishOnly": "nr build",
5050
"release": "bumpp && npm publish",
5151
"test": "vitest",
@@ -61,8 +61,10 @@
6161
"@types/which": "^3.0.3",
6262
"bumpp": "^9.2.1",
6363
"eslint": "^8.56.0",
64+
"eslint-plugin-oxlint": "^0.2.7",
6465
"esno": "^4.0.0",
6566
"lint-staged": "^15.2.0",
67+
"oxlint": "^0.2.14",
6668
"pnpm": "^8.14.0",
6769
"prettier": "^3.2.5",
6870
"rimraf": "^5.0.5",
@@ -77,7 +79,7 @@
7779
"pre-commit": "pnpm install && pnpm lint-staged && pnpm typecheck"
7880
},
7981
"lint-staged": {
80-
"*": "eslint . --fix"
82+
"*": "oxlint . && eslint . --fix"
8183
},
8284
"dependencies": {
8385
"@neodx/fs": "^0.0.11",

pnpm-lock.yaml

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { MigrateCommand } from '@/commands/migrate/migrate.command'
22
import { RunCommand } from '@/commands/run/run.command'
3+
import { RunManyCommand } from '@/commands/run-many/run-many.command'
34
import { ShowCommand } from '@/commands/show/show.command'
45

5-
export const Commands = [MigrateCommand, RunCommand, ShowCommand]
6+
export const Commands = [
7+
RunCommand,
8+
MigrateCommand,
9+
ShowCommand,
10+
RunManyCommand
11+
]

src/commands/migrate/migrate.command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { resolve } from 'node:path'
1111
import { ConfigService } from '@/config'
1212
import { LoggerService } from '@/logger'
1313
import type { AbstractPackageManager } from '@/pkg-manager'
14-
import { InjectPackageManager } from '@/pkg-manager'
14+
import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator'
1515
import { ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
1616
import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types'
1717
import type { TargetOptions } from '@/resolver/targets/targets-resolver.schema'
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {
2+
compact,
3+
concurrent,
4+
concurrently,
5+
isEmpty,
6+
isTypeOfBoolean
7+
} from '@neodx/std'
8+
import { Inject } from '@nestjs/common'
9+
import {
10+
CliUtilityService,
11+
Command,
12+
CommandRunner,
13+
Option
14+
} from 'nest-commander'
15+
import { cpus } from 'node:os'
16+
import { RunCommand } from '@/commands/run/run.command'
17+
import { LoggerService } from '@/logger'
18+
import type { AbstractPackageManager } from '@/pkg-manager'
19+
import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator'
20+
import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types'
21+
import { invariant } from '@/shared/misc'
22+
23+
export interface RunManyCommandOptions {
24+
all?: boolean
25+
exclude?: string[]
26+
parallel?: number
27+
projects?: string[]
28+
targets?: string[]
29+
}
30+
31+
@Command({
32+
name: 'run-many',
33+
// TODO: add descriptions
34+
description: ''
35+
})
36+
export class RunManyCommand extends CommandRunner {
37+
constructor(
38+
@InjectPackageManager() private readonly manager: AbstractPackageManager,
39+
@Inject(LoggerService) private readonly logger: LoggerService,
40+
@Inject(RunCommand) private readonly runner: RunCommand,
41+
private readonly utilityService: CliUtilityService
42+
) {
43+
super()
44+
}
45+
46+
private readonly DEFAULT_CONCURRENT_THREADS = 1 as const
47+
48+
public async run(_: string[], opts: RunManyCommandOptions) {
49+
await this.manager.computeWorkspaceProjects()
50+
51+
const timeEnd = this.logger.time()
52+
const { projects } = this.manager
53+
54+
const computeProjectsToRun = (): WorkspaceProject[] => {
55+
if (opts.all) return projects
56+
57+
const isExcluded = ({ name }: WorkspaceProject) =>
58+
!opts.exclude?.includes(name)
59+
const isIncluded = ({ name }: WorkspaceProject) =>
60+
opts.projects?.includes(name)
61+
62+
if (opts.exclude) return projects.filter(isExcluded)
63+
64+
if (opts.projects) return projects.filter(isIncluded)
65+
66+
this.logger.error(
67+
'Invalid options provided: Either "all", "exclude", or "projects" should be specified.'
68+
)
69+
process.exit(1)
70+
}
71+
const projectsToRun = computeProjectsToRun()
72+
73+
invariant(opts.targets && !isEmpty(opts.targets), 'Targets are required.')
74+
75+
const targetsToRun = opts.targets as string[]
76+
const threadsConcurrency = isTypeOfBoolean(opts.parallel)
77+
? cpus().length
78+
: opts.parallel ?? this.DEFAULT_CONCURRENT_THREADS
79+
80+
await concurrently(
81+
projectsToRun,
82+
async (projectMeta) => {
83+
const targetsConcurrency = Math.min(targetsToRun.length, cpus().length)
84+
85+
const splitTargets = concurrent<string, void>(async (target) => {
86+
const project = projectMeta.name
87+
this.logger.log(
88+
`${this.logger.greaterSignPrefix} gx run ${project}:${target}\n`
89+
)
90+
91+
this.logger.mute()
92+
try {
93+
await this.runner.run([target, project], {})
94+
} catch {}
95+
96+
this.logger.unmute()
97+
}, targetsConcurrency)
98+
99+
await splitTargets(targetsToRun)
100+
},
101+
threadsConcurrency
102+
)
103+
104+
const projectNames = projectsToRun.map(({ name }) => name)
105+
106+
const joinCommas = (arr: string[]) => arr.join(', ')
107+
108+
timeEnd(
109+
`Successfully ran targets ${joinCommas(targetsToRun)} for projects ${joinCommas(projectNames)}`
110+
)
111+
}
112+
113+
@Option({
114+
flags: '--all [boolean]',
115+
name: 'all'
116+
})
117+
public parseAll(all: string) {
118+
return this.utilityService.parseBoolean(all)
119+
}
120+
121+
@Option({
122+
flags: '--parallel [boolean]',
123+
name: 'parallel'
124+
})
125+
public parseParallel(parallel: string) {
126+
return this.utilityService.parseInt(parallel)
127+
}
128+
129+
@Option({
130+
flags: '-e --exclude [string]',
131+
name: 'exclude'
132+
})
133+
public parseExclude(exclude: string) {
134+
return serializeCommas(exclude)
135+
}
136+
137+
@Option({
138+
flags: '-p --projects [string]',
139+
name: 'projects'
140+
})
141+
public parseProjects(projects: string) {
142+
return serializeCommas(projects)
143+
}
144+
145+
@Option({
146+
flags: '-t --targets [string]',
147+
name: 'targets'
148+
})
149+
public parseTargets(targets: string) {
150+
return serializeCommas(targets)
151+
}
152+
}
153+
154+
const serializeCommas = (str: string) => compact(str.split(', '))

src/commands/run/run.command.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import { dirname } from 'node:path'
1212
import { buildTargetInfoPrompt } from '@/commands/run/run.prompts'
1313
import { LoggerService } from '@/logger'
1414
import type { AbstractPackageManager } from '@/pkg-manager'
15-
import { InjectPackageManager } from '@/pkg-manager'
1615
import { PackageManager, ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
16+
import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator'
1717
import { ResolverService } from '@/resolver/resolver.service'
1818
import { invariant } from '@/shared/misc'
1919
import { createIndependentTargetCommand } from './utils/independent-target-command'
2020

21+
// TODO: comment everything
22+
2123
export interface RunCommandOptions {
2224
args?: string
2325
parallel?: boolean
@@ -54,7 +56,7 @@ export class RunCommand extends CommandRunner {
5456
invariant(target, 'Please specify a target. It cannot be empty.')
5557

5658
const timeEnd = this.logger.time()
57-
let projectCwd = process.cwd()
59+
let projectCwd = options.cwd ?? process.cwd()
5860

5961
if (project) {
6062
const projectMeta = this.manager.projects.find(
@@ -73,11 +75,13 @@ export class RunCommand extends CommandRunner {
7375

7476
const { targets, type: targetType } =
7577
await this.resolver.resolveProjectTargets(projectCwd)
78+
const hasTarget = isObject(targets) && hasOwn(targets, target)
7679

77-
invariant(
78-
isObject(targets) && hasOwn(targets, target),
79-
`Could not find target ${target} in project ${project}.`
80-
)
80+
if (!hasTarget) {
81+
return this.logger.error(
82+
`Could not find target ${target} in project ${project}.`
83+
)
84+
}
8185

8286
if (targetType === 'package-scripts') {
8387
const command = this.manager.createRunCommand({

src/commands/show/show.command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from 'nest-commander'
99
import { LoggerService } from '@/logger'
1010
import type { AbstractPackageManager } from '@/pkg-manager'
11-
import { InjectPackageManager } from '@/pkg-manager'
11+
import { InjectPackageManager } from '@/pkg-manager/pkg-manager.decorator'
1212

1313
export interface ShowCommandOptions {
1414
json?: boolean

0 commit comments

Comments
 (0)