@finesoft/create-app 0.1.34 → 0.1.35

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 (198) hide show
  1. package/dist/index.mjs +22 -26
  2. package/package.json +14 -5
  3. package/templates/react/README.md +35 -0
  4. package/templates/react/index.html +2 -3
  5. package/templates/react/package.json +12 -8
  6. package/templates/react/src/App.tsx +7 -15
  7. package/templates/react/src/app-definition.ts +35 -0
  8. package/templates/react/src/components/Layout.tsx +5 -52
  9. package/templates/react/src/components/Loading.tsx +1 -1
  10. package/templates/react/src/components/Navigation.tsx +10 -37
  11. package/templates/react/src/components/ProductCard.tsx +5 -23
  12. package/templates/react/src/config.ts +2 -0
  13. package/templates/react/src/lib/controllers/about.ts +18 -13
  14. package/templates/react/src/lib/controllers/error.ts +11 -7
  15. package/templates/react/src/lib/controllers/home.ts +36 -41
  16. package/templates/react/src/lib/controllers/product-detail.ts +71 -40
  17. package/templates/react/src/lib/controllers/search.ts +33 -54
  18. package/templates/react/src/lib/data/products.ts +36 -0
  19. package/templates/react/src/lib/mappers/product.ts +3 -3
  20. package/templates/react/src/lib/models/product.ts +11 -1
  21. package/templates/react/src/lib/services/api-client.ts +7 -4
  22. package/templates/react/src/main.ts +34 -0
  23. package/templates/react/src/pages/About.tsx +2 -4
  24. package/templates/react/src/pages/Home.tsx +6 -15
  25. package/templates/react/src/pages/NotFound.tsx +4 -14
  26. package/templates/react/src/pages/ProductDetail.tsx +5 -23
  27. package/templates/react/src/pages/Search.tsx +5 -7
  28. package/templates/react/src/ssr.ts +11 -0
  29. package/templates/react/src/styles.css +71 -0
  30. package/templates/react/src/views.ts +12 -0
  31. package/templates/react/tsconfig.json +14 -9
  32. package/templates/react/vite.config.ts +9 -13
  33. package/templates/react-minimal/README.md +39 -0
  34. package/templates/react-minimal/index.html +2 -3
  35. package/templates/react-minimal/package.json +12 -8
  36. package/templates/react-minimal/src/App.tsx +40 -85
  37. package/templates/react-minimal/src/app-definition.ts +48 -0
  38. package/templates/react-minimal/src/config.ts +2 -0
  39. package/templates/react-minimal/src/lib/controllers/detail.ts +19 -18
  40. package/templates/react-minimal/src/lib/controllers/error.ts +15 -0
  41. package/templates/react-minimal/src/lib/controllers/home.ts +19 -24
  42. package/templates/react-minimal/src/lib/controllers/notes.ts +7 -5
  43. package/templates/react-minimal/src/lib/locale.ts +16 -0
  44. package/templates/react-minimal/src/lib/models/page.ts +27 -0
  45. package/templates/react-minimal/src/lib/navigation.ts +11 -0
  46. package/templates/react-minimal/src/locales/en-US.json +5 -0
  47. package/templates/react-minimal/src/locales/zh-Hans.json +5 -0
  48. package/templates/react-minimal/src/main.ts +43 -0
  49. package/templates/react-minimal/src/pages/Detail.tsx +20 -0
  50. package/templates/react-minimal/src/pages/Home.tsx +38 -0
  51. package/templates/react-minimal/src/pages/NotFound.tsx +11 -0
  52. package/templates/react-minimal/src/pages/Notes.tsx +21 -0
  53. package/templates/react-minimal/src/ssr.ts +11 -0
  54. package/templates/react-minimal/src/styles.css +74 -0
  55. package/templates/react-minimal/src/views.ts +6 -0
  56. package/templates/react-minimal/src/vite-env.d.ts +3 -0
  57. package/templates/react-minimal/tsconfig.json +14 -9
  58. package/templates/react-minimal/vite.config.ts +9 -7
  59. package/templates/svelte/README.md +35 -0
  60. package/templates/svelte/index.html +1 -2
  61. package/templates/svelte/package.json +9 -5
  62. package/templates/svelte/src/App.svelte +8 -80
  63. package/templates/svelte/src/app-definition.ts +35 -0
  64. package/templates/svelte/src/components/Layout.svelte +2 -2
  65. package/templates/svelte/src/components/Loading.svelte +1 -1
  66. package/templates/svelte/src/components/Navigation.svelte +5 -66
  67. package/templates/svelte/src/components/ProductCard.svelte +3 -24
  68. package/templates/svelte/src/config.ts +2 -0
  69. package/templates/svelte/src/lib/controllers/about.ts +18 -13
  70. package/templates/svelte/src/lib/controllers/error.ts +11 -7
  71. package/templates/svelte/src/lib/controllers/home.ts +36 -41
  72. package/templates/svelte/src/lib/controllers/product-detail.ts +71 -40
  73. package/templates/svelte/src/lib/controllers/search.ts +33 -54
  74. package/templates/svelte/src/lib/data/products.ts +36 -0
  75. package/templates/svelte/src/lib/mappers/product.ts +3 -3
  76. package/templates/svelte/src/lib/models/product.ts +12 -1
  77. package/templates/svelte/src/lib/services/api-client.ts +7 -4
  78. package/templates/svelte/src/main.ts +28 -24
  79. package/templates/svelte/src/pages/About.svelte +2 -2
  80. package/templates/svelte/src/pages/Home.svelte +3 -15
  81. package/templates/svelte/src/pages/NotFound.svelte +3 -12
  82. package/templates/svelte/src/pages/ProductDetail.svelte +4 -21
  83. package/templates/svelte/src/pages/Search.svelte +2 -10
  84. package/templates/svelte/src/ssr.ts +10 -9
  85. package/templates/svelte/src/styles.css +71 -0
  86. package/templates/svelte/src/views.ts +12 -0
  87. package/templates/svelte/svelte.config.js +2 -0
  88. package/templates/svelte/tsconfig.json +13 -8
  89. package/templates/svelte/vite.config.ts +9 -13
  90. package/templates/svelte-minimal/README.md +39 -0
  91. package/templates/svelte-minimal/index.html +1 -2
  92. package/templates/svelte-minimal/package.json +9 -5
  93. package/templates/svelte-minimal/src/App.svelte +49 -61
  94. package/templates/svelte-minimal/src/app-definition.ts +48 -0
  95. package/templates/svelte-minimal/src/config.ts +2 -0
  96. package/templates/svelte-minimal/src/lib/controllers/detail.ts +23 -0
  97. package/templates/svelte-minimal/src/lib/controllers/error.ts +15 -0
  98. package/templates/svelte-minimal/src/lib/controllers/home.ts +24 -23
  99. package/templates/svelte-minimal/src/lib/controllers/notes.ts +18 -0
  100. package/templates/svelte-minimal/src/lib/locale.ts +16 -0
  101. package/templates/svelte-minimal/src/lib/models/page.ts +27 -0
  102. package/templates/svelte-minimal/src/lib/navigation.ts +11 -0
  103. package/templates/svelte-minimal/src/locales/en-US.json +1 -3
  104. package/templates/svelte-minimal/src/locales/zh-Hans.json +1 -3
  105. package/templates/svelte-minimal/src/main.ts +37 -19
  106. package/templates/svelte-minimal/src/pages/Detail.svelte +11 -0
  107. package/templates/svelte-minimal/src/pages/Home.svelte +17 -21
  108. package/templates/svelte-minimal/src/pages/NotFound.svelte +10 -0
  109. package/templates/svelte-minimal/src/pages/Notes.svelte +11 -0
  110. package/templates/svelte-minimal/src/ssr.ts +9 -17
  111. package/templates/svelte-minimal/src/styles.css +74 -0
  112. package/templates/svelte-minimal/src/views.ts +6 -0
  113. package/templates/svelte-minimal/src/vite-env.d.ts +3 -0
  114. package/templates/svelte-minimal/svelte.config.js +2 -0
  115. package/templates/svelte-minimal/tsconfig.json +13 -8
  116. package/templates/svelte-minimal/vite.config.ts +9 -10
  117. package/templates/vue/README.md +35 -0
  118. package/templates/vue/index.html +1 -2
  119. package/templates/vue/package.json +9 -5
  120. package/templates/vue/src/App.vue +7 -22
  121. package/templates/vue/src/app-definition.ts +35 -0
  122. package/templates/vue/src/components/Layout.vue +4 -6
  123. package/templates/vue/src/components/Loading.vue +1 -1
  124. package/templates/vue/src/components/Navigation.vue +10 -43
  125. package/templates/vue/src/components/ProductCard.vue +4 -28
  126. package/templates/vue/src/config.ts +2 -0
  127. package/templates/vue/src/lib/controllers/about.ts +18 -13
  128. package/templates/vue/src/lib/controllers/error.ts +11 -7
  129. package/templates/vue/src/lib/controllers/home.ts +36 -41
  130. package/templates/vue/src/lib/controllers/product-detail.ts +71 -40
  131. package/templates/vue/src/lib/controllers/search.ts +33 -54
  132. package/templates/vue/src/lib/data/products.ts +36 -0
  133. package/templates/vue/src/lib/mappers/product.ts +3 -3
  134. package/templates/vue/src/lib/models/product.ts +12 -1
  135. package/templates/vue/src/lib/services/api-client.ts +7 -7
  136. package/templates/vue/src/main.ts +28 -43
  137. package/templates/vue/src/pages/About.vue +3 -5
  138. package/templates/vue/src/pages/Home.vue +5 -23
  139. package/templates/vue/src/pages/NotFound.vue +4 -13
  140. package/templates/vue/src/pages/ProductDetail.vue +5 -23
  141. package/templates/vue/src/pages/Search.vue +4 -18
  142. package/templates/vue/src/ssr.ts +5 -18
  143. package/templates/vue/src/styles.css +71 -0
  144. package/templates/vue/src/views.ts +12 -0
  145. package/templates/vue/tsconfig.json +14 -9
  146. package/templates/vue/vite.config.ts +9 -13
  147. package/templates/vue-minimal/README.md +39 -0
  148. package/templates/vue-minimal/index.html +1 -2
  149. package/templates/vue-minimal/package.json +9 -5
  150. package/templates/vue-minimal/src/App.vue +46 -43
  151. package/templates/vue-minimal/src/app-definition.ts +48 -0
  152. package/templates/vue-minimal/src/config.ts +2 -0
  153. package/templates/vue-minimal/src/lib/controllers/detail.ts +19 -18
  154. package/templates/vue-minimal/src/lib/controllers/error.ts +15 -0
  155. package/templates/vue-minimal/src/lib/controllers/home.ts +19 -24
  156. package/templates/vue-minimal/src/lib/controllers/notes.ts +7 -5
  157. package/templates/vue-minimal/src/lib/locale.ts +16 -0
  158. package/templates/vue-minimal/src/lib/models/page.ts +27 -0
  159. package/templates/vue-minimal/src/lib/navigation.ts +11 -0
  160. package/templates/vue-minimal/src/locales/en-US.json +5 -0
  161. package/templates/vue-minimal/src/locales/zh-Hans.json +5 -0
  162. package/templates/vue-minimal/src/main.ts +35 -70
  163. package/templates/vue-minimal/src/pages/Detail.vue +20 -0
  164. package/templates/vue-minimal/src/pages/Home.vue +34 -0
  165. package/templates/vue-minimal/src/pages/NotFound.vue +13 -0
  166. package/templates/vue-minimal/src/pages/Notes.vue +22 -0
  167. package/templates/vue-minimal/src/ssr.ts +7 -46
  168. package/templates/vue-minimal/src/styles.css +74 -0
  169. package/templates/vue-minimal/src/views.ts +6 -0
  170. package/templates/vue-minimal/src/vite-env.d.ts +3 -0
  171. package/templates/vue-minimal/tsconfig.json +14 -9
  172. package/templates/vue-minimal/vite.config.ts +9 -7
  173. package/templates/react/src/actions.ts +0 -14
  174. package/templates/react/src/bootstrap.ts +0 -36
  175. package/templates/react/src/components/PageRenderer.tsx +0 -41
  176. package/templates/react/src/main.tsx +0 -49
  177. package/templates/react/src/ssr.tsx +0 -23
  178. package/templates/react-minimal/src/bootstrap.ts +0 -39
  179. package/templates/react-minimal/src/main.tsx +0 -99
  180. package/templates/react-minimal/src/ssr.tsx +0 -51
  181. package/templates/react-minimal/src/views/DetailView.tsx +0 -28
  182. package/templates/react-minimal/src/views/HomeView.tsx +0 -27
  183. package/templates/react-minimal/src/views/NotesView.tsx +0 -11
  184. package/templates/svelte/src/actions.ts +0 -14
  185. package/templates/svelte/src/bootstrap.ts +0 -36
  186. package/templates/svelte/src/components/PageRenderer.svelte +0 -22
  187. package/templates/svelte/src/lib/framework-svelte.ts +0 -34
  188. package/templates/svelte/src/lib/render.ts +0 -15
  189. package/templates/svelte-minimal/src/bootstrap.ts +0 -6
  190. package/templates/svelte-minimal/src/lib/framework-svelte.ts +0 -34
  191. package/templates/svelte-minimal/src/lib/render.ts +0 -12
  192. package/templates/vue/src/actions.ts +0 -14
  193. package/templates/vue/src/bootstrap.ts +0 -42
  194. package/templates/vue/src/components/PageRenderer.vue +0 -26
  195. package/templates/vue-minimal/src/bootstrap.ts +0 -39
  196. package/templates/vue-minimal/src/views/DetailView.vue +0 -29
  197. package/templates/vue-minimal/src/views/HomeView.vue +0 -26
  198. package/templates/vue-minimal/src/views/NotesView.vue +0 -12
