Skip to content

Commit

Permalink
Feat: support create component sub-command
Browse files Browse the repository at this point in the history
  • Loading branch information
Lruihao committed Jan 14, 2025
1 parent 761c93f commit e79ca30
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 29 deletions.
3 changes: 2 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Options:
-h, --help display help for command
Commands:
create|new [project-name] create a new FixIt project from a template
create|new [project-name] create a new FixIt project/component from a template
check check the latest version of FixIt theme
help [command] display help for command
```
Expand Down Expand Up @@ -98,6 +98,7 @@ This CLI tool is developed based on the following projects:
- [FixIt](https://github.com/hugo-fixit/FixIt)
- [hugo-fixit-starter](https://github.com/hugo-fixit/hugo-fixit-starter)
- [hugo-fixit-starter1](https://github.com/hugo-fixit/hugo-fixit-starter1)
- [component-skeleton](https://github.com/hugo-fixit/component-skeleton)

## Author

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Options:
-h, --help display help for command
Commands:
create|new [project-name] create a new FixIt project from a template
create|new [project-name] create a new FixIt project/component from a template
check check the latest version of FixIt theme
help [command] display help for command
```
Expand Down Expand Up @@ -96,7 +96,6 @@ pnpm remove -g fixit-cli
- [ ] 检查是否安装 Hugo,没有安装,可通过 Node 包安装 `hugo-bin`/`hugo-extended`
- [ ] 执行 `fixit check` 命令后,获取到新版本后,提示是否更新
- [ ] 新增 `fixit add` 命令,用于添加新的 FixIt 主题组件(`fixit create` 命令增加主题组件选项)
- [ ] `fixit create component` 子命令,用于创建新的 FixIt 主题组件

## 相关项目

Expand All @@ -105,6 +104,7 @@ pnpm remove -g fixit-cli
- [FixIt](https://github.com/hugo-fixit/FixIt)
- [hugo-fixit-starter](https://github.com/hugo-fixit/hugo-fixit-starter)
- [hugo-fixit-starter1](https://github.com/hugo-fixit/hugo-fixit-starter1)
- [component-skeleton](https://github.com/hugo-fixit/component-skeleton)

## 作者

Expand Down
163 changes: 141 additions & 22 deletions src/bin/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from 'simple-git'
import {
getLatestRelease,
handleTargetDir,
type ReleaseInfo,
timer,
} from '../lib/utils.js'
Expand Down Expand Up @@ -86,28 +87,7 @@ async function createAction(projectName: string) {
},
}) as string
}
// check if target directory is empty
let targetDir = answers.name
if (fs.existsSync(answers.name)) {
const action = await p.select({
message: `Target Directory ${answers.name} is not empty. Please choose how to proceed:`,
options: [
{ value: 'cancel', label: 'Cancel operation' },
{ value: 'rename', label: 'Rename target directory' },
{ value: 'remove', label: 'Remove existing files and continue' },
],
})
if (action === 'cancel' || p.isCancel(action)) {
p.cancel('Operation cancelled.')
process.exit(0)
}
if (action === 'rename') {
targetDir = `${answers.name}-${Date.now().toString(36)}`
}
else if (action === 'remove') {
shell.rm('-rf', answers.name)
}
}
const targetDir = await handleTargetDir(answers.name)
p.log.step(`Initializing FixIt project ${targetDir}, please wait a moment...`)
// 1. download template
const spinnerClone = p.spinner()
Expand Down Expand Up @@ -278,6 +258,144 @@ async function createAction(projectName: string) {
})
}

