@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.
- package/README.md +4 -4
- package/dist/Outlet.svelte +39 -0
- package/dist/Outlet.svelte.d.ts +7 -0
- package/dist/browser-CR5vhgXg.mjs +1317 -0
- package/dist/browser-kFMjlLGT.d.mts +262 -0
- package/dist/browser.d.mts +7 -2
- package/dist/browser.mjs +9 -1
- package/dist/controller-types-CgmJ6-le.d.mts +16 -0
- package/dist/cookies-BiRlUanX.d.mts +786 -0
- package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
- package/dist/host-guard-DDWxLpFL.mjs +222 -0
- package/dist/http-B6CJqDyf.d.mts +46 -0
- package/dist/http-CaxrMD1A.d.mts +1 -0
- package/dist/http-D70PL72H.mjs +257 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +2 -0
- package/dist/index-node.d.mts +15 -0
- package/dist/index-node.mjs +15 -0
- package/dist/index.d.mts +48 -697
- package/dist/index.mjs +44 -261
- package/dist/load-node.d.mts +5 -0
- package/dist/load-node.mjs +10 -0
- package/dist/load-portable.d.mts +5 -0
- package/dist/load-portable.mjs +8 -0
- package/dist/lru-map-BKoUAySU.mjs +50 -0
- package/dist/messages-CAt2QdGr.mjs +140 -0
- package/dist/native-contract-DuR25hYB.d.mts +14 -0
- package/dist/native-contract.d.mts +2 -0
- package/dist/native-contract.mjs +1 -0
- package/dist/node-D9hB4dsz.d.mts +35 -0
- package/dist/node.d.mts +2 -0
- package/dist/node.mjs +59 -0
- package/dist/path-CGFl2w7D.mjs +113 -0
- package/dist/path-CXT6xGPO.d.mts +261 -0
- package/dist/portable-CaxrMD1A.d.mts +1 -0
- package/dist/portable.d.mts +11 -0
- package/dist/portable.mjs +12 -0
- package/dist/proxy-2dSWO-Xw.d.mts +53 -0
- package/dist/proxy-z02VvGIj.mjs +7520 -0
- package/dist/public-types-BcJM-AYc.mjs +835 -0
- package/dist/react-DhwBRw01.d.mts +16 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +29 -0
- package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
- package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
- package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
- package/dist/session-DnB4ZC3x.d.mts +1279 -0
- package/dist/src-BQBfaaPO.mjs +3825 -0
- package/dist/src-Ftl_0rhu.mjs +28 -0
- package/dist/ssr-BLzYP4wU.d.mts +207 -0
- package/dist/ssr-Tn4YkuxM.mjs +357 -0
- package/dist/ssr.d.mts +3 -0
- package/dist/ssr.mjs +3 -0
- package/dist/svelte-Dr5to3SE.d.mts +16 -0
- package/dist/svelte.d.mts +3 -0
- package/dist/svelte.mjs +13 -0
- package/dist/typegen-C-WeJCtf.d.mts +12 -0
- package/dist/typegen-cli.d.mts +1 -0
- package/dist/typegen-cli.mjs +11 -0
- package/dist/typegen.d.mts +3 -0
- package/dist/typegen.mjs +2 -0
- package/dist/types-B1BT0N3t.mjs +402 -0
- package/dist/undici-CPfL25Hr.mjs +22262 -0
- package/dist/vite-Cj4SPA8D.d.mts +277 -0
- package/dist/vite.d.mts +4 -0
- package/dist/vite.mjs +2354 -0
- package/dist/vue-DGmzuKho.d.mts +32 -0
- package/dist/vue.d.mts +3 -0
- package/dist/vue.mjs +56 -0
- package/dist/web.d.mts +6 -0
- package/dist/web.mjs +8 -0
- package/dist/worker.d.mts +2 -0
- package/dist/worker.mjs +2 -0
- package/docs/01-getting-started.md +67 -199
- package/docs/02-routing-and-controllers.md +163 -241
- package/docs/03-middleware.md +10 -212
- package/docs/04-rendering-and-hydration.md +7 -332
- package/docs/05-i18n.md +7 -237
- package/docs/06-http-client.md +20 -263
- package/docs/07-di-container.md +23 -257
- package/docs/08-observability.md +6 -286
- package/docs/09-server-and-deployment.md +59 -219
- package/docs/10-features-platform-pwa.md +7 -231
- package/docs/11-navigation.md +143 -288
- package/docs/12-session-restoration.md +6 -214
- package/docs/README.md +7 -7
- package/docs/advanced/custom-action-handler.md +29 -229
- package/docs/advanced/custom-adapter.md +7 -259
- package/docs/advanced/custom-event-recorder.md +7 -312
- package/docs/advanced/inline-proxy-codegen.md +6 -185
- package/docs/advanced/multi-tenant-scopes.md +10 -323
- package/docs/engineering/ci-release-flow.md +35 -222
- package/docs/engineering/project-structure.md +34 -277
- package/docs/engineering/testing.md +8 -310
- package/docs/pitfalls/container-scope-leak.md +2 -214
- package/docs/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/pitfalls/ssr-hydration-mismatch.md +9 -155
- package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/docs/zh/01-getting-started.md +67 -199
- package/docs/zh/02-routing-and-controllers.md +166 -244
- package/docs/zh/03-middleware.md +10 -212
- package/docs/zh/04-rendering-and-hydration.md +7 -332
- package/docs/zh/05-i18n.md +7 -237
- package/docs/zh/06-http-client.md +20 -263
- package/docs/zh/07-di-container.md +23 -257
- package/docs/zh/08-observability.md +6 -283
- package/docs/zh/09-server-and-deployment.md +59 -219
- package/docs/zh/10-features-platform-pwa.md +7 -231
- package/docs/zh/11-navigation.md +130 -290
- package/docs/zh/12-session-restoration.md +6 -214
- package/docs/zh/README.md +4 -4
- package/docs/zh/advanced/custom-action-handler.md +29 -229
- package/docs/zh/advanced/custom-adapter.md +7 -259
- package/docs/zh/advanced/custom-event-recorder.md +7 -312
- package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
- package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
- package/docs/zh/engineering/ci-release-flow.md +35 -222
- package/docs/zh/engineering/project-structure.md +34 -277
- package/docs/zh/engineering/testing.md +8 -310
- package/docs/zh/pitfalls/container-scope-leak.md +2 -214
- package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/zh/pitfalls/ssr-hydration-mismatch.md +9 -155
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/package.json +118 -20
- package/dist/browser-BHhVWXik.mjs +0 -2
- package/dist/browser-BV2BBXm7.d.mts +0 -2811
|
@@ -1,244 +1,57 @@
|
|
|
1
|
-
# Engineering:
|
|
1
|
+
# Engineering: validation and release
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@finesoft/front` and `@finesoft/create-app` are the published packages. Core, Web, Browser, SSR and Server remain private. The front build bundles their implementation into explicit ESM entries and declarations. Each native renderer keeps its selected UI peer external.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Local checks
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`create-finesoft-app` is its own published package (a CLI), separate from the framework runtime.
|
|
16
|
-
|
|
17
|
-
## The release workflow
|
|
18
|
-
|
|
19
|
-
The repo ships a single `.github/workflows/release.yml` that handles everything inline. Trigger: push to `main`.
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
push to main
|
|
23
|
-
│
|
|
24
|
-
▼
|
|
25
|
-
Checkout main (with PAT, not GITHUB_TOKEN)
|
|
26
|
-
│
|
|
27
|
-
▼
|
|
28
|
-
Reconcile npm registry with main
|
|
29
|
-
├── npm == main? → continue
|
|
30
|
-
├── main > npm? → catch-up publish current main version
|
|
31
|
-
└── npm > main? → error, manual investigation
|
|
32
|
-
│
|
|
33
|
-
▼
|
|
34
|
-
Generate auto-changeset (one patch per push)
|
|
35
|
-
│
|
|
36
|
-
▼
|
|
37
|
-
Apply version bump
|
|
38
|
-
├── changes? → continue
|
|
39
|
-
└── no changes? → done, nothing to publish
|
|
40
|
-
│
|
|
41
|
-
▼
|
|
42
|
-
Commit "chore(release): version packages"
|
|
43
|
-
│
|
|
44
|
-
▼
|
|
45
|
-
Build all packages, publish @finesoft/front to npm
|
|
46
|
-
│
|
|
47
|
-
▼
|
|
48
|
-
Push commit + tag back to main (with rebase retry)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Why one inline workflow instead of changesets/action's PR mode
|
|
52
|
-
|
|
53
|
-
The standard changesets workflow opens a PR ("Version Packages") that, when merged, triggers a second workflow run that publishes. **But `GITHUB_TOKEN`-merged commits don't trigger subsequent workflows** (GitHub's anti-recursion safety) — the publish never fires. The inline workflow does everything in one run, no PR hop.
|
|
54
|
-
|
|
55
|
-
### Why a PAT instead of `GITHUB_TOKEN`
|
|
56
|
-
|
|
57
|
-
The repo has a ruleset enforcing signed commits, linear history, and required PRs on `main`. Bypass actors include `RepositoryRole=5 (admin)` but **not** `github-actions[bot]`. GitHub's UI does not allow adding the bot to the bypass list. Push from a PAT owned by an admin user matches the existing bypass entry.
|
|
58
|
-
|
|
59
|
-
The PAT is `Contents: Read & Write` only — the minimum needed for `git push`.
|
|
60
|
-
|
|
61
|
-
## Concurrency
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
- Cancellation mid-publish leaves npm in an inconsistent state
|
|
70
|
-
- Each push must wait for the previous to finish to avoid version-number races
|
|
71
|
-
- The next run's reconcile step picks up whatever the previous one published
|
|
72
|
-
|
|
73
|
-
## Idempotency
|
|
74
|
-
|
|
75
|
-
`changeset publish` skips versions already on npm. So if push to `main` fails after publish:
|
|
17
|
+
`vp pack` is the library build command. `vp pm pack` creates a local package tarball. Front's paired prepack/postpack scripts rewrite its publish manifest and restore the exact working manifest, including when preparation fails. Scaffolder preparation copies the current six templates and makes their package and TypeScript configuration standalone.
|
|
76
18
|
|
|
77
|
-
|
|
78
|
-
- main: stuck at 0.1.74
|
|
19
|
+
## Repository automation
|
|
79
20
|
|
|
80
|
-
|
|
21
|
+
Both Quality jobs first build front and its workspace dependencies. The root Vite configuration loads the controller type generator, which imports the built core package; this preparation is required on a fresh checkout.
|
|
81
22
|
|
|
82
|
-
|
|
23
|
+
Pull requests run Quality (`vp check` and `vp test --coverage`). A push to `main` starts Release, which calls the same Quality workflow and waits for both checks to pass. CodeQL runs independently over `packages/{core,web,browser,ssr,server,front}/src/**`.
|
|
83
24
|
|
|
84
|
-
|
|
25
|
+
Release checks out the tested commit, generates a patch changeset for both public packages, applies Changesets (including any explicit minor/major changesets), updates the lockfile and builds. It then pushes the version commit to `main` with the built-in `GITHUB_TOKEN` before publishing to npm. If another push advances `main`, publication stops; the newer push gets its own validation and release. The workflow never rebases an already-built artifact or overwrites newer commits. Git tags are pushed after npm publication succeeds.
|
|
85
26
|
|
|
86
|
-
|
|
87
|
-
name: Release
|
|
27
|
+
The publishing job uses a GitHub-hosted runner with `id-token: write`, no dependency cache and no npm token. Changesets invokes the repository's pnpm 11 through Vite+; pnpm supports npm OIDC and automatic provenance natively. Both public packages declare their source repository and public registry. `RELEASE_PUSH_TOKEN` and `NPM_TOKEN` are no longer used.
|
|
88
28
|
|
|
89
|
-
|
|
90
|
-
push:
|
|
91
|
-
branches:
|
|
92
|
-
- main
|
|
29
|
+
### npm configuration
|
|
93
30
|
|
|
94
|
-
|
|
31
|
+
In Settings for **each** of `@finesoft/front` and `@finesoft/create-app`, add a GitHub Actions trusted publisher:
|
|
95
32
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
- uses: actions/checkout@v5
|
|
104
|
-
with:
|
|
105
|
-
ref: main
|
|
106
|
-
fetch-depth: 0
|
|
107
|
-
token: ${{ secrets.RELEASE_PUSH_TOKEN }}
|
|
33
|
+
| Field | Value |
|
|
34
|
+
| -------------------- | ----------------------------------------------------- |
|
|
35
|
+
| Organization or user | `GaKireiTsuki` |
|
|
36
|
+
| Repository | `finesoft-front` |
|
|
37
|
+
| Workflow filename | `release.yml` |
|
|
38
|
+
| Environment name | Leave empty; the workflow does not use an environment |
|
|
39
|
+
| Allow npm publish | Enabled for this automatic release workflow |
|
|
108
40
|
|
|
109
|
-
|
|
110
|
-
with:
|
|
111
|
-
node-version: 24
|
|
112
|
-
cache: true
|
|
41
|
+
Keep “Require two-factor authentication and disallow bypass 2fa tokens” selected; it is compatible with trusted publishing. This pipeline publishes directly, so a stage-only connection cannot authorize it. See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/). Saving the connection alone does not prove publication works: verify the first Release run and the actual versions on npm.
|
|
113
42
|
|
|
114
|
-
|
|
43
|
+
### Retry a failed release
|
|
115
44
|
|
|
116
|
-
|
|
117
|
-
run: |
|
|
118
|
-
git config user.name "github-actions[bot]"
|
|
119
|
-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
45
|
+
After fixing the cause, select **Release → Run workflow → main**, or run:
|
|
120
46
|
|
|
121
|
-
|
|
122
|
-
|
|
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 # your deploy command
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
## Conventional commits + auto-changeset
|
|
158
|
-
|
|
159
|
-
The `release:auto:changeset` script (in this repo, generates one patch changeset per push) is intentionally simple — every merged PR becomes one patch bump. For semver-driven versioning, replace it with a script that:
|
|
160
|
-
|
|
161
|
-
- Reads `git log` since the last tag
|
|
162
|
-
- Maps commit prefixes (`feat:`, `fix:`, `BREAKING:`) to changeset types
|
|
163
|
-
- Writes the right `.changeset/*.md`
|
|
164
|
-
|
|
165
|
-
The framework's repo uses patch-only because:
|
|
166
|
-
|
|
167
|
-
- Every push is a small change; large changes go through review and become small commits anyway
|
|
168
|
-
- Real breaking changes are rare and warrant manual changesets
|
|
169
|
-
- It avoids a class of "the prefix lies" bugs
|
|
170
|
-
|
|
171
|
-
Pick the policy that matches how your team commits.
|
|
172
|
-
|
|
173
|
-
## Per-PR validation (`quality.yml`)
|
|
174
|
-
|
|
175
|
-
The repo also has a `quality.yml` workflow on PRs:
|
|
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
|
-
The `check` job (format + lint + types) is gated off by `if: false` in this repo because Vite+ runs them all locally via pre-commit. Re-enable it if your team doesn't run pre-commit hooks consistently.
|
|
201
|
-
|
|
202
|
-
## CodeQL
|
|
203
|
-
|
|
204
|
-
The repo enables CodeQL on a schedule and PRs. Scan scope is restricted to `packages/{core,browser,ssr,server,front}/src/**` — tests, templates, scripts, and the scaffolder are excluded.
|
|
205
|
-
|
|
206
|
-
For application repos, enable the default CodeQL config — its noise is low and it catches real issues (open redirects, SQL injection, secret exposure).
|
|
207
|
-
|
|
208
|
-
## Required status checks
|
|
209
|
-
|
|
210
|
-
The repo ruleset requires:
|
|
211
|
-
|
|
212
|
-
- `Coverage` (from `quality.yml`)
|
|
213
|
-
- `CodeQL`
|
|
214
|
-
|
|
215
|
-
PRs cannot merge until both pass. The release workflow bypasses these via the admin PAT — release runs after merge, on `main`, so the checks already passed on the PR.
|
|
216
|
-
|
|
217
|
-
## Migration: from changesets PR mode to inline
|
|
218
|
-
|
|
219
|
-
If you're moving an existing repo from `changesets/action` (PR mode):
|
|
220
|
-
|
|
221
|
-
1. Delete the old release workflow
|
|
222
|
-
2. Create the inline workflow above
|
|
223
|
-
3. Generate a fine-grained PAT, store as `RELEASE_PUSH_TOKEN`
|
|
224
|
-
4. The first push to main after this change will:
|
|
225
|
-
- Detect main == npm (no catch-up needed)
|
|
226
|
-
- Generate one patch changeset
|
|
227
|
-
- Bump + publish + push back to main
|
|
228
|
-
|
|
229
|
-
If there's a pending "Version Packages" PR from the old workflow, close it without merging. The auto-changeset will pick up everything from there.
|
|
230
|
-
|
|
231
|
-
## What can go wrong
|
|
51
|
+
Manual dispatch validates and builds the versions already in `main`; it does **not** create another changeset or bump versions. Changesets skips versions already present on npm, so partial publication can be retried. The tag step restores missing release tags. If failure occurred before the version commit reached `main`, retry the original push run while its commit is still the branch head, or push the correction to start a new release.
|
|
232
52
|
|
|
233
|
-
|
|
234
|
-
| ----------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
235
|
-
| `[remote rejected] HEAD -> main` | PAT actor not in ruleset bypass; PAT lacks `Contents: Write` | Verify PAT scope; confirm push actor is an admin user |
|
|
236
|
-
| `npm ... is ahead of main` | Previous run published but failed to push | Open a PR syncing `packages/front/package.json` to npm version |
|
|
237
|
-
| Workflow doesn't trigger after a release commit | `if: "!startsWith(github.event.head_commit.message, 'chore(release):'"` filtering | Working as intended — recursion prevention |
|
|
238
|
-
| Pre-commit hook (`vp check`) fails CI | Local formatter not run | `vp check --fix` locally; commit; rerun |
|
|
53
|
+
Local versioning uses `vp run changeset` and `vp run version`. `vp run release` builds and publishes; `vp run release:publish` only publishes an existing build. A local build or tarball test does not publish, deploy, push a commit or validate an external environment.
|
|
239
54
|
|
|
240
|
-
##
|
|
55
|
+
## Application projects
|
|
241
56
|
|
|
242
|
-
|
|
243
|
-
- The actual quality workflow: `.github/workflows/quality.yml`
|
|
244
|
-
- [Changesets docs](https://github.com/changesets/changesets) — for understanding `vp run version` and `vp run release`
|
|
57
|
+
Use the generated application's own `vp run build` and deployment configuration. Keep environment secrets in the chosen host's bindings. Install only the renderer and host peers that the application imports. The complete local runtime and installed-consumer evidence is described in the repository's `docs/application-boundaries-acceptance.md`.
|
|
@@ -1,296 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Application structure
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The React, Vue and Svelte templates have two consistent tiers. Each generated project is standalone and uses only the public package entries.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Choose a tier
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
│ ├── main.ts # browser entry
|
|
12
|
-
│ ├── ssr.ts # SSR entry
|
|
13
|
-
│ ├── App.vue # root component
|
|
14
|
-
│ │
|
|
15
|
-
│ ├── routes/ # route definitions, grouped by domain
|
|
16
|
-
│ │ ├── home.ts
|
|
17
|
-
│ │ ├── product.ts
|
|
18
|
-
│ │ ├── checkout.ts
|
|
19
|
-
│ │ └── admin.ts
|
|
20
|
-
│ │
|
|
21
|
-
│ ├── controllers/ # controllers, one file per intent
|
|
22
|
-
│ │ ├── home.ts
|
|
23
|
-
│ │ ├── product.ts
|
|
24
|
-
│ │ └── checkout.ts
|
|
25
|
-
│ │
|
|
26
|
-
│ ├── views/ # view components, mirror controllers
|
|
27
|
-
│ │ ├── Home.vue
|
|
28
|
-
│ │ ├── Product.vue
|
|
29
|
-
│ │ └── Checkout.vue
|
|
30
|
-
│ │
|
|
31
|
-
│ ├── lib/
|
|
32
|
-
│ │ ├── api/ # HttpClient subclasses
|
|
33
|
-
│ │ │ ├── user.ts
|
|
34
|
-
│ │ │ └── product.ts
|
|
35
|
-
│ │ ├── guards/ # reusable middleware
|
|
36
|
-
│ │ │ ├── auth.ts
|
|
37
|
-
│ │ │ ├── locale.ts
|
|
38
|
-
│ │ │ └── analytics.ts
|
|
39
|
-
│ │ ├── di/
|
|
40
|
-
│ │ │ ├── keys.ts # APP_KEYS const map
|
|
41
|
-
│ │ │ └── register.ts # central registration
|
|
42
|
-
│ │ ├── i18n/
|
|
43
|
-
│ │ │ └── translator.ts # Translator factory
|
|
44
|
-
│ │ └── pages/
|
|
45
|
-
│ │ └── types.ts # Page union type
|
|
46
|
-
│ │
|
|
47
|
-
│ ├── locales/
|
|
48
|
-
│ │ ├── en-US.json
|
|
49
|
-
│ │ ├── zh-Hans.json
|
|
50
|
-
│ │ └── ja-JP.json
|
|
51
|
-
│ │
|
|
52
|
-
│ └── env.ts # env var parsing + validation
|
|
53
|
-
│
|
|
54
|
-
├── public/ # static assets, served at /
|
|
55
|
-
├── index.html
|
|
56
|
-
├── vite.config.ts
|
|
57
|
-
├── package.json
|
|
58
|
-
└── tsconfig.json
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Why this shape
|
|
62
|
-
|
|
63
|
-
### `routes/` separate from `controllers/`
|
|
64
|
-
|
|
65
|
-
A route is **where** an intent is exposed (URL pattern, guards, render mode). A controller is **what** an intent does. Splitting them lets you:
|
|
66
|
-
|
|
67
|
-
- Reuse a controller across multiple URLs without route definitions cluttering its file
|
|
68
|
-
- Find "what URLs does my app serve" by reading one folder
|
|
69
|
-
- Find "what does intent X compute" by reading one folder
|
|
70
|
-
|
|
71
|
-
### `controllers/` mirrors `views/`
|
|
72
|
-
|
|
73
|
-
One file per intent on both sides. The intent id, controller file, and view file share the same name. Finding the rendering code for `/products/:id` becomes mechanical.
|
|
74
|
-
|
|
75
|
-
### `lib/` for everything cross-cutting
|
|
76
|
-
|
|
77
|
-
Anything that isn't a route, controller, or view. The framework's `bootstrap()` reaches into `lib/di/register.ts` for the heavy registration; controllers reach into `lib/api/*` for clients; guards live in `lib/guards/`.
|
|
78
|
-
|
|
79
|
-
### `env.ts` at the top of `src/`
|
|
80
|
-
|
|
81
|
-
Parse and validate environment variables once, in one file. Use [zod](https://zod.dev/) or hand-rolled checks. Re-export typed constants everywhere else.
|
|
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
|
-
This makes "what env vars does this app need" trivially discoverable, and the build fails fast on missing values instead of crashing at request time.
|
|
99
|
-
|
|
100
|
-
## `bootstrap.ts` shape
|
|
101
|
-
|
|
102
|
-
Keep `bootstrap.ts` thin — it should orchestrate, not contain logic:
|
|
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
|
-
Each `*Routes` function calls `defineRoutes(framework, [...])` with its own routes. Adding a new route group is one import + one call.
|
|
124
|
-
|
|
125
|
-
## Per-domain route file
|
|
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
|
-
## Central DI registration
|
|
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
|
-
Centralizing keeps the wiring inspectable. Adding a service is a single edit, not a search across the project.
|
|
181
|
-
|
|
182
|
-
## Typed DI keys
|
|
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
|
-
Then in any controller:
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
import { APP_KEYS } from "../lib/di/keys";
|
|
7
|
+
| Tier | Pages and routes | Demonstrated behavior |
|
|
8
|
+
| ------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
9
|
+
| Full | Home `/`, product `/products/:id`, search `/search?q=...`, About `/about` | SSR, CSR About, URL actions, typed parameters, DI, mapper/API client examples, authentication and SEO guards |
|
|
10
|
+
| Minimal | Feed `/`, detail `/item/:id`, Notes `/notes` | Tabs and stacks, entry drafts, global profile, session restoration, locale JSON and SSR hydration |
|
|
200
11
|
|
|
201
|
-
|
|
202
|
-
const api = container.resolve<UserApi>(APP_KEYS.USER_API);
|
|
203
|
-
// ...
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
A typo in `APP_KEYS.USER_PAI` is a compile error. A typo in `"userPai"` is a runtime error.
|
|
208
|
-
|
|
209
|
-
## Page type 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
|
-
Use a discriminated union with a `kind` field. The view layer's root component switches on `page.kind`:
|
|
12
|
+
Within a tier the three frameworks have the same routes, controller results, content and interactions. Both tiers use one native App with its layout around Outlet. The names describe the example applications, not a ranking of framework capabilities.
|
|
222
13
|
|
|
223
|
-
|
|
224
|
-
<script setup lang="ts">
|
|
225
|
-
import type { Page } from "@/lib/pages/types";
|
|
226
|
-
const props = defineProps<{ page: Page }>();
|
|
227
|
-
</script>
|
|
14
|
+
## Files
|
|
228
15
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
16
|
+
```text
|
|
17
|
+
src/config.ts # app identity / persistence namespace
|
|
18
|
+
src/app-definition.ts # page definitions, routes, guards and optional navigation
|
|
19
|
+
src/views.ts # native view registry shared by browser and SSR
|
|
20
|
+
src/main.ts # standard browser startup and HMR disposal
|
|
21
|
+
src/ssr.ts # selected standard SSR renderer
|
|
22
|
+
src/App.<native> # full layout or minimal navigation/profile chrome
|
|
23
|
+
src/pages/ # native page components (.tsx / .vue / .svelte)
|
|
24
|
+
src/components/ # reusable full-template presentation components
|
|
25
|
+
src/lib/controllers/ # BaseController page loaders and explicit public data
|
|
26
|
+
src/lib/models/ # typed page/data contracts
|
|
27
|
+
src/styles.css # identical styling within each tier
|
|
28
|
+
src/locales/ # minimal: en-US and zh-Hans JSON messages
|
|
235
29
|
```
|
|
236
30
|
|
|
237
|
-
|
|
31
|
+
Full additionally keeps its product data, mapper, HTTP client and guards under `src/lib/`,. Minimal reads `app.getSnapshot()` through the selected native `useSnapshot` binding. Profile state and component roots belong to the native app; the Web session owns guarded navigation and persistence.
|
|
238
32
|
|
|
239
|
-
##
|
|
33
|
+
## Controllers and page declarations
|
|
240
34
|
|
|
241
|
-
|
|
35
|
+
All six templates use `BaseController`: classes in `lib/controllers/` implement business loading, while `app-definition.ts` registers factories with `definePage({ id, create: () => new HomeController() })` and reuses the returned `route()`, `leaf()` and `bindView()` references. `views.ts` binds native components, which receive the result through `page`.
|
|
242
36
|
|
|
243
|
-
|
|
244
|
-
- `main.ts` calls `startBrowserApp` and mounts the view layer
|
|
37
|
+
Import controllers, Web page types and `markPublic` from `@finesoft/front`. Controllers are created for actual execution; entry drafts and global profile state belong to page instances and the application store respectively. The `getErrorPage` factory creates error pages. See [routes, controllers and typed pages](../02-routing-and-controllers.md) for parameters, DI, `fallback` and function handlers.
|
|
245
38
|
|
|
246
|
-
|
|
39
|
+
## State and locale
|
|
247
40
|
|
|
248
|
-
|
|
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
|
+
In minimal, the detail input and Notes textarea live inside `data-restore-root`. Switching tabs retains both drafts; reloading restores them. Popping a detail entry destroys its draft. The profile is global to one mounted application and survives reload. `mountApplication(target, persistenceKey, "memory", "/notes")` allows an embedded application with a distinct persistence key.
|
|
282
42
|
|
|
283
|
-
|
|
43
|
+
All three minimal templates default to `zh-Hans`. Set `configuration.locale` in `src/app-definition.ts` to `en-US` or `zh-Hans` and restart/rebuild. The locale demonstration covers JSON loading and translation after hydration; the navigation labels and sample item data remain English.
|
|
284
44
|
|
|
285
|
-
|
|
45
|
+
Full's unauthenticated `/admin` route redirects to `/login?from=...`; the template deliberately leaves the application's real login page and authentication service to its author. A demo `auth_token` cookie lets the guard continue.
|
|
286
46
|
|
|
287
|
-
|
|
288
|
-
- **Lazy-loaded route bundles** with `import()` inside the route definition. The Vite plugin splits them automatically.
|
|
289
|
-
- **Workspace packages** if multiple apps share the same controllers / API clients. Move shared code to `packages/shared` and import from there.
|
|
47
|
+
## Maintaining parity
|
|
290
48
|
|
|
291
|
-
|
|
49
|
+
Framework-neutral files are identical checked-in source in each same-tier project. Native imports, UI syntax, framework dependencies and app identities are the allowed differences. The projects need no sibling-template imports or private runtime aliases. The scaffolder packages these same source files and includes a local README.
|
|
292
50
|
|
|
293
|
-
|
|
51
|
+
Run `vp test packages/create-app/test` to check file/adapter parity, then build the six templates and run `vp exec node scripts/verify-template-renderers.mjs` for shared browser behavior. `vp exec node scripts/verify-created-consumers.mjs /absolute/path/to/front.tgz` prepares, installs and builds every generated project outside the workspace against the selected local package.
|
|
294
52
|
|
|
295
|
-
-
|
|
296
|
-
- [DI container](../07-di-container.md) — registration patterns
|
|
53
|
+
Optional independent data operations and Node/Worker hosts can be added in separate `src/data-app.ts`, `src/node.ts` and `src/worker.ts` modules. They share application contracts through the public entries.
|