Skip to content

Commit

Permalink
feat: create-vue-vine (#82)
Browse files Browse the repository at this point in the history
Co-authored-by: ShenQingchuan <[email protected]>
  • Loading branch information
alexzhang1030 and ShenQingchuan committed May 3, 2024
1 parent 76de79f commit c404c79
Show file tree
Hide file tree
Showing 39 changed files with 1,767 additions and 283 deletions.
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = antfu(
'packages/docs/.vitepress/cache',
'packages/e2e-test/**/*.vine.ts',
'packages/playground/**/*.vine.ts',
'packages/create-vue-vine/template/**/*.vine.[j|t]s',
],
},
{
Expand Down
47 changes: 47 additions & 0 deletions packages/create-vue-vine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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" />

The official CLI for creating your Vue Vine projects.

> **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.
With NPM:

```bash
$ npm create vue-vine@latest
```

With Yarn:

```bash
$ yarn create vue-vine
```

With PNPM:

```bash
$ pnpm create vue-vine
```

With Bun:

```bash
$ bun create vue-vine
```

Then follow the prompts!

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:

```bash
# npm 7+, extra double-dash is needed:
npm create vue-vine@latest my-vue-vine-app -- --router

# yarn
yarn create vue-vine my-vue-vine-app --router

# pnpm
pnpm create vue-vine my-vue-vine-app --router

# Bun
bun create vue-vine my-vue-vine-app --router
```
3 changes: 3 additions & 0 deletions packages/create-vue-vine/bin/create-vue-vine.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node
'use strict'
import '../dist/index.mjs'
54 changes: 54 additions & 0 deletions packages/create-vue-vine/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "create-vue-vine",
"version": "0.0.1",
"description": "Official CLI for creating Vue Vine project.",
"author": "ShenQingchuan",
"license": "MIT",
"funding": "https://github.com/vue-vine/vue-vine?sponsor=1",
"homepage": "https://github.com/vue-vine/vue-vine/tree/main/packages/create-vue-vine#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/vue-vine/vue-vine.git",
"directory": "packages/create-vue-vine"
},
"bugs": {
"url": "https://github.com/vue-vine/vue-vine/issues"
},
"keywords": [
"Vue",
"Vine"
],
"exports": {
".": {
"import": "./dist/index.mjs"
}
},
"module": "./dist/index.mjs",
"bin": "./bin/create-vue-vine.mjs",
"files": [
"bin",
"dist",
"template"
],
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
},
"dependencies": {
"@clack/prompts": "^0.7.0",
"clerc": "^0.42.2",
"execa": "^8.0.1",
"pkg-dir": "^7.0.0",
"yoctocolors": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.4.9",
"eslint": "^8.48.0",
"unocss": "^0.55.3",
"unplugin-auto-import": "^0.16.6",
"vite": "^4.4.9",
"vite-plugin-inspect": "^0.7.38",
"vue": "^3.3.4",
"vue-vine": "workspace:*"
}
}
108 changes: 108 additions & 0 deletions packages/create-vue-vine/src/commands/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { join, relative } from 'node:path'
import { rm } from 'node:fs/promises'
import process from 'node:process'
import { intro, log, outro, spinner } from '@clack/prompts'
import { Root, defineCommand } from 'clerc'
import { bold, green } from 'yoctocolors'
import { cancel, confirm, exists, formatPmCommand, getPmCommand, getTemplateDirectory, gradientBanner, runPmCommand, text, validateProjectName } from '@/utils'
import { creaateProjectOptions, createProject } from '@/create'
import { useFlags } from '@/flags'

const defaultProjectName = 'vue-vine-project'

const { flags, executeFlags } = useFlags()

export const createCommand = defineCommand({
name: Root,
description: 'Create a Vue Vine project',
parameters: [
'[projectName]',
],
flags: {
force: {
type: Boolean,
description: 'Delete existing folder',
alias: 'f',
default: false,
},
install: {
type: Boolean,
description: 'Install dependencies',
alias: 'i',
},
...flags,
},
alias: 'create',
}, async (ctx) => {
intro(gradientBanner)
const cwd = process.cwd()
if (!ctx.parameters.projectName) {
ctx.parameters.projectName = await text({
message: 'Project name:',
placeholder: defaultProjectName,
defaultValue: defaultProjectName,
validate: (value) => {
if (!validateProjectName(value)) {
return 'Invalid project name'
}
},
})
}
const projectPath = join(cwd, ctx.parameters.projectName)
if (await exists(projectPath)) {
if (!ctx.flags.force) {
ctx.flags.force = await confirm({
message: `Folder ${ctx.parameters.projectName} already exists. Delete?`,
initialValue: false,
})
}
if (!ctx.flags.force) {
cancel(`Folder ${ctx.parameters.projectName} already exists. Goodbye!`)
}
else {
log.info(`Folder ${ctx.parameters.projectName} will be deleted.`)
await rm(projectPath, { recursive: true })
}
}
const templateDir = await getTemplateDirectory()
if (!templateDir) {
cancel('Unable to find template directory')
}

const projectOptions = creaateProjectOptions({
path: projectPath,
name: ctx.parameters.projectName,
templateDir,
})

await executeFlags(ctx.flags, projectOptions)

const s = spinner()
s.start(`Creating project ${ctx.parameters.projectName}`)
await createProject(projectOptions)
s.stop(`Project created at: ${projectPath}`)
if (ctx.flags.install === undefined) {
ctx.flags.install = await confirm({
message: 'Install dependencies?',
initialValue: true,
})
}

if (ctx.flags.install) {
s.start('Installing dependencies')
await runPmCommand('install', projectPath)
s.stop('Dependencies installed!')
}
const cdProjectPath = relative(cwd, projectPath)
const helpText = [
'You\'re all set! Now run:',
'',
` cd ${bold(green(cdProjectPath.includes(' ') ? `"${cdProjectPath}"` : cdProjectPath))}`,
ctx.flags.install ? undefined : ` ${bold(green(formatPmCommand(getPmCommand('install'))))}`,
` ${bold(green(formatPmCommand(getPmCommand('dev'))))}`,
'',
' Happy hacking!',
].filter(s => s !== undefined).join('\n')
outro(helpText)
process.exit() // Ugh, why
})
32 changes: 32 additions & 0 deletions packages/create-vue-vine/src/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { join } from 'node:path'
import { mkdir, writeFile } from 'node:fs/promises'
import { getTemplateDirectory, renderTemplate } from './utils'

