moyan-mfw-cli 0.1.0 → 0.1.2

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 (62) hide show
  1. package/README.md +12 -61
  2. package/dist/index.cjs +35 -16
  3. package/dist/index.js +34 -14
  4. package/dist/templates/business/backend/.env.hbs +17 -0
  5. package/dist/templates/business/backend/nest-cli.json.hbs +10 -0
  6. package/dist/templates/business/backend/package.json.hbs +37 -0
  7. package/dist/templates/business/backend/src/app-types.config.ts.hbs +3 -0
  8. package/dist/templates/business/backend/src/app.modules.ts.hbs +7 -0
  9. package/dist/templates/business/backend/src/main.ts.hbs +46 -0
  10. package/dist/templates/business/backend/src/permissions.ts.hbs +4 -0
  11. package/dist/templates/business/backend/tsconfig.json.hbs +18 -0
  12. package/dist/templates/business/frontend/index.html.hbs +35 -0
  13. package/dist/templates/business/frontend/package.json.hbs +32 -0
  14. package/dist/templates/business/frontend/src/main.ts.hbs +28 -0
  15. package/dist/templates/business/frontend/src/permissions.ts.hbs +4 -0
  16. package/dist/templates/business/frontend/src/router.ts.hbs +9 -0
  17. package/dist/templates/business/frontend/src/views/dashboard/Index.vue.hbs +93 -0
  18. package/dist/templates/business/frontend/src/views/dashboard/index.ts.hbs +11 -0
  19. package/dist/templates/business/frontend/tsconfig.json.hbs +19 -0
  20. package/dist/templates/business/frontend/vite.config.ts.hbs +27 -0
  21. package/dist/templates/business/package.json.hbs +17 -0
  22. package/dist/templates/business/pnpm-workspace.yaml.hbs +35 -0
  23. package/dist/templates/business/shared/package.json.hbs +34 -0
  24. package/dist/templates/business/shared/src/index.ts.hbs +1 -0
  25. package/dist/templates/business/shared/src/permissions.ts.hbs +3 -0
  26. package/dist/templates/business/shared/tsconfig.cjs.json.hbs +13 -0
  27. package/dist/templates/business/shared/tsconfig.json.hbs +13 -0
  28. package/dist/templates/business/tsconfig.base.json.hbs +12 -0
  29. package/dist/templates/extension/README.md.hbs +27 -0
  30. package/dist/templates/extension/backend/.env.hbs +10 -0
  31. package/dist/templates/extension/backend/nest-cli.json.hbs +1 -0
  32. package/dist/templates/extension/backend/package.json.hbs +1 -0
  33. package/dist/templates/extension/backend/src/controller/.gitkeep +0 -0
  34. package/dist/templates/extension/backend/src/dto/.gitkeep +0 -0
  35. package/dist/templates/extension/backend/src/entities/.gitkeep +0 -0
  36. package/dist/templates/extension/backend/src/index.ts.hbs +1 -0
  37. package/dist/templates/extension/backend/src/main.ts.hbs +34 -0
  38. package/dist/templates/extension/backend/src/service/.gitkeep +0 -0
  39. package/dist/templates/extension/backend/src/{{name}}.module.ts.hbs +4 -0
  40. package/dist/templates/extension/backend/tsconfig.json.hbs +1 -0
  41. package/dist/templates/extension/frontend/api.build.cjs.hbs +8 -0
  42. package/dist/templates/extension/frontend/index.html.hbs +12 -0
  43. package/dist/templates/extension/frontend/package.json.hbs +1 -0
  44. package/dist/templates/extension/frontend/src/apis/.gitkeep +0 -0
  45. package/dist/templates/extension/frontend/src/components/.gitkeep +0 -0
  46. package/dist/templates/extension/frontend/src/env.d.ts.hbs +1 -0
  47. package/dist/templates/extension/frontend/src/index.ts.hbs +13 -0
  48. package/dist/templates/extension/frontend/src/main.ts.hbs +13 -0
  49. package/dist/templates/extension/frontend/src/views/.gitkeep +0 -0
  50. package/dist/templates/extension/frontend/tsconfig.json.hbs +1 -0
  51. package/dist/templates/extension/frontend/vite.config.mts.hbs +30 -0
  52. package/dist/templates/extension/package.json.hbs +1 -0
  53. package/dist/templates/extension/shared/package.json.hbs +1 -0
  54. package/dist/templates/extension/shared/src/constants.ts.hbs +12 -0
  55. package/dist/templates/extension/shared/src/dict.ts.hbs +8 -0
  56. package/dist/templates/extension/shared/src/index.ts.hbs +10 -0
  57. package/dist/templates/extension/shared/src/paths.ts.hbs +5 -0
  58. package/dist/templates/extension/shared/src/permission-values.ts.hbs +3 -0
  59. package/dist/templates/extension/shared/src/types.ts.hbs +5 -0
  60. package/dist/templates/extension/shared/tsconfig.json.hbs +1 -0
  61. package/dist/templates/extension/tsconfig.json.hbs +1 -0
  62. package/package.json +52 -1