@@ -0,0 +1,71 @@
1
+ :root {
2
+ color: #333;
3
+ font-family: Arial, sans-serif;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ }
9
+
10
+ .app-shell {
11
+ min-height: 100vh;
12
+ }
13
+
14
+ .navigation {
15
+ display: flex;
16
+ gap: 1rem;
17
+ padding: 1rem;
18
+ border-bottom: 1px solid #eee;
19
+ }
20
+
21
+ .navigation-link {
22
+ color: #333;
23
+ text-decoration: none;
24
+ }
25
+
26
+ .navigation-link:hover,
27
+ .navigation-link.active {
28
+ color: #007bff;
29
+ }
30
+
31
+ .navigation-link.active {
32
+ font-weight: bold;
33
+ }
34
+
35
+ .app-main {
36
+ padding: 1rem;
37
+ }
38
+
39
+ .loading {
40
+ padding: 2rem;
41
+ color: #999;
42
+ text-align: center;
43
+ }
44
+
45
+ .shelf,
46
+ .results {
47
+ display: flex;
48
+ flex-wrap: wrap;
49
+ gap: 1rem;
50
+ }
51
+
52
+ .shelf.horizontal {
53
+ overflow-x: auto;
54
+ flex-wrap: nowrap;
55
+ }
56
+
57
+ .product-card {
58
+ min-width: 200px;
59
+ padding: 1rem;
60
+ border: 1px solid #eee;
61
+ border-radius: 8px;
62
+ }
63
+
64
+ .price {
65
+ color: #007bff;
66
+ font-weight: bold;
67
+ }
68
+
69
+ .price-large {
70
+ font-size: 1.5rem;
71
+ }
@@ -0,0 +1,12 @@
1
+ import About from "./pages/About.svelte";
2
+ import Home from "./pages/Home.svelte";
3
+ import NotFound from "./pages/NotFound.svelte";
4
+ import ProductDetail from "./pages/ProductDetail.svelte";
5
+ import Search from "./pages/Search.svelte";
6
+ export const views = {
7
+ home: Home,
8
+ product: ProductDetail,
9
+ search: Search,
10
+ about: About,
11
+ error: NotFound,
12
+ };
@@ -0,0 +1,2 @@
1
+ /** Standard Svelte defaults; compiler configuration belongs to the Vite plugin. */
2
+ export default {};
@@ -4,19 +4,24 @@
4
4
  "target": "ESNext",
