Skip to content

Commit c1e5487

Browse files
committed
feat: targets.json support
1 parent 65333f1 commit c1e5487

24 files changed

+368
-79
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"vitest": "^1.1.3"
7272
},
7373
"simple-git-hooks": {
74-
"pre-commit": "pnpm lint-staged && pnpm typecheck"
74+
"pre-commit": "pnpm install && pnpm lint-staged && pnpm typecheck"
7575
},
7676
"lint-staged": {
7777
"*": "eslint . --fix"
@@ -83,13 +83,15 @@
8383
"@nestjs/core": "^10.3.3",
8484
"@nestjs/platform-express": "^10.3.3",
8585
"chalk": "^5.3.0",
86+
"envfile": "^7.1.0",
8687
"eslint-kit": "^10.19.0",
8788
"execa": "^8.0.1",
8889
"inquirer": "^9.2.15",
8990
"inquirer-search-list": "^1.2.6",
9091
"inquirer-tree-prompt": "^1.1.2",
9192
"nest-commander": "^3.12.5",
9293
"path-equal": "^1.2.5",
93-
"which": "^4.0.0"
94+
"which": "^4.0.0",
95+
"zod": "^3.22.4"
9496
}
9597
}

pnpm-lock.yaml

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

shims.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
declare module 'inquirer-tree-prompt'
22
declare module 'inquirer-search-list'
3+
declare module 'envfile'

