Skip to content

Commit

Permalink
feat: targets.json support
Browse files Browse the repository at this point in the history
  • Loading branch information
gearonix committed Mar 13, 2024
1 parent 65333f1 commit c1e5487
Show file tree
Hide file tree
Showing 24 changed files with 368 additions and 79 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"vitest": "^1.1.3"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged && pnpm typecheck"
"pre-commit": "pnpm install && pnpm lint-staged && pnpm typecheck"
},
"lint-staged": {
"*": "eslint . --fix"
Expand All @@ -83,13 +83,15 @@
"@nestjs/core": "^10.3.3",
"@nestjs/platform-express": "^10.3.3",
"chalk": "^5.3.0",
"envfile": "^7.1.0",
"eslint-kit": "^10.19.0",
"execa": "^8.0.1",
"inquirer": "^9.2.15",
"inquirer-search-list": "^1.2.6",
"inquirer-tree-prompt": "^1.1.2",
"nest-commander": "^3.12.5",
"path-equal": "^1.2.5",
"which": "^4.0.0"
"which": "^4.0.0",
"zod": "^3.22.4"
}
}
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions shims.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
declare module 'inquirer-tree-prompt'
declare module 'inquirer-search-list'
declare module 'envfile'
85 changes: 51 additions & 34 deletions src/commands/run/run.command.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { ensureFile } from '@neodx/fs'
import { ensureDir } from '@neodx/fs'
import { hasOwn, isEmpty, isObject } from '@neodx/std'
import { Inject } from '@nestjs/common'
import { execaCommand } from 'execa'
import { Command, CommandRunner, Option } from 'nest-commander'
import { dirname, resolve } from 'node:path'
import { dirname } from 'node:path'
import { buildTargetInfoPrompt } from '@/commands/run/run.prompts'
import { createIndependentTargetCommand } from '@/commands/run/utils/independent-target-command'
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 type { PackageJson } from '@/shared/json'
import { readJson } from '@/shared/json'
import { ResolverService } from '@/resolver/resolver.service'
import type { AnyTarget } from '@/resolver/targets/targets-resolver.schema'
import { invariant } from '@/shared/misc'