5
5
  "module": "ESNext",
6
6
  "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
7
11
  "esModuleInterop": true,
8
12
  "forceConsistentCasingInFileNames": true,
9
13
  "skipLibCheck": true,
10
14
  "noEmit": true,
15
+ "rootDir": ".",
11
16
  "paths": {
12
- "@/*": ["./src/*"],
13
- "@finesoft/front": ["../../packages/front/src/index.ts"],
14
- "@finesoft/front/browser": ["../../packages/front/src/browser.ts"],
15
- "@finesoft/core": ["../../packages/core/src/index.ts"],
16
- "@finesoft/browser": ["../../packages/browser/src/index.ts"],
17
- "@finesoft/ssr": ["../../packages/ssr/src/index.ts"],
18
- "@finesoft/server": ["../../packages/server/src/index.ts"]
17
+ "@finesoft/front": [
18
+ "./.finesoft/front.d.ts"
19
+ ]
19
20
  }
20
21
  },
21
- "include": ["src/**/*", "env.d.ts", "vite.config.ts"]
22
+ "include": [
23
+ "src/**/*",
24
+ "env.d.ts",
25
+ "vite.config.ts"
26
+ ]
22
27
  }
@@ -1,18 +1,14 @@
1
1
  import { finesoftFrontViteConfig } from "@finesoft/front";
