Skip to content

Commit 86ea25c

Browse files
committed
feat: command generate plugin
1 parent b69599b commit 86ea25c

File tree

17 files changed

+843
-1
lines changed

17 files changed

+843
-1
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,36 @@ USAGE
2020
<!-- usagestop -->
2121
# Commands
2222
<!-- commands -->
23+
* [`fastify generate plugin [NAME]`](#fastify-generate-plugin-name)
2324
* [`fastify generate project [NAME]`](#fastify-generate-project-name)
2425
* [`fastify help [COMMAND]`](#fastify-help-command)
2526
* [`fastify start ENTRY`](#fastify-start-entry)
2627

28+
## `fastify generate plugin [NAME]`
29+
30+
Generate fastify plugin
31+
32+
```
33+
USAGE
34+
$ fastify generate plugin [NAME] [--location <value>] [--overwrite] [--repo <value>] [--language <value>] [--lint
35+
<value>] [--test <value>] [--help]
36+
37+
ARGUMENTS
38+
NAME Name of the plugin
39+
40+
FLAGS
41+
--help Show CLI help.
42+
--language=<value> Programming Language you would like to use in this project.
43+
--lint=<value> Lint Tools you would like to use in this project.
44+
--location=<value> Location to place the project.
45+
--overwrite Force to overwrite the project location when it exist.
46+
--repo=<value> Git repository url of the project.
47+
--test=<value> Test Framework you would like to use in this project.
48+
49+
DESCRIPTION
50+
Generate fastify plugin
51+
```
52+
2753
## `fastify generate project [NAME]`
2854

2955
Generate fastify project

src/commands/generate/plugin.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { Flags } from '@oclif/core'
2+
import { execSync } from 'child_process'
3+
import { compile } from 'ejs'
4+
import { access, mkdir, readFile, rm, stat, writeFile } from 'fs/promises'
5+
import { prompt } from 'inquirer'
6+
import { basename, dirname, join, resolve } from 'path'
7+
import { Command } from '../../utils/command/command'
8+
import { computePackageJSON } from '../../utils/package-json/plugin'
9+
import { toCamelCase } from '../../utils/string'
10+
11+
export default class Plugin extends Command {
12+
static description = 'Generate fastify plugin'
13+
14+
static args = [
15+
{ name: 'name', required: false, description: 'Name of the plugin' }
16+
]
17+
18+
static flags = {
19+
location: Flags.string({ description: 'Location to place the project.' }),
20+
overwrite: Flags.boolean({ description: 'Force to overwrite the project location when it exist.', default: false }),
21+
repo: Flags.string({ description: 'Git repository url of the project.' }),
22+
language: Flags.string({ description: 'Programming Language you would like to use in this project.' }),
23+
lint: Flags.string({ description: 'Lint Tools you would like to use in this project.' }),
24+
test: Flags.string({ description: 'Test Framework you would like to use in this project.' }),
25+
help: Flags.help()
26+
}
27+
28+
shouldOverwrite = false
29+
30+
async run (): Promise<void> {
31+
const { args, flags } = await this.parse(Plugin)
32+
33+
// validate
34+
if (flags.language !== undefined) this.corceLanguage(flags.language)
35+
36+
const answer: any = {}
37+
38+
Object.assign(answer, await prompt([
39+
{ type: 'input', name: 'name', message: 'What is your project name?', validate: this.questionNameValidate },
40+
{ type: 'input', name: 'location', message: 'Where do you want to place your project?', default: this.questionLocationDefault },
41+
{ type: 'confirm', name: 'overwrite', message: 'The folder already exist. Do you want to overwrite?', default: flags.overwrite ?? false, when: this.questionOverwriteWhen, askAnswered: true }
42+
], {
43+
name: args.name,
44+
location: flags.location,
45+
overwrite: flags.overwrite
46+
}))
47+
48+
if (this.shouldOverwrite) this.questionOverwriteValidate(answer.overwrite)
49+
50+
Object.assign(answer, await prompt([
51+
{ type: 'input', name: 'repo', message: 'What is the repo url?' },
52+
{ type: 'list', name: 'language', message: 'Which language will you use?', default: 'JavaScript', choices: ['JavaScript'] },
53+
{ type: 'list', name: 'lint', message: 'Which linter would you like to use?', default: this.questionLintDefault, choices: this.questionLintChoices },
54+
{ type: 'list', name: 'test', message: 'Which test framework would you like to use?', default: 'tap', choices: ['tap'] }
55+
], {
56+
repo: flags.repo,
57+
language: flags.language,
58+
lint: flags.lint,
59+
test: flags.test
60+
}))
61+
62+
answer.camelCaseName = toCamelCase(answer.name)
63+
64+
const fileList = await this.computeFileList(answer)
65+
const files = await this.prepareFiles(fileList, answer)
66+
await this.writeFiles(files, answer)
67+
await this.npmInstall(answer)
68+
this.log(`project "${answer.name as string}" initialized in "${answer.location as string}"`)
69+
}
70+
71+
questionNameValidate = (input: string): true | string => {
72+
if (String(input).trim() === '') {
73+
return 'Project Name cannot be empty.'
74+
}
75+
return true
76+
}
77+
78+
questionLocationDefault = (answer: any): string => {
79+
return this.toLocation(answer.name)
80+
}
81+
82+
questionOverwriteWhen = async (answer: any): Promise<boolean> => {
83+
this.shouldOverwrite = !(await this.validateProjectLocation(answer.location))
84+
return this.shouldOverwrite
85+
}
86+
87+
questionOverwriteValidate = (input: boolean): true | undefined => {
88+
if (input) return true
89+
this.log('Terminated because location cannot be overwrite.')
90+
this.exit(0)
91+
}
92+
93+
questionLintDefault =(answer: any): string => {
94+
switch (this.corceLanguage(answer.language)) {
95+
case 'JavaScript':
96+
return 'standard'
97+
}
98+
}
99+
100+
questionLintChoices = (answer: any): string[] => {
101+
switch (this.corceLanguage(answer.language)) {
102+
case 'JavaScript':
103+
return ['standard', 'eslint', 'eslint + standard']
104+
}
105+
}
106+
107+
corceLanguage (str: string): 'JavaScript' {
108+
switch (str.trim().toLowerCase()) {
109+
case 'js':
110+
case 'javascript':
111+
return 'JavaScript'
112+
default:
113+
throw new Error(`Programming Language expected to be "JavaScript", but recieved "${str}"`)
114+
}
115+
}
116+
117+
toLocation (name: string): string {
118+
return name.trim().toLowerCase().replace(/\s+/g, '-')
119+
}
120+
121+
async isFileExist (path: string): Promise<boolean> {
122+
try {
123+
const stats = await stat(path)
124+
return stats.isFile()
125+
} catch {
126+
return false
127+
}
128+
}
129+
130+
async validateProjectLocation (location: string): Promise<boolean> {
131+
const path = resolve(location)
132+
try {
133+
await access(path)
134+
return false
135+
} catch {
136+
return true
137+
}
138+
}
139+
140+
computeFileList (answer: any): string[] {
141+
// we do not add .ejs in here
142+
// we should find the file if .ejs exist first and then compile to the destination
143+
const files: string[] = [
144+
'README.md',
145+
'__gitignore',
146+
'.github/dependabot.yml',
147+
'.github/workflows/ci.yml'
148+
]
149+
150+
if (answer.language === 'JavaScript') {
151+
files.push('tsconfig.json')
152+
files.push('index.js')
153+
files.push('index.d.ts')
154+
files.push('test/index.test.js')
155+
files.push('test/index.test-d.ts')
156+
}
157+
158+
return files
159+
}
160+
161+
async resolveFile (file: string): Promise<{ template: boolean, content: string }> {
162+
const o = { template: false, content: '' }
163+
const ejsPath = resolve(join('templates', 'plugin', `${file}.ejs`))
164+
const isEJSTemplate = await this.isFileExist(ejsPath)
165+
if (isEJSTemplate) {
166+
o.template = true
167+
const data = await readFile(ejsPath)
168+
o.content = data.toString()
169+
return o
170+
}
171+
const filePath = resolve(join('templates', 'plugin', file))
172+
const isFileExist = await this.isFileExist(filePath)
173+
if (isFileExist) {
174+
const data = await readFile(filePath)
175+
o.content = data.toString()
176+
return o
177+
}
178+
throw new Error(`File ${file} is missing, please check if the module installed properly.`)
179+
}
180+
181+
async prepareFiles (files: string[], answer: any): Promise<{ [path: string]: string }> {
182+
const o: { [path: string]: string } = {}
183+
o['package.json'] = computePackageJSON(answer)
184+
for (let file of files) {
185+
const { template, content } = await this.resolveFile(file)
186+
const dir = dirname(file)
187+
file = `${dir}/${basename(file).replace('__', '.')}`
188+
if (template) {
189+
const render = compile(content, { async: true })
190+
o[file] = await render(answer)
191+
} else {
192+
o[file] = content
193+
}
194+
}
195+
return o
196+
}
197+
198+
async writeFiles (files: { [path: string]: string }, answer: any): Promise<void> {
199+
if (this.shouldOverwrite) {
200+
this.log(`remove folder "${answer.location as string}"`)
201+
await rm(resolve(answer.location as string), { recursive: true, force: true })
202+
}
203+
for (const [path, content] of Object.entries(files)) {
204+
const realpath = join(answer.location, path)
205+
const fullpath = resolve(realpath)
206+
await mkdir(dirname(fullpath), { recursive: true })
207+
await writeFile(fullpath, content)
208+
this.log(`write file "${path}" to "${realpath}"`)
209+
}
210+
}
211+
212+
async npmInstall (answer: any): Promise<void> {
213+
this.log('run "npm install"')
214+
const result: any = await prompt([
215+
{ type: 'confirm', name: 'npm', message: 'Do you want to run "npm install"?', default: true }
216+
])
217+
if (result.npm === true) {
218+
execSync('npm install', {
219+
cwd: resolve(answer.location),
220+
stdio: 'inherit'
221+
})
222+
}
223+
}
224+
}

src/commands/generate/project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { access, mkdir, readFile, rm, stat, writeFile } from 'fs/promises'
55
import { prompt } from 'inquirer'
66
import { basename, dirname, join, resolve } from 'path'
77
import { Command } from '../../utils/command/command'
8-
import { computePackageJSON } from '../../utils/package-json'
8+
import { computePackageJSON } from '../../utils/package-json/project'
99

1010
export default class Project extends Command {
1111
static description = 'Generate fastify project'

src/utils/package-json/base.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { resolve } from 'path'
2+
3+
export function findPackageJSON (): {
4+
version: string
5+
dependencies: { [key: string]: string }
6+
devDependencies: { [key: string]: string }
7+
} {
8+
// eslint-disable-next-line @typescript-eslint/no-var-requires
9+
const pkg = require(resolve('package.json'))
10+
return {
11+
version: pkg.version,
12+
dependencies: pkg.dependencies,
13+
devDependencies: pkg.devDependencies
14+
}
15+
}
16+
17+
export function sort (dependencies: { [key: string]: string }): { [key: string]: string } {
18+
const keys = Object.keys(dependencies).sort()
19+
const obj: { [key: string]: string } = {}
20+
for (const key of keys) {
21+
obj[key] = dependencies[key]
22+
}
23+
return obj
24+
}

src/utils/package-json/plugin.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { findPackageJSON, sort } from './base'
2+
3+
export function computePackageJSON (answer: any): string {
4+
const pkg = findPackageJSON()
5+
const template: any = {}
6+
template.name = answer.name
7+
template.main = 'index.js'
8+
template.scripts = computeScripts(answer)
9+
template.dependencies = computeDependencies(answer, pkg)
10+
template.devDependencies = computeDevDependencies(answer, pkg)
11+
if (String(answer.lint).includes('eslint')) {
12+
template.eslintConfig = computeESLintConfig(template.devDependencies)
13+
}
14+
template.tsd = computeTSDConfig()
15+
16+
return JSON.stringify(template, null, 2)
17+
}
18+
19+
export function computeScripts (answer: any): { [key: string]: string } {
20+
const scripts: any = {}
21+
// Lint Command
22+
if (answer.lint === 'standard') {
23+
scripts.lint = 'standard'
24+
}
25+
if (String(answer.lint).includes('eslint')) {
26+
scripts.lint = 'eslint .'
27+
}
28+
29+
// Other Commands
30+
if (answer.language === 'JavaScript') {
31+
scripts.test = 'npm run unit && npm run tsd'
32+
scripts.unit = 'tap "test/**/*.test.js"'
33+
scripts.tsd = 'tsd'
34+
}
35+
return scripts
36+
}
37+
38+
export function computeDependencies (_answer: any, pkg: any): { [key: string]: string } {
39+
const dependencies: any = {}
40+
dependencies['fastify-plugin'] = pkg.dependencies['fastify-plugin']
41+
return sort(dependencies)
42+
}
43+
44+
export function computeDevDependencies (answer: any, pkg: any): { [key: string]: string } {
45+
const dependencies: any = {}
46+
dependencies.tap = pkg.devDependencies.tap
47+
dependencies.tsd = pkg.devDependencies.tsd
48+
dependencies.fastify = pkg.dependencies.fastify
49+
50+
if (String(answer.lint).includes('eslint')) {
51+
// shared
52+
dependencies.eslint = '^8.16.0'
53+
dependencies['eslint-plugin-import'] = pkg.devDependencies['eslint-plugin-import']
54+
dependencies['eslint-plugin-promise'] = '^6.0.0'
55+
56+
// standard
57+
if (String(answer.lint).includes('standard')) {
58+
dependencies['eslint-config-standard'] = '^17.0.0'
59+
dependencies['eslint-plugin-n'] = '^15.2.0'
60+
}
61+
}
62+
if (answer.lint === 'standard') {
63+
dependencies.standard = '^17.0.0'
64+
}
65+
66+
return sort(dependencies)
67+
}
68+
69+
export function computeESLintConfig (devDependencies: { [key: string]: string }): any {
70+
const keys = Object.keys(devDependencies)
71+
// standard
72+
if (keys.includes('eslint-config-standard')) {
73+
return {
74+
extends: 'standard'
75+
}
76+
}
77+
// eslint
78+
const config: {
79+
extends: string[]
80+
plugins: string[]
81+
[key: string]: any
82+
} = {
83+
extends: ['eslint:recommended'],
84+
plugins: []
85+
}
86+
if (keys.includes('eslint-plugin-promise')) {
87+
config.plugins.push('promise')
88+
}
89+
if (keys.includes('eslint-plugin-import')) {
90+
config.extends.push('plugin:import/recommended')
91+
config.plugins.push('import')
92+
}
93+
if (keys.includes('eslint-plugin-n')) {
94+
config.extends.push('plugin:n/recommended')
95+
}
96+
return config
97+
}
98+
99+
export function computeTSDConfig (): any {
100+
return {
101+
directory: 'test'
102+
}
103+
}

0 commit comments

Comments
 (0)