@finesoft/front 0.5.1 → 0.5.3

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 (128) hide show
  1. package/README.md +4 -4
  2. package/dist/Outlet.svelte +39 -0
  3. package/dist/Outlet.svelte.d.ts +7 -0
  4. package/dist/browser-CR5vhgXg.mjs +1317 -0
  5. package/dist/browser-kFMjlLGT.d.mts +262 -0
  6. package/dist/browser.d.mts +7 -2
  7. package/dist/browser.mjs +9 -1
  8. package/dist/controller-types-CgmJ6-le.d.mts +16 -0
  9. package/dist/cookies-BiRlUanX.d.mts +786 -0
  10. package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
  11. package/dist/host-guard-DDWxLpFL.mjs +222 -0
  12. package/dist/http-B6CJqDyf.d.mts +46 -0
  13. package/dist/http-CaxrMD1A.d.mts +1 -0
  14. package/dist/http-D70PL72H.mjs +257 -0
  15. package/dist/http.d.mts +3 -0
  16. package/dist/http.mjs +2 -0
  17. package/dist/index-node.d.mts +15 -0
  18. package/dist/index-node.mjs +15 -0
  19. package/dist/index.d.mts +48 -697
  20. package/dist/index.mjs +44 -261
  21. package/dist/load-node.d.mts +5 -0
  22. package/dist/load-node.mjs +10 -0
  23. package/dist/load-portable.d.mts +5 -0
  24. package/dist/load-portable.mjs +8 -0
  25. package/dist/lru-map-BKoUAySU.mjs +50 -0
  26. package/dist/messages-CAt2QdGr.mjs +140 -0
  27. package/dist/native-contract-DuR25hYB.d.mts +14 -0
  28. package/dist/native-contract.d.mts +2 -0
  29. package/dist/native-contract.mjs +1 -0
  30. package/dist/node-D9hB4dsz.d.mts +35 -0
  31. package/dist/node.d.mts +2 -0
  32. package/dist/node.mjs +59 -0
  33. package/dist/path-CGFl2w7D.mjs +113 -0
  34. package/dist/path-CXT6xGPO.d.mts +261 -0
  35. package/dist/portable-CaxrMD1A.d.mts +1 -0
  36. package/dist/portable.d.mts +11 -0
  37. package/dist/portable.mjs +12 -0
  38. package/dist/proxy-2dSWO-Xw.d.mts +53 -0
  39. package/dist/proxy-z02VvGIj.mjs +7520 -0
  40. package/dist/public-types-BcJM-AYc.mjs +835 -0
  41. package/dist/react-DhwBRw01.d.mts +16 -0
  42. package/dist/react.d.mts +3 -0
  43. package/dist/react.mjs +29 -0
  44. package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
  45. package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
  46. package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
  47. package/dist/session-DnB4ZC3x.d.mts +1279 -0
  48. package/dist/src-BQBfaaPO.mjs +3825 -0
  49. package/dist/src-Ftl_0rhu.mjs +28 -0
  50. package/dist/ssr-BLzYP4wU.d.mts +207 -0
  51. package/dist/ssr-Tn4YkuxM.mjs +357 -0
  52. package/dist/ssr.d.mts +3 -0
  53. package/dist/ssr.mjs +3 -0
  54. package/dist/svelte-Dr5to3SE.d.mts +16 -0
  55. package/dist/svelte.d.mts +3 -0
  56. package/dist/svelte.mjs +13 -0
  57. package/dist/typegen-C-WeJCtf.d.mts +12 -0
  58. package/dist/typegen-cli.d.mts +1 -0
  59. package/dist/typegen-cli.mjs +11 -0
  60. package/dist/typegen.d.mts +3 -0
  61. package/dist/typegen.mjs +2 -0
  62. package/dist/types-B1BT0N3t.mjs +402 -0
  63. package/dist/undici-CPfL25Hr.mjs +22262 -0
  64. package/dist/vite-Cj4SPA8D.d.mts +277 -0
  65. package/dist/vite.d.mts +4 -0
  66. package/dist/vite.mjs +2354 -0
  67. package/dist/vue-DGmzuKho.d.mts +32 -0
  68. package/dist/vue.d.mts +3 -0
  69. package/dist/vue.mjs +56 -0
  70. package/dist/web.d.mts +6 -0
  71. package/dist/web.mjs +8 -0
  72. package/dist/worker.d.mts +2 -0
  73. package/dist/worker.mjs +2 -0
  74. package/docs/01-getting-started.md +67 -199
  75. package/docs/02-routing-and-controllers.md +163 -241
  76. package/docs/03-middleware.md +10 -212
  77. package/docs/04-rendering-and-hydration.md +7 -332
  78. package/docs/05-i18n.md +7 -237
  79. package/docs/06-http-client.md +20 -263
  80. package/docs/07-di-container.md +23 -257
  81. package/docs/08-observability.md +6 -286
  82. package/docs/09-server-and-deployment.md +59 -219
  83. package/docs/10-features-platform-pwa.md +7 -231
  84. package/docs/11-navigation.md +143 -288
  85. package/docs/12-session-restoration.md +6 -214
  86. package/docs/README.md +7 -7
  87. package/docs/advanced/custom-action-handler.md +29 -229
  88. package/docs/advanced/custom-adapter.md +7 -259
  89. package/docs/advanced/custom-event-recorder.md +7 -312
  90. package/docs/advanced/inline-proxy-codegen.md +6 -185
  91. package/docs/advanced/multi-tenant-scopes.md +10 -323
  92. package/docs/engineering/ci-release-flow.md +35 -222
  93. package/docs/engineering/project-structure.md +34 -277
  94. package/docs/engineering/testing.md +8 -310
  95. package/docs/pitfalls/container-scope-leak.md +2 -214
  96. package/docs/pitfalls/i18n-bundle-size.md +6 -176
  97. package/docs/pitfalls/proxy-binary-payloads.md +8 -12
  98. package/docs/pitfalls/ssr-hydration-mismatch.md +9 -155
  99. package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
  100. package/docs/zh/01-getting-started.md +67 -199
  101. package/docs/zh/02-routing-and-controllers.md +166 -244
  102. package/docs/zh/03-middleware.md +10 -212
  103. package/docs/zh/04-rendering-and-hydration.md +7 -332
  104. package/docs/zh/05-i18n.md +7 -237
  105. package/docs/zh/06-http-client.md +20 -263
  106. package/docs/zh/07-di-container.md +23 -257
  107. package/docs/zh/08-observability.md +6 -283
  108. package/docs/zh/09-server-and-deployment.md +59 -219
  109. package/docs/zh/10-features-platform-pwa.md +7 -231
  110. package/docs/zh/11-navigation.md +130 -290
  111. package/docs/zh/12-session-restoration.md +6 -214
  112. package/docs/zh/README.md +4 -4
  113. package/docs/zh/advanced/custom-action-handler.md +29 -229
  114. package/docs/zh/advanced/custom-adapter.md +7 -259
  115. package/docs/zh/advanced/custom-event-recorder.md +7 -312
  116. package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
  117. package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
  118. package/docs/zh/engineering/ci-release-flow.md +35 -222
  119. package/docs/zh/engineering/project-structure.md +34 -277
  120. package/docs/zh/engineering/testing.md +8 -310
  121. package/docs/zh/pitfalls/container-scope-leak.md +2 -214
  122. package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
  123. package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
  124. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +9 -155
  125. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
  126. package/package.json +118 -20
  127. package/dist/browser-BHhVWXik.mjs +0 -2
  128. package/dist/browser-BV2BBXm7.d.mts +0 -2811