export interface BaseInitOptions {
Expand All @@ -26,7 +28,8 @@ export interface BaseInitOptions {
export class RunCommand extends CommandRunner {
constructor(
@InjectPackageManager() private readonly manager: AbstractPackageManager,
@Inject(LoggerService) private readonly logger: LoggerService
@Inject(LoggerService) private readonly logger: LoggerService,
@Inject(ResolverService) private readonly resolver: ResolverService
) {
super()
}
Expand All @@ -36,14 +39,14 @@ export class RunCommand extends CommandRunner {
await this.manager.computeWorkspaceProjects()
}

const [target, project = 'root'] = isEmpty(params)
const [target, project = ROOT_PROJECT] = isEmpty(params)
? await buildTargetInfoPrompt(this.manager.projects)
: params

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

const timeEnd = this.logger.time()
let packageJsonPath = resolve(process.cwd(), 'package.json')
let projectCwd = process.cwd()

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

packageJsonPath = resolve(projectMeta.location, 'package.json')
projectCwd = projectMeta.location
}

await ensureFile(packageJsonPath)
await ensureDir(projectCwd)

const pkg = await readJson<PackageJson>(packageJsonPath)
const projectName = project ?? ROOT_PROJECT
const { targets, type: targetType } =
await this.resolver.resolveProjectTargets(projectCwd)

invariant(
isObject(pkg.scripts) && hasOwn(pkg.scripts, target),
`Could not find target ${target} in project ${projectName}.`
isObject(targets) && hasOwn(targets, target),
`Could not find target ${target} in project ${project}.`
)

const command = this.manager.createRunCommand({
target,
project,
packageJsonPath,
args: options.args
})

try {
// https://github.com/oven-sh/bun/issues/6386
const cwd =
this.manager.agent === PackageManager.BUN
? dirname(packageJsonPath)
: process.cwd()

await this.manager.exec(command, { stdio: 'inherit', cwd })
} catch (error) {
this.logger.error(
`Error occurred while executing a command via ${this.manager.agent} agent.`
if (targetType === 'package-scripts') {
const command = this.manager.createRunCommand({
target,
project,
packageJsonPath: projectCwd,
args: options.args
})

try {
// https://github.com/oven-sh/bun/issues/6386
const cwd =
this.manager.agent === PackageManager.BUN
? dirname(projectCwd)
: process.cwd()

await this.manager.exec(command, { stdio: 'inherit', cwd })
} catch (error) {
this.logger.error(
`Error occurred while executing a command via ${this.manager.agent} agent.`
)
this.logger.error(error)
return
}
}

if (targetType === 'targets') {
const { command, env, cwd, args } = createIndependentTargetCommand(
targets[target] as AnyTarget,
{ defaultArgs: options.args, projectCwd }
)
this.logger.error(error)
return

await execaCommand(`${command} ${args}`, {
cwd,
env
})
}

timeEnd(`Successfully ran target ${target} for project ${projectName}`)
timeEnd(`Successfully ran target ${target} for project ${project}`)
}

@Option({
Expand Down
3 changes: 2 additions & 1 deletion src/commands/run/run.prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { entries, keys } from '@neodx/std'
import inquirer from 'inquirer'
import { ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types'
import { formatTargetCommand } from '@/resolver/targets/targets-resolver.utils'
import { invariant, truncateString } from '@/shared/misc'

export async function buildTargetInfoPrompt(
Expand Down Expand Up @@ -68,7 +69,7 @@ async function buildTargetPromptTree(projects: WorkspaceProject[]): Promise<{
project: project.name,
target: targetName
},
short: truncateString(script, 14)
short: truncateString(formatTargetCommand(script), 14)
})
)

Expand Down
63 changes: 63 additions & 0 deletions src/commands/run/utils/independent-target-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { compact } from '@neodx/std'
import envfile from 'envfile'
import type { AnyTarget } from '@/resolver/targets/targets-resolver.schema'
import { invariant, toAbsolutePath } from '@/shared/misc'

interface CreateIndependentCommandOptions {
defaultArgs?: string
projectCwd: string
}

enum TargetSeparators {
NORMAL = ' && ',
PARALLEL = ' & '
}

export function createIndependentTargetCommand(
opts: AnyTarget,
{ defaultArgs, projectCwd }: CreateIndependentCommandOptions
) {
const normalizeCommand = ({
command,
commands,
parallel
}: Partial<AnyTarget>): string => {
invariant(
command ?? commands,
'Either "command" or "commands" must be provided.'
)

const result = Array.isArray(command) ? command.join(' ') : command

if (!result) {
const separator = parallel
? TargetSeparators.PARALLEL
: TargetSeparators.NORMAL

return commands!.join(separator)
}

return result
}

const normalizeEnv = ({
env,
envFile
}: Partial<AnyTarget>): Record<string, string> | undefined => {
if (envFile) return envfile.parse(toAbsolutePath(envFile))
if (env) return env
return undefined
}

const command = normalizeCommand(opts)
const args = compact([defaultArgs, opts.args]).join(' ')
const env = normalizeEnv(opts)
const cwd = toAbsolutePath(opts.cwd ?? projectCwd ?? process.cwd())

return {
command,
args,
cwd,
env
}
}
30 changes: 13 additions & 17 deletions src/pkg-manager/managers/abstract.pkg-manager.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { isObject, isTypeOfString } from '@neodx/std'
import { isTypeOfString } from '@neodx/std'
import type { Options as ExecaOptions } from 'execa'
import { execaCommand as $ } from 'execa'
import { resolve } from 'node:path'
import { ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
import type { PackageManagerFactoryOptions } from '@/pkg-manager/pkg-manager.factory'
import type {
RunCommandOptions,
WorkspaceProject
} from '@/pkg-manager/pkg-manager.types'
import type { PackageJson } from '@/shared/json'
import { readJson } from '@/shared/json'
import type { ResolverService } from '@/resolver/resolver.service'

export abstract class AbstractPackageManager {
// TODO: map set
public projects: WorkspaceProject[] = []
protected readonly resolver: ResolverService

constructor(
protected options: PackageManagerFactoryOptions,
private command: string
) {
this.resolver = options.resolver

constructor(private command: string) {
this.computeWorkspaceProjects()
}

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

public async resolveProjectTargets(
projectPath: string
): Promise<Record<string, string>> {
const pkgJsonPath = resolve(projectPath, 'package.json')
const pkg = await readJson<PackageJson>(pkgJsonPath)

if (!isObject(pkg.scripts)) return {}

return pkg.scripts
}

public get agent() {
return this.command
}
Expand All @@ -48,11 +43,12 @@ export abstract class AbstractPackageManager {
workspaces: WorkspaceProject[] = []
): Promise<void> {
const cwd = process.cwd()
const { targets } = await this.resolver.resolveProjectTargets(cwd)

const root = {
name: ROOT_PROJECT,
location: cwd,
targets: await this.resolveProjectTargets(cwd)
targets
} satisfies WorkspaceProject

this.projects = [root, ...workspaces]
Expand Down
10 changes: 6 additions & 4 deletions src/pkg-manager/managers/bun.pkg-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ 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, ROOT_PROJECT } from '@/pkg-manager/pkg-manager.consts'
import { PackageManager } 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'
import { readJson } from '@/shared/json'

// TODO: split file structure to modules

export class BunPackageManager extends AbstractPackageManager {
constructor() {
super(PackageManager.BUN)
constructor(opts: PackageManagerFactoryOptions) {
super(opts, PackageManager.BUN)
}

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

const workspaceName = scopedPkgJson.name ?? null
const workspaceDir = dirname(pattern)
const targets = await this.resolveProjectTargets(workspaceDir)
const { targets } =
await this.resolver.resolveProjectTargets(workspaceDir)

return {
name: workspaceName,
Expand Down
Loading

0 comments on commit c1e5487

Please sign in to comment.