create-pangza 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # create-pangza
2
+
3
+ 初始化空项目,并按需生成 `entity` / `mapper` / `service` / `controller`。
4
+
5
+ ## init
6
+
7
+ ```bash
8
+ npx create-pangza init my-app
9
+ # 或
10
+ npm create pangza@latest -- init my-app
11
+ ```
12
+
13
+ 交互勾选模块:`orm` / `auth` / `redis` / `log` / `schedule` / `encrypt` / `validate`。
14
+
15
+ ```bash
16
+ npx create-pangza init my-app --modules=orm,auth,redis --db=mysql
17
+ ```
18
+
19
+ ## generate(`g`)
20
+
21
+ 在项目根目录执行:
22
+
23
+ ```bash
24
+ npx create-pangza g controller user
25
+ npx create-pangza g service user
26
+ npx create-pangza g mapper user
27
+ npx create-pangza g entity user
28
+
29
+ # 一次生成整套用户模块
30
+ npx create-pangza g all user
31
+ ```
32
+
33
+ 别名:`c|controller|cotroller`、`s|service`、`m|mapper`、`e|entity`、`a|all`。
34
+ 覆盖已有文件:加 `--force`。
35
+
36
+ 生成路径示例(`user`):
37
+
38
+ ```text
39
+ src/entity/user.ts
40
+ src/mapper/user.ts
41
+ src/service/user.ts
42
+ src/controller/user/index.ts → /api/user
43
+ ```
44
+
45
+ ## 发布
46
+
47
+ ```bash
48
+ cd create-pangza
49
+ npm publish --access public --registry=https://registry.npmjs.org
50
+ ```
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { MODULES, DB_CHOICES } = require("../lib/modules");
7
+ const { scaffold } = require("../lib/scaffold");
8
+ const { generate, resolveKind } = require("../lib/generate");
9
+ const {
10
+ createRl,
11
+ askText,
12
+ askSelect,
13
+ askMulti,
14
+ askYesNo,
15
+ } = require("../lib/prompt");
16
+
17
+ function usage() {
18
+ console.log(`create-pangza — scaffold & generate for @pangza/express
19
+
20
+ Init (empty app + optional modules):
21
+ create-pangza init [project-name]
22
+ create-pangza init my-app --modules=orm,redis --db=mysql
23
+
24
+ Generate (run inside a project):
25
+ create-pangza g controller user
26
+ create-pangza g service user
27
+ create-pangza g mapper user
28
+ create-pangza g entity user
29
+ create-pangza g all user
30
+
31
+ Aliases: g|generate c|controller|cotroller s|service m|mapper e|entity a|all
32
+ Flags: --force overwrite existing files
33
+
34
+ Modules (init):
35
+ ${MODULES.map((m) => ` ${m.id.padEnd(10)} ${m.hint}`).join("\n")}
36
+ `);
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const args = {
41
+ cmd: null,
42
+ name: null,
43
+ modules: null,
44
+ db: null,
45
+ help: false,
46
+ force: false,
47
+ genKind: null,
48
+ genName: null,
49
+ };
50
+ const rest = [];
51
+ for (const a of argv) {
52
+ if (a === "-h" || a === "--help") args.help = true;
53
+ else if (a === "--force" || a === "-f") args.force = true;
54
+ else if (a.startsWith("--modules=")) {
55
+ args.modules = a
56
+ .slice("--modules=".length)
57
+ .split(",")
58
+ .map((s) => s.trim())
59
+ .filter(Boolean);
60
+ } else if (a.startsWith("--db=")) {
61
+ args.db = a.slice("--db=".length).trim();
62
+ } else {
63
+ rest.push(a);
64
+ }
65
+ }
66
+
67
+ const head = rest[0];
68
+ if (head === "init") {
69
+ args.cmd = "init";
70
+ args.name = rest[1] || null;
71
+ } else if (head === "g" || head === "generate") {
72
+ args.cmd = "generate";
73
+ args.genKind = rest[1] || null;
74
+ args.genName = rest[2] || null;
75
+ } else if (head) {
76
+ // 兼容:create-pangza my-app → init
77
+ args.cmd = "init";
78
+ args.name = head;
79
+ }
80
+ return args;
81
+ }
82
+
83
+ function isValidName(name) {
84
+ return Boolean(name) && /^[a-zA-Z0-9._@/-]+$/.test(name) && !name.includes("..");
85
+ }
86
+
87
+ function normalizeProjectName(raw) {
88
+ const base = path.basename(String(raw).replace(/[\\/]+$/, ""));
89
+ return base.startsWith("@") ? base : base.toLowerCase();
90
+ }
91
+
92
+ async function runInit(args) {
93
+ const rl = createRl();
94
+ try {
95
+ let projectNameInput = args.name;
96
+ if (!projectNameInput) {
97
+ projectNameInput = await askText(rl, "项目目录名", "pangza-app");
98
+ }
99
+ if (!isValidName(projectNameInput)) {
100
+ console.error(`Invalid project name: ${projectNameInput}`);
101
+ process.exit(1);
102
+ }
103
+
104
+ const targetDir = path.resolve(process.cwd(), projectNameInput);
105
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
106
+ console.error(`Directory is not empty: ${targetDir}`);
107
+ process.exit(1);
108
+ }
109
+
110
+ let modules = args.modules;
111
+ if (!modules) {
112
+ modules = await askMulti(
113
+ rl,
114
+ "选择要启用的模块(空模板默认只有 HTTP 核心)",
115
+ MODULES.map((m) => ({
116
+ value: m.id,
117
+ label: m.label,
118
+ hint: m.hint,
119
+ })),
120
+ );
121
+ } else {
122
+ const known = new Set(MODULES.map((m) => m.id));
123
+ const bad = modules.filter((m) => !known.has(m));
124
+ if (bad.length) {
125
+ console.error(`Unknown modules: ${bad.join(", ")}`);
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ if (modules.includes("auth") && !modules.includes("redis")) {
131
+ const addRedis =
132
+ args.modules != null
133
+ ? true
134
+ : await askYesNo(
135
+ rl,
136
+ "Auth 通常需要 Redis 存 token,是否一并启用 redis?",
137
+ true,
138
+ );
139
+ if (addRedis) modules = [...new Set([...modules, "redis"])];
140
+ }
141
+
142
+ let dbType = args.db || "mysql";
143
+ if (modules.includes("orm")) {
144
+ if (!args.db) {
145
+ dbType = await askSelect(rl, "ORM 数据库类型", DB_CHOICES, "mysql");
146
+ } else if (!DB_CHOICES.some((c) => c.value === args.db)) {
147
+ console.error(`Unknown --db=${args.db}`);
148
+ process.exit(1);
149
+ }
150
+ }
151
+
152
+ const pkgName = normalizeProjectName(projectNameInput);
153
+ scaffold({
154
+ targetDir,
155
+ projectName: pkgName,
156
+ modules,
157
+ dbType,
158
+ });
159
+
160
+ console.log(`
161
+ ✓ 已初始化空项目: ${projectNameInput}
162
+ 模块: ${modules.length ? modules.join(", ") : "(仅核心)"}
163
+ ${modules.includes("orm") ? ` 数据库: ${dbType}\n` : ""}
164
+ 下一步:
165
+ cd ${projectNameInput}
166
+ cp .env.example .env
167
+ npm install
168
+ npm run dev
169
+
170
+ 生成业务文件:
171
+ npx create-pangza g all user
172
+ `);
173
+ } finally {
174
+ rl.close();
175
+ }
176
+ }
177
+
178
+ function runGenerate(args) {
179
+ if (!args.genKind || !args.genName) {
180
+ console.error("Usage: create-pangza g <controller|service|mapper|entity|all> <name>");
181
+ process.exit(1);
182
+ }
183
+ if (!resolveKind(args.genKind)) {
184
+ console.error(`Unknown type: ${args.genKind}`);
185
+ console.error("Use: controller|service|mapper|entity|all (or c|s|m|e|a)");
186
+ process.exit(1);
187
+ }
188
+
189
+ try {
190
+ const { root, results, Pascal } = generate({
191
+ kind: args.genKind,
192
+ name: args.genName,
193
+ force: args.force,
194
+ });
195
+
196
+ console.log(`✓ generate ${args.genKind} ${Pascal} (root: ${root})`);
197
+ for (const r of results) {
198
+ const mark = r.status === "skip" ? "skip" : "write";
199
+ console.log(` [${mark}] ${r.rel}`);
200
+ }
201
+ if (results.some((r) => r.status === "skip")) {
202
+ console.log(" (已存在文件已跳过;覆盖请加 --force)");
203
+ }
204
+ } catch (err) {
205
+ console.error(err.message || err);
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ async function main() {
211
+ const args = parseArgs(process.argv.slice(2));
212
+ if (args.help || !args.cmd) {
213
+ usage();
214
+ process.exit(args.help ? 0 : 1);
215
+ }
216
+ if (args.cmd === "init") {
217
+ await runInit(args);
218
+ return;
219
+ }
220
+ if (args.cmd === "generate") {
221
+ runGenerate(args);
222
+ return;
223
+ }
224
+ usage();
225
+ process.exit(1);
226
+ }
227
+
228
+ main().catch((err) => {
229
+ console.error(err);
230
+ process.exit(1);
231
+ });
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { toSnake, toPascal, toApiPath, toFileBase } = require("./names");
6
+
7
+ const KINDS = {
8
+ entity: { alias: ["e", "entity"] },
9
+ mapper: { alias: ["m", "mapper"] },
10
+ service: { alias: ["s", "service"] },
11
+ controller: { alias: ["c", "controller", "cotroller"] }, // cotroller: 常见拼写容错
12
+ all: { alias: ["all", "a"] },
13
+ };
14
+
15
+ function resolveKind(raw) {
16
+ const key = String(raw || "").toLowerCase();
17
+ for (const [kind, meta] of Object.entries(KINDS)) {
18
+ if (meta.alias.includes(key)) return kind;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function findProjectRoot(cwd = process.cwd()) {
24
+ let dir = cwd;
25
+ for (;;) {
26
+ const pkg = path.join(dir, "package.json");
27
+ if (fs.existsSync(pkg)) {
28
+ try {
29
+ const json = JSON.parse(fs.readFileSync(pkg, "utf8"));
30
+ const deps = {
31
+ ...(json.dependencies || {}),
32
+ ...(json.devDependencies || {}),
33
+ };
34
+ if (deps["@pangza/express"]) return dir;
35
+ } catch {
36
+ /* ignore */
37
+ }
38
+ // 有 package.json 也当作项目根(容错)
39
+ if (fs.existsSync(path.join(dir, "src"))) return dir;
40
+ }
41
+ const parent = path.dirname(dir);
42
+ if (parent === dir) break;
43
+ dir = parent;
44
+ }
45
+ return cwd;
46
+ }
47
+
48
+ function ensureDir(dir) {
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ }
51
+
52
+ function writeFile(filePath, content, { force }) {
53
+ if (fs.existsSync(filePath) && !force) {
54
+ return { path: filePath, status: "skip" };
55
+ }
56
+ ensureDir(path.dirname(filePath));
57
+ fs.writeFileSync(filePath, content, "utf8");
58
+ return { path: filePath, status: fs.existsSync(filePath) ? "write" : "write" };
59
+ }
60
+
61
+ function tplEntity({ Pascal, snake }) {
62
+ return `import { TableField, TableId, TableName } from "@pangza/express";
63
+
64
+ @TableName("${snake}")
65
+ export default class ${Pascal}Entity {
66
+ @TableId()
67
+ id!: string;
68
+
69
+ @TableField({
70
+ value: "create_time",
71
+ insertFill: () => new Date(),
72
+ })
73
+ createTime?: Date;
74
+
75
+ @TableField({
76
+ value: "update_time",
77
+ insertFill: () => new Date(),
78
+ updateFill: () => new Date(),
79
+ })
80
+ updateTime?: Date;
81
+
82
+ @TableField({
83
+ value: "delete_flag",
84
+ insertFill: 0,
85
+ })
86
+ deleteFlag?: number;
87
+ }
88
+ `;
89
+ }
90
+
91
+ function tplMapper({ Pascal, fileBase }) {
92
+ return `import { Mapper, MapperType } from "@pangza/express";
93
+ import ${Pascal}Entity from "../entity/${fileBase}";
94
+
95
+ @Mapper(${Pascal}Entity)
96
+ export default class ${Pascal}Mapper extends MapperType {}
97
+ `;
98
+ }
99
+
100
+ function tplService({ Pascal, fileBase }) {
101
+ return `import { BaseService, Service } from "@pangza/express";
102
+ import ${Pascal}Mapper from "../mapper/${fileBase}";
103
+
104
+ @Service(${Pascal}Mapper)
105
+ export default class ${Pascal}Service extends BaseService {}
106
+ `;
107
+ }
108
+
109
+ function tplController({ Pascal, fileBase, apiPath, camel }) {
110
+ return `import {
111
+ Controller,
112
+ DeleteMapping,
113
+ GetMapping,
114
+ PostMapping,
115
+ PutMapping,
116
+ RequestBody,
117
+ RequestPath,
118
+ Resource,
119
+ } from "@pangza/express";
120
+ import ${Pascal}Service from "../../service/${fileBase}";
121
+
122
+ @Controller("/api/${apiPath}")
123
+ export default class ${Pascal}Controller {
124
+ @Resource(${Pascal}Service)
125
+ ${camel}Service!: ${Pascal}Service;
126
+
127
+ @GetMapping("/")
128
+ list() {
129
+ return this.${camel}Service.selectList();
130
+ }
131
+
132
+ @GetMapping("/:id")
133
+ detail(@RequestPath() id: string) {
134
+ return this.${camel}Service.selectById(id);
135
+ }
136
+
137
+ @PostMapping("/")
138
+ create(@RequestBody() body: Record<string, unknown>) {
139
+ return this.${camel}Service.insert(body);
140
+ }
141
+
142
+ @PutMapping("/:id")
143
+ update(@RequestPath() id: string, @RequestBody() body: Record<string, unknown>) {
144
+ return this.${camel}Service.updateById({ ...body, id });
145
+ }
146
+
147
+ @DeleteMapping("/:id")
148
+ remove(@RequestPath() id: string) {
149
+ return this.${camel}Service.deleteById(id);
150
+ }
151
+ }
152
+ `;
153
+ }
154
+
155
+ /**
156
+ * @param {{
157
+ * kind: string,
158
+ * name: string,
159
+ * cwd?: string,
160
+ * force?: boolean,
161
+ * }} opts
162
+ */
163
+ function generate(opts) {
164
+ const kind = resolveKind(opts.kind);
165
+ if (!kind) {
166
+ throw new Error(
167
+ `Unknown type "${opts.kind}". Use: controller|service|mapper|entity|all`,
168
+ );
169
+ }
170
+ if (!opts.name || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(opts.name)) {
171
+ throw new Error(`Invalid name: ${opts.name}`);
172
+ }
173
+
174
+ const root = findProjectRoot(opts.cwd || process.cwd());
175
+ const fileBase = toFileBase(opts.name);
176
+ const Pascal = toPascal(opts.name);
177
+ const snake = toSnake(opts.name);
178
+ const apiPath = toApiPath(opts.name);
179
+ const camel = fileBase;
180
+ const force = Boolean(opts.force);
181
+
182
+ const ctx = { Pascal, snake, fileBase, apiPath, camel };
183
+
184
+ /** @type {{ kind: string, rel: string, content: string }[]} */
185
+ const jobs = [];
186
+
187
+ const want = kind === "all"
188
+ ? ["entity", "mapper", "service", "controller"]
189
+ : [kind];
190
+
191
+ for (const k of want) {
192
+ if (k === "entity") {
193
+ jobs.push({
194
+ kind: k,
195
+ rel: path.join("src", "entity", `${fileBase}.ts`),
196
+ content: tplEntity(ctx),
197
+ });
198
+ } else if (k === "mapper") {
199
+ jobs.push({
200
+ kind: k,
201
+ rel: path.join("src", "mapper", `${fileBase}.ts`),
202
+ content: tplMapper(ctx),
203
+ });
204
+ } else if (k === "service") {
205
+ jobs.push({
206
+ kind: k,
207
+ rel: path.join("src", "service", `${fileBase}.ts`),
208
+ content: tplService(ctx),
209
+ });
210
+ } else if (k === "controller") {
211
+ jobs.push({
212
+ kind: k,
213
+ rel: path.join("src", "controller", fileBase, "index.ts"),
214
+ content: tplController(ctx),
215
+ });
216
+ }
217
+ }
218
+
219
+ const results = [];
220
+ for (const job of jobs) {
221
+ const abs = path.join(root, job.rel);
222
+ const r = writeFile(abs, job.content, { force });
223
+ results.push({ kind: job.kind, ...r, rel: job.rel });
224
+ }
225
+
226
+ return { root, results, Pascal, snake };
227
+ }
228
+
229
+ module.exports = {
230
+ KINDS,
231
+ resolveKind,
232
+ findProjectRoot,
233
+ generate,
234
+ };
package/lib/modules.js ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+
3
+ /** 可选能力模块(当前均由单包 @pangza/express 提供,差异在依赖与脚手架文件) */
4
+ const MODULES = [
5
+ {
6
+ id: "orm",
7
+ label: "ORM",
8
+ hint: "Db / Mapper / QueryWrapper / 多数据源",
9
+ },
10
+ {
11
+ id: "auth",
12
+ label: "Auth",
13
+ hint: "登录态、角色权限装饰器(建议同时选 Redis)",
14
+ },
15
+ {
16
+ id: "redis",
17
+ label: "Redis",
18
+ hint: "缓存、限流、Auth token 存储",
19
+ },
20
+ {
21
+ id: "log",
22
+ label: "Log",
23
+ hint: "操作审计日志装饰器",
24
+ },
25
+ {
26
+ id: "schedule",
27
+ label: "Schedule",
28
+ hint: "定时任务 (cron)",
29
+ },
30
+ {
31
+ id: "encrypt",
32
+ label: "Encrypt",
33
+ hint: "AES 等传输加密工具",
34
+ },
35
+ {
36
+ id: "validate",
37
+ label: "Validate",
38
+ hint: "参数校验装饰器",
39
+ },
40
+ ];
41
+
42
+ const DB_CHOICES = [
43
+ { value: "mysql", label: "MySQL (mysql2)" },
44
+ { value: "postgresql", label: "PostgreSQL (pg)" },
45
+ { value: "mssql", label: "SQL Server (mssql)" },
46
+ ];
47
+
48
+ const DB_DEPS = {
49
+ mysql: { mysql2: "^3.24.4" },
50
+ postgresql: { pg: "^8.23.1" },
51
+ mssql: { mssql: "^12.7.9" },
52
+ };
53
+
54
+ const DB_ENV = {
55
+ mysql: [
56
+ "DB_TYPE=mysql",
57
+ "MYSQL_HOST=127.0.0.1",
58
+ "MYSQL_PORT=3306",
59
+ "MYSQL_USER=root",
60
+ "MYSQL_PASSWORD=",
61
+ "MYSQL_DATABASE=pangza",
62
+ ],
63
+ postgresql: [
64
+ "DB_TYPE=postgresql",
65
+ "PG_HOST=127.0.0.1",
66
+ "PG_PORT=5432",
67
+ "PG_USER=postgres",
68
+ "PG_PASSWORD=",
69
+ "PG_DATABASE=pangza",
70
+ ],
71
+ mssql: [
72
+ "DB_TYPE=mssql",
73
+ "MSSQL_HOST=127.0.0.1",
74
+ "MSSQL_PORT=1433",
75
+ "MSSQL_USER=sa",
76
+ "MSSQL_PASSWORD=",
77
+ "MSSQL_DATABASE=pangza",
78
+ ],
79
+ };
80
+
81
+ /** @param {string[]} moduleIds @param {string} [dbType] */
82
+ function resolveDependencies(moduleIds, dbType = "mysql") {
83
+ const deps = {
84
+ "@pangza/express": "^1.0.0",
85
+ cors: "^2.8.5",
86
+ express: "^5.2.1",
87
+ inversify: "^8.2.3",
88
+ "reflect-metadata": "^0.2.2",
89
+ };
90
+ const set = new Set(moduleIds);
91
+
92
+ if (set.has("orm")) {
93
+ Object.assign(deps, DB_DEPS[dbType] || DB_DEPS.mysql);
94
+ }
95
+ if (set.has("redis") || set.has("auth")) {
96
+ deps.redis = "^6.2.1";
97
+ }
98
+ if (set.has("auth")) {
99
+ deps.jsonwebtoken = "^9.0.3";
100
+ deps.bcryptjs = "^3.0.3";
101
+ }
102
+ if (set.has("schedule")) {
103
+ deps["node-cron"] = "^4.6.0";
104
+ }
105
+
106
+ return deps;
107
+ }
108
+
109
+ /** @param {string[]} moduleIds @param {string} [dbType] */
110
+ function resolveEnvLines(moduleIds, dbType = "mysql") {
111
+ const lines = ["PORT=5270"];
112
+ const set = new Set(moduleIds);
113
+
114
+ if (set.has("orm")) {
115
+ lines.push("", "# Database", ...(DB_ENV[dbType] || DB_ENV.mysql));
116
+ }
117
+ if (set.has("redis") || set.has("auth")) {
118
+ lines.push(
119
+ "",
120
+ "# Redis",
121
+ "REDIS_HOST=127.0.0.1",
122
+ "REDIS_PORT=6379",
123
+ "REDIS_PASSWORD=",
124
+ );
125
+ }
126
+ if (set.has("auth")) {
127
+ lines.push("", "# Auth", "JWT_SECRET=change-me");
128
+ }
129
+ return lines;
130
+ }
131
+
132
+ /** @param {string[]} moduleIds */
133
+ function resolveDirs(moduleIds) {
134
+ const dirs = ["src"];
135
+ const set = new Set(moduleIds);
136
+ if (set.has("orm")) {
137
+ dirs.push("src/entity", "src/mapper", "src/service", "src/controller");
138
+ } else {
139
+ dirs.push("src/controller");
140
+ }
141
+ if (set.has("schedule")) dirs.push("src/schedule");
142
+ return dirs;
143
+ }
144
+
145
+ /**
146
+ * 生成空项目入口:只起 HTTP,模块相关注释提示用户自行接入。
147
+ * @param {string[]} moduleIds
148
+ */
149
+ function generateMainTs(moduleIds) {
150
+ const set = new Set(moduleIds);
151
+ const imports = [
152
+ 'import "reflect-metadata";',
153
+ 'import cors from "cors";',
154
+ 'import { json } from "express";',
155
+ 'import { Application, Logger } from "@pangza/express";',
156
+ ];
157
+
158
+ const notes = [];
159
+ if (set.has("orm")) {
160
+ notes.push(
161
+ "// ORM: 配置 .env 中的数据库变量后即可使用 @Mapper / QueryWrapper",
162
+ );
163
+ }
164
+ if (set.has("redis") || set.has("auth")) {
165
+ notes.push("// Redis: 配置 REDIS_* 后由框架自动连接(见文档)");
166
+ }
167
+ if (set.has("auth")) {
168
+ notes.push(
169
+ '// Auth: Application.registry(Auth({ ignore: ["/api/pub/**"] }))',
170
+ );
171
+ }
172
+ if (set.has("log")) {
173
+ notes.push("// Log: configureLog({ writer }) 后配合 @Log 装饰器");
174
+ }
175
+ if (set.has("schedule")) {
176
+ notes.push("// Schedule: 在 src/schedule 编写任务并由扫描加载");
177
+ }
178
+ if (set.has("encrypt")) {
179
+ notes.push("// Encrypt: 从 @pangza/express 导入 AESUtil 等");
180
+ }
181
+ if (set.has("validate")) {
182
+ notes.push("// Validate: 在 Controller 参数上使用校验装饰器");
183
+ }
184
+
185
+ const body = [
186
+ ...imports,
187
+ "",
188
+ ...(notes.length ? [...notes, ""] : []),
189
+ "Application.registry(Logger({ level: \"info\" }))",
190
+ " .registry(cors())",
191
+ " .registry(json())",
192
+ ' .scan("src")',
193
+ " .start();",
194
+ "",
195
+ ];
196
+ return body.join("\n");
197
+ }
198
+
199
+ function generateGitkeepReadme(moduleIds) {
200
+ const set = new Set(moduleIds);
201
+ const files = {
202
+ "src/controller/.gitkeep": "",
203
+ };
204
+ if (set.has("orm")) {
205
+ files["src/entity/.gitkeep"] = "";
206
+ files["src/mapper/.gitkeep"] = "";
207
+ files["src/service/.gitkeep"] = "";
208
+ }
209
+ if (set.has("schedule")) {
210
+ files["src/schedule/.gitkeep"] = "";
211
+ }
212
+ return files;
213
+ }
214
+
215
+ module.exports = {
216
+ MODULES,
217
+ DB_CHOICES,
218
+ resolveDependencies,
219
+ resolveEnvLines,
220
+ resolveDirs,
221
+ generateMainTs,
222
+ generateGitkeepReadme,
223
+ };
package/lib/names.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ /** user_role / user-role / userRole → user_role (table) */
4
+ function toSnake(name) {
5
+ return String(name)
6
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
7
+ .replace(/[-\s]+/g, "_")
8
+ .replace(/__+/g, "_")
9
+ .toLowerCase();
10
+ }
11
+
12
+ /** user_role → userRole */
13
+ function toCamel(name) {
14
+ const snake = toSnake(name);
15
+ return snake.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
16
+ }
17
+
18
+ /** user_role → UserRole */
19
+ function toPascal(name) {
20
+ const camel = toCamel(name);
21
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
22
+ }
23
+
24
+ /** user_role → user-role (url segment often snake or kebab; we use snake for path) */
25
+ function toApiPath(name) {
26
+ return toSnake(name).replace(/_/g, "-");
27
+ }
28
+
29
+ /** file base under src/* : prefer camel for multi-word, keep simple names as-is */
30
+ function toFileBase(name) {
31
+ return toCamel(name);
32
+ }
33
+
34
+ module.exports = {
35
+ toSnake,
36
+ toCamel,
37
+ toPascal,
38
+ toApiPath,
39
+ toFileBase,
40
+ };
package/lib/prompt.js ADDED
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ const readline = require("readline");
4
+
5
+ function createRl() {
6
+ return readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout,
9
+ });
10
+ }
11
+
12
+ function ask(rl, question) {
13
+ return new Promise((resolve) => {
14
+ rl.question(question, (answer) => resolve(String(answer || "").trim()));
15
+ });
16
+ }
17
+
18
+ /**
19
+ * @param {string} message
20
+ * @param {string} [fallback]
21
+ */
22
+ async function askText(rl, message, fallback = "") {
23
+ const hint = fallback ? ` (${fallback})` : "";
24
+ const answer = await ask(rl, `${message}${hint}: `);
25
+ return answer || fallback;
26
+ }
27
+
28
+ /**
29
+ * @param {string} message
30
+ * @param {{ value: string, label: string }[]} choices
31
+ * @param {string} [fallback]
32
+ */
33
+ async function askSelect(rl, message, choices, fallback) {
34
+ console.log(`\n${message}`);
35
+ choices.forEach((c, i) => {
36
+ console.log(` ${i + 1}) ${c.label}`);
37
+ });
38
+ const def =
39
+ fallback != null
40
+ ? choices.findIndex((c) => c.value === fallback) + 1
41
+ : 1;
42
+ const answer = await ask(rl, `选择 [1-${choices.length}] (默认 ${def}): `);
43
+ const n = Number(answer || def);
44
+ if (!Number.isInteger(n) || n < 1 || n > choices.length) {
45
+ console.log(`无效选择,使用默认: ${choices[def - 1].label}`);
46
+ return choices[def - 1].value;
47
+ }
48
+ return choices[n - 1].value;
49
+ }
50
+
51
+ /**
52
+ * Multi-select by comma-separated numbers.
53
+ * @param {string} message
54
+ * @param {{ value: string, label: string, hint?: string }[]} choices
55
+ * @param {string[]} [preselected]
56
+ */
57
+ async function askMulti(rl, message, choices, preselected = []) {
58
+ console.log(`\n${message}`);
59
+ console.log("(输入序号,逗号分隔;直接回车=不选额外模块)");
60
+ choices.forEach((c, i) => {
61
+ const mark = preselected.includes(c.value) ? "*" : " ";
62
+ const hint = c.hint ? ` — ${c.hint}` : "";
63
+ console.log(` ${i + 1}) [${mark}] ${c.label}${hint}`);
64
+ });
65
+ const answer = await ask(rl, "> ");
66
+ if (!answer) return [...preselected];
67
+
68
+ const picked = new Set(preselected);
69
+ for (const part of answer.split(/[,,\s]+/)) {
70
+ if (!part) continue;
71
+ const n = Number(part);
72
+ if (!Number.isInteger(n) || n < 1 || n > choices.length) {
73
+ console.log(`忽略无效项: ${part}`);
74
+ continue;
75
+ }
76
+ picked.add(choices[n - 1].value);
77
+ }
78
+ return [...picked];
79
+ }
80
+
81
+ async function askYesNo(rl, message, defaultYes = true) {
82
+ const tip = defaultYes ? "Y/n" : "y/N";
83
+ const answer = (await ask(rl, `${message} (${tip}): `)).toLowerCase();
84
+ if (!answer) return defaultYes;
85
+ return answer === "y" || answer === "yes";
86
+ }
87
+
88
+ module.exports = {
89
+ createRl,
90
+ askText,
91
+ askSelect,
92
+ askMulti,
93
+ askYesNo,
94
+ };
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const {
6
+ resolveDependencies,
7
+ resolveEnvLines,
8
+ resolveDirs,
9
+ generateMainTs,
10
+ generateGitkeepReadme,
11
+ } = require("./modules");
12
+
13
+ const ROOT = path.join(__dirname, "..");
14
+ const BASE_TEMPLATE = path.join(ROOT, "template", "base");
15
+
16
+ function ensureDir(dir) {
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ }
19
+
20
+ function writeFile(filePath, content) {
21
+ ensureDir(path.dirname(filePath));
22
+ fs.writeFileSync(filePath, content, "utf8");
23
+ }
24
+
25
+ function copyFile(src, dest) {
26
+ ensureDir(path.dirname(dest));
27
+ fs.copyFileSync(src, dest);
28
+ }
29
+
30
+ /**
31
+ * @param {{
32
+ * targetDir: string,
33
+ * projectName: string,
34
+ * modules: string[],
35
+ * dbType?: string,
36
+ * }} opts
37
+ */
38
+ function scaffold(opts) {
39
+ const {
40
+ targetDir,
41
+ projectName,
42
+ modules,
43
+ dbType = "mysql",
44
+ } = opts;
45
+
46
+ ensureDir(targetDir);
47
+
48
+ // base static files
49
+ copyFile(
50
+ path.join(BASE_TEMPLATE, "tsconfig.json"),
51
+ path.join(targetDir, "tsconfig.json"),
52
+ );
53
+ copyFile(
54
+ path.join(BASE_TEMPLATE, "gitignore"),
55
+ path.join(targetDir, ".gitignore"),
56
+ );
57
+
58
+ for (const dir of resolveDirs(modules)) {
59
+ ensureDir(path.join(targetDir, dir));
60
+ }
61
+
62
+ const pkg = {
63
+ name: projectName,
64
+ version: "1.0.0",
65
+ private: true,
66
+ description: "Pangza Express app",
67
+ scripts: {
68
+ dev: "ts-node -r tsconfig-paths/register src/main.ts",
69
+ build: "tsc -p tsconfig.json",
70
+ start: "node dist/main.js",
71
+ },
72
+ dependencies: resolveDependencies(modules, dbType),
73
+ devDependencies: {
74
+ "@types/cors": "^2.8.19",
75
+ "@types/express": "^5.0.6",
76
+ "@types/node": "^22.0.0",
77
+ "ts-node": "^10.9.2",
78
+ "tsconfig-paths": "^4.2.0",
79
+ typescript: "^5.9.3",
80
+ },
81
+ };
82
+
83
+ writeFile(
84
+ path.join(targetDir, "package.json"),
85
+ JSON.stringify(pkg, null, 2) + "\n",
86
+ );
87
+ writeFile(
88
+ path.join(targetDir, ".env.example"),
89
+ resolveEnvLines(modules, dbType).join("\n") + "\n",
90
+ );
91
+ writeFile(path.join(targetDir, "src", "main.ts"), generateMainTs(modules));
92
+
93
+ const extras = generateGitkeepReadme(modules);
94
+ for (const [rel, content] of Object.entries(extras)) {
95
+ writeFile(path.join(targetDir, rel), content);
96
+ }
97
+
98
+ writeFile(
99
+ path.join(targetDir, "README.md"),
100
+ `# ${projectName}
101
+
102
+ Generated by \`create-pangza init\`.
103
+
104
+ ## Modules
105
+
106
+ ${modules.length ? modules.map((m) => `- ${m}`).join("\n") : "- (core only)"}
107
+
108
+ ${modules.includes("orm") ? `Database: **${dbType}**\n` : ""}
109
+ ## Run
110
+
111
+ \`\`\`bash
112
+ cp .env.example .env
113
+ npm install
114
+ npm run dev
115
+ \`\`\`
116
+ `,
117
+ );
118
+ }
119
+
120
+ module.exports = { scaffold };
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
- {
2
- "name": "create-pangza",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "create-pangza",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold & generate for @pangza/express (init / g controller|service|mapper|entity|all)",
5
+ "bin": {
6
+ "create-pangza": "./bin/create-pangza.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "lib",
11
+ "template",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "pangza",
16
+ "express",
17
+ "scaffold",
18
+ "create",
19
+ "init",
20
+ "typescript"
21
+ ],
22
+ "author": "",
23
+ "license": "ISC",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/lx-ayan/pangza-admin-express.git",
27
+ "directory": "create-pangza"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/lx-ayan/pangza-admin-express/issues"
31
+ },
32
+ "homepage": "https://github.com/lx-ayan/pangza-admin-express#readme",
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "outDir": "dist",
6
+ "rootDir": "src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "experimentalDecorators": true,
11
+ "emitDecoratorMetadata": true,
12
+ "moduleResolution": "node"
13
+ },
14
+ "include": ["src/**/*"]
15
+ }