@dengzhibo/create-vue-template 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -7
- package/LICENSE +21 -21
- package/README.md +63 -63
- package/bin/index.js +39 -39
- package/lib/generate.js +90 -90
- package/lib/options.js +48 -48
- package/lib/prompts.js +317 -318
- package/package.json +1 -1
- package/templates/features/code-quality/.editorconfig +8 -8
- package/templates/features/code-quality/.oxfmtrc.json +5 -5
- package/templates/features/code-quality/.oxlintrc.json +10 -10
- package/templates/features/code-quality/eslint.config.ts +38 -38
- package/templates/features/code-quality/package.fragment.json +21 -21
- package/templates/features/code-quality/settings.fragment.json +7 -7
- package/templates/features/e2e/e2e/tsconfig.json +4 -4
- package/templates/features/e2e/e2e/vue.spec.ts +8 -8
- package/templates/features/e2e/package.fragment.json +8 -8
- package/templates/features/e2e/playwright.config.ts +110 -110
- package/templates/features/unit-test/package.fragment.json +11 -11
- package/templates/features/unit-test/src/components/__tests__/HelloWorld.spec.ts +11 -11
- package/templates/features/unit-test/tsconfig.vitest.json +19 -19
- package/templates/features/unit-test/vitest.config.ts +14 -14
package/CHANGELOG.md
CHANGED
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) [2026] [DengZhiBo]
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026] [DengZhiBo]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
# Vue + Vite 模板脚手架
|
|
2
|
-
|
|
3
|
-
生成现有 Vue + Vite + TypeScript 应用,保留 Router、Pinia 和 Vue DevTools。
|
|
4
|
-
|
|
5
|
-
## 用户使用(发布到 npm 后)
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
pnpm create @dengzhibo/vue-template@latest my-vue-app
|
|
9
|
-
|
|
10
|
-
# 等价的完整包名调用
|
|
11
|
-
pnpm dlx @dengzhibo/create-vue-template@latest my-vue-app
|
|
12
|
-
|
|
13
|
-
# 全选并安装依赖
|
|
14
|
-
pnpm create @dengzhibo/vue-template@latest my-vue-app --yes "--features=code-quality,unit-test,e2e" --install
|
|
15
|
-
|
|
16
|
-
# 不选任何功能,不安装依赖
|
|
17
|
-
pnpm create @dengzhibo/vue-template@latest my-vue-app --yes --features=
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
以上命令会从 npm 获取并运行脚手架,无需下载源码或全局安装;必须先发布对应包。`--install` 控制的是生成项目的依赖安装,不是脚手架自身的下载。每个示例独立执行,请使用空目录。
|
|
21
|
-
|
|
22
|
-
命令说明:[pnpm create](https://pnpm.io/cli/create)、[pnpm dlx](https://pnpm.io/cli/dlx)。
|
|
23
|
-
|
|
24
|
-
项目名取目标目录名,支持 my-vue-app 等带连字符名称;目录参数可使用相对路径、绝对路径或当前目录 .。目标目录必须为空,允许保留 .git。
|
|
25
|
-
|
|
26
|
-
向导顺序:创建位置 → 功能多选 → 是否安装依赖 → 确认、返回修改或取消。
|
|
27
|
-
|
|
28
|
-
功能默认全部不选,Space 勾选,Enter 确认,可任意组合:
|
|
29
|
-
|
|
30
|
-
- 代码规范(code-quality):ESLint + Oxlint + Oxfmt。
|
|
31
|
-
- 单元测试(unit-test):Vitest、Vue Test Utils 和现有测试示例。
|
|
32
|
-
- 端到端测试(e2e):Playwright 和现有测试示例。
|
|
33
|
-
|
|
34
|
-
Windows PowerShell 中,包含逗号的 `--features` 参数应加引号,如上例。
|
|
35
|
-
|
|
36
|
-
--yes 使用非交互模式;--features= 指定逗号分隔的功能,空值为全部不选;--install 执行 pnpm install,默认不安装。
|
|
37
|
-
|
|
38
|
-
未选择的功能不生成对应配置、依赖、脚本或编辑器推荐。测试的 ESLint 规则仅在代码规范与相应测试同时选择时生成。
|
|
39
|
-
|
|
40
|
-
不生成 Git hooks、CI、Docker 或部署管理。基础模板现有 Vue RC 与 TypeScript bridge 覆盖保持原样;本次仅拆分现有能力。
|
|
41
|
-
|
|
42
|
-
CLI Node 要求见 package.json;生成应用的 Node 要求见 templates/base/package.json。
|
|
43
|
-
|
|
44
|
-
## 源码开发与维护
|
|
45
|
-
|
|
46
|
-
仅在本包源码目录中使用以下命令进行本地调试:
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
node bin/index.js my-vue-app
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
验证:
|
|
53
|
-
|
|
54
|
-
```sh
|
|
55
|
-
node --test tests/*.test.mjs
|
|
56
|
-
node scripts/check-package.mjs
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
测试覆盖 8 种功能组合、CLI 参数、向导返回修改、取消及目标目录保护。不要在 templates 内安装依赖。
|
|
60
|
-
|
|
61
|
-
## 发布文件范围
|
|
62
|
-
|
|
63
|
-
`bin/`、`lib/`、`templates/`、README、CHANGELOG、LICENSE 与 package.json 随 npm 包发布。测试、预检脚本、AGENTS.md 和本地缓存仅用于仓库维护,不需要上传 npm。`_gitignore` 在生成项目时恢复为 `.gitignore`。
|
|
1
|
+
# Vue + Vite 模板脚手架
|
|
2
|
+
|
|
3
|
+
生成现有 Vue + Vite + TypeScript 应用,保留 Router、Pinia 和 Vue DevTools。
|
|
4
|
+
|
|
5
|
+
## 用户使用(发布到 npm 后)
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm create @dengzhibo/vue-template@latest my-vue-app
|
|
9
|
+
|
|
10
|
+
# 等价的完整包名调用
|
|
11
|
+
pnpm dlx @dengzhibo/create-vue-template@latest my-vue-app
|
|
12
|
+
|
|
13
|
+
# 全选并安装依赖
|
|
14
|
+
pnpm create @dengzhibo/vue-template@latest my-vue-app --yes "--features=code-quality,unit-test,e2e" --install
|
|
15
|
+
|
|
16
|
+
# 不选任何功能,不安装依赖
|
|
17
|
+
pnpm create @dengzhibo/vue-template@latest my-vue-app --yes --features=
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
以上命令会从 npm 获取并运行脚手架,无需下载源码或全局安装;必须先发布对应包。`--install` 控制的是生成项目的依赖安装,不是脚手架自身的下载。每个示例独立执行,请使用空目录。
|
|
21
|
+
|
|
22
|
+
命令说明:[pnpm create](https://pnpm.io/cli/create)、[pnpm dlx](https://pnpm.io/cli/dlx)。
|
|
23
|
+
|
|
24
|
+
项目名取目标目录名,支持 my-vue-app 等带连字符名称;目录参数可使用相对路径、绝对路径或当前目录 .。目标目录必须为空,允许保留 .git。
|
|
25
|
+
|
|
26
|
+
向导顺序:创建位置 → 功能多选 → 是否安装依赖 → 确认、返回修改或取消。
|
|
27
|
+
|
|
28
|
+
功能默认全部不选,Space 勾选,Enter 确认,可任意组合:
|
|
29
|
+
|
|
30
|
+
- 代码规范(code-quality):ESLint + Oxlint + Oxfmt。
|
|
31
|
+
- 单元测试(unit-test):Vitest、Vue Test Utils 和现有测试示例。
|
|
32
|
+
- 端到端测试(e2e):Playwright 和现有测试示例。
|
|
33
|
+
|
|
34
|
+
Windows PowerShell 中,包含逗号的 `--features` 参数应加引号,如上例。
|
|
35
|
+
|
|
36
|
+
--yes 使用非交互模式;--features= 指定逗号分隔的功能,空值为全部不选;--install 执行 pnpm install,默认不安装。
|
|
37
|
+
|
|
38
|
+
未选择的功能不生成对应配置、依赖、脚本或编辑器推荐。测试的 ESLint 规则仅在代码规范与相应测试同时选择时生成。
|
|
39
|
+
|
|
40
|
+
不生成 Git hooks、CI、Docker 或部署管理。基础模板现有 Vue RC 与 TypeScript bridge 覆盖保持原样;本次仅拆分现有能力。
|
|
41
|
+
|
|
42
|
+
CLI Node 要求见 package.json;生成应用的 Node 要求见 templates/base/package.json。
|
|
43
|
+
|
|
44
|
+
## 源码开发与维护
|
|
45
|
+
|
|
46
|
+
仅在本包源码目录中使用以下命令进行本地调试:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
node bin/index.js my-vue-app
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
验证:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
node --test tests/*.test.mjs
|
|
56
|
+
node scripts/check-package.mjs
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
测试覆盖 8 种功能组合、CLI 参数、向导返回修改、取消及目标目录保护。不要在 templates 内安装依赖。
|
|
60
|
+
|
|
61
|
+
## 发布文件范围
|
|
62
|
+
|
|
63
|
+
`bin/`、`lib/`、`templates/`、README、CHANGELOG、LICENSE 与 package.json 随 npm 包发布。测试、预检脚本、AGENTS.md 和本地缓存仅用于仓库维护,不需要上传 npm。`_gitignore` 在生成项目时恢复为 `.gitignore`。
|
package/bin/index.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console */
|
|
3
|
-
import { execSync } from 'node:child_process'
|
|
4
|
-
import { createPlan, writePlan } from '../lib/generate.js'
|
|
5
|
-
import { help, parseArgs } from '../lib/options.js'
|
|
6
|
-
import { Cancelled, collectOptions } from '../lib/prompts.js'
|
|
7
|
-
|
|
8
|
-
async function main() {
|
|
9
|
-
const args = process.argv.slice(2)
|
|
10
|
-
const parsed = parseArgs(args)
|
|
11
|
-
if (parsed.help) {
|
|
12
|
-
console.log(help)
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
if (!parsed.yes && (!process.stdin.isTTY || !process.stdout.isTTY))
|
|
16
|
-
throw new Error('非交互环境请使用 --yes')
|
|
17
|
-
const options = parsed.yes ? parsed.options : await collectOptions(parsed.options)
|
|
18
|
-
if (!options) {
|
|
19
|
-
console.log('已取消,未创建项目。')
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
const target = writePlan(createPlan(options))
|
|
23
|
-
console.log(`✔ 项目已创建:${target}`)
|
|
24
|
-
if (options.install) {
|
|
25
|
-
try {
|
|
26
|
-
execSync('pnpm install', { cwd: target, stdio: 'inherit' })
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
console.error('安装失败,文件保留,请手动运行 pnpm install')
|
|
30
|
-
process.exitCode = 1
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
console.log(`cd "${target}"\n${options.install ? '' : 'pnpm install\n'}pnpm dev`)
|
|
35
|
-
}
|
|
36
|
-
main().catch((error) => {
|
|
37
|
-
console.error(error instanceof Cancelled ? '已取消,未创建项目。' : error.message)
|
|
38
|
-
process.exitCode = error instanceof Cancelled ? 130 : 1
|
|
39
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { createPlan, writePlan } from '../lib/generate.js'
|
|
5
|
+
import { help, parseArgs } from '../lib/options.js'
|
|
6
|
+
import { Cancelled, collectOptions } from '../lib/prompts.js'
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const args = process.argv.slice(2)
|
|
10
|
+
const parsed = parseArgs(args)
|
|
11
|
+
if (parsed.help) {
|
|
12
|
+
console.log(help)
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
if (!parsed.yes && (!process.stdin.isTTY || !process.stdout.isTTY))
|
|
16
|
+
throw new Error('非交互环境请使用 --yes')
|
|
17
|
+
const options = parsed.yes ? parsed.options : await collectOptions(parsed.options)
|
|
18
|
+
if (!options) {
|
|
19
|
+
console.log('已取消,未创建项目。')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const target = writePlan(createPlan(options))
|
|
23
|
+
console.log(`✔ 项目已创建:${target}`)
|
|
24
|
+
if (options.install) {
|
|
25
|
+
try {
|
|
26
|
+
execSync('pnpm install', { cwd: target, stdio: 'inherit' })
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.error('安装失败,文件保留,请手动运行 pnpm install')
|
|
30
|
+
process.exitCode = 1
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
console.log(`cd "${target}"\n${options.install ? '' : 'pnpm install\n'}pnpm dev`)
|
|
35
|
+
}
|
|
36
|
+
main().catch((error) => {
|
|
37
|
+
console.error(error instanceof Cancelled ? '已取消,未创建项目。' : error.message)
|
|
38
|
+
process.exitCode = error instanceof Cancelled ? 130 : 1
|
|
39
|
+
})
|
package/lib/generate.js
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
import { assertTarget, featureLabels, normalizeOptions } from './options.js'
|
|
5
|
-
|
|
6
|
-
const templateDir = fileURLToPath(new URL('../templates/', import.meta.url))
|
|
7
|
-
const skip = new Set(['node_modules', 'dist', '.npmignore', '.git', '.DS_Store', 'pnpm-lock.yaml', '.turbo'])
|
|
8
|
-
const rename = { _gitignore: '.gitignore' }
|
|
9
|
-
const encode = value => `${JSON.stringify(value, null, 2)}\n`
|
|
10
|
-
export function createPlan(input = {}, cwd = process.cwd()) {
|
|
11
|
-
const options = normalizeOptions(input)
|
|
12
|
-
const target = path.resolve(cwd, options.location)
|
|
13
|
-
const name = path.basename(target)
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { assertTarget, featureLabels, normalizeOptions } from './options.js'
|
|
5
|
+
|
|
6
|
+
const templateDir = fileURLToPath(new URL('../templates/', import.meta.url))
|
|
7
|
+
const skip = new Set(['node_modules', 'dist', '.npmignore', '.git', '.DS_Store', 'pnpm-lock.yaml', '.turbo'])
|
|
8
|
+
const rename = { _gitignore: '.gitignore' }
|
|
9
|
+
const encode = value => `${JSON.stringify(value, null, 2)}\n`
|
|
10
|
+
export function createPlan(input = {}, cwd = process.cwd()) {
|
|
11
|
+
const options = normalizeOptions(input)
|
|
12
|
+
const target = path.resolve(cwd, options.location)
|
|
13
|
+
const name = path.basename(target)
|
|
14
14
|
if (!/^[a-z0-9][\w.-]*$/i.test(name))
|
|
15
|
-
throw new Error('项目名不合法')
|
|
16
|
-
assertTarget(target)
|
|
17
|
-
const files = new Map()
|
|
18
|
-
const has = feature => options.features.includes(feature)
|
|
19
|
-
function add(directory, relative = '') {
|
|
20
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
15
|
+
throw new Error('项目名不合法')
|
|
16
|
+
assertTarget(target)
|
|
17
|
+
const files = new Map()
|
|
18
|
+
const has = feature => options.features.includes(feature)
|
|
19
|
+
function add(directory, relative = '') {
|
|
20
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
21
21
|
if (skip.has(entry.name) || entry.name.endsWith('.fragment.json'))
|
|
22
|
-
continue
|
|
23
|
-
const source = path.join(directory, entry.name)
|
|
22
|
+
continue
|
|
23
|
+
const source = path.join(directory, entry.name)
|
|
24
24
|
if (entry.isSymbolicLink())
|
|
25
|
-
throw new Error(`模板不支持符号链接:${source}`)
|
|
26
|
-
const destination = path.posix.join(relative, rename[entry.name] || entry.name)
|
|
25
|
+
throw new Error(`模板不支持符号链接:${source}`)
|
|
26
|
+
const destination = path.posix.join(relative, rename[entry.name] || entry.name)
|
|
27
27
|
if (entry.isDirectory()) {
|
|
28
28
|
add(source, destination)
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
31
|
if (files.has(destination))
|
|
32
|
-
throw new Error(`模板文件冲突:${destination}`)
|
|
33
|
-
files.set(destination, fs.readFileSync(source))
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
add(path.join(templateDir, 'base'))
|
|
38
|
-
const parse = file => JSON.parse(files.get(file).toString())
|
|
39
|
-
const pkg = parse('package.json')
|
|
40
|
-
for (const feature of options.features) {
|
|
41
|
-
const directory = path.join(templateDir, 'features', feature)
|
|
42
|
-
add(directory)
|
|
43
|
-
const fragment = JSON.parse(fs.readFileSync(path.join(directory, 'package.fragment.json'), 'utf8'))
|
|
44
|
-
for (const field of ['scripts', 'dependencies', 'devDependencies']) {
|
|
45
|
-
for (const [key, value] of Object.entries(fragment[field] || {})) {
|
|
46
|
-
pkg[field] ||= {}
|
|
32
|
+
throw new Error(`模板文件冲突:${destination}`)
|
|
33
|
+
files.set(destination, fs.readFileSync(source))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
add(path.join(templateDir, 'base'))
|
|
38
|
+
const parse = file => JSON.parse(files.get(file).toString())
|
|
39
|
+
const pkg = parse('package.json')
|
|
40
|
+
for (const feature of options.features) {
|
|
41
|
+
const directory = path.join(templateDir, 'features', feature)
|
|
42
|
+
add(directory)
|
|
43
|
+
const fragment = JSON.parse(fs.readFileSync(path.join(directory, 'package.fragment.json'), 'utf8'))
|
|
44
|
+
for (const field of ['scripts', 'dependencies', 'devDependencies']) {
|
|
45
|
+
for (const [key, value] of Object.entries(fragment[field] || {})) {
|
|
46
|
+
pkg[field] ||= {}
|
|
47
47
|
if (Object.hasOwn(pkg[field], key) && pkg[field][key] !== value)
|
|
48
|
-
throw new Error(`配置冲突:${key}`)
|
|
49
|
-
pkg[field][key] = value
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const extensions = parse('.vscode/extensions.json')
|
|
54
|
-
if (has('unit-test')) {
|
|
55
|
-
const tsconfig = parse('tsconfig.json')
|
|
56
|
-
tsconfig.references.push({ path: './tsconfig.vitest.json' })
|
|
57
|
-
files.set('tsconfig.json', encode(tsconfig))
|
|
58
|
-
extensions.recommendations.push('vitest.explorer')
|
|
59
|
-
}
|
|
48
|
+
throw new Error(`配置冲突:${key}`)
|
|
49
|
+
pkg[field][key] = value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const extensions = parse('.vscode/extensions.json')
|
|
54
|
+
if (has('unit-test')) {
|
|
55
|
+
const tsconfig = parse('tsconfig.json')
|
|
56
|
+
tsconfig.references.push({ path: './tsconfig.vitest.json' })
|
|
57
|
+
files.set('tsconfig.json', encode(tsconfig))
|
|
58
|
+
extensions.recommendations.push('vitest.explorer')
|
|
59
|
+
}
|
|
60
60
|
if (has('e2e'))
|
|
61
|
-
extensions.recommendations.push('ms-playwright.playwright')
|
|
62
|
-
if (has('code-quality')) {
|
|
63
|
-
let eslint = files.get('eslint.config.ts').toString().replace(/\r\n/g, '\n')
|
|
64
|
-
for (const [feature, plugin, dependency] of [['unit-test', 'pluginVitest', '@vitest/eslint-plugin'], ['e2e', 'pluginPlaywright', 'eslint-plugin-playwright']]) {
|
|
65
|
-
if (!has(feature)) {
|
|
66
|
-
delete pkg.devDependencies[dependency]
|
|
67
|
-
eslint = eslint.replace(new RegExp(`^import ${plugin}.*\\n`, 'm'), '')
|
|
68
|
-
eslint = eslint.replace(new RegExp(` \{\\n \\.\\.\\.${plugin}[\\s\\S]*?\\n \},\\n`), '')
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
files.set('eslint.config.ts', eslint)
|
|
72
|
-
const oxlint = parse('.oxlintrc.json')
|
|
61
|
+
extensions.recommendations.push('ms-playwright.playwright')
|
|
62
|
+
if (has('code-quality')) {
|
|
63
|
+
let eslint = files.get('eslint.config.ts').toString().replace(/\r\n/g, '\n')
|
|
64
|
+
for (const [feature, plugin, dependency] of [['unit-test', 'pluginVitest', '@vitest/eslint-plugin'], ['e2e', 'pluginPlaywright', 'eslint-plugin-playwright']]) {
|
|
65
|
+
if (!has(feature)) {
|
|
66
|
+
delete pkg.devDependencies[dependency]
|
|
67
|
+
eslint = eslint.replace(new RegExp(`^import ${plugin}.*\\n`, 'm'), '')
|
|
68
|
+
eslint = eslint.replace(new RegExp(` \{\\n \\.\\.\\.${plugin}[\\s\\S]*?\\n \},\\n`), '')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
files.set('eslint.config.ts', eslint)
|
|
72
|
+
const oxlint = parse('.oxlintrc.json')
|
|
73
73
|
if (!has('unit-test'))
|
|
74
|
-
oxlint.plugins = oxlint.plugins.filter(p => p !== 'vitest')
|
|
75
|
-
files.set('.oxlintrc.json', encode(oxlint))
|
|
76
|
-
const settings = JSON.parse(fs.readFileSync(path.join(templateDir, 'features/code-quality/settings.fragment.json'), 'utf8'))
|
|
77
|
-
files.set('.vscode/settings.json', encode({ ...parse('.vscode/settings.json'), ...settings }))
|
|
78
|
-
extensions.recommendations.push('dbaeumer.vscode-eslint', 'EditorConfig.EditorConfig', 'oxc.oxc-vscode')
|
|
79
|
-
}
|
|
80
|
-
files.set('.vscode/extensions.json', encode(extensions))
|
|
81
|
-
pkg.name = name
|
|
82
|
-
files.set('package.json', encode(pkg))
|
|
83
|
-
let readme = files.get('README.md').toString().replace(/\r\n/g, '\n')
|
|
84
|
-
const sections = [['unit-test', 'Run Unit Tests'], ['e2e', 'Run End-to-End Tests'], ['code-quality', 'Lint with']]
|
|
85
|
-
for (const [feature, title] of sections) {
|
|
74
|
+
oxlint.plugins = oxlint.plugins.filter(p => p !== 'vitest')
|
|
75
|
+
files.set('.oxlintrc.json', encode(oxlint))
|
|
76
|
+
const settings = JSON.parse(fs.readFileSync(path.join(templateDir, 'features/code-quality/settings.fragment.json'), 'utf8'))
|
|
77
|
+
files.set('.vscode/settings.json', encode({ ...parse('.vscode/settings.json'), ...settings }))
|
|
78
|
+
extensions.recommendations.push('dbaeumer.vscode-eslint', 'EditorConfig.EditorConfig', 'oxc.oxc-vscode')
|
|
79
|
+
}
|
|
80
|
+
files.set('.vscode/extensions.json', encode(extensions))
|
|
81
|
+
pkg.name = name
|
|
82
|
+
files.set('package.json', encode(pkg))
|
|
83
|
+
let readme = files.get('README.md').toString().replace(/\r\n/g, '\n')
|
|
84
|
+
const sections = [['unit-test', 'Run Unit Tests'], ['e2e', 'Run End-to-End Tests'], ['code-quality', 'Lint with']]
|
|
85
|
+
for (const [feature, title] of sections) {
|
|
86
86
|
if (!has(feature))
|
|
87
|
-
readme = readme.replace(new RegExp(`### ${title}[\\s\\S]*?(?=\\n### |$)`), '')
|
|
88
|
-
}
|
|
89
|
-
files.set('README.md', `${readme.replace('# base', `# ${name}`).trimEnd()}\n\n已选功能:${options.features.map(f => featureLabels[f]).join('、') || '无'}\n`)
|
|
90
|
-
return { target, options, files }
|
|
91
|
-
}
|
|
92
|
-
export function writePlan(plan) {
|
|
93
|
-
assertTarget(plan.target)
|
|
94
|
-
for (const [relative, content] of plan.files) {
|
|
95
|
-
const file = path.join(plan.target, relative)
|
|
96
|
-
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
97
|
-
fs.writeFileSync(file, content)
|
|
98
|
-
}
|
|
99
|
-
return plan.target
|
|
100
|
-
}
|
|
87
|
+
readme = readme.replace(new RegExp(`### ${title}[\\s\\S]*?(?=\\n### |$)`), '')
|
|
88
|
+
}
|
|
89
|
+
files.set('README.md', `${readme.replace('# base', `# ${name}`).trimEnd()}\n\n已选功能:${options.features.map(f => featureLabels[f]).join('、') || '无'}\n`)
|
|
90
|
+
return { target, options, files }
|
|
91
|
+
}
|
|
92
|
+
export function writePlan(plan) {
|
|
93
|
+
assertTarget(plan.target)
|
|
94
|
+
for (const [relative, content] of plan.files) {
|
|
95
|
+
const file = path.join(plan.target, relative)
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
97
|
+
fs.writeFileSync(file, content)
|
|
98
|
+
}
|
|
99
|
+
return plan.target
|
|
100
|
+
}
|
package/lib/options.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
export const featureLabels = { 'code-quality': '代码规范', 'unit-test': '单元测试', 'e2e': '端到端测试' }
|
|
5
|
-
export function normalizeOptions(input = {}) {
|
|
6
|
-
const features = input.features ?? []
|
|
7
|
-
if (!Array.isArray(features) || features.some(f => !Object.hasOwn(featureLabels, f)))
|
|
8
|
-
throw new Error('功能仅支持 code-quality、unit-test、e2e')
|
|
9
|
-
return { location: input.location || '.', features: Object.keys(featureLabels).filter(f => features.includes(f)), install: Boolean(input.install) }
|
|
10
|
-
}
|
|
11
|
-
export function assertTarget(target) {
|
|
12
|
-
let current = target
|
|
13
|
-
while (true) {
|
|
14
|
-
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
15
|
-
throw new Error('目标路径不能经过符号链接')
|
|
16
|
-
const parent = path.dirname(current)
|
|
17
|
-
if (parent === current)
|
|
18
|
-
break
|
|
19
|
-
current = parent
|
|
20
|
-
}
|
|
21
|
-
if (fs.existsSync(target) && (!fs.statSync(target).isDirectory() || fs.readdirSync(target).some(name => name !== '.git')))
|
|
22
|
-
throw new Error('目标必须为空目录(允许保留 .git)')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const help = `用法:create-vue-template [目录] [选项]
|
|
26
|
-
--yes, -y 非交互创建
|
|
27
|
-
--features=code-quality,unit-test,e2e 选择功能,默认全部不选
|
|
28
|
-
--features= 全部不选
|
|
29
|
-
--install 安装依赖,默认不安装
|
|
30
|
-
--help, -h 帮助
|
|
31
|
-
项目名取目标目录名,例如 my-vue-app。`
|
|
32
|
-
export function parseArgs(args) {
|
|
33
|
-
if (args.includes('--help') || args.includes('-h'))
|
|
34
|
-
return { help: true }
|
|
35
|
-
const options = normalizeOptions()
|
|
36
|
-
const locations = []
|
|
37
|
-
let yes = false
|
|
38
|
-
for (const arg of args) {
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const featureLabels = { 'code-quality': '代码规范', 'unit-test': '单元测试', 'e2e': '端到端测试' }
|
|
5
|
+
export function normalizeOptions(input = {}) {
|
|
6
|
+
const features = input.features ?? []
|
|
7
|
+
if (!Array.isArray(features) || features.some(f => !Object.hasOwn(featureLabels, f)))
|
|
8
|
+
throw new Error('功能仅支持 code-quality、unit-test、e2e')
|
|
9
|
+
return { location: input.location || '.', features: Object.keys(featureLabels).filter(f => features.includes(f)), install: Boolean(input.install) }
|
|
10
|
+
}
|
|
11
|
+
export function assertTarget(target) {
|
|
12
|
+
let current = target
|
|
13
|
+
while (true) {
|
|
14
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
15
|
+
throw new Error('目标路径不能经过符号链接')
|
|
16
|
+
const parent = path.dirname(current)
|
|
17
|
+
if (parent === current)
|
|
18
|
+
break
|
|
19
|
+
current = parent
|
|
20
|
+
}
|
|
21
|
+
if (fs.existsSync(target) && (!fs.statSync(target).isDirectory() || fs.readdirSync(target).some(name => name !== '.git')))
|
|
22
|
+
throw new Error('目标必须为空目录(允许保留 .git)')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const help = `用法:create-vue-template [目录] [选项]
|
|
26
|
+
--yes, -y 非交互创建
|
|
27
|
+
--features=code-quality,unit-test,e2e 选择功能,默认全部不选
|
|
28
|
+
--features= 全部不选
|
|
29
|
+
--install 安装依赖,默认不安装
|
|
30
|
+
--help, -h 帮助
|
|
31
|
+
项目名取目标目录名,例如 my-vue-app。`
|
|
32
|
+
export function parseArgs(args) {
|
|
33
|
+
if (args.includes('--help') || args.includes('-h'))
|
|
34
|
+
return { help: true }
|
|
35
|
+
const options = normalizeOptions()
|
|
36
|
+
const locations = []
|
|
37
|
+
let yes = false
|
|
38
|
+
for (const arg of args) {
|
|
39
39
|
if (['--yes', '-y'].includes(arg))
|
|
40
|
-
yes = true
|
|
40
|
+
yes = true
|
|
41
41
|
else if (arg.startsWith('--features='))
|
|
42
|
-
options.features = arg.slice('--features='.length) === '' ? [] : arg.slice('--features='.length).split(',')
|
|
42
|
+
options.features = arg.slice('--features='.length) === '' ? [] : arg.slice('--features='.length).split(',')
|
|
43
43
|
else if (arg === '--install')
|
|
44
|
-
options.install = true
|
|
44
|
+
options.install = true
|
|
45
45
|
else if (arg.startsWith('-'))
|
|
46
|
-
throw new Error(`未知参数:${arg}`)
|
|
47
|
-
else locations.push(arg)
|
|
48
|
-
}
|
|
46
|
+
throw new Error(`未知参数:${arg}`)
|
|
47
|
+
else locations.push(arg)
|
|
48
|
+
}
|
|
49
49
|
if (locations.length > 1)
|
|
50
|
-
throw new Error('只能指定一个创建目录')
|
|
51
|
-
options.location = locations[0] || '.'
|
|
52
|
-
return { options: normalizeOptions(options), yes }
|
|
53
|
-
}
|
|
50
|
+
throw new Error('只能指定一个创建目录')
|
|
51
|
+
options.location = locations[0] || '.'
|
|
52
|
+
return { options: normalizeOptions(options), yes }
|
|
53
|
+
}
|