Skip to content

Commit

Permalink
refactor(cli): ♻️ reorganize the entire structure of the code to impr…
Browse files Browse the repository at this point in the history
…ove its organization
  • Loading branch information
shlroland committed Oct 16, 2024
1 parent d251adb commit 4d25537
Show file tree
Hide file tree
Showing 37 changed files with 601 additions and 582 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-candles-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shlroland/lint-cli": minor
---

Reorganize the entire structure of the code to improve its organization
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { tsImport } from 'tsx/esm/api'

const { shlroland } = (await tsImport('./packages/eslint/index.ts', import.meta.url))

export default shlroland()
export default shlroland({
})
2 changes: 1 addition & 1 deletion packages/cli/bin/index.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
'use strict'

import '../dist/cli.js'
import '../dist/main.js'
49 changes: 49 additions & 0 deletions packages/cli/src/answer/abstract/answer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Config } from './config'
import type { Installer } from './install'
import { deleteFile, shouldOverridePrompt } from '../../utils'

export abstract class AbstractAnswer {
static toolName: string

abstract answerName: string

abstract installer: Installer

abstract config: Config

async configGuard(): Promise<void> {
const configs = this.config.pendingConfigs
for (const config of configs) {
if (await config.checkConfigFileExisted()) {
const shouldOverride = await shouldOverridePrompt(this.answerName)
if (shouldOverride) {
config.overrideFile = async () => {
await deleteFile(config.configFilePath)
}
this.config.addPendingConfig(config)
}
else {
this.config.removePendingConfig(config)
}
}
}
}

get pendingPackages(): string[] {
return this.installer.pendingPackages
}

get pendingConfigs(): { configFilePath: string, configFileContent: string, overrideFile: () => Promise<void> }[] {
const result: { configFilePath: string, configFileContent: string, overrideFile: () => Promise<void> }[] = []

for (const config of this.config.pendingConfigs) {
result.push({
configFilePath: config.configFilePath,
configFileContent: config.configFileContent,
overrideFile: config.overrideFile,
})
}

return result
}
}
104 changes: 104 additions & 0 deletions packages/cli/src/answer/abstract/config-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { cjsConfigFactory, ensureConfig, esmConfigFactory } from '../../utils'
import { AnswerContext } from './context'

export abstract class ConfigOption {
protected context: AnswerContext = AnswerContext.instance

abstract configFileName: string

abstract configFilePath: string

abstract configFileContent: string

abstract checkConfigFileExisted(): Promise<boolean>

async overrideFile(): Promise<void> {
return Promise.resolve()
}
}

export class CommonConfigOption extends ConfigOption {
configFileName: string

configFilePath: string

configFileContent: string

constructor(options: { configFileName: string, content: string }) {
super()
this.configFileName = options.configFileName
this.configFilePath = path.join(this.context.cwd, this.configFileName)
this.configFileContent = options.content
}

async checkConfigFileExisted(): Promise<boolean> {
const filePath = path.resolve(process.cwd(), this.configFilePath)
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false)
return exists
}
}

export class CosmiConfigOption extends ConfigOption {
configFileName: string

configFilePath: string

configFileContent: string

private esmImportConfigContent: string

private esmExportConfigContent: string

private cjsImportConfigContent: string

private cjsExportConfigContent: string

private checkConfigNames: string[]

constructor(
options: {
configFileName: string
esmImportConfigContent: string
esmExportConfigContent: string
cjsImportConfigContent: string
cjsExportConfigContent: string
checkConfigNames: string[]
},
) {
super()
this.configFileName = options.configFileName
this.esmImportConfigContent = options.esmImportConfigContent
this.esmExportConfigContent = options.esmExportConfigContent
this.cjsImportConfigContent = options.cjsImportConfigContent
this.cjsExportConfigContent = options.cjsExportConfigContent
this.checkConfigNames = options.checkConfigNames
const { content, path } = this.configFileContentFactory()
this.configFileContent = content
this.configFilePath = path
}

private configFileContentFactory(): { content: string, path: string } {
const content = this.context.moduleType === 'module'
? esmConfigFactory(this.esmExportConfigContent, this.esmImportConfigContent)
: cjsConfigFactory(this.cjsExportConfigContent, this.cjsImportConfigContent)

return {
content,
path: path.join(this.context.cwd, this.configFileName),
}
}

async checkConfigFileExisted(): Promise<boolean> {
const checkConfigNames = this.checkConfigNames
for (const configName of checkConfigNames) {
const config = await ensureConfig(configName)
if (config) {
return true
}
}
return false
}
}
27 changes: 27 additions & 0 deletions packages/cli/src/answer/abstract/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ConfigOption } from './config-option'

export class Config {
#pendingConfigs: Map<string, ConfigOption> = new Map()

constructor(defaultConfigs: ConfigOption[]) {
for (const config of defaultConfigs) {
this.#pendingConfigs.set(config.configFileName, config)
}
}

addPendingConfig(config: ConfigOption) {
this.#pendingConfigs.set(config.configFileName, config)
}

removePendingConfig(config: ConfigOption) {
this.#pendingConfigs.delete(config.configFileName)
}

clearPendingConfigs() {
this.#pendingConfigs.clear()
}

get pendingConfigs() {
return this.#pendingConfigs.values()
}
}
32 changes: 32 additions & 0 deletions packages/cli/src/answer/abstract/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import process from 'node:process'
import { getModuleType } from '../../utils'

export interface Context {
moduleType: 'module' | 'commonjs'
cwd: string
}

