@ynight/create-app 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 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,202 @@
1
- # Temporary Holding Version
1
+ # @ynight/create-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 一行命令即可生成最小可运行的 Elpis 业务工程(zero-client 模板),专注于服务端的 Model、Controller、Service 与 DSL 配置。
4
+
5
+ ## 概述
6
+
7
+ `@ynight/create-app` 是 Elpis 框架官方维护的 **最小业务工程脚手架**。它会把随包发布的 `templates/basic` 目录原样拷贝到指定目录,并对模板中预置的占位符进行批量替换(`__PROJECT_NAME__`、`__PACKAGE_NAME__`、`__PROJECT_TITLE__`、`__ELPIS_VERSION__`),随后按所选包管理器执行 `install`。
8
+
9
+ 生成出来的工程具有以下特点:
10
+
11
+ - 已包含 `elpis.config.ts`、`server/app.ts`、`package.json`、`tsconfig.*.json` 等关键文件
12
+ - 自带一个基于 **内存数据** 的「商品」示例 Controller / Router / Service,方便直接 `pnpm dev` 看效果
13
+ - 提供一套基于 Model DSL 的「业务后台」菜单(商品列表 + 本地帮助 iframe),Schema 列表、搜索、CRUD 弹窗、Project List、Dashboard 全部由 `@ynight/framework/client` 提供
14
+ - **不带任何 Vue/前端源码**(zero-client 模型),如需自定义控件或页面,可选地在 `types/empty-client.ts` 处按框架约定注册
15
+
16
+ ## 快速开始
17
+
18
+ ```bash
19
+ # 使用 npx
20
+ npx @ynight/create-app my-elpis-app
21
+
22
+ # 或使用 pnpm
23
+ pnpm dlx @ynight/create-app my-elpis-app
24
+ ```
25
+
26
+ 执行后会看到如下流程:
27
+
28
+ 1. 校验目标目录(必须不存在、为空目录或为符号链接以外的空目录)
29
+ 2. 将 `templates/basic` 拷贝到目标目录,并做占位符替换
30
+ 3. 除非传入了 `--skip-install`,否则调用所选包管理器在目标目录中执行 `install`
31
+ 4. 在终端打印「下一步」提示(开发命令、内存 Service 提醒、Basic Auth 提醒等)
32
+
33
+ 成功示例:
34
+
35
+ ```text
36
+ Elpis 项目已创建: /Users/you/work/my-elpis-app
37
+ 开发: cd "/Users/you/work/my-elpis-app" && npm run dev
38
+ 提示:开发环境使用 devAnonymous,生产示例使用环境变量配置的 Basic Auth;示例 Service 使用内存数据,重启后数据会丢失。
39
+ ```
40
+
41
+ ## 参数与选项
42
+
43
+ ```text
44
+ 用法: create-elpis-app <目录> [--package-manager npm|pnpm] [--skip-install] [--yes]
45
+
46
+ 位置参数:
47
+ <目录> 目标工程目录(必须不存在或为空)
48
+
49
+ 选项:
50
+ --package-manager <npm|pnpm> 指定包管理器,默认 npm
51
+ --skip-install 跳过依赖安装步骤
52
+ --yes (当前版本作为保留位,与 --skip-install 共存解析,不影响结果)
53
+ --help, -h 打印帮助信息
54
+ --version, -v 打印版本号
55
+ ```
56
+
57
+ 如果传入未知参数或未提供目标目录,CLI 会以非零状态退出并打印错误信息,例如:
58
+
59
+ ```text
60
+ 创建失败: 请指定新项目目录
61
+ 创建失败: 目标目录必须不存在或为空,且不能是符号链接: /tmp/foo
62
+ 创建失败: --package-manager 只能是 npm 或 pnpm
63
+ ```
64
+
65
+ ## 模板结构
66
+
67
+ `templates/basic` 是脚手架唯一内置的模板,目录布局如下(拷贝后会保留全部文件):
68
+
69
+ ```text
70
+ my-elpis-app/
71
+ ├── elpis.config.ts # 工程配置(server / model / public / build)
72
+ ├── package.json # 含 elpis dev / build / type-check / generate:types 脚本
73
+ ├── tsconfig.json # 根 tsconfig,仅做引用转发
74
+ ├── tsconfig.server.json # 后端 tsconfig:extends @ynight/cli/tsconfig/server.json
75
+ ├── tsconfig.client.json # 前端 tsconfig:仅 include types/empty-client.ts
76
+ ├── gitignore # 拷贝后被重命名为 .gitignore(node_modules / dist / .elpis / *.log)
77
+ ├── server/
78
+ │ ├── app.ts # defineServer 入口:开启 devAnonymous、配置 Basic Auth 适配器
79
+ │ ├── controller/
80
+ │ │ └── product.ts # 示例 ProductController(继承 BaseController)
81
+ │ ├── router/
82
+ │ │ └── product.ts # 示例商品 REST 路由(list / create / detail / update / remove)
83
+ │ └── service/
84
+ │ └── product.ts # 示例 ProductService:继承 BaseService,数据保存在进程内存
85
+ ├── model/
86
+ │ └── business/
87
+ │ ├── model.ts # 业务模型定义:name / menu(schema + iframe)
88
+ │ └── project/
89
+ │ └── demo.ts # 演示项目:name / desc / homePage / menu 覆盖
90
+ ├── types/
91
+ │ └── empty-client.ts # zero-client 占位:仅 export {},供 vue-tsc 消费
92
+ └── public/
93
+ ├── help.html # 供 iframe('help', ...) 引用的本地帮助页
94
+ └── .gitkeep # 保留 public 目录为空时的占位文件
95
+ ```
96
+
97
+ 拷贝后 `gitignore` 会被自动重命名为 `.gitignore`,避免 npm 发布的 tar 包忽略问题。
98
+
99
+ ## 模板工程做了什么
100
+
101
+ 模板工程刻意保持「最小可用」状态,关键边界如下,请在使用前先理解:
102
+
103
+ ### 1. zero-client 模型
104
+
105
+ 模板中 **不存在任何 Vue 源码**。`tsconfig.client.json` 只 `include` 一个空实现的 `types/empty-client.ts`,用来满足 `vue-tsc` 的强制类型入口需要。这意味着:
106
+
107
+ | 能力 | 由谁提供 |
108
+ | ---------------------------- | ----------------------------------------- |
109
+ | Schema 列表、搜索、CRUD 弹窗 | `@ynight/framework/client` 自带组件 |
110
+ | Project List、Dashboard 壳层 | `@ynight/framework/client` 自带页面 |
111
+ | 本地帮助 iframe 帮助页 | 用户工程 `public/help.html` 自提供 |
112
+ | 自定义控件 / 自定义页面 | 用户按需在 `types/empty-client.ts` 中注册 |
113
+
114
+ ### 2. 业务示例源码属于用户工程
115
+
116
+ `model/business/model.ts`、`model/business/project/demo.ts`、`server/controller/product.ts`、`server/router/product.ts`、`server/service/product.ts` **都是用户工程的实例源码**,不归属于任何 `@ynight/*` 框架包。你可以自由修改、删除或替换它们,框架包只负责提供运行时与默认壳层。
117
+
118
+ ### 3. 认证策略
119
+
120
+ `server/app.ts` 通过 `defineServer` 注册:
121
+
122
+ - `devAnonymous: true`:开发环境下跳过登录校验,方便直接调试
123
+ - `auth.challenge = 'Basic realm="Elpis demo"'`、`authenticate` 读取 `Authorization: Basic …` 并比对 `ELPIS_DEMO_USERNAME` / `ELPIS_DEMO_PASSWORD`
124
+ - `auth.authorize`:演示版始终返回 `true`
125
+
126
+ **示例未实现完整的账号体系**(没有用户表、没有登录页、没有 Token 刷新)。正式项目应:
127
+
128
+ - 把 `ELPIS_DEMO_USERNAME` / `ELPIS_DEMO_PASSWORD` 替换为真实的用户表查询
129
+ - 在 `auth.authorize` 中按模块(`access.module`)和操作(`access.action`)做 RBAC 校验
130
+ - 接入真正的会话、Token 或 SSO 方案
131
+
132
+ ### 4. 数据层
133
+
134
+ `ProductService` 把数据保存在 `this.products` 数组中,**重启后恢复到只有一条「示例商品」的初始状态**。这是为了让 `pnpm dev` 起来就有数据可看,但 **绝对不能直接用于生产**。请把 `ProductService` 替换为对接数据库(MySQL / PostgreSQL / MongoDB / …)的实现。
135
+
136
+ ## 启动新工程
137
+
138
+ ```bash
139
+ cd my-elpis-app
140
+ pnpm dev
141
+ ```
142
+
143
+ 默认监听 `http://127.0.0.1:3000`(见 `elpis.config.ts` 的 `host` / `port`)。打开后会看到框架提供的 Project List 页面,「演示项目」即对应 `model/business/project/demo.ts`;点击进入后能看到商品列表、搜索、CRUD 操作以及左侧「本地使用帮助」iframe。
144
+
145
+ ## 后续步骤建议
146
+
147
+ 按顺序补齐以下内容即可上线:
148
+
149
+ 1. **替换数据层**:把 `server/service/product.ts` 改为对接真实数据库的实现;删掉或迁移 `this.products` 内存数组
150
+ 2. **替换认证**:在 `server/app.ts` 中改写 `auth.authenticate` 与 `auth.authorize`,或接入完整的 `auth` 适配器(OAuth / OIDC / 自研账号系统)
151
+ 3. **调整 DSL**:在 `model/business/model.ts` / `model/business/project/demo.ts` 中按真实业务改造菜单、表单、表格字段
152
+ 4. **(可选)扩展前端**:在 `types/empty-client.ts` 处注册自定义控件 / 自定义页面,参考 `@ynight/framework/client` 的扩展约定
153
+ 5. **配置 CI**:在 `.gitignore` 已经覆盖 `node_modules` / `dist` / `.elpis` / `*.log` 的基础上,按需补充 IDE / 操作系统相关条目
154
+
155
+ ## 常见问题
156
+
157
+ ### 目标目录已存在怎么办?
158
+
159
+ CLI 会拒绝非空目录、符号链接目录或非目录路径,并抛出:
160
+
161
+ ```text
162
+ 创建失败: 目标目录必须不存在或为空,且不能是符号链接: /path/to/dir
163
+ ```
164
+
165
+ 请先清空目标目录,或换一个名字重新执行。
166
+
167
+ ### 想换包管理器怎么办?
168
+
169
+ ```bash
170
+ npx @ynight/create-app my-app --package-manager pnpm
171
+ # 或
172
+ npx @ynight/create-app my-app --package-manager npm
173
+ ```
174
+
175
+ 未指定时默认使用 `npm`。
176
+
177
+ ### 不想立刻安装依赖怎么办?
178
+
179
+ ```bash
180
+ npx @ynight/create-app my-app --skip-install
181
+ ```
182
+
183
+ 随后可以进入工程手动执行 `pnpm install` / `npm install` / `yarn install`。
184
+
185
+ ### 安装失败会保留工程吗?
186
+
187
+ 会。如果 `install` 阶段非零退出,目标工程会被保留在原位置,CLI 会提示在工程目录中重新执行依赖安装命令。
188
+
189
+ ## 相关包
190
+
191
+ - [`@ynight/framework`](../framework) - 框架主入口,被生成的工程依赖(`dependencies`)
192
+ - [`@ynight/cli`](../cli) - 提供 `elpis dev` / `elpis build` / `elpis type-check` / `elpis generate:types` 命令
193
+ - [`@ynight/contracts`](../contracts) - DSL / 配置类型契约
194
+
195
+ ## 引擎要求
196
+
197
+ - Node.js `>=22.0.0`
198
+ - 包管理器:npm 或 pnpm(推荐 pnpm)
199
+
200
+ ## 许可证
201
+
202
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const tslib_1 = require("tslib");
5
+ const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
6
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
7
+ const node_child_process_1 = require("node:child_process");
8
+ const VERSION = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.resolve(__dirname, '../package.json'), 'utf8')).version;
9
+ const HELP = `创建 Elpis 管理后台
10
+
11
+ 用法: create-elpis-app <目录> [--package-manager npm|pnpm] [--skip-install] [--yes]`;
12
+ function parseArgs(argv) {
13
+ if (argv.includes('--help') || argv.includes('-h')) {
14
+ return 'help';
15
+ }
16
+ if (argv.includes('--version') || argv.includes('-v')) {
17
+ return 'version';
18
+ }
19
+ const options = { packageManager: 'npm', skipInstall: false };
20
+ for (let index = 0; index < argv.length; index += 1) {
21
+ const arg = argv[index];
22
+ if (arg && !arg.startsWith('-') && !options.target) {
23
+ options.target = arg;
24
+ }
25
+ else if (arg === '--package-manager') {
26
+ const manager = argv[++index];
27
+ if (manager !== 'npm' && manager !== 'pnpm') {
28
+ throw new Error('--package-manager 只能是 npm 或 pnpm');
29
+ }
30
+ options.packageManager = manager;
31
+ }
32
+ else if (arg === '--skip-install' || arg === '--yes') {
33
+ if (arg === '--skip-install') {
34
+ options.skipInstall = true;
35
+ }
36
+ }
37
+ else if (arg) {
38
+ throw new Error(`未知参数: ${arg}`);
39
+ }
40
+ }
41
+ if (!options.target) {
42
+ throw new Error('请指定新项目目录');
43
+ }
44
+ return options;
45
+ }
46
+ function normalizePackageName(projectName) {
47
+ const normalized = projectName
48
+ .normalize('NFKD')
49
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
50
+ .replace(/^[._-]+|[._-]+$/g, '')
51
+ .toLowerCase()
52
+ .slice(0, 214)
53
+ .replace(/^[._-]+|[._-]+$/g, '');
54
+ return normalized || 'elpis-app';
55
+ }
56
+ function replaceText(filePath, projectName, packageName) {
57
+ const stat = node_fs_1.default.statSync(filePath);
58
+ if (stat.isDirectory()) {
59
+ node_fs_1.default.readdirSync(filePath).forEach(name => replaceText(node_path_1.default.resolve(filePath, name), projectName, packageName));
60
+ return;
61
+ }
62
+ const content = node_fs_1.default
63
+ .readFileSync(filePath, 'utf8')
64
+ .replaceAll('__PROJECT_NAME__', projectName)
65
+ .replaceAll('__PACKAGE_NAME__', packageName)
66
+ .replaceAll('__PROJECT_TITLE__', JSON.stringify(`${projectName} 管理后台`))
67
+ .replaceAll('__ELPIS_VERSION__', VERSION);
68
+ node_fs_1.default.writeFileSync(filePath, content);
69
+ }
70
+ /** 删除模板中因移除可选扩展而留下的空目录。 */
71
+ function removeEmptyDirectories(directory) {
72
+ node_fs_1.default.readdirSync(directory, { withFileTypes: true }).forEach(entry => {
73
+ if (entry.isDirectory()) {
74
+ const child = node_path_1.default.resolve(directory, entry.name);
75
+ removeEmptyDirectories(child);
76
+ if (node_fs_1.default.readdirSync(child).length === 0) {
77
+ node_fs_1.default.rmdirSync(child);
78
+ }
79
+ }
80
+ });
81
+ }
82
+ function createProject(options) {
83
+ const target = node_path_1.default.resolve(options.target);
84
+ if (node_fs_1.default.existsSync(target)) {
85
+ const stat = node_fs_1.default.lstatSync(target);
86
+ if (stat.isSymbolicLink() || !stat.isDirectory() || node_fs_1.default.readdirSync(target).length > 0) {
87
+ throw new Error(`目标目录必须不存在或为空,且不能是符号链接: ${target}`);
88
+ }
89
+ }
90
+ const parent = node_path_1.default.dirname(target);
91
+ node_fs_1.default.mkdirSync(parent, { recursive: true });
92
+ const temporary = node_fs_1.default.mkdtempSync(node_path_1.default.join(parent, '.elpis-create-'));
93
+ const template = node_path_1.default.resolve(__dirname, '../templates/basic');
94
+ try {
95
+ node_fs_1.default.cpSync(template, temporary, { recursive: true });
96
+ const packedGitignore = node_path_1.default.resolve(temporary, 'gitignore');
97
+ if (node_fs_1.default.existsSync(packedGitignore)) {
98
+ node_fs_1.default.renameSync(packedGitignore, node_path_1.default.resolve(temporary, '.gitignore'));
99
+ }
100
+ removeEmptyDirectories(temporary);
101
+ const projectName = node_path_1.default.basename(target);
102
+ replaceText(temporary, projectName, normalizePackageName(projectName));
103
+ if (node_fs_1.default.existsSync(target)) {
104
+ node_fs_1.default.rmdirSync(target);
105
+ }
106
+ node_fs_1.default.renameSync(temporary, target);
107
+ }
108
+ catch (error) {
109
+ node_fs_1.default.rmSync(temporary, { recursive: true, force: true });
110
+ throw error;
111
+ }
112
+ return target;
113
+ }
114
+ function install(target, packageManager) {
115
+ const command = packageManager === 'pnpm' ? 'pnpm' : 'npm';
116
+ const args = packageManager === 'pnpm' ? ['install'] : ['install'];
117
+ const result = (0, node_child_process_1.spawnSync)(command, args, { cwd: target, stdio: 'inherit' });
118
+ if (result.error || result.status !== 0) {
119
+ throw new Error(`依赖安装失败。项目已保留,请在 ${target} 中重新执行 ${command} install`);
120
+ }
121
+ }
122
+ function main() {
123
+ const parsed = parseArgs(process.argv.slice(2));
124
+ if (parsed === 'help') {
125
+ console.log(HELP);
126
+ return;
127
+ }
128
+ if (parsed === 'version') {
129
+ console.log(VERSION);
130
+ return;
131
+ }
132
+ const target = createProject(parsed);
133
+ if (!parsed.skipInstall) {
134
+ install(target, parsed.packageManager);
135
+ }
136
+ console.log(`Elpis 项目已创建: ${target}`);
137
+ console.log('开发: cd "' + target + '" && npm run dev');
138
+ console.log('提示:开发环境使用 devAnonymous,生产示例使用环境变量配置的 Basic Auth;示例 Service 使用内存数据,重启后数据会丢失。');
139
+ }
140
+ try {
141
+ main();
142
+ }
143
+ catch (error) {
144
+ console.error(`创建失败: ${error instanceof Error ? error.message : error}`);
145
+ process.exitCode = 1;
146
+ }
147
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@ynight/create-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "Elpis 业务工程脚手架",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+ssh://git@git.code.tencent.com/YNight/elpis.git"
8
+ },
9
+ "license": "MIT",
10
+ "type": "commonjs",
11
+ "bin": {
12
+ "create-elpis-app": "./dist/bin.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "templates",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22.0.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "tslib": "^2.8.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "rimraf": "^5.0.10",
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "scripts": {
35
+ "clean": "rimraf dist",
36
+ "build": "rimraf dist && tsc -p tsconfig.json",
37
+ "type-check": "tsc --noEmit -p tsconfig.json",
38
+ "lint": "eslint src"
39
+ }
6
40
  }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from '@ynight/framework/server';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ host: '127.0.0.1',
