Skip to content

Commit c404c79

Browse files
feat: create-vue-vine (#82)
Co-authored-by: ShenQingchuan <[email protected]>
1 parent 76de79f commit c404c79

39 files changed

+1767
-283
lines changed

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = antfu(
1212
'packages/docs/.vitepress/cache',
1313
'packages/e2e-test/**/*.vine.ts',
1414
'packages/playground/**/*.vine.ts',
15+
'packages/create-vue-vine/template/**/*.vine.[j|t]s',
1516
],
1617
},
1718
{

packages/create-vue-vine/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# create-vue-vine <a href="https://npmjs.com/package/create-vite"><img src="https://img.shields.io/npm/v/create-vue-vine" alt="npm package"></a> <img src="https://img.shields.io/badge/experimental-aa58ff" />
2+
3+
The official CLI for creating your Vue Vine projects.
4+
5+
> **Compatibility Note**: Vue Vine dev-server which is `vite` is requires Node.js version 18+, 20+. However, some templates require a higher Node.js version to work, please upgrade if your package manager warns about it.
6+
7+
With NPM:
8+
9+
```bash
10+
$ npm create vue-vine@latest
11+
```
12+
13+
With Yarn:
14+
15+
```bash
16+
$ yarn create vue-vine
17+
```
18+
19+
With PNPM:
20+
21+
```bash
22+
$ pnpm create vue-vine
23+
```
24+
25+
With Bun:
26+
27+
```bash
28+
$ bun create vue-vine
29+
```
30+
31+
Then follow the prompts!
32+
33+
You can also directly specify the project name and the template you want to use via additional command line options. For example, to scaffold a Vue Vine `vue-router` app, run:
34+
35+
```bash
36+
# npm 7+, extra double-dash is needed:
37+
npm create vue-vine@latest my-vue-vine-app -- --router
38+
39+
# yarn
40+
yarn create vue-vine my-vue-vine-app --router
41+
42+
# pnpm
43+
pnpm create vue-vine my-vue-vine-app --router
44+
45+
# Bun
46+
bun create vue-vine my-vue-vine-app --router
47+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env node
2+
'use strict'
3+
import '../dist/index.mjs'

packages/create-vue-vine/package.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "create-vue-vine",
3+
"version": "0.0.1",
4+
"description": "Official CLI for creating Vue Vine project.",
5+
"author": "ShenQingchuan",
6+
"license": "MIT",
7+
"funding": "https://github.com/vue-vine/vue-vine?sponsor=1",
8+
"homepage": "https://github.com/vue-vine/vue-vine/tree/main/packages/create-vue-vine#readme",
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/vue-vine/vue-vine.git",
12+
"directory": "packages/create-vue-vine"
13+
},
14+
"bugs": {
15+
"url": "https://github.com/vue-vine/vue-vine/issues"
16+
},
17+
"keywords": [
18+
"Vue",
19+
"Vine"
20+
],
21+
"exports": {
22+
".": {
23+
"import": "./dist/index.mjs"
24+
}
25+
},
26+
"module": "./dist/index.mjs",
27+
"bin": "./bin/create-vue-vine.mjs",
28+
"files": [
29+
"bin",
30+
"dist",
31+
"template"
32+
],
33+
"scripts": {
34+
"dev": "tsup --watch",
35+
"build": "tsup"
36+
},
37+
"dependencies": {
38+
"@clack/prompts": "^0.7.0",
39+
"clerc": "^0.42.2",
40+
"execa": "^8.0.1",
41+
"pkg-dir": "^7.0.0",
42+
"yoctocolors": "^1.0.0"
43+
},
44+
"devDependencies": {
45+
"@types/node": "^20.4.9",
46+
"eslint": "^8.48.0",
47+
"unocss": "^0.55.3",
48+
"unplugin-auto-import": "^0.16.6",
49+
"vite": "^4.4.9",
50+
"vite-plugin-inspect": "^0.7.38",
51+
"vue": "^3.3.4",
52+
"vue-vine": "workspace:*"
53+
}
54+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { join, relative } from 'node:path'
2+
import { rm } from 'node:fs/promises'
3+
import process from 'node:process'
4+
import { intro, log, outro, spinner } from '@clack/prompts'
5+
import { Root, defineCommand } from 'clerc'
6+
import { bold, green } from 'yoctocolors'
7+
import { cancel, confirm, exists, formatPmCommand, getPmCommand, getTemplateDirectory, gradientBanner, runPmCommand, text, validateProjectName } from '@/utils'
8+
import { creaateProjectOptions, createProject } from '@/create'
9+
import { useFlags } from '@/flags'
10+
11+
const defaultProjectName = 'vue-vine-project'
12+
13+
const { flags, executeFlags } = useFlags()
14+
15+
export const createCommand = defineCommand({
16+
name: Root,
17+
description: 'Create a Vue Vine project',
18+
parameters: [
19+
'[projectName]',
20+
],
21+
flags: {
22+
force: {
23+
type: Boolean,
24+
description: 'Delete existing folder',
25+
alias: 'f',
26+
default: false,
27+
},
28+
install: {
29+
type: Boolean,
30+
description: 'Install dependencies',
31+
alias: 'i',
32+
},
33+
...flags,
34+
},
35+
alias: 'create',
36+
}, async (ctx) => {
37+
intro(gradientBanner)
38+
const cwd = process.cwd()
39+
if (!ctx.parameters.projectName) {
40+
ctx.parameters.projectName = await text({
41+
message: 'Project name:',
42+
placeholder: defaultProjectName,
43+
defaultValue: defaultProjectName,
44+
validate: (value) => {
45+
if (!validateProjectName(value)) {
46+
return 'Invalid project name'
47+
}
48+
},
49+
})
50+
}
51+
const projectPath = join(cwd, ctx.parameters.projectName)
52+
if (await exists(projectPath)) {
53+
if (!ctx.flags.force) {
54+
ctx.flags.force = await confirm({
55+
message: `Folder ${ctx.parameters.projectName} already exists. Delete?`,
56+
initialValue: false,
57+
})
58+
}
59+
if (!ctx.flags.force) {
60+
cancel(`Folder ${ctx.parameters.projectName} already exists. Goodbye!`)
61+
}
62+
else {
63+
log.info(`Folder ${ctx.parameters.projectName} will be deleted.`)
64+
await rm(projectPath, { recursive: true })
65+
}
66+
}
67+
const templateDir = await getTemplateDirectory()
68+
if (!templateDir) {
69+
cancel('Unable to find template directory')
70+
}
71+
72+
const projectOptions = creaateProjectOptions({
73+
path: projectPath,
74+
name: ctx.parameters.projectName,
75+
templateDir,
76+
})
77+
78+
await executeFlags(ctx.flags, projectOptions)
79+
80+
const s = spinner()
81+
s.start(`Creating project ${ctx.parameters.projectName}`)
82+
await createProject(projectOptions)
83+
s.stop(`Project created at: ${projectPath}`)
84+
if (ctx.flags.install === undefined) {
85+
ctx.flags.install = await confirm({
86+
message: 'Install dependencies?',
87+
initialValue: true,
88+
})
89+
}
90+
91+
if (ctx.flags.install) {
92+
s.start('Installing dependencies')
93+
await runPmCommand('install', projectPath)
94+
s.stop('Dependencies installed!')
95+
}
96+
const cdProjectPath = relative(cwd, projectPath)
97+
const helpText = [
98+
'You\'re all set! Now run:',
99+
'',
100+
` cd ${bold(green(cdProjectPath.includes(' ') ? `"${cdProjectPath}"` : cdProjectPath))}`,
101+
ctx.flags.install ? undefined : ` ${bold(green(formatPmCommand(getPmCommand('install'))))}`,
102+
` ${bold(green(formatPmCommand(getPmCommand('dev'))))}`,
103+
'',
104+
' Happy hacking!',
105+
].filter(s => s !== undefined).join('\n')
106+
outro(helpText)
107+
process.exit() // Ugh, why
108+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { join } from 'node:path'
2+
import { mkdir, writeFile } from 'node:fs/promises'
3+
import { getTemplateDirectory, renderTemplate } from './utils'
4+
5+
export interface ProjectOptions {
6+
path: string
7+
name: string
8+
templateDir: string
9+
10+
templates: string[]
11+
}
12+
13+
export function creaateProjectOptions(params: Pick<ProjectOptions, 'path' | 'name' | 'templateDir'>): ProjectOptions {
14+
return {
15+
...params,
16+
templates: [],
17+
}
18+
}
19+
20+
export async function createProject(options: ProjectOptions) {
21+
const templateDirectory = (await getTemplateDirectory())!
22+
const withBase = (path: string) => join(templateDirectory, path)
23+
24+
await mkdir(options.path)
25+
await writeFile(join(options.path, 'package.json'), JSON.stringify({
26+
name: options.name,
27+
}, null, 2))
28+
29+
for (const template of ['common', 'code/base', 'config/ts', ...options.templates]) {
30+
await renderTemplate(withBase(template), options.path)
31+
}
32+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { ProjectOptions } from '@/create'
2+
import { confirm, defineFlagMeta } from '@/utils'
3+
import type { FeatureFlagActionCtx } from '@/utils'
4+
5+
const metas = {
6+
router: defineFlagMeta({
7+
name: 'router',
8+
message: 'Use Vue Router?',
9+
flag: {
10+
type: Boolean,
11+
description: 'Add Vue Router',
12+
default: false,
13+
} as const,
14+
initialValue: false,
15+
}),
16+
}
17+
18+
const flags = Object.entries(metas).reduce((acc, [key, value]) => {
19+
Reflect.set(acc, key, value.flag)
20+
return acc
21+
}, {} as {
22+
[K in keyof typeof metas]: typeof metas[K]['flag']
23+
})
24+
25+
export type ParsedFlags = {
26+
[K in keyof typeof metas]: boolean
27+
}
28+
29+
export function useFlags() {
30+
return {
31+
flags,
32+
executeFlags: async (flags: ParsedFlags, options: ProjectOptions) => {
33+
const context: FeatureFlagActionCtx = {
34+
template: (...path) => {
35+
options.templates.push(...path)
36+
},
37+
}
38+
39+
// Confirm flags, order is sensitive
40+
for (const item of [metas.router.name]) {
41+
if (!flags[item]) {
42+
const { initialValue, message } = metas[item]
43+
flags[item] = await confirm({
44+
message,
45+
initialValue,
46+
})
47+
}
48+
}
49+
if (flags.router) {
50+
context.template('code/router', 'config/router')
51+
}
52+
},
53+
}
54+
}

packages/create-vue-vine/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Clerc, friendlyErrorPlugin, helpPlugin, notFoundPlugin, strictFlagsPlugin } from 'clerc'
2+
3+
import { description, name, version } from '../package.json'
4+
import { createCommand } from './commands/create'
5+
6+
Clerc.create(name, version, description)
7+
.use(helpPlugin())
8+
.use(notFoundPlugin())
9+
.use(strictFlagsPlugin())
10+
.use(notFoundPlugin())
11+
.use(friendlyErrorPlugin())
12+
.command(createCommand)
13+
.parse()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const defaultBanner = 'Vue Vine - Another style of writing Vue components'
2+
3+
/**
4+
* generated by the following code:
5+
*
6+
* import gradient from 'gradient-string'
7+
*
8+
* gradient(['#364fae', '#43954b'])
9+
* ('Vue Vine - Another style of writing Vue components')
10+
*
11+
* Use the output directly here to keep the bundle small.
12+
*
13+
* How to generate:
14+
*
15+
* - get graient text
16+
* - text.replace(/\x1B/g, '\\x1B')
17+
* - write the output to a txt file (which will not handle the ansi escapes) and copy the output here
18+
*/
19+
const gradientBanner
20+
= '\x1B[38;2;54;79;174mV\x1B[39m\x1B[38;2;54;81;172mu\x1B[39m\x1B[38;2;55;82;169me\x1B[39m \x1B[38;2;55;84;167mV\x1B[39m\x1B[38;2;55;86;164mi\x1B[39m\x1B[38;2;56;88;162mn\x1B[39m\x1B[38;2;56;89;160me\x1B[39m \x1B[38;2;56;91;157m-\x1B[39m \x1B[38;2;57;93;155mA\x1B[39m\x1B[38;2;57;94;152mn\x1B[39m\x1B[38;2;57;96;150mo\x1B[39m\x1B[38;2;57;98;147mt\x1B[39m\x1B[38;2;58;99;145mh\x1B[39m\x1B[38;2;58;101;143me\x1B[39m\x1B[38;2;58;103;140mr\x1B[39m \x1B[38;2;59;105;138ms\x1B[39m\x1B[38;2;59;106;135mt\x1B[39m\x1B[38;2;59;108;133my\x1B[39m\x1B[38;2;60;110;131ml\x1B[39m\x1B[38;2;60;111;128me\x1B[39m \x1B[38;2;60;113;126mo\x1B[39m\x1B[38;2;61;115;123mf\x1B[39m \x1B[38;2;61;117;121mw\x1B[39m\x1B[38;2;61;118;118mr\x1B[39m\x1B[38;2;62;120;116mi\x1B[39m\x1B[38;2;62;122;114mt\x1B[39m\x1B[38;2;62;123;111mi\x1B[39m\x1B[38;2;63;125;109mn\x1B[39m\x1B[38;2;63;127;106mg\x1B[39m \x1B[38;2;63;129;104mV\x1B[39m\x1B[38;2;64;130;102mu\x1B[39m\x1B[38;2;64;132;99me\x1B[39m \x1B[38;2;64;134;97mc\x1B[39m\x1B[38;2;64;135;94mo\x1B[39m\x1B[38;2;65;137;92mm\x1B[39m\x1B[38;2;65;139;89mp\x1B[39m\x1B[38;2;65;140;87mo\x1B[39m\x1B[38;2;66;142;85mn\x1B[39m\x1B[38;2;66;144;82me\x1B[39m\x1B[38;2;66;146;80mn\x1B[39m\x1B[38;2;67;147;77mt\x1B[39m\x1B[38;2;67;149;75ms\x1B[39m'
21+
22+
export { defaultBanner, gradientBanner }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import process from 'node:process'
2+
import { cancel as _cancel, confirm as _confirm, text as _text, isCancel } from '@clack/prompts'
3+
4+
export function cancel(...args: Parameters<typeof _cancel>): never {
5+
_cancel(...args)
6+
process.exit(0)
7+
}
8+
9+
function wrapClack<T extends (...args: any[]) => any>(fn: T) {
10+
return async (...args: Parameters<T>) => {
11+
const result = await fn(...args)
12+
if (isCancel(result)) {
13+
cancel('Operation cancelled. Goodbye!')
14+
process.exit(0)
15+
}
16+
return result as Exclude<Awaited<ReturnType<T>>, symbol>
17+
}
18+
}
19+
20+
export const text = wrapClack(_text)
21+
export const confirm = wrapClack(_confirm)

0 commit comments

Comments
 (0)