@@ -1,244 +1,57 @@
1
- # 工程实践:CI 与发布流程
1
+ # 工程:验证与发布
2
2
 
3
- 框架自身的发布方式,以及给依赖它的应用设置同样 workflow 的方法。
3
+ 公开包是 `@finesoft/front` 和 `@finesoft/create-app`。Core、Web、Browser、SSR、Server 保持私有,由 front 构建合入明确的 ESM 入口和声明。各原生渲染器保留所选 UI 的外部依赖。
4
4
 
5
- ## 发布什么
5
+ ## 本地检查
6
6
 
7
- 只有 `@finesoft/front` 发布到 npm。内部的 `core` / `browser` / `ssr` / `server` 包通过 `tsdown` 的 `noExternal: [@finesoft/*]` 打包进 `front`。
8
-
9
- 这意味着:
10
-
11
- - 用户只装一个 npm 包
12
- - 内部重构不会带动多个版本号
13
- - 一份 CHANGELOG 可读
14
-
15
- `create-finesoft-app` 是它自己发布的包(CLI),独立于框架运行时。
16
-
17
- ## 发布 workflow
18
-
19
- 仓库只有一个 `.github/workflows/release.yml`,内联处理一切。触发:push 到 `main`。
20
-
21
- ```
22
- push to main
23
-
24
-
25
- Checkout main(用 PAT 而非 GITHUB_TOKEN)
26
-
27
-
28
- 对账 npm 注册表与 main
29
- ├── npm == main? → 继续
30
- ├── main > npm? → catch-up publish 当前 main 版本
31
- └── npm > main? → 报错,需人工排查
32
-
33
-
34
- 生成自动 changeset(每次 push 一个 patch)
35
-
36
-
37
- 应用版本 bump
38
- ├── 有变更? → 继续
39
- └── 无变更? → 完成,不发布
40
-
41
-
42
- Commit "chore(release): version packages"
43
-
44
-
45
- 构建所有包,publish @finesoft/front 到 npm
46
-
47
-
48
- Push commit + tag 回 main(带 rebase 重试)
49
- ```
50
-
51
- ### 为什么用一个内联 workflow 而不是 changesets/action 的 PR 模式
52
-
53
- 标准 changesets workflow 开一个 PR(「Version Packages」),合并时触发第二次 workflow run 来发布。**但 `GITHUB_TOKEN` 合并的 commit 不触发后续 workflow**(GitHub 反递归安全策略)—— 发布永远不跑。内联 workflow 一次 run 里做完,没有 PR hop。
54
-
55
- ### 为什么用 PAT 而不是 `GITHUB_TOKEN`
56
-
57
- 仓库 ruleset 强制签名 commit、线性历史、`main` 上必须 PR。bypass actor 包括 `RepositoryRole=5 (admin)` 但**不**包括 `github-actions[bot]`。GitHub UI 不允许把这个 bot 加进 bypass list。用 admin 用户拥有的 PAT push 命中已有的 admin bypass 条目。
58
-
59
- PAT 只授 `Contents: Read & Write` —— `git push` 需要的最小权限。
60
-
61
- ## 并发
62
-
63
- ```yaml
64
- concurrency: release-${{ github.ref }}
7
+ ```sh
8
+ vp install
9
+ vp run --filter '@finesoft/front...' build
10
+ vp check
11
+ vp test --coverage
12
+ vp run -r build
13
+ vp exec node scripts/verify-publish-transforms.mjs
14
+ vp exec node scripts/verify-runtime-boundaries.mjs
65
15
  ```
66
16
 
67
- 多次 push `main` 排队而不是取消。这点很重要:
68
-
69
- - publish 中途取消会让 npm 处于不一致状态
70
- - 每次 push 必须等前一次完成,避免版本号竞争
71
- - 下一个 run 的对账步骤会捡起前一个已发布的版本
72
-
73
- ## 幂等
74
-
75
- `changeset publish` 跳过 npm 上已有的版本。所以 push 到 `main` 在 publish 之后失败:
17
+ `vp pack` 构建库,`vp pm pack` 生成本地安装包。Front 的 prepack/postpack 成对改写并恢复原始发布清单;准备失败也会恢复。脚手架准备脚本复制六套当前模板,并生成可独立安装的依赖与 TypeScript 配置。
76
18
 
77
- - npm:有 0.1.75
78
- - main:还是 0.1.74
19
+ ## 仓库自动化
79
20
 
80
- 下一次 release run 的对账步骤检测到 `main < npm`,拒绝「往回 catch-up」并报错。人工补救:开一个 PR 把 `packages/front/package.json` bump npm 版本并合并。之后 push 正常。
21
+ Quality 的两个作业都会先构建 front 及其工作区依赖。根 Vite 配置加载的控制器类型生成器会导入 core 的构建产物,因此全新检出必须先完成这一步。
81
22
 
82
- ## 给应用设置同样的
23
+ PR 运行 Quality(`vp check` 和 `vp test --coverage`)。推送到 `main` 时启动 Release,复用同一份 Quality 工作流,并等待两项检查通过。CodeQL 独立扫描 `packages/{core,web,browser,ssr,server,front}/src/**`。
83
24
 
84
- 大多数应用不需要 publish 步骤 —— 它们有部署。但 changeset + 自动 bump 形状仍然管用:
25
+ Release 检出通过检查的提交,为两个公开包生成 patch changeset,应用 Changesets(保留手写 changeset minor/major 意图),同步锁文件并构建。随后使用内置 `GITHUB_TOKEN` 把版本提交推回 `main`,成功后才发布 npm。如果 `main` 已被其他推送推进,发布会停止,由新提交重新验证和发布;不会把旧构建强行变基,也不会覆盖后续提交。npm 发布成功后再推送 Git 标签。
85
26
 
86
- ```yaml
87
- name: Release
27
+ 发布作业使用 GitHub 托管 runner、`id-token: write` 和 npm OIDC Trusted Publishing,不使用依赖缓存或 npm token。Changesets 经 Vite+ 调用仓库指定的 pnpm 11;它原生支持 npm OIDC 和自动 provenance。两个公开包声明了源码仓库与公开 registry。工作流不再使用 `RELEASE_PUSH_TOKEN`、`NPM_TOKEN`。
88
28
 
89
- on:
90
- push:
91
- branches:
92
- - main
29
+ ### npm 配置
93
30
 
94
- concurrency: release-${{ github.ref }}
31
+ `@finesoft/front` `@finesoft/create-app` **各自**的 Settings 页面添加 GitHub Actions Trusted Publisher:
95
32
 
96
- jobs:
97
- release:
98
- runs-on: ubuntu-latest
99
- if: "!startsWith(github.event.head_commit.message, 'chore(release):')"
100
- permissions:
101
- contents: write
102
- steps:
103
- - uses: actions/checkout@v5
104
- with:
105
- ref: main
106
- fetch-depth: 0
107
- token: ${{ secrets.RELEASE_PUSH_TOKEN }}
33
+ | 字段 | 值 |
34
+ | -------------------- | ---------------------------------- |
35
+ | Organization or user | `GaKireiTsuki` |
36
+ | Repository | `finesoft-front` |
37
+ | Workflow filename | `release.yml` |
38
+ | Environment name | 留空;工作流未使用 environment |
39
+ | Allow npm publish | 勾选,允许当前自动发布流程直接发布 |
108
40
 
109
- - uses: voidzero-dev/setup-vp@v1
110
- with:
111
- node-version: 24
112
- cache: true
41
+ 保留 “Require two-factor authentication and disallow bypass 2fa tokens” 设置;它与 Trusted Publishing 兼容。当前流程直接发布,只有 stage 权限的连接无法授权。详见 [npm 官方文档](https://docs.npmjs.com/trusted-publishers/)。保存连接不等于发布成功,须检查首次 Release 运行和 npm 上的实际版本。
113
42
 
114
- - run: vp install --frozen-lockfile
43
+ ### 失败重试
115
44
 
116
- - name: Configure git
117
- run: |
118
- git config user.name "github-actions[bot]"
119
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
45
+ 修复原因后,在 **Release Run workflow → main** 手动运行,或执行:
120
46
 
121
- - name: Generate auto changeset
122
- run: vp run release:auto:changeset
123
-
124
- - name: Apply version bump
125
- id: bump
126
- run: |
127
- vp run version
128
- if git diff --quiet; then
129
- echo "should_publish=false" >> "$GITHUB_OUTPUT"
130
- else
131
- NEW=$(node -p "require('./package.json').version")
132
- echo "version=$NEW" >> "$GITHUB_OUTPUT"
133
- echo "should_publish=true" >> "$GITHUB_OUTPUT"
134
- fi
135
-
136
- - name: Commit version
137
- if: steps.bump.outputs.should_publish == 'true'
138
- run: |
139
- git add -A
140
- git commit -m "chore(release): version packages"
141
-
142
- - name: Build
143
- if: steps.bump.outputs.should_publish == 'true'
144
- run: vp run build
145
-
146
- - name: Deploy
147
- if: steps.bump.outputs.should_publish == 'true'
148
- run: vp run deploy # 你的部署命令
149
-
150
- - name: Push tag and commit
151
- if: steps.bump.outputs.should_publish == 'true'
152
- run: git push --follow-tags origin HEAD:main
47
+ ```sh
48
+ gh workflow run release.yml --ref main
153
49
  ```
154
50
 
155
- `vp run deploy` 换成你平台的部署命令(Vercel、Cloudflare、自家基建)。
156
-
157
- ## Conventional commits + 自动 changeset
158
-
159
- `release:auto:changeset` 脚本(本仓库的,每次 push 生成一个 patch changeset)有意简单 —— 每个合并 PR 变成一次 patch bump。要语义化版本驱动,替换为这样的脚本:
160
-
161
- - 读上一个 tag 之后的 `git log`
162
- - 把 commit 前缀(`feat:`、`fix:`、`BREAKING:`)映射到 changeset 类型
163
- - 写对应的 `.changeset/*.md`
164
-
165
- 框架仓库用纯 patch,因为:
166
-
167
- - 每次 push 是小变更;大变更经过 review 后还是会变成小 commit
168
- - 真正的破坏性变更很少,值得手写 changeset
169
- - 避开「前缀撒谎」的一类 bug
170
-
171
- 按你团队的提交习惯挑策略。
172
-
173
- ## 按 PR 校验(`quality.yml`)
174
-
175
- 仓库还有 `quality.yml` workflow,PR 上跑:
176
-
177
- ```yaml
178
- on:
179
- pull_request:
180
- push:
181
- branches:
182
- - main
183
-
184
- jobs:
185
- coverage:
186
- runs-on: ubuntu-latest
187
- steps:
188
- - uses: actions/checkout@v5
189
- - uses: voidzero-dev/setup-vp@v1
190
- with: { node-version: 24, cache: true }
191
- - run: vp install --frozen-lockfile
192
- - run: vp test --coverage
193
- - uses: actions/upload-artifact@v7
194
- with:
195
- name: coverage-report
196
- path: reports/coverage
197
- if-no-files-found: error
198
- ```
199
-
200
- `check` job(fmt + lint + types)在本仓库被 `if: false` 关掉,因为 Vite+ 本地走 pre-commit 跑这些。如果你的团队 pre-commit hook 跑得不稳,重新打开。
201
-
202
- ## CodeQL
203
-
204
- 仓库定时和 PR 上跑 CodeQL。扫描范围限定 `packages/{core,browser,ssr,server,front}/src/**`。测试、模板、脚本、脚手架都排除。
205
-
206
- 应用仓库启用默认 CodeQL 配置即可 —— 噪声低,能抓真实问题(open redirect、SQL 注入、密钥暴露)。
207
-
208
- ## 必需状态检查
209
-
210
- 仓库 ruleset 要求:
211
-
212
- - `Coverage`(来自 `quality.yml`)
213
- - `CodeQL`
214
-
215
- 两个都过才能合 PR。release workflow 通过 admin PAT bypass —— release 在 PR 合并后跑在 `main` 上,那些 check 在 PR 上已经过了。
216
-
217
- ## 迁移:从 changesets PR 模式到内联
218
-
219
- 把现有仓库从 `changesets/action`(PR 模式)迁过来:
220
-
221
- 1. 删旧 release workflow
222
- 2. 创建上面的内联 workflow
223
- 3. 生成一个 fine-grained PAT,存为 `RELEASE_PUSH_TOKEN`
224
- 4. 这次改动后下一次 push 到 main 会:
225
- - 检测到 main == npm(不需要 catch-up)
226
- - 生成一个 patch changeset
227
- - bump + publish + push 回 main
228
-
229
- 如果有旧 workflow 留下的「Version Packages」PR pending,不要合直接关掉。自动 changeset 会从那里接着写。
230
-
231
- ## 可能出错的情况
51
+ 手动运行会检查、构建并发布 `main` 已有的版本,**不会**再生成 changeset 或升版。Changesets 跳过 npm 已存在的版本,因此两个包只成功一个时可以补发另一个;标签步骤也会补齐缺失标签。如果失败发生在版本提交推回 `main` 之前,可在原始提交仍是分支头时重跑原推送任务,或推送修复提交来重新发版。
232
52
 
233
- | 症状 | 原因 | 修法 |
234
- | -------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------ |
235
- | `[remote rejected] HEAD -> main` | PAT actor 不在 ruleset bypass;PAT 缺 `Contents: Write` | 验 PAT scope;确认 push actor 是 admin 用户 |
236
- | `npm ... is ahead of main` | 前一次 run publish 后 push 失败 | 开 PR 把 `packages/front/package.json` 同步到 npm 版本 |
237
- | 一次 release commit 后 workflow 不触发 | `if: "!startsWith(github.event.head_commit.message, 'chore(release):'"` 过滤 | 按设计 —— 防递归 |
238
- | Pre-commit hook(`vp check`)CI 失败 | 本地没跑 formatter | 本地 `vp check --fix`;commit;重跑 |
53
+ 本地使用 `vp run changeset`、`vp run version` 管理版本。`vp run release` 构建并发布,`vp run release:publish` 仅发布已有构建。本地构建和安装包验证不等于推送、发布、部署或外部环境验收。
239
54
 
240
- ## 参考
55
+ ## 应用项目
241
56
 
242
- - 实际的 release workflow:`.github/workflows/release.yml`
243
- - 实际的 quality workflow:`.github/workflows/quality.yml`
244
- - [Changesets 文档](https://github.com/changesets/changesets) —— 理解 `vp run version` 和 `vp run release`
57
+ 使用生成项目自身的 `vp run build` 和部署配置。密钥放在所选主机的绑定中,只安装应用使用的渲染器与主机依赖。完整本地运行时和独立安装消费者证据见仓库 `docs/application-boundaries-acceptance.md`。
@@ -1,296 +1,53 @@
1
- # 工程实践:项目结构
1
+ # 应用结构
2
2
 
3
- 应用规模超过脚手架起点后的推荐布局。这是 ~20 路由、~10 工程师不大改就能用的形态。
3
+ React、Vue、Svelte 的六个模板保留 full、minimal 两档。同档位的页面、路由、数据、文案和交互一致;生成的每个项目都能独立使用,只依赖公开包入口。
4
4
 
5
- ## 布局
5
+ ## 选择档位
6
6
 
7
- ```
8
- my-app/
9
- ├── src/
10
- │ ├── bootstrap.ts # 路由 + DI 设置(SSR + CSR 共享)
11
- │ ├── main.ts # 浏览器入口
12
- │ ├── ssr.ts # SSR 入口
13
- │ ├── App.vue # 根组件
14
- │ │
15
- │ ├── routes/ # 路由定义,按领域分组
16
- │ │ ├── home.ts
17
- │ │ ├── product.ts
18
- │ │ ├── checkout.ts
19
- │ │ └── admin.ts
20
- │ │
21
- │ ├── controllers/ # Controller,每个 intent 一个文件
22
- │ │ ├── home.ts
23
- │ │ ├── product.ts
24
- │ │ └── checkout.ts
25
- │ │
26
- │ ├── views/ # 视图组件,与 Controller 镜像对应
27
- │ │ ├── Home.vue
28
- │ │ ├── Product.vue
29
- │ │ └── Checkout.vue
30
- │ │
31
- │ ├── lib/
32
- │ │ ├── api/ # HttpClient 子类
33
- │ │ │ ├── user.ts
34
- │ │ │ └── product.ts
35
- │ │ ├── guards/ # 可复用的中间件
36
- │ │ │ ├── auth.ts
37
- │ │ │ ├── locale.ts
38
- │ │ │ └── analytics.ts
39
- │ │ ├── di/
40
- │ │ │ ├── keys.ts # APP_KEYS 常量 map
41
- │ │ │ └── register.ts # 集中注册
42
- │ │ ├── i18n/
43
- │ │ │ └── translator.ts # Translator 工厂
44
- │ │ └── pages/
45
- │ │ └── types.ts # Page union 类型
46
- │ │
47
- │ ├── locales/
48
- │ │ ├── en-US.json
49
- │ │ ├── zh-Hans.json
50
- │ │ └── ja-JP.json
51
- │ │
52
- │ └── env.ts # 环境变量解析 + 校验
53
-
54
- ├── public/ # 静态资源,从 / 服务
55
- ├── index.html
56
- ├── vite.config.ts
57
- ├── package.json
58
- └── tsconfig.json
59
- ```
60
-
61
- ## 为什么是这个形状
62
-
63
- ### `routes/` 与 `controllers/` 分离
64
-
65
- 路由是**哪里**暴露一个 intent(URL 模式、守卫、渲染模式)。Controller 是 intent **做什么**。拆分让你:
66
-
67
- - 跨多个 URL 复用 Controller,路由定义不会污染 Controller 文件
68
- - 读一个目录就知道「应用服务哪些 URL」
69
- - 读一个目录就知道「intent X 计算什么」
70
-
71
- ### `controllers/` 与 `views/` 镜像
72
-
73
- 两边每个 intent 一个文件。intent id、Controller 文件、view 文件同名。找 `/products/:id` 的渲染代码变成机械动作。
74
-
75
- ### 横切关注点都进 `lib/`
76
-
77
- 不是路由、Controller、view 的都进 `lib/`。框架 `bootstrap()` 通过 `lib/di/register.ts` 做重的注册;Controller 用 `lib/api/*` 里的客户端;守卫住 `lib/guards/`。
78
-
79
- ### `env.ts` 放在 `src/` 顶层
80
-
81
- 在一个文件里一次性解析并校验环境变量。用 [zod](https://zod.dev/) 或手写检查。在别处重新导出强类型常量。
82
-
83
- ```ts
84
- // src/env.ts
85
- function requireEnv(name: string): string {
86
- const v = process.env[name];
87
- if (!v) throw new Error(`Missing env: ${name}`);
88
- return v;
89
- }
90
-
91
- export const env = {
92
- UPSTREAM_URL: requireEnv("UPSTREAM_URL"),
93
- SESSION_SECRET: requireEnv("SESSION_SECRET"),
94
- NODE_ENV: process.env.NODE_ENV ?? "development",
95
- } as const;
96
- ```
97
-
98
- 让「这个应用需要什么环境变量」一目了然,构建时遇到缺失就立刻失败,而不是请求时崩。
99
-
100
- ## `bootstrap.ts` 的形态
101
-
102
- `bootstrap.ts` 保持薄 —— 它该是编排,不是逻辑:
103
-
104
- ```ts
105
- // src/bootstrap.ts
106
- import { type Framework } from "@finesoft/front";
107
- import { registerDependencies } from "./lib/di/register";
108
- import { homeRoutes } from "./routes/home";
109
- import { productRoutes } from "./routes/product";
110
- import { checkoutRoutes } from "./routes/checkout";
111
- import { adminRoutes } from "./routes/admin";
112
-
113
- export function bootstrap(framework: Framework): void {
114
- registerDependencies(framework.container);
115
-
116
- homeRoutes(framework);
117
- productRoutes(framework);
118
- checkoutRoutes(framework);
119
- adminRoutes(framework);
120
- }
121
- ```
122
-
123
- 每个 `*Routes` 函数用自己的路由调 `defineRoutes(framework, [...])`。加一个新路由组就是一个 import + 一次调用。
124
-
125
- ## 按领域的路由文件
126
-
127
- ```ts
128
- // src/routes/product.ts
129
- import { defineRoutes, type Framework } from "@finesoft/front";
130
- import { ProductController } from "../controllers/product";
131
- import { ProductListController } from "../controllers/product-list";
132
- import { authGuard } from "../lib/guards/auth";
133
-
134
- export function productRoutes(framework: Framework): void {
135
- defineRoutes(framework, [
136
- { path: "/products", intentId: "product-list", controller: new ProductListController() },
137
- { path: "/products/:id", intentId: "product", controller: new ProductController() },
138
- {
139
- path: "/products/:id/edit",
140
- intentId: "product-edit",
141
- controller: new ProductEditController(),
142
- beforeLoad: [authGuard],
143
- renderMode: "csr",
144
- },
145
- ]);
146
- }
147
- ```
148
-
149
- ## 集中式 DI 注册
150
-
151
- ```ts
152
- // src/lib/di/register.ts
153
- import { type Container, DEP_KEYS } from "@finesoft/front";
154
- import { APP_KEYS } from "./keys";
155
- import { UserApi } from "../api/user";
156
- import { ProductApi } from "../api/product";
157
- import { ConsoleLogger } from "@finesoft/front";
158
- import { env } from "../../env";
159
-
160
- export function registerDependencies(container: Container): void {
161
- container.register(DEP_KEYS.LOGGER, () => new ConsoleLogger("app"));
162
-
163
- container.register(
164
- APP_KEYS.USER_API,
165
- () =>
166
- new UserApi({
167
- baseUrl: env.UPSTREAM_URL,
168
- }),
169
- );
170
- container.register(
171
- APP_KEYS.PRODUCT_API,
172
- () =>
173
- new ProductApi({
174
- baseUrl: env.UPSTREAM_URL,
175
- }),
176
- );
177
- }
178
- ```
179
-
180
- 集中式让接线可检视。加服务是一次编辑,不是跨项目搜索。
181
-
182
- ## 强类型 DI key
183
-
184
- ```ts
185
- // src/lib/di/keys.ts
186
- export const APP_KEYS = {
187
- USER_API: "userApi",
188
- PRODUCT_API: "productApi",
189
- SESSION: "session",
190
- FEATURE_BUCKETING: "featureBucketing",
191
- } as const;
192
-
193
- export type AppKey = (typeof APP_KEYS)[keyof typeof APP_KEYS];
194
- ```
195
-
196
- 然后在任何 Controller 里:
197
-
198
- ```ts
199
- import { APP_KEYS } from "../lib/di/keys";
7
+ | 档位 | 页面与路由 | 示例能力 |
8
+ | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
9
+ | Full | 首页 `/`、商品 `/products/:id`、搜索 `/search?q=...`、关于 `/about` | SSR、关于页 CSR、URL 动作、类型化参数、DI、数据映射与 HTTP 客户端、认证和 SEO 守卫 |
10
+ | Minimal | Feed `/`、详情 `/item/:id`、Notes `/notes` | 标签与栈导航、页面草稿、全局资料、会话恢复、JSON 语言文件和 SSR 水合 |
200
11
 
201
- async execute(params, container) {
202
- const api = container.resolve<UserApi>(APP_KEYS.USER_API);
203
- // ...
204
- }
205
- ```
206
-
207
- `APP_KEYS.USER_PAI` 这种拼错是编译错误。`"userPai"` 这种拼错是运行时错误。
208
-
209
- ## Page 类型 union
210
-
211
- ```ts
212
- // src/lib/pages/types.ts
213
- import type { HomePage } from "../../controllers/home";
214
- import type { ProductPage } from "../../controllers/product";
215
- import type { CheckoutPage } from "../../controllers/checkout";
216
- import type { ErrorPage } from "./error";
217
-
218
- export type Page = HomePage | ProductPage | CheckoutPage | ErrorPage;
219
- ```
220
-
221
- 带 `kind` 字段的可辨识 union。视图层根组件按 `page.kind` 分支:
12
+ 两档模板都使用一个原生 App,布局包住 Outlet。这是两类业务示例,不代表框架能力的高低。
222
13
 
223
- ```vue
224
- <script setup lang="ts">
225
- import type { Page } from "@/lib/pages/types";
226
- const props = defineProps<{ page: Page }>();
227
- </script>
14
+ ## 文件职责
228
15
 
229
- <template>
230
- <Home v-if="page.kind === 'home'" :page="page" />
231
- <Product v-else-if="page.kind === 'product'" :page="page" />
232
- <Checkout v-else-if="page.kind === 'checkout'" :page="page" />
233
- <Error v-else-if="page.kind === 'error'" :page="page" />
234
- </template>
16
+ ```text
17
+ src/config.ts # 应用标识,也是会话存储的命名空间
18
+ src/app-definition.ts # 页面定义、路由、守卫及可选导航结构
19
+ src/views.ts # 浏览器与 SSR 共用的原生视图注册表
20
+ src/main.ts # 标准浏览器启动与 HMR 销毁
21
+ src/ssr.ts # 所选框架的标准 SSR renderer
22
+ src/App.<native> # full 布局,或 minimal 的导航栏与全局资料
23
+ src/pages/ # 原生页面组件(.tsx / .vue / .svelte)
24
+ src/components/ # full 的可复用展示组件
25
+ src/lib/controllers/ # BaseController 页面加载器及显式公开数据
26
+ src/lib/models/ # 页面与数据类型
27
+ src/styles.css # 同档位一致的样式
28
+ src/locales/ # minimal:en-US 与 zh-Hans 语言文件
235
29
  ```
236
30
 
237
- discriminant 在每个分支里把类型收窄 —— view 组件拿到完整强类型 `page` prop,不用 cast。
31
+ Full 的商品数据、mapper、HTTP 客户端和守卫继续放在 `src/lib/`。Minimal 通过原生 useSnapshot 绑定读取已提交快照。资料状态与组件根归原生应用所有,框架负责导航事务与持久化。
238
32
 
239
- ## 拆分 SSR 和浏览器入口
33
+ ## 控制器与页面声明
240
34
 
241
- `ssr.ts` 和 `main.ts` 保持薄。两者只在:
35
+ 六个模板都使用 `BaseController`:`lib/controllers/` 中的类实现业务加载,`app-definition.ts` 用 `definePage({ id, create: () => new HomeController() })` 注册工厂,并复用返回引用的 `route()`、`leaf()` 和 `bindView()`。`views.ts` 绑定原生组件,组件通过 `page` 接收结果。
242
36
 
243
- - `ssr.ts` `createSSRRender` export `render` + `serializeServerData`
244
- - `main.ts` 调 `startBrowserApp` 挂载视图层
37
+ 控制器、Web 页面类型和 `markPublic` 统一从 `@finesoft/front` 导入。控制器在实际执行时创建,页面草稿和全局资料分别交给页面实例与应用 store。错误页由 `getErrorPage` 工厂生成。完整的参数、DI、`fallback` 和函数 `handler` 用法见[路由、控制器与类型化页面](../02-routing-and-controllers.md)。
245
38
 
246
- 其他一切 —— 路由、Controller、DI、i18n —— 都通过 `bootstrap.ts` 共享。
39
+ ## 状态与语言
247
40
 
248
- ```ts
249
- // src/ssr.ts
250
- import { createSSRRender, serializeServerData } from "@finesoft/front";
251
- import { renderToString } from "vue/server-renderer";
252
- import { createSSRApp } from "vue";
253
- import App from "./App.vue";
254
- import { bootstrap } from "./bootstrap";
255
-
256
- export const render = createSSRRender({
257
- bootstrap,
258
- getErrorPage: () => ({ kind: "error", title: "Server error" }),
259
- async renderApp(page) {
260
- const html = await renderToString(createSSRApp(App, { page }));
261
- return { html, head: `<title>${page.title}</title>`, css: "" };
262
- },
263
- });
264
-
265
- export { serializeServerData };
266
- ```
267
-
268
- ```ts
269
- // src/main.ts
270
- import { startBrowserApp } from "@finesoft/front/browser";
271
- import { createSSRApp } from "vue";
272
- import App from "./App.vue";
273
- import { bootstrap } from "./bootstrap";
274
-
275
- startBrowserApp({
276
- bootstrap,
277
- mount(target, { framework }) {
278
- createSSRApp(App, { framework }).mount(target);
279
- },
280
- });
281
- ```
41
+ Minimal 的详情输入框和 Notes 文本框标记 `data-restore-root`。切换标签保留草稿,刷新恢复草稿;返回并移除详情实例后,其草稿被销毁。姓名属于当前应用实例的全局资料,可跨标签、跨刷新恢复。嵌入第二个应用时,调用 `mountApplication(target, persistenceKey, "memory", "/notes")` 并使用独立的存储键。
282
42
 
283
- ## 什么时候打破这个布局
43
+ 三套 minimal 默认 `zh-Hans`。修改 `src/app-definition.ts` 中的 `configuration.locale` 为 `en-US` 或 `zh-Hans` 后重新启动或构建。该示例展示语言文件加载及水合后的翻译;导航标签和示例条目保持英文。
284
44
 
285
- 上面的形状能撑到 ~50 路由。过了之后考虑:
45
+ Full `/admin` 在缺少认证时重定向至 `/login?from=...`;实际登录页和认证服务由应用补充。示例 `auth_token` cookie 用于演示守卫放行。
286
46
 
287
- - **按特性的文件夹**(`src/features/checkout/{routes,controllers,views,api}.ts`)—— 大型应用。每个特性独立可理解。
288
- - **懒加载路由 bundle**,在路由定义里 `import()`。Vite 插件自动拆分。
289
- - **workspace 包** —— 多应用共享 Controller / API 客户端时。共享代码搬到 `packages/shared`,从那里 import。
47
+ ## 保持三框架一致
290
48
 
291
- 不要预先重组。扁平的 `routes/` + `controllers/` 形状到上百文件都没问题。
49
+ 同档位的公共 TypeScript 和样式直接包含在各项目中,并保持文件内容一致。允许的差异为原生 UI 语法、renderer 导入、框架依赖与应用标识。项目无需导入相邻模板,也没有私有运行包路径别名。脚手架复制这些相同源码,并附带当前档位的 README。
292
50
 
293
- ## 参考
51
+ 仓库维护时运行 `vp test packages/create-app/test` 检查结构与文件差异;构建六套模板后,运行 `vp exec node scripts/verify-template-renderers.mjs` 验证同一组浏览器交互。使用 `vp exec node scripts/verify-created-consumers.mjs /absolute/path/to/front.tgz`,可以在仓库外对六个生成项目分别安装和构建。
294
52
 
295
- - [工程实践 · 测试](./testing.md) —— 怎么针对这个结构测
296
- - [DI 容器](../07-di-container.md) —— 注册模式
53
+ 独立数据操作、Node、Worker 宿主可分别放入 `src/data-app.ts`、`src/node.ts`、`src/worker.ts`,通过公开入口使用应用契约。