6
+ port: 3000,
7
+ entry: './server/app.ts',
8
+ businessDir: './server',
9
+ },
10
+ model: { dir: './model' },
11
+ // Model、Project 与业务接口均是此实例项目的源码;框架只提供运行时和默认壳层。
12
+ public: {
13
+ title: __PROJECT_TITLE__,
14
+ homePage: '/project-list',
15
+ projectListMode: 'never',
16
+ defaultProjectKey: 'demo',
17
+ },
18
+ build: { outDir: './dist' },
19
+ });
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ .elpis/
4
+ *.log
@@ -0,0 +1,139 @@
1
+ import {
2
+ EventKey,
3
+ SchemaComponentName,
4
+ SchemaFormControlType,
5
+ defineModel,
6
+ iframe,
7
+ schema,
8
+ } from '@ynight/framework/model';
9
+
10
+ export default defineModel({
11
+ name: '业务后台',
12
+ mode: 'dashboard',
13
+ menu: [
14
+ schema('product-list', '商品列表', {
15
+ api: '/api/product',
16
+ schema: {
17
+ type: 'object',
18
+ properties: {
19
+ id: {
20
+ type: 'string',
21
+ label: '商品编号',
22
+ tableOption: { visible: false },
23
+ },
24
+ productName: {
25
+ type: 'string',
26
+ label: '商品名称',
27
+ minLength: 1,
28
+ tableOption: { visible: true },
29
+ searchOption: { comType: SchemaFormControlType.Input, visible: true },
30
+ CreateFormOption: { comType: SchemaFormControlType.Input, visible: true },
31
+ UpdateFormOption: { comType: SchemaFormControlType.Input, visible: true },
32
+ DetailViewOption: { visible: true },
33
+ },
34
+ price: {
35
+ type: 'number',
36
+ label: '价格',
37
+ minimum: 0,
38
+ tableOption: { visible: true, toFixed: 2 },
39
+ CreateFormOption: {
40
+ comType: SchemaFormControlType.InputNumber,
41
+ visible: true,
42
+ min: 0,
43
+ precision: 2,
44
+ },
45
+ UpdateFormOption: {
46
+ comType: SchemaFormControlType.InputNumber,
47
+ visible: true,
48
+ min: 0,
49
+ precision: 2,
50
+ },
51
+ DetailViewOption: { visible: true },
52
+ },
53
+ stock: {
54
+ type: 'number',
55
+ label: '库存',
56
+ minimum: 0,
57
+ tableOption: { visible: true },
58
+ CreateFormOption: {
59
+ comType: SchemaFormControlType.InputNumber,
60
+ visible: true,
61
+ min: 0,
62
+ precision: 0,
63
+ },
64
+ UpdateFormOption: {
65
+ comType: SchemaFormControlType.InputNumber,
66
+ visible: true,
67
+ min: 0,
68
+ precision: 0,
69
+ },
70
+ DetailViewOption: { visible: true },
71
+ },
72
+ status: {
73
+ type: 'string',
74
+ label: '状态',
75
+ oneOf: [
76
+ { const: 'on', title: '在售' },
77
+ { const: 'off', title: '下架' },
78
+ ],
79
+ tableOption: { visible: true, filter: true, filterMultiple: false },
80
+ searchOption: { comType: SchemaFormControlType.Select, visible: true },
81
+ CreateFormOption: { comType: SchemaFormControlType.Select, visible: true },
82
+ UpdateFormOption: { comType: SchemaFormControlType.Select, visible: true },
83
+ DetailViewOption: { visible: true },
84
+ },
85
+ createTime: {
86
+ type: 'string',
87
+ label: '创建时间',
88
+ tableOption: { visible: true },
89
+ DetailViewOption: { visible: true },
90
+ },
91
+ },
92
+ },
93
+ searchConfig: {},
94
+ tableConfig: {
95
+ headerButtons: [
96
+ {
97
+ label: '新增商品',
98
+ eventKey: EventKey.SHOW_COMPONENT,
99
+ eventOption: { comName: SchemaComponentName.CreateForm },
100
+ type: 'primary',
101
+ },
102
+ ],
103
+ rowButtons: [
104
+ {
105
+ label: '查看',
106
+ eventKey: EventKey.SHOW_COMPONENT,
107
+ eventOption: {
108
+ params: { id: 'schema::id' },
109
+ comName: SchemaComponentName.DetailView,
110
+ },
111
+ link: true,
112
+ },
113
+ {
114
+ label: '编辑',
115
+ eventKey: EventKey.SHOW_COMPONENT,
116
+ eventOption: {
117
+ params: { id: 'schema::id' },
118
+ comName: SchemaComponentName.UpdateForm,
119
+ },
120
+ link: true,
121
+ },
122
+ {
123
+ label: '删除',
124
+ eventKey: EventKey.DELETE,
125
+ eventOption: {},
126
+ type: 'danger',
127
+ link: true,
128
+ },
129
+ ],
130
+ },
131
+ components: {
132
+ CreateForm: { title: '新增商品', saveBtnText: '保存' },
133
+ UpdateForm: { title: '编辑商品', saveBtnText: '保存' },
134
+ DetailView: { title: '商品详情' },
135
+ },
136
+ }),
137
+ iframe('help', '本地使用帮助', '/help.html'),
138
+ ],
139
+ });
@@ -0,0 +1,8 @@
1
+ import { defineModel } from '@ynight/framework/model';
2
+
3
+ export default defineModel({
4
+ name: '演示项目',
5
+ desc: '仅用于展示框架接入方式',
6
+ homePage: '/dashboard?projectKey=demo&key=product-list',
7
+ menu: [],
8
+ });
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "scripts": {
7
+ "dev": "elpis dev",
8
+ "build": "elpis build",
9
+ "type-check": "elpis type-check",
10
+ "generate:types": "elpis generate:types",
11
+ "generate:types:check": "elpis generate:types --check",
12
+ "start": "node dist/server/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@ynight/framework": "__ELPIS_VERSION__",
16
+ "element-plus": "^2.13.1",
17
+ "pinia": "^3.0.4",
18
+ "vue": "^3.5.26",
19
+ "vue-router": "^4.6.4"
20
+ },
21
+ "devDependencies": {
22
+ "@ynight/cli": "__ELPIS_VERSION__",
23
+ "@types/node": "^22.0.0",
24
+ "typescript": "^5.9.3"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.0.0"
28
+ }
29
+ }
File without changes
@@ -0,0 +1,21 @@
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" />
6
+ <title>本地使用帮助</title>
7
+ <style>
8
+ body { margin: 0; padding: 32px; color: #24324a; font: 16px/1.7 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f9fc; }
9
+ main { max-width: 720px; margin: 0 auto; padding: 28px; border: 1px solid #e4eaf2; border-radius: 12px; background: #fff; }
10
+ h1 { margin-top: 0; }
11
+ code { padding: 2px 6px; border-radius: 4px; background: #eef3f9; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <main>
16
+ <h1>本地使用帮助</h1>
17
+ <p>这是由脚手架生成的本地 iframe 页面,说明静态资源由业务项目自己提供。</p>
18
+ <p>商品数据保存在进程内存中,服务重启后会恢复为初始示例数据,生产环境请替换为数据库 Service。</p>
19
+ </main>
20
+ </body>
21
+ </html>
@@ -0,0 +1,34 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { defineServer, type AuthIdentity, type Context } from '@ynight/framework/server';
3
+
4
+ function safeEqual(actual: string, expected: string): boolean {
5
+ const actualBytes = Buffer.from(actual);
6
+ const expectedBytes = Buffer.from(expected);
7
+ return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
8
+ }
9
+
10
+ function authenticate(ctx: Context): AuthIdentity | null {
11
+ const [scheme, encoded] = ctx.get('authorization').split(' ', 2);
12
+ if (scheme?.toLowerCase() !== 'basic' || !encoded) return null;
13
+ const decoded = Buffer.from(encoded, 'base64').toString('utf8');
14
+ const separator = decoded.indexOf(':');
15
+ const username = separator < 0 ? decoded : decoded.slice(0, separator);
16
+ const password = separator < 0 ? '' : decoded.slice(separator + 1);
17
+ const expectedUsername = process.env.ELPIS_DEMO_USERNAME;
18
+ const expectedPassword = process.env.ELPIS_DEMO_PASSWORD;
19
+ if (!expectedUsername || !expectedPassword) return null;
20
+ return safeEqual(username, expectedUsername) && safeEqual(password, expectedPassword)
21
+ ? { id: 'demo-admin', name: '演示管理员' }
22
+ : null;
23
+ }
24
+
25
+ export default defineServer({
26
+ devAnonymous: true,
27
+ auth: {
28
+ challenge: 'Basic realm="Elpis demo"',
29
+ authenticate,
30
+ authorize() {
31
+ return true;
32
+ },
33
+ },
34
+ });
@@ -0,0 +1,72 @@
1
+ import { BaseController } from '@ynight/framework/server';
2
+ import type { Context } from '@ynight/framework/server';
3
+
4
+ export default class ProductController extends BaseController {
5
+ list(ctx: Context) {
6
+ const service = this.app.service.product as { list(): unknown[] };
7
+ const data = service.list();
8
+ this.success(ctx, data, { total: data.length });
9
+ }
10
+
11
+ create(ctx: Context) {
12
+ const body = (ctx.request as typeof ctx.request & { body: unknown }).body as {
13
+ productName?: unknown;
14
+ price?: unknown;
15
+ stock?: unknown;
16
+ status?: unknown;
17
+ };
18
+ if (typeof body.productName !== 'string' || typeof body.price !== 'number') {
19
+ this.fail(ctx, '商品名称和价格不能为空', 400);
20
+ return;
21
+ }
22
+ const service = this.app.service.product as {
23
+ create(payload: {
24
+ productName: string;
25
+ price: number;
26
+ stock: number;
27
+ status: 'on' | 'off';
28
+ }): unknown;
29
+ };
30
+ this.success(
31
+ ctx,
32
+ service.create({
33
+ productName: body.productName,
34
+ price: body.price,
35
+ stock: typeof body.stock === 'number' ? body.stock : 0,
36
+ status: body.status === 'off' ? 'off' : 'on',
37
+ })
38
+ );
39
+ }
40
+
41
+ detail(ctx: Context) {
42
+ const service = this.app.service.product as { get(id: string): unknown };
43
+ const data = service.get(ctx.params.id);
44
+ if (!data) {
45
+ this.fail(ctx, '商品不存在', 404);
46
+ return;
47
+ }
48
+ this.success(ctx, data);
49
+ }
50
+
51
+ update(ctx: Context) {
52
+ const service = this.app.service.product as {
53
+ update(id: string, payload: Record<string, unknown>): unknown;
54
+ };
55
+ const body = (ctx.request as typeof ctx.request & { body: unknown }).body;
56
+ const data = service.update(ctx.params.id, body as Record<string, unknown>);
57
+ if (!data) {
58
+ this.fail(ctx, '商品不存在', 404);
59
+ return;
60
+ }
61
+ this.success(ctx, data);
62
+ }
63
+
64
+ remove(ctx: Context) {
65
+ const service = this.app.service.product as { remove(id: string): boolean };
66
+ if (!service.remove(ctx.params.id)) {
67
+ this.fail(ctx, '商品不存在', 404);
68
+ return;
69
+ }
70
+ this.success(ctx, { id: ctx.params.id });
71
+ }
72
+ }
@@ -0,0 +1,39 @@
1
+ import { defineRoutes } from '@ynight/framework/server';
2
+ import type { Context } from '@ynight/framework/server';
3
+
4
+ export default defineRoutes(app => [
5
+ {
6
+ method: 'GET',
7
+ path: '/api/product',
8
+ access: { module: 'product', action: 'read' },
9
+ handler: (ctx: Context) => (app.controller.product as { list(ctx: Context): void }).list(ctx),
10
+ },
11
+ {
12
+ method: 'POST',
13
+ path: '/api/product',
14
+ access: { module: 'product', action: 'create' },
15
+ handler: (ctx: Context) =>
16
+ (app.controller.product as { create(ctx: Context): void }).create(ctx),
17
+ },
18
+ {
19
+ method: 'GET',
20
+ path: '/api/product/:id',
21
+ access: { module: 'product', action: 'read' },
22
+ handler: (ctx: Context) =>
23
+ (app.controller.product as { detail(ctx: Context): void }).detail(ctx),
24
+ },
25
+ {
26
+ method: 'PUT',
27
+ path: '/api/product/:id',
28
+ access: { module: 'product', action: 'update' },
29
+ handler: (ctx: Context) =>
30
+ (app.controller.product as { update(ctx: Context): void }).update(ctx),
31
+ },
32
+ {
33
+ method: 'DELETE',
34
+ path: '/api/product/:id',
35
+ access: { module: 'product', action: 'delete' },
36
+ handler: (ctx: Context) =>
37
+ (app.controller.product as { remove(ctx: Context): void }).remove(ctx),
38
+ },
39
+ ]);
@@ -0,0 +1,56 @@
1
+ import { BaseService } from '@ynight/framework/server';
2
+
3
+ export default class ProductService extends BaseService {
4
+ private readonly products: Array<{
5
+ id: string;
6
+ productName: string;
7
+ price: number;
8
+ stock: number;
9
+ status: 'on' | 'off';
10
+ createTime: string;
11
+ }> = [
12
+ {
13
+ id: 'p-1',
14
+ productName: '示例商品',
15
+ price: 99,
16
+ stock: 10,
17
+ status: 'on',
18
+ createTime: new Date().toISOString(),
19
+ },
20
+ ];
21
+
22
+ list() {
23
+ return this.products;
24
+ }
25
+
26
+ create(payload: Omit<(typeof this.products)[number], 'id' | 'createTime'>) {
27
+ const product = {
28
+ ...payload,
29
+ id: `p-${this.products.length + 1}`,
30
+ createTime: new Date().toISOString(),
31
+ };
32
+ this.products.push(product);
33
+ return product;
34
+ }
35
+
36
+ get(id: string) {
37
+ return this.products.find(item => item.id === id);
38
+ }
39
+
40
+ update(id: string, payload: Record<string, unknown>) {
41
+ const product = this.get(id);
42
+ if (!product) return undefined;
43
+ if (typeof payload.productName === 'string') product.productName = payload.productName;
44
+ if (typeof payload.price === 'number') product.price = payload.price;
45
+ if (typeof payload.stock === 'number') product.stock = payload.stock;
46
+ if (payload.status === 'on' || payload.status === 'off') product.status = payload.status;
47
+ return product;
48
+ }
49
+
50
+ remove(id: string) {
51
+ const index = this.products.findIndex(item => item.id === id);
52
+ if (index < 0) return false;
53
+ this.products.splice(index, 1);
54
+ return true;
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@ynight/cli/tsconfig/client.json",
3
+ "include": ["types/empty-client.ts"]
4
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@ynight/cli/tsconfig/server.json",
3
+ "compilerOptions": { "rootDir": ".", "outDir": ".elpis/build/app" },
4
+ "include": ["elpis.config.ts", "server/**/*.ts", "model/**/*.ts", ".elpis/types/**/*.d.ts"],
5
+ "exclude": ["client", "dist", ".elpis/build"]
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@ynight/cli/tsconfig/server.json",
3
+ "compilerOptions": { "rootDir": ".", "outDir": ".elpis/build/app" },
4
+ "include": ["elpis.config.ts", "server/**/*.ts", "model/**/*.ts", ".elpis/types/**/*.d.ts"],
5
+ "exclude": ["client", "dist", ".elpis/build"]
6
+ }
@@ -0,0 +1,2 @@
1
+ /** zero-client 项目没有 Vue 源码;此文件仅给无条件执行的 vue-tsc 提供类型输入。 */
2
+ export {};