create-pangza 0.0.0-stage → 1.0.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 +51 -3
- package/bin/create-pangza.js +231 -0
- package/lib/generate.js +234 -0
- package/lib/modules.js +213 -0
- package/lib/names.js +40 -0
- package/lib/prompt.js +94 -0
- package/lib/scaffold.js +115 -0
- package/package.json +39 -6
- package/template/base/gitignore +5 -0
- package/template/base/tsconfig.json +15 -0
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
勾选 `orm` / `redis` 时会写入 `EnableOrm()` / `EnableRedis()`;未勾选则启动不连库、不因缺配置报错。不生成 `.gitkeep`。
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx create-pangza init my-app --modules=orm,auth,redis --db=mysql
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## generate(`g`)
|
|
21
|
+
|
|
22
|
+
在项目根目录执行:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx create-pangza g controller user
|
|
26
|
+
npx create-pangza g service user
|
|
27
|
+
npx create-pangza g mapper user
|
|
28
|
+
npx create-pangza g entity user
|
|
29
|
+
|
|
30
|
+
# 一次生成整套用户模块
|
|
31
|
+
npx create-pangza g all user
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
别名:`c|controller|cotroller`、`s|service`、`m|mapper`、`e|entity`、`a|all`。
|
|
35
|
+
覆盖已有文件:加 `--force`。
|
|
36
|
+
|
|
37
|
+
生成路径示例(`user`):
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
src/entity/user.ts
|
|
41
|
+
src/mapper/user.ts
|
|
42
|
+
src/service/user.ts
|
|
43
|
+
src/controller/user/index.ts → /api/user
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 发布
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd create-pangza
|
|
50
|
+
npm publish --access public --registry=https://registry.npmjs.org
|
|
51
|
+
```
|
|
@@ -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
|
+
});
|
package/lib/generate.js
ADDED
|
@@ -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,213 @@
|
|
|
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.1",
|
|
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;选了 redis/orm 才 Enable*,否则启动不连库。
|
|
147
|
+
* @param {string[]} moduleIds
|
|
148
|
+
*/
|
|
149
|
+
function generateMainTs(moduleIds) {
|
|
150
|
+
const set = new Set(moduleIds);
|
|
151
|
+
const named = ["Application", "Logger"];
|
|
152
|
+
if (set.has("orm")) named.push("EnableOrm");
|
|
153
|
+
if (set.has("redis") || set.has("auth")) named.push("EnableRedis");
|
|
154
|
+
if (set.has("auth")) named.push("Auth", "Redis");
|
|
155
|
+
if (set.has("log")) named.push("Log");
|
|
156
|
+
|
|
157
|
+
const imports = [
|
|
158
|
+
'import "reflect-metadata";',
|
|
159
|
+
'import cors from "cors";',
|
|
160
|
+
'import { json } from "express";',
|
|
161
|
+
`import { ${named.join(", ")} } from "@pangza/express";`,
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const notes = [];
|
|
165
|
+
if (set.has("schedule")) {
|
|
166
|
+
notes.push("// Schedule: 在 src/schedule 编写任务并由扫描加载");
|
|
167
|
+
}
|
|
168
|
+
if (set.has("encrypt")) {
|
|
169
|
+
notes.push("// Encrypt: 从 @pangza/express 导入 AESUtil 等");
|
|
170
|
+
}
|
|
171
|
+
if (set.has("validate")) {
|
|
172
|
+
notes.push("// Validate: 在 Controller 参数上使用校验装饰器");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const chain = [
|
|
176
|
+
'Application.registry(Logger({ level: "info" }))',
|
|
177
|
+
" .registry(cors())",
|
|
178
|
+
" .registry(json())",
|
|
179
|
+
];
|
|
180
|
+
if (set.has("orm")) {
|
|
181
|
+
chain.push(" .registry(EnableOrm())");
|
|
182
|
+
}
|
|
183
|
+
if (set.has("redis") || set.has("auth")) {
|
|
184
|
+
chain.push(" .registry(EnableRedis())");
|
|
185
|
+
}
|
|
186
|
+
if (set.has("auth")) {
|
|
187
|
+
chain.push(
|
|
188
|
+
' .registry(Auth({ ignore: ["/api/pub/**"], redis: Redis }))',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (set.has("log")) {
|
|
192
|
+
chain.push(" .registry(Log())");
|
|
193
|
+
}
|
|
194
|
+
chain.push(' .scan("src")');
|
|
195
|
+
chain.push(" .start();");
|
|
196
|
+
|
|
197
|
+
return [
|
|
198
|
+
...imports,
|
|
199
|
+
"",
|
|
200
|
+
...(notes.length ? [...notes, ""] : []),
|
|
201
|
+
...chain,
|
|
202
|
+
"",
|
|
203
|
+
].join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
MODULES,
|
|
208
|
+
DB_CHOICES,
|
|
209
|
+
resolveDependencies,
|
|
210
|
+
resolveEnvLines,
|
|
211
|
+
resolveDirs,
|
|
212
|
+
generateMainTs,
|
|
213
|
+
};
|
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
|
+
};
|
package/lib/scaffold.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
} = require("./modules");
|
|
11
|
+
|
|
12
|
+
const ROOT = path.join(__dirname, "..");
|
|
13
|
+
const BASE_TEMPLATE = path.join(ROOT, "template", "base");
|
|
14
|
+
|
|
15
|
+
function ensureDir(dir) {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeFile(filePath, content) {
|
|
20
|
+
ensureDir(path.dirname(filePath));
|
|
21
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function copyFile(src, dest) {
|
|
25
|
+
ensureDir(path.dirname(dest));
|
|
26
|
+
fs.copyFileSync(src, dest);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{
|
|
31
|
+
* targetDir: string,
|
|
32
|
+
* projectName: string,
|
|
33
|
+
* modules: string[],
|
|
34
|
+
* dbType?: string,
|
|
35
|
+
* }} opts
|
|
36
|
+
*/
|
|
37
|
+
function scaffold(opts) {
|
|
38
|
+
const {
|
|
39
|
+
targetDir,
|
|
40
|
+
projectName,
|
|
41
|
+
modules,
|
|
42
|
+
dbType = "mysql",
|
|
43
|
+
} = opts;
|
|
44
|
+
|
|
45
|
+
ensureDir(targetDir);
|
|
46
|
+
|
|
47
|
+
copyFile(
|
|
48
|
+
path.join(BASE_TEMPLATE, "tsconfig.json"),
|
|
49
|
+
path.join(targetDir, "tsconfig.json"),
|
|
50
|
+
);
|
|
51
|
+
copyFile(
|
|
52
|
+
path.join(BASE_TEMPLATE, "gitignore"),
|
|
53
|
+
path.join(targetDir, ".gitignore"),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
for (const dir of resolveDirs(modules)) {
|
|
57
|
+
ensureDir(path.join(targetDir, dir));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pkg = {
|
|
61
|
+
name: projectName,
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
private: true,
|
|
64
|
+
description: "Pangza Express app",
|
|
65
|
+
scripts: {
|
|
66
|
+
dev: "ts-node -r tsconfig-paths/register src/main.ts",
|
|
67
|
+
build: "tsc -p tsconfig.json",
|
|
68
|
+
start: "node dist/main.js",
|
|
69
|
+
},
|
|
70
|
+
dependencies: resolveDependencies(modules, dbType),
|
|
71
|
+
devDependencies: {
|
|
72
|
+
"@types/cors": "^2.8.19",
|
|
73
|
+
"@types/express": "^5.0.6",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"ts-node": "^10.9.2",
|
|
76
|
+
"tsconfig-paths": "^4.2.0",
|
|
77
|
+
typescript: "^5.9.3",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
writeFile(
|
|
82
|
+
path.join(targetDir, "package.json"),
|
|
83
|
+
JSON.stringify(pkg, null, 2) + "\n",
|
|
84
|
+
);
|
|
85
|
+
writeFile(
|
|
86
|
+
path.join(targetDir, ".env.example"),
|
|
87
|
+
resolveEnvLines(modules, dbType).join("\n") + "\n",
|
|
88
|
+
);
|
|
89
|
+
writeFile(path.join(targetDir, "src", "main.ts"), generateMainTs(modules));
|
|
90
|
+
|
|
91
|
+
writeFile(
|
|
92
|
+
path.join(targetDir, "README.md"),
|
|
93
|
+
`# ${projectName}
|
|
94
|
+
|
|
95
|
+
Generated by \`create-pangza init\`.
|
|
96
|
+
|
|
97
|
+
## Modules
|
|
98
|
+
|
|
99
|
+
${modules.length ? modules.map((m) => `- ${m}`).join("\n") : "- (core only)"}
|
|
100
|
+
|
|
101
|
+
${modules.includes("orm") ? `Database: **${dbType}**\n` : ""}
|
|
102
|
+
## Run
|
|
103
|
+
|
|
104
|
+
\`\`\`bash
|
|
105
|
+
cp .env.example .env
|
|
106
|
+
npm install
|
|
107
|
+
npm run dev
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
需要 Redis / 数据库时,请在入口 \`registry(EnableRedis())\` / \`registry(EnableOrm())\`(脚手架勾选对应模块时已自动写入)。
|
|
111
|
+
`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { scaffold };
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-pangza",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-pangza",
|
|
3
|
+
"version": "1.0.1",
|
|
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,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
|
+
}
|