@ljjjjjjj/my-cli 1.1.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.
Files changed (3) hide show
  1. package/README.md +198 -3
  2. package/bin/index.js +95 -142
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,3 +1,198 @@
1
- # Button
2
-
3
- Button 组件说明。
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,11 +5,15 @@ 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
 
12
- // ---------- 模板映射(不变) ----------
13
+ // ---------- 读取 package.json ----------
14
+ const pkg = await fs.readJson(path.join(__dirname, '../package.json'));
15
+
16
+ // ---------- 模板映射 ----------
13
17
  const TEMPLATE_MAP = {
14
18
  'Vue3 + Vite': path.resolve(__dirname, '../templates/vue3-vite'),
15
19
  'React + Vite': path.resolve(__dirname, '../templates/react-vite')
@@ -27,14 +31,6 @@ function renderTemplate(templatePath, replacements) {
27
31
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => replacements[key] ?? '');
28
32
  }
29
33
 
30
- function printHelp() {
31
- console.log(`使用方法:
32
- my-cli create <项目名称>
33
- my-cli add component <组件名称> [--force]
34
- my-cli add page <页面名称> [--force]
35
- my-cli help`);
36
- }
37
-
38
34
  // ---------- 查找项目根目录 ----------
39
35
  function findProjectRoot(startDir = process.cwd()) {
40
36
  let current = startDir;
@@ -45,29 +41,27 @@ function findProjectRoot(startDir = process.cwd()) {
45
41
  }
46
42
  current = path.dirname(current);
47
43
  }
48
- return startDir; // 未找到则返回当前目录
44
+ return startDir;
49
45
  }
50
46
 
51
47
  // ---------- 加载项目配置 ----------