package/README.md CHANGED
@@ -57,6 +57,12 @@ extension-my-ext/
57
57
  └── package.json
58
58
  ```
59
59
 
60
+ **非交互模式**(跳过所有提示,使用默认值):
61
+
62
+ ```bash
63
+ mfw create extension my-ext -y
64
+ ```
65
+
60
66
  **后续步骤**:
61
67
 
62
68
  ```bash
@@ -82,6 +88,12 @@ mfw create business my-shop
82
88
  | 后端端口 | `3000` | NestJS 服务端口 |
83
89
  | 前端端口 | `5173` | Vite 开发服务器端口 |
84
90
 
91
+ **非交互模式**(跳过所有提示,使用默认值):
92
+
93
+ ```bash
94
+ mfw create business my-shop -y
95
+ ```
96
+
85
97
  **生成目录结构**:
86
98
 
87
99
  ```
@@ -120,67 +132,6 @@ pnpm --filter my-shop-backend dev # 启动后端
120
132
  pnpm --filter my-shop-frontend dev # 启动前端
121
133
  ```
122
134
 
123
- ## 模板说明
124
-
125
- ### Handlebars 模板变量
126
-
127
- | 变量 | 示例值 | 说明 |
128
- |------|--------|------|
129
- | `{{name}}` | `my-ext` | kebab-case 项目名 |
130
- | `{{displayName}}` | `MyExt` | 显示名称 |
131
- | `{{description}}` | 用户输入 | 项目描述 |
132
- | `{{className}}` | `MyExt` | PascalCase 类名 |
133
- | `{{version}}` | `0.1.0` | 初始版本 |
134
- | `{{year}}` | `2026` | 当前年份 |
135
-
136
- ### 自定义 Helpers
137
-
138
- | Helper | 输入 | 输出 | 说明 |
139
- |--------|------|------|------|
140
- | `pascalCase` | `my-ext` | `MyExt` | 转 PascalCase |
141
- | `pascalCaseUpper` | `my-ext` | `MYEXT` | 转大写 PascalCase |
142
- | `camelCase` | `my-ext` | `myExt` | 转 camelCase |
143
- | `snakeCase` | `my-ext` | `my_ext` | 转 snake_case |
144
-
145
- ### 模板变量示例
146
-
147
- ```hbs
148
- <!-- 权限常量声明 -->
149
- export const {{pascalCaseUpper name}}_PERMISSION_VALUES = [...]
150
-
151
- <!-- 数据库名 -->
152
- DB_NAME={{snakeCase name}}
153
-
154
- <!-- 包名引用 -->
155
- "moyan-mfw-extension-{{name}}": "workspace:*"
156
- ```
157
-
158
- ## 开发
159
-
160
- ```bash
161
- # 克隆仓库后进入 CLI 目录
162
- cd packages/cli
163
-
164
- # 安装依赖
165
- pnpm install
166
-
167
- # 开发模式(watch)
168
- pnpm dev
169
-
170
- # 构建
171
- pnpm build
172
-
173
- # 类型检查
174
- pnpm typecheck
175
- ```
176
-
177
- ### 添加新模板
178
-
179
- 1. 在 `src/templates/` 下创建新目录
180
- 2. 编写 `.hbs` 模板文件
181
- 3. 在 `src/commands/` 添加对应命令
182
- 4. 在 `src/utils/template.ts` 注册 helpers(如需)
183
-
184
135
  ## 已知修复
