create-component-template-cli 1.0.0
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +12 -0
- package/CHANGELOG.md +49 -0
- package/CLAUDE.md +102 -0
- package/README.md +29 -0
- package/bin/cli.mjs +114 -0
- package/package.json +29 -0
- package/plop-templates/data.ts.hbs +7 -0
- package/plop-templates/hook.ts.hbs +29 -0
- package/plop-templates/index.scss.hbs +1 -0
- package/plop-templates/index.setup.vue.hbs +25 -0
- package/plop-templates/index.ts.hbs +3 -0
- package/plop-templates/index.vue.hbs +38 -0
- package/plop-templates/typing.ts.hbs +14 -0
- package/plopfile.mjs +133 -0
- package/src/generate.mjs +85 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
+
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
+
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
6
|
+
|
|
7
|
+
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "restricted",
|
|
8
|
+
"baseBranch": "master",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"ignore": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(gh auth:*)",
|
|
5
|
+
"Bash(gh repo:*)",
|
|
6
|
+
"Bash(git filter-branch:*)",
|
|
7
|
+
"Bash(node bin/cli.mjs --help)",
|
|
8
|
+
"Bash(node bin/cli.mjs --version)",
|
|
9
|
+
"Bash(node bin/cli.mjs --name=TestComp --rootPath=src/components/platform)"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# create-vue-component-template
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- test
|
|
8
|
+
|
|
9
|
+
## 1.0.11
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 修改模板
|
|
14
|
+
|
|
15
|
+
## 1.0.10
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 修改 hooks
|
|
20
|
+
|
|
21
|
+
## 1.0.9
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 修改提示
|
|
26
|
+
|
|
27
|
+
## 1.0.8
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 测试新增模板选项
|
|
32
|
+
|
|
33
|
+
## 1.0.7
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- 完善 hooks
|
|
38
|
+
|
|
39
|
+
## 1.0.6
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- test
|
|
44
|
+
|
|
45
|
+
## 1.0.5
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- 修改 vue 组件代码
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
**create-vue-component-template** (CLI command: `cvct`) — a CLI scaffolding tool that generates Vue component directory structures with predefined templates. Published to npm as a global CLI utility.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
### Non-Interactive Mode (for agents/CI)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Generate a setup-syntax component
|
|
15
|
+
node bin/cli.mjs --name=MyComponent --rootPath=src/components/platform
|
|
16
|
+
|
|
17
|
+
# Generate a normal (defineComponent) component
|
|
18
|
+
node bin/cli.mjs --name=MyComponent --rootPath=src/components/platform --templateType=normal
|
|
19
|
+
|
|
20
|
+
# Overwrite existing directory
|
|
21
|
+
node bin/cli.mjs --name=MyComponent --rootPath=src/components/platform --collisionStrategy=overwrite
|
|
22
|
+
|
|
23
|
+
# Strict non-interactive (error if missing required args)
|
|
24
|
+
node bin/cli.mjs --name=MyComponent --rootPath=src/components/platform --nonInteractive
|
|
25
|
+
|
|
26
|
+
# Help & version
|
|
27
|
+
node bin/cli.mjs --help
|
|
28
|
+
node bin/cli.mjs --version
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Interactive Mode (for humans)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Run without args to enter plop interactive prompts
|
|
35
|
+
node bin/cli.mjs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Version bump via changesets
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx changeset # create a changeset
|
|
42
|
+
npx changeset version # consume changesets, bump package.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CLI Flags Reference
|
|
46
|
+
|
|
47
|
+
| Flag | Required | Default | Description |
|
|
48
|
+
|------|----------|---------|-------------|
|
|
49
|
+
| `--name` | Yes (non-interactive) | — | Component name, PascalCase (e.g. `MyTest`) |
|
|
50
|
+
| `--rootPath` | Yes (non-interactive) | — | Target directory (e.g. `src/components/platform`) |
|
|
51
|
+
| `--templateType` | No | `setup` | `setup` = `<script setup>` + defineProps; `normal` = defineComponent + PropType |
|
|
52
|
+
| `--collisionStrategy` | No | `skip` | `skip` = error if exists; `overwrite` = delete & regenerate |
|
|
53
|
+
| `--nonInteractive` | No | false | Fail on missing args instead of prompting |
|
|
54
|
+
| `--help` / `-h` | No | — | Print help text |
|
|
55
|
+
| `--version` / `-v` | No | — | Print version |
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
**Entry point:** `bin/cli.mjs` — dispatches to non-interactive mode (`src/generate.mjs`) when all required CLI args are provided, otherwise falls back to plop interactive mode.
|
|
60
|
+
|
|
61
|
+
**Core generation:** `src/generate.mjs` — `generateComponent({ name, rootPath, templateType, collisionStrategy })` — pure function that:
|
|
62
|
+
1. Validates inputs
|
|
63
|
+
2. Converts case (PascalCase, kebab-case via `change-case`)
|
|
64
|
+
3. Detects directory collisions (skip or overwrite)
|
|
65
|
+
4. Renders `plop-templates/*.hbs` templates and writes files
|
|
66
|
+
5. Returns `{ success, files, componentName, targetFolder }` or `{ success: false, error }`
|
|
67
|
+
|
|
68
|
+
**Interactive mode:** `plopfile.mjs` — plop generator for human interactive use.
|
|
69
|
+
|
|
70
|
+
**Templates:** `plop-templates/*.hbs` — Handlebars-style template files.
|
|
71
|
+
|
|
72
|
+
### Two Template Modes
|
|
73
|
+
|
|
74
|
+
- **setup** (`index.setup.vue.hbs`): `<script setup>` + `defineProps<>()` + `defineOptions()`
|
|
75
|
+
- **normal** (`index.vue.hbs`): `defineComponent` + `PropType` + `setup()` function
|
|
76
|
+
|
|
77
|
+
### Generated Component Structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
{rootPath}/{ComponentName}/
|
|
81
|
+
├── index.ts # Re-exports component and types
|
|
82
|
+
└── src/
|
|
83
|
+
├── index.vue # Component template
|
|
84
|
+
├── index.scss # Scoped styles
|
|
85
|
+
├── typing.ts # Props interface + enums
|
|
86
|
+
├── hook.ts # use{ComponentName}() composable
|
|
87
|
+
└── data.ts # Data arrays
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Template Variables
|
|
91
|
+
|
|
92
|
+
| Variable | Source | Example |
|
|
93
|
+
|----------|--------|---------|
|
|
94
|
+
| `{{componentName}}` | PascalCase of input name | `MyTest` |
|
|
95
|
+
| `{{class}}` | kebab-case of input name | `my-test` |
|
|
96
|
+
| `{{interfaceName}}` | Same as componentName | `MyTest` |
|
|
97
|
+
|
|
98
|
+
The `change-case` library handles all case conversions.
|
|
99
|
+
|
|
100
|
+
## Versioning
|
|
101
|
+
|
|
102
|
+
Uses `@changesets/cli` for version management. Changeset access is `restricted` (private by default). Base branch is `master`.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# create-vue-component-template
|
|
2
|
+
|
|
3
|
+
A CLI tool for creating standardized Vue 3 + TypeScript + Sass public component templates.
|
|
4
|
+
|
|
5
|
+
## USAGE
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# create a components via npm
|
|
9
|
+
npm create-vue-component-template --rootPath src/components/platform
|
|
10
|
+
|
|
11
|
+
# or
|
|
12
|
+
npx create-vue-component-template --rootPath src/components/platform
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
directory tree like this:
|
|
17
|
+
|
|
18
|
+
.
|
|
19
|
+
└── Demo/
|
|
20
|
+
├── src/
|
|
21
|
+
│ ├── components/
|
|
22
|
+
│ │ ├── aaa/
|
|
23
|
+
│ │ ├── bbb/
|
|
24
|
+
│ │ └── .../
|
|
25
|
+
│ ├── data.ts
|
|
26
|
+
│ ├── typing.ts
|
|
27
|
+
│ ├── hooks.ts
|
|
28
|
+
│ └── index.scss
|
|
29
|
+
└── index.ts
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import minimist from 'minimist';
|
|
6
|
+
import { generateComponent } from '../src/generate.mjs';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const argv = minimist(args);
|
|
11
|
+
|
|
12
|
+
function printVersion() {
|
|
13
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
14
|
+
console.log(`cvct v${pkg.version}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
cvct — Vue 组件模板生成器 (create-vue-component-template)
|
|
20
|
+
|
|
21
|
+
用法:
|
|
22
|
+
cvct [选项]
|
|
23
|
+
|
|
24
|
+
必填参数 (非交互模式):
|
|
25
|
+
--name=<ComponentName> 组件名称,PascalCase 格式 (例: MyTest)
|
|
26
|
+
--rootPath=<path> 组件生成路径 (例: src/components/platform)
|
|
27
|
+
|
|
28
|
+
可选参数:
|
|
29
|
+
--templateType=<type> 模板类型: setup (默认) | normal
|
|
30
|
+
- setup: <script setup> + defineProps<> + defineOptions
|
|
31
|
+
- normal: defineComponent + PropType + setup()
|
|
32
|
+
|
|
33
|
+
--collisionStrategy=<s> 目录碰撞策略: skip (默认) | overwrite
|
|
34
|
+
- skip: 目录已存在时报错退出
|
|
35
|
+
- overwrite: 删除旧目录后重新生成
|
|
36
|
+
|
|
37
|
+
--nonInteractive 强制非交互模式,缺少必填参数时报错而非提示
|
|
38
|
+
|
|
39
|
+
-h, --help 显示帮助信息
|
|
40
|
+
-v, --version 显示版本号
|
|
41
|
+
|
|
42
|
+
非交互模式示例:
|
|
43
|
+
cvct --name=MyTest --rootPath=src/components/platform
|
|
44
|
+
cvct --name=MyTest --rootPath=src/components/platform --templateType=normal
|
|
45
|
+
cvct --name=MyTest --rootPath=src/components/platform --collisionStrategy=overwrite
|
|
46
|
+
|
|
47
|
+
生成的目录结构:
|
|
48
|
+
{rootPath}/{ComponentName}/
|
|
49
|
+
├── index.ts # 导出组件和类型
|
|
50
|
+
└── src/
|
|
51
|
+
├── index.vue # 组件模板
|
|
52
|
+
├── index.scss # 样式
|
|
53
|
+
├── typing.ts # Props 接口 + 枚举
|
|
54
|
+
├── hook.ts # use{ComponentName}() composable
|
|
55
|
+
└── data.ts # 数据数组
|
|
56
|
+
|
|
57
|
+
交互模式:
|
|
58
|
+
不带参数运行 cvct 将进入交互式提示界面 (plop)
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (argv.help || argv.h) {
|
|
63
|
+
printHelp();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (argv.version || argv.v) {
|
|
68
|
+
printVersion();
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hasAllRequired = argv.name && argv.rootPath;
|
|
73
|
+
const nonInteractive = argv.nonInteractive;
|
|
74
|
+
|
|
75
|
+
if (nonInteractive && !hasAllRequired) {
|
|
76
|
+
console.error('错误:非交互模式需要 --name 和 --rootPath 参数');
|
|
77
|
+
console.error('运行 cvct --help 查看帮助');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (hasAllRequired) {
|
|
82
|
+
const result = generateComponent({
|
|
83
|
+
name: argv.name,
|
|
84
|
+
rootPath: argv.rootPath,
|
|
85
|
+
templateType: argv.templateType || 'setup',
|
|
86
|
+
collisionStrategy: argv.collisionStrategy || 'skip',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
console.error(`错误:${result.error}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`✓ 已生成组件: ${result.componentName}`);
|
|
95
|
+
console.log(` 路径: ${result.targetFolder}`);
|
|
96
|
+
result.files.forEach(f => console.log(` - ${f}`));
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fallback: interactive mode via plop
|
|
101
|
+
const { Plop, run } = await import('plop');
|
|
102
|
+
|
|
103
|
+
Plop.prepare({
|
|
104
|
+
configPath: path.join(__dirname, '../plopfile.mjs'),
|
|
105
|
+
import: []
|
|
106
|
+
}, env => {
|
|
107
|
+
Plop.execute(env, (env) => {
|
|
108
|
+
const options = {
|
|
109
|
+
...env,
|
|
110
|
+
dest: process.cwd()
|
|
111
|
+
};
|
|
112
|
+
return run(options, undefined, true);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-component-template-cli",
|
|
3
|
+
"description": "create-vue-component-template,A CLI tool for creating standardized Vue 3 + TypeScript + Sass public component templates.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"cvct": "bin/cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"access": "public",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@types/minimist": "^1.2.5",
|
|
15
|
+
"change-case": "^5.4.4",
|
|
16
|
+
"minimist": "^1.2.8",
|
|
17
|
+
"plop": "^4.0.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@changesets/cli": "^2.29.8"
|
|
21
|
+
},
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "young-man",
|
|
24
|
+
"email": "2419445705@qq.com"
|
|
25
|
+
},
|
|
26
|
+
"package-info": {
|
|
27
|
+
"branch": "agent-cli"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { data } from './data'
|
|
3
|
+
import { useTimeline } from '@/hooks';
|
|
4
|
+
import { {{ componentName }}Props } from './typing'
|
|
5
|
+
import emitter, { useEmitter } from '@/base/toolkit';
|
|
6
|
+
import { usePlatformStore, useProductStore, useGlobalStore } from '@/store';
|
|
7
|
+
import { ButtonGroupItem } from '@/components/common/SinglePickButtonGroup';
|
|
8
|
+
export const use{{componentName}} = (props: {{componentName}}Props) => {
|
|
9
|
+
const model = ref<string>('')
|
|
10
|
+
const platformStore = usePlatformStore();
|
|
11
|
+
const productStore = useProductStore();
|
|
12
|
+
const globalStore = useGlobalStore();
|
|
13
|
+
|
|
14
|
+
useTimeline(model,({currentTime,productTypes})=>{
|
|
15
|
+
// TODO 产品绑定时间轴,时间轴变化请求数据
|
|
16
|
+
})
|
|
17
|
+
useEmitter([
|
|
18
|
+
{
|
|
19
|
+
eventName:'test',
|
|
20
|
+
callback:()=> {
|
|
21
|
+
// TODO
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
model
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.{{class}} { position:absolute;z-index:999;width:500px;height:500px;top:0;left:0;background-color:#327ccb52}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="{{ class }}"></div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { {{ componentName }}Props, {{ componentName }}Enum } from './typing'
|
|
7
|
+
import { use{{componentName}} } from './hook'
|
|
8
|
+
import SinglePickButtonGroup from '@/components/common/SinglePickButtonGroup';
|
|
9
|
+
|
|
10
|
+
defineOptions({
|
|
11
|
+
name: `{{ componentName }}`
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{{ componentName }}Props>();
|
|
15
|
+
|
|
16
|
+
// withDefaults(defineProps<{{ componentName }}Props>(), {});
|
|
17
|
+
|
|
18
|
+
const { } = use{{componentName}}(props);
|
|
19
|
+
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<style lang="scss" scoped>
|
|
23
|
+
@import './index.scss';
|
|
24
|
+
// 如果有用到v-bind(),它在index.scss中定义会不生效,请在该文件内写入对应的样式。
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<div class="{{ class }}"></div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import {
|
|
9
|
+
defineComponent,
|
|
10
|
+
PropType
|
|
11
|
+
} from 'vue';
|
|
12
|
+
import { {{ componentName }}Props, {{ componentName }}Enum } from './typing'
|
|
13
|
+
import { use{{componentName}} } from './hook'
|
|
14
|
+
import SinglePickButtonGroup, { ButtonGroupItem } from '@/components/common/SinglePickButtonGroup';
|
|
15
|
+
export default defineComponent({
|
|
16
|
+
name: `{{ componentName }}`,
|
|
17
|
+
components: { },
|
|
18
|
+
props:{
|
|
19
|
+
// 示例,使用 PropType 强制指定接口类型
|
|
20
|
+
user: {
|
|
21
|
+
type: Object as PropType<{{ componentName }}Props['user']>,
|
|
22
|
+
required: false,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
setup(props, { emit }) {
|
|
26
|
+
|
|
27
|
+
const { } = use{{componentName}}(props)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
return {};
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style lang="scss" scoped>
|
|
36
|
+
@import './index.scss';
|
|
37
|
+
// 如果有用到v-bind(),它在index.scss中定义会不生效,请在该文件内写入对应的样式。
|
|
38
|
+
</style>
|
package/plopfile.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as changeCase from 'change-case';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const rootPath = `src/components/platform`;
|
|
9
|
+
|
|
10
|
+
export default (plop) => {
|
|
11
|
+
plop.setGenerator('create-component-template', {
|
|
12
|
+
description: '----组件模板生成器----',
|
|
13
|
+
prompts: [
|
|
14
|
+
{
|
|
15
|
+
type: 'input',
|
|
16
|
+
name: 'rootPath',
|
|
17
|
+
message: '请输入组件生成路径:',
|
|
18
|
+
// 逻辑:如果命令行传入了 --rootPath,则使用它,否则使用默认值
|
|
19
|
+
default: (data) => data.rootPath || `src/components/platform`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'list',
|
|
23
|
+
name: 'templateType',
|
|
24
|
+
message: '请选择组件模板(使用上下箭头进行选择):',
|
|
25
|
+
choices: [
|
|
26
|
+
{ name: 'VUE的setup语法糖组件模板', value: 'setup' },
|
|
27
|
+
{ name: 'VUE普通组合式API组件模板', value: 'normal' },
|
|
28
|
+
],
|
|
29
|
+
default: 'setup'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'name',
|
|
34
|
+
message: '请输入组件名称 (大驼峰):',
|
|
35
|
+
// 逻辑:如果命令行传入了 --name,则使用它,否则使用 'CustomComponent'
|
|
36
|
+
default: (data) => data.name || 'CustomComponent',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'list', // 使用 list 提供明确的选择
|
|
40
|
+
name: 'collisionStrategy',
|
|
41
|
+
message: (data) => `目录 ${data.rootPath}/${data.name} 已存在,请选择操作:`,
|
|
42
|
+
choices: [
|
|
43
|
+
{ name: '覆盖', value: 'overwrite' },
|
|
44
|
+
{ name: '重新输入名称', value: 'retry' },
|
|
45
|
+
],
|
|
46
|
+
// 关键:只有当目录存在时才触发
|
|
47
|
+
when: (data) => {
|
|
48
|
+
const targetPath = path.join(process.cwd(), data.rootPath, data.name);
|
|
49
|
+
return fs.existsSync(targetPath);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// 关键点:如果用户选择 'retry',我们通过 validate 拦截并强制用户回到上一步
|
|
54
|
+
type: 'input',
|
|
55
|
+
name: 'retryName',
|
|
56
|
+
message: '请重新输入组件名称(按回车确认后将刷新名称):',
|
|
57
|
+
when: (data) => data.collisionStrategy === 'retry',
|
|
58
|
+
validate: (value, data) => {
|
|
59
|
+
// 这里利用 validate 的特性:
|
|
60
|
+
// 我们直接修改上层 data.name,然后返回一个错误信息,
|
|
61
|
+
// 实际上会迫使 Inquirer 停留在这里,但此时 data.name 已经被修正
|
|
62
|
+
if (value && value !== data.name) {
|
|
63
|
+
data.name = value; // 覆盖之前的 name
|
|
64
|
+
// 检查新名字是否依然冲突
|
|
65
|
+
const newPath = path.join(process.cwd(), data.rootPath, value);
|
|
66
|
+
if (fs.existsSync(newPath)) {
|
|
67
|
+
return '新名称依然存在,请再次输入或 Ctrl+C 退出';
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return '请输入一个新的名称以避开冲突';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
actions: (data) => {
|
|
77
|
+
const actions = [];
|
|
78
|
+
// 最终确定的组件名(可能是初始输入的,也可能是 retry 后修正的)
|
|
79
|
+
const finalName = data.name;
|
|
80
|
+
|
|
81
|
+
const camelCase = changeCase.camelCase(finalName);
|
|
82
|
+
const componentName = changeCase.pascalCase(camelCase);
|
|
83
|
+
const className = changeCase.kebabCase(camelCase);
|
|
84
|
+
const targetFolder = `${data.rootPath}/${componentName}`;
|
|
85
|
+
|
|
86
|
+
// 1. 如果选择了覆盖,执行删除动作
|
|
87
|
+
if (data.collisionStrategy === 'overwrite') {
|
|
88
|
+
actions.push({
|
|
89
|
+
type: 'deleteTargetDir',
|
|
90
|
+
path: targetFolder,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const vueTemplateFile = data.templateType === 'setup' ? 'index.setup.vue.hbs' : 'index.vue.hbs';
|
|
95
|
+
|
|
96
|
+
// 2. 生成文件列表 (统一配置)
|
|
97
|
+
const templates = [
|
|
98
|
+
{ file: 'index.scss', data: { class: className } },
|
|
99
|
+
{
|
|
100
|
+
file: 'index.vue',
|
|
101
|
+
templateFile: vueTemplateFile,
|
|
102
|
+
data: { class: className, componentName }
|
|
103
|
+
},
|
|
104
|
+
{ file: 'typing.ts', data: { interfaceName: componentName } },
|
|
105
|
+
{ file: 'hook.ts', data: { componentName } },
|
|
106
|
+
{ file: 'index.ts', data: { componentName } },
|
|
107
|
+
{ file: 'data.ts' },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
templates.forEach(item => {
|
|
111
|
+
actions.push({
|
|
112
|
+
type: 'add',
|
|
113
|
+
force: true, // 既然上面已经处理了删除逻辑,这里 force: true 确保写入成功
|
|
114
|
+
path: item.file === 'index.ts' ? `${targetFolder}/index.ts` : `${targetFolder}/src/${item.file}`,
|
|
115
|
+
templateFile: `${__dirname}/plop-templates/${item.templateFile || item.file + '.hbs'}`,
|
|
116
|
+
data: item.data,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return actions;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 注册自定义 Action:删除目录
|
|
125
|
+
plop.setActionType('deleteTargetDir', (answers, config) => {
|
|
126
|
+
const targetDir = path.join(process.cwd(), config.path);
|
|
127
|
+
if (fs.existsSync(targetDir)) {
|
|
128
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
129
|
+
return `[Clean] 已清理旧目录: ${config.path}`;
|
|
130
|
+
}
|
|
131
|
+
return '无需清理';
|
|
132
|
+
});
|
|
133
|
+
};
|
package/src/generate.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as changeCase from 'change-case';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'plop-templates');
|
|
9
|
+
|
|
10
|
+
const VALID_TEMPLATE_TYPES = ['setup', 'normal'];
|
|
11
|
+
const VALID_COLLISION_STRATEGIES = ['skip', 'overwrite'];
|
|
12
|
+
|
|
13
|
+
const TEMPLATE_CONFIG = [
|
|
14
|
+
{ file: 'index.scss', dest: 'src', templateFile: 'index.scss.hbs', getData: (cn, cl) => ({ class: cl }) },
|
|
15
|
+
{ file: 'index.vue', dest: 'src', getData: (cn, cl, tt) => ({
|
|
16
|
+
templateFile: tt === 'setup' ? 'index.setup.vue.hbs' : 'index.vue.hbs',
|
|
17
|
+
data: { class: cl, componentName: cn },
|
|
18
|
+
})},
|
|
19
|
+
{ file: 'typing.ts', dest: 'src', templateFile: 'typing.ts.hbs', getData: (cn) => ({ interfaceName: cn }) },
|
|
20
|
+
{ file: 'hook.ts', dest: 'src', templateFile: 'hook.ts.hbs', getData: (cn) => ({ componentName: cn }) },
|
|
21
|
+
{ file: 'data.ts', dest: 'src', templateFile: 'data.ts.hbs', getData: () => ({}) },
|
|
22
|
+
{ file: 'index.ts', dest: '', templateFile: 'index.ts.hbs', getData: (cn) => ({ componentName: cn }) },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function renderTemplate(templateName, data) {
|
|
26
|
+
const filePath = path.join(TEMPLATES_DIR, templateName);
|
|
27
|
+
const source = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
+
let result = source;
|
|
29
|
+
for (const [key, value] of Object.entries(data)) {
|
|
30
|
+
result = result.replaceAll(`{{ ${key} }}`, String(value));
|
|
31
|
+
result = result.replaceAll(`{{${key}}}`, String(value));
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function generateComponent({ name, rootPath, templateType = 'setup', collisionStrategy = 'skip' }) {
|
|
37
|
+
if (!name || typeof name !== 'string') {
|
|
38
|
+
return { success: false, error: '缺少必填参数: --name (组件名称)' };
|
|
39
|
+
}
|
|
40
|
+
if (!rootPath || typeof rootPath !== 'string') {
|
|
41
|
+
return { success: false, error: '缺少必填参数: --rootPath (组件生成路径)' };
|
|
42
|
+
}
|
|
43
|
+
if (!VALID_TEMPLATE_TYPES.includes(templateType)) {
|
|
44
|
+
return { success: false, error: `无效的 templateType: "${templateType}",可选值: ${VALID_TEMPLATE_TYPES.join(', ')}` };
|
|
45
|
+
}
|
|
46
|
+
if (!VALID_COLLISION_STRATEGIES.includes(collisionStrategy)) {
|
|
47
|
+
return { success: false, error: `无效的 collisionStrategy: "${collisionStrategy}",可选值: ${VALID_COLLISION_STRATEGIES.join(', ')}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const camelName = changeCase.camelCase(name);
|
|
51
|
+
const componentName = changeCase.pascalCase(camelName);
|
|
52
|
+
const className = changeCase.kebabCase(camelName);
|
|
53
|
+
const targetFolder = path.join(rootPath, componentName);
|
|
54
|
+
const fullTargetPath = path.join(process.cwd(), targetFolder);
|
|
55
|
+
|
|
56
|
+
if (fs.existsSync(fullTargetPath)) {
|
|
57
|
+
if (collisionStrategy === 'skip') {
|
|
58
|
+
return { success: false, error: `目录已存在: ${targetFolder}(使用 --collisionStrategy=overwrite 覆盖)` };
|
|
59
|
+
}
|
|
60
|
+
if (collisionStrategy === 'overwrite') {
|
|
61
|
+
fs.rmSync(fullTargetPath, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const generatedFiles = [];
|
|
66
|
+
|
|
67
|
+
for (const item of TEMPLATE_CONFIG) {
|
|
68
|
+
const itemResult = item.getData(componentName, className, templateType);
|
|
69
|
+
const templateFile = itemResult.templateFile || item.templateFile;
|
|
70
|
+
const templateData = itemResult.data || itemResult;
|
|
71
|
+
|
|
72
|
+
const destDir = item.dest === ''
|
|
73
|
+
? fullTargetPath
|
|
74
|
+
: path.join(fullTargetPath, item.dest);
|
|
75
|
+
const destFile = path.join(destDir, item.file);
|
|
76
|
+
|
|
77
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
78
|
+
|
|
79
|
+
const content = renderTemplate(templateFile, templateData);
|
|
80
|
+
fs.writeFileSync(destFile, content, 'utf-8');
|
|
81
|
+
generatedFiles.push(path.join(targetFolder, item.dest, item.file).replace(/\\/g, '/'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { success: true, files: generatedFiles, componentName, targetFolder };
|
|
85
|
+
}
|