/**
* action for create component subcommand
* clone https://github.com/hugo-fixit/component-skeleton
* @param {string} componentName component name
* @example fixit create component [component-name]
*/
async function createComponentAction(componentName: string) {
timer.start('Creating a new FixIt component step by step!')
const repository = 'https://github.com/hugo-fixit/component-skeleton'
const answers = await p.group(
{
name: () => p.text({
message: 'Please input component name:',
placeholder: 'Component name, e.g. `my-component`',
initialValue: componentName || '',
validate: (val: string) => {
if (val === '') {
return 'Component name is required!'
}
},
}),
author: () => p.text({
message: 'Please input your GitHub username:',
placeholder: 'GitHub username, e.g. `Lruihao`',
initialValue: 'hugo-fixit',
validate: (val: string) => {
if (val === '') {
return 'GitHub username is required!'
}
},
}),
},
{
onCancel: () => {
p.cancel('Operation cancelled.')
process.exit(0)
},
},
)
const targetDir = await handleTargetDir(answers.name)
p.log.step(`Initializing FixIt component ${targetDir}, please wait a moment...`)
// 1. download skeleton
const spinnerClone = p.spinner()
spinnerClone.start(`Skeleton downloading from ${c.cyan(repository)}.`)
const progress = ({ method, stage, progress }: SimpleGitProgressEvent) => {
spinnerClone.message(c.yellow(`git.${method} ${stage} stage ${progress}% complete${'.'.repeat(Math.floor(Math.random() * 3) + 1)}`))
}
const git: SimpleGit = simpleGit({ progress })
git.clean(CleanOptions.FORCE)
git.clone(repository, targetDir, { '--depth': 1, '--branch': 'main', '--single-branch': null }, (err) => {
if (err) {
spinnerClone.stop(err.message, -1)
return
}
spinnerClone.stop(`${c.green('✔')} Skeleton downloaded from ${c.cyan(repository)}`, 0)
// 2. initialize FixIt component
const spinnerInit = p.spinner()
spinnerInit.start(`Initializing FixIt component ${targetDir}.`)
// remove remote origin
git.cwd(targetDir)
spinnerInit.message('Removing remote origin.')
git.removeRemote('origin', (err) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
spinnerInit.message(`${c.green('✔')} removed remote origin.`)
})
// initialize Hugo module
spinnerInit.message('Initializing Hugo module.')
const goMod = join(process.cwd(), targetDir, 'go.mod')
fs.readFile(goMod, 'utf8', (err, data) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
const result = data.replace(/module .*/, `module github.com/${answers.author}/${answers.name}`)
fs.writeFile(goMod, result, 'utf8', (err) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
spinnerInit.message(`${c.green('✔')} modified module path in go.mod.`)
})
})
// modify LICENSE
const license = join(process.cwd(), targetDir, 'LICENSE')
fs.readFile(license, 'utf8', (err, data) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
const currentYear = new Date().getFullYear()
const result = data.replace(/Copyright \(c\) \d+ .*/, `Copyright (c) ${currentYear} ${answers.author} (https://github.com/${answers.author})`)
fs.writeFile(license, result, 'utf8', (err) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
spinnerInit.message(`${c.green('✔')} modified author in LICENSE.`)
})
})
// modify README.md and README.en.md
for (const readmeFile of ['README.md', 'README.en.md']) {
const readme = join(process.cwd(), targetDir, readmeFile)
fs.readFile(readme, 'utf8', (err, data) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
const result = data.replace(/hugo-fixit\/\{component-xxx\}/g, `${answers.author}/${answers.name}`)
.replace(/\{component-xxx\}/g, answers.name)
fs.writeFile(readme, result, 'utf8', (err) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
spinnerInit.message(`${c.green('✔')} modified author in ${readmeFile}.`)
})
})
}
// 3. commit first commit and remove history commits
spinnerInit.message('Removing history commits.')
git.raw(['update-ref', '-d', 'HEAD'], (err) => {
if (err) {
spinnerInit.stop(err.message, -1)
return
}
spinnerInit.message(`${c.green('✔')} removed history commits.`)
}).then(async () => {
await git.add('./*')
await git.commit('first commit')
spinnerInit.stop(`${c.green('✔')} FixIt component ${targetDir} initialized!`, 0)
p.log.success('🎉 Congratulations! You have created a new FixIt component.')
p.outro(`Done in ${timer.stop() / 1000}s`)
})
})
}
/**
* action for check command
* @example fixit check
Expand Down Expand Up @@ -312,4 +430,5 @@ function checkAction() {
export {
checkAction,
createAction,
createComponentAction,
}
12 changes: 11 additions & 1 deletion src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { importJson } from '../lib/utils.js'
import {
checkAction,
createAction,
createComponentAction,
} from './actions.js'

const pkg = importJson('/package.json')
Expand All @@ -30,14 +31,23 @@ FixIt is a clean, elegant but advanced blog theme for Hugo
built with love by Lruihao and his friends.\n
Complete documentation is available at ${c.cyan('https://fixit.lruihao.cn/')}.`

// define subcommand `create component` under `create`
const createComponentCmd = new Command('component')
.alias('cmpt')
.description('create a new component from a template')
.argument('[component-name]', 'name of the component to create')
.action(createComponentAction)

// define commands
program
.command('create')
.alias('new')
.description('create a new FixIt project from a template')
.usage('[project-name]|<command>')
.description('create a new FixIt project/component from a template')
.argument('[project-name]', 'FixIt project name, e.g. `my-blog`')
.helpOption(false)
.action(createAction)
.addCommand(createComponentCmd)
program
.command('check')
.description('check the latest version of FixIt theme')
Expand Down
37 changes: 34 additions & 3 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { readFileSync } from 'node:fs'
import fs, { readFileSync } from 'node:fs'
import https from 'node:https'
import { dirname, join } from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { intro } from '@clack/prompts'
import * as p from '@clack/prompts'
import shell from 'shelljs'

interface ReleaseInfo {
version: string
Expand Down Expand Up @@ -77,12 +78,41 @@ function getLatestRelease(repoOwner: string, repoName: string): Promise<ReleaseI
})
}

/**
* handle target directory
* @param {string} targetDir target directory
* @returns {Promise<string>} target directory
*/
async function handleTargetDir(targetDir: string): Promise<string> {
if (fs.existsSync(targetDir)) {
const action = await p.select({
message: `Target Directory ${targetDir} is not empty. Please choose how to proceed:`,
options: [
{ value: 'cancel', label: 'Cancel operation' },
{ value: 'rename', label: 'Rename target directory' },
{ value: 'remove', label: 'Remove existing files and continue' },
],
})
if (action === 'cancel' || p.isCancel(action)) {
p.cancel('Operation cancelled.')
process.exit(0)
}
if (action === 'rename') {
targetDir = `${targetDir}-${Date.now().toString(36)}`
}
else if (action === 'remove') {
shell.rm('-rf', targetDir)
}
}
return Promise.resolve(targetDir)
}

const timer: Timer = {
__start: 0,
__end: 0,
start: (msg): void => {
timer.__start = Date.now()
msg && intro(msg)
msg && p.intro(msg)
},
stop: (): number => {
timer.__end = Date.now()
Expand All @@ -92,6 +122,7 @@ const timer: Timer = {

export {
getLatestRelease,
handleTargetDir,
importJson,
ReleaseInfo,
timer,
Expand Down

0 comments on commit e79ca30

Please sign in to comment.