2
2
  import { svelte } from "@sveltejs/vite-plugin-svelte";
3
- import { defineConfig } from "vite-plus";
3
+ import { defineConfig, lazyPlugins } from "vite-plus";
4
+
5
+ const front = finesoftFrontViteConfig({
6
+ controllerTypes: { root: import.meta.dirname },
7
+ ssr: { entry: "src/ssr.ts" },
8
+ proxies: [{ prefix: "/api", target: "https://jsonplaceholder.typicode.com" }],
9
+ });
4
10
 
5
11
  export default defineConfig({
6
- plugins: [
7
- svelte(),
8
- finesoftFrontViteConfig({
9
- ssr: { entry: "src/ssr.ts" },
10
- proxies: [
11
- {
12
- prefix: "/api",
13
- target: "https://jsonplaceholder.typicode.com",
14
- },
15
- ],
16
- }),
17
- ],
12
+ lint: { options: { typeAware: true, typeCheck: true } },
13
+ plugins: lazyPlugins(() => [front, svelte()]),
18
14
  });
@@ -0,0 +1,39 @@
1
+ # Finesoft Front minimal application
2
+
3
+ Feed, detail and Notes demonstrate tabs/stacks, retained drafts, a per-application profile and session restoration. All three native frameworks have the same example.
4
+
5
+ ## Run
6
+
7
+ Node must satisfy `^22.18.0 || >=24.11.0`. Use Vite+ for tooling.
8
+
9
+ ```sh
10
+ vp install
11
+ vp run dev
12
+ vp run build
13
+ vp run preview
14
+ ```
15
+
16
+ Visit `/`, open a Feed item, enter a detail note, switch to Notes and type a draft. Switch tabs and reload to see both drafts restored. Use Back to remove the detail entry; opening it again starts a fresh draft. Your name is global to this application and is saved on blur.
17
+
18
+ ## Server controller example
19
+
20
+ `src/lib/controllers/detail.ts` extends `BaseServerController` from `@finesoft/front`. Open `/item/2` and inspect the response header `x-demo-request-method: GET`. Return to Feed and open another item: browser navigation loads the controller remotely using POST, whose response contains `x-demo-request-method: POST`. This demonstrates reading `context.request` and explicitly writing `context.responseHeaders` without adding an authentication policy.
21
+
22
+ SSR executes the controller directly, and hydration reuses the result. Its implementation stays on the server; `markPublic` selects the data sent to the native view. Home and Notes remain ordinary `BaseController` examples. Route types come from `src/app-definition.ts`; the Vite plugin maintains the generated `Input` type with the appropriate server context.
23
+
24
+ Keep the configured SSR entry and deploy to a request host such as Node or Worker. Serving only `dist/client` cannot execute this controller. Login state and any credential forwarding remain application-owned.
25
+
26
+ ## Edit
27
+
28
+ - `src/config.ts`: application identity and persistence namespace.
29
+ - `src/app-definition.ts`: pages, routes, guards and navigation.
30
+ - `src/lib/controllers/` and `src/lib/models/`: page loading and typed public data.
31
+ - `src/pages/`, `src/App.*`, `src/styles.css`: native UI and presentation.
32
+ - `src/views.ts`: the view registry shared by browser and server.
33
+ - `src/main.ts` and `src/ssr.ts`: standard browser and SSR startup.
34
+
35
+ `src/instance.ts` creates one profile store/provider per mount. For an embedded application, call `mountApplication(target, uniquePersistenceKey, "memory", "/notes")`. The `data-restore-root` fields belong to their page entries.
36
+
37
+ `src/locales/` contains English and Chinese JSON messages. Set `frameworkConfig.locale` in `src/app-definition.ts` to `en-US` or `zh-Hans` (default), then restart/rebuild. Navigation labels and sample items remain English.
38
+
39
+ All framework APIs import from `@finesoft/front`. Select the native component with `Outlet("react")`, `Outlet("vue")`, or `Outlet("svelte")`, and pass the same renderer to `useSnapshot`. The plugin maintains `.finesoft/front.d.ts` and the exact TypeScript import mapping; install only the chosen renderer. For standalone TypeScript checks, first run `vp exec finesoft-types`. The project is standalone and requires no sibling templates or private runtime packages.
@@ -6,8 +6,7 @@
6
6
  <!--ssr-head-->
