@ljjjjjjj/my-cli 1.0.0 → 1.1.1
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/README.md +198 -0
- package/bin/index.js +338 -37
- package/package.json +2 -2
- package/templates/generator/component/react/component.css.tpl +70 -0
- package/templates/generator/component/react/component.jsx.tpl +19 -0
- package/templates/generator/component/vue/component.scss.tpl +73 -0
- package/templates/generator/component/vue/component.vue.tpl +40 -0
- package/templates/generator/page/react/api.ts.tpl +3 -0
- package/templates/generator/page/react/hooks.ts.tpl +3 -0
- package/templates/generator/page/react/page.css.tpl +3 -0
- package/templates/generator/page/react/page.jsx.tpl +5 -0
- package/templates/generator/page/react/store.ts.tpl +3 -0
- package/templates/generator/page/react/types.ts.tpl +3 -0
- package/templates/generator/page/vue/api.ts.tpl +3 -0
- package/templates/generator/page/vue/hooks.ts.tpl +3 -0
- package/templates/generator/page/vue/page.scss.tpl +3 -0
- package/templates/generator/page/vue/page.vue.tpl +14 -0
- package/templates/generator/page/vue/store.ts.tpl +3 -0
- package/templates/generator/page/vue/types.ts.tpl +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# my-cli
|
|
2
|
+
|
|
3
|
+
一个基于 Node.js 的前端脚手架工具,帮助你快速生成 Vite 项目模板,并在已有项目中生成组件和页面骨架。
|
|
4
|
+
|
|
5
|
+
## 功能特点
|
|
6
|
+
|
|
7
|
+
- 支持创建 Vue3 + Vite 或 React + Vite 项目模板
|
|
8
|
+
- 支持在当前项目中生成组件代码
|
|
9
|
+
- 支持在当前项目中生成页面代码
|
|
10
|
+
- 会自动识别项目是 Vue 还是 React
|
|
11
|
+
- 支持通过配置文件自定义生成目录
|
|
12
|
+
- 支持使用 --force 覆盖已存在文件
|
|
13
|
+
|
|
14
|
+
## 支持的模板
|
|
15
|
+
|
|
16
|
+
- Vue3 + Vite
|
|
17
|
+
- React + Vite
|
|
18
|
+
|
|
19
|
+
## 安装方式
|
|
20
|
+
|
|
21
|
+
### 方式一:作为公开包使用(推荐给他人)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @ljjjjjjj/my-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
安装后可以直接使用:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
my-cli --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 方式二:本地开发调试使用
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npm link
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
然后就可以在当前环境中直接执行:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
my-cli create your-project-name
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 命令说明
|
|
47
|
+
|
|
48
|
+
### 0. 查看帮助
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
my-cli help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
或:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
my-cli --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
会看到当前支持的命令:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
my-cli create <项目名称>
|
|
64
|
+
my-cli add component <组件名称> [--force]
|
|
65
|
+
my-cli add page <页面名称> [--force]
|
|
66
|
+
my-cli help
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 1. 创建项目
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
my-cli create your-project-name
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
执行后会提示你:
|
|
76
|
+
|
|
77
|
+
- 选择项目模板
|
|
78
|
+
- 输入项目描述
|
|
79
|
+
|
|
80
|
+
创建成功后,会生成一个新的 Vite 项目目录。
|
|
81
|
+
|
|
82
|
+
### 2. 生成组件
|
|
83
|
+
|
|
84
|
+
在当前项目中执行:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
my-cli add component Button
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
脚手架会先从当前目录向上查找 package.json,定位项目根目录;随后根据项目类型自动选择对应模板。
|
|
91
|
+
|
|
92
|
+
默认生成位置:
|
|
93
|
+
|
|
94
|
+
- Vue 项目:src/components/Button/
|
|
95
|
+
- React 项目:src/components/Button/
|
|
96
|
+
|
|
97
|
+
默认生成内容:
|
|
98
|
+
|
|
99
|
+
Vue 项目:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
src/components/Button/
|
|
103
|
+
├── Button.vue
|
|
104
|
+
├── Button.module.scss
|
|
105
|
+
├── index.ts
|
|
106
|
+
├── README.md
|
|
107
|
+
└── Button.test.ts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
React 项目:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
src/components/Button/
|
|
114
|
+
├── Button.jsx
|
|
115
|
+
├── Button.module.css
|
|
116
|
+
├── index.js
|
|
117
|
+
├── README.md
|
|
118
|
+
└── Button.test.jsx
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. 生成页面
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
my-cli add page User
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
默认生成位置:
|
|
128
|
+
|
|
129
|
+
- Vue 项目:src/pages/User/
|
|
130
|
+
- React 项目:src/pages/User/
|
|
131
|
+
|
|
132
|
+
默认生成内容:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
src/pages/User/
|
|
136
|
+
├── index.vue
|
|
137
|
+
├── style.scss
|
|
138
|
+
├── api.ts
|
|
139
|
+
├── hooks.ts
|
|
140
|
+
├── store.ts
|
|
141
|
+
└── types.ts
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
React 项目则会生成对应的 jsx 和 css 文件。
|
|
145
|
+
|
|
146
|
+
### 4. 覆盖已存在文件
|
|
147
|
+
|
|
148
|
+
如果目标文件已存在,默认会询问是否覆盖;如果你想直接覆盖,可以使用:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
my-cli add component Button --force
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
或简写:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
my-cli add page User -f
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 配置说明
|
|
161
|
+
|
|
162
|
+
脚手架会先从当前目录向上查找 package.json,定位项目根目录。
|
|
163
|
+
|
|
164
|
+
你可以在项目根目录创建 .my-cli.json 来指定生成目录和框架:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"framework": "vue",
|
|
169
|
+
"componentsDir": "src/components",
|
|
170
|
+
"pagesDir": "src/pages"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
也可以在 package.json 中配置:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"myCli": {
|
|
179
|
+
"framework": "react",
|
|
180
|
+
"componentsDir": "src/components",
|
|
181
|
+
"pagesDir": "src/pages"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
如果没有配置,脚手架会自动根据 package.json 或项目文件结构进行检测。
|
|
187
|
+
|
|
188
|
+
## 适用场景
|
|
189
|
+
|
|
190
|
+
这个脚手架适合用于:
|
|
191
|
+
|
|
192
|
+
- 快速初始化前端项目
|
|
193
|
+
- 在已有项目中快速生成组件和页面
|
|
194
|
+
- 作为学习和练习 CLI 工具的示例项目
|
|
195
|
+
|
|
196
|
+
## 说明
|
|
197
|
+
|
|
198
|
+
如果你希望把这个脚手架给他人使用,请直接使用发布到 npm 的包名进行安装。
|
package/bin/index.js
CHANGED
|
@@ -5,64 +5,365 @@ import fs from 'fs-extra';
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import { program } from 'commander'; // ← 关键新增
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
11
12
|
|
|
13
|
+
// ---------- 读取 package.json ----------
|
|
14
|
+
const pkg = await fs.readJson(path.join(__dirname, '../package.json'));
|
|
15
|
+
|
|
16
|
+
// ---------- 模板映射 ----------
|
|
12
17
|
const TEMPLATE_MAP = {
|
|
13
18
|
'Vue3 + Vite': path.resolve(__dirname, '../templates/vue3-vite'),
|
|
14
19
|
'React + Vite': path.resolve(__dirname, '../templates/react-vite')
|
|
15
20
|
};
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// ---------- 工具函数 ----------
|
|
23
|
+
function toPascalCase(value) {
|
|
24
|
+
return value
|
|
25
|
+
.replace(/[_\-\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
|
|
26
|
+
.replace(/^(.)/, (char) => char.toUpperCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderTemplate(templatePath, replacements) {
|
|
30
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
31
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => replacements[key] ?? '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------- 查找项目根目录 ----------
|
|
35
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
36
|
+
let current = startDir;
|
|
37
|
+
while (current !== path.parse(current).root) {
|
|
38
|
+
const pkgPath = path.join(current, 'package.json');
|
|
39
|
+
if (fs.existsSync(pkgPath)) {
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
current = path.dirname(current);
|
|
43
|
+
}
|
|
44
|
+
return startDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------- 加载项目配置 ----------
|
|
48
|
+
function loadProjectConfig(projectRoot) {
|
|
49
|
+
const config = {
|
|
50
|
+
framework: null,
|
|
51
|
+
componentsDir: 'src/components',
|
|
52
|
+
pagesDir: 'src/pages'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const cliConfigPath = path.join(projectRoot, '.my-cli.json');
|
|
56
|
+
if (fs.existsSync(cliConfigPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const userConfig = fs.readJsonSync(cliConfigPath);
|
|
59
|
+
Object.assign(config, userConfig);
|
|
60
|
+
} catch {
|
|
61
|
+
console.warn('⚠️ 解析 .my-cli.json 失败,使用默认配置');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
66
|
+
if (fs.existsSync(pkgPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
69
|
+
if (pkg.myCli) {
|
|
70
|
+
Object.assign(config, pkg.myCli);
|
|
71
|
+
}
|
|
72
|
+
} catch { }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
config.componentsDir = config.componentsDir.replace(/^\.?\//, '');
|
|
76
|
+
config.pagesDir = config.pagesDir.replace(/^\.?\//, '');
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- 检测项目类型 ----------
|
|
81
|
+
function detectProjectType(projectRoot, config) {
|
|
82
|
+
if (config.framework) {
|
|
83
|
+
if (config.framework === 'react' || config.framework === 'vue') {
|
|
84
|
+
return config.framework;
|
|
85
|
+
}
|
|
86
|
+
console.warn(`⚠️ 未知框架 "${config.framework}",将自动检测`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
90
|
+
if (fs.existsSync(pkgPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
93
|
+
const deps = {
|
|
94
|
+
...(pkg.dependencies || {}),
|
|
95
|
+
...(pkg.devDependencies || {})
|
|
96
|
+
};
|
|
97
|
+
if (deps.react || deps['@vitejs/plugin-react'] || deps.preact) return 'react';
|
|
98
|
+
if (deps.vue || deps['@vitejs/plugin-vue']) return 'vue';
|
|
99
|
+
} catch { }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const srcPath = path.join(projectRoot, 'src');
|
|
103
|
+
if (fs.existsSync(srcPath)) {
|
|
104
|
+
const files = fs.readdirSync(srcPath);
|
|
105
|
+
if (files.some(f => f.endsWith('.jsx') || f.endsWith('.tsx'))) return 'react';
|
|
106
|
+
if (files.some(f => f.endsWith('.vue'))) return 'vue';
|
|
107
|
+
}
|
|
108
|
+
return 'vue';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------- 获取脚手架配置 ----------
|
|
112
|
+
function getScaffoldConfig(projectRoot, projectType) {
|
|
113
|
+
const templateRoot = path.resolve(__dirname, '../templates/generator');
|
|
114
|
+
if (projectType === 'react') {
|
|
115
|
+
return {
|
|
116
|
+
projectType,
|
|
117
|
+
componentExt: 'jsx',
|
|
118
|
+
styleExt: 'css',
|
|
119
|
+
entryExt: 'js',
|
|
120
|
+
componentTemplatePath: path.join(templateRoot, 'component/react/component.jsx.tpl'),
|
|
121
|
+
styleTemplatePath: path.join(templateRoot, 'component/react/component.css.tpl'),
|
|
122
|
+
pageTemplatePath: path.join(templateRoot, 'page/react/page.jsx.tpl'),
|
|
123
|
+
pageStyleTemplatePath: path.join(templateRoot, 'page/react/page.css.tpl'),
|
|
124
|
+
pageFiles: [
|
|
125
|
+
{ name: 'api.ts', templatePath: path.join(templateRoot, 'page/react/api.ts.tpl') },
|
|
126
|
+
{ name: 'hooks.ts', templatePath: path.join(templateRoot, 'page/react/hooks.ts.tpl') },
|
|
127
|
+
{ name: 'store.ts', templatePath: path.join(templateRoot, 'page/react/store.ts.tpl') },
|
|
128
|
+
{ name: 'types.ts', templatePath: path.join(templateRoot, 'page/react/types.ts.tpl') }
|
|
129
|
+
],
|
|
130
|
+
testTemplate: (componentName) =>
|
|
131
|
+
`import { describe, it, expect } from 'vitest';\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// vue
|
|
135
|
+
return {
|
|
136
|
+
projectType: 'vue',
|
|
137
|
+
componentExt: 'vue',
|
|
138
|
+
styleExt: 'scss',
|
|
139
|
+
entryExt: 'ts',
|
|
140
|
+
componentTemplatePath: path.join(templateRoot, 'component/vue/component.vue.tpl'),
|
|
141
|
+
styleTemplatePath: path.join(templateRoot, 'component/vue/component.scss.tpl'),
|
|
142
|
+
pageTemplatePath: path.join(templateRoot, 'page/vue/page.vue.tpl'),
|
|
143
|
+
pageStyleTemplatePath: path.join(templateRoot, 'page/vue/page.scss.tpl'),
|
|
144
|
+
pageFiles: [
|
|
145
|
+
{ name: 'api.ts', templatePath: path.join(templateRoot, 'page/vue/api.ts.tpl') },
|
|
146
|
+
{ name: 'hooks.ts', templatePath: path.join(templateRoot, 'page/vue/hooks.ts.tpl') },
|
|
147
|
+
{ name: 'store.ts', templatePath: path.join(templateRoot, 'page/vue/store.ts.tpl') },
|
|
148
|
+
{ name: 'types.ts', templatePath: path.join(templateRoot, 'page/vue/types.ts.tpl') }
|
|
149
|
+
],
|
|
150
|
+
testTemplate: (componentName) =>
|
|
151
|
+
`import { describe, it, expect } from 'vitest';\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
20
154
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
155
|
+
// ---------- 安全写入文件 ----------
|
|
156
|
+
async function safeWriteFile(filePath, content, force = false) {
|
|
157
|
+
if (fs.existsSync(filePath) && !force) {
|
|
158
|
+
const { overwrite } = await inquirer.prompt([
|
|
159
|
+
{
|
|
160
|
+
type: 'confirm',
|
|
161
|
+
name: 'overwrite',
|
|
162
|
+
message: `文件 ${path.basename(filePath)} 已存在,是否覆盖?`,
|
|
163
|
+
default: false
|
|
164
|
+
}
|
|
165
|
+
]);
|
|
166
|
+
if (!overwrite) return false;
|
|
167
|
+
}
|
|
168
|
+
await fs.outputFile(filePath, content);
|
|
169
|
+
return true;
|
|
24
170
|
}
|
|
25
171
|
|
|
26
|
-
|
|
27
|
-
|
|
172
|
+
// ---------- 生成组件 ----------
|
|
173
|
+
async function generateComponentFiles(name, projectRoot, config, force) {
|
|
174
|
+
const componentName = toPascalCase(name);
|
|
175
|
+
const baseDir = path.join(projectRoot, config.componentsDir, componentName);
|
|
176
|
+
await fs.ensureDir(baseDir);
|
|
28
177
|
|
|
29
|
-
const
|
|
178
|
+
const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
|
|
179
|
+
const replacements = {
|
|
180
|
+
componentName,
|
|
181
|
+
componentNameLower: componentName.toLowerCase()
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const filesToGenerate = [
|
|
185
|
+
{
|
|
186
|
+
path: path.join(baseDir, `${componentName}.${scaffoldConfig.componentExt}`),
|
|
187
|
+
content: renderTemplate(scaffoldConfig.componentTemplatePath, replacements)
|
|
188
|
+
},
|
|
30
189
|
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
message: '请选择项目模板:',
|
|
34
|
-
choices: Object.keys(TEMPLATE_MAP)
|
|
190
|
+
path: path.join(baseDir, `${componentName}.module.${scaffoldConfig.styleExt}`),
|
|
191
|
+
content: renderTemplate(scaffoldConfig.styleTemplatePath, replacements)
|
|
35
192
|
},
|
|
36
193
|
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
194
|
+
path: path.join(baseDir, `index.${scaffoldConfig.entryExt}`),
|
|
195
|
+
content: `export { default } from './${componentName}.${scaffoldConfig.componentExt}';\n`
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
path: path.join(baseDir, 'README.md'),
|
|
199
|
+
content: `# ${componentName}\n\n${componentName} 组件说明。\n`
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
path: path.join(baseDir, `${componentName}.${scaffoldConfig.projectType === 'react' ? 'test.jsx' : 'test.ts'}`),
|
|
203
|
+
content: scaffoldConfig.testTemplate(componentName)
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
let overwrittenCount = 0, skippedCount = 0;
|
|
208
|
+
for (const file of filesToGenerate) {
|
|
209
|
+
const result = await safeWriteFile(file.path, file.content, force);
|
|
210
|
+
result ? overwrittenCount++ : skippedCount++;
|
|
211
|
+
}
|
|
212
|
+
return { overwrittenCount, skippedCount, baseDir };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------- 生成页面 ----------
|
|
216
|
+
async function generatePageFiles(name, projectRoot, config, force) {
|
|
217
|
+
const pageName = toPascalCase(name);
|
|
218
|
+
const baseDir = path.join(projectRoot, config.pagesDir, pageName);
|
|
219
|
+
await fs.ensureDir(baseDir);
|
|
220
|
+
|
|
221
|
+
const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
|
|
222
|
+
const replacements = {
|
|
223
|
+
pageName,
|
|
224
|
+
pageNameLower: pageName.toLowerCase()
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const filesToGenerate = [
|
|
228
|
+
{
|
|
229
|
+
path: path.join(baseDir, `index.${scaffoldConfig.componentExt}`),
|
|
230
|
+
content: renderTemplate(scaffoldConfig.pageTemplatePath, replacements)
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
path: path.join(baseDir, `style.${scaffoldConfig.styleExt}`),
|
|
234
|
+
content: renderTemplate(scaffoldConfig.pageStyleTemplatePath, replacements)
|
|
41
235
|
}
|
|
42
|
-
]
|
|
236
|
+
];
|
|
237
|
+
for (const pageFile of scaffoldConfig.pageFiles) {
|
|
238
|
+
filesToGenerate.push({
|
|
239
|
+
path: path.join(baseDir, pageFile.name),
|
|
240
|
+
content: renderTemplate(pageFile.templatePath, replacements)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
43
243
|
|
|
44
|
-
|
|
45
|
-
const
|
|
244
|
+
let overwrittenCount = 0, skippedCount = 0;
|
|
245
|
+
for (const file of filesToGenerate) {
|
|
246
|
+
const result = await safeWriteFile(file.path, file.content, force);
|
|
247
|
+
result ? overwrittenCount++ : skippedCount++;
|
|
248
|
+
}
|
|
249
|
+
return { overwrittenCount, skippedCount, baseDir };
|
|
250
|
+
}
|
|
46
251
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
252
|
+
// ---------- Commander 配置 ----------
|
|
253
|
+
program
|
|
254
|
+
.version(pkg.version, '-V, --version', '显示版本号')
|
|
255
|
+
.description('一个快速生成 Vue3/React + Vite 项目的脚手架工具');
|
|
50
256
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
257
|
+
// ---------- create 命令 ----------
|
|
258
|
+
program
|
|
259
|
+
.command('create <project-name>')
|
|
260
|
+
.description('创建一个新项目')
|
|
261
|
+
.option('-t, --template <name>', '指定模板名称(Vue3 + Vite / React + Vite)')
|
|
262
|
+
.option('-d, --description <desc>', '项目描述')
|
|
263
|
+
.action(async (projectName, options) => {
|
|
264
|
+
console.log(`✨ 正在创建项目: ${projectName}`);
|
|
55
265
|
|
|
56
|
-
|
|
57
|
-
|
|
266
|
+
let templateKey = options.template;
|
|
267
|
+
let description = options.description;
|
|
268
|
+
|
|
269
|
+
if (!templateKey) {
|
|
270
|
+
const answer = await inquirer.prompt([
|
|
271
|
+
{
|
|
272
|
+
type: 'select',
|
|
273
|
+
name: 'templateKey',
|
|
274
|
+
message: '请选择项目模板:',
|
|
275
|
+
choices: Object.keys(TEMPLATE_MAP)
|
|
276
|
+
}
|
|
277
|
+
]);
|
|
278
|
+
templateKey = answer.templateKey;
|
|
279
|
+
} else if (!Object.keys(TEMPLATE_MAP).includes(templateKey)) {
|
|
280
|
+
console.error(`❌ 未知模板: ${templateKey}`);
|
|
281
|
+
console.log(`可用模板: ${Object.keys(TEMPLATE_MAP).join(', ')}`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!description) {
|
|
286
|
+
const answer = await inquirer.prompt([
|
|
287
|
+
{
|
|
288
|
+
type: 'input',
|
|
289
|
+
name: 'description',
|
|
290
|
+
message: '项目描述:',
|
|
291
|
+
default: 'A new project'
|
|
292
|
+
}
|
|
293
|
+
]);
|
|
294
|
+
description = answer.description;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const targetPath = path.resolve(process.cwd(), projectName);
|
|
298
|
+
const sourceTemplatePath = TEMPLATE_MAP[templateKey];
|
|
299
|
+
|
|
300
|
+
const spinner = ora('正在复制模板文件...').start();
|
|
301
|
+
try {
|
|
302
|
+
await fs.copy(sourceTemplatePath, targetPath);
|
|
303
|
+
const pkgJsonPath = path.join(targetPath, 'package.json');
|
|
304
|
+
const pkg = await fs.readJson(pkgJsonPath);
|
|
305
|
+
pkg.description = description;
|
|
306
|
+
await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 });
|
|
307
|
+
spinner.succeed('✅ 项目创建成功!');
|
|
308
|
+
console.log(`
|
|
58
309
|
接下来执行命令:
|
|
59
|
-
cd ${projectName}
|
|
60
|
-
npm install
|
|
61
|
-
npm run dev
|
|
310
|
+
cd ${projectName}
|
|
311
|
+
npm install
|
|
312
|
+
npm run dev
|
|
62
313
|
`);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
spinner.fail('❌ 创建项目失败');
|
|
316
|
+
console.error(err);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
63
319
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
320
|
+
// ---------- add 命令 ----------
|
|
321
|
+
program
|
|
322
|
+
.command('add <type> <name>')
|
|
323
|
+
.description('添加组件或页面(type: component | page)')
|
|
324
|
+
.option('-f, --force', '强制覆盖已存在的文件')
|
|
325
|
+
.action(async (type, name, options) => {
|
|
326
|
+
if (!['component', 'page'].includes(type)) {
|
|
327
|
+
console.error('❌ 类型必须是 component 或 page');
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
332
|
+
const projectConfig = loadProjectConfig(projectRoot);
|
|
333
|
+
const framework = projectConfig.framework || detectProjectType(projectRoot, projectConfig);
|
|
334
|
+
projectConfig.framework = framework;
|
|
335
|
+
|
|
336
|
+
const spinner = ora('正在生成业务代码...').start();
|
|
337
|
+
try {
|
|
338
|
+
let result;
|
|
339
|
+
if (type === 'component') {
|
|
340
|
+
result = await generateComponentFiles(name, projectRoot, projectConfig, options.force);
|
|
341
|
+
spinner.succeed(
|
|
342
|
+
`✅ 已生成组件文件: ${toPascalCase(name)}\n` +
|
|
343
|
+
` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
|
|
344
|
+
` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
result = await generatePageFiles(name, projectRoot, projectConfig, options.force);
|
|
348
|
+
spinner.succeed(
|
|
349
|
+
`✅ 已生成页面文件: ${toPascalCase(name)}\n` +
|
|
350
|
+
` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
|
|
351
|
+
` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
spinner.fail('❌ 生成业务代码失败');
|
|
356
|
+
console.error(err);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
program.on('command:*', () => {
|
|
361
|
+
console.error('❌ 未知命令: %s', program.args.join(' '));
|
|
362
|
+
program.help();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
program.parse(process.argv);
|
|
366
|
+
|
|
367
|
+
if (!process.argv.slice(2).length) {
|
|
368
|
+
program.help();
|
|
369
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ljjjjjjj/my-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "vite脚手架,快速生成Vue3/React+vite项目模板",
|
|
5
5
|
"main": "./bin/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "node --test tests/"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"cli",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* 基础样式 */
|
|
2
|
+
.button {
|
|
3
|
+
font-family: inherit;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
border: none;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
transition: all 0.2s ease-in-out;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: 6px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* 尺寸变体 */
|
|
16
|
+
.small {
|
|
17
|
+
padding: 6px 16px;
|
|
18
|
+
font-size: 14px;
|
|
19
|
+
}
|
|
20
|
+
.medium {
|
|
21
|
+
padding: 10px 24px;
|
|
22
|
+
font-size: 16px;
|
|
23
|
+
}
|
|
24
|
+
.large {
|
|
25
|
+
padding: 14px 32px;
|
|
26
|
+
font-size: 18px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* 颜色变体 */
|
|
30
|
+
.primary {
|
|
31
|
+
background-color: #646cff;
|
|
32
|
+
color: #fff;
|
|
33
|
+
}
|
|
34
|
+
.primary:hover:not(:disabled) {
|
|
35
|
+
background-color: #535bf2;
|
|
36
|
+
transform: translateY(-2px);
|
|
37
|
+
box-shadow: 0 4px 12px rgba(100, 108, 255, 0.4);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.secondary {
|
|
41
|
+
background-color: #e9ecef;
|
|
42
|
+
color: #495057;
|
|
43
|
+
}
|
|
44
|
+
.secondary:hover:not(:disabled) {
|
|
45
|
+
background-color: #dee2e6;
|
|
46
|
+
transform: translateY(-2px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.danger {
|
|
50
|
+
background-color: #ff6b6b;
|
|
51
|
+
color: #fff;
|
|
52
|
+
}
|
|
53
|
+
.danger:hover:not(:disabled) {
|
|
54
|
+
background-color: #fa5252;
|
|
55
|
+
transform: translateY(-2px);
|
|
56
|
+
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* 禁用状态 */
|
|
60
|
+
.button:disabled {
|
|
61
|
+
opacity: 0.5;
|
|
62
|
+
cursor: not-allowed;
|
|
63
|
+
transform: none !important;
|
|
64
|
+
box-shadow: none !important;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* 点击反馈 */
|
|
68
|
+
.button:active:not(:disabled) {
|
|
69
|
+
transform: scale(0.96) !important;
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import styles from './{{componentName}}.module.css';
|
|
2
|
+
|
|
3
|
+
export default function {{componentName}}({
|
|
4
|
+
children,
|
|
5
|
+
onClick,
|
|
6
|
+
variant = 'primary',
|
|
7
|
+
disabled = false,
|
|
8
|
+
size = 'medium'
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<button
|
|
12
|
+
className={`${styles.button} ${styles[variant]} ${styles[size]}`}
|
|
13
|
+
onClick={onClick}
|
|
14
|
+
disabled={disabled}
|
|
15
|
+
>
|
|
16
|
+
{children || '{{componentName}}'}
|
|
17
|
+
</button>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// 基础样式
|
|
2
|
+
.button {
|
|
3
|
+
font-family: inherit;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
border: none;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
transition: all 0.2s ease-in-out;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: 6px;
|
|
13
|
+
|
|
14
|
+
// 尺寸变体
|
|
15
|
+
&.small {
|
|
16
|
+
padding: 6px 16px;
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
}
|
|
19
|
+
&.medium {
|
|
20
|
+
padding: 10px 24px;
|
|
21
|
+
font-size: 16px;
|
|
22
|
+
}
|
|
23
|
+
&.large {
|
|
24
|
+
padding: 14px 32px;
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 颜色变体
|
|
29
|
+
&.primary {
|
|
30
|
+
background-color: #646cff;
|
|
31
|
+
color: #fff;
|
|
32
|
+
|
|
33
|
+
&:hover:not(:disabled) {
|
|
34
|
+
background-color: #535bf2;
|
|
35
|
+
transform: translateY(-2px);
|
|
36
|
+
box-shadow: 0 4px 12px rgba(100, 108, 255, 0.4);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&.secondary {
|
|
41
|
+
background-color: #e9ecef;
|
|
42
|
+
color: #495057;
|
|
43
|
+
|
|
44
|
+
&:hover:not(:disabled) {
|
|
45
|
+
background-color: #dee2e6;
|
|
46
|
+
transform: translateY(-2px);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&.danger {
|
|
51
|
+
background-color: #ff6b6b;
|
|
52
|
+
color: #fff;
|
|
53
|
+
|
|
54
|
+
&:hover:not(:disabled) {
|
|
55
|
+
background-color: #fa5252;
|
|
56
|
+
transform: translateY(-2px);
|
|
57
|
+
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 禁用状态
|
|
62
|
+
&:disabled {
|
|
63
|
+
opacity: 0.5;
|
|
64
|
+
cursor: not-allowed;
|
|
65
|
+
transform: none !important;
|
|
66
|
+
box-shadow: none !important;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 点击反馈
|
|
70
|
+
&:active:not(:disabled) {
|
|
71
|
+
transform: scale(0.96) !important;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
:class="[styles.button, styles[variant], styles[size]]"
|
|
4
|
+
:disabled="disabled"
|
|
5
|
+
@click="handleClick"
|
|
6
|
+
>
|
|
7
|
+
<!-- 支持插槽,方便传入复杂内容 -->
|
|
8
|
+
<slot>{{ componentName }}</slot>
|
|
9
|
+
</button>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
// 导入同名的 CSS Modules 样式文件
|
|
14
|
+
import styles from './{{componentName}}.module.scss';
|
|
15
|
+
|
|
16
|
+
// 定义组件属性
|
|
17
|
+
defineProps({
|
|
18
|
+
variant: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'primary',
|
|
21
|
+
validator: (val) => ['primary', 'secondary', 'danger'].includes(val)
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'medium',
|
|
26
|
+
validator: (val) => ['small', 'medium', 'large'].includes(val)
|
|
27
|
+
},
|
|
28
|
+
disabled: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: false
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 定义事件
|
|
35
|
+
const emit = defineEmits(['click']);
|
|
36
|
+
|
|
37
|
+
const handleClick = (event) => {
|
|
38
|
+
emit('click', event);
|
|
39
|
+
};
|
|
40
|
+
</script>
|