@ynight/framework 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +511 -2
- package/assets/.gitkeep +0 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/assets/index.d.ts +41 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +68 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/client/common.d.ts +77 -0
- package/dist/client/common.d.ts.map +1 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.mjs +8 -0
- package/dist/client/index.mjs.LICENSE.txt +8 -0
- package/dist/client/layout/Layout.vue.d.ts +21 -0
- package/dist/client/layout/Layout.vue.d.ts.map +1 -0
- package/dist/client/layout/SiderLayout.vue.d.ts +32 -0
- package/dist/client/layout/SiderLayout.vue.d.ts.map +1 -0
- package/dist/client/layout/SubMenu.vue.d.ts +8 -0
- package/dist/client/layout/SubMenu.vue.d.ts.map +1 -0
- package/dist/client/package.json +3 -0
- package/dist/client/pages/DashboardPage.vue.d.ts +4 -0
- package/dist/client/pages/DashboardPage.vue.d.ts.map +1 -0
- package/dist/client/pages/ErrorPage.vue.d.ts +9 -0
- package/dist/client/pages/ErrorPage.vue.d.ts.map +1 -0
- package/dist/client/pages/PageSkeleton.vue.d.ts +4 -0
- package/dist/client/pages/PageSkeleton.vue.d.ts.map +1 -0
- package/dist/client/pages/ProjectListPage.vue.d.ts +4 -0
- package/dist/client/pages/ProjectListPage.vue.d.ts.map +1 -0
- package/dist/client/pages/project-navigation.d.ts +21 -0
- package/dist/client/pages/project-navigation.d.ts.map +1 -0
- package/dist/client/public-components.d.ts +56 -0
- package/dist/client/public-components.d.ts.map +1 -0
- package/dist/client/runtime.d.ts +55 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/client/schema-types.d.ts +129 -0
- package/dist/client/schema-types.d.ts.map +1 -0
- package/dist/client/schema.d.ts +105 -0
- package/dist/client/schema.d.ts.map +1 -0
- package/dist/client/style.css +147 -0
- package/dist/client/types.d.ts +195 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/views/ModuleHost.vue.d.ts +8 -0
- package/dist/client/views/ModuleHost.vue.d.ts.map +1 -0
- package/dist/client/views/SchemaView.vue.d.ts +8 -0
- package/dist/client/views/SchemaView.vue.d.ts.map +1 -0
- package/dist/client/views/schema/create-form.vue.d.ts +13 -0
- package/dist/client/views/schema/create-form.vue.d.ts.map +1 -0
- package/dist/client/views/schema/detail-view.vue.d.ts +10 -0
- package/dist/client/views/schema/detail-view.vue.d.ts.map +1 -0
- package/dist/client/views/schema/search-panel.vue.d.ts +11 -0
- package/dist/client/views/schema/search-panel.vue.d.ts.map +1 -0
- package/dist/client/views/schema/table-panel.vue.d.ts +17 -0
- package/dist/client/views/schema/table-panel.vue.d.ts.map +1 -0
- package/dist/client/views/schema/update-form.vue.d.ts +14 -0
- package/dist/client/views/schema/update-form.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/SchemaField.vue.d.ts +18 -0
- package/dist/client/widgets/schema-form/SchemaField.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/complex-view/index.d.ts +11 -0
- package/dist/client/widgets/schema-form/complex-view/index.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/complex-view/schema-async-search-select.vue.d.ts +39 -0
- package/dist/client/widgets/schema-form/complex-view/schema-async-search-select.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/complex-view/schema-dynamic-select.vue.d.ts +32 -0
- package/dist/client/widgets/schema-form/complex-view/schema-dynamic-select.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/complex-view/schema-tag-input.vue.d.ts +25 -0
- package/dist/client/widgets/schema-form/complex-view/schema-tag-input.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/complex-view/schema-upload.vue.d.ts +16 -0
- package/dist/client/widgets/schema-form/complex-view/schema-upload.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/hooks/use-schema-rules.d.ts +45 -0
- package/dist/client/widgets/schema-form/hooks/use-schema-rules.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/index.vue.d.ts +26 -0
- package/dist/client/widgets/schema-form/index.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/type.d.ts +295 -0
- package/dist/client/widgets/schema-form/type.d.ts.map +1 -0
- package/dist/client/widgets/schema-form/validator.d.ts +127 -0
- package/dist/client/widgets/schema-form/validator.d.ts.map +1 -0
- package/dist/client/widgets/schema-table/index.vue.d.ts +9 -0
- package/dist/client/widgets/schema-table/index.vue.d.ts.map +1 -0
- package/dist/client/widgets/schema-table/type.d.ts +91 -0
- package/dist/client/widgets/schema-table/type.d.ts.map +1 -0
- package/dist/model/index.d.ts +75 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +108 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model.d.ts +13 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +16 -0
- package/dist/model.js.map +1 -0
- package/dist/server/application.d.ts +33 -0
- package/dist/server/application.d.ts.map +1 -0
- package/dist/server/application.js +1082 -0
- package/dist/server/application.js.map +1 -0
- package/dist/server/base-class.d.ts +50 -0
- package/dist/server/base-class.d.ts.map +1 -0
- package/dist/server/base-class.js +66 -0
- package/dist/server/base-class.js.map +1 -0
- package/dist/server/config.d.ts +44 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +231 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/context.d.ts +30 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +3 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/define-routes.d.ts +29 -0
- package/dist/server/define-routes.d.ts.map +1 -0
- package/dist/server/define-routes.js +39 -0
- package/dist/server/define-routes.js.map +1 -0
- package/dist/server/define-server.d.ts +19 -0
- package/dist/server/define-server.d.ts.map +1 -0
- package/dist/server/define-server.js +23 -0
- package/dist/server/define-server.js.map +1 -0
- package/dist/server/index.d.ts +26 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +35 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/legacy.d.ts +13 -0
- package/dist/server/legacy.d.ts.map +1 -0
- package/dist/server/legacy.js +15 -0
- package/dist/server/legacy.js.map +1 -0
- package/dist/server/types.d.ts +272 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -0
- package/package.json +106 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 diaogroup
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,512 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ynight/framework
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 框架主入口——把服务端装配(Koa)、客户端运行时(Vue 3 + Element Plus + Pinia)、模型 DSL(defineModel / defineControlContracts)和内置静态资源整合在同一个包中。
|
|
4
|
+
|
|
5
|
+
`@ynight/framework` 是 Elpis 的统一运行时包。应用开发者主要依赖此包:服务端用 `@ynight/framework`(或 `@ynight/framework/server`)启动 Koa 应用、装配业务模块;客户端用 `@ynight/framework/client` 启动内置后台;模型层用 `@ynight/framework/model` 声明菜单 / 模块 / 自定义控件契约。
|
|
6
|
+
|
|
7
|
+
底层能力来自 `@ynight/core`(Koa 容器与生命周期)、`@ynight/contracts`(DSL 类型契约)与 `@ynight/model-runtime`(模型加载与校验)。
|
|
8
|
+
|
|
9
|
+
## 子导出(Sub-path Exports)
|
|
10
|
+
|
|
11
|
+
| 子路径 | 角色 | 主要内容 |
|
|
12
|
+
| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `@ynight/framework`(默认) | 服务端装配入口 | `createApplication`、`startApplication`、`defineServer`、`BaseService`、`BaseController`、类型集合 |
|
|
14
|
+
| `@ynight/framework/server` | 同上(显式路径) | 与默认入口导出完全一致,便于需要明确语义的位置 |
|
|
15
|
+
| `@ynight/framework/model` | 模型 / 菜单 DSL | `defineModel`、`defineControlContracts`、`custom` / `iframe` / `group` / `sider` / `schema` 等菜单工厂;契约 / DTO 类型 |
|
|
16
|
+
| `@ynight/framework/client` | 内置后台客户端 | `createAdminApp`、`defineClientExtensions`、`SchemaForm`、`SchemaTable`、schema 解析工具 |
|
|
17
|
+
| `@ynight/framework/assets` | 框架静态资源 | `frameworkPageNames`、`readFrameworkPage`、`frameworkAssetsRoot` |
|
|
18
|
+
| `@ynight/framework/style.css` | 客户端样式 | 客户端运行时需要的样式(含 Element Plus 的扩展) |
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @ynight/framework element-plus pinia vue vue-router
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> `@ynight/framework` 在 `peerDependencies` 里显式声明 `element-plus ^2.13.1`、`pinia ^3.0.4`、`vue ^3.5.26`、`vue-router ^4.6.4`,需要在宿主应用中安装这些包(按版本范围匹配)。
|
|
27
|
+
>
|
|
28
|
+
> 包内部还依赖 `ajv` / `ajv-formats` / `ajv-i18n` / `koa` / `koa-bodyparser` / `koa-static` / `lodash` 等运行时依赖,以及 `@ynight/contracts`、`@ynight/core`、`@ynight/model-runtime` 三个工作空间内部包。
|
|
29
|
+
|
|
30
|
+
要求 Node.js `>=22.0.0`。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 服务端(`@ynight/framework` 或 `@ynight/framework/server`)
|
|
35
|
+
|
|
36
|
+
服务端入口基于 Koa + `@koa/router`,把"业务模块扫描 → 模型加载 → 路由校验 → 鉴权 → 静态资源 → History 兜底"装配为一个托管应用。
|
|
37
|
+
|
|
38
|
+
### 核心 API
|
|
39
|
+
|
|
40
|
+
| API | 说明 |
|
|
41
|
+
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `createApplication(context)` | 创建已装配的 Koa 应用,不监听端口;返回 `ManagedApplication`(含 `app / listen / close`) |
|
|
43
|
+
| `startApplication(context)` | 一键装配并按 `production` 模式启动(即使环境变量是 development 也强制为 production,因为这是真正对外监听的场景) |
|
|
44
|
+
| `defineServer(setup)` | 类型断言助手:声明 `ServerExtension`(auth / devAnonymous / onReady / onClose)并享受完整类型提示 |
|
|
45
|
+
| `defineRoutes(setup)` | 类型断言助手:声明 `RouteFactory`,返回值可为 `RouteDefinition[]` 或 `undefined`(Proxy 模式) |
|
|
46
|
+
| `defineConfig(config)` | 类型断言助手:声明 `ElpisConfig` 对象或基于 mode 的工厂 |
|
|
47
|
+
| `BaseService` / `BaseController` | 业务基类(构造期注入 `app`;控制器提供 `success` / `fail` 统一响应封装) |
|
|
48
|
+
| `resolveConfig(rootDir, mode, input?)` | 解析 `elpis.config` + 环境变量 + 默认值,返回 `{ config, paths }` |
|
|
49
|
+
| `resolveApplicationPaths(rootDir, config?, mode?, publicRootOverride?)` | 计算并校验所有生成路径(带"必须在根目录内"、"禁止覆盖受保护目录"等安全校验) |
|
|
50
|
+
| `createLegacyApplication` | 旧版迁移桥接(直接透传 `@ynight/core` 的 `createApplication`),仅供存量项目使用 |
|
|
51
|
+
|
|
52
|
+
### 最小示例
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// entry.ts
|
|
56
|
+
import { createApplication, defineServer } from '@ynight/framework';
|
|
57
|
+
|
|
58
|
+
const app = await createApplication({
|
|
59
|
+
rootDir: __dirname,
|
|
60
|
+
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
61
|
+
moduleExtension: '.js', // CLI 编译后业务模块的扩展名
|
|
62
|
+
server: defineServer({
|
|
63
|
+
auth: myAuthAdapter,
|
|
64
|
+
devAnonymous: true, // 仅非生产生效
|
|
65
|
+
onReady(app) {
|
|
66
|
+
// 可继续 app.use() 注册额外中间件或探针
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await app.listen({ host: '0.0.0.0', port: 3000 });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `createApplication` 装配顺序(严格按阶段执行)
|
|
75
|
+
|
|
76
|
+
`createApplication` 在一个 try/catch 中按以下顺序装配;任一阶段抛错都会在 finally 中尝试关闭已启动容器并抛出原错误。
|
|
77
|
+
|
|
78
|
+
1. **解析配置与路径**:调用 `resolveConfig(rootDir, mode, context.config)`;当传入 `moduleRoot` 时,重新拼接 `serverRoot / modelRoot / sharedRoot`,便于 CLI 在 `dist` 中加载业务模块。
|
|
79
|
+
2. **预热框架内置页面**:通过 `frameworkPageNames.forEach(p => readFrameworkPage(...))` 把 `project-list / dashboard / error` 三个 HTML 模板读入缓存,保证后续路由兜底能同步返回 HTML。
|
|
80
|
+
3. **加载并校验业务模型**:
|
|
81
|
+
- `loadModels({ modelDir, extension })` 加载 `model/` 目录下所有模块;
|
|
82
|
+
- 对每个项目检查 `mode === 'site'` 直接抛错(site 模板尚未实现,启动期拒止,避免运行时半成品);
|
|
83
|
+
- `validateModels(models, { controlContracts })` 校验模型(控件契约 JSON Schema);
|
|
84
|
+
- 通过 `loadControlContracts` 装载 `model.controlContracts` 索引文件(不存在则为空对象,配置缺失时抛 `ERR_CONTROL_CONTRACT_MISSING`)。
|
|
85
|
+
4. **创建 Koa 容器**:通过 `createApplicationContainer` 装配 host/port/closeTimeoutMs,并通过 `validateListen` 钩子确保 `devAnonymous` 仅监听回环地址(`127.0.0.1 / localhost / ::1 / 0:0:0:0:0:0:0:1`)。
|
|
86
|
+
5. **注入框架扩展字段**:用 `Object.defineProperties` 把 `config / paths / mode / framework / routerSchema / serverExtension` 设为只读;`service / controller / middlewares` 在装载阶段写入,标记为 writable。
|
|
87
|
+
6. **注册全局错误兜底 + bodyParser + 认证/匿名中间件**:
|
|
88
|
+
- 顶层 try/catch 中间件:捕获下游所有抛错,统一输出 `{ success, code, message }` 结构(`/api/*` 路径)或 `text/plain` 兜底(其它路径),并触发 `app.emit('error', error, ctx)`;
|
|
89
|
+
- `bodyParser`(启用 json/form/text 三种类型);
|
|
90
|
+
- 认证:调用 `serverExtension.auth.authenticate(ctx)`;未登录时回退到 `devAnonymous`(仅非生产)或返回 401 + `WWW-Authenticate` 头。
|
|
91
|
+
7. **业务装载**:`server/service/` 与 `server/controller/` 通过 `loadClassModules` 自动实例化(构造期注入 `app`),`server/middleware/` 下的工厂被依次 `app.use(...)`。
|
|
92
|
+
8. **注册框架内置路由**(业务路由与之冲突会立即报错):
|
|
93
|
+
- `GET /api/session`——当前登录身份 + 公共配置;
|
|
94
|
+
- `GET /api/project/model_list`——按模型聚合的可见项目列表;
|
|
95
|
+
- `GET /api/project/list`——扁平项目列表(支持 `?modelKey=...` 过滤);
|
|
96
|
+
- `GET /api/project`——单个项目详情(按 `?projectKey=...` 过滤 + 菜单授权过滤)。
|
|
97
|
+
9. **注册业务路由 + 参数校验**:
|
|
98
|
+
- 业务路由文件支持两种声明方式:
|
|
99
|
+
- **Proxy 模式**:`export default (app, router) => { router.get('/api/foo', handler); router.post('/api/foo', handler); }`;
|
|
100
|
+
- **声明式**:`export default defineRoutes(app => [{ method, path, handler, access?, schema?, projectRequired? }])`;
|
|
101
|
+
- 路由注册按"路由级 access → attachProject(写入 ctx.state.projectKey)→ 内联 schema 或集中 schema → handler"装配;
|
|
102
|
+
- 装配完成后调用 `assertNoOrphanSchemas`,确保 `router-schema/` 目录下每条校验规则都对应一个实际路由(避免定义却未使用的死代码)。
|
|
103
|
+
10. **静态资源 + History 兜底**:
|
|
104
|
+
- `koaStatic(publicRoot)` 之前先做两道护栏:`/api/*` 永远走接口(含同名静态文件也不能劫持);含扩展名的静态业务资源会经过 `module=public, action=read` 授权;
|
|
105
|
+
- 兜底中间件:`/api/*` 未匹配路由返回 404 结构化响应;非 API + 404 + GET/HEAD 走 `pageForPath` 命中 `project-list / dashboard / error` 任一页面,否则尝试 `redirectCustomPageShortcut`(自定义菜单短链重定向),最终 404 兜底。
|
|
106
|
+
11. **`onReady` 钩子**:所有装配完成后调用 `serverExtension.onReady?.(app)`。
|
|
107
|
+
|
|
108
|
+
### `ApplicationContext`
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
interface ApplicationContext {
|
|
112
|
+
rootDir: string; // 项目根目录(必填)
|
|
113
|
+
sourceRoot?: string; // 配置所在源码根,未传则等于 rootDir
|
|
114
|
+
moduleRoot?: string; // CLI 编译产物根;提供后从该目录加载 server/model
|
|
115
|
+
mode?: ElpisMode; // 覆盖 NODE_ENV 推断
|
|
116
|
+
moduleExtension?: '.ts' | '.js' | '.cjs'; // 业务模块扩展名;未传则按目录推断
|
|
117
|
+
config?: ConfigInput; // ElpisConfig 或基于 mode 的工厂函数
|
|
118
|
+
server?: ServerExtension; // 认证 / 钩子
|
|
119
|
+
publicRoot?: string; // 覆盖默认的 ./public 静态资源目录
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `ApplicationContext` 装配出的 `FrameworkApplication`
|
|
124
|
+
|
|
125
|
+
`createApplication` 通过 `Object.defineProperties` 在 Koa 实例上追加以下只读 / 可写字段:
|
|
126
|
+
|
|
127
|
+
| 字段 | 读写性 | 说明 |
|
|
128
|
+
| ---------------------------------- | -------- | --------------------------------------------------------- |
|
|
129
|
+
| `config: ResolvedElpisConfig` | 只读 | 解析后的运行时配置(含默认值 + 环境变量覆盖) |
|
|
130
|
+
| `paths: ApplicationPaths` | 只读 | 所有目录的绝对路径集合 |
|
|
131
|
+
| `mode: ElpisMode` | 只读 | 当前运行模式 |
|
|
132
|
+
| `framework: FrameworkServices` | 只读 | `{ models, routerSchemas, controlContracts }` |
|
|
133
|
+
| `routerSchema: RouterSchemas` | 只读 | 集中维护的请求校验规则 |
|
|
134
|
+
| `serverExtension: ServerExtension` | 只读 | 业务注入的扩展(auth / devAnonymous / onReady / onClose) |
|
|
135
|
+
| `service: BusinessServices` | writable | 业务 service 索引(key 为文件名) |
|
|
136
|
+
| `controller: BusinessControllers` | writable | 业务 controller 索引 |
|
|
137
|
+
| `middlewares: BusinessMiddlewares` | writable | 业务 middleware 索引(中间件已挂载,这里主要用于可见性) |
|
|
138
|
+
|
|
139
|
+
### 业务模块约定
|
|
140
|
+
|
|
141
|
+
业务模块按"约定优于配置"的方式被框架扫描:
|
|
142
|
+
|
|
143
|
+
| 目录 | 导出 | 说明 |
|
|
144
|
+
| -------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
|
|
145
|
+
| `server/controller/<name>.ts` | `export default class extends BaseController` | 构造期接收 `app`;实例被索引到 `app.controller[name]` |
|
|
146
|
+
| `server/service/<name>.ts` | `export default class extends BaseService` | 同上;实例被索引到 `app.service[name]` |
|
|
147
|
+
| `server/middleware/<name>.ts` | `export default (app) => Middleware` | 工厂在装配期被调用,返回值依次 `app.use()` |
|
|
148
|
+
| `server/router/<name>.ts` | `export default (app, router) => void \| RouteDefinition[]` | 推荐 Proxy 模式;声明式用 `defineRoutes` |
|
|
149
|
+
| `server/router-schema/<name>.ts` | `export default { '/api/xxx': { get: { query: {...} } } }` | 集中维护的请求参数校验 |
|
|
150
|
+
| `model/<name>.ts` | `defineModel({ ... })` 或 `defineControlContracts({ ... })` | 由 `@ynight/framework/model` 提供工厂 |
|
|
151
|
+
|
|
152
|
+
### 路由校验(schema)
|
|
153
|
+
|
|
154
|
+
框架提供两种校验来源:
|
|
155
|
+
|
|
156
|
+
1. **路由内联 schema**:在 `RouteDefinition.schema` 字段直接写 `body/query/params/headers` 四位置的 JSON Schema;
|
|
157
|
+
2. **集中校验规则**:`server/router-schema/<name>.ts` 默认导出 `{ '/api/path': { get: { query: ... }, post: { body: ... } } }`,按路由 + HTTP 方法匹配。
|
|
158
|
+
|
|
159
|
+
校验器基于 Ajv(`allErrors` + `coerceTypes` + `strict`)并注入 `ajv-formats`。校验顺序为 `body → query → params → headers`,任一位置失败立即返回:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"success": false,
|
|
164
|
+
"code": 400,
|
|
165
|
+
"message": "参数校验失败: body.field.x [required] 必须有 property \"x\""
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
启动期还会做"孤儿 schema"检查:集中规则里出现但没有任何业务路由对应的方法会在 `createApplication` 阶段直接抛错,避免"定义却未使用"的死代码。
|
|
170
|
+
|
|
171
|
+
### 认证与授权
|
|
172
|
+
|
|
173
|
+
`ServerExtension.auth` 是可选的 `AuthAdapter`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface AuthAdapter {
|
|
177
|
+
challenge?: string; // 401 响应的 WWW-Authenticate 头
|
|
178
|
+
authenticate(ctx): AuthIdentity | null | Promise<AuthIdentity | null>;
|
|
179
|
+
authorize(ctx, access, identity): boolean | Promise<boolean>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
框架在以下四层做授权判定:
|
|
184
|
+
|
|
185
|
+
1. **路由级 access**:在 `RouteDefinition.access` 或 `RouteDefinition.projectRequired` 中声明;命中失败直接 403/400;
|
|
186
|
+
2. **模块级 access**:业务路由按"`{ module: <routePath>, action: <method> }`"调用 `authorize`;
|
|
187
|
+
3. **项目级 access**:内部路由 `/api/project/*`、菜单可见性、项目详情都通过 `isProjectAllowed` 判定;
|
|
188
|
+
4. **静态资源 access**:`public` 目录下的含扩展名静态资源也会经过 `module=public, action=read` 的授权。
|
|
189
|
+
|
|
190
|
+
`devAnonymous` 字段:
|
|
191
|
+
|
|
192
|
+
- 仅在非生产(`development` / `test`)生效;
|
|
193
|
+
- 必须显式开启(`!auth && !devAnonymous` 时 `createApplication` 直接抛错,防止"忘记关"导致开发配置漏到测试);
|
|
194
|
+
- 启用时 `validateListen` 会强制 host 必须是回环地址(`127.0.0.1 / localhost / ::1`)。
|
|
195
|
+
|
|
196
|
+
### `BaseService` / `BaseController`
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
class ProductService extends BaseService {
|
|
200
|
+
async list(query: { id: string }) {
|
|
201
|
+
return this.app.service.otherService.someMethod(query);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class ProductController extends BaseController {
|
|
206
|
+
async list(ctx) {
|
|
207
|
+
try {
|
|
208
|
+
const data = await this.app.service.product.list(ctx.query);
|
|
209
|
+
this.success(ctx, data);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.fail(ctx, error.message, 500);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`BaseController.success(ctx, data, metadata?)` 与 `BaseController.fail(ctx, message, code?, errors?)` 输出统一的 `{ success, data/code/message, metadata?/errors? }` 协议,便于前端统一处理。
|
|
218
|
+
|
|
219
|
+
### 静态资源与 History 兜底
|
|
220
|
+
|
|
221
|
+
- `/api/*` 路径永远不会被静态文件劫持;
|
|
222
|
+
- 含扩展名的静态业务资源也走授权(`module=public, action=read`);
|
|
223
|
+
- 无扩展名的路径会落到框架内置 HTML 模板(History 兜底):`/project-list`、`/dashboard*`、`/error`;
|
|
224
|
+
- `custom` 模块还支持"短链重定向":直接访问原始 `moduleConfig.path` 时,框架自动重定向到带 `projectKey/key/siderKey` query 的 dashboard 形式。
|
|
225
|
+
|
|
226
|
+
### 路由定义示例(声明式)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// server/router/product.ts
|
|
230
|
+
import { defineRoutes } from '@ynight/framework';
|
|
231
|
+
|
|
232
|
+
export default defineRoutes(app => [
|
|
233
|
+
{
|
|
234
|
+
method: 'GET',
|
|
235
|
+
path: '/api/product',
|
|
236
|
+
projectRequired: true,
|
|
237
|
+
access: { module: '/api/product', action: 'read' },
|
|
238
|
+
schema: {
|
|
239
|
+
query: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: { page: { type: 'integer', minimum: 1 } },
|
|
242
|
+
required: ['page'],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
handler(ctx) {
|
|
246
|
+
return app.service.product.list(ctx.query);
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 路由定义示例(Proxy 模式)
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// server/router/order.ts
|
|
256
|
+
export default (app, router) => {
|
|
257
|
+
router.get('/api/order', async ctx => {
|
|
258
|
+
ctx.body = { success: true, data: await app.service.order.list(ctx.query) };
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
router.post('/api/order', async ctx => {
|
|
262
|
+
ctx.body = { success: true, data: await app.service.order.create(ctx.request.body) };
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 模型 / 菜单 DSL(`@ynight/framework/model`)
|
|
270
|
+
|
|
271
|
+
模型入口只导出 DSL、查询工具与 DTO 类型,避免业务侧误用到服务端装配 API。模块入口:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
export * from './model/index';
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`src/model/index.ts` 主要导出:
|
|
278
|
+
|
|
279
|
+
- **工厂函数**:`defineModel`、`defineControlContracts`、`custom`、`iframe`、`group`、`sider`、`schema`;
|
|
280
|
+
- **透传自 `@ynight/contracts` 的工具与常量**:`EventKey`、`MenuType`、`ModuleType`、`SchemaComponentName`、`SchemaFormControlType`、`transQueryToString`;
|
|
281
|
+
- **类型**:`BusinessControls`、`IModel`、`MenuItemType`、`ModuleItem`、`SiderModuleConfig`、`PathModuleConfig`、`ButtonConfig`、`TableColumnConfig`、`CreateFormConfig`、`UpdateFormConfig`、`DetailViewConfig`、`SchemaProperty`、`SchemaModuleConfig`、`SchemaFormFieldConfig`、`DynamicSelectConfig`、`AsyncSearchSelectConfig`、`TagInputConfig`、`ChoiceOption`、`CascaderOption`、`ProjectDto`、`ModelDto` 等。
|
|
282
|
+
|
|
283
|
+
### `defineModel`
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { defineModel, schema } from '@ynight/framework/model';
|
|
287
|
+
|
|
288
|
+
export default defineModel({
|
|
289
|
+
key: 'admin',
|
|
290
|
+
name: '管理后台',
|
|
291
|
+
desc: '示例模型',
|
|
292
|
+
project: {
|
|
293
|
+
'demo-project': {
|
|
294
|
+
key: 'demo-project',
|
|
295
|
+
name: '示例项目',
|
|
296
|
+
desc: '示例项目',
|
|
297
|
+
homePage: '/dashboard',
|
|
298
|
+
menu: [
|
|
299
|
+
schema('user-list', '用户列表', {
|
|
300
|
+
api: '/api/user',
|
|
301
|
+
schema: { type: 'object', properties: { id: { type: 'string' } } },
|
|
302
|
+
tableConfig: { ... },
|
|
303
|
+
searchConfig: { ... },
|
|
304
|
+
components: { CreateForm: {...}, UpdateForm: {...}, DetailView: {...} },
|
|
305
|
+
}),
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`defineModel` 仅做类型断言(identity),让业务侧在写模型字面量时获得完整字段补全;通过可选的 `__controls` 把业务自定义控件类型透传给后续钩子,配合 `defineControlContracts` 可以让 `schema` 字段中的 `control` 类型受 `IModel` 推断约束。
|
|
313
|
+
|
|
314
|
+
### `defineControlContracts`
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { defineControlContracts } from '@ynight/framework/model';
|
|
318
|
+
|
|
319
|
+
export default defineControlContracts({
|
|
320
|
+
AsyncSearchSelect: {
|
|
321
|
+
propsSchema: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
properties: { api: { type: 'string' } },
|
|
324
|
+
required: ['api'],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
TagInput: {
|
|
328
|
+
propsSchema: {
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: { max: { type: 'integer', minimum: 1 } },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
控件契约通过 `model.controlContracts` 索引文件路径注入到 `createApplication`;模型装载阶段会使用 Ajv 对每个控件实例的 `component.config.props` 进行校验。
|
|
337
|
+
|
|
338
|
+
### 菜单工厂
|
|
339
|
+
|
|
340
|
+
| 工厂 | 说明 |
|
|
341
|
+
| --------------------------------- | ------------------------------------------------------------------- |
|
|
342
|
+
| `custom(key, name, path)` | 声明一个"自定义模块"菜单项(业务方提供独立路由 + 页面,跨项目通用) |
|
|
343
|
+
| `iframe(key, name, path)` | 声明 iframe 模块菜单项(path 为第三方页面 URL) |
|
|
344
|
+
| `group(key, name, subMenu)` | 侧边栏分组(子菜单为空时整组不渲染) |
|
|
345
|
+
| `sider(key, name, menu)` | 侧边栏容器(自身对应一个可访问的页面) |
|
|
346
|
+
| `schema(key, name, moduleConfig)` | schema 驱动的表格式 CRUD 页面 |
|
|
347
|
+
|
|
348
|
+
每种菜单对应 `moduleType` 之一(`custom` / `iframe` / `sider` / `schema`),由前端 dashboard 据此渲染。
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 客户端(`@ynight/framework/client`)
|
|
353
|
+
|
|
354
|
+
内置后台客户端基于 Vue 3 + Pinia + Vue Router 4 + Element Plus。模块入口 `src/client/index.ts` 统一对外暴露:
|
|
355
|
+
|
|
356
|
+
- **入口 API**:`createAdminApp(options)`、`defineClientExtensions(setup)`;
|
|
357
|
+
- **运行时**:`useElpisContext()`(组件内获取上下文)、`createClientContext`、`installClientContext`;
|
|
358
|
+
- **核心组件**:`SchemaForm`、`SchemaTable`(DSL 驱动的表单 / 表格);
|
|
359
|
+
- **默认壳层**:`DefaultProjectListPage`、`DefaultDashboardPage`(可通过 `extensions.shell` 整体替换);
|
|
360
|
+
- **schema 工具**:`resolveSchemaOptions`、`buildSchemaViewData`、`validateControlReferences`、`resolveMenuPath`、`findMenuTrail`、`findMenu`、`firstModule`、`SchemaViewDataInjectKey`;
|
|
361
|
+
- **通用工具**:`createLogger(scope)`、`measureButtonWidth(label)`;
|
|
362
|
+
- **类型**:`AdminAppRuntime`、`ApiResult`、`ClientExtensions`、`ControlRegistration`、`CreateAdminAppOptions`、`ElpisClientContext`、`FormValue(s)`、`RequestAdapter`、`RequestAdapterOption`、`ShellExtensions`、`ViewField`、`ViewSchema`、`SchemaFormProps`、`SchemaFormExposed`、`SchemaFormValidationFailedPayload`、`SchemaFormSchema`、`TableSchema`、`DtoSchema`、`DtoSchemaProperty`、`AjvFieldError`、以及 contracts 中的 `IModel`、`MenuItemType`、`ModelDto`、`ProjectDto`。
|
|
363
|
+
|
|
364
|
+
### `createAdminApp(options)`
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { createAdminApp, defineClientExtensions } from '@ynight/framework/client';
|
|
368
|
+
|
|
369
|
+
const runtime = await createAdminApp({
|
|
370
|
+
page: 'project-list',
|
|
371
|
+
mount: '#root',
|
|
372
|
+
// base: '/project-list', // 默认等于 /<page>
|
|
373
|
+
config: {
|
|
374
|
+
/* 任意启动配置键值对 */
|
|
375
|
+
},
|
|
376
|
+
extensions: defineClientExtensions({
|
|
377
|
+
controls: {
|
|
378
|
+
AsyncSearchSelect: { component: AsyncSearchSelectImpl },
|
|
379
|
+
TagInput: { component: TagInputImpl },
|
|
380
|
+
},
|
|
381
|
+
shell: {
|
|
382
|
+
projectList: MyProjectList,
|
|
383
|
+
dashboard: MyDashboard, // 内部需含 <router-view />
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
requestAdapter: async (input, init) => fetch(input, init), // 可选
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// runtime.app / runtime.router / runtime.pinia / runtime.context / runtime.mounted
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
装配流程:
|
|
393
|
+
|
|
394
|
+
1. 构造 vue-router(HTML5 history 模式),按 `page` 生成对应路由;
|
|
395
|
+
2. 创建 Pinia 状态管理实例;
|
|
396
|
+
3. 注册 Element Plus → Pinia → Router 到 Vue 应用;
|
|
397
|
+
4. 通过 `installClientContext` 把 `ElpisClientContext` 注入到应用,业务组件可用 `useElpisContext()` 拿到;
|
|
398
|
+
5. 等 router 首次解析完成后挂载到 DOM(避免首屏空白竞态)。
|
|
399
|
+
|
|
400
|
+
返回的 `AdminAppRuntime`:
|
|
401
|
+
|
|
402
|
+
| 字段 | 类型 | 说明 |
|
|
403
|
+
| --------- | -------------------- | ----------------------------------- |
|
|
404
|
+
| `app` | `App` | Vue 应用实例 |
|
|
405
|
+
| `router` | `Router` | vue-router 实例 |
|
|
406
|
+
| `pinia` | `Pinia` | Pinia 实例 |
|
|
407
|
+
| `context` | `ElpisClientContext` | 客户端上下文 |
|
|
408
|
+
| `mounted` | `Promise<App>` | router 就绪并完成首次挂载的 Promise |
|
|
409
|
+
|
|
410
|
+
### 客户端上下文(`ElpisClientContext`)
|
|
411
|
+
|
|
412
|
+
通过 `useElpisContext()` 获取,包含:
|
|
413
|
+
|
|
414
|
+
- `projectKey: ComputedRef<string | undefined>`——当前项目 key(URL query 优先);
|
|
415
|
+
- `currentProject: Ref<ProjectDto | undefined>`、`currentModel: Ref<IModel | undefined>`、`projectList: Ref<ProjectDto[]>`、`modelList: Ref<ModelDto[]>`、`user: Ref<... | null>`;
|
|
416
|
+
- `config: Readonly<Record<string, unknown>>`——启动配置;
|
|
417
|
+
- `requestAdapter: RequestAdapter`——生效的请求适配器;
|
|
418
|
+
- `request<T>(input, init?): Promise<ApiResult<T>>`——通用请求方法,自动对同源 `/api/` 注入 `projectKey`(query + header),自动解析 `{ success, data, message, metadata }` 响应;
|
|
419
|
+
- `setProject(project)`——切换当前项目。
|
|
420
|
+
|
|
421
|
+
业务侧可传入自定义 `RequestAdapter`(函数或 `{ request }` 对象),用于 Mock / 统一前缀 / 鉴权注入等场景。
|
|
422
|
+
|
|
423
|
+
### `defineClientExtensions(setup)`
|
|
424
|
+
|
|
425
|
+
类型断言助手,仅做类型层面穿透,让业务侧在书写 extensions 配置时获得完整的 TypeScript 类型推断(如自定义控件名对应的 component 类型),运行时不产生任何额外开销。
|
|
426
|
+
|
|
427
|
+
### 内置组件
|
|
428
|
+
|
|
429
|
+
- **`SchemaForm`**(`src/widgets/schema-form/index.vue`)——DSL 驱动的表单,基于 `el-form` 的表单校验管控 + AJV 的表单校验;
|
|
430
|
+
- **`SchemaTable`**(`src/widgets/schema-table/index.vue`)——DSL 驱动的表格;
|
|
431
|
+
- **默认壳层**:`ProjectListPage`、`DashboardPage`、`ErrorPage`、`PageSkeleton`、`ModuleHost`、`SchemaView`;
|
|
432
|
+
- **complex-view 自定义控件**:`async-search-select`、`dynamic-select`、`tag-input`、`upload` 等。
|
|
433
|
+
|
|
434
|
+
### DSL 解析
|
|
435
|
+
|
|
436
|
+
- **`resolveSchemaOptions(property)`**:从 JSON Schema 的 `oneOf` / `items.oneOf` 中提取 `{label, value}[]`,用于 Select / SelectV2 / RadioGroup / CheckboxGroup / Segmented 等枚举类控件;
|
|
437
|
+
- **`createViewSchema(schema, getOption, inheritRequired?)`**:合并 option + required + 注入 options,产出渲染所需的 `ViewSchema`;
|
|
438
|
+
- **`buildSchemaViewData(menu, extensions)`**:装配表格 / 搜索表单 / CreateForm / UpdateForm / DetailView 的视图数据;同时校验 `extensions.controls` 是否注册了 schema 字段引用的自定义控件;
|
|
439
|
+
- **`validateControlReferences(module, extensions)`**:校验 `search / CreateForm / UpdateForm / DetailView` 四场景的 `option.comType`,Element Plus 内置控件直接放行;
|
|
440
|
+
- **`resolveMenuPath(menu)`**:根据菜单项返回 dashboard 路由 path(custom / iframe / schema / sider / group 各自规则);
|
|
441
|
+
- **`findMenuTrail(menus, matches)`**:在菜单树中查找所有满足 matches 的菜单,并保留分组 / 侧栏祖先;
|
|
442
|
+
- **`findMenu(menus, key)`**:按 key 精确查找(递归分组与侧栏);
|
|
443
|
+
- **`firstModule(menus)`**:返回菜单树中的第一个 module(深度优先)。
|
|
444
|
+
|
|
445
|
+
### 通用工具
|
|
446
|
+
|
|
447
|
+
- **`createLogger(scope)`**——轻量日志器(debug/warn/error,自动加 `[scope]` 前缀);
|
|
448
|
+
- **`measureButtonWidth(label)`**——按字符估算按钮宽度(CJK 14px、其它 7px、padding 16px);
|
|
449
|
+
- **`SchemaViewDataInjectKey`**——Vue `provide/inject` 注入键(类型 `SchemaViewData`),由 schema 视图容器提供。
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## 静态资源(`@ynight/framework/assets`)
|
|
454
|
+
|
|
455
|
+
`src/assets/index.ts` 提供框架内置后台的 HTML 模板定位与读取能力,模板由 CLI 在构建阶段生成到 `assets/` 目录下。
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
export const frameworkPageNames = ['project-list', 'dashboard', 'error'] as const;
|
|
459
|
+
export type FrameworkPageName = (typeof frameworkPageNames)[number];
|
|
460
|
+
|
|
461
|
+
export const frameworkAssetsRoot = path.resolve(__dirname, '../../assets');
|
|
462
|
+
|
|
463
|
+
export function frameworkPagePath(publicRoot: string, page: FrameworkPageName): string;
|
|
464
|
+
export function readFrameworkPage(publicRoot: string, page: FrameworkPageName): string;
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- `frameworkPageNames`——约束合法的框架页面名称;
|
|
468
|
+
- `frameworkAssetsRoot`——框架内置资源根目录的绝对路径;
|
|
469
|
+
- `frameworkPagePath(publicRoot, page)`——根据公共资源根与页面名解析模板绝对路径;
|
|
470
|
+
- `readFrameworkPage(publicRoot, page)`——读取模板 HTML;模板不存在抛 `ELPIS_PUBLIC_TEMPLATE_MISSING`,模板缺少 `#root` 挂载节点抛 `ELPIS_PUBLIC_TEMPLATE_INVALID`。
|
|
471
|
+
|
|
472
|
+
服务端装配阶段会预热三个页面(`createApplication` 第 2 步),确保 History 兜底能同步响应 HTML。
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## 安全与配置约束
|
|
477
|
+
|
|
478
|
+
`createApplication` / `resolveConfig` 在多个层面保证配置安全:
|
|
479
|
+
|
|
480
|
+
- **强制认证**:生产模式(`mode === 'production'`)必须有 `serverExtension.auth`,否则抛 `ELPIS_AUTH_REQUIRED`;
|
|
481
|
+
- **devAnonymous 显式**:非生产模式下若没有 auth,必须显式设置 `devAnonymous: true`,否则同样抛 `ELPIS_AUTH_REQUIRED`;
|
|
482
|
+
- **回环地址护栏**:devAnonymous 仅允许监听回环地址(`127.0.0.1 / localhost / ::1 / 0:0:0:0:0:0:0:1`),由 `validateListen` 钩子在 `createApplicationContainer` 中保证;
|
|
483
|
+
- **路径安全**:所有生成路径(`generatedRoot / compiledRoot / outputRoot / serverRoot / modelRoot / sharedRoot / publicRoot`)必须位于 `sourceRoot` 内,防御目录穿越(`..`、绝对路径)与 symlink 逃逸(`realpath` 逐段校验);
|
|
484
|
+
- **输出目录安全**:`build.outDir`(`outputRoot`)额外禁止覆盖 `node_modules / server / model / .git` 等受保护目录;
|
|
485
|
+
- **配置字段白名单**:`ElpisConfig` 顶级字段限定为 `server / model / client / public / dev / build`,其它字段启动期报错;
|
|
486
|
+
- **路由 schema 完整性**:启动期校验集中 schema 不存在"孤儿",避免定义却未使用;
|
|
487
|
+
- **业务路由冲突**:业务路由若与框架内置路由(`/api/session` / `/api/project/*`)同名/同方法立即抛错;
|
|
488
|
+
- **history 兜底**:模板缺失抛 `ELPIS_PUBLIC_TEMPLATE_MISSING`,缺少 `#root` 节点抛 `ELPIS_PUBLIC_TEMPLATE_INVALID`;
|
|
489
|
+
- **site 模板**:模型中声明 `mode === 'site'` 时启动期直接拒止(`ELPIS_UNSUPPORTED_TEMPLATE`,尚未实现)。
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## 相关包
|
|
494
|
+
|
|
495
|
+
- **`@ynight/core`**——底层引擎(Koa 应用容器、模块加载、应用生命周期);
|
|
496
|
+
- **`@ynight/contracts`**——DSL 类型契约(菜单 / 模块 / 控件 / DTO 等),被 `@ynight/framework/model` 透传;
|
|
497
|
+
- **`@ynight/model-runtime`**——模型加载与校验运行时(`loadModels` / `validateModels` / 控件契约);
|
|
498
|
+
- **`@ynight/cli`**——工程脚手架与开发命令(生成 elpis.config / 编译业务模块 / 启动 dev / build)。
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 引擎要求
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
node >=22.0.0
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 许可证
|
|
511
|
+
|
|
512
|
+
MIT
|
package/assets/.gitkeep
ADDED
|
File without changes
|