export class AnswerContext {
moduleType: 'module' | 'commonjs'

cwd: string

constructor(context: Context) {
this.cwd = context.cwd

this.moduleType = context.moduleType
}

static instance: AnswerContext
}

export async function initAnswerContext() {
const cwd = process.cwd()
const moduleType = await getModuleType(cwd)
const context = {
moduleType,
cwd,
}

AnswerContext.instance = new AnswerContext(context)
}
5 changes: 5 additions & 0 deletions packages/cli/src/answer/abstract/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class Installer extends Set<string> {
get pendingPackages(): string[] {
return Array.from(this.values())
}
}
23 changes: 23 additions & 0 deletions packages/cli/src/answer/commitlint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AbstractAnswer } from './abstract/answer'
import { Config } from './abstract/config'
import { CosmiConfigOption } from './abstract/config-option'
import { Installer } from './abstract/install'

export class CommitlintAnswer extends AbstractAnswer {
static toolName = 'commitlint'

answerName = 'commitlint'

installer = new Installer(['czg', '@commitlint/cli', '@shlroland/cz-config'])

config = new Config([
new CosmiConfigOption({
configFileName: 'commitlint.config.js',
esmImportConfigContent: ``,
esmExportConfigContent: `{ extends: ['@shlroland/cz-config/commitlint'] }`,
cjsImportConfigContent: ``,
cjsExportConfigContent: `{ extends: ['@shlroland/cz-config/commitlint'] }`,
checkConfigNames: ['commitlint'],
}),
])
}
25 changes: 25 additions & 0 deletions packages/cli/src/answer/eslint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AbstractAnswer } from './abstract/answer'
import { Config } from './abstract/config'
import { CosmiConfigOption } from './abstract/config-option'
import { Installer } from './abstract/install'

export class EslintAnswer extends AbstractAnswer {
static toolName = 'eslint'

answerName = 'eslint'

installer = new Installer(['eslint', '@shlroland/eslint-config', 'eslint-plugin-format'])

config = new Config(
[
new CosmiConfigOption({
configFileName: 'eslint.config.js',
esmImportConfigContent: 'import { shlroland } from "@shlroland/eslint-config"',
esmExportConfigContent: 'shlroland()',
cjsImportConfigContent: 'const { shlroland } = require("@shlroland/eslint-config")',
cjsExportConfigContent: 'shlroland()',
checkConfigNames: ['eslint'],
}),
],
)
}
56 changes: 56 additions & 0 deletions packages/cli/src/answer/husky.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import huskyConfig from '@shlroland/husky-config'
import { deleteFile, initGit, isGitRepository, shouldInitGitPrompt, shouldOverridePrompt } from '../utils'
import { AbstractAnswer } from './abstract/answer'
import { Config } from './abstract/config'
import { CommonConfigOption } from './abstract/config-option'
import { AnswerContext } from './abstract/context'
import { Installer } from './abstract/install'

export class HuskyAnswer extends AbstractAnswer {
static toolName = 'husky'

context = AnswerContext.instance

answerName = 'husky'

installer = new Installer(['husky'])

config = new Config([
new CommonConfigOption({
configFileName: '.husky/pre-commit',
content: huskyConfig.hooks['pre-commit'],
}),
new CommonConfigOption({
configFileName: '.husky/commit-msg',
content: huskyConfig.hooks['commit-msg'],
}),
])

async configGuard(): Promise<void> {
const hasGit = await isGitRepository(this.context.cwd)
if (!hasGit) {
const shouldInitGit = await shouldInitGitPrompt()
if (!shouldInitGit) {
this.config.clearPendingConfigs()
return
}
await initGit()
}

const configs = this.config.pendingConfigs
for (const config of configs) {
if (await config.checkConfigFileExisted()) {
const shouldOverride = await shouldOverridePrompt(config.configFileName)
if (shouldOverride) {
config.overrideFile = async () => {
await deleteFile(config.configFilePath)
}
this.config.addPendingConfig(config)
}
else {
this.config.removePendingConfig(config)
}
}
}
}
}
47 changes: 47 additions & 0 deletions packages/cli/src/answer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { AbstractAnswer } from './abstract/answer'
import * as p from '@clack/prompts'
import { onCancel } from '../utils'
import { CommitlintAnswer } from './commitlint'
import { EslintAnswer } from './eslint'
import { HuskyAnswer } from './husky'
import { LintStagedAnswer } from './lint-staged'

export const answers = [
new EslintAnswer(),
new LintStagedAnswer(),
new CommitlintAnswer(),
new HuskyAnswer(),
]

export const toSelectAnswers = new Map([
[EslintAnswer.toolName, EslintAnswer],
[LintStagedAnswer.toolName, LintStagedAnswer],
[CommitlintAnswer.toolName, CommitlintAnswer],
[HuskyAnswer.toolName, HuskyAnswer],
])

export async function getAnswers(): Promise<AbstractAnswer[]> {
const answerKeys = Array.from(toSelectAnswers.keys())
const options = answerKeys.map(answer => ({
value: answer,
}))

const selectedAnswers = await p.multiselect({
message: 'Please select lint tools to install:',
options,
initialValues: answerKeys,
})

if (p.isCancel(selectedAnswers)) {
onCancel()
return []
}

return selectedAnswers.map((answer) => {
const AnswerCls = toSelectAnswers.get(answer)
if (!AnswerCls) {
throw new Error(`Answer ${answer} not found`)
}
return new AnswerCls()
})
}
Loading

0 comments on commit 4d25537

Please sign in to comment.