7
7
  </head>
8
8
  <body>
9
- <div id="app"><!--ssr-body--></div>
10
- <!--ssr-data-->
9
+ <div id="app"><!--ssr-body--><!--ssr-data--></div>
11
10
  <script type="module" src="/src/main.ts"></script>
12
11
  </body>
13
12
  </html>
@@ -8,12 +8,16 @@
8
8
  "preview": "vp preview"
9
9
  },
10
10
  "dependencies": {
11
- "@finesoft/front": "^0.5.1",
12
- "svelte": "^5.55.7"
11
+ "@finesoft/front": "^0.5.2",
12
+ "svelte": "^5.56.8"
13
13
  },
14
14
  "devDependencies": {
15
- "@sveltejs/vite-plugin-svelte": "^5.0.0",
16
- "vite": "npm:@voidzero-dev/vite-plus-core@0.1.16",
17
- "vite-plus": "0.1.24"
15
+ "@hono/node-server": "^2.1.0",
16
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
17
+ "@types/node": "^22.20.1",
18
+ "hono": "^4.13.1",
19
+ "typescript": "^6.0.3",
20
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
21
+ "vite-plus": "0.2.8"
18
22
  }
19
23
  }
@@ -1,63 +1,51 @@
1
1
  <script lang="ts">
2
- import { type BasePage, Framework } from "@finesoft/front";
3
- import { untrack } from "svelte";
4
- import { setFrameworkContext } from "./lib/framework-svelte";
5
- import Home from "./pages/Home.svelte";
6
-
7
- type Props = {
8
- framework?: Framework;
9
- page?: BasePage | null;
10
- };
11
-
12
- let props: Props = $props();
13
-
14
- // context 只需在初始化时设置一次,用 untrack 明确声明仅读取初始值
15
- const framework = untrack(() => props.framework);
16
- if (framework) {
17
- setFrameworkContext(framework);
18
- }
19
-
20
- /** 将导航 Promise 的 rejection 转为错误页,避免 {:catch} 暴露原始异常 */
21
- function wrapError(p: Promise<BasePage>): Promise<BasePage> {
22
- return p.catch(
23
- (err): BasePage => ({
24
- id: "error",
25
- pageType: "error",
26
- title: "Error",
27
- description:
28
- err instanceof Error ? err.message : "Failed to load page",
29
- }),
30
- );
31
- }
32
-
33
- /**
34
- * 当前页面数据:
35
- * - BasePage: SSR 直出 或 已完成的导航
36
- * - Promise<BasePage>: SPA 导航中
37
- * - null: 初始空白态
38
- */
39
- let currentPage = $state<Promise<BasePage> | BasePage | null>(
40
- untrack(() => props.page ?? null),
41
- );
42
-
43
- /** 由 main.ts 的 mount 回调调用,驱动 SPA 导航 */
44
- export function updatePage(newPage: Promise<BasePage> | BasePage): void {
45
- if (newPage instanceof Promise) {
46
- currentPage = wrapError(newPage);
47
- } else {
48
- currentPage = newPage;
49
- }
50
- }
2
+ import { Outlet as selectOutlet, useSnapshot, type WebAppView } from "@finesoft/front";
3
+ import { onMount, untrack } from "svelte";
4
+ import { TAB_LABELS } from "./lib/navigation";
5
+ import { views } from "./views";
6
+ const Outlet = selectOutlet("svelte");
7
+ let { app }: { app: WebAppView } = $props();
8
+ const snapshot = untrack(() => useSnapshot("svelte", app));
9
+ let name = $state("");
10
+ onMount(() => app.session?.register({
11
+ key: "profile",
12
+ version: 1,
13
+ decode: (data) => {
14
+ if (
15
+ !data ||
16
+ typeof data !== "object" ||
17
+ typeof (data as { name?: unknown }).name !== "string"
18
+ ) throw Error("invalid-profile-state");
19
+ return data as { name: string };
20
+ },
21
+ capture: () => ({ name }),
22
+ restore: (data) => {
23
+ name = data.name;
24
+ },
25
+ }));
51
26
  </script>