185
136
 
186
137
  | 版本 | 修复内容 |
package/dist/index.cjs CHANGED
@@ -32,8 +32,6 @@ var import_commander = require("commander");
32
32
  var import_inquirer = __toESM(require("inquirer"), 1);
33
33
  var import_chalk = __toESM(require("chalk"), 1);
34
34
  var path3 = __toESM(require("path"), 1);
35
- var import_node_path = require("path");
36
- var import_node_url = require("url");
37
35
  var fs3 = __toESM(require("fs/promises"), 1);
38
36
 
39
37
  // src/utils/fs.ts
@@ -58,7 +56,10 @@ async function exists(filePath) {
58
56
  // src/utils/template.ts
59
57
  var import_handlebars = __toESM(require("handlebars"), 1);
60
58
  var fs2 = __toESM(require("fs/promises"), 1);
59
+ var fsSync = __toESM(require("fs"), 1);
61
60
  var path2 = __toESM(require("path"), 1);
61
+ var import_node_url = require("url");
62
+ var import_meta = {};
62
63
  import_handlebars.default.registerHelper(
63
64
  "pascalCase",
64
65
  (str) => str.replace(/(^\w|-\w)/g, (c) => c.slice(-1).toUpperCase())
@@ -72,6 +73,14 @@ import_handlebars.default.registerHelper("pascalCaseUpper", (str) => {
72
73
  return pascal.toUpperCase();
73
74
  });
74
75
  import_handlebars.default.registerHelper("snakeCase", (str) => str.replace(/-/g, "_"));
76
+ function getTemplateDir(type) {
77
+ const currentDir = path2.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
78
+ const distTemplate = path2.resolve(currentDir, `templates/${type}`);
79
+ const srcTemplate = path2.resolve(currentDir, `../templates/${type}`);
80
+ if (fsSync.existsSync(distTemplate)) return distTemplate;
81
+ if (fsSync.existsSync(srcTemplate)) return srcTemplate;
82
+ return distTemplate;
83
+ }
75
84
  function getParser(filePath) {
76
85
  const ext = path2.extname(filePath);
77
86
  const map = {
@@ -148,10 +157,17 @@ async function getAllFiles(dir) {
148
157
  }
149
158
 
150
159
  // src/commands/create.ts
151
- var import_meta = {};
152
- var __filename = (0, import_node_url.fileURLToPath)(import_meta.url);
153
- var __dirname = (0, import_node_path.dirname)(__filename);
154
- var createCommand = new import_commander.Command("extension").description("Create a new MFW extension package").argument("<name>", 'Extension name in kebab-case (e.g., "ad", "blog")').option("-t, --template <name>", "Template to use", "default").option("-d, --dir <path>", "Output directory", "packages/extensions").option("-f, --force", "Force overwrite existing directory", false).action(async (name, opts) => {
160
+ function defaultExtensionAnswers(name, className) {
161
+ return {
162
+ displayName: className,
163
+ description: "",
164
+ routePrefix: `/ext/${name}`,
165
+ hasBackend: true,
166
+ hasFrontend: true,
167
+ hasShared: true
168
+ };
169
+ }
170
+ var createCommand = new import_commander.Command("extension").description("Create a new MFW extension package").argument("<name>", 'Extension name in kebab-case (e.g., "ad", "blog")').option("-t, --template <name>", "Template to use", "default").option("-d, --dir <path>", "Output directory", "packages/extensions").option("-f, --force", "Force overwrite existing directory", false).option("-y, --yes", "Skip prompts and use default values", false).action(async (name, opts) => {
155
171
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
156
172
  console.error(import_chalk.default.red(`Invalid name "${name}". Must be kebab-case.`));
157
173
  process.exit(1);
@@ -165,7 +181,7 @@ Use --force to overwrite.`));
165
181
  process.exit(1);
166
182
  }
167
183
  }
168
- const answers = await import_inquirer.default.prompt([
184
+ const answers = opts.yes ? defaultExtensionAnswers(name, className) : await import_inquirer.default.prompt([
169
185
  { name: "displayName", message: "\u663E\u793A\u540D\u79F0:", default: className },
170
186
  { name: "description", message: "\u63CF\u8FF0:", default: "" },
171
187
  { name: "routePrefix", message: "\u8DEF\u7531\u524D\u7F00:", default: `/ext/${name}` },
@@ -191,7 +207,7 @@ Use --force to overwrite.`));
191
207
  version: "0.1.0",
192
208
  year: (/* @__PURE__ */ new Date()).getFullYear()
193
209
  };
194
- const templateDir = path3.resolve(__dirname, "../templates/extension");
210
+ const templateDir = getTemplateDir("extension");
195
211
  console.log(import_chalk.default.blue(`
196
212
  \u{1F528} Generating extension "${name}" at ${targetDir}...`));
197
213
  await renderTemplateToDir(templateDir, targetDir, vars);
@@ -221,12 +237,15 @@ var import_commander2 = require("commander");
221
237
  var import_inquirer2 = __toESM(require("inquirer"), 1);
222
238
  var import_chalk2 = __toESM(require("chalk"), 1);
223
239
  var path4 = __toESM(require("path"), 1);
224
- var import_node_path2 = require("path");
225
- var import_node_url2 = require("url");
226
- var import_meta2 = {};
227
- var __filename2 = (0, import_node_url2.fileURLToPath)(import_meta2.url);
228
- var __dirname2 = (0, import_node_path2.dirname)(__filename2);
229
- var createBusinessCommand = new import_commander2.Command("business").description("Create a new business project (backend + frontend + shared)").argument("<name>", 'Project name in kebab-case (e.g., "my-shop")').option("-d, --dir <path>", "Output directory", ".").option("-f, --force", "Force overwrite existing directory", false).action(async (name, opts) => {
240
+ function defaultBusinessAnswers(className) {
241
+ return {
242
+ displayName: className,
243
+ description: `${className} \u4E1A\u52A1\u9879\u76EE`,
244
+ port: "3000",
245
+ frontendPort: "5173"
246
+ };
247
+ }
248
+ var createBusinessCommand = new import_commander2.Command("business").description("Create a new business project (backend + frontend + shared)").argument("<name>", 'Project name in kebab-case (e.g., "my-shop")').option("-d, --dir <path>", "Output directory", ".").option("-f, --force", "Force overwrite existing directory", false).option("-y, --yes", "Skip prompts and use default values", false).action(async (name, opts) => {
230
249
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
231
250
  console.error(import_chalk2.default.red(`Invalid name "${name}". Must be kebab-case (e.g., "my-shop").`));
232
251
  process.exit(1);
@@ -240,7 +259,7 @@ Use --force to overwrite.`));
240
259
  process.exit(1);
241
260
  }
242
261
  }
243
- const answers = await import_inquirer2.default.prompt([
262
+ const answers = opts.yes ? defaultBusinessAnswers(className) : await import_inquirer2.default.prompt([
244
263
  {
245
264
  name: "displayName",
246
265
  message: "\u663E\u793A\u540D\u79F0:",
@@ -280,7 +299,7 @@ Use --force to overwrite.`));
280
299
  version: "0.1.0",
281
300
  year: (/* @__PURE__ */ new Date()).getFullYear()
282
301
  };
283
- const templateDir = path4.resolve(__dirname2, "../templates/business");
302
+ const templateDir = getTemplateDir("business");
284
303
  console.log(import_chalk2.default.blue(`
285
304
  \u{1F528} Generating business project "${name}" at ${targetDir}...`));
286
305
  await renderTemplateToDir(templateDir, targetDir, vars);
package/dist/index.js CHANGED
@@ -9,8 +9,6 @@ import { Command } from "commander";
9
9
  import inquirer from "inquirer";
10
10
  import chalk from "chalk";
11
11
  import * as path3 from "path";
12
- import { dirname as dirname2 } from "path";
13
- import { fileURLToPath } from "url";
14
12
  import * as fs3 from "fs/promises";
15
13
 
16
14
  // src/utils/fs.ts
@@ -35,7 +33,9 @@ async function exists(filePath) {
35
33
  // src/utils/template.ts
36
34
  import Handlebars from "handlebars";
37
35
  import * as fs2 from "fs/promises";
36
+ import * as fsSync from "fs";
38
37
  import * as path2 from "path";
38
+ import { fileURLToPath } from "url";
39
39
  Handlebars.registerHelper(
40
40
  "pascalCase",
41
41
  (str) => str.replace(/(^\w|-\w)/g, (c) => c.slice(-1).toUpperCase())
@@ -49,6 +49,14 @@ Handlebars.registerHelper("pascalCaseUpper", (str) => {
49
49
  return pascal.toUpperCase();
50
50
  });
51
51
  Handlebars.registerHelper("snakeCase", (str) => str.replace(/-/g, "_"));
52
+ function getTemplateDir(type) {
53
+ const currentDir = path2.dirname(fileURLToPath(import.meta.url));
54
+ const distTemplate = path2.resolve(currentDir, `templates/${type}`);
55
+ const srcTemplate = path2.resolve(currentDir, `../templates/${type}`);
56
+ if (fsSync.existsSync(distTemplate)) return distTemplate;
57
+ if (fsSync.existsSync(srcTemplate)) return srcTemplate;
58
+ return distTemplate;
59
+ }
52
60
  function getParser(filePath) {
53
61
  const ext = path2.extname(filePath);
54
62
  const map = {
@@ -125,9 +133,17 @@ async function getAllFiles(dir) {
125
133
  }
126
134
 
127
135
  // src/commands/create.ts
128
- var __filename = fileURLToPath(import.meta.url);
129
- var __dirname = dirname2(__filename);
130
- var createCommand = new Command("extension").description("Create a new MFW extension package").argument("<name>", 'Extension name in kebab-case (e.g., "ad", "blog")').option("-t, --template <name>", "Template to use", "default").option("-d, --dir <path>", "Output directory", "packages/extensions").option("-f, --force", "Force overwrite existing directory", false).action(async (name, opts) => {
136
+ function defaultExtensionAnswers(name, className) {
137
+ return {
138
+ displayName: className,
139
+ description: "",
140
+ routePrefix: `/ext/${name}`,
141
+ hasBackend: true,
142
+ hasFrontend: true,
143
+ hasShared: true
144
+ };
145
+ }
146
+ var createCommand = new Command("extension").description("Create a new MFW extension package").argument("<name>", 'Extension name in kebab-case (e.g., "ad", "blog")').option("-t, --template <name>", "Template to use", "default").option("-d, --dir <path>", "Output directory", "packages/extensions").option("-f, --force", "Force overwrite existing directory", false).option("-y, --yes", "Skip prompts and use default values", false).action(async (name, opts) => {
131
147
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
132
148
  console.error(chalk.red(`Invalid name "${name}". Must be kebab-case.`));
133
149
  process.exit(1);
@@ -141,7 +157,7 @@ Use --force to overwrite.`));
141
157
  process.exit(1);
142
158
  }
143
159
  }
144
- const answers = await inquirer.prompt([
160
+ const answers = opts.yes ? defaultExtensionAnswers(name, className) : await inquirer.prompt([
145
161
  { name: "displayName", message: "\u663E\u793A\u540D\u79F0:", default: className },
146
162
  { name: "description", message: "\u63CF\u8FF0:", default: "" },
147
163
  { name: "routePrefix", message: "\u8DEF\u7531\u524D\u7F00:", default: `/ext/${name}` },
@@ -167,7 +183,7 @@ Use --force to overwrite.`));
167
183
  version: "0.1.0",
168
184
  year: (/* @__PURE__ */ new Date()).getFullYear()
169
185
  };
170
- const templateDir = path3.resolve(__dirname, "../templates/extension");
186
+ const templateDir = getTemplateDir("extension");
171
187
  console.log(chalk.blue(`
172
188
  \u{1F528} Generating extension "${name}" at ${targetDir}...`));
173
189
  await renderTemplateToDir(templateDir, targetDir, vars);
@@ -197,11 +213,15 @@ import { Command as Command2 } from "commander";
197
213
  import inquirer2 from "inquirer";
198
214
  import chalk2 from "chalk";
199
215
  import * as path4 from "path";
200
- import { dirname as dirname3 } from "path";
201
- import { fileURLToPath as fileURLToPath2 } from "url";
202
- var __filename2 = fileURLToPath2(import.meta.url);
203
- var __dirname2 = dirname3(__filename2);
204
- var createBusinessCommand = new Command2("business").description("Create a new business project (backend + frontend + shared)").argument("<name>", 'Project name in kebab-case (e.g., "my-shop")').option("-d, --dir <path>", "Output directory", ".").option("-f, --force", "Force overwrite existing directory", false).action(async (name, opts) => {
216
+ function defaultBusinessAnswers(className) {
217
+ return {
218
+ displayName: className,
219
+ description: `${className} \u4E1A\u52A1\u9879\u76EE`,
220
+ port: "3000",
221
+ frontendPort: "5173"
222
+ };
223
+ }
224
+ var createBusinessCommand = new Command2("business").description("Create a new business project (backend + frontend + shared)").argument("<name>", 'Project name in kebab-case (e.g., "my-shop")').option("-d, --dir <path>", "Output directory", ".").option("-f, --force", "Force overwrite existing directory", false).option("-y, --yes", "Skip prompts and use default values", false).action(async (name, opts) => {
205
225
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
206
226
  console.error(chalk2.red(`Invalid name "${name}". Must be kebab-case (e.g., "my-shop").`));
207
227
  process.exit(1);
@@ -215,7 +235,7 @@ Use --force to overwrite.`));
215
235
  process.exit(1);
216
236
  }
217
237
  }
218
- const answers = await inquirer2.prompt([
238
+ const answers = opts.yes ? defaultBusinessAnswers(className) : await inquirer2.prompt([
219
239
  {
220
240
  name: "displayName",
221
241
  message: "\u663E\u793A\u540D\u79F0:",
@@ -255,7 +275,7 @@ Use --force to overwrite.`));
255
275
  version: "0.1.0",
256
276
  year: (/* @__PURE__ */ new Date()).getFullYear()
257
277
  };
258
- const templateDir = path4.resolve(__dirname2, "../templates/business");
278
+ const templateDir = getTemplateDir("business");
259
279
  console.log(chalk2.blue(`
260
280
  \u{1F528} Generating business project "${name}" at ${targetDir}...`));
261
281
  await renderTemplateToDir(templateDir, targetDir, vars);
@@ -0,0 +1,17 @@
1
+ # 数据库配置
2
+ DB_TYPE=mysql
3
+ DB_HOST=localhost
4
+ DB_PORT=3306
5
+ DB_USERNAME=root
6
+ DB_PASSWORD=root
7
+ DB_NAME={{snakeCase name}}
8
+
9
+ # JWT 配置
10
+ JWT_SECRET=your-jwt-secret-change-in-production
11
+ JWT_EXPIRES_IN=7d
12
+
13
+ # 服务端口
14
+ PORT={{port}}
15
+
16
+ # 运行环境
17
+ NODE_ENV=development
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true,
7
+ "webpack": false,
8
+ "tsConfigPath": "tsconfig.json"
9
+ }
10
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "{{name}}-backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "prebuild": "pnpm --filter {{name}}-shared build",
7
+ "dev": "pnpm run prebuild && nest start --watch",
8
+ "build": "nest build",
9
+ "typecheck": "tsc --noEmit -p tsconfig.json",
10
+ "start": "node dist/main",
11
+ "start:prod": "node dist/main"
12
+ },
13
+ "dependencies": {
14
+ "@nestjs/common": "catalog:",
15
+ "@nestjs/core": "catalog:",
16
+ "@nestjs/platform-express": "catalog:",
17
+ "@nestjs/typeorm": "catalog:",
18
+ "@nestjs/swagger": "catalog:",
19
+ "typeorm": "catalog:",
20
+ "class-transformer": "catalog:",
21
+ "class-validator": "catalog:",
22
+ "reflect-metadata": "catalog:",
23
+ "rxjs": "catalog:",
24
+ "mysql2": "catalog:",
25
+ "dotenv": "catalog:",
26
+ "moyan-mfw-base": "^1.0.0",
27
+ "moyan-mfw-extension-ad": "^0.1.0",
28
+ "{{name}}-shared": "workspace:*"
29
+ },
30
+ "devDependencies": {
31
+ "@nestjs/cli": "catalog:",
32
+ "@nestjs/schematics": "catalog:",
33
+ "typescript": "catalog:",
34
+ "ts-node": "catalog:",
35
+ "tsconfig-paths": "catalog:"
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ import { AppTypeConfig } from 'moyan-mfw-base/backend'
2
+
3
+ export const appTypesConfig: AppTypeConfig[] = []
@@ -0,0 +1,7 @@
1
+ import { Module } from '@nestjs/common'
2
+ import { AdModule } from 'moyan-mfw-extension-ad/backend'
3
+
4
+ @Module({
5
+ imports: [AdModule],
6
+ })
7
+ export class AppModule {}
@@ -0,0 +1,46 @@
1
+ import 'dotenv/config'
2
+ import { createBaseBackendApp, SwaggerGroupConfig } from 'moyan-mfw-base/backend'
3
+ import { appTypesConfig } from './app-types.config'
4
+ import { AppModule } from './app.modules'
5
+ import { AdModule, AD_EXTENSION_PERMISSION_VALUES } from 'moyan-mfw-extension-ad/backend'
6
+
7
+ const swaggerGroups: SwaggerGroupConfig[] = [
8
+ {
9
+ name: 'ad-extension',
10
+ title: '广告管理API文档',
11
+ description: '广告位类型、广告位、广告内容管理 API',
12
+ include: [AdModule],
13
+ },
14
+ ]
15
+
16
+ async function bootstrap() {
17
+ const startTime = Date.now()
18
+ console.log('[{{displayName}}] ====== Bootstrap Start ======')
19
+
20
+ try {
21
+ const app = await createBaseBackendApp({
22
+ name: '{{displayName}}',
23
+ appTypes: appTypesConfig,
24
+ syncAppTypes: true,
25
+ modules: [AppModule],
26
+ swagger: swaggerGroups,
27
+ permissionValues: [...AD_EXTENSION_PERMISSION_VALUES],
28
+ hooks: {
29
+ onAppInit: async (ctx) => {
30
+ console.log('[{{displayName}}] 应用初始化完成')
31
+ },
32
+ },
33
+ })
34
+
35
+ const port = Number(process.env.PORT) || {{port}}
36
+ await app.listen(port)
37
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2)
38
+ console.log(`\n[{{displayName}}] ✅ Server listening on http://localhost:${port} (${elapsed}s)`)
39
+ console.log(`[{{displayName}}] 📖 Swagger: http://localhost:${port}/api-docs/sys`)
40
+ } catch (error) {
41
+ console.error('\n[{{displayName}}] ❌ Bootstrap failed:', error)
42
+ process.exit(1)
43
+ }
44
+ }
45
+
46
+ bootstrap()
@@ -0,0 +1,4 @@
1
+ import { createBusinessPermissionDecorator } from 'moyan-mfw-base/backend'
2
+ import { {{pascalCaseUpper name}}_PERMISSION_VALUES } from '{{name}}-shared'
3
+
4
+ export const Permission = createBusinessPermissionDecorator({{pascalCaseUpper name}}_PERMISSION_VALUES)
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "strictPropertyInitialization": false,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "sourceMap": true,
11
+ "baseUrl": ".",
12
+ "paths": {
13
+ "@/*": ["./src/*"]
14
+ }
15
+ },
16
+ "include": ["src/**/*.ts"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{displayName}}</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ #loading-placeholder {
10
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
11
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
12
+ background: linear-gradient(135deg, #ffffff 0%, #f8fafc 50%, #ffffff 100%);
13
+ z-index: 9999; transition: opacity 0.4s ease;
14
+ }
15
+ #loading-placeholder.hidden { opacity: 0; pointer-events: none; }
16
+ .loader {
17
+ width: 48px; height: 48px;
18
+ border: 4px solid #e5e7eb;
19
+ border-top-color: #3b82f6;
20
+ border-radius: 50%;
21
+ animation: spin 0.8s linear infinite;
22
+ }
23
+ @keyframes spin { to { transform: rotate(360deg); } }
24
+ .loading-text { margin-top: 16px; font-family: system-ui, sans-serif; font-size: 14px; color: #6b7280; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <div id="app"></div>
29
+ <div id="loading-placeholder">
30
+ <div class="loader"></div>
31
+ <div class="loading-text">正在加载...</div>
32
+ </div>
33
+ <script type="module" src="/src/main.ts"></script>
34
+ </body>
35
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{name}}-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --config vite.config.ts",
8
+ "build": "vue-tsc --noEmit -p tsconfig.json && vite build --config vite.config.ts",
9
+ "typecheck:vue": "vue-tsc --noEmit -p tsconfig.json",
10
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json"
11
+ },
12
+ "dependencies": {
13
+ "vue": "catalog:",
14
+ "vue-router": "catalog:",
15
+ "element-plus": "catalog:",
16
+ "@element-plus/icons-vue": "catalog:",
17
+ "pinia": "catalog:",
18
+ "@vueuse/core": "catalog:",
19
+ "axios": "catalog:",
20
+ "moyan-mfw-base": "^1.0.0",
21
+ "moyan-mfw-extension-ad": "^0.1.0",
22
+ "{{name}}-shared": "workspace:*"
23
+ },
24
+ "devDependencies": {
25
+ "vite": "catalog:",
26
+ "@vitejs/plugin-vue": "catalog:",
27
+ "@vitejs/plugin-vue-jsx": "catalog:",
28
+ "vue-tsc": "catalog:",
29
+ "sass": "catalog:",
30
+ "typescript": "catalog:"
31
+ }
32
+ }
@@ -0,0 +1,28 @@
1
+ import { createBaseAdminApp, registerPermissionValues } from 'moyan-mfw-base/frontend'
2
+ import { businessRoutes } from './router'
3
+ import { adRoutes } from 'moyan-mfw-extension-ad/frontend'
4
+ import { AD_EXTENSION_PERMISSION_VALUES } from 'moyan-mfw-extension-ad/shared'
5
+ import './permissions'
6
+
7
+ registerPermissionValues([...AD_EXTENSION_PERMISSION_VALUES])
8
+
9
+ const admin = createBaseAdminApp({
10
+ title: '{{displayName}}',
11
+ routes: [...businessRoutes, ...adRoutes],
12
+ layout: {
13
+ layoutMode: 'dual',
14
+ showTabs: true,
15
+ colorMode: 'system',
16
+ themePackage: 'default',
17
+ },
18
+ navigation: {
19
+ brandName: '{{displayName}}',
20
+ brandTagline: '{{description}}',
21
+ homePath: '/dashboard',
22
+ },
23
+ })
24
+
25
+ const values = await admin.fetchPermissionValues()
26
+ admin.initPermissionCache(values)
27
+
28
+ await admin.mount('#app')
@@ -0,0 +1,4 @@
1
+ import { createBusinessPageConfigFn } from 'moyan-mfw-base/frontend'
2
+ import { {{pascalCaseUpper name}}_PERMISSION_VALUES } from '{{name}}-shared'
3
+
4
+ export const defineBusinessPageConfig = createBusinessPageConfigFn({{pascalCaseUpper name}}_PERMISSION_VALUES)
@@ -0,0 +1,9 @@
1
+ import type { RouteRecordRaw } from 'vue-router'
2
+ import { buildRoutesFromConfigs } from 'moyan-mfw-base/frontend'
3
+
4
+ const allConfigs = import.meta.glob('./views/**/index.{ts,tsx}', {
5
+ eager: true,
6
+ import: 'default',
7
+ }) as Record<string, unknown>
8
+
9
+ export const businessRoutes: RouteRecordRaw[] = buildRoutesFromConfigs(allConfigs, { minSegments: 1 })