@finesoft/create-app 0.1.34 → 0.1.36

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
@@ -1,36 +1,12 @@
1
- import { BaseController, makeFlowAction } from "@finesoft/front";
2
- import type { HomePage, ProductItem, ProductShelf } from "../models/product";
3
-
4
- const MOCK_PRODUCTS: ProductItem[] = [
5
- {
6
- id: "1",
7
- itemType: "product",
8
- name: "TypeScript Handbook",
9
- price: 29.99,
10
- imageUrl: "/img/ts.svg",
11
- clickAction: makeFlowAction("/products/1"),
12
- },
13
- {
14
- id: "2",
15
- itemType: "product",
16
- name: "Vite Starter Kit",
17
- price: 19.99,
18
- imageUrl: "/img/vite.svg",
19
- clickAction: makeFlowAction("/products/2"),
20
- },
21
- {
22
- id: "3",
23
- itemType: "product",
24
- name: "Hono Framework Guide",
25
- price: 24.99,
26
- imageUrl: "/img/hono.svg",
27
- clickAction: makeFlowAction("/products/3"),
28
- },
29
- ];
30
-
31
- export class HomeController extends BaseController<Record<string, string>, HomePage> {
32
- readonly intentId = "home";
1
+ import { PRODUCTS } from "../data/products";
2
+ import { BaseController, type ControllerInput } from "@finesoft/front";
3
+ import { markPublic, makeFlowAction } from "@finesoft/front";
4
+ import type { HomePage, ProductShelf } from "../models/product";
33
5
 
6
+ export class HomeController extends BaseController<
7
+ ControllerInput<Record<string, string>>,
8
+ HomePage
9
+ > {
34
10
  execute(): HomePage {
35
11
  const shelf: ProductShelf = {
36
12
  id: "featured",
@@ -38,16 +14,35 @@ export class HomeController extends BaseController<Record<string, string>, HomeP
38
14
  title: "Featured Products",
39
15
  isHorizontal: true,
40
16
  seeAllAction: makeFlowAction("/search"),
41
- items: MOCK_PRODUCTS,
17
+ items: PRODUCTS.slice(0, 3),
42
18
  };
43
19
 
44
- return {
45
- id: "home",
46
- pageType: "home",
47
- url: "/",
48
- title: "Home",
49
- description: "Welcome to Finesoft Front Demo",
50
- shelves: [shelf],
51
- };
20
+ return markPublic(
21
+ {
22
+ id: "home",
23
+ pageType: "home",
24
+ title: "Home",
25
+ description: "Welcome to Finesoft Front Demo",
26
+ url: "/",
27
+ shelves: [shelf],
28
+ },
29
+ {
30
+ shelves: {
31
+ id: true,
32
+ shelfType: true,
33
+ title: true,
34
+ isHorizontal: true,
35
+ seeAllAction: { kind: true, url: true },
36
+ items: {
37
+ id: true,
38
+ itemType: true,
39
+ name: true,
40
+ price: true,
41
+ imageUrl: true,
42
+ clickAction: { kind: true, url: true },
43
+ },
44
+ },
45
+ },
46
+ );
52
47
  }
53
48
  }
@@ -1,52 +1,83 @@
1
- import {
2
- BaseController,
3
- type Container,
4
- DEP_KEYS,
5
- type Logger,
6
- type LoggerFactory,
7
- } from "@finesoft/front";
1
+ import { DEP_KEYS, type Logger, type LoggerFactory } from "@finesoft/front";
2
+ import { BaseServerController } from "@finesoft/front";
3
+ import { markPublic } from "@finesoft/front";
8
4
  import type { ProductPage } from "../models/product";
5
+ import type {
6
+ ProductDetailControllerFailure as Failure,
7
+ ProductDetailControllerInput as Input,
8
+ } from "../../../.finesoft/controller-types";
9
9
 
10
- export class ProductDetailController extends BaseController<{ id: number }, ProductPage> {
11
- readonly intentId = "product-detail";
12
-
13
- async execute(params: { id: number }, container: Container): Promise<ProductPage> {
14
- const loggerFactory = container.resolve<LoggerFactory>(DEP_KEYS.LOGGER_FACTORY);
10
+ export class ProductDetailController extends BaseServerController<Input, ProductPage> {
11
+ async execute({ params, context }: Input): Promise<ProductPage> {
12
+ const loggerFactory = await context.get<LoggerFactory>(DEP_KEYS.LOGGER_FACTORY);
15
13
  const log: Logger = loggerFactory.loggerFor("ProductDetailController");
16
14
  log.info(`Loading product ${params.id}`);
17
15
 
16
+ // Application-owned browsing preference, unrelated to authentication.
17
+ // getCookie reads the incoming request; setCookie only writes the response.
18
+ context.responseHeaders.set("cache-control", "private, no-store");
19
+ if (context.getCookie("last_product") !== String(params.id)) {
20
+ context.setCookie("last_product", String(params.id), {
21
+ httpOnly: true,
22
+ sameSite: "Lax",
23
+ secure: new URL(context.request.url).protocol === "https:",
24
+ });
25
+ }
26
+
18
27
  // Simulated API fetch — replace with real HttpClient call
19
- return {
20
- id: `product-${params.id}`,
21
- pageType: "product",
22
- url: `/products/${params.id}`,
23
- title: `Product ${params.id}`,
24
- description: `Details for product ${params.id}`,
25
- product: {
26
- id: String(params.id),
27
- name: `Product ${params.id}`,
28
- price: 29.99,
29
- description:
30
- "This is a demo product showcasing BaseController with typed params and DI container usage.",
31
- imageUrl: `/img/product-${params.id}.svg`,
28
+ return markPublic(
29
+ {
30
+ id: `product-${params.id}`,
31
+ pageType: "product",
32
+ title: `Product ${params.id}`,
33
+ description: `Details for product ${params.id}`,
34
+ url: `/products/${params.id}`,
35
+ product: {
36
+ id: String(params.id),
37
+ name: `Product ${params.id}`,
38
+ price: 29.99,
39
+ description:
40
+ "This is a demo product showcasing route-inferred params and DI container usage.",
41
+ imageUrl: `/img/product-${params.id}.svg`,
42
+ },
32
43
  },
33
- };
44
+ {
45
+ product: {
46
+ id: true,
47
+ name: true,
48
+ price: true,
49
+ description: true,
50
+ imageUrl: true,
51
+ },
52
+ },
53
+ );
34
54
  }
35
55
 
36
- fallback(params: { id: number }, error: Error): ProductPage {
37
- return {
38
- id: `product-${params.id}`,
39
- pageType: "product",
40
- url: `/products/${params.id}`,
41
- title: "Product Not Found",
42
- description: error.message,
43
- product: {
44
- id: String(params.id),
45
- name: "Unknown Product",
46
- price: 0,
47
- description: "Could not load product data.",
48
- imageUrl: "/img/placeholder.svg",
56
+ fallback({ params }: Failure): ProductPage {
57
+ return markPublic(
58
+ {
59
+ id: `product-${params.id}`,
60
+ pageType: "product",
61
+ title: "Product Not Found",
62
+ description: "The product is currently unavailable.",
63
+ url: `/products/${params.id}`,
64
+ product: {
65
+ id: String(params.id),
66
+ name: "Unknown Product",
67
+ price: 0,
68
+ description: "Could not load product data.",
69
+ imageUrl: "/img/placeholder.svg",
70
+ },
71
+ },
72
+ {
73
+ product: {
74
+ id: true,
75
+ name: true,
76
+ price: true,
77
+ description: true,
78
+ imageUrl: true,
79
+ },
49
80
  },
50
- };
81
+ );
51
82
  }
52
83
  }
@@ -1,58 +1,37 @@
1
- import { BaseController, makeFlowAction } from "@finesoft/front";
2
- import type { ProductItem, SearchPage } from "../models/product";
1
+ import { PRODUCTS } from "../data/products";
2
+ import { BaseController } from "@finesoft/front";
3
+ import { markPublic } from "@finesoft/front";
4
+ import type { SearchPage } from "../models/product";
5
+ import type { SearchControllerInput as Input } from "../../../.finesoft/controller-types";
3
6
 
4
- const ALL_PRODUCTS: ProductItem[] = [
5
- {
6
- id: "1",
7
- itemType: "product",
8
- name: "TypeScript Handbook",
9
- price: 29.99,
10
- imageUrl: "/img/ts.svg",
11
- clickAction: makeFlowAction("/products/1"),
12
- },
13
- {
14
- id: "2",
15
- itemType: "product",
16
- name: "Vite Starter Kit",
17
- price: 19.99,
18
- imageUrl: "/img/vite.svg",
19
- clickAction: makeFlowAction("/products/2"),
20
- },
21
- {
22
- id: "3",
23
- itemType: "product",
24
- name: "Hono Framework Guide",
25
- price: 24.99,
26
- imageUrl: "/img/hono.svg",
27
- clickAction: makeFlowAction("/products/3"),
28
- },
29
- {
30
- id: "4",
31
- itemType: "product",
32
- name: "SSR Deep Dive",
33
- price: 34.99,
34
- imageUrl: "/img/ssr.svg",
35
- clickAction: makeFlowAction("/products/4"),
36
- },
37
- ];
7
+ export class SearchController extends BaseController<Input, SearchPage> {
8
+ execute({ query }: Input): SearchPage {
9
+ const term = query.q;
10
+ const results = term
11
+ ? PRODUCTS.filter((p) => p.name.toLowerCase().includes(term.toLowerCase()))
12
+ : PRODUCTS;
38
13
 
39
- export class SearchController extends BaseController<{ q?: string }, SearchPage> {
40
- readonly intentId = "search";
41
-
42
- execute(params: { q?: string }): SearchPage {
43
- const query = params.q ?? "";
44
- const results = query
45
- ? ALL_PRODUCTS.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
46
- : ALL_PRODUCTS;
47
-
48
- return {
49
- id: "search",
50
- pageType: "search",
51
- url: "/search",
52
- title: query ? `Search: ${query}` : "All Products",
53
- description: `${results.length} result(s)`,
54
- query,
55
- results,
56
- };
14
+ return markPublic(
15
+ {
16
+ id: "search",
17
+ pageType: "search",
18
+ title: term ? `Search: ${term}` : "All Products",
19
+ description: `${results.length} result(s)`,
20
+ url: "/search",
21
+ query: term,
22
+ results,
23
+ },
24
+ {
25
+ query: true,
26
+ results: {
27
+ id: true,
28
+ itemType: true,
29
+ name: true,
30
+ price: true,
31
+ imageUrl: true,
32
+ clickAction: { kind: true, url: true },
33
+ },
34
+ },
35
+ );
57
36
  }
58
37
  }
@@ -0,0 +1,36 @@
1
+ import type { ProductItem } from "../models/product";
2
+
3
+ export const PRODUCTS: ProductItem[] = [
4
+ {
5
+ id: "1",
6
+ itemType: "product",
7
+ name: "TypeScript Handbook",
8
+ price: 29.99,
9
+ imageUrl: "/img/ts.svg",
10
+ clickAction: { url: "/products/1" },
11
+ },
12
+ {
13
+ id: "2",
14
+ itemType: "product",
15
+ name: "Vite Starter Kit",
16
+ price: 19.99,
17
+ imageUrl: "/img/vite.svg",
18
+ clickAction: { url: "/products/2" },
19
+ },
20
+ {
21
+ id: "3",
22
+ itemType: "product",
23
+ name: "Hono Framework Guide",
24
+ price: 24.99,
25
+ imageUrl: "/img/hono.svg",
26
+ clickAction: { url: "/products/3" },
27
+ },
28
+ {
29
+ id: "4",
30
+ itemType: "product",
31
+ name: "SSR Deep Dive",
32
+ price: 34.99,
33
+ imageUrl: "/img/ssr.svg",
34
+ clickAction: { url: "/products/4" },
35
+ },
36
+ ];
@@ -1,4 +1,4 @@
1
- import { makeFlowAction, mapEach, type Mapper } from "@finesoft/front";
1
+ import { mapEach, type Mapper } from "@finesoft/front";
2
2
  import type { ProductItem } from "../models/product";
3
3
 
4
4
  /** Raw API response shape */
@@ -16,12 +16,12 @@ const toProductItem: Mapper<ApiProduct, ProductItem> = (raw) => ({
16
16
  name: raw.title,
17
17
  price: raw.price,
18
18
  imageUrl: raw.thumbnail ?? "/img/placeholder.svg",
19
- clickAction: makeFlowAction(`/products/${raw.id}`),
19
+ clickAction: { url: `/products/${raw.id}` },
20
20
  });
21
21
 
22
22
  /**
23
23
  * Composed mapper pipeline: ApiProduct[] → ProductItem[]
24
24
  *
25
- * Demonstrates pipe() + mapEach() for data transformation.
25
+ * Demonstrates mapEach() for data transformation.
26
26
  */
27
27
  export const mapProducts: Mapper<ApiProduct[], ProductItem[]> = mapEach(toProductItem);
@@ -1,15 +1,25 @@
1
- import type { BaseItem, BasePage, BaseShelf } from "@finesoft/front";
1
+ import type { BasePage } from "@finesoft/front";
2
+ interface BaseItem {
3
+ readonly id: string;
4
+ }
5
+ interface BaseShelf {
6
+ readonly id: string;
7
+ readonly title: string;
8
+ }
2
9
 
3
10
  export interface ProductItem extends BaseItem {
4
11
  itemType: "product";
5
12
  name: string;
6
13
  price: number;
7
14
  imageUrl: string;
15
+ clickAction?: { readonly url: string };
8
16
  }
9
17
 
10
18
  export interface ProductShelf extends BaseShelf {
11
19
  shelfType: "products";
12
20
  items: ProductItem[];
21
+ isHorizontal?: boolean;
22
+ seeAllAction?: { readonly url: string };
13
23
  }
14
24
 
15
25
  export interface HomePage extends BasePage {
@@ -44,4 +54,5 @@ export interface ErrorPage extends BasePage {
44
54
  status: number;
45
55
  }
46
56
 
57
+ /** 所有页面类型联合 */
47
58
  export type AppPage = HomePage | ProductPage | SearchPage | AboutPage | ErrorPage;
@@ -6,11 +6,14 @@ import { HttpClient, type HttpClientConfig } from "@finesoft/front";
6
6
  * Showcases HttpClient subclass pattern with base URL and custom fetch.
7
7
  */
8
8
  export class ApiClient extends HttpClient {
9
- constructor(config?: Partial<HttpClientConfig>) {
9
+ constructor(config: Partial<HttpClientConfig> & Pick<HttpClientConfig, "fetch">) {
10
10
  super({
11
- baseUrl: config?.baseUrl ?? "/api",
12
- defaultHeaders: { "Content-Type": "application/json", ...config?.defaultHeaders },
13
- fetch: config?.fetch,
11
+ baseUrl: config.baseUrl ?? "/api",
12
+ defaultHeaders: { "Content-Type": "application/json", ...config.defaultHeaders },
13
+ fetch: config.fetch,
14
+ validateDns: config.validateDns,
15
+ lookup: config.lookup,
16
+ allowInternalHosts: config.allowInternalHosts,
14
17
  });
15
18
  }
16
19
 
@@ -1,27 +1,31 @@
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";
3
4
  import App from "./App.svelte";
4
- import { bootstrap } from "./bootstrap";
5
- import type { AppPage } from "./lib/models/product";
5
+ import "./styles.css";
6
6
 
7
- void startBrowserApp({
8
- bootstrap,
9
- mount(target: HTMLElement, { framework }: { framework: Framework }) {
10
- // Svelte 5 中必须用 hydrate() 激活 SSR 输出,new Component() 已被移除
11
- const app = hydrate(App, { target, props: { framework } });
7
+ const target = document.getElementById("app")!;
8
+ export async function mountApplication(
9
+ target: HTMLElement,
10
+ history: "browser" | "memory" = "browser",
11
+ url?: string,
12
+ ) {
13
+ const handle = await createBrowserApp({ definition: app, target, history, url });
14
+ if (!handle.shouldHydrate) target.replaceChildren();
15
+ const root = (handle.shouldHydrate ? hydrate : mount)(App, { target, props: { app: handle } });
16
+ const originalDispose = handle.dispose.bind(handle);
17
+ let disposal: Promise<void> | undefined;
18
+ Object.assign(handle, {
19
+ dispose: () => (disposal ??= originalDispose().finally(() => unmount(root))),
20
+ });
21
+ await handle.ready;
22
+ return handle;
23
+ }
12
24
 
13
- return (props: { page: Promise<BasePage> | BasePage; isFirstPage?: boolean }) => {
14
- (app as { updatePage: (p: Promise<AppPage> | AppPage) => void }).updatePage(
15
- props.page as Promise<AppPage> | AppPage,
16
- );
17
- };
18
- },
19
- callbacks: {
20
- onNavigate(pathname) {
21
- console.log("[navigate]", pathname);
22
- },
23
- onModal(page) {
24
- console.log("[modal]", page.title);
25
- },
26
- },
27
- });
25
+ export const started = mountApplication(target);
26
+
27
+ if (import.meta.hot) {
28
+ import.meta.hot.dispose(async () => {
29
+ await (await started).dispose();
30
+ });
31
+ }
@@ -4,7 +4,7 @@
4
4
  let { page }: { page: AboutPage } = $props();
5
5
  </script>
6
6
 
7
- <div>
7
+ <section class="page page-about">
8
8
  <h1>{page.title}</h1>
9
9
  <p>{page.content}</p>
10
- </div>
10
+ </section>
@@ -5,12 +5,12 @@
5
5
  let { page }: { page: HomePage } = $props();
6
6
  </script>
7
7
 
8
- <div>
8
+ <section class="page page-home">
9
9
  <h1>{page.title}</h1>
10
10
  <p>{page.description}</p>
11
11
 
12
12
  {#each page.shelves as shelf}
13
- <section>
13
+ <section class="shelf-section">
14
14
  <h2>{shelf.title}</h2>
15
15
  <div class="shelf" class:horizontal={shelf.isHorizontal}>
16
16
  {#each shelf.items as item}
@@ -19,16 +19,4 @@
19
19
  </div>
20
20
  </section>
21
21
  {/each}
22
- </div>
23
-
24
- <style>
25
- .shelf {
26
- display: flex;
27
- gap: 1rem;
28
- flex-wrap: wrap;
29
- }
30
- .shelf.horizontal {
31
- flex-wrap: nowrap;
32
- overflow-x: auto;
33
- }
34
- </style>
22
+ </section>
@@ -1,23 +1,14 @@
1
1
  <script lang="ts">
2
- import { NAV_ACTIONS } from "../actions";
3
- import { getPerform } from "../lib/framework-svelte";
4
2
  import type { ErrorPage } from "../lib/models/product";
5
3
 
6
4
  let { page }: { page: ErrorPage } = $props();
7
-
8
- const perform = getPerform();
9
5
  </script>
10
6
 
11
- <div>
7
+ <section class="page page-error">
12
8
  <h1>{page.title}</h1>
13
9
  <p>{page.description}</p>
14
10
  <a
15
11
  href="/"
16
- onclick={(e) => {
17
- if (perform) {
18
- e.preventDefault();
19
- void perform(NAV_ACTIONS.home);
20
- }
21
- }}>← Go Home</a
12
+ >← Go Home</a
22
13
  >
23
- </div>
14
+ </section>
@@ -1,32 +1,15 @@
1
1
  <script lang="ts">
2
- import { NAV_ACTIONS } from "../actions";
3
- import { getPerform } from "../lib/framework-svelte";
4
2
  import type { ProductPage } from "../lib/models/product";
5
3
 
6
4
  let { page }: { page: ProductPage } = $props();
7
-
8
- const perform = getPerform();
9
5
  </script>
10
6
 
11
- <div>
7
+ <section class="page page-product">
12
8
  <a
13
9
  href="/"
14
- onclick={(e) => {
15
- if (perform) {
16
- e.preventDefault();
17
- void perform(NAV_ACTIONS.home);
18
- }
19
- }}>← Back</a
10
+ >← Back</a
20
11
  >
21
12
  <h1>{page.product.name}</h1>
22
- <p class="price">${page.product.price.toFixed(2)}</p>
13
+ <p class="price price-large">${page.product.price.toFixed(2)}</p>
23
14
  <p>{page.product.description}</p>
24
- </div>
25
-
26
- <style>
27
- .price {
28
- font-size: 1.5rem;
29
- color: #007bff;
30
- font-weight: bold;
31
- }
32
- </style>
15
+ </section>
@@ -5,7 +5,7 @@
5
5
  let { page }: { page: SearchPage } = $props();
6
6
  </script>
7
7
 
8
- <div>
8
+ <section class="page page-search">
9
9
  <h1>{page.title}</h1>
10
10
  <p>{page.description}</p>
11
11
 
@@ -18,12 +18,4 @@
18
18
  {#if page.results.length === 0}
19
19
  <p>No products found.</p>
20
20
  {/if}
21
- </div>
22
-
23
- <style>
24
- .results {
25
- display: flex;
26
- gap: 1rem;
27
- flex-wrap: wrap;
28
- }
29
- </style>
21
+ </section>
@@ -1,12 +1,13 @@
1
- import { createSSRRender, serializeServerData } from "@finesoft/front";
2
- import { bootstrap } from "./bootstrap";
3
- import { getErrorPage } from "./lib/controllers/error";
4
- import { renderApp } from "./lib/render";
1
+ import { render as renderSvelte } from "svelte/server";
2
+ import { createSSRRender } from "@finesoft/front";
3
+ import { app } from "./app-definition";
4
+ import App from "./App.svelte";
5
5
 
6
6
  export const render = createSSRRender({
7
- bootstrap,
8
- getErrorPage,
9
- renderApp,
7
+ definition: app,
8
+ render: (app) => {
9
+ const result = renderSvelte(App, { props: { app } });
10
+ return { html: result.body, head: result.head, css: "" };
11
+ },
10
12
  });
11
-
12
- export { serializeServerData };
13
+ export { serializeServerData } from "@finesoft/front";