52
-
53
- <main style="padding: 1rem">
54
- {#if currentPage instanceof Promise}
55
- {#await currentPage}
56
- <p style="text-align: center; color: #999">Loading…</p>
57
- {:then page}
58
- <Home {page} />
59
- {/await}
60
- {:else if currentPage}
61
- <Home page={currentPage} />
62
- {/if}
63
- </main>
27
+ <div class="app-chrome">
28
+ <header class="profile">
29
+ <label>
30
+ Your name (global):
31
+ <input bind:value={name} placeholder="anon" onblur={() => app.session?.save()} />
32
+ </label>
33
+ {#if name}<span>👋 {name}</span>{/if}
34
+ </header>
35
+ {#if $snapshot.navigation.tabs}
36
+ <nav class="tabs" aria-label="Pages">
37
+ {#each $snapshot.navigation.tabs.order as key (key)}
38
+ <button
39
+ aria-current={key === $snapshot.navigation.tabs.active}
40
+ onclick={() => app.perform({ kind: "selectTab", key })}
41
+ >
42
+ {TAB_LABELS[key] ?? key}
43
+ </button>
44
+ {/each}
45
+ </nav>
46
+ {/if}
47
+ {#if $snapshot.navigation.canGoBack}
48
+ <button onclick={() => app.perform({ kind: "pop" })}>← Back</button>
49
+ {/if}
50
+ <Outlet {app} {views} />
51
+ </div>
@@ -0,0 +1,48 @@
1
+ import { loadMessages } from "virtual:finesoft-front/i18n-loader";
2
+ import { appId } from "./config";
3
+ import { getErrorPage } from "./lib/controllers/error";
4
+ import {
5
+ definePage,
6
+ defineWebApp,
7
+ stack,
8
+ tabs,
9
+ type LeafNode,
10
+ type RouteMatch,
11
+ } from "@finesoft/front";
12
+ import { DetailController } from "./lib/controllers/detail";
13
+ import { HomeController } from "./lib/controllers/home";
14
+ import { NotesController } from "./lib/controllers/notes";
15
+ export const homePage = definePage({
16
+ id: "home",
17
+ create: () => new HomeController(),
18
+ routes: ["/"],
19
+ });
20
+ export const detailPage = definePage({
21
+ id: "detail",
22
+ create: () => new DetailController(),
23
+ routes: ["/item/:id"],
24
+ });
25
+ export const notesPage = definePage({
26
+ id: "notes",
27
+ create: () => new NotesController(),
28
+ routes: ["/notes"],
29
+ });
30
+ export function createNavigation({ match, target }: { match?: RouteMatch; target?: LeafNode }) {
31
+ if (!match || !target) return undefined;
32
+ const home =
33
+ match.intent.id === "detail" ? stack([homePage.leaf(), target]) : stack(homePage.leaf());
34
+ return tabs({
35
+ active: match.intent.id === "notes" ? "notes" : "home",
36
+ branches: { home, notes: stack(match.intent.id === "notes" ? target : notesPage.leaf()) },
37
+ });
38
+ }
39
+
40
+ export const app = defineWebApp({
41
+ id: appId,
42
+ loadMessages,
43
+ configuration: { locale: "zh-Hans" },
44
+ pages: [homePage, detailPage, notesPage],
45
+ getErrorPage,
46
+
47
+ navigation: createNavigation,
48
+ });
@@ -0,0 +1,2 @@
1
+ /** App identity also namespaces persisted browser sessions. */
2
+ export const appId = "svelte-minimal";
@@ -0,0 +1,23 @@
1
+ import { BaseServerController } from "@finesoft/front";
2
+ import { markPublic } from "@finesoft/front";
3
+ import type { DetailPage } from "../models/page";
4
+ import type { DetailControllerInput as Input } from "../../../.finesoft/controller-types";
5
+
6
+ export class DetailController extends BaseServerController<Input, DetailPage> {
7
+ execute({ params, context }: Input): DetailPage {
8
+ const id = params.id ?? "?";
9
+ // GET during SSR, POST when browser navigation loads this controller remotely.
10
+ context.responseHeaders.set("x-demo-request-method", context.request.method);
11
+ return markPublic(
12
+ {
13
+ id: `detail-${id}`,
14
+ pageType: "detail",
15
+ url: `/item/${id}`,
16
+ title: `Item ${id}`,
17
+ description: "A pushed detail screen. Its scoped note is lost once you pop it.",
18
+ itemId: id,
19
+ },
20
+ ["itemId"],
21
+ );
22
+ }
23
+ }
@@ -0,0 +1,15 @@
1
+ import { markPublic } from "@finesoft/front";
2
+ import type { ErrorPage } from "../models/page";
3
+
4
+ export function getErrorPage(status: number, message: string): ErrorPage {
5
+ return markPublic(
6
+ {
7
+ id: "error",
8
+ pageType: "error",
9
+ title: `Error ${status}`,
10
+ description: message,
11
+ status,
12
+ },
13
+ ["status"],
14
+ );
15
+ }
@@ -1,27 +1,28 @@
1
- import {
2
- BaseController,
3
- DEP_KEYS,
4
- type BasePage,
5
- type Container,
6
- type Translator,
7
- } from "@finesoft/front";
1
+ import { BaseController, type ControllerInput } from "@finesoft/front";
2
+ import { markPublic } from "@finesoft/front";
3
+ import type { FeedItem, HomePage } from "../models/page";
8
4
 
9
- export class HomeController extends BaseController<Record<string, string>, BasePage> {
10
- readonly intentId = "home";
5
+ const ITEMS: readonly FeedItem[] = [
6
+ { id: "1", title: "Structured navigation" },
7
+ { id: "2", title: "Session restoration" },
8
+ { id: "3", title: "Navigation-scoped state" },
9
+ ];
11
10
 
12
- execute(_params: Record<string, string>, container: Container): BasePage {
13
- const translator = container.has(DEP_KEYS.TRANSLATOR)
14
- ? container.resolve<Translator>(DEP_KEYS.TRANSLATOR)
15
- : undefined;
16
-
17
- return {
18
- id: "home",
19
- pageType: "home",
20
- url: "/",
21
- title: translator?.t("home.title") ?? "Hello from Finesoft Front",
22
- description:
23
- translator?.t("home.description") ??
24
- "This page uses locale JSON files loaded by finesoftFrontViteConfig.",
25
- };
11
+ export class HomeController extends BaseController<
12
+ ControllerInput<Record<string, string>>,
13
+ HomePage
14
+ > {
15
+ execute(): HomePage {
16
+ return markPublic(
17
+ {
18
+ id: "home",
19
+ pageType: "home",
20
+ url: "/",
21
+ title: "Feed",
22
+ description: "Tap an item to push a detail screen.",
23
+ items: ITEMS,
24
+ },
25
+ { items: { id: true, title: true } },
26
+ );
26
27
  }
27
28
  }
@@ -0,0 +1,18 @@
1
+ import { BaseController, type ControllerInput } from "@finesoft/front";
2
+ import type { NotesPage } from "../models/page";
3
+
4
+ /** Notes 页面:第二个 tab,演示切 tab 保活该分支的作用域状态。 */
5
+ export class NotesController extends BaseController<
6
+ ControllerInput<Record<string, string>>,
7
+ NotesPage
8
+ > {
9
+ execute(): NotesPage {
10
+ return {
11
+ id: "notes",
12
+ pageType: "notes",
13
+ url: "/notes",
14
+ title: "Notes",
15
+ description: "This textarea is navigation-scoped — switch tabs and it survives.",
16
+ };
17
+ }
18
+ }
@@ -0,0 +1,16 @@
1
+ import type { WebAppView } from "@finesoft/front";
2
+
3
+ /** Both SSR and hydrated views read the translator from their own application. */
4
+ export function getHomeLocale(app?: WebAppView) {
5
+ const translator = app?.translator;
6
+ return {
7
+ lang: app?.locale?.lang ?? "unknown",
8
+ label: translator?.t("home.localeLabel") ?? "Current locale",
9
+ badge:
10
+ translator?.t("home.runtimeBadge") ??
11
+ "Hydration kept the translator alive on the client.",
12
+ hint:
13
+ translator?.t("home.switchHint") ??
14
+ "Change frameworkConfig.locale in src/app-definition.ts to load another JSON file.",
15
+ };
16
+ }
@@ -0,0 +1,27 @@
1
+ import type { BasePage } from "@finesoft/front";
2
+
3
+ export interface FeedItem {
4
+ readonly id: string;
5
+ readonly title: string;
6
+ }
7
+
8
+ export interface HomePage extends BasePage {
9
+ readonly pageType: "home";
10
+ readonly items: readonly FeedItem[];
11
+ }
12
+
13
+ export interface DetailPage extends BasePage {
14
+ readonly pageType: "detail";
15
+ readonly itemId: string;
16
+ }
17
+
18
+ export interface NotesPage extends BasePage {
19
+ readonly pageType: "notes";
20
+ }
21
+
22
+ export interface ErrorPage extends BasePage {
23
+ readonly pageType: "error";
24
+ readonly status: number;
25
+ }
26
+
27
+ export type AppPage = HomePage | DetailPage | NotesPage | ErrorPage;
@@ -0,0 +1,11 @@
1
+ import { isStackNode, isTabsNode, type NavigationSnapshot } from "@finesoft/front";
2
+
3
+ export const TAB_LABELS: Readonly<Record<string, string>> = { home: "Feed", notes: "Notes" };
4
+
5
+ /** The renderer supplies each committed snapshot; chrome needs no second subscription. */
6
+ export function getNavigationChrome(snapshot?: NavigationSnapshot) {
7
+ const tree = snapshot?.tree;
8
+ const tabs = tree && isTabsNode(tree) ? { order: tree.order, active: tree.active } : null;
9
+ const branch = tree && isTabsNode(tree) ? tree.branches[tree.active] : null;
10
+ return { tabs, canGoBack: !!branch && isStackNode(branch) && branch.entries.length > 1 };
11
+ }
@@ -1,7 +1,5 @@
1
1
  {
2
- "home.title": "Hello from Finesoft Front",
3
- "home.description": "This page uses locale JSON files loaded by finesoftFrontViteConfig.",
4
2
  "home.localeLabel": "Current locale",
5
3
  "home.runtimeBadge": "Hydration kept the translator alive on the client.",
6
- "home.switchHint": "Change frameworkConfig.locale in src/bootstrap.ts to load another JSON file."
4
+ "home.switchHint": "Change frameworkConfig.locale in src/app-definition.ts to en-US or zh-Hans to load the corresponding JSON file."
7
5
  }
@@ -1,7 +1,5 @@
1
1
  {
2
- "home.title": "你好,来自 Finesoft Front",
3
- "home.description": "这个页面的文案来自 finesoftFrontViteConfig 自动加载的 JSON 翻译文件。",
4
2
  "home.localeLabel": "当前语言",
5
3
  "home.runtimeBadge": "浏览器接管后,翻译器仍然可用。",
6
- "home.switchHint": " src/bootstrap.ts 里的 frameworkConfig.locale 改成 zh-Hans,就会加载这份翻译文件。"
4
+ "home.switchHint": "修改 src/app-definition.ts 中的 frameworkConfig.locale:en-US 为英文,zh-Hans 为中文。"
7
5
  }
@@ -1,22 +1,40 @@
1
- import { startBrowserApp, type BasePage, type Framework } from "@finesoft/front";
2
- import { hydrate } from "svelte";
1
+ import { createBrowserApp } from "@finesoft/front";
2
+ import { hydrate, mount, unmount } from "svelte";
3
+ import { app } from "./app-definition";
4
+ import { appId } from "./config";
3
5
  import App from "./App.svelte";
4
- import { bootstrap } from "./bootstrap";
6
+ import "./styles.css";
5
7
 
6
- void startBrowserApp({
7
- bootstrap,
8
- mount(target: HTMLElement, { framework }: { framework: Framework }) {
9
- // Svelte 5 中必须用 hydrate() 激活 SSR 输出,new Component() 已被移除
10
- const app = hydrate(App, { target, props: { framework } });
8
+ export async function mountApplication(
9
+ target: HTMLElement,
10
+ persistenceKey = appId,
11
+ history: "browser" | "memory" = "browser",
12
+ url?: string,
13
+ ) {
14
+ const handle = await createBrowserApp({
15
+ definition: app,
16
+ target,
17
+ history,
18
+ url,
19
+ persistenceKey,
20
+ domRestore: true,
21
+ session: {},
22
+ });
23
+ if (!handle.shouldHydrate) target.replaceChildren();
24
+ const root = (handle.shouldHydrate ? hydrate : mount)(App, { target, props: { app: handle } });
25
+ const originalDispose = handle.dispose.bind(handle);
26
+ let disposal: Promise<void> | undefined;
27
+ Object.assign(handle, {
28
+ dispose: () => (disposal ??= originalDispose().finally(() => unmount(root))),
29
+ });
30
+ await handle.ready;
31
+ return handle;
32
+ }
11
33
 
12
- return (props: { page: Promise<BasePage> | BasePage; isFirstPage?: boolean }) => {
13
- (app as { updatePage: (p: Promise<BasePage> | BasePage) => void }).updatePage(
14
- props.page,
15
- );
16
- };
17
- },
18
- callbacks: {
19
- onNavigate() {},
20
- onModal() {},
21
- },
22
- });
34
+ export const started = mountApplication(document.getElementById("app")!);
35
+
36
+ if (import.meta.hot) {
37
+ import.meta.hot.dispose(async () => {
38
+ await (await started).dispose();
39
+ });
40
+ }