@_tc/template-core 0.0.1-bate.53 → 0.0.1-bate.55

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 (33) hide show
  1. package/.skills/tc-component-usage-skills/SKILL.md +2 -0
  2. package/.skills/tc-component-usage-skills/reference/component-api.md +3 -3
  3. package/.skills/tc-component-usage-skills/reference/patterns.md +2 -2
  4. package/.skills/tc-component-usage-skills/reference/template-core-frontend.md +1 -1
  5. package/.skills/tc-generator/reference/project-template/model/product/project/default.js +2 -0
  6. package/.skills/tc-generator/reference/runtime-extensions.md +70 -0
  7. package/AGENT_README.md +67 -12
  8. package/README.md +94 -13
  9. package/cjs/app/extend/crypto.js +1 -0
  10. package/cjs/app/view/entry.tpl +3 -0
  11. package/cjs/bundler/index.js +1 -1
  12. package/cjs/bundler/utils.js +3 -1
  13. package/esm/app/extend/crypto.js +31 -0
  14. package/esm/app/view/entry.tpl +3 -0
  15. package/esm/bundler/index.js +3 -7
  16. package/esm/bundler/utils.js +94 -59
  17. package/fe/frontend/apps/dash/Dashboard.js +2 -2
  18. package/fe/frontend/apps/ui-components/index.js +2 -10
  19. package/fe/frontend/src/common/auth/index.d.ts +6 -2
  20. package/fe/frontend/src/common/auth/index.js +13 -10
  21. package/fe/frontend/src/common/request.js +1 -1
  22. package/fe/frontend/src/index.d.ts +2 -1
  23. package/fe/frontend/src/index.js +2 -1
  24. package/fe/frontend/src/stores/mode.js +1 -1
  25. package/fe/model/types/data/component.d.ts +2 -2
  26. package/model/frontend/src/common/auth/index.d.ts +6 -2
  27. package/model/model/types/data/component.d.ts +2 -2
  28. package/package.json +1 -1
  29. package/types/app/extend/crypto.d.ts +64 -0
  30. package/types/app/type.d.ts +1 -0
  31. package/types/app/typings.d.ts +2 -0
  32. package/types/bundler/index.d.ts +1 -1
  33. package/types/index.d.ts +1 -0
@@ -119,6 +119,8 @@ When consuming the published TemplateCore package, `@_tc/template-core/fe/rc` re
119
119
  import { Button, DataTable, TableSearch } from '@_tc/template-core/fe/rc'
120
120
  ```
121
121
 
122
+ Inside this source repository, use `@tc/ui-react` for the raw UI workspace package. The `@_tc/template-core/fe/rc` path is a published TemplateCore package export generated into `dist/package.json`.
123
+
122
124
  Style entry:
123
125
 
124
126
  ```tsx
@@ -310,7 +310,7 @@ interface SchemaFormProps<T> {
310
310
  buttonClassName?: string
311
311
  submitButtonProps?: Omit<ButtonElementProps, 'type' | 'onClick' | 'children'>
312
312
  cancelButtonProps?: Omit<ButtonElementProps, 'onClick' | 'children'>
313
- preserve?: boolean // default: false
313
+ preserve?: boolean // passed through to rc-field-form; TemplateCore does not set a default
314
314
  }
315
315
  ```
316
316
 
@@ -709,8 +709,8 @@ Max 5 visible messages.
709
709
 
710
710
  ```typescript
