Skip to content

Commit 85defef

Browse files
authored
feat(cli): add CLI argument parsing and help text generation (#83)
1 parent 9dfda19 commit 85defef

File tree

9 files changed

+264
-47
lines changed

9 files changed

+264
-47
lines changed

src/index.ts

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,75 @@
11
// Node
22
import { dirname, join, resolve } from 'node:path'
33
import { fileURLToPath } from 'node:url'
4-
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
4+
import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'
55

66
// Types
77
import type { ContextState } from './utils/prompts'
88
import type { NuxtPresetName } from './utils/presets'
99

1010
// Utils
1111
import { initPrompts } from './utils/prompts'
12+
import { resolveNonInteractiveContext } from './utils/nonInteractivePrompts'
13+
import { parseCliArgs, cliOptionsToContext, getHelpText, getVersionText } from './utils/cli'
1214
import { red } from 'kolorist'
1315
import { createBanner } from './utils/banner'
14-
import minimist from 'minimist'
1516
import { installDependencies, renderTemplate } from './utils'
1617
import { renderNuxtTemplate } from './utils/nuxt/renderNuxtTemplate'
1718

18-
const validPresets = ['base', 'custom', 'default', 'essentials']
19-
2019
async function run () {
21-
const argv = minimist(process.argv.slice(2), {
22-
alias: {
23-
typescript: ['ts'],
24-
},
25-
})
20+
const args = process.argv.slice(2).slice()
21+
const banner = createBanner()
2622

27-
if (argv.preset && !validPresets.includes(argv.preset)) {
28-
throw new Error(`'${argv.preset}' is not a valid preset. Valid presets are: ${validPresets.join(', ')}.`)
23+
if (args.length === 0) {
24+
console.log(`\n${banner}\n`)
25+
26+
const initialContext: ContextState = {
27+
canOverwrite: false,
28+
cwd: process.cwd(),
29+
projectName: 'vuetify-project',
30+
}
31+
32+
const finalContext = await initPrompts(initialContext)
33+
34+
await createProject(finalContext)
35+
return
2936
}
3037

31-
const banner = createBanner()
38+
const cliOptions = parseCliArgs(args)
39+
40+
if (cliOptions.help) {
41+
console.log(getHelpText())
42+
process.exit(0)
43+
}
44+
45+
if (cliOptions.version) {
46+
console.log(getVersionText())
47+
process.exit(0)
48+
}
3249

3350
console.log(`\n${banner}\n`)
3451

35-
const context: ContextState = {
36-
canOverwrite: false,
37-
cwd: process.cwd(),
38-
projectName: 'vuetify-project',
39-
useRouter: false,
40-
useTypeScript: argv.typescript,
41-
usePreset: argv.preset,
42-
useStore: undefined,
43-
usePackageManager: undefined,
52+
const cliContext = cliOptionsToContext(cliOptions, process.cwd())
53+
54+
const initialContext: ContextState = {
55+
cwd: cliContext.cwd!,
56+
projectName: cliContext.projectName,
57+
canOverwrite: cliContext.canOverwrite,
58+
useTypeScript: cliContext.useTypeScript,
59+
usePreset: cliContext.usePreset,
60+
usePackageManager: cliContext.usePackageManager,
61+
installDependencies: cliContext.installDependencies,
62+
useNuxtModule: cliContext.useNuxtModule,
63+
useNuxtSSR: cliContext.useNuxtSSR,
64+
useNuxtSSRClientHints: cliContext.useNuxtSSRClientHints,
4465
}
4566

67+
const finalContext = resolveNonInteractiveContext(initialContext)
68+
69+
await createProject(finalContext)
70+
}
71+
72+
async function createProject (finalContext: any) {
4673
const {
4774
canOverwrite,
4875
cwd,
@@ -54,16 +81,15 @@ async function run () {
5481
useNuxtModule,
5582
useNuxtSSR,
5683
useNuxtSSRClientHints,
57-
} = await initPrompts(context)
84+
} = finalContext
5885

5986
const projectRoot = join(cwd, projectName)
6087

61-
if (canOverwrite) {
62-
// Clean dir
88+
if (canOverwrite && existsSync(projectRoot)) {
6389
rmSync(projectRoot, { recursive: true })
6490
}
6591

66-
const preset = context.usePreset ?? usePreset
92+
const preset = finalContext.usePreset ?? usePreset
6793

6894
if (preset.startsWith('nuxt-')) {
6995
const templateRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../template/typescript')

src/utils/cli/helpText.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pkg from '../../../package.json' with { type: 'json' }
2+
3+
export function getHelpText (): string {
4+
return `
5+
Usage: create-vuetify [project-name] [options]
6+
7+
Options:
8+
-p, --preset <preset> Choose a preset (base, default, essentials, nuxt-base, nuxt-default, nuxt-essentials)
9+
--ts, --typescript Use TypeScript
10+
--pm, --package-manager <manager> Package manager to use (npm, pnpm, yarn, bun, none)
11+
-i, --install Install dependencies
12+
-f, --force, --overwrite Overwrite existing directory
13+
--nuxt-module Use vuetify-nuxt-module (for Nuxt presets)
14+
--nuxt-ssr, --ssr Enable Nuxt SSR (for Nuxt presets)
15+
--nuxt-ssr-client-hints Enable Nuxt SSR Client Hints (for Nuxt presets)
16+
-h, --help Show help
17+
-v, --version Show version
18+
19+
Examples:
20+
create-vuetify # Interactive mode
21+
create-vuetify my-app --preset default --typescript # Non-interactive with TypeScript
22+
create-vuetify my-app --preset nuxt-essentials --ssr # Nuxt project with SSR
23+
create-vuetify my-app --force --install --pm pnpm # Force overwrite and install with pnpm
24+
25+
Presets:
26+
default - Barebones (Only Vue & Vuetify)
27+
base - Default (Adds routing, ESLint & SASS variables)
28+
essentials - Recommended (Everything from Default. Adds auto importing, layouts & pinia)
29+
nuxt-base - Nuxt Default (Adds Nuxt ESLint & SASS variables)
30+
nuxt-default - Nuxt Barebones (Only Vuetify)
31+
nuxt-essentials - Nuxt Recommended (Everything from Default. Enables auto importing & layouts)
32+
`.trim()
33+
}
34+
35+
export function getVersionText (): string {
36+
return `${pkg.version}`
37+
}

src/utils/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './helpText'
2+
export * from './parseArgs'

src/utils/cli/parseArgs.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import minimist from 'minimist'
2+
import type { ContextState } from '../prompts'
3+
4+
export interface CliOptions {
5+
projectName?: string
6+
preset?: 'base' | 'default' | 'essentials' | 'nuxt-base' | 'nuxt-default' | 'nuxt-essentials'
7+
typescript?: boolean
8+
packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'none'
9+
installDependencies?: boolean
10+
overwrite?: boolean
11+
nuxtModule?: boolean
12+
nuxtSSR?: boolean
13+
nuxtSSRClientHints?: boolean
14+
help?: boolean
15+
version?: boolean
16+
}
17+
18+
const validPresets = ['base', 'default', 'essentials', 'nuxt-base', 'nuxt-default', 'nuxt-essentials'] as const
19+
const validPackageManagers = ['npm', 'pnpm', 'yarn', 'bun', 'none'] as const
20+
21+
export function parseCliArgs (args: string[]): CliOptions {
22+
const argv = minimist(args, {
23+
alias: {
24+
typescript: ['ts'],
25+
preset: ['p'],
26+
packageManager: ['pm', 'package-manager'],
27+
installDependencies: ['install', 'i'],
28+
overwrite: ['force', 'f'],
29+
nuxtModule: ['nuxt-module'],
30+
nuxtSSR: ['nuxt-ssr', 'ssr'],
31+
nuxtSSRClientHints: ['nuxt-ssr-client-hints', 'client-hints'],
32+
help: ['h'],
33+
version: ['v'],
34+
},
35+
boolean: [
36+
'typescript',
37+
'installDependencies',
38+
'overwrite',
39+
'nuxtModule',
40+
'nuxtSSR',
41+
'nuxtSSRClientHints',
42+
'help',
43+
'version',
44+
],
45+
string: [
46+
'preset',
47+
'packageManager',
48+
],
49+
})
50+
51+
if (argv.preset && !validPresets.includes(argv.preset)) {
52+
throw new Error(`'${argv.preset}' is not a valid preset. Valid presets are: ${validPresets.join(', ')}.`)
53+
}
54+
55+
if (argv.packageManager && !validPackageManagers.includes(argv.packageManager)) {
56+
throw new Error(`'${argv.packageManager}' is not a valid package manager. Valid options are: ${validPackageManagers.join(', ')}.`)
57+
}
58+
59+
const projectName = argv._[0] as string | undefined
60+
61+
return {
62+
projectName,
63+
preset: argv.preset,
64+
typescript: argv.typescript,
65+
packageManager: argv.packageManager!,
66+
installDependencies: argv.installDependencies,
67+
overwrite: argv.overwrite,
68+
nuxtModule: argv.nuxtModule,
69+
nuxtSSR: argv.nuxtSSR,
70+
nuxtSSRClientHints: argv.nuxtSSRClientHints,
71+
help: argv.help,
72+
version: argv.version,
73+
}
74+
}
75+
76+
export function cliOptionsToContext (cliOptions: CliOptions, cwd: string): Partial<ContextState> {
77+
return {
78+
cwd,
79+
projectName: cliOptions.projectName || 'vuetify-project',
80+
useTypeScript: cliOptions.typescript,
81+
usePreset: cliOptions.preset ?? 'default',
82+
usePackageManager: cliOptions.packageManager === 'none' ? undefined : cliOptions.packageManager,
83+
installDependencies: cliOptions.installDependencies,
84+
canOverwrite: cliOptions.overwrite ?? false,
85+
useNuxtModule: cliOptions.nuxtModule ?? true,
86+
useNuxtSSR: cliOptions.nuxtSSR ?? true,
87+
useNuxtSSRClientHints: cliOptions.nuxtSSRClientHints ?? (cliOptions.nuxtModule && cliOptions.nuxtSSR),
88+
}
89+
}

src/utils/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
export { installDependencies } from './installDependencies'
2-
3-
export { renderTemplate } from './renderTemplate'
1+
export * from './banner'
2+
export * from './cli'
3+
export * from './deepMerge'
4+
export * from './installDependencies'
5+
export * from './nonInteractivePrompts'
6+
export * from './presets'
7+
export * from './prompts'
8+
export * from './renderTemplate'

src/utils/nonInteractivePrompts.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { join, resolve } from 'node:path'
2+
import { existsSync, readdirSync } from 'node:fs'
3+
4+
import type { ContextState } from './prompts'
5+
6+
import { presets } from './presets'
7+
import { red } from 'kolorist'
8+
import { packageManager as defaultPackageManager } from './installDependencies'
9+
10+
type DefinedContextState = { [P in keyof ContextState]-?: ContextState[P] }
11+
12+
export function resolveNonInteractiveContext (context: ContextState): DefinedContextState {
13+
if (context.usePreset) {
14+
context = {
15+
...context,
16+
...presets[context.usePreset],
17+
}
18+
}
19+
20+
const projectName = context.projectName || 'vuetify-project'
21+
22+
const projectPath = join(context.cwd, projectName)
23+
const directoryExists = existsSync(projectPath)
24+
const directoryNotEmpty = directoryExists && readdirSync(projectPath).length > 0
25+
26+
if (directoryNotEmpty && !context.canOverwrite) {
27+
console.error('\n\n', red('✖') + ` Target directory ${resolve(context.cwd, projectName)} exists and is not empty.`)
28+
console.error('Use --force or --overwrite flag to overwrite the directory.')
29+
process.exit(1)
30+
}
31+
32+
let useTypeScript = context.useTypeScript
33+
if (useTypeScript === undefined) {
34+
const preset = context.usePreset
35+
useTypeScript = preset ? preset.startsWith('nuxt-') : false
36+
}
37+
38+
let usePackageManager = context.usePackageManager
39+
if (usePackageManager === undefined) {
40+
const preset = context.usePreset
41+
if (!preset || !preset.startsWith('nuxt-')) {
42+
usePackageManager = defaultPackageManager as 'npm' | 'pnpm' | 'yarn' | 'bun'
43+
}
44+
}
45+
46+
let installDependencies = context.installDependencies
47+
if (installDependencies === undefined) {
48+
const preset = context.usePreset
49+
installDependencies = preset ? !preset.startsWith('nuxt-') : true
50+
}
51+
52+
const usePreset = context.usePreset || 'default'
53+
54+
const useNuxtModule = context.useNuxtModule
55+
const useNuxtSSR = context.useNuxtSSR
56+
const useNuxtSSRClientHints = context.useNuxtSSRClientHints
57+
58+
return {
59+
cwd: context.cwd,
60+
projectName,
61+
canOverwrite: context.canOverwrite || false,
62+
useTypeScript: useTypeScript || false,
63+
usePackageManager: usePackageManager || 'npm',
64+
installDependencies: installDependencies || false,
65+
usePreset,
66+
useNuxtModule: useNuxtModule || false,
67+
useNuxtSSR: useNuxtSSR || false,
68+
useNuxtSSRClientHints: useNuxtSSRClientHints || false,
69+
}
70+
}

src/utils/presets.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,16 @@
1-
export interface Preset {
2-
useEslint: boolean
3-
useRouter: boolean
4-
useStore: boolean
5-
}
1+
export interface Preset {}
62

73
export type NuxtPresetName = 'nuxt-base' | 'nuxt-default' | 'nuxt-essentials'
84
export type PresetName = 'base' | 'default' | 'essentials' | NuxtPresetName
95

10-
const defaultContext: Preset = {
11-
useEslint: false,
12-
useRouter: false,
13-
useStore: false,
14-
}
6+
const defaultContext: Preset = {}
157

168
const baseContext: Preset = {
179
...defaultContext,
18-
useEslint: true,
19-
useRouter: true,
2010
}
2111

2212
const essentialsContext: Preset = {
2313
...baseContext,
24-
useStore: true,
2514
}
2615

2716
const presets: Record<PresetName, Preset> = {

src/utils/prompts.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ type ContextState = {
2020
usePackageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun'
2121
installDependencies?: boolean
2222
usePreset?: 'base' | 'default' | 'essentials' | 'nuxt-base' | 'nuxt-default' | 'nuxt-essentials'
23-
useEslint?: boolean
24-
useRouter?: boolean
25-
useStore?: boolean
2623
useNuxtModule?: boolean
2724
useNuxtSSR?: boolean
2825
useNuxtSSRClientHints?: boolean

src/utils/renderTemplate.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { copyFileSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
1+
import { copyFileSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, existsSync } from 'node:fs'
22
import { basename, dirname, resolve } from 'node:path'
33

44
import { deepMerge } from './deepMerge'
55

66
function mergePkg (source: string, destination: string) {
7-
const target = JSON.parse(readFileSync(destination, 'utf8'))
7+
const target = existsSync(destination) ? JSON.parse(readFileSync(destination, 'utf8')) : {}
88
const src = JSON.parse(readFileSync(source, 'utf8'))
99
const mergedPkg = deepMerge(target, src)
1010

1111
const keysToSort = ['devDependencies', 'dependencies']
1212
for (const k of keysToSort) {
13-
mergedPkg[k] = Object.keys(mergedPkg[k]).toSorted().reduce((a: { [key: string]: string }, c) => (a[c] = mergedPkg[k][c], a), {})
13+
if (mergedPkg[k]) {
14+
mergedPkg[k] = Object.keys(mergedPkg[k]).toSorted().reduce((a: { [key: string]: string }, c) => (a[c] = mergedPkg[k][c], a), {})
15+
}
1416
}
1517

1618
writeFileSync(destination, JSON.stringify(mergedPkg, null, 2) + '\n')

0 commit comments

Comments
 (0)