src/commands/run/run.command.ts

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { ensureFile } from '@neodx/fs'
1+
import { ensureDir } from '@neodx/fs'
22
import { hasOwn, isEmpty, isObject } from '@neodx/std'
33
import { Inject } from '@nestjs/common'
4+
import { execaCommand } from 'execa'
45
import { Command, CommandRunner, Option } from 'nest-commander'
5-
import { dirname, resolve } from 'node:path'
6+
import { dirname } from 'node:path'
67
import { buildTargetInfoPrompt } from '@/commands/run/run.prompts'
8+
import { createIndependentTargetCommand } from '@/commands/run/utils/independent-target-command'
79
import { LoggerService } from '@/logger'
810
import type { AbstractPackageManager } from '@/pkg-manager'
911
import { InjectPackageManager } from '@/pkg-manager'
1012
import { PackageManager, ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
11-
import type { PackageJson } from '@/shared/json'
12-
import { readJson } from '@/shared/json'
13+
import { ResolverService } from '@/resolver/resolver.service'
14+
import type { AnyTarget } from '@/resolver/targets/targets-resolver.schema'
1315
import { invariant } from '@/shared/misc'
1416

1517
export interface BaseInitOptions {
@@ -26,7 +28,8 @@ export interface BaseInitOptions {
2628
export class RunCommand extends CommandRunner {
2729
constructor(
2830
@InjectPackageManager() private readonly manager: AbstractPackageManager,
29-
@Inject(LoggerService) private readonly logger: LoggerService
31+
@Inject(LoggerService) private readonly logger: LoggerService,
32+
@Inject(ResolverService) private readonly resolver: ResolverService
3033
) {
3134
super()
3235
}
@@ -36,14 +39,14 @@ export class RunCommand extends CommandRunner {
3639
await this.manager.computeWorkspaceProjects()
3740
}
3841

39-
const [target, project = 'root'] = isEmpty(params)
42+
const [target, project = ROOT_PROJECT] = isEmpty(params)
4043
? await buildTargetInfoPrompt(this.manager.projects)
4144
: params
4245

4346
invariant(target, 'Please specify a target. It cannot be empty.')
4447

4548
const timeEnd = this.logger.time()
46-
let packageJsonPath = resolve(process.cwd(), 'package.json')
49+
let projectCwd = process.cwd()
4750

4851
if (project) {
4952
const projectMeta = this.manager.projects.find(
@@ -55,43 +58,57 @@ export class RunCommand extends CommandRunner {
5558
`Project ${project} not found. Please ensure it exists.`
5659
)
5760

58-
packageJsonPath = resolve(projectMeta.location, 'package.json')
61+
projectCwd = projectMeta.location
5962
}
6063

61-
await ensureFile(packageJsonPath)
64+
await ensureDir(projectCwd)
6265

63-
const pkg = await readJson<PackageJson>(packageJsonPath)
64-
const projectName = project ?? ROOT_PROJECT
66+
const { targets, type: targetType } =
67+
await this.resolver.resolveProjectTargets(projectCwd)
6568

6669
invariant(
67-
isObject(pkg.scripts) && hasOwn(pkg.scripts, target),
68-
`Could not find target ${target} in project ${projectName}.`
70+
isObject(targets) && hasOwn(targets, target),
71+
`Could not find target ${target} in project ${project}.`
6972
)
7073

71-
const command = this.manager.createRunCommand({
72-
target,
73-
project,
74-
packageJsonPath,
75-
args: options.args
76-
})
77-
78-
try {
79-
// https://github.com/oven-sh/bun/issues/6386
80-
const cwd =
81-
this.manager.agent === PackageManager.BUN
82-
? dirname(packageJsonPath)
83-
: process.cwd()
84-
85-
await this.manager.exec(command, { stdio: 'inherit', cwd })
86-
} catch (error) {
87-
this.logger.error(
88-
`Error occurred while executing a command via ${this.manager.agent} agent.`
74+
if (targetType === 'package-scripts') {
75+
const command = this.manager.createRunCommand({
76+
target,
77+
project,
78+
packageJsonPath: projectCwd,
79+
args: options.args
80+
})
81+
82+
try {
83+
// https://github.com/oven-sh/bun/issues/6386
84+
const cwd =
85+
this.manager.agent === PackageManager.BUN
86+
? dirname(projectCwd)
87+
: process.cwd()
88+
89+
await this.manager.exec(command, { stdio: 'inherit', cwd })
90+
} catch (error) {
91+
this.logger.error(
92+
`Error occurred while executing a command via ${this.manager.agent} agent.`
93+
)
94+
this.logger.error(error)
95+
return
96+
}
97+
}
98+
99+
if (targetType === 'targets') {
100+
const { command, env, cwd, args } = createIndependentTargetCommand(
101+
targets[target] as AnyTarget,
102+
{ defaultArgs: options.args, projectCwd }
89103
)
90-
this.logger.error(error)
91-
return
104+
105+
await execaCommand(`${command} ${args}`, {
106+
cwd,
107+
env
108+
})
92109
}
93110

94-
timeEnd(`Successfully ran target ${target} for project ${projectName}`)
111+
timeEnd(`Successfully ran target ${target} for project ${project}`)
95112
}
96113

97114
@Option({

src/commands/run/run.prompts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { entries, keys } from '@neodx/std'
33
import inquirer from 'inquirer'
44
import { ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
55
import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types'
6+
import { formatTargetCommand } from '@/resolver/targets/targets-resolver.utils'
67
import { invariant, truncateString } from '@/shared/misc'
78

89
export async function buildTargetInfoPrompt(
@@ -68,7 +69,7 @@ async function buildTargetPromptTree(projects: WorkspaceProject[]): Promise<{
6869
project: project.name,
6970
target: targetName
7071
},
71-
short: truncateString(script, 14)
72+
short: truncateString(formatTargetCommand(script), 14)
7273
})
7374
)
7475

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { compact } from '@neodx/std'
2+
import envfile from 'envfile'
3+
import type { AnyTarget } from '@/resolver/targets/targets-resolver.schema'
4+
import { invariant, toAbsolutePath } from '@/shared/misc'
5+
6+
interface CreateIndependentCommandOptions {
7+
defaultArgs?: string
8+
projectCwd: string
9+
}
10+
11+
enum TargetSeparators {
12+
NORMAL = ' && ',
13+
PARALLEL = ' & '
14+
}
15+
16+
export function createIndependentTargetCommand(
17+
opts: AnyTarget,
18+
{ defaultArgs, projectCwd }: CreateIndependentCommandOptions
19+
) {
20+
const normalizeCommand = ({
21+
command,
22+
commands,
23+
parallel
24+
}: Partial<AnyTarget>): string => {
25+
invariant(
26+
command ?? commands,
27+
'Either "command" or "commands" must be provided.'
28+
)
29+
30+
const result = Array.isArray(command) ? command.join(' ') : command
31+
32+
if (!result) {
33+
const separator = parallel
34+
? TargetSeparators.PARALLEL
35+
: TargetSeparators.NORMAL
36+
37+
return commands!.join(separator)
38+
}
39+
40+
return result
41+
}
42+
43+
const normalizeEnv = ({
44+
env,
45+
envFile
46+
}: Partial<AnyTarget>): Record<string, string> | undefined => {
47+
if (envFile) return envfile.parse(toAbsolutePath(envFile))
48+
if (env) return env
49+
return undefined
50+
}
51+
52+
const command = normalizeCommand(opts)
53+
const args = compact([defaultArgs, opts.args]).join(' ')
54+
const env = normalizeEnv(opts)
55+
const cwd = toAbsolutePath(opts.cwd ?? projectCwd ?? process.cwd())
56+
57+
return {
58+
command,
59+
args,
60+
cwd,
61+
env
62+
}
63+
}

src/pkg-manager/managers/abstract.pkg-manager.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
import { isObject, isTypeOfString } from '@neodx/std'
1+
import { isTypeOfString } from '@neodx/std'
22
import type { Options as ExecaOptions } from 'execa'
33
import { execaCommand as $ } from 'execa'
4-
import { resolve } from 'node:path'
54
import { ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
5+
import type { PackageManagerFactoryOptions } from '@/pkg-manager/pkg-manager.factory'
66
import type {
77
RunCommandOptions,
88
WorkspaceProject
99
} from '@/pkg-manager/pkg-manager.types'
10-
import type { PackageJson } from '@/shared/json'
11-
import { readJson } from '@/shared/json'
10+
import type { ResolverService } from '@/resolver/resolver.service'
1211

1312
export abstract class AbstractPackageManager {
13+
// TODO: map set
1414
public projects: WorkspaceProject[] = []
15+
protected readonly resolver: ResolverService
16+
17+
constructor(
18+
protected options: PackageManagerFactoryOptions,
19+
private command: string
20+
) {
21+
this.resolver = options.resolver
1522

16-
constructor(private command: string) {
1723
this.computeWorkspaceProjects()
1824
}
1925

@@ -29,17 +35,6 @@ export abstract class AbstractPackageManager {
2935
return output.stdout as string
3036
}
3137

32-
public async resolveProjectTargets(
33-
projectPath: string
34-
): Promise<Record<string, string>> {
35-
const pkgJsonPath = resolve(projectPath, 'package.json')
36-
const pkg = await readJson<PackageJson>(pkgJsonPath)
37-
38-
if (!isObject(pkg.scripts)) return {}
39-
40-
return pkg.scripts
41-
}
42-
4338
public get agent() {
4439
return this.command
4540
}
@@ -48,11 +43,12 @@ export abstract class AbstractPackageManager {
4843
workspaces: WorkspaceProject[] = []
4944
): Promise<void> {
5045
const cwd = process.cwd()
46+
const { targets } = await this.resolver.resolveProjectTargets(cwd)
5147

5248
const root = {
5349
name: ROOT_PROJECT,
5450
location: cwd,
55-
targets: await this.resolveProjectTargets(cwd)
51+
targets
5652
} satisfies WorkspaceProject
5753

5854
this.projects = [root, ...workspaces]

src/pkg-manager/managers/bun.pkg-manager.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import { isTypeOfString } from '@neodx/std'
33
import { dirname, resolve } from 'node:path'
44
import * as process from 'process'
55
import { AbstractPackageManager } from '@/pkg-manager/managers/abstract.pkg-manager'
6-
import { PackageManager, ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
6+
import { PackageManager } from '@/pkg-manager/pkg-manager.consts'
7+
import type { PackageManagerFactoryOptions } from '@/pkg-manager/pkg-manager.factory'
78
import type { RunCommandOptions } from '@/pkg-manager/pkg-manager.types'
89
import type { PackageJson } from '@/shared/json'
910
import { readJson } from '@/shared/json'
1011

1112
// TODO: split file structure to modules
1213

1314
export class BunPackageManager extends AbstractPackageManager {
14-
constructor() {
15-
super(PackageManager.BUN)
15+
constructor(opts: PackageManagerFactoryOptions) {
16+
super(opts, PackageManager.BUN)
1617
}
1718

1819
public async computeWorkspaceProjects(): Promise<void> {
@@ -35,7 +36,8 @@ export class BunPackageManager extends AbstractPackageManager {
3536

3637
const workspaceName = scopedPkgJson.name ?? null
3738
const workspaceDir = dirname(pattern)
38-
const targets = await this.resolveProjectTargets(workspaceDir)
39+
const { targets } =
40+
await this.resolver.resolveProjectTargets(workspaceDir)
3941

4042
return {
4143
name: workspaceName,

0 commit comments

Comments
 (0)