52
48
  function loadProjectConfig(projectRoot) {
53
49
  const config = {
54
- framework: null, // 'vue' | 'react' | null
50
+ framework: null,
55
51
  componentsDir: 'src/components',
56
52
  pagesDir: 'src/pages'
57
53
  };
58
54
 
59
- // 1. 读取 .my-cli.json
60
55
  const cliConfigPath = path.join(projectRoot, '.my-cli.json');
61
56
  if (fs.existsSync(cliConfigPath)) {
62
57
  try {
63
58
  const userConfig = fs.readJsonSync(cliConfigPath);
64
59
  Object.assign(config, userConfig);
65
- } catch (err) {
60
+ } catch {
66
61
  console.warn('⚠️ 解析 .my-cli.json 失败,使用默认配置');
67
62
  }
68
63
  }
69
64
 
70
- // 2. 读取 package.json 中的 myCli 字段
71
65
  const pkgPath = path.join(projectRoot, 'package.json');
72
66
  if (fs.existsSync(pkgPath)) {
73
67
  try {
@@ -75,21 +69,16 @@ function loadProjectConfig(projectRoot) {
75
69
  if (pkg.myCli) {
76
70
  Object.assign(config, pkg.myCli);
77
71
  }
78
- } catch (err) {
79
- // ignore
80
- }
72
+ } catch { }
81
73
  }
82
74
 
83
- // 确保路径使用相对路径(相对于项目根)
84
75
  config.componentsDir = config.componentsDir.replace(/^\.?\//, '');
85
76
  config.pagesDir = config.pagesDir.replace(/^\.?\//, '');
86
-
87
77
  return config;
88
78
  }
89
79
 
90
- // ---------- 检测项目类型(增强版) ----------
80
+ // ---------- 检测项目类型 ----------
91
81
  function detectProjectType(projectRoot, config) {
92
- // 优先使用配置文件指定的框架
93
82
  if (config.framework) {
94
83
  if (config.framework === 'react' || config.framework === 'vue') {
95
84
  return config.framework;
@@ -97,7 +86,6 @@ function detectProjectType(projectRoot, config) {
97
86
  console.warn(`⚠️ 未知框架 "${config.framework}",将自动检测`);
98
87
  }
99
88
 
100
- // 自动检测
101
89
  const pkgPath = path.join(projectRoot, 'package.json');
102
90
  if (fs.existsSync(pkgPath)) {
103
91
  try {
@@ -106,37 +94,23 @@ function detectProjectType(projectRoot, config) {
106
94
  ...(pkg.dependencies || {}),
107
95
  ...(pkg.devDependencies || {})
108
96
  };
109
- if (deps.react || deps['@vitejs/plugin-react'] || deps.preact) {
110
- return 'react';
111
- }
112
- if (deps.vue || deps['@vitejs/plugin-vue']) {
113
- return 'vue';
114
- }
115
- } catch (err) {
116
- // ignore
117
- }
97
+ if (deps.react || deps['@vitejs/plugin-react'] || deps.preact) return 'react';
98
+ if (deps.vue || deps['@vitejs/plugin-vue']) return 'vue';
99
+ } catch { }
118
100
  }
119
101
 
120
- // 通过文件存在性检测
121
102
  const srcPath = path.join(projectRoot, 'src');
122
103
  if (fs.existsSync(srcPath)) {
123
104
  const files = fs.readdirSync(srcPath);
124
- if (files.some(f => f.endsWith('.jsx') || f.endsWith('.tsx'))) {
125
- return 'react';
126
- }
127
- if (files.some(f => f.endsWith('.vue'))) {
128
- return 'vue';
129
- }
105
+ if (files.some(f => f.endsWith('.jsx') || f.endsWith('.tsx'))) return 'react';
106
+ if (files.some(f => f.endsWith('.vue'))) return 'vue';
130
107
  }
131
-
132
- // 默认返回 vue(兼容旧逻辑)
133
108
  return 'vue';
134
109
  }
135
110
 
136
111
  // ---------- 获取脚手架配置 ----------
137
112
  function getScaffoldConfig(projectRoot, projectType) {
138
113
  const templateRoot = path.resolve(__dirname, '../templates/generator');
139
-
140
114
  if (projectType === 'react') {
141
115
  return {
142
116
  projectType,
@@ -154,10 +128,9 @@ function getScaffoldConfig(projectRoot, projectType) {
154
128
  { name: 'types.ts', templatePath: path.join(templateRoot, 'page/react/types.ts.tpl') }
155
129
  ],
156
130
  testTemplate: (componentName) =>
157
- `import { describe, it, expect } from 'vitest';\n\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
131
+ `import { describe, it, expect } from 'vitest';\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
158
132
  };
159
133
  }
160
-
161
134
  // vue
162
135
  return {
163
136
  projectType: 'vue',
@@ -175,11 +148,11 @@ function getScaffoldConfig(projectRoot, projectType) {
175
148
  { name: 'types.ts', templatePath: path.join(templateRoot, 'page/vue/types.ts.tpl') }
176
149
  ],
177
150
  testTemplate: (componentName) =>
178
- `import { describe, it, expect } from 'vitest';\n\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
151
+ `import { describe, it, expect } from 'vitest';\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
179
152
  };
180
153
  }
181
154
 
182
- // ---------- 辅助:安全写入文件(带覆盖确认) ----------
155
+ // ---------- 安全写入文件 ----------
183
156
  async function safeWriteFile(filePath, content, force = false) {
184
157
  if (fs.existsSync(filePath) && !force) {
185
158
  const { overwrite } = await inquirer.prompt([
@@ -190,12 +163,10 @@ async function safeWriteFile(filePath, content, force = false) {
190
163
  default: false
191
164
  }
192
165
  ]);
193
- if (!overwrite) {
194
- return false; // 跳过
195
- }
166
+ if (!overwrite) return false;
196
167
  }
197
168
  await fs.outputFile(filePath, content);
198
- return true; // 写入成功
169
+ return true;
199
170
  }
200
171
 
201
172
  // ---------- 生成组件 ----------
@@ -205,7 +176,6 @@ async function generateComponentFiles(name, projectRoot, config, force) {
205
176
  await fs.ensureDir(baseDir);
206
177
 
207
178
  const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
208
-
209
179
  const replacements = {
210
180
  componentName,
211
181
  componentNameLower: componentName.toLowerCase()
@@ -234,18 +204,11 @@ async function generateComponentFiles(name, projectRoot, config, force) {
234
204
  }
235
205
  ];
236
206
 
237
- let overwrittenCount = 0;
238
- let skippedCount = 0;
239
-
207
+ let overwrittenCount = 0, skippedCount = 0;
240
208
  for (const file of filesToGenerate) {
241
209
  const result = await safeWriteFile(file.path, file.content, force);
242
- if (result) {
243
- overwrittenCount++;
244
- } else {
245
- skippedCount++;
246
- }
210
+ result ? overwrittenCount++ : skippedCount++;
247
211
  }
248
-
249
212
  return { overwrittenCount, skippedCount, baseDir };
250
213
  }
251
214
 
@@ -256,7 +219,6 @@ async function generatePageFiles(name, projectRoot, config, force) {
256
219
  await fs.ensureDir(baseDir);
257
220
 
258
221
  const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
259
-
260
222
  const replacements = {
261
223
  pageName,
262
224
  pageNameLower: pageName.toLowerCase()
@@ -272,8 +234,6 @@ async function generatePageFiles(name, projectRoot, config, force) {
272
234
  content: renderTemplate(scaffoldConfig.pageStyleTemplatePath, replacements)
273
235
  }
274
236
  ];
275
-
276
- // 添加额外的页面文件(api.ts, hooks.ts 等)
277
237
  for (const pageFile of scaffoldConfig.pageFiles) {
278
238
  filesToGenerate.push({
279
239
  path: path.join(baseDir, pageFile.name),
@@ -281,123 +241,110 @@ async function generatePageFiles(name, projectRoot, config, force) {
281
241
  });
282
242
  }
283
243
 
284
- let overwrittenCount = 0;
285
- let skippedCount = 0;
286
-
244
+ let overwrittenCount = 0, skippedCount = 0;
287
245
  for (const file of filesToGenerate) {
288
246
  const result = await safeWriteFile(file.path, file.content, force);
289
- if (result) {
290
- overwrittenCount++;
291
- } else {
292
- skippedCount++;
293
- }
247
+ result ? overwrittenCount++ : skippedCount++;
294
248
  }
295
-
296
249
  return { overwrittenCount, skippedCount, baseDir };
297
250
  }
298
251
 
299
- // ---------- 主函数 ----------
300
- (async function run() {
301
- const args = process.argv.slice(2);
302
- let command = args[0];
303
- let rest = args.slice(1);
304
-
305
- // 解析 --force / -f
306
- let force = false;
307
- const filteredArgs = [];
308
- for (const arg of rest) {
309
- if (arg === '--force' || arg === '-f') {
310
- force = true;
311
- } else {
312
- filteredArgs.push(arg);
313
- }
314
- }
315
- rest = filteredArgs;
316
-
317
- if (!command || command === 'help' || command === '--help' || command === '-h') {
318
- printHelp();
319
- process.exit(0);
320
- }
252
+ // ---------- Commander 配置 ----------
253
+ program
254
+ .version(pkg.version, '-V, --version', '显示版本号')
255
+ .description('一个快速生成 Vue3/React + Vite 项目的脚手架工具');
256
+
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}`);
321
265
 
322
- if (command === 'create') {
323
- const projectName = rest[0];
324
- if (!projectName) {
325
- printHelp();
326
- process.exit(0);
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);
327
283
  }
328
284
 
329
- console.log(`✨ 正在创建项目: ${projectName}`);
330
-
331
- const answers = await inquirer.prompt([
332
- {
333
- type: 'select',
334
- name: 'templateKey',
335
- message: '请选择项目模板:',
336
- choices: Object.keys(TEMPLATE_MAP)
337
- },
338
- {
339
- type: 'input',
340
- name: 'description',
341
- message: '项目描述:',
342
- default: 'A new project'
343
- }
344
- ]);
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
+ }
345
296
 
346
297
  const targetPath = path.resolve(process.cwd(), projectName);
347
- const sourceTemplatePath = TEMPLATE_MAP[answers.templateKey];
298
+ const sourceTemplatePath = TEMPLATE_MAP[templateKey];
348
299
 
349
300
  const spinner = ora('正在复制模板文件...').start();
350
301
  try {
351
302
  await fs.copy(sourceTemplatePath, targetPath);
352
-
353
303
  const pkgJsonPath = path.join(targetPath, 'package.json');
354
304
  const pkg = await fs.readJson(pkgJsonPath);
355
- pkg.description = answers.description;
305
+ pkg.description = description;
356
306
  await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 });
357
-
358
307
  spinner.succeed('✅ 项目创建成功!');
359
308
  console.log(`
360
309
  接下来执行命令:
361
- cd ${projectName}
362
- npm install
363
- npm run dev
310
+ cd ${projectName}
311
+ npm install
312
+ npm run dev
364
313
  `);
365
314
  } catch (err) {
366
315
  spinner.fail('❌ 创建项目失败');
367
316
  console.error(err);
368
317
  }
369
- return;
370
- }
371
-
372
- if (command === 'add') {
373
- const type = rest[0];
374
- const name = rest[1];
375
-
376
- if (!type || !name || !['component', 'page'].includes(type)) {
377
- printHelp();
378
- process.exit(0);
318
+ });
319
+
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);
379
329
  }
380
330
 
381
- // 定位项目根目录
382
331
  const projectRoot = findProjectRoot(process.cwd());
383
332
  const projectConfig = loadProjectConfig(projectRoot);
384
-
385
- // 确定框架类型(用于模板选择)
386
333
  const framework = projectConfig.framework || detectProjectType(projectRoot, projectConfig);
387
- projectConfig.framework = framework; // 确保后续使用
334
+ projectConfig.framework = framework;
388
335
 
389
336
  const spinner = ora('正在生成业务代码...').start();
390
337
  try {
391
338
  let result;
392
339
  if (type === 'component') {
393
- result = await generateComponentFiles(name, projectRoot, projectConfig, force);
340
+ result = await generateComponentFiles(name, projectRoot, projectConfig, options.force);
394
341
  spinner.succeed(
395
342
  `✅ 已生成组件文件: ${toPascalCase(name)}\n` +
396
343
  ` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
397
344
  ` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
398
345
  );
399
346
  } else {
400
- result = await generatePageFiles(name, projectRoot, projectConfig, force);
347
+ result = await generatePageFiles(name, projectRoot, projectConfig, options.force);
401
348
  spinner.succeed(
402
349
  `✅ 已生成页面文件: ${toPascalCase(name)}\n` +
403
350
  ` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
@@ -408,9 +355,15 @@ npm run dev
408
355
  spinner.fail('❌ 生成业务代码失败');
409
356
  console.error(err);
410
357
  }
411
- return;
412
- }
358
+ });
359
+
360
+ program.on('command:*', () => {
361
+ console.error('❌ 未知命令: %s', program.args.join(' '));
362
+ program.help();
363
+ });
364
+
365
+ program.parse(process.argv);
413
366
 
414
- printHelp();
415
- process.exit(0);
416
- })();
367
+ if (!process.argv.slice(2).length) {
368
+ program.help();
369
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ljjjjjjj/my-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "vite脚手架,快速生成Vue3/React+vite项目模板",
5
5
  "main": "./bin/index.js",
6
6
  "scripts": {