711
711
  interface NotificationProps {
712
- type: 'info' | 'success' | 'warning' | 'error'
713
- title: ReactNode
712
+ type?: 'info' | 'success' | 'warning' | 'error' // default: 'info'
713
+ title?: ReactNode
714
714
  message?: ReactNode
715
715
  duration?: number
716
716
  closable?: boolean
@@ -350,10 +350,10 @@ import { Notification } from '@tc/ui-react'
350
350
 
351
351
  ## Compound Component Pattern
352
352
 
353
- Three components use compound patterns with Context:
353
+ Three component families use context-based composition:
354
354
 
355
355
  1. **Menu**: `<Menu>` + `<Menu.Item>` + `<Menu.SubMenu>` + `<Menu.CollapseToggle>`
356
- 2. **Radio**: `<Radio.Group>` + `<Radio>`
356
+ 2. **Radio**: `<RadioGroup>` + `<Radio>`
357
357
  3. **Breadcrumb**: `<Breadcrumb>` + `<BreadcrumbList>` + `<BreadcrumbItem>` + ...
358
358
 
359
359
  Children auto-detect parent via Context and inherit configuration.
@@ -9,7 +9,7 @@ import { AsyncSelect, LanguageSwitch, ThemeSwitch } from '@_tc/template-core/fe'
9
9
  import { Button, DataTable, TableSearch } from '@_tc/template-core/fe/rc'
10
10
  ```
11
11
 
12
- Inside this repository, `@tc/ui-react` is also valid for the raw UI library. In published business projects, prefer `@_tc/template-core/fe/rc` for UI components and `@_tc/template-core/fe` for frontend runtime helpers.
12
+ Inside this repository, use `@tc/ui-react` for the raw UI workspace package. In published business projects, prefer `@_tc/template-core/fe/rc` for UI components and `@_tc/template-core/fe` for frontend runtime helpers.
13
13
 
14
14
  ## LanguageSwitch
15
15
 
@@ -1,5 +1,7 @@
1
1
  module.exports = {
2
2
  name: "Default Product Project",
3
3
  desc: "Project-level overrides can be added here.",
4
+ // Single-project scaffolds can include projk directly. For shared multi-project
5
+ // models, prefer keeping mode.js project-neutral and override homePage per project.
4
6
  homePage: "/_sidebar_/product?projk=default",
5
7
  };
@@ -76,6 +76,74 @@ module.exports = (app) => {
76
76
 
77
77
  If the runtime does not support the built-in SQLite module or the project needs another store, generate `app/extend/db.js` or `app/extend/db.ts` and preserve the same method names so existing controllers keep working.
78
78
 
79
+ ## Default Crypto
80
+
81
+ `app.extends.crypto` is available by default for backend code. It uses `app.config.signKey` as the default signing secret.
82
+
83
+ Use it for lightweight signed payloads, auth tokens, API signatures, base64url encoding, and constant-time string comparison.
84
+
85
+ Common methods:
86
+
87
+ ```ts
88
+ app.extends.crypto.base64urlEncode(value)
89
+ app.extends.crypto.base64urlDecode(value)
90
+ app.extends.crypto.base64urlDecodeToBuffer(value)
91
+ app.extends.crypto.hmacSign(payload, { secret, algorithm })
92
+ app.extends.crypto.safeEqual(left, right)
93
+ app.extends.crypto.createSignedPayload(payload, { secret, algorithm, separator })
94
+ app.extends.crypto.verifySignedPayload(token, { secret, algorithm, separator })
95
+ ```
96
+
97
+ Signed payload example:
98
+
99
+ ```js
100
+ module.exports = (app) => {
101
+ return class AuthService {
102
+ createToken(account) {
103
+ const now = Date.now();
104
+
105
+ return app.extends.crypto.createSignedPayload({
106
+ account,
107
+ exp: now + 1000 * 60 * 60 * 24 * 7,
108
+ iat: now,
109
+ });
110
+ }
111
+
112
+ verifyToken(token) {
113
+ const payload = app.extends.crypto.verifySignedPayload(
114
+ token && token.replace(/^Bearer\s+/i, "")
115
+ );
116
+
117
+ if (!payload || Date.now() > payload.exp) return null;
118
+
119
+ return payload;
120
+ }
121
+
122
+ comparePassword(inputPassword, storedPassword) {
123
+ return app.extends.crypto.safeEqual(inputPassword, storedPassword);
124
+ }
125
+ };
126
+ };
127
+ ```
128
+
129
+ Custom signing options:
130
+
131
+ ```js
132
+ const token = app.extends.crypto.createSignedPayload(
133
+ { id: 1, scope: "admin" },
134
+ {
135
+ secret: "custom-secret",
136
+ algorithm: "sha512",
137
+ separator: ".",
138
+ }
139
+ );
140
+
141
+ const payload = app.extends.crypto.verifySignedPayload(token, {
142
+ secret: "custom-secret",
143
+ algorithm: "sha512",
144
+ });
145
+ ```
146
+
79
147
  ## Dashboard Component Slot
80
148
 
81
149
  Use `frontend/extended/dash/components.tsx` to fill supported Dashboard slots. The current slot is:
@@ -177,6 +245,8 @@ Import UI components from the published UI re-export:
177
245
  import { Button, DataTable, TableSearch } from '@_tc/template-core/fe/rc'
178
246
  ```
179
247
 
248
+ Inside this source repository, use `@tc/ui-react` for the raw UI workspace package. The `@_tc/template-core/fe/rc` path is for published TemplateCore consumers.
249
+
180
250
  `LanguageSwitch` updates the shared i18n language and persists through `tc_language`. `ThemeSwitch` toggles the root `dark` class and persists through `tc_theme`. `AsyncSelect` loads options from a `fetchConfig` endpoint returning `{ label, value, searchText? }[]`.
181
251
 
182
252
  ## SchemaForm Extensions
package/AGENT_README.md CHANGED
@@ -12,7 +12,7 @@ TemplateCore 是一个 TypeScript + Koa + React 的后台框架包。它提供
12
12
  - 前端构建 `buildFE()` 和消费方 Node/backend 构建 `buildBE()`。
13
13
  - 类型扩展机制,用于扩展 SchemaForm 字段、CallCom 组件和 Koa app 类型。
14
14
 
15
- 优先使用公开入口,不要从包内部源码路径导入。
15
+ 优先使用公开入口,不要从包内部源码路径导入。下面这些入口以 npm 发布产物为准;源码仓库根 `package.json` 的 `exports` 会在构建发布包时生成到 `dist/package.json`。
16
16
 
17
17
  | 入口 | 用途 |
18
18
  | --- | --- |
@@ -83,11 +83,11 @@ export default {
83
83
  http://localhost:9000/dash?projk=demo
84
84
  ```
85
85
 
86
- `projk` 对应 `model/{modelKey}/project/{projectKey}.ts`。
86
+ `projk` 对应 `model/{modelKey}/project/{projectKey}.ts` 里的 `projectKey`,运行时按 project key 全局查找,建议保持全局唯一。
87
87
 
88
88
  ## 3. 后端约定
89
89
 
90
- `serverStart({ baseDir })` 会按目录自动加载业务代码:
90
+ `serverStart({ baseDir })` 会按目录自动加载业务代码;`model/` 由内置 project service 通过 `modelLoader(app)` 读取,不会在 `serverStart` 阶段直接挂到 `app.model`:
91
91
 
92
92
  ```text
93
93
  app/controller/**/*.(js|ts) -> app.controller
@@ -99,7 +99,7 @@ app/router-schema/**/*.(js|ts) -> app.routerSchema
99
99
  app/extend/*.(js|ts) -> app.extends
100
100
  config/config.default.(js|ts) -> app.config
101
101
  config/config.{env}.(js|ts) -> app.config
102
- model/**/*.(js|ts) -> 项目模型配置
102
+ model/**/*.(js|ts) -> 内置 project service 读取的项目模型配置
103
103
  ```
104
104
 
105
105
  Controller 示例:
@@ -184,6 +184,7 @@ export default (_app: KoaApp) => {
184
184
  内置扩展:
185
185
 
186
186
  - `app.extends.$fetch`:Node 侧 axios 风格 fetch 实例。
187
+ - `app.extends.crypto`:Node 侧加密辅助方法,支持 base64url 编解码、HMAC 签名、签名 payload 和常量时间比较。
187
188
  - `app.extends.db`:默认 SQLite 数据存储,可用业务 `app/extend/db.ts` 覆盖。
188
189
 
189
190
  ## 4. model 配置
@@ -361,11 +362,64 @@ import { Button, DataTable, Form, Input, Modal, Select } from '@_tc/template-cor
361
362
  - 请求方法:`request`、`get`、`post`、`put`、`patch`、`del`。
362
363
  - Schema 通信:`schemaEventBus`、`eventsInfo`、`merge`。
363
364
  - 共享状态:`modeStore`、`schemaStore`、`apiFreezerStore` 及对应 hooks。
364
- - 多语言:`i18nStore.addResources()`,配置文案可写 `$i18n::...`。
365
+ - 多语言:`i18nStore.getState().addResources()`,配置文案可写 `$i18n::...`。
365
366
  - 主题:`ThemeSwitch` 会同步根节点 `dark` class 和 `localStorage`。
366
367
 
367
368
  不要直接依赖 `frontend/src/...`、`packages/react/ui/...` 这类包内部路径。
368
369
 
370
+ ### 自定义前端入口和 HTML 插槽
371
+
372
+ 前端入口必须命名为 `.entry.tsx`、`.entry.ts`、`.entry.jsx` 或 `.entry.js`,否则 `buildFE()` 不会扫描。
373
+
374
+ 最小入口:
375
+
376
+ ```tsx
377
+ // frontend/admin/admin.entry.tsx
378
+ import { initApp } from '@_tc/template-core/fe'
379
+ import type { ReactNode } from 'react'
380
+
381
+ const AdminApp = ({ children }: { children: ReactNode }) => (
382
+ <main>{children}</main>
383
+ )
384
+
385
+ initApp(AdminApp, {
386
+ initModeData: false,
387
+ })
388
+ ```
389
+
390
+ 如果需要给页面加额外 `meta`、预连接资源或启动脚本,可以在入口同目录放 `.html` 插槽文件。这个文件不是完整 HTML,只能包含 `tc-slot` 块:
391
+
392
+ ```html
393
+ <!-- frontend/admin/admin.html -->
394
+ <!-- tc-slot:head -->
395
+ <meta name="description" content="Admin Console" />
396
+ <link rel="preconnect" href="https://example.com" />
397
+ <!-- /tc-slot:head -->
398
+
399
+ <!-- tc-slot:body-before-root -->
400
+ <div id="boot-mask"></div>
401
+ <!-- /tc-slot:body-before-root -->
402
+
403
+ <!-- tc-slot:body-after-root -->
404
+ <script>
405
+ window.__ADMIN_BOOT_TIME__ = Date.now()
406
+ </script>
407
+ <!-- /tc-slot:body-after-root -->
408
+ ```
409
+
410
+ 命名规则:
411
+
412
+ ```text
413
+ frontend/admin/admin.entry.tsx
414
+ frontend/admin/admin.html yes
415
+
416
+ frontend/report/index.entry.tsx
417
+ frontend/report/index.html yes
418
+ frontend/report/report.html yes
419
+ ```
420
+
421
+ 不要在插槽文件里写 `<!DOCTYPE html>`、`<html>`、`<body>` 或 `<div id="root"></div>`。页面外壳由 TemplateCore 的 `app/view/entry.tpl` 统一生成,`window._basePath`、`window._projKey`、`window._signKey`、`window.appOptions` 也由它注入。构建器会把合成后的临时 HTML 写入 `.tc-entry-html/`,这个目录不要提交,也不要在业务代码中引用。
422
+
369
423
  ## 6. 构建
370
424
 
371
425
  前端资源构建:
@@ -495,8 +549,9 @@ chokidar
495
549
 
496
550
  1. 在业务 `frontend/` 下添加入口 `*.entry.tsx`。
497
551
  2. 需要样式时引入 `@_tc/template-core/fe/tailwind_ui.css`。
498
- 3. `model` 菜单中使用 `moduleType: 'custom'`,让 `customConfig.path` 指向页面路径。
499
- 4. 使用 `buildFE('dev')` `buildFE('prod')` 生成资源。
552
+ 3. 如需改 head root 前后内容,只添加入口同目录 `.html` 插槽文件,不要写完整 HTML。
553
+ 4. `model` 菜单中使用 `moduleType: 'custom'`,让 `customConfig.path` 指向页面路径。
554
+ 5. 使用 `buildFE('dev')` 或 `buildFE('prod')` 生成资源。
500
555
 
501
556
  扩展前端类型或组件:
502
557
 
@@ -513,11 +568,11 @@ chokidar
513
568
  | 任务 | 建议阅读 |
514
569
  | --- | --- |
515
570
  | 完整安装和使用说明 | npm 包根目录 `README.md` |
516
- | 生成项目、model、CRUD | `.skills/tc-generator/SKILL.md` |
517
- | 组件选型和用法 | `.skills/tc-component-usage-skills/SKILL.md` |
518
- | model/schema 配置细节 | `.skills/tc-generator/reference/model-schema.md` |
519
- | 完整 CRUD 示例 | `.skills/tc-generator/reference/example.md` |
520
- | 可复制项目骨架 | `.skills/tc-generator/reference/project-template/` |
571
+ | 生成项目、model、CRUD | npm 包:`.skills/tc-generator/SKILL.md`;源码仓库:`skills/tc-generator/SKILL.md` |
572
+ | 组件选型和用法 | npm 包:`.skills/tc-component-usage-skills/SKILL.md`;源码仓库:`skills/tc-component-usage-skills/SKILL.md` |
573
+ | model/schema 配置细节 | npm 包:`.skills/tc-generator/reference/model-schema.md`;源码仓库:`skills/tc-generator/reference/model-schema.md` |
574
+ | 完整 CRUD 示例 | npm 包:`.skills/tc-generator/reference/example.md`;源码仓库:`skills/tc-generator/reference/example.md` |
575
+ | 可复制项目骨架 | npm 包:`.skills/tc-generator/reference/project-template/`;源码仓库:`skills/tc-generator/reference/project-template/` |
521
576
  | UI 组件现场预览 | 启动服务后访问 `/ui-components` |
522
577
  | 已安装包的类型细节 | 查看对应公开入口的 `.d.ts` 文件 |
523
578
 
package/README.md CHANGED
@@ -12,7 +12,7 @@ pnpm add @_tc/template-core
12
12
 
13
13
  - Node.js >= 18
14
14
  - pnpm >= 8
15
- - 使用内置前端时,需要安装 React、Vite peer dependencies
15
+ - 使用内置前端时,需要安装 peer dependencies;完整清单以包内 `package.json` 的 `peerDependencies` 为准,包括 Koa、React、Vite、路由、状态、表单和 UI 相关依赖。
16
16
 
17
17
  ## 可选:给项目 AI 助手的文档与 Skill
18
18
 
@@ -28,9 +28,9 @@ pnpm add @_tc/template-core
28
28
  如果你的 Agent 支持本地 skills 目录,可以从已安装的 npm 包复制安装。以 Codex 为例:
29
29
 
30
30
  ```bash
31
- mkdir -p “${CODEX_HOME:-$HOME/.codex}/skills
32
- cp -R node_modules/@_tc/template-core/.skills/tc-generator “${CODEX_HOME:-$HOME/.codex}/skills/tc-generator
33
- cp -R node_modules/@_tc/template-core/.skills/tc-component-usage-skills “${CODEX_HOME:-$HOME/.codex}/skills/tc-component-usage-skills
31
+ mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
32
+ cp -R node_modules/@_tc/template-core/.skills/tc-generator "${CODEX_HOME:-$HOME/.codex}/skills/tc-generator"
33
+ cp -R node_modules/@_tc/template-core/.skills/tc-component-usage-skills "${CODEX_HOME:-$HOME/.codex}/skills/tc-component-usage-skills"
34
34
  ```
35
35
 
36
36
  安装后重启对应 Agent,让新 skill 生效。使用时可以直接说”用 tc-generator 生成一个商品管理模块”或”用 tc-component-usage-skills 帮我写一个带搜索的分页表格”。
@@ -99,7 +99,6 @@ main().catch((error) => {
99
99
  | `baseDir` | `string` | 否 | - | 项目根目录,通常为 `join(__dirname, '.')` |
100
100
  | `pageBasePage` | `string` | 否 | `'/'` | 页面访问前缀,所有前端页面路由的基础路径 |
101
101
  | `homePage` | `string` | 否 | - | 应用首页路径 |
102
- | `frameBaseDir` | `string` | 否 | - | 框架根目录,通常不需要手动设置 |
103
102
  | `additionalPublicPaths` | `string | string[]` | 否 | - | 追加静态资源目录和 Nunjucks 模板查找目录,必须传绝对路径 |
104
103
 
105
104
  框架会自动读取 `config/config.default.ts` 中的配置(如 `port`、`env` 等)。
@@ -117,7 +116,7 @@ buildFE(mode: 'dev' | 'prod', options?: BuildFEOptions)
117
116
  - `'prod'`:生产模式,构建优化后的静态资源
118
117
 
119
118
  **options.output**:构建输出位置
120
- - `'frame'`(默认):输出到框架包内的 `node_modules/@_tc/template-core/app/public/dist`
119
+ - `'frame'`(默认):输出到当前导入格式对应的框架静态目录,例如 `node_modules/@_tc/template-core/esm/app/public/dist` 或 `node_modules/@_tc/template-core/cjs/app/public/dist`
121
120
  - `'run'`:输出到项目根目录的 `app/public/dist`,适合 TypeScript 编译后的项目
122
121
  - `() => string`:自定义函数,返回输出目录的绝对路径
123
122
 
@@ -153,7 +152,9 @@ http://localhost:9000/dash?projk=demo
153
152
 
154
153
  `projk` 对应 `model/{modelKey}/project/{projectKey}.ts` 里的 `projectKey`。
155
154
 
156
- ## 公开入口
155
+ ## 常用公开入口
156
+
157
+ 完整 exports 以发布包内 `package.json` 为准,除下表外还包含 `./fe/main`、`./fe/*`、`./fe/rc/*`、`./fe/rc/hooks/*` 等子路径。
157
158
 
158
159
  | 入口 | 用途 |
159
160
  | --- | --- |
@@ -292,12 +293,12 @@ import type {
292
293
 
293
294
  ### 前端多语言
294
295
 
295
- TemplateCore 前端复用 `@tc/ui-react/i18n`。框架内置中文、英文资源,项目可以继续追加自己的资源。
296
+ TemplateCore 前端通过 `@_tc/template-core/fe` 暴露 i18n 能力。框架内置中文、英文资源,项目可以继续追加自己的资源。
296
297
 
297
298
  **追加自定义语言资源**:
298
299
 
299
300
  ```ts
300
- import { i18nStore } from '@tc/ui-react/i18n'
301
+ import { i18nStore } from '@_tc/template-core/fe'
301
302
 
302
303
  i18nStore.getState().addResources('zh-CN', {
303
304
  business: {
@@ -383,7 +384,7 @@ const title = '$i18n::business.product.createTitle'
383
384
  **切换语言**:
384
385
 
385
386
  ```ts
386
- import { i18nStore } from '@tc/ui-react/i18n'
387
+ import { i18nStore } from '@_tc/template-core/fe'
387
388
 
388
389
  i18nStore.getState().setLanguage('en-US')
389
390
  i18nStore.getState().setLanguage('zh-CN')
@@ -582,7 +583,7 @@ try {
582
583
  | --- | --- | --- |
583
584
  | `Modal` | 模态框组件 | `@_tc/template-core/fe/rc` |
584
585
  | `Drawer` | 抽屉组件 | `@_tc/template-core/fe/rc` |
585
- | `Message` | 消息提示组件 | `@_tc/template-core/fe/rc` |
586
+ | `message` | 消息提示方法 | `@_tc/template-core/fe/rc` |
586
587
  | `Notification` | 通知组件 | `@_tc/template-core/fe/rc` |
587
588
  | `ConfirmDialog` | 确认对话框组件 | `@_tc/template-core/fe/rc` |
588
589
  | `Popup` | 弹出层组件 | `@_tc/template-core/fe/rc` |
@@ -658,6 +659,7 @@ model/**/*.(js|ts) -> 项目模型配置
658
659
  内置扩展:
659
660
 
660
661
  - `app.extends.$fetch`:Node 侧基于 `fetch` 的 axios 风格请求实例,支持 `get/post/put/patch/delete` 和 `create(config)`。
662
+ - `app.extends.crypto`:Node 侧加密辅助方法,支持 base64url 编解码、HMAC 签名、签名 payload 和常量时间比较。
661
663
  - `app.extends.db`:默认 SQLite 框架数据库,提供 `getDBData/setDBData` 等通用数据方法。
662
664
 
663
665
  ### 数据库扩展
@@ -721,6 +723,36 @@ SQL 注入边界:
721
723
  - `queryDB`、`runDB`、`getDBFirst` 是原始 SQL 入口,用户输入必须放到 `params`,不要拼接到 SQL 字符串里。
722
724
  - `execDB` 没有参数绑定能力,只建议用于固定 SQL,例如建表或迁移。
723
725
 
726
+ ### 加密扩展
727
+
728
+ `app.extends.crypto` 默认使用 `app.config.signKey` 作为签名密钥,适合生成登录 token、接口签名和安全比较。
729
+
730
+ 常用方法:
731
+
732
+ - `base64urlEncode(value)`:把字符串或二进制数据编码为 base64url。
733
+ - `base64urlDecode(value)`:把 base64url 解码回 UTF-8 字符串。
734
+ - `base64urlDecodeToBuffer(value)`:把 base64url 解码回 Buffer。
735
+ - `hmacSign(payload, options)`:对文本做 HMAC 签名,默认算法 `sha256`。
736
+ - `safeEqual(left, right)`:常量时间比较字符串。
737
+ - `createSignedPayload(payload, options)`:生成 `payload.signature` 形式的签名串。
738
+ - `verifySignedPayload(token, options)`:验证并解析签名串,失败返回 `null`。
739
+
740
+ ```ts
741
+ const token = app.extends.crypto.createSignedPayload({
742
+ account: 'admin',
743
+ exp: Date.now() + 1000 * 60 * 60 * 24 * 7,
744
+ iat: Date.now(),
745
+ })
746
+
747
+ const payload = app.extends.crypto.verifySignedPayload<{
748
+ account: string
749
+ exp: number
750
+ iat: number
751
+ }>(token)
752
+
753
+ const same = app.extends.crypto.safeEqual('abc', 'abc')
754
+ ```
755
+
724
756
  ```ts
725
757
  // 推荐:参数绑定
726
758
  const users = app.extends.db.queryDB(
@@ -1536,6 +1568,55 @@ frontend/admin/admin.entry.tsx
1536
1568
 
1537
1569
  入口文件名必须包含 `.entry.`,构建器才会扫描。
1538
1570
 
1571
+ 最小入口示例:
1572
+
1573
+ ```tsx
1574
+ // frontend/admin/admin.entry.tsx
1575
+ import { initApp } from '@_tc/template-core/fe'
1576
+ import type { ReactNode } from 'react'
1577
+
1578
+ const AdminApp = ({ children }: { children: ReactNode }) => (
1579
+ <main>{children}</main>
1580
+ )
1581
+
1582
+ initApp(AdminApp, {
1583
+ initModeData: false,
1584
+ })
1585
+ ```
1586
+
1587
+ 如果需要给页面加额外 `meta`、预连接资源或启动脚本,可以在入口同目录放 `.html` 插槽文件。这个文件不是完整 HTML,只能包含 `tc-slot` 块:
1588
+
1589
+ ```html
1590
+ <!-- frontend/admin/admin.html -->
1591
+ <!-- tc-slot:head -->
1592
+ <meta name="description" content="Admin Console" />
1593
+ <link rel="preconnect" href="https://example.com" />
1594
+ <!-- /tc-slot:head -->
1595
+
1596
+ <!-- tc-slot:body-before-root -->
1597
+ <div id="boot-mask"></div>
1598
+ <!-- /tc-slot:body-before-root -->
1599
+
1600
+ <!-- tc-slot:body-after-root -->
1601
+ <script>
1602
+ window.__ADMIN_BOOT_TIME__ = Date.now()
1603
+ </script>
1604
+ <!-- /tc-slot:body-after-root -->
1605
+ ```
1606
+
1607
+ 命名规则:
1608
+
1609
+ ```text
1610
+ frontend/admin/admin.entry.tsx
1611
+ frontend/admin/admin.html yes
1612
+
1613
+ frontend/report/index.entry.tsx
1614
+ frontend/report/index.html yes
1615
+ frontend/report/report.html yes
1616
+ ```
1617
+
1618
+ 不要在插槽文件里写 `<!DOCTYPE html>`、`<html>`、`<body>` 或 `<div id="root"></div>`。页面外壳由 TemplateCore 的 `app/view/entry.tpl` 统一生成,`window._basePath`、`window._projKey`、`window._signKey`、`window.appOptions` 也由它注入。
1619
+
1539
1620
  扩展 Dashboard 路由:
1540
1621
 
1541
1622
  ```tsx
@@ -1597,7 +1678,7 @@ buildFE('dev')
1597
1678
  buildFE('prod')
1598
1679
  ```
1599
1680
 
1600
- 默认输出到框架包的 `app/public/dist`。如果要输出到项目:
1681
+ 默认输出到当前导入格式对应的框架静态目录,例如 `esm/app/public/dist` 或 `cjs/app/public/dist`。如果要输出到项目:
1601
1682
 
1602
1683
  ```ts
1603
1684
  buildFE('prod', {
@@ -1768,4 +1849,4 @@ await buildBE({
1768
1849
  - Dashboard 登录守卫放在 `frontend/extended/dash/routeGuard.ts`;登录页通常不在 Dash 路由下,返回登录页 URL 即可。
1769
1850
  - 服务端兜底守卫放在业务 `app/router-guard.ts|js`,不是 `app/business` 子目录。
1770
1851
  - 前端入口必须命名为 `.entry.tsx`、`.entry.ts`、`.entry.jsx` 或 `.entry.js`。
1771
- - **使用方不建议在 `/frontend/apps/` 下保留 `html` 文件。** Vite 构建时会根据模板文件(`app/view/entry.tpl`)自动生成 html,如果保留 html 文件,可能与模板不同步而导致内容过时。
1852
+ - 入口同目录的 `.html` 只能作为插槽文件使用,不能写完整 HTML 文档;页面外壳始终由 `app/view/entry.tpl` 生成。
@@ -0,0 +1 @@
1
+ require(`../../_virtual/_rolldown/runtime.js`);let e=require(`node:crypto`);var t=`sha256`,n=`.`,r=`template-core-crypto`,i=(e,t)=>String(t??e.config.signKey??r),a=e=>e??t,o=e=>e||n,s=e=>Buffer.isBuffer(e)?e:Buffer.from(e),c=t=>{let n=e=>s(e).toString(`base64url`),r=e=>Buffer.from(e,`base64url`),c=e=>r(e).toString(`utf8`),l=(n,r)=>(0,e.createHmac)(a(r?.algorithm),i(t,r?.secret)).update(n).digest(`base64url`),u=(t,n)=>{let r=Buffer.from(t),i=Buffer.from(n);return r.length===i.length?(0,e.timingSafeEqual)(r,i):!1};return{base64urlEncode:n,base64urlDecode:c,base64urlDecodeToBuffer:r,hmacSign:l,safeEqual:u,createSignedPayload:(e,t)=>{let r=o(t?.separator),i=n(JSON.stringify(e));return`${i}${r}${l(i,t)}`},verifySignedPayload:(e,t)=>{if(!e)return null;let n=o(t?.separator),[r,i,...a]=e.split(n);if(!r||!i||a.length>0||!u(l(r,t),i))return null;try{return JSON.parse(c(r))}catch{return null}}}};module.exports=c;
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/x-icon" href="/static/logo.png" />
7
7
  <title>{{ name }}</title>
8
+ <!-- TC_HEAD_SLOT -->
8
9
  </head>
9
10
 
10
11
  <script id="input">
@@ -22,7 +23,9 @@
22
23
  </script>
23
24
 
24
25
  <body>
26
+ <!-- TC_BODY_BEFORE_ROOT_SLOT -->
25
27
  <div id="root"></div>
28
+ <!-- TC_BODY_AFTER_ROOT_SLOT -->
26
29
  </body>
27
30
 
28
31
  <!-- {{InsertScript}} -->
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./buildBE.js`),t=require(`../packages/common/log/index.js`),n=require(`./state.js`),r=require(`./dev.js`),i=require(`./prod.js`);var a=(e,a)=>{switch(n.setBuildFEOptions(a),e){case`dev`:r.dev();break;case`prod`:i.prod();break;default:t.logRed(`Usage: npx tsx scripts/index.ts [dev|prod]`)}};exports.buildBE=e.buildBE,exports.buildFE=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./buildBE.js`),t=require(`../packages/common/log/index.js`),n=require(`./state.js`),r=require(`./dev.js`),i=require(`./prod.js`);var a=(e,a)=>{switch(n.setBuildFEOptions(a),e){case`dev`:return r.dev();case`prod`:return i.prod();default:return t.logRed(`Usage: npx tsx scripts/index.ts [dev|prod]`),Promise.resolve()}};exports.buildBE=e.buildBE,exports.buildFE=a;
@@ -1 +1,3 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`../_virtual/_rolldown/runtime.js`),t=require(`../packages/utils/path.js`);require(`../packages/utils/index.js`);const n=require(`../packages/common/log/index.js`),r=require(`./state.js`);let i=require(`path`);i=e.__toESM(i);let a=require(`glob`);a=e.__toESM(a);let o=require(`fs`),s=require(`vite`),c=require(`@tailwindcss/vite`);c=e.__toESM(c);let l=require(`module`),u=require(`url`);var d=typeof __filename==`string`?__filename:(0,u.fileURLToPath)({}.url),f=i.default.dirname(d),p=process.cwd(),m=(0,l.createRequire)(d),h=i.default.resolve(f,`../../`),g=i.default.resolve(f,`../`),_=i.default.resolve(h,`./fe/frontend`),v=i.default.resolve(p,`.`),y=i.default.resolve(v,`./frontend`),b=e=>i.default.dirname(m.resolve(`${e}/package.json`));function x(){n.logWhite(`frameFEPath: `+_),n.logWhite(`runFEPath: `+y);let e=[_,y].reduce((e,n)=>{let r=[`js`,`jsx`,`ts`,`tsx`].map(e=>t.resolve(n,`/**`,`/*.`+e)),i=a.sync(r).filter(e=>e.includes(`.entry.`)).filter(e=>!e.includes(`.d.ts`));return[...e,...i]},[]),r={};if(!e.length)return r;let s;for(let n=0;n<e.length;n++){let c=e[n],l=i.default.resolve(c,`../`),u=a.sync(t.resolve(l,`*.html`))[0];if(u){let e=i.default.basename(u,`.html`);r[e]=i.default.resolve(p,u)}else{s||=(0,o.readFileSync)(i.default.resolve(g,`./app/view/entry.tpl`),`utf-8`);let e=c.split(i.default.sep).at(-1),t=s.replace(`<!-- {{InsertScript}} -->`,`<script type="module" src="./${e}"><\/script>`),n=l.split(i.default.sep).at(-1),a=i.default.join(l,n+`.html`);(0,o.writeFileSync)(a,t),r[n]=a}}return r}function S(){let e=r.buildFESharedState.options.output??`frame`;return e!==`frame`&&n.logRed(`Please pay attention to the build output.`),e===`run`?i.default.join(v,`./app/public/dist`):typeof e==`function`?e():i.default.join(g,`./app/public/dist`)}var C=async e=>{let t=(await import(`chokidar`)).watch(y,{ignored:/(^|[\/\\])\../,persistent:!0,ignoreInitial:!0}),r=!1;t.on(`change`,async t=>{if(!r){r=!0,n.logYellow(`changed: ${t}`);try{await e?.()}catch(e){n.logRed(e)}finally{r=!1}}}),n.logYellow(`watching ${y}/...`)};function w(e=`.html`){let t=[...new Set(Array.isArray(e)?e:[e])];return{name:`flatten-html`,apply:`build`,closeBundle(){let e=S();if(!(0,o.existsSync)(e))return;let r=a.sync(`**/*.html`,{cwd:e,dot:!0});for(let n of r){let r=i.default.join(e,n),a=i.default.basename(n,`.html`),s=`.entry`;a.indexOf(s)===-1&&(a+=s);for(let n of t){let t=a+n,s=i.default.join(e,t);r!==s&&(0,o.copyFileSync)(r,s)}t.some(t=>r===i.default.join(e,a+t))||(0,o.unlinkSync)(r)}let s=[...new Set(r.map(e=>i.default.dirname(e)))].sort().reverse(),c=e=>{if(!((0,o.existsSync)(e)?(0,o.readdirSync)(e):[1]).length){(0,o.rmdirSync)(e);let t=e.split(i.default.sep).slice(0,-1);t.length&&c(t.join(i.default.sep))}};for(let t of s)t&&t!==`.`&&c(i.default.join(e,t));let l=t.flatMap(t=>a.sync(`**/*`+t,{cwd:e,dot:!0}));for(let e of l)n.logYellow(`final file path -----> `+e)}}}function T(e){let t=[``,`.ts`,`.tsx`,`.js`,`.jsx`];return e.reduce((e,[n,r,a=y,s=`./defaultAlias`])=>{let c=t.map(e=>i.default.resolve(a,r+e)).find(e=>(0,o.existsSync)(e)),l=i.default.resolve(f,s);return c&&(l=c),e[n]=l,e},{})}var E=(...e)=>new RegExp(e.join(`[\\/]`)),D=e=>`_${e}_tcC`,O=async(e,t)=>{try{await(0,s.build)({configFile:!1,base:`/dist`,plugins:[(await import(`@vitejs/plugin-react`)).default(),w([`.tpl`,`.html`]),(0,c.default)()],build:{outDir:S(),emptyOutDir:!0,sourcemap:t===`dev`?`inline`:`hidden`,rolldownOptions:{input:e,output:{codeSplitting:{groups:[{name:D(`__nm`),test:E(`node_modules`),maxSize:5e5,minShareCount:2,priority:1},{name:D(`r`),test:E(`node_modules`,`react`),minShareCount:2,maxSize:5e5,priority:5},{name:D(`r-d`),test:E(`node_modules`,`react-dom`),minShareCount:2,maxSize:2e5,priority:10},{name:D(`r-r-d`),test:E(`node_modules`,`react-router-dom`),minShareCount:2,maxSize:5e5,priority:10},{name:D(`ajv`),test:E(`node_modules`,`ajv`),minShareCount:2,maxSize:2e5,priority:10},{name:D(`ui-lib`),test:E(`components`),minShareCount:2,maxSize:2e5,priority:5},{name:D(`ui-f-lib`),test:E(`components`,`.*?[Ff]orm.*?`),minShareCount:2,maxSize:5e4,priority:10}]}}}},resolve:{alias:{react:b(`react`),"react-dom":b(`react-dom`),"@tc/ui-react/hooks":i.default.resolve(f,`../../fe/packages/react/hooks/`),"@tc/ui-react":i.default.resolve(f,`../../fe/packages/react/ui/`),...T([[`@tc/scalability/SchemaForm/frameData`,`./extended/SchemaForm/data`,_],[`@tc/scalability/SchemaForm/data`,`./extended/SchemaForm/data`],[`@tc/scalability/SchemaPage/CallCom/data`,`./extended/SchemaPage/CallCom/data`],[`@tc/scalability/dash/customRoutes`,`./extended/dash/customRoutes`],[`@tc/scalability/dash/components`,`./extended/dash/components`],[`@tc/scalability/dash/routeGuard`,`./extended/dash/routeGuard`,void 0,`./defaultRouteGuard`]])}}}),n.logJoinColorized(n.colorize(`builded`,`green`),n.colorize(` -> `,`white`),n.colorize(`time: `,`cyan`),n.colorize(new Date,`pink`))}catch(e){if(n.logRed(e),t!==`dev`)throw e}};exports.VBuildFE=O,exports.entries=x,exports.outDir=S,exports.watchFiles=C;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`../_virtual/_rolldown/runtime.js`),t=require(`../packages/utils/path.js`);require(`../packages/utils/index.js`);const n=require(`../packages/common/log/index.js`),r=require(`./state.js`);let i=require(`path`);i=e.__toESM(i);let a=require(`glob`);a=e.__toESM(a);let o=require(`fs`),s=require(`vite`),c=require(`@tailwindcss/vite`);c=e.__toESM(c);let l=require(`module`),u=require(`url`);var d=typeof __filename==`string`?__filename:(0,u.fileURLToPath)({}.url),f=i.default.dirname(d),p=process.cwd(),m=(0,l.createRequire)(d),h=i.default.resolve(f,`../../`),g=i.default.resolve(f,`../`),_=i.default.resolve(h,`./fe/frontend`),v=i.default.resolve(p,`.`),y=i.default.resolve(v,`./frontend`),b=e=>i.default.dirname(m.resolve(`${e}/package.json`)),x=i.default.join(v,`.tc-entry-html`),S=`<!-- {{InsertScript}} -->`,C={head:`<!-- TC_HEAD_SLOT -->`,bodyBeforeRoot:`<!-- TC_BODY_BEFORE_ROOT_SLOT -->`,bodyAfterRoot:`<!-- TC_BODY_AFTER_ROOT_SLOT -->`},w=[`head`,`body-before-root`,`body-after-root`],T=()=>({head:``,"body-before-root":``,"body-after-root":``}),E=e=>e.split(i.default.sep).join(`/`),D=e=>{let t=E(e);return t.startsWith(`.`)||t.startsWith(`/`)?t:`./${t}`},O=e=>/<html[\s>]/i.test(e)&&e.includes(`window['_basePath']`)&&/<div\s+id=["']root["']\s*><\/div>/i.test(e),k=e=>{let t=T(),r=(0,o.readFileSync)(e,`utf-8`),i=/<!--\s*tc-slot:([\w-]+)\s*-->([\s\S]*?)<!--\s*\/tc-slot:\1\s*-->/g,a=!1,s=r;if(r.replace(i,(n,r,i)=>{if(a=!0,!w.includes(r))throw Error(`Invalid html slot "${r}" in ${e}. Allowed slots: ${w.join(`, `)}.`);return t[r]+=i.trim()?`${i}\n`:``,s=s.replace(n,``),n}),s.trim()){if(O(r))return n.logYellow(`ignored legacy generated entry html: ${e}`),T();throw Error([`Invalid custom entry html: ${e}.`,`Custom entry html can only contain tc-slot blocks, for example:`,`<!-- tc-slot:head -->`,`<meta name="description" content="..." />`,`<!-- /tc-slot:head -->`].join(`
2
+ `))}return a&&n.logYellow(`using custom entry html slots: ${e}`),t},A=(e,n)=>{let r=i.default.basename(e),s=[i.default.join(e,`${n}.html`),i.default.join(e,`${r}.html`)].find(e=>(0,o.existsSync)(e));if(s)return s;if(a.sync(t.resolve(e,`*.html`)).length)throw Error([`Entry html filename is not supported in ${e}.`,`Use ${n}.html or ${r}.html as a tc-slot file.`].join(`
3
+ `))},j=(e,t)=>e.replace(C.head,t.head).replace(C.bodyBeforeRoot,t[`body-before-root`]).replace(C.bodyAfterRoot,t[`body-after-root`]),M=(e,t,n,r)=>{let a=i.default.join(x,`${n}.html`),s=D(i.default.relative(i.default.dirname(a),t));return(0,o.writeFileSync)(a,j(e,r).replace(S,`<script type="module" src="${s}"><\/script>`)),a};function N(){n.logWhite(`frameFEPath: `+_),n.logWhite(`runFEPath: `+y);let e=[_,y].reduce((e,n)=>{let r=[`js`,`jsx`,`ts`,`tsx`].map(e=>t.resolve(n,`/**`,`/*.`+e)),i=a.sync(r).filter(e=>e.includes(`.entry.`)).filter(e=>!e.includes(`.d.ts`));return[...e,...i]},[]),r={};if(!e.length)return r;(0,o.rmSync)(x,{recursive:!0,force:!0}),(0,o.mkdirSync)(x,{recursive:!0});let s=(0,o.readFileSync)(i.default.resolve(g,`./app/view/entry.tpl`),`utf-8`);for(let t=0;t<e.length;t++){let n=e[t],a=i.default.resolve(n,`../`),o=A(a,i.default.basename(n).replace(/\.entry\.[^.]+$/,``)),c=o?i.default.basename(o,`.html`):a.split(i.default.sep).at(-1);r[c]=M(s,n,c,o?k(o):T())}return r}function P(){let e=r.buildFESharedState.options.output??`frame`;return e!==`frame`&&n.logRed(`Please pay attention to the build output.`),e===`run`?i.default.join(v,`./app/public/dist`):typeof e==`function`?e():i.default.join(g,`./app/public/dist`)}var F=async e=>{let t=(await import(`chokidar`)).watch(y,{ignored:/(^|[\/\\])\../,persistent:!0,ignoreInitial:!0}),r=!1;t.on(`change`,async t=>{if(!r){r=!0,n.logYellow(`changed: ${t}`);try{await e?.()}catch(e){n.logRed(e)}finally{r=!1}}}),n.logYellow(`watching ${y}/...`)};function I(e=`.html`){let t=[...new Set(Array.isArray(e)?e:[e])];return{name:`flatten-html`,apply:`build`,closeBundle(){let e=P();if(!(0,o.existsSync)(e))return;let r=a.sync(`**/*.html`,{cwd:e,dot:!0});for(let n of r){let r=i.default.join(e,n),a=i.default.basename(n,`.html`),s=`.entry`;a.indexOf(s)===-1&&(a+=s);for(let n of t){let t=a+n,s=i.default.join(e,t);r!==s&&(0,o.copyFileSync)(r,s)}t.some(t=>r===i.default.join(e,a+t))||(0,o.unlinkSync)(r)}let s=[...new Set(r.map(e=>i.default.dirname(e)))].sort().reverse(),c=e=>{if(!((0,o.existsSync)(e)?(0,o.readdirSync)(e):[1]).length){(0,o.rmdirSync)(e);let t=e.split(i.default.sep).slice(0,-1);t.length&&c(t.join(i.default.sep))}};for(let t of s)t&&t!==`.`&&c(i.default.join(e,t));let l=t.flatMap(t=>a.sync(`**/*`+t,{cwd:e,dot:!0}));for(let e of l)n.logYellow(`final file path -----> `+e)}}}function L(e){let t=[``,`.ts`,`.tsx`,`.js`,`.jsx`];return e.reduce((e,[n,r,a=y,s=`./defaultAlias`])=>{let c=t.map(e=>i.default.resolve(a,r+e)).find(e=>(0,o.existsSync)(e)),l=i.default.resolve(f,s);return c&&(l=c),e[n]=l,e},{})}var R=(...e)=>new RegExp(e.join(`[\\/]`)),z=e=>`_${e}_tcC`,B=async(e,t)=>{try{await(0,s.build)({configFile:!1,base:`/dist`,plugins:[(await import(`@vitejs/plugin-react`)).default(),I([`.tpl`,`.html`]),(0,c.default)()],build:{outDir:P(),emptyOutDir:!0,sourcemap:t===`dev`?`inline`:`hidden`,rolldownOptions:{input:e,output:{codeSplitting:{groups:[{name:z(`__nm`),test:R(`node_modules`),maxSize:5e5,minShareCount:2,priority:1},{name:z(`r`),test:R(`node_modules`,`react`),minShareCount:2,maxSize:5e5,priority:5},{name:z(`r-d`),test:R(`node_modules`,`react-dom`),minShareCount:2,maxSize:2e5,priority:10},{name:z(`r-r-d`),test:R(`node_modules`,`react-router-dom`),minShareCount:2,maxSize:5e5,priority:10},{name:z(`ajv`),test:R(`node_modules`,`ajv`),minShareCount:2,maxSize:2e5,priority:10},{name:z(`ui-lib`),test:R(`components`),minShareCount:2,maxSize:2e5,priority:5},{name:z(`ui-f-lib`),test:R(`components`,`.*?[Ff]orm.*?`),minShareCount:2,maxSize:5e4,priority:10}]}}}},resolve:{alias:{react:b(`react`),"react-dom":b(`react-dom`),"@tc/ui-react/hooks":i.default.resolve(f,`../../fe/packages/react/hooks/`),"@tc/ui-react":i.default.resolve(f,`../../fe/packages/react/ui/`),...L([[`@tc/scalability/SchemaForm/frameData`,`./extended/SchemaForm/data`,_],[`@tc/scalability/SchemaForm/data`,`./extended/SchemaForm/data`],[`@tc/scalability/SchemaPage/CallCom/data`,`./extended/SchemaPage/CallCom/data`],[`@tc/scalability/dash/customRoutes`,`./extended/dash/customRoutes`],[`@tc/scalability/dash/components`,`./extended/dash/components`],[`@tc/scalability/dash/routeGuard`,`./extended/dash/routeGuard`,void 0,`./defaultRouteGuard`]])}}}),n.logJoinColorized(n.colorize(`builded`,`green`),n.colorize(` -> `,`white`),n.colorize(`time: `,`cyan`),n.colorize(new Date,`pink`))}catch(e){if(n.logRed(e),t!==`dev`)throw e}};exports.VBuildFE=B,exports.entries=N,exports.outDir=P,exports.watchFiles=F;
@@ -0,0 +1,31 @@
1
+ import { createHmac as e, timingSafeEqual as t } from "node:crypto";
2
+ //#region app/extend/crypto.ts
3
+ var n = "sha256", r = ".", i = "template-core-crypto", a = (e, t) => String(t ?? e.config.signKey ?? i), o = (e) => e ?? n, s = (e) => e || r, c = (e) => Buffer.isBuffer(e) ? e : Buffer.from(e), l = (n) => {
4
+ let r = (e) => c(e).toString("base64url"), i = (e) => Buffer.from(e, "base64url"), l = (e) => i(e).toString("utf8"), u = (t, r) => e(o(r?.algorithm), a(n, r?.secret)).update(t).digest("base64url"), d = (e, n) => {
5
+ let r = Buffer.from(e), i = Buffer.from(n);
6
+ return r.length === i.length ? t(r, i) : !1;
7
+ };
8
+ return {
9
+ base64urlEncode: r,
10
+ base64urlDecode: l,
11
+ base64urlDecodeToBuffer: i,
12
+ hmacSign: u,
13
+ safeEqual: d,
14
+ createSignedPayload: (e, t) => {
15
+ let n = s(t?.separator), i = r(JSON.stringify(e));
16
+ return `${i}${n}${u(i, t)}`;
17
+ },
18
+ verifySignedPayload: (e, t) => {
19
+ if (!e) return null;
20
+ let n = s(t?.separator), [r, i, ...a] = e.split(n);
21
+ if (!r || !i || a.length > 0 || !d(u(r, t), i)) return null;
22
+ try {
23
+ return JSON.parse(l(r));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ };
29
+ };
30
+ //#endregion
31
+ export { l as default };
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/x-icon" href="/static/logo.png" />
7
7
  <title>{{ name }}</title>
8
+ <!-- TC_HEAD_SLOT -->
8
9
  </head>
9
10
 
10
11
  <script id="input">
@@ -22,7 +23,9 @@
22
23
  </script>
23
24
 
24
25
  <body>
26
+ <!-- TC_BODY_BEFORE_ROOT_SLOT -->
25
27
  <div id="root"></div>
28
+ <!-- TC_BODY_AFTER_ROOT_SLOT -->
26
29
  </body>
27
30
 
28
31
  <!-- {{InsertScript}} -->
@@ -6,13 +6,9 @@ import { prod as i } from "./prod.js";
6
6
  //#region bundler/index.ts
7
7
  var a = (e, a) => {
8
8
  switch (n(a), e) {
9
- case "dev":
10
- r();
11
- break;
12
- case "prod":
13
- i();
14
- break;
15
- default: t("Usage: npx tsx scripts/index.ts [dev|prod]");
9
+ case "dev": return r();
10
+ case "prod": return i();
11
+ default: return t("Usage: npx tsx scripts/index.ts [dev|prod]"), Promise.resolve();
16
12
  }
17
13
  };
18
14
  //#endregion
@@ -4,16 +4,54 @@ import { colorize as t, logJoinColorized as n, logRed as r, logWhite as i, logYe
4
4
  import { buildFESharedState as o } from "./state.js";
5
5
  import s from "path";
6
6
  import * as c from "glob";
7
- import { copyFileSync as l, existsSync as u, readFileSync as d, readdirSync as f, rmdirSync as p, unlinkSync as m, writeFileSync as h } from "fs";
8
- import { build as g } from "vite";
9
- import _ from "@tailwindcss/vite";
10
- import { createRequire as v } from "module";
11
- import { fileURLToPath as y } from "url";
7
+ import { copyFileSync as l, existsSync as u, mkdirSync as d, readFileSync as f, readdirSync as p, rmSync as m, rmdirSync as h, unlinkSync as g, writeFileSync as _ } from "fs";
8
+ import { build as v } from "vite";
9
+ import y from "@tailwindcss/vite";
10
+ import { createRequire as b } from "module";
11
+ import { fileURLToPath as x } from "url";
12
12
  //#region bundler/utils.ts
13
- var b = typeof __filename == "string" ? __filename : y(import.meta.url), x = s.dirname(b), S = process.cwd(), C = v(b), w = s.resolve(x, "../../"), T = s.resolve(x, "../"), E = s.resolve(w, "./fe/frontend"), D = s.resolve(S, "."), O = s.resolve(D, "./frontend"), k = (e) => s.dirname(C.resolve(`${e}/package.json`));
14
- function A() {
15
- i("frameFEPath: " + E), i("runFEPath: " + O);
16
- let t = [E, O].reduce((t, n) => {
13
+ var S = typeof __filename == "string" ? __filename : x(import.meta.url), C = s.dirname(S), w = process.cwd(), T = b(S), E = s.resolve(C, "../../"), D = s.resolve(C, "../"), O = s.resolve(E, "./fe/frontend"), k = s.resolve(w, "."), A = s.resolve(k, "./frontend"), j = (e) => s.dirname(T.resolve(`${e}/package.json`)), M = s.join(k, ".tc-entry-html"), N = "<!-- {{InsertScript}} -->", P = {
14
+ head: "<!-- TC_HEAD_SLOT -->",
15
+ bodyBeforeRoot: "<!-- TC_BODY_BEFORE_ROOT_SLOT -->",
16
+ bodyAfterRoot: "<!-- TC_BODY_AFTER_ROOT_SLOT -->"
17
+ }, F = [
18
+ "head",
19
+ "body-before-root",
20
+ "body-after-root"
21
+ ], I = () => ({
22
+ head: "",
23
+ "body-before-root": "",
24
+ "body-after-root": ""
25
+ }), L = (e) => e.split(s.sep).join("/"), R = (e) => {
26
+ let t = L(e);
27
+ return t.startsWith(".") || t.startsWith("/") ? t : `./${t}`;
28
+ }, z = (e) => /<html[\s>]/i.test(e) && e.includes("window['_basePath']") && /<div\s+id=["']root["']\s*><\/div>/i.test(e), B = (e) => {
29
+ let t = I(), n = f(e, "utf-8"), r = /<!--\s*tc-slot:([\w-]+)\s*-->([\s\S]*?)<!--\s*\/tc-slot:\1\s*-->/g, i = !1, o = n;
30
+ if (n.replace(r, (n, r, a) => {
31
+ if (i = !0, !F.includes(r)) throw Error(`Invalid html slot "${r}" in ${e}. Allowed slots: ${F.join(", ")}.`);
32
+ return t[r] += a.trim() ? `${a}\n` : "", o = o.replace(n, ""), n;
33
+ }), o.trim()) {
34
+ if (z(n)) return a(`ignored legacy generated entry html: ${e}`), I();
35
+ throw Error([
36
+ `Invalid custom entry html: ${e}.`,
37
+ "Custom entry html can only contain tc-slot blocks, for example:",
38
+ "<!-- tc-slot:head -->",
39
+ "<meta name=\"description\" content=\"...\" />",
40
+ "<!-- /tc-slot:head -->"
41
+ ].join("\n"));
42
+ }
43
+ return i && a(`using custom entry html slots: ${e}`), t;
44
+ }, V = (t, n) => {
45
+ let r = s.basename(t), i = [s.join(t, `${n}.html`), s.join(t, `${r}.html`)].find((e) => u(e));
46
+ if (i) return i;
47
+ if (c.sync(e(t, "*.html")).length) throw Error([`Entry html filename is not supported in ${t}.`, `Use ${n}.html or ${r}.html as a tc-slot file.`].join("\n"));
48
+ }, H = (e, t) => e.replace(P.head, t.head).replace(P.bodyBeforeRoot, t["body-before-root"]).replace(P.bodyAfterRoot, t["body-after-root"]), U = (e, t, n, r) => {
49
+ let i = s.join(M, `${n}.html`), a = R(s.relative(s.dirname(i), t));
50
+ return _(i, H(e, r).replace(N, `<script type="module" src="${a}"><\/script>`)), i;
51
+ };
52
+ function W() {
53
+ i("frameFEPath: " + O), i("runFEPath: " + A);
54
+ let t = [O, A].reduce((t, n) => {
17
55
  let r = [
18
56
  "js",
19
57
  "jsx",
@@ -23,26 +61,23 @@ function A() {
23
61
  return [...t, ...i];
24
62
  }, []), n = {};
25
63
  if (!t.length) return n;
26
- let r;
27
- for (let i = 0; i < t.length; i++) {
28
- let a = t[i], o = s.resolve(a, "../"), l = c.sync(e(o, "*.html"))[0];
29
- if (l) {
30
- let e = s.basename(l, ".html");
31
- n[e] = s.resolve(S, l);
32
- } else {
33
- r ||= d(s.resolve(T, "./app/view/entry.tpl"), "utf-8");
34
- let e = a.split(s.sep).at(-1), t = r.replace("<!-- {{InsertScript}} -->", `<script type="module" src="./${e}"><\/script>`), i = o.split(s.sep).at(-1), c = s.join(o, i + ".html");
35
- h(c, t), n[i] = c;
36
- }
64
+ m(M, {
65
+ recursive: !0,
66
+ force: !0
67
+ }), d(M, { recursive: !0 });
68
+ let r = f(s.resolve(D, "./app/view/entry.tpl"), "utf-8");
69
+ for (let e = 0; e < t.length; e++) {
70
+ let i = t[e], a = s.resolve(i, "../"), o = V(a, s.basename(i).replace(/\.entry\.[^.]+$/, "")), c = o ? s.basename(o, ".html") : a.split(s.sep).at(-1);
71
+ n[c] = U(r, i, c, o ? B(o) : I());
37
72
  }
38
73
  return n;
39
74
  }
40
- function j() {
75
+ function G() {
41
76
  let e = o.options.output ?? "frame";
42
- return e !== "frame" && r("Please pay attention to the build output."), e === "run" ? s.join(D, "./app/public/dist") : typeof e == "function" ? e() : s.join(T, "./app/public/dist");
77
+ return e !== "frame" && r("Please pay attention to the build output."), e === "run" ? s.join(k, "./app/public/dist") : typeof e == "function" ? e() : s.join(D, "./app/public/dist");
43
78
  }
44
- var M = async (e) => {
45
- let t = (await import("chokidar")).watch(O, {
79
+ var K = async (e) => {
80
+ let t = (await import("chokidar")).watch(A, {
46
81
  ignored: /(^|[\/\\])\../,
47
82
  persistent: !0,
48
83
  ignoreInitial: !0
@@ -58,15 +93,15 @@ var M = async (e) => {
58
93
  n = !1;
59
94
  }
60
95
  }
61
- }), a(`watching ${O}/...`);
96
+ }), a(`watching ${A}/...`);
62
97
  };
63
- function N(e = ".html") {
98
+ function q(e = ".html") {
64
99
  let t = [...new Set(Array.isArray(e) ? e : [e])];
65
100
  return {
66
101
  name: "flatten-html",
67
102
  apply: "build",
68
103
  closeBundle() {
69
- let e = j();
104
+ let e = G();
70
105
  if (!u(e)) return;
71
106
  let n = c.sync("**/*.html", {
72
107
  cwd: e,
@@ -79,11 +114,11 @@ function N(e = ".html") {
79
114
  let t = i + r, a = s.join(e, t);
80
115
  n !== a && l(n, a);
81
116
  }
82
- t.some((t) => n === s.join(e, i + t)) || m(n);
117
+ t.some((t) => n === s.join(e, i + t)) || g(n);
83
118
  }
84
119
  let r = [...new Set(n.map((e) => s.dirname(e)))].sort().reverse(), i = (e) => {
85
- if (!(u(e) ? f(e) : [1]).length) {
86
- p(e);
120
+ if (!(u(e) ? p(e) : [1]).length) {
121
+ h(e);
87
122
  let t = e.split(s.sep).slice(0, -1);
88
123
  t.length && i(t.join(s.sep));
89
124
  }
@@ -97,7 +132,7 @@ function N(e = ".html") {
97
132
  }
98
133
  };
99
134
  }
100
- function P(e) {
135
+ function J(e) {
101
136
  let t = [
102
137
  "",
103
138
  ".ts",
@@ -105,73 +140,73 @@ function P(e) {
105
140
  ".js",
106
141
  ".jsx"
107
142
  ];
108
- return e.reduce((e, [n, r, i = O, a = "./defaultAlias"]) => {
109
- let o = t.map((e) => s.resolve(i, r + e)).find((e) => u(e)), c = s.resolve(x, a);
143
+ return e.reduce((e, [n, r, i = A, a = "./defaultAlias"]) => {
144
+ let o = t.map((e) => s.resolve(i, r + e)).find((e) => u(e)), c = s.resolve(C, a);
110
145
  return o && (c = o), e[n] = c, e;
111
146
  }, {});
112
147
  }
113
- var F = (...e) => new RegExp(e.join("[\\/]")), I = (e) => `_${e}_tcC`, L = async (e, i) => {
148
+ var Y = (...e) => new RegExp(e.join("[\\/]")), X = (e) => `_${e}_tcC`, Z = async (e, i) => {
114
149
  try {
115
- await g({
150
+ await v({
116
151
  configFile: !1,
117
152
  base: "/dist",
118
153
  plugins: [
119
154
  (await import("@vitejs/plugin-react")).default(),
120
- N([".tpl", ".html"]),
121
- _()
155
+ q([".tpl", ".html"]),
156
+ y()
122
157
  ],
123
158
  build: {
124
- outDir: j(),
159
+ outDir: G(),
125
160
  emptyOutDir: !0,
126
161
  sourcemap: i === "dev" ? "inline" : "hidden",
127
162
  rolldownOptions: {
128
163
  input: e,
129
164
  output: { codeSplitting: { groups: [
130
165
  {
131
- name: I("__nm"),
132
- test: F("node_modules"),
166
+ name: X("__nm"),
167
+ test: Y("node_modules"),
133
168
  maxSize: 5e5,
134
169
  minShareCount: 2,
135
170
  priority: 1
136
171
  },
137
172
  {
138
- name: I("r"),
139
- test: F("node_modules", "react"),
173
+ name: X("r"),
174
+ test: Y("node_modules", "react"),
140
175
  minShareCount: 2,
141
176
  maxSize: 5e5,
142
177
  priority: 5
143
178
  },
144
179
  {
145
- name: I("r-d"),
146
- test: F("node_modules", "react-dom"),
180
+ name: X("r-d"),
181
+ test: Y("node_modules", "react-dom"),
147
182
  minShareCount: 2,
148
183
  maxSize: 2e5,
149
184
  priority: 10
150
185
  },
151
186
  {
152
- name: I("r-r-d"),
153
- test: F("node_modules", "react-router-dom"),
187
+ name: X("r-r-d"),
188
+ test: Y("node_modules", "react-router-dom"),
154
189
  minShareCount: 2,
155
190
  maxSize: 5e5,
156
191
  priority: 10
157
192
  },
158
193
  {
159
- name: I("ajv"),
160
- test: F("node_modules", "ajv"),
194
+ name: X("ajv"),
195
+ test: Y("node_modules", "ajv"),
161
196
  minShareCount: 2,
162
197
  maxSize: 2e5,
163
198
  priority: 10
164
199
  },
165
200
  {
166
- name: I("ui-lib"),
167
- test: F("components"),
201
+ name: X("ui-lib"),
202
+ test: Y("components"),
168
203
  minShareCount: 2,
169
204
  maxSize: 2e5,
170
205
  priority: 5
171
206
  },
172
207
  {
173
- name: I("ui-f-lib"),
174
- test: F("components", ".*?[Ff]orm.*?"),
208
+ name: X("ui-f-lib"),
209
+ test: Y("components", ".*?[Ff]orm.*?"),
175
210
  minShareCount: 2,
176
211
  maxSize: 5e4,
177
212
  priority: 10
@@ -180,15 +215,15 @@ var F = (...e) => new RegExp(e.join("[\\/]")), I = (e) => `_${e}_tcC`, L = async
180
215
  }
181
216
  },
182
217
  resolve: { alias: {
183
- react: k("react"),
184
- "react-dom": k("react-dom"),
185
- "@tc/ui-react/hooks": s.resolve(x, "../../fe/packages/react/hooks/"),
186
- "@tc/ui-react": s.resolve(x, "../../fe/packages/react/ui/"),
187
- ...P([
218
+ react: j("react"),
219
+ "react-dom": j("react-dom"),
220
+ "@tc/ui-react/hooks": s.resolve(C, "../../fe/packages/react/hooks/"),
221
+ "@tc/ui-react": s.resolve(C, "../../fe/packages/react/ui/"),
222
+ ...J([
188
223
  [
189
224
  "@tc/scalability/SchemaForm/frameData",
190
225
  "./extended/SchemaForm/data",
191
- E
226
+ O
192
227
  ],
193
228
  ["@tc/scalability/SchemaForm/data", "./extended/SchemaForm/data"],
194
229
  ["@tc/scalability/SchemaPage/CallCom/data", "./extended/SchemaPage/CallCom/data"],
@@ -208,4 +243,4 @@ var F = (...e) => new RegExp(e.join("[\\/]")), I = (e) => `_${e}_tcC`, L = async
208
243
  }
209
244
  };
210
245
  //#endregion
211
- export { L as VBuildFE, A as entries, j as outDir, M as watchFiles };
246
+ export { Z as VBuildFE, W as entries, G as outDir, K as watchFiles };
@@ -1,6 +1,6 @@
1
+ import { getAuthToken } from "../../src/common/auth/index.js";
1
2
  import { getText } from "../../src/common/language.js";
2
3
  import { generateMenuItemData } from "../../src/common/generateMenuData.js";
3
- import { getAuthTokenSync } from "../../src/common/auth/index.js";
4
4
  import defaultAlias_default from "../../../bundler/defaultAlias.js";
5
5
  import { Menu } from "../../../packages/react/ui/components/Menu/Menu.js";
6
6
  import "../../../packages/react/ui/components/index.js";
@@ -113,7 +113,7 @@ var Dashboard = () => {
113
113
  if (initData && projectInfo) {
114
114
  if (isInit.current) return;
115
115
  if (typeof defaultRouteGuard_default === "function") {
116
- const authToken = getAuthTokenSync();
116
+ const authToken = getAuthToken();
117
117
  const loginUrl = defaultRouteGuard_default({
118
118
  projectInfo,
119
119
  params,
@@ -1,16 +1,8 @@
1
- import { frontendLangKeys } from "../../src/language/index.js";
2
- import { getText } from "../../src/common/language.js";
3
- import { Button } from "../../../packages/react/ui/components/Button/Button.js";
4
- import "../../../packages/react/ui/components/index.js";
5
1
  import TestPage from "../../../packages/react/ui/components/testPage/index.js";
6
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { jsx } from "react/jsx-runtime";
7
3
  //#region frontend/apps/ui-components/index.tsx
8
4
  var ui_components_default = () => {
9
- return /* @__PURE__ */ jsxs("div", { children: [
10
- /* @__PURE__ */ jsx("h1", { children: getText(frontendLangKeys.uiComponentsTitle) }),
11
- /* @__PURE__ */ jsx(Button, { children: getText(frontendLangKeys.uiComponentsButton) }),
12
- /* @__PURE__ */ jsx(TestPage, {})
13
- ] });
5
+ return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(TestPage, {}) });
14
6
  };
15
7
  //#endregion
16
8
  export { ui_components_default as default };
@@ -1,6 +1,10 @@
1
+ export declare const localKeyMap: {
2
+ AUTH_LOCAL_KEY: string;
3
+ API_AUTH_LOCAL_KEY: string;
4
+ };
1
5
  export declare const AUTH_HEADER_KEY = "Authorization";
2
- export declare function getAuthTokenSync(): string | null;
3
- export declare function getAuthToken(): Promise<string | null>;
6
+ export declare function getAuthToken(): string | null;
7
+ export declare function setAuthToken(t: string): void | null;
4
8
  export declare function clearAuthToken(): Promise<void>;
5
9
  export declare const API_AUTH_HEADER_KEY = "projk";
6
10
  export declare const getApiAuth: () => string;
@@ -1,24 +1,27 @@
1
1
  //#region frontend/src/common/auth/index.ts
2
- var AUTH_LOCAL_KEY = "auth_token";
3
- var API_AUTH_LOCAL_KEY = "p_J_k";
2
+ var localKeyMap = {
3
+ AUTH_LOCAL_KEY: "auth_token",
4
+ API_AUTH_LOCAL_KEY: "p_J_k"
5
+ };
4
6
  var AUTH_HEADER_KEY = "Authorization";
5
- function getAuthTokenSync() {
7
+ function getAuthToken() {
6
8
  if (typeof localStorage === "undefined") return null;
7
- return localStorage.getItem(AUTH_LOCAL_KEY);
9
+ return localStorage.getItem(localKeyMap.AUTH_LOCAL_KEY);
8
10
  }
9
- async function getAuthToken() {
10
- return getAuthTokenSync();
11
+ function setAuthToken(t) {
12
+ if (typeof localStorage === "undefined") return null;
13
+ return localStorage.setItem(localKeyMap.AUTH_LOCAL_KEY, t);
11
14
  }
12
15
  async function clearAuthToken() {
13
16
  if (typeof localStorage === "undefined") return;
14
- localStorage.removeItem(AUTH_LOCAL_KEY);
17
+ localStorage.removeItem(localKeyMap.AUTH_LOCAL_KEY);
15
18
  }
16
19
  var API_AUTH_HEADER_KEY = "projk";
17
20
  var getApiAuth = () => {
18
- return localStorage.getItem(API_AUTH_LOCAL_KEY) ?? "";
21
+ return localStorage.getItem(localKeyMap.API_AUTH_LOCAL_KEY) ?? "";
19
22
  };
20
23
  var setApiAuth = (v) => {
21
- return localStorage.setItem(API_AUTH_LOCAL_KEY, v);
24
+ return localStorage.setItem(localKeyMap.API_AUTH_LOCAL_KEY, v);
22
25
  };
23
26
  //#endregion
24
- export { API_AUTH_HEADER_KEY, AUTH_HEADER_KEY, clearAuthToken, getApiAuth, getAuthToken, getAuthTokenSync, setApiAuth };
27
+ export { API_AUTH_HEADER_KEY, AUTH_HEADER_KEY, clearAuthToken, getApiAuth, getAuthToken, localKeyMap, setApiAuth, setAuthToken };
@@ -1,6 +1,6 @@
1
+ import { API_AUTH_HEADER_KEY, AUTH_HEADER_KEY, clearAuthToken, getApiAuth, getAuthToken } from "./auth/index.js";
1
2
  import { frontendLangKeys } from "../language/index.js";
2
3
  import { getText } from "./language.js";
3
- import { API_AUTH_HEADER_KEY, AUTH_HEADER_KEY, clearAuthToken, getApiAuth, getAuthToken } from "./auth/index.js";
4
4
  import { FreezeState, apiFreezerStore } from "../stores/apiFreezer.js";
5
5
  import FetchAxios from "../../../packages/common/http/index.js";
6
6
  import md5 from "md5";
@@ -2,10 +2,11 @@ import "./typing/scalability";
2
2
  export type * from "../apps/dash/types";
3
3
  export * from "./main";
4
4
  export type { CallComComponentsMap, CallComRenderer } from "./defaultPages/SchemaPage/schemaType";
5
- export type { FormFieldSchema, SchemaFormComponentsMap, SchemaFormNamespace, SelectProps } from "../../packages/react/ui/index";
6
5
  export * from "./components/index";
6
+ export type { FormFieldSchema, SchemaFormComponentsMap, SchemaFormNamespace, SelectProps } from "../../packages/react/ui/index";
7
7
  export { del, get, patch, post, put, request } from "./common/request";
8
8
  export * from "./defaultPages/SchemaPage/data/eventInfo";
9
9
  export { merge } from "./defaultPages/SchemaPage/utils/permissions";
10
10
  export * from "./exportStore";
11
11
  export { i18n, i18nStore } from "../../packages/react/ui/i18n/index";
12
+ export { getAuthToken, localKeyMap, setAuthToken } from "./common/auth/index";
@@ -1,3 +1,4 @@
1
+ import { getAuthToken, localKeyMap, setAuthToken } from "./common/auth/index.js";
1
2
  import { i18n, i18nStore } from "../../packages/common/i18n/index.js";
2
3
  import "../../packages/react/ui/i18n/index.js";
3
4
  import { FreezeState, apiFreezerStore, useApiFreezer } from "./stores/apiFreezer.js";
@@ -16,4 +17,4 @@ import ThemeSwitch_default from "./components/ThemeSwitch/index.js";
16
17
  import "./components/index.js";
17
18
  import { eventsInfo } from "./defaultPages/SchemaPage/data/eventInfo.js";
18
19
  import { merge } from "./defaultPages/SchemaPage/utils/permissions.js";
19
- export { AsyncSelect_default as AsyncSelect, FreezeState, HeaderView, LanguageSwitch_default as LanguageSwitch, ThemeSwitch_default as ThemeSwitch, apiFreezerStore, del, eventsInfo, generateRouter, get, i18n, i18nStore, initApp, merge, modeStore, patch, post, put, request, schemaEventBus, schemaStore, themeSwitchStorageKey, useApiFreezer, useModeStore, useSchemaStore };
20
+ export { AsyncSelect_default as AsyncSelect, FreezeState, HeaderView, LanguageSwitch_default as LanguageSwitch, ThemeSwitch_default as ThemeSwitch, apiFreezerStore, del, eventsInfo, generateRouter, get, getAuthToken, i18n, i18nStore, initApp, localKeyMap, merge, modeStore, patch, post, put, request, schemaEventBus, schemaStore, setAuthToken, themeSwitchStorageKey, useApiFreezer, useModeStore, useSchemaStore };
@@ -1,5 +1,5 @@
1
- import { createStoreHook } from "../../../packages/react/ui/lib/createStoreHook.js";
2
1
  import { setApiAuth } from "../common/auth/index.js";
2
+ import { createStoreHook } from "../../../packages/react/ui/lib/createStoreHook.js";
3
3
  import { getModelList, getProjectData } from "../api/baseInfo.js";
4
4
  import { deserializationUrlSearch } from "../common/menu.js";
5
5
  import { createStore } from "zustand";
@@ -1,5 +1,5 @@
1
- import type { FormFieldType, FormItemProps, SchemaFormAllPropsMap } from "../../../packages/react/ui/index";
2
1
  import type { BuiltInCallComConfigMap } from "../../../frontend/src/defaultPages/SchemaPage/components/CallCom/builtIn";
2
+ import type { FormFieldType, FormItemProps, SchemaFormAllPropsMap } from "../../../packages/react/ui/index";
3
3
  export type { BuiltInCallComConfigMap, BuiltInCallComNames } from "../../../frontend/src/defaultPages/SchemaPage/components/CallCom/builtIn";
4
4
  export declare namespace CallComNamespace {
5
5
  interface ConfigMap {
@@ -11,7 +11,7 @@ export type ComponentConfig = BuiltInCallComConfigMap & Partial<CallComNamespace
11
11
  export type ComponentNames = Extract<keyof ComponentConfig, string>;
12
12
  type FormComponentOption = FormItemProps & {
13
13
  visible?: boolean;
14
- disbaled?: boolean;
14
+ disabled?: boolean;
15
15
  default?: any;
16
16
  } & ComponentOption;
17
17
  export type BuiltInCallComOptionMap = {
@@ -1,6 +1,10 @@
1
+ export declare const localKeyMap: {
2
+ AUTH_LOCAL_KEY: string;
3
+ API_AUTH_LOCAL_KEY: string;
4
+ };
1
5
  export declare const AUTH_HEADER_KEY = "Authorization";
2
- export declare function getAuthTokenSync(): string | null;
3
- export declare function getAuthToken(): Promise<string | null>;
6
+ export declare function getAuthToken(): string | null;
7
+ export declare function setAuthToken(t: string): void | null;
4
8
  export declare function clearAuthToken(): Promise<void>;
5
9
  export declare const API_AUTH_HEADER_KEY = "projk";
6
10
  export declare const getApiAuth: () => string;
@@ -1,5 +1,5 @@
1
- import type { FormFieldType, FormItemProps, SchemaFormAllPropsMap } from "../../../../fe/packages/react/ui/index";
2
1
  import type { BuiltInCallComConfigMap } from "../../../../fe/frontend/src/defaultPages/SchemaPage/components/CallCom/builtIn";
2
+ import type { FormFieldType, FormItemProps, SchemaFormAllPropsMap } from "../../../../fe/packages/react/ui/index";
3
3
  export type { BuiltInCallComConfigMap, BuiltInCallComNames } from "../../../../fe/frontend/src/defaultPages/SchemaPage/components/CallCom/builtIn";
4
4
  export declare namespace CallComNamespace {
5
5
  interface ConfigMap {
@@ -11,7 +11,7 @@ export type ComponentConfig = BuiltInCallComConfigMap & Partial<CallComNamespace
11
11
  export type ComponentNames = Extract<keyof ComponentConfig, string>;
12
12
  type FormComponentOption = FormItemProps & {
13
13
  visible?: boolean;
14
- disbaled?: boolean;
14
+ disabled?: boolean;
15
15
  default?: any;
16
16
  } & ComponentOption;
17
17
  export type BuiltInCallComOptionMap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@_tc/template-core",
3
- "version": "0.0.1-bate.53",
3
+ "version": "0.0.1-bate.55",
4
4
  "description": "A full-stack TypeScript admin framework powered by Koa, React, and Vite - monorepo root",
5
5
  "types": "./types/index.d.ts",
6
6
  "exports": {
@@ -0,0 +1,64 @@
1
+ import type { KoaApp } from "../type";
2
+ export type CryptoHashAlgorithm = 'sha256' | 'sha384' | 'sha512';
3
+ export interface HmacSignOptions {
4
+ secret?: string;
5
+ algorithm?: CryptoHashAlgorithm;
6
+ }
7
+ export interface SignedPayloadOptions extends HmacSignOptions {
8
+ separator?: string;
9
+ }
10
+ export interface VerifySignedPayloadOptions extends HmacSignOptions {
11
+ separator?: string;
12
+ }
13
+ export interface CryptoExtend {
14
+ /**
15
+ * 将 UTF-8 字符串或二进制数据编码为 base64url 字符串。
16
+ *
17
+ * base64url 不包含 `+`、`/` 和结尾 `=`,适合放在 URL、Cookie、Header 或 token 片段中。
18
+ */
19
+ base64urlEncode: (value: string | Buffer | Uint8Array) => string;
20
+ /**
21
+ * 将 base64url 字符串解码为 UTF-8 字符串。
22
+ *
23
+ * 常用于还原 token payload;如果原始内容不是 UTF-8 文本,请使用 `base64urlDecodeToBuffer`。
24
+ */
25
+ base64urlDecode: (value: string) => string;
26
+ /**
27
+ * 将 base64url 字符串解码为 Buffer。
28
+ *
29
+ * 适合处理二进制 payload,避免 UTF-8 字符串转换造成数据损坏。
30
+ */
31
+ base64urlDecodeToBuffer: (value: string) => Buffer;
32
+ /**
33
+ * 使用 HMAC 对文本内容签名,并返回 base64url 签名。
34
+ *
35
+ * 默认使用 `app.config.signKey` 作为密钥、`sha256` 作为算法;可通过 options 覆盖。
36
+ */
37
+ hmacSign: (payload: string, options?: HmacSignOptions) => string;
38
+ /**
39
+ * 常量时间比较两个字符串是否相等。
40
+ *
41
+ * 用于比较密码、签名、token 等敏感值,降低普通字符串比较带来的计时攻击风险。
42
+ */
43
+ safeEqual: (left: string, right: string) => boolean;
44
+ /**
45
+ * 创建带签名的 payload 字符串。
46
+ *
47
+ * 返回格式默认为 `${base64url(JSON.stringify(payload))}.${hmacSign(encodedPayload)}`,
48
+ * 适合生成轻量登录 token 或一次性校验串。
49
+ */
50
+ createSignedPayload: <T>(payload: T, options?: SignedPayloadOptions) => string;
51
+ /**
52
+ * 验证带签名的 payload 字符串并还原原始对象。
53
+ *
54
+ * 签名不匹配、格式错误或 JSON 解析失败时返回 null,不向外抛出解析异常。
55
+ */
56
+ verifySignedPayload: <T = unknown>(signedPayload: string | null | undefined, options?: VerifySignedPayloadOptions) => T | null;
57
+ }
58
+ /**
59
+ * 创建框架加密扩展对象。
60
+ *
61
+ * 该扩展会挂载到 `app.extends.crypto`,用于复用 base64url 编码、HMAC 签名、签名 payload 和安全比较能力。
62
+ */
63
+ declare const _default: (app: KoaApp) => CryptoExtend;
64
+ export default _default;
@@ -3,3 +3,4 @@ export type KoaRouter = import('./typings').KoaRouter;
3
3
  export type Ctx = KoaApp['context'];
4
4
  export type ControllerFN<T extends object = object> = (app: KoaApp) => new () => T;
5
5
  export type ServiceFN<T extends object = object> = (app: KoaApp) => new () => T;
6
+ export type RouterFN = (app: KoaApp, router: KoaRouter) => void;
@@ -4,6 +4,7 @@ import type { DefaultAppConfig } from "../config/config.default";
4
4
  import type getProjectController from "./controller/project";
5
5
  import type getViewController from "./controller/view";
6
6
  import type getFetch from "./extend/$fetch";
7
+ import type getCrypto from "./extend/crypto";
7
8
  import type getDB from "./extend/db";
8
9
  import type generateErrorMessage from "./extend/generateErrorMessage";
9
10
  import type getLogger from "./extend/logger";
@@ -21,6 +22,7 @@ declare module '../packages/core/index' {
21
22
  view: GetInstance<typeof getViewController>;
22
23
  }
23
24
  interface IExtendsAugmented {
25
+ crypto: ReturnType<typeof getCrypto>;
24
26
  db: UnwrapPromise<ReturnType<typeof getDB>>;
25
27
  logger: UnwrapPromise<ReturnType<typeof getLogger>>;
26
28
  generateErrorMessage: ReturnType<typeof generateErrorMessage>;
@@ -1,4 +1,4 @@
1
1
  import type { BuildFEOptions } from "./state";
2
2
  export { buildBE } from "./buildBE";
3
3
  export type { BuildBEAlias, BuildBEFormat, BuildBEOptions, BuildBEResult } from "./buildBE";
4
- export declare const buildFE: (mode: "dev" | "prod", options?: BuildFEOptions) => void;
4
+ export declare const buildFE: (mode: "dev" | "prod", options?: BuildFEOptions) => Promise<void>;
package/types/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import "./app/typings";
4
4
  import { MOmit } from "./typings/type";
5
5
  export type { FrameworkAugment, KoaApp } from "./packages/core/index";
6
6
  export type { ControllerFN, Ctx, ServiceFN } from "./app/type";
7
+ export type { CryptoExtend, CryptoHashAlgorithm, HmacSignOptions, SignedPayloadOptions, VerifySignedPayloadOptions, } from "./app/extend/crypto";
7
8
  export type { DB, DBDataOptions, DBDataRecord, DBFactory, DBRunResult, FrameworkDB, ListDBDataOptions, SQLiteParams, SQLiteValue, } from "./app/extend/db";
8
9
  export type { Router };
9
10
  export declare const serverStart: (options?: MOmit<StartOptions, "frameBaseDir">) => Promise<import("./packages/core/index").KoaApp>;