export interface ProjectOptions {
path: string
name: string
templateDir: string

templates: string[]
}

export function creaateProjectOptions(params: Pick<ProjectOptions, 'path' | 'name' | 'templateDir'>): ProjectOptions {
return {
...params,
templates: [],
}
}

export async function createProject(options: ProjectOptions) {
const templateDirectory = (await getTemplateDirectory())!
const withBase = (path: string) => join(templateDirectory, path)

await mkdir(options.path)
await writeFile(join(options.path, 'package.json'), JSON.stringify({
name: options.name,
}, null, 2))

for (const template of ['common', 'code/base', 'config/ts', ...options.templates]) {
await renderTemplate(withBase(template), options.path)
}
}
54 changes: 54 additions & 0 deletions packages/create-vue-vine/src/flags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ProjectOptions } from '@/create'
import { confirm, defineFlagMeta } from '@/utils'
import type { FeatureFlagActionCtx } from '@/utils'

const metas = {
router: defineFlagMeta({
name: 'router',
message: 'Use Vue Router?',
flag: {
type: Boolean,
description: 'Add Vue Router',
default: false,
} as const,
initialValue: false,
}),
}

const flags = Object.entries(metas).reduce((acc, [key, value]) => {
Reflect.set(acc, key, value.flag)
return acc
}, {} as {
[K in keyof typeof metas]: typeof metas[K]['flag']
})

export type ParsedFlags = {
[K in keyof typeof metas]: boolean
}

export function useFlags() {
return {
flags,
executeFlags: async (flags: ParsedFlags, options: ProjectOptions) => {
const context: FeatureFlagActionCtx = {
template: (...path) => {
options.templates.push(...path)
},
}

// Confirm flags, order is sensitive
for (const item of [metas.router.name]) {
if (!flags[item]) {
const { initialValue, message } = metas[item]
flags[item] = await confirm({
message,
initialValue,
})
}
}
if (flags.router) {
context.template('code/router', 'config/router')
}
},
}
}
13 changes: 13 additions & 0 deletions packages/create-vue-vine/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Clerc, friendlyErrorPlugin, helpPlugin, notFoundPlugin, strictFlagsPlugin } from 'clerc'

import { description, name, version } from '../package.json'
import { createCommand } from './commands/create'

Clerc.create(name, version, description)
.use(helpPlugin())
.use(notFoundPlugin())
.use(strictFlagsPlugin())
.use(notFoundPlugin())
.use(friendlyErrorPlugin())
.command(createCommand)
.parse()
22 changes: 22 additions & 0 deletions packages/create-vue-vine/src/utils/banner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const defaultBanner = 'Vue Vine - Another style of writing Vue components'

/**
* generated by the following code:
*
* import gradient from 'gradient-string'
*
* gradient(['#364fae', '#43954b'])
* ('Vue Vine - Another style of writing Vue components')
*
* Use the output directly here to keep the bundle small.
*
* How to generate:
*
* - get graient text
* - text.replace(/\x1B/g, '\\x1B')
* - write the output to a txt file (which will not handle the ansi escapes) and copy the output here
*/
const gradientBanner
= '\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'

export { defaultBanner, gradientBanner }
21 changes: 21 additions & 0 deletions packages/create-vue-vine/src/utils/clack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import process from 'node:process'
import { cancel as _cancel, confirm as _confirm, text as _text, isCancel } from '@clack/prompts'

export function cancel(...args: Parameters<typeof _cancel>): never {
_cancel(...args)
process.exit(0)
}

function wrapClack<T extends (...args: any[]) => any>(fn: T) {
return async (...args: Parameters<T>) => {
const result = await fn(...args)
if (isCancel(result)) {
cancel('Operation cancelled. Goodbye!')
process.exit(0)
}
return result as Exclude<Awaited<ReturnType<T>>, symbol>
}
}

export const text = wrapClack(_text)
export const confirm = wrapClack(_confirm)
28 changes: 28 additions & 0 deletions packages/create-vue-vine/src/utils/deepMerge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const isObject = (val: unknown) => val && typeof val === 'object'
const mergeArrayWithDedupe = (a: unknown[], b: unknown[]) => Array.from(new Set([...a, ...b]))

/**
* Recursively merge the content of the new object to the existing one
* @param target the existing object
* @param obj the new object
*/
export function deepMerge(target: Record<string, any>, obj: Record<string, any>) {
for (const key of Object.keys(obj)) {
const oldVal = target[key]
const newVal = obj[key]

if (Array.isArray(oldVal) && Array.isArray(newVal)) {
target[key] = mergeArrayWithDedupe(oldVal, newVal)
}
else if (isObject(oldVal) && isObject(newVal)) {
target[key] = deepMerge(oldVal, newVal)
}
else {
target[key] = newVal
}
}

return target
}

export default deepMerge

0 comments on commit c404c79

Please sign in to comment.