create-koa-boot 1.0.3 → 1.0.4
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 +25 -0
- package/index.js +103 -36
- package/interactive.js +112 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,9 +6,34 @@
|
|
|
6
6
|
|
|
7
7
|
## 快速开始
|
|
8
8
|
|
|
9
|
+
### 交互式(推荐,create-vue 风格)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx create-koa-boot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
按提示依次选择即可:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
? Project name (my-app): my-app
|
|
19
|
+
? Include Vue 3 + Element Plus frontend? (Y/n): Yes
|
|
20
|
+
? Include uniapp mobile app? (y/N): No
|
|
21
|
+
? Auto install dependencies? (y/N): No
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 后端(Koa 3)**始终生成**,前端 / uniapp 按需勾选;
|
|
25
|
+
- 选 `Auto install dependencies` 会自动安装依赖(自动检测 pnpm / npm)。
|
|
26
|
+
|
|
27
|
+
### 非交互(脚本 / CI)
|
|
28
|
+
|
|
9
29
|
```bash
|
|
10
30
|
npx create-koa-boot my-app # 纯后端骨架
|
|
11
31
|
npx create-koa-boot my-app --example # 三端完整示例(backend + frontend + uniapp)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
生成后:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
12
37
|
cd my-app
|
|
13
38
|
npm install
|
|
14
39
|
# 编辑 .env 配置 DATABASE_URL(MySQL),然后:
|
package/index.js
CHANGED
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
* 3. 生成 package.json / tsconfig / tsoa.json / .env.example 等
|
|
11
11
|
*
|
|
12
12
|
* 用法:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* create-koa-boot # 交互式(create-vue 风格提问)
|
|
14
|
+
* create-koa-boot <project-name> # 纯后端骨架
|
|
15
|
+
* create-koa-boot <project-name> --example # 三端完整示例(backend + frontend + uniapp)
|
|
15
16
|
*/
|
|
16
17
|
const fs = require('fs');
|
|
17
18
|
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
const { runInteractive, c } = require('./interactive');
|
|
18
21
|
|
|
19
22
|
const TEMPLATE_DIR = path.join(__dirname, 'templates');
|
|
20
23
|
|
|
21
|
-
//
|
|
22
|
-
const SKIP_BASE = new Set(['node_modules', 'dist', 'build', '.git']);
|
|
24
|
+
// 复制时排除的目录/文件(生成物由用户项目自行产生;example 仅 --example/交互选择时单独复制)
|
|
25
|
+
const SKIP_BASE = new Set(['node_modules', 'dist', 'build', '.git', 'example']);
|
|
23
26
|
// 三端示例额外排除:本机 workaround 产物、构建产物、敏感配置
|
|
24
27
|
const SKIP_EXAMPLE = new Set([
|
|
25
28
|
'node_modules', 'dist', 'build', '.git',
|
|
@@ -111,8 +114,72 @@ function parseArgs(argv) {
|
|
|
111
114
|
};
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
// 自动安装依赖:优先 pnpm,其次 npm(检测本机可用性)
|
|
118
|
+
function runInstall(targetDir) {
|
|
119
|
+
const detect = (cmd) => {
|
|
120
|
+
const r = spawnSync(cmd, ['--version'], { shell: true, stdio: 'ignore' });
|
|
121
|
+
return !r.error && r.status === 0;
|
|
122
|
+
};
|
|
123
|
+
const pm = detect('pnpm') ? 'pnpm' : 'npm';
|
|
124
|
+
console.log(`\n ⚡ 正在使用 ${pm} 安装依赖(可能需要几分钟)...\n`);
|
|
125
|
+
const r = spawnSync(pm, ['install'], { cwd: targetDir, shell: true, stdio: 'inherit' });
|
|
126
|
+
if (r.error || r.status !== 0) {
|
|
127
|
+
console.log(`\n ${c.red('✖')} 自动安装失败,请手动执行:`);
|
|
128
|
+
console.log(` cd ${path.basename(targetDir)}`);
|
|
129
|
+
console.log(` ${pm} install\n`);
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`\n ${c.green('✓')} 依赖安装完成\n`);
|
|
132
|
+
}
|
|
133
|
+
return pm;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 按选择的端打印后续步骤(useSubdir=true 表示三端布局,后端在 backend/ 子目录)
|
|
137
|
+
function printNextSteps(opts) {
|
|
138
|
+
const { projectName, frontend, uniapp, pm, useSubdir } = opts;
|
|
139
|
+
console.log('\n ✅ 项目生成完成!下一步:\n');
|
|
140
|
+
console.log(` cd ${projectName}`);
|
|
141
|
+
console.log(` ${pm} install # 或 npm install,安装全部依赖`);
|
|
142
|
+
if (useSubdir) {
|
|
143
|
+
console.log(' # 后端:');
|
|
144
|
+
console.log(' cd backend && cp .env.example .env # Windows: copy .env.example .env');
|
|
145
|
+
console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
|
|
146
|
+
console.log(' pnpm gen:types && pnpm db:push && pnpm dev');
|
|
147
|
+
console.log(' API 文档: http://localhost:3003/api-docs/business\n');
|
|
148
|
+
} else {
|
|
149
|
+
console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
|
|
150
|
+
console.log(' npx prisma db push # 建表(自动创建 admin/admin123)');
|
|
151
|
+
console.log(' npm run dev # 启动: http://localhost:3003');
|
|
152
|
+
console.log(' API 文档: http://localhost:3003/api-docs/business');
|
|
153
|
+
console.log(' 系统文档: http://localhost:3003/api-docs/system\n');
|
|
154
|
+
}
|
|
155
|
+
if (frontend) {
|
|
156
|
+
console.log(' # 前端(另开终端):');
|
|
157
|
+
console.log(' cd frontend && pnpm dev # http://localhost:5175\n');
|
|
158
|
+
}
|
|
159
|
+
if (uniapp) {
|
|
160
|
+
console.log(' # uniapp:用 HBuilderX 打开 uniapp 目录启动预览\n');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const argv = process.argv.slice(2);
|
|
166
|
+
|
|
167
|
+
// 无参数 → 交互式(create-vue 风格)
|
|
168
|
+
let opts;
|
|
169
|
+
if (argv.length === 0) {
|
|
170
|
+
const ans = await runInteractive();
|
|
171
|
+
opts = {
|
|
172
|
+
projectName: ans.projectName,
|
|
173
|
+
frontend: ans.frontend,
|
|
174
|
+
uniapp: ans.uniapp,
|
|
175
|
+
autoInstall: ans.autoInstall,
|
|
176
|
+
skipInstall: false,
|
|
177
|
+
};
|
|
178
|
+
} else {
|
|
179
|
+
opts = parseArgs(argv);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { projectName, skipInstall, example = false, frontend = false, uniapp = false, autoInstall = false } = opts;
|
|
116
183
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
117
184
|
|
|
118
185
|
if (fs.existsSync(targetDir)) {
|
|
@@ -120,34 +187,30 @@ function main() {
|
|
|
120
187
|
process.exit(1);
|
|
121
188
|
}
|
|
122
189
|
|
|
123
|
-
|
|
124
|
-
|
|
190
|
+
// --example 等价于「后端 + 前端 + uniapp」全选
|
|
191
|
+
const withExample = example || frontend || uniapp;
|
|
192
|
+
|
|
193
|
+
if (withExample) {
|
|
194
|
+
console.log(`\n ⚡ 正在生成 koa-boot 项目: ${projectName}\n`);
|
|
195
|
+
|
|
196
|
+
copyDir(path.join(TEMPLATE_DIR, 'example', 'backend'), path.join(targetDir, 'backend'), SKIP_EXAMPLE);
|
|
197
|
+
console.log(' ✓ backend 模板已复制(koa-boot-runtime ^1.0.0)');
|
|
125
198
|
|
|
126
|
-
|
|
127
|
-
copyDir(path.join(TEMPLATE_DIR, 'example',
|
|
199
|
+
if (frontend || example) {
|
|
200
|
+
copyDir(path.join(TEMPLATE_DIR, 'example', 'frontend'), path.join(targetDir, 'frontend'), SKIP_EXAMPLE);
|
|
201
|
+
console.log(' ✓ frontend 模板已复制(Vue 3 + Element Plus)');
|
|
202
|
+
}
|
|
203
|
+
if (uniapp || example) {
|
|
204
|
+
copyDir(path.join(TEMPLATE_DIR, 'example', 'uniapp'), path.join(targetDir, 'uniapp'), SKIP_EXAMPLE);
|
|
205
|
+
console.log(' ✓ uniapp 模板已复制(含 uni_modules 组件库)');
|
|
128
206
|
}
|
|
129
207
|
if (fs.existsSync(path.join(TEMPLATE_DIR, 'example', 'AGENTS.md'))) {
|
|
130
208
|
fs.copyFileSync(path.join(TEMPLATE_DIR, 'example', 'AGENTS.md'), path.join(targetDir, 'AGENTS.md'));
|
|
131
209
|
console.log(' ✓ AGENTS.md(AI 开发规则)已生成');
|
|
132
210
|
}
|
|
133
|
-
console.log(' ✓ backend 模板已复制(koa-boot-runtime ^1.0.0)');
|
|
134
|
-
console.log(' ✓ frontend 模板已复制');
|
|
135
|
-
console.log(' ✓ uniapp 模板已复制(含 uni_modules 组件库)');
|
|
136
211
|
|
|
137
212
|
patchExampleBackend(targetDir, projectName);
|
|
138
213
|
console.log(' ✓ backend/package.json 已配置');
|
|
139
|
-
|
|
140
|
-
console.log('\n ✅ 三端示例生成完成!下一步:\n');
|
|
141
|
-
console.log(` cd ${projectName}`);
|
|
142
|
-
console.log(' pnpm install # 或 npm install,安装全部依赖');
|
|
143
|
-
console.log(' # 后端:');
|
|
144
|
-
console.log(' cd backend && cp .env.example .env # Windows: copy .env.example .env');
|
|
145
|
-
console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
|
|
146
|
-
console.log(' pnpm gen:types && pnpm db:push && pnpm dev');
|
|
147
|
-
console.log(' API 文档: http://localhost:3003/api-docs/business\n');
|
|
148
|
-
console.log(' # 前端(另开终端):');
|
|
149
|
-
console.log(' cd frontend && pnpm dev\n');
|
|
150
|
-
console.log(' # uniapp:用 HBuilderX 打开 uniapp 目录启动预览\n');
|
|
151
214
|
} else {
|
|
152
215
|
console.log(`\n ⚡ 正在生成 koa-boot 项目: ${projectName}\n`);
|
|
153
216
|
|
|
@@ -165,21 +228,25 @@ function main() {
|
|
|
165
228
|
pkg.name = projectName;
|
|
166
229
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
167
230
|
console.log(' ✓ package.json 已生成');
|
|
168
|
-
|
|
169
|
-
// 4. 提示后续步骤
|
|
170
|
-
console.log('\n ✅ 项目生成完成!下一步:\n');
|
|
171
|
-
console.log(` cd ${projectName}`);
|
|
172
|
-
console.log(' npm install # 或 pnpm install');
|
|
173
|
-
console.log(' 编辑 .env 配置 DATABASE_URL(MySQL)后:');
|
|
174
|
-
console.log(' npx prisma db push # 建表(自动创建 admin/admin123)');
|
|
175
|
-
console.log(' npm run dev # 启动: http://localhost:3003');
|
|
176
|
-
console.log(' API 文档: http://localhost:3003/api-docs/business');
|
|
177
|
-
console.log(' 系统文档: http://localhost:3003/api-docs/system\n');
|
|
178
231
|
}
|
|
179
232
|
|
|
233
|
+
let pm = 'pnpm';
|
|
180
234
|
if (skipInstall) {
|
|
181
235
|
console.log(' (已跳过自动安装,请手动 npm install)\n');
|
|
236
|
+
} else if (autoInstall) {
|
|
237
|
+
pm = runInstall(targetDir);
|
|
182
238
|
}
|
|
239
|
+
|
|
240
|
+
printNextSteps({
|
|
241
|
+
projectName,
|
|
242
|
+
frontend: frontend || example,
|
|
243
|
+
uniapp: uniapp || example,
|
|
244
|
+
pm,
|
|
245
|
+
useSubdir: withExample,
|
|
246
|
+
});
|
|
183
247
|
}
|
|
184
248
|
|
|
185
|
-
main()
|
|
249
|
+
main().catch((err) => {
|
|
250
|
+
console.error(err);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
package/interactive.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* create-koa-boot 交互式提示(create-vue 风格)
|
|
4
|
+
*
|
|
5
|
+
* 零依赖实现:基于 Node 内置 readline。
|
|
6
|
+
* 仅在「无参数」运行时启用;带参数运行走原有非交互逻辑。
|
|
7
|
+
*
|
|
8
|
+
* 实现说明:采用「行队列」模型——所有到达的输入行先入队,
|
|
9
|
+
* 每个问题从队列取一行消费,避免一次性 stdin(管道/脚本)下
|
|
10
|
+
* readline 同步派发行而 await 恢复滞后导致丢行的问题。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
|
|
15
|
+
// 轻量 ANSI 颜色(Windows Terminal / PowerShell 7+ 均支持)
|
|
16
|
+
const c = {
|
|
17
|
+
cyan: (s) => `\x1b[36m${s}\x1b[39m`,
|
|
18
|
+
green: (s) => `\x1b[32m${s}\x1b[39m`,
|
|
19
|
+
yellow: (s) => `\x1b[33m${s}\x1b[39m`,
|
|
20
|
+
red: (s) => `\x1b[31m${s}\x1b[39m`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function createInterface() {
|
|
24
|
+
const rl = readline.createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout,
|
|
27
|
+
});
|
|
28
|
+
const lines = []; // 已到达、待消费的输入行
|
|
29
|
+
const pending = []; // 等待输入的问题回调(参数为行内容,null 表示 EOF)
|
|
30
|
+
let closed = false;
|
|
31
|
+
|
|
32
|
+
rl.on('line', (l) => {
|
|
33
|
+
const cb = pending.shift();
|
|
34
|
+
if (cb) cb(l);
|
|
35
|
+
else lines.push(l);
|
|
36
|
+
});
|
|
37
|
+
rl.on('close', () => {
|
|
38
|
+
closed = true;
|
|
39
|
+
while (pending.length) pending.shift()(null); // EOF:未答问题取默认值
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { rl, lines, pending, closed: () => closed };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 取一行输入:优先消费缓冲行,否则注册等待;EOF/关闭时返回 defaultValue */
|
|
46
|
+
function nextLine(io, promptText, defaultValue) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
if (io.closed()) return resolve(defaultValue);
|
|
49
|
+
if (io.lines.length) {
|
|
50
|
+
process.stdout.write(promptText);
|
|
51
|
+
return resolve(io.lines.shift());
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(promptText);
|
|
54
|
+
io.pending.push((l) => resolve(l === null ? defaultValue : l));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 文本输入:返回用户输入;空输入取默认值;validate 返回错误文案时重新询问 */
|
|
59
|
+
async function askText(io, question, defaultValue, validate) {
|
|
60
|
+
const suffix = defaultValue ? ` ${c.cyan(`(${defaultValue})`)}` : '';
|
|
61
|
+
const prompt = ` ${c.cyan('?')} ${question}${suffix}: `;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const line = await nextLine(io, prompt, defaultValue);
|
|
64
|
+
const val = (line || '').trim() || defaultValue;
|
|
65
|
+
if (validate) {
|
|
66
|
+
const err = validate(val);
|
|
67
|
+
if (err) {
|
|
68
|
+
console.log(` ${c.red('✖')} ${err}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return val;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 确认输入:y/n/回车(默认值);返回 boolean */
|
|
77
|
+
async function askConfirm(io, question, defaultYes) {
|
|
78
|
+
const hint = defaultYes ? '(Y/n)' : '(y/N)';
|
|
79
|
+
const prompt = ` ${c.cyan('?')} ${question} ${c.yellow(hint)}: `;
|
|
80
|
+
for (;;) {
|
|
81
|
+
const line = await nextLine(io, prompt, defaultYes ? 'y' : 'n');
|
|
82
|
+
const a = (line || '').trim().toLowerCase();
|
|
83
|
+
if (a === 'y' || a === 'yes') return true;
|
|
84
|
+
if (a === 'n' || a === 'no') return false;
|
|
85
|
+
if (!a) return defaultYes;
|
|
86
|
+
// 无效输入:重新询问
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 交互式提问主流程,返回:
|
|
92
|
+
* { projectName, frontend, uniapp, autoInstall }
|
|
93
|
+
*/
|
|
94
|
+
async function runInteractive() {
|
|
95
|
+
const io = createInterface();
|
|
96
|
+
try {
|
|
97
|
+
const projectName = await askText(io, 'Project name', 'my-app', (v) => {
|
|
98
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_.]*$/.test(v)) {
|
|
99
|
+
return '仅允许字母、数字、- _ .,且不能以符号开头';
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
});
|
|
103
|
+
const frontend = await askConfirm(io, 'Include Vue 3 + Element Plus frontend?', true);
|
|
104
|
+
const uniapp = await askConfirm(io, 'Include uniapp mobile app?', false);
|
|
105
|
+
const autoInstall = await askConfirm(io, 'Auto install dependencies?', false);
|
|
106
|
+
return { projectName, frontend, uniapp, autoInstall };
|
|
107
|
+
} finally {
|
|
108
|
+
io.rl.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { runInteractive, askText, askConfirm, c };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-koa-boot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Scaffolding CLI: generate a koa-boot-runtime project with system routes, demo business modules and merged Prisma schema",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"koa",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"bin",
|
|
28
28
|
"index.js",
|
|
29
|
+
"interactive.js",
|
|
29
30
|
"ai.js",
|
|
30
31
|
"ai",
|
|
31
32
|
"templates"
|