From 4436f5bb66b8d8fa3eb3c9e964506cd188dfd355 Mon Sep 17 00:00:00 2001 From: Uzhanin Egor Date: Wed, 13 Mar 2024 14:45:24 +0300 Subject: [PATCH] feat: custom configuration --- package.json | 1 + pnpm-lock.yaml | 7 +++ shims.d.ts | 6 +++ src/app/app.module.ts | 3 +- src/commands/run/run.command.ts | 4 +- src/config/config.module.ts | 10 +++++ src/config/config.schema.ts | 16 +++++++ src/config/config.service.ts | 42 ++++++++++++++++++ src/config/index.ts | 2 + src/resolver/resolver.service.ts | 43 ++++++++++++++----- src/resolver/resolver.types.ts | 6 ++- .../targets/targets-resolver.service.ts | 12 +++--- src/shared/types.ts | 3 ++ 13 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 src/config/config.module.ts create mode 100644 src/config/config.schema.ts create mode 100644 src/config/config.service.ts create mode 100644 src/config/index.ts create mode 100644 src/shared/types.ts diff --git a/package.json b/package.json index 26f2127..48d9450 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "devDependencies": { "@antfu/ni": "^0.21.12", + "@types/ini": "^4.1.0", "@types/inquirer": "^9.0.7", "@types/node": "^20.10.7", "@types/which": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df059ad..077e284 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@antfu/ni': specifier: ^0.21.12 version: 0.21.12 + '@types/ini': + specifier: ^4.1.0 + version: 4.1.0 '@types/inquirer': specifier: ^9.0.7 version: 9.0.7 @@ -1491,6 +1494,10 @@ packages: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} dev: true + /@types/ini@4.1.0: + resolution: {integrity: sha512-mTehMtc+xtnWBBvqizcqYCktKDBH2WChvx1GU3Sfe4PysFDXiNe+1YwtpVX1MDtCa4NQrSPw2+3HmvXHY3gt1w==} + dev: true + /@types/inquirer@9.0.7: resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==} dependencies: diff --git a/shims.d.ts b/shims.d.ts index 75d4509..0d234cb 100644 --- a/shims.d.ts +++ b/shims.d.ts @@ -1,3 +1,9 @@ declare module 'inquirer-tree-prompt' declare module 'inquirer-search-list' declare module 'envfile' + +declare namespace NodeJS { + interface Process { + GX_CONFIG_PATH: string + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d47cf49..d0a544f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common' import { Commands } from '@/commands' +import { ConfigModule } from '@/config' import { LoggerModule } from '@/logger' import { PackageManagerModule } from '@/pkg-manager' @Module({ - imports: [PackageManagerModule, LoggerModule], + imports: [PackageManagerModule, LoggerModule, ConfigModule], providers: [...Commands] }) export class AppModule {} diff --git a/src/commands/run/run.command.ts b/src/commands/run/run.command.ts index 499bb57..4f8730e 100644 --- a/src/commands/run/run.command.ts +++ b/src/commands/run/run.command.ts @@ -1,7 +1,7 @@ import { ensureDir } from '@neodx/fs' import { hasOwn, isEmpty, isObject } from '@neodx/std' import { Inject } from '@nestjs/common' -import { execaCommand } from 'execa' +import { execaCommand as $ } from 'execa' import { Command, CommandRunner, Option } from 'nest-commander' import { dirname } from 'node:path' import { buildTargetInfoPrompt } from '@/commands/run/run.prompts' @@ -102,7 +102,7 @@ export class RunCommand extends CommandRunner { { defaultArgs: options.args, projectCwd } ) - await execaCommand(`${command} ${args}`, { + await $(`${command} ${args}`, { cwd, env }) diff --git a/src/config/config.module.ts b/src/config/config.module.ts new file mode 100644 index 0000000..dcba204 --- /dev/null +++ b/src/config/config.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common' +import { ConfigService } from './config.service' + +@Global() +@Module({ + controllers: [], + providers: [ConfigService], + exports: [ConfigService] +}) +export class ConfigModule {} diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts new file mode 100644 index 0000000..9555b99 --- /dev/null +++ b/src/config/config.schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +export const ConfigSchema = z.object({ + commandsFile: z.string().default('targets.json'), + preferredResolvingOrder: z + .array(z.union([z.literal('package-scripts'), z.literal('targets')])) + .default(['targets', 'package-scripts']) +}) + +function createConfigValues() { + return class ConfigValues {} as { + new (): z.infer + } +} + +export class ConfigValues extends createConfigValues() {} diff --git a/src/config/config.service.ts b/src/config/config.service.ts new file mode 100644 index 0000000..373e76b --- /dev/null +++ b/src/config/config.service.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common' +import ini from 'ini' +import { existsSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import process from 'node:process' +import { ConfigSchema, ConfigValues } from '@/config/config.schema' +import { LoggerService } from '@/logger' + +@Injectable() +export class ConfigService extends ConfigValues { + constructor(@Inject(LoggerService) private readonly logger: LoggerService) { + super() + Object.assign(this, this.config) + } + + private get config() { + const userConfig = existsSync(this.configPath) + ? ini.parse(readFileSync(this.configPath).toString()) + : {} + + try { + return ConfigSchema.parse(userConfig) + } catch (error) { + this.logger.error(error) + process.exit(1) + } + } + + private get configPath() { + const customRcPath = process.env.GX_CONFIG_PATH + + const isWindows = process.platform === 'win32' + const home = isWindows ? process.env.USERPROFILE : process.env.HOME + + const defaultRcPath = resolve(home ?? '~/', '.gxrc') + const projectRcPath = resolve(process.cwd(), '.gxrc') + + const baseRcPath = customRcPath ?? defaultRcPath + + return existsSync(projectRcPath) ? projectRcPath : baseRcPath + } +} diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..34970cc --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,2 @@ +export { ConfigModule } from './config.module' +export { ConfigService } from './config.service' diff --git a/src/resolver/resolver.service.ts b/src/resolver/resolver.service.ts index 2e5595c..101ad1d 100644 --- a/src/resolver/resolver.service.ts +++ b/src/resolver/resolver.service.ts @@ -1,29 +1,50 @@ -import { isNull } from '@neodx/std' -import { Injectable } from '@nestjs/common' +import { entries, sortObjectByOrder } from '@neodx/std' +import { Inject, Injectable } from '@nestjs/common' +import { ConfigService } from '@/config' +import { LoggerService } from '@/logger' import type { PackageJsonResolverService } from '@/resolver/package/package.resolver.service' -import type { ResolvedTargets } from '@/resolver/resolver.types' +import type { ResolvedTargets, TargetType } from '@/resolver/resolver.types' +import type { TargetOptions } from '@/resolver/targets/targets-resolver.schema' import type { TargetsResolverService } from '@/resolver/targets/targets-resolver.service' @Injectable() export class ResolverService { constructor( private readonly targetsResolver: TargetsResolverService, - private readonly pkgResolver: PackageJsonResolverService + private readonly pkgResolver: PackageJsonResolverService, + @Inject(ConfigService) private readonly cfg: ConfigService, + @Inject(LoggerService) private readonly logger: LoggerService ) {} public async resolveProjectTargets(cwd: string): Promise { - const targets = await this.targetsResolver.resolveProjectTargets(cwd) + const sortedResolvers = sortObjectByOrder( + this.resolverMethods, + this.cfg.preferredResolvingOrder + ) - if (isNull(targets)) { - return { - type: 'package-scripts', - targets: await this.pkgResolver.resolvePackageJsonScripts(cwd) + for (const [type, resolveTargets] of entries(sortedResolvers)) { + const targets = await resolveTargets.call(this, cwd) + + if (targets) { + return { type, targets } } } + this.logger.error( + `Error occurred while resolving project targets. + Please check if preferredResolvingOrder is set correctly` + ) + process.exit(1) + } + + private get resolverMethods(): Record< + TargetType, + (cwd: string) => Promise + > { return { - type: 'targets', - targets + 'package-scripts': this.pkgResolver.resolvePackageJsonScripts, + // eslint-disable-next-line prettier/prettier + 'targets': this.targetsResolver.resolveProjectTargets } } } diff --git a/src/resolver/resolver.types.ts b/src/resolver/resolver.types.ts index f873169..9d4bafe 100644 --- a/src/resolver/resolver.types.ts +++ b/src/resolver/resolver.types.ts @@ -1,6 +1,10 @@ +import type { ConfigValues } from '@/config/config.schema' import type { TargetOptions } from '@/resolver/targets/targets-resolver.schema' +import type { InferArrayItem } from '@/shared/types' + +export type TargetType = InferArrayItem export interface ResolvedTargets { - type: 'package-scripts' | 'targets' + type: TargetType targets: TargetOptions } diff --git a/src/resolver/targets/targets-resolver.service.ts b/src/resolver/targets/targets-resolver.service.ts index 0a1f7f0..d100593 100644 --- a/src/resolver/targets/targets-resolver.service.ts +++ b/src/resolver/targets/targets-resolver.service.ts @@ -3,6 +3,7 @@ import type { AnyRecord } from '@neodx/std' import { Inject, Injectable } from '@nestjs/common' import { resolve } from 'node:path' import { z } from 'zod' +import { ConfigService } from '@/config' import { LoggerService } from '@/logger' import { readJson } from '@/shared/json' import type { TargetOptions } from './targets-resolver.schema' @@ -11,13 +12,14 @@ import { TargetsSchema } from './targets-resolver.schema' @Injectable() export class TargetsResolverService { constructor( - @Inject(LoggerService) private readonly loggerService: LoggerService + @Inject(LoggerService) private readonly logger: LoggerService, + @Inject(ConfigService) private readonly cfg: ConfigService ) {} public async resolveProjectTargets( projectCwd: string ): Promise { - const targetJsonPath = resolve(projectCwd, 'commands.json') + const targetJsonPath = resolve(projectCwd, this.cfg.commandsFile) if (!exists(targetJsonPath)) return null @@ -36,14 +38,14 @@ export class TargetsResolverService { const isZodError = error_ instanceof z.ZodError if (!isZodError) { - this.loggerService.error('Unknown error parsing commands.json') + this.logger.error('Unknown error parsing commands.json') throw error_ } - this.loggerService.error( + this.logger.error( `Invalid commands.json file. (${projectPath}) See below for detailed info. \n` ) - this.loggerService.error(error_.format()) + this.logger.error(error_.format()) process.exit(1) } diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..4c36c3c --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,3 @@ +export type InferArrayItem = T extends (infer S)[] + ? S + : never