diff --git a/src/commands/index.ts b/src/commands/index.ts index cc96933..46c050a 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,4 +1,5 @@ -import { RunCommand } from './run/run.command' -import { ShowCommand } from './show/show.command' +import { MigrateCommand } from '@/commands/migrate/migrate.command' +import { RunCommand } from '@/commands/run/run.command' +import { ShowCommand } from '@/commands/show/show.command' -export const Commands = [RunCommand, ShowCommand] +export const Commands = [MigrateCommand, RunCommand, ShowCommand] diff --git a/src/commands/migrate/migrate.command.ts b/src/commands/migrate/migrate.command.ts new file mode 100644 index 0000000..38301e6 --- /dev/null +++ b/src/commands/migrate/migrate.command.ts @@ -0,0 +1,111 @@ +import { assertDir, ensureFile } from '@neodx/fs' +import { mapValues } from '@neodx/std' +import { Inject } from '@nestjs/common' +import { + CliUtilityService, + Command, + CommandRunner, + Option +} from 'nest-commander' +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 { 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' +import type { PackageJson, PackageScripts } from '@/shared/json' +import { writeJson } from '@/shared/json' +import { invariant } from '@/shared/misc' + +export interface MigrateCommandOptions { + all?: boolean +} + +@Command({ + name: 'migrate', + // TODO: add descriptions + description: '' +}) +export class MigrateCommand extends CommandRunner { + constructor( + @InjectPackageManager() private readonly manager: AbstractPackageManager, + @Inject(LoggerService) private readonly logger: LoggerService, + @Inject(ConfigService) private readonly cfg: ConfigService, + private readonly utilityService: CliUtilityService + ) { + super() + } + + public async run(params: string[], options: MigrateCommandOptions) { + await this.manager.computeWorkspaceProjects() + + const { projects: workspaceProjects } = this.manager + + if (options.all) { + await Promise.all( + workspaceProjects.map(async (projectMeta) => + this.migrateProjectCommands(projectMeta, options) + ) + ) + return + } + + const [projectName = ROOT_PROJECT] = params + + const projectMeta = workspaceProjects.find( + ({ name }) => name === projectName + ) + + invariant(projectMeta, `Project '${projectName}' not found.`) + + await this.migrateProjectCommands(projectMeta) + } + + private async migrateProjectCommands( + projectMeta: WorkspaceProject, + options?: MigrateCommandOptions + ): Promise { + const { location: projectCwd, type: targetType } = projectMeta + + await assertDir(projectMeta.location) + + if (targetType === 'package-scripts') { + const commandsFilePath = resolve( + projectMeta.location, + this.cfg.commandsFile + ) + const pkgScripts = projectMeta.targets as NonNullable< + PackageJson['scripts'] + > + + await ensureFile(commandsFilePath) + + const serializedTargets = mapValues( + pkgScripts, + (command) => ({ command }) + ) + + await writeJson(commandsFilePath, serializedTargets) + + this.logger.info(`Generated ${this.cfg.commandsFile} in ${projectCwd}`) + return + } + + if (!options?.all) { + this.logger.error( + `The specified target type '${targetType}' is not allowed. + It seems you may already have a '${this.cfg.commandsFile}' file in your project.` + ) + } + } + + @Option({ + flags: '--all [boolean]', + name: 'all' + }) + public parseAll(all: string) { + return this.utilityService.parseBoolean(all) + } +} diff --git a/src/commands/run/run.command.ts b/src/commands/run/run.command.ts index 38291e2..b461293 100644 --- a/src/commands/run/run.command.ts +++ b/src/commands/run/run.command.ts @@ -10,13 +10,13 @@ import { } from 'nest-commander' 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 { ResolverService } from '@/resolver/resolver.service' import { invariant } from '@/shared/misc' +import { createIndependentTargetCommand } from './utils/independent-target-command' export interface RunCommandOptions { args?: string diff --git a/src/pkg-manager/managers/abstract.pkg-manager.ts b/src/pkg-manager/managers/abstract.pkg-manager.ts index 5a1129b..16ef06c 100644 --- a/src/pkg-manager/managers/abstract.pkg-manager.ts +++ b/src/pkg-manager/managers/abstract.pkg-manager.ts @@ -38,12 +38,13 @@ export abstract class AbstractPackageManager { workspaces: WorkspaceProject[] = [] ): Promise { const cwd = process.cwd() - const { targets } = await this.resolver.resolveProjectTargets(cwd) + const { targets, type } = await this.resolver.resolveProjectTargets(cwd) const root = { name: ROOT_PROJECT, location: cwd, - targets + targets, + type } satisfies WorkspaceProject this.projects = [root, ...workspaces] diff --git a/src/pkg-manager/managers/bun.pkg-manager.ts b/src/pkg-manager/managers/bun.pkg-manager.ts index dd254ea..8cf59d9 100644 --- a/src/pkg-manager/managers/bun.pkg-manager.ts +++ b/src/pkg-manager/managers/bun.pkg-manager.ts @@ -36,10 +36,10 @@ export class BunPackageManager extends AbstractPackageManager { const workspaceName = scopedPkgJson.name ?? null const workspaceDir = dirname(pattern) - const { targets } = + const { targets, type } = await this.resolver.resolveProjectTargets(workspaceDir) - return { name: workspaceName, location: workspaceDir, targets } + return { name: workspaceName, location: workspaceDir, targets, type } }) ) diff --git a/src/pkg-manager/managers/npm.pkg-manager.ts b/src/pkg-manager/managers/npm.pkg-manager.ts index f585793..54d3ce0 100644 --- a/src/pkg-manager/managers/npm.pkg-manager.ts +++ b/src/pkg-manager/managers/npm.pkg-manager.ts @@ -50,10 +50,10 @@ export class NpmPackageManager extends AbstractPackageManager { const normalizedPath = dependency.resolved.replace(/^file:..\//, '') const absolutePath = resolve(cwd, normalizedPath) - const { targets } = + const { targets, type } = await this.resolver.resolveProjectTargets(absolutePath) - return { name, location: absolutePath, targets } + return { name, location: absolutePath, targets, type } }) ) diff --git a/src/pkg-manager/managers/pnpm.pkg-manager.ts b/src/pkg-manager/managers/pnpm.pkg-manager.ts index 24b51f3..a674fc1 100644 --- a/src/pkg-manager/managers/pnpm.pkg-manager.ts +++ b/src/pkg-manager/managers/pnpm.pkg-manager.ts @@ -31,9 +31,10 @@ export class PnpmPackageManager extends AbstractPackageManager { const isRoot = pathEqual(path, process.cwd()) if (isRoot) return null - const { targets } = await this.resolver.resolveProjectTargets(path) + const { targets, type } = + await this.resolver.resolveProjectTargets(path) - return { name, location: path, targets } + return { name, location: path, targets, type } }) ) diff --git a/src/pkg-manager/managers/yarn-berry.pkg-manager.ts b/src/pkg-manager/managers/yarn-berry.pkg-manager.ts index d2cae91..1b700b5 100644 --- a/src/pkg-manager/managers/yarn-berry.pkg-manager.ts +++ b/src/pkg-manager/managers/yarn-berry.pkg-manager.ts @@ -20,11 +20,11 @@ export class YarnBerryPackageManager extends AbstractPackageManager { const workspaces = await Promise.all( serializedLines.map(async (serializedMeta) => { const project = parseJson(serializedMeta) - const { targets } = await this.resolver.resolveProjectTargets( + const { targets, type } = await this.resolver.resolveProjectTargets( project.location ) - return { ...project, targets } + return { ...project, targets, type } }) ) diff --git a/src/pkg-manager/managers/yarn.pkg-manager.ts b/src/pkg-manager/managers/yarn.pkg-manager.ts index 3b7a8f3..a97bc35 100644 --- a/src/pkg-manager/managers/yarn.pkg-manager.ts +++ b/src/pkg-manager/managers/yarn.pkg-manager.ts @@ -37,9 +37,10 @@ export class YarnPackageManager extends AbstractPackageManager { const yarnWorkspaces = await Promise.all( workspacesEntries.map(async ([name, metadata]) => { const location = toAbsolutePath(metadata.location) - const { targets } = await this.resolver.resolveProjectTargets(location) + const { targets, type } = + await this.resolver.resolveProjectTargets(location) - return { name, location, targets } + return { name, location, targets, type } }) ) diff --git a/src/pkg-manager/pkg-manager.types.ts b/src/pkg-manager/pkg-manager.types.ts index 576a585..a97ec93 100644 --- a/src/pkg-manager/pkg-manager.types.ts +++ b/src/pkg-manager/pkg-manager.types.ts @@ -1,9 +1,11 @@ +import type { TargetType } from '@/resolver/resolver.types' import type { TargetOptions } from '@/resolver/targets/targets-resolver.schema' export interface WorkspaceProject { name: string | null location: string targets: TargetOptions + type: TargetType } export interface RunCommandOptions { diff --git a/src/shared/json.ts b/src/shared/json.ts index 19f4293..0e6dbff 100644 --- a/src/shared/json.ts +++ b/src/shared/json.ts @@ -1,10 +1,11 @@ -import { parseJson } from '@neodx/fs' +import { parseJson, serializeJson } from '@neodx/fs' import type { AnyRecord } from '@neodx/std' import chalk from 'chalk' -import { readFile } from 'node:fs/promises' +import { readFile, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' import process from 'process' import { ERROR_PREFIX } from '@/logger' +import { toAbsolutePath } from '@/shared/misc' export interface PackageJson { name?: string @@ -18,6 +19,8 @@ export interface PackageJson { workspaces?: string[] | (Record & { packages: string[] }) } +export type PackageScripts = NonNullable + export async function readJson( path: string, error = 'Unable to parse JSON string.' @@ -32,3 +35,17 @@ export async function readJson( process.exit(1) } } + +export async function writeJson( + path: string, + content: Result, + error = 'Unable to write JSON.' +) { + try { + await writeFile(toAbsolutePath(path), serializeJson(content)) + } catch { + console.error(`\n ${ERROR_PREFIX} ${chalk.bold(chalk.red(error))}\n`) + + process.exit(1) + } +}