@finesoft/create-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/index.mjs +129 -0
  2. package/package.json +26 -0
  3. package/templates/react/env.d.ts +1 -0
  4. package/templates/react/index.html +13 -0
  5. package/templates/react/package.json +21 -0
  6. package/templates/react/src/App.tsx +20 -0
  7. package/templates/react/src/actions.ts +14 -0
  8. package/templates/react/src/bootstrap.ts +36 -0
  9. package/templates/react/src/components/Layout.tsx +63 -0
  10. package/templates/react/src/components/Loading.tsx +3 -0
  11. package/templates/react/src/components/Navigation.tsx +51 -0
  12. package/templates/react/src/components/PageRenderer.tsx +41 -0
  13. package/templates/react/src/components/ProductCard.tsx +35 -0
  14. package/templates/react/src/lib/controllers/about.ts +18 -0
  15. package/templates/react/src/lib/controllers/error.ts +11 -0
  16. package/templates/react/src/lib/controllers/home.ts +53 -0
  17. package/templates/react/src/lib/controllers/product-detail.ts +52 -0
  18. package/templates/react/src/lib/controllers/search.ts +58 -0
  19. package/templates/react/src/lib/guards/auth.ts +17 -0
  20. package/templates/react/src/lib/guards/seo.ts +15 -0
  21. package/templates/react/src/lib/mappers/product.ts +27 -0
  22. package/templates/react/src/lib/models/product.ts +48 -0
  23. package/templates/react/src/lib/services/api-client.ts +28 -0
  24. package/templates/react/src/main.tsx +49 -0
  25. package/templates/react/src/pages/About.tsx +16 -0
  26. package/templates/react/src/pages/Home.tsx +34 -0
  27. package/templates/react/src/pages/NotFound.tsx +25 -0
  28. package/templates/react/src/pages/ProductDetail.tsx +34 -0
  29. package/templates/react/src/pages/Search.tsx +23 -0
  30. package/templates/react/src/ssr.tsx +23 -0
  31. package/templates/react/tsconfig.json +17 -0
  32. package/templates/react/vite.config.ts +20 -0
  33. package/templates/react-minimal/env.d.ts +1 -0
  34. package/templates/react-minimal/index.html +13 -0
  35. package/templates/react-minimal/package.json +21 -0
  36. package/templates/react-minimal/src/App.tsx +22 -0
  37. package/templates/react-minimal/src/bootstrap.ts +6 -0
  38. package/templates/react-minimal/src/lib/controllers/home.ts +14 -0
  39. package/templates/react-minimal/src/main.tsx +44 -0
  40. package/templates/react-minimal/src/ssr.tsx +25 -0
  41. package/templates/react-minimal/tsconfig.json +17 -0
  42. package/templates/react-minimal/vite.config.ts +12 -0
  43. package/templates/svelte/env.d.ts +9 -0
  44. package/templates/svelte/index.html +13 -0
  45. package/templates/svelte/package.json +18 -0
  46. package/templates/svelte/src/App.svelte +85 -0
  47. package/templates/svelte/src/actions.ts +14 -0
  48. package/templates/svelte/src/bootstrap.ts +36 -0
  49. package/templates/svelte/src/components/Layout.svelte +19 -0
  50. package/templates/svelte/src/components/Loading.svelte +1 -0
  51. package/templates/svelte/src/components/Navigation.svelte +75 -0
  52. package/templates/svelte/src/components/PageRenderer.svelte +22 -0
  53. package/templates/svelte/src/components/ProductCard.svelte +37 -0
  54. package/templates/svelte/src/lib/controllers/about.ts +18 -0
  55. package/templates/svelte/src/lib/controllers/error.ts +11 -0
  56. package/templates/svelte/src/lib/controllers/home.ts +53 -0
  57. package/templates/svelte/src/lib/controllers/product-detail.ts +52 -0
  58. package/templates/svelte/src/lib/controllers/search.ts +58 -0
  59. package/templates/svelte/src/lib/framework-svelte.ts +34 -0
  60. package/templates/svelte/src/lib/guards/auth.ts +17 -0
  61. package/templates/svelte/src/lib/guards/seo.ts +15 -0
  62. package/templates/svelte/src/lib/mappers/product.ts +27 -0
  63. package/templates/svelte/src/lib/models/product.ts +47 -0
  64. package/templates/svelte/src/lib/render.ts +15 -0
  65. package/templates/svelte/src/lib/services/api-client.ts +28 -0
  66. package/templates/svelte/src/main.ts +27 -0
  67. package/templates/svelte/src/pages/About.svelte +10 -0
  68. package/templates/svelte/src/pages/Home.svelte +34 -0
  69. package/templates/svelte/src/pages/NotFound.svelte +23 -0
  70. package/templates/svelte/src/pages/ProductDetail.svelte +32 -0
  71. package/templates/svelte/src/pages/Search.svelte +29 -0
  72. package/templates/svelte/src/ssr.ts +12 -0
  73. package/templates/svelte/tsconfig.json +16 -0
  74. package/templates/svelte/vite.config.ts +21 -0
  75. package/templates/svelte-minimal/env.d.ts +9 -0
  76. package/templates/svelte-minimal/index.html +13 -0
  77. package/templates/svelte-minimal/package.json +18 -0
  78. package/templates/svelte-minimal/src/App.svelte +63 -0
  79. package/templates/svelte-minimal/src/bootstrap.ts +6 -0
  80. package/templates/svelte-minimal/src/lib/controllers/home.ts +15 -0
  81. package/templates/svelte-minimal/src/lib/framework-svelte.ts +34 -0
  82. package/templates/svelte-minimal/src/lib/render.ts +12 -0
  83. package/templates/svelte-minimal/src/main.ts +22 -0
  84. package/templates/svelte-minimal/src/pages/Home.svelte +10 -0
  85. package/templates/svelte-minimal/src/ssr.ts +18 -0
  86. package/templates/svelte-minimal/tsconfig.json +16 -0
  87. package/templates/svelte-minimal/vite.config.ts +12 -0
  88. package/templates/vue/env.d.ts +7 -0
  89. package/templates/vue/index.html +13 -0
  90. package/templates/vue/package.json +18 -0
  91. package/templates/vue/src/App.vue +29 -0
  92. package/templates/vue/src/actions.ts +14 -0
  93. package/templates/vue/src/bootstrap.ts +42 -0
  94. package/templates/vue/src/components/Layout.vue +18 -0
  95. package/templates/vue/src/components/Loading.vue +3 -0
  96. package/templates/vue/src/components/Navigation.vue +58 -0
  97. package/templates/vue/src/components/PageRenderer.vue +26 -0
  98. package/templates/vue/src/components/ProductCard.vue +39 -0
  99. package/templates/vue/src/lib/controllers/about.ts +18 -0
  100. package/templates/vue/src/lib/controllers/error.ts +11 -0
  101. package/templates/vue/src/lib/controllers/home.ts +53 -0
  102. package/templates/vue/src/lib/controllers/product-detail.ts +52 -0
  103. package/templates/vue/src/lib/controllers/search.ts +58 -0
  104. package/templates/vue/src/lib/guards/auth.ts +17 -0
  105. package/templates/vue/src/lib/guards/seo.ts +15 -0
  106. package/templates/vue/src/lib/mappers/product.ts +27 -0
  107. package/templates/vue/src/lib/models/product.ts +47 -0
  108. package/templates/vue/src/lib/services/api-client.ts +31 -0
  109. package/templates/vue/src/main.ts +46 -0
  110. package/templates/vue/src/pages/About.vue +16 -0
  111. package/templates/vue/src/pages/Home.vue +38 -0
  112. package/templates/vue/src/pages/NotFound.vue +22 -0
  113. package/templates/vue/src/pages/ProductDetail.vue +34 -0
  114. package/templates/vue/src/pages/Search.vue +33 -0
  115. package/templates/vue/src/ssr.ts +24 -0
  116. package/templates/vue/tsconfig.json +17 -0
  117. package/templates/vue/vite.config.ts +20 -0
  118. package/templates/vue-minimal/env.d.ts +7 -0
  119. package/templates/vue-minimal/index.html +13 -0
  120. package/templates/vue-minimal/package.json +18 -0
  121. package/templates/vue-minimal/src/App.vue +21 -0
  122. package/templates/vue-minimal/src/bootstrap.ts +6 -0
  123. package/templates/vue-minimal/src/lib/controllers/home.ts +15 -0
  124. package/templates/vue-minimal/src/main.ts +41 -0
  125. package/templates/vue-minimal/src/pages/Home.vue +12 -0
  126. package/templates/vue-minimal/src/ssr.ts +28 -0
  127. package/templates/vue-minimal/tsconfig.json +17 -0
  128. package/templates/vue-minimal/vite.config.ts +12 -0
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import { Framework } from "@finesoft/front";
3
+ import { untrack } from "svelte";
4
+ import Layout from "./components/Layout.svelte";
5
+ import Loading from "./components/Loading.svelte";
6
+ import PageRenderer from "./components/PageRenderer.svelte";
7
+ import { setFrameworkContext } from "./lib/framework-svelte";
8
+ import type { AppPage } from "./lib/models/product";
9
+
10
+ type Props = {
11
+ framework?: Framework;
12
+ page?: AppPage | null;
13
+ };
14
+
15
+ let props: Props = $props();
16
+
17
+ // context 只需在初始化时设置一次,用 untrack 明确声明仅读取初始值
18
+ const framework = untrack(() => props.framework);
19
+ if (framework) {
20
+ setFrameworkContext(framework);
21
+ }
22
+
23
+ /**
24
+ * 将导航 Promise 的 rejection 转为 ErrorPage,避免 {:catch} 暴露原始错误。
25
+ * 对应 template-project 的 transformRejectionIntoErrorPage 模式。
26
+ */
27
+ function wrapError(p: Promise<AppPage>): Promise<AppPage> {
28
+ return p.catch(
29
+ (err): AppPage =>
30
+ ({
31
+ id: "error",
32
+ pageType: "error",
33
+ title: "Error",
34
+ description:
35
+ err instanceof Error
36
+ ? err.message
37
+ : "Failed to load page",
38
+ status: 500,
39
+ }) as AppPage,
40
+ );
41
+ }
42
+
43
+ /**
44
+ * 当前页面数据:
45
+ * - AppPage: SSR 直出 或 已完成的导航
46
+ * - Promise<AppPage>: SPA 导航中(等待 controller 返回)
47
+ * - null: 初始空白态
48
+ */
49
+ let currentPage = $state<Promise<AppPage> | AppPage | null>(
50
+ untrack(() => props.page ?? null),
51
+ );
52
+
53
+ /** 用于 Navigation 高亮当前路径;Promise 导航完成后更新 */
54
+ let currentPath = $state<string>(untrack(() => props.page?.url ?? "/"));
55
+
56
+ /**
57
+ * 由 main.ts 的 mount 回调调用,驱动 SPA 导航。
58
+ * - 传入 Promise: 显示 Loading,resolve 后渲染页面
59
+ * - 传入 AppPage: 直接更新(SSR hydration 首帧)
60
+ */
61
+ export function updatePage(newPage: Promise<AppPage> | AppPage): void {
62
+ if (newPage instanceof Promise) {
63
+ const wrapped = wrapError(newPage);
64
+ currentPage = wrapped;
65
+ void wrapped.then((p) => {
66
+ currentPath = p.url ?? "/";
67
+ });
68
+ } else {
69
+ currentPage = newPage;
70
+ currentPath = newPage.url ?? "/";
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <Layout {currentPath}>
76
+ {#if currentPage instanceof Promise}
77
+ {#await currentPage}
78
+ <Loading />
79
+ {:then page}
80
+ <PageRenderer {page} />
81
+ {/await}
82
+ {:else if currentPage}
83
+ <PageRenderer page={currentPage} />
84
+ {/if}
85
+ </Layout>
@@ -0,0 +1,14 @@
1
+ import { makeExternalUrlAction, makeFlowAction, type Action } from "@finesoft/front";
2
+
3
+ /** 创建指向商品详情的 FlowAction */
4
+ export function productDetailAction(id: string): Action {
5
+ return makeFlowAction(`/products/${id}`);
6
+ }
7
+
8
+ /** 导航 Actions */
9
+ export const NAV_ACTIONS = {
10
+ home: makeFlowAction("/"),
11
+ search: makeFlowAction("/search"),
12
+ about: makeFlowAction("/about"),
13
+ github: makeExternalUrlAction("https://github.com/nicepkg/finesoft"),
14
+ } as const;
@@ -0,0 +1,36 @@
1
+ import { type Framework, defineRoutes } from "@finesoft/front";
2
+ import { AboutController } from "./lib/controllers/about";
3
+ import { HomeController } from "./lib/controllers/home";
4
+ import { ProductDetailController } from "./lib/controllers/product-detail";
5
+ import { SearchController } from "./lib/controllers/search";
6
+ import { authGuard } from "./lib/guards/auth";
7
+ import { seoGuard } from "./lib/guards/seo";
8
+
9
+ export function bootstrap(framework: Framework): void {
10
+ framework.afterLoad(seoGuard);
11
+
12
+ defineRoutes(framework, [
13
+ { path: "/", intentId: "home", controller: new HomeController() },
14
+ {
15
+ path: "/products/:id",
16
+ intentId: "product-detail",
17
+ controller: new ProductDetailController(),
18
+ },
19
+ {
20
+ path: "/search",
21
+ intentId: "search",
22
+ controller: new SearchController(),
23
+ },
24
+ {
25
+ path: "/about",
26
+ intentId: "about",
27
+ controller: new AboutController(),
28
+ renderMode: "csr",
29
+ },
30
+ {
31
+ path: "/admin",
32
+ intentId: "home",
33
+ beforeLoad: [authGuard],
34
+ },
35
+ ]);
36
+ }
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import Navigation from "./Navigation.svelte";
4
+
5
+ let {
6
+ children,
7
+ currentPath = "/",
8
+ }: {
9
+ children: Snippet;
10
+ currentPath?: string;
11
+ } = $props();
12
+ </script>
13
+
14
+ <div>
15
+ <Navigation {currentPath} />
16
+ <main style="padding: 1rem">
17
+ {@render children()}
18
+ </main>
19
+ </div>
@@ -0,0 +1 @@
1
+ <div style="text-align: center; padding: 2rem; color: #999">Loading…</div>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import {
3
+ makeExternalUrlAction,
4
+ makeFlowAction,
5
+ type Action,
6
+ } from "@finesoft/front";
7
+ import { getPerform } from "../lib/framework-svelte";
8
+
9
+ let { currentPath = "/" }: { currentPath?: string } = $props();
10
+
11
+ // 在组件初始化阶段获取 perform,getContext 只能在此阶段调用
12
+ const perform = getPerform();
13
+
14
+ const links = [
15
+ {
16
+ label: "Home",
17
+ action: makeFlowAction("/"),
18
+ path: "/" as string | null,
19
+ },
20
+ {
21
+ label: "Search",
22
+ action: makeFlowAction("/search"),
23
+ path: "/search" as string | null,
24
+ },
25
+ {
26
+ label: "About",
27
+ action: makeFlowAction("/about"),
28
+ path: "/about" as string | null,
29
+ },
30
+ {
31
+ label: "GitHub",
32
+ action: makeExternalUrlAction("https://github.com"),
33
+ path: null as string | null,
34
+ },
35
+ ];
36
+
37
+ function handleNavClick(action: Action) {
38
+ return (e: MouseEvent) => {
39
+ if (perform) {
40
+ e.preventDefault();
41
+ void perform(action);
42
+ }
43
+ };
44
+ }
45
+ </script>
46
+
47
+ <nav>
48
+ {#each links as link}
49
+ <a
50
+ href={link.action.url}
51
+ class:active={link.path !== null && currentPath === link.path}
52
+ onclick={handleNavClick(link.action)}>{link.label}</a
53
+ >
54
+ {/each}
55
+ </nav>
56
+
57
+ <style>
58
+ nav {
59
+ display: flex;
60
+ gap: 1rem;
61
+ padding: 1rem;
62
+ border-bottom: 1px solid #eee;
63
+ }
64
+ a {
65
+ text-decoration: none;
66
+ color: #333;
67
+ }
68
+ a:hover {
69
+ color: #007bff;
70
+ }
71
+ a.active {
72
+ color: #007bff;
73
+ font-weight: bold;
74
+ }
75
+ </style>
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import type { AppPage } from "../lib/models/product";
3
+ import About from "../pages/About.svelte";
4
+ import Home from "../pages/Home.svelte";
5
+ import NotFound from "../pages/NotFound.svelte";
6
+ import ProductDetail from "../pages/ProductDetail.svelte";
7
+ import Search from "../pages/Search.svelte";
8
+
9
+ let { page }: { page: AppPage } = $props();
10
+
11
+ const pageComponents: Record<string, any> = {
12
+ home: Home,
13
+ product: ProductDetail,
14
+ search: Search,
15
+ about: About,
16
+ error: NotFound,
17
+ };
18
+
19
+ const PageComponent = $derived(pageComponents[page.pageType] ?? NotFound);
20
+ </script>
21
+
22
+ <PageComponent {page} />
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import { getPerform } from "../lib/framework-svelte";
3
+ import type { ProductItem } from "../lib/models/product";
4
+
5
+ let { item }: { item: ProductItem } = $props();
6
+
7
+ const perform = getPerform();
8
+ </script>
9
+
10
+ <div class="product-card">
11
+ <h3>{item.name}</h3>
12
+ <p class="price">${item.price.toFixed(2)}</p>
13
+ {#if item.clickAction && "url" in item.clickAction}
14
+ <a
15
+ href={item.clickAction.url}
16
+ onclick={(e) => {
17
+ if (perform && item.clickAction) {
18
+ e.preventDefault();
19
+ void perform(item.clickAction);
20
+ }
21
+ }}>View Details &rarr;</a
22
+ >
23
+ {/if}
24
+ </div>
25
+
26
+ <style>
27
+ .product-card {
28
+ border: 1px solid #eee;
29
+ border-radius: 8px;
30
+ padding: 1rem;
31
+ min-width: 200px;
32
+ }
33
+ .price {
34
+ color: #007bff;
35
+ font-weight: bold;
36
+ }
37
+ </style>
@@ -0,0 +1,18 @@
1
+ import { BaseController } from "@finesoft/front";
2
+ import type { AboutPage } from "../models/product";
3
+
4
+ export class AboutController extends BaseController<Record<string, string>, AboutPage> {
5
+ readonly intentId = "about";
6
+
7
+ execute(): AboutPage {
8
+ return {
9
+ id: "about",
10
+ pageType: "about",
11
+ url: "/about",
12
+ title: "About",
13
+ description: "About Finesoft Front",
14
+ content:
15
+ "Finesoft Front is a full-stack TypeScript framework with router, DI, middleware, SSR, and multi-platform deployment.",
16
+ };
17
+ }
18
+ }
@@ -0,0 +1,11 @@
1
+ import type { ErrorPage } from "../models/product";
2
+
3
+ export function getErrorPage(status: number, message: string): ErrorPage {
4
+ return {
5
+ id: "error",
6
+ pageType: "error",
7
+ title: `Error ${status}`,
8
+ description: message,
9
+ status,
10
+ };
11
+ }
@@ -0,0 +1,53 @@
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";
33
+
34
+ execute(): HomePage {
35
+ const shelf: ProductShelf = {
36
+ id: "featured",
37
+ shelfType: "products",
38
+ title: "Featured Products",
39
+ isHorizontal: true,
40
+ seeAllAction: makeFlowAction("/search"),
41
+ items: MOCK_PRODUCTS,
42
+ };
43
+
44
+ return {
45
+ id: "home",
46
+ pageType: "home",
47
+ url: "/",
48
+ title: "Home",
49
+ description: "Welcome to Finesoft Front Demo",
50
+ shelves: [shelf],
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,52 @@
1
+ import {
2
+ BaseController,
3
+ type Container,
4
+ DEP_KEYS,
5
+ type Logger,
6
+ type LoggerFactory,
7
+ } from "@finesoft/front";
8
+ import type { ProductPage } from "../models/product";
9
+
10
+ export class ProductDetailController extends BaseController<{ id: string }, ProductPage> {
11
+ readonly intentId = "product-detail";
12
+
13
+ async execute(params: { id: string }, container: Container): Promise<ProductPage> {
14
+ const loggerFactory = container.resolve<LoggerFactory>(DEP_KEYS.LOGGER_FACTORY);
15
+ const log: Logger = loggerFactory.loggerFor("ProductDetailController");
16
+ log.info(`Loading product ${params.id}`);
17
+
18
+ // 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: 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`,
32
+ },
33
+ };
34
+ }
35
+
36
+ fallback(params: { id: string }, 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: params.id,
45
+ name: "Unknown Product",
46
+ price: 0,
47
+ description: "Could not load product data.",
48
+ imageUrl: "/img/placeholder.svg",
49
+ },
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,58 @@
1
+ import { BaseController, makeFlowAction } from "@finesoft/front";
2
+ import type { ProductItem, SearchPage } from "../models/product";
3
+
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
+ ];
38
+
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
+ };
57
+ }
58
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Svelte Context 集成 — Framework 实例的 Svelte context 传递
3
+ *
4
+ * 在根组件(App.svelte)中调用 setFrameworkContext,
5
+ * 子组件通过 getPerform 获取 perform 函数,无需逐层传递 onaction 回调。
6
+ */
7
+
8
+ import type { Action } from "@finesoft/front";
9
+ import { Framework } from "@finesoft/front";
10
+ import { getContext, setContext } from "svelte";
11
+
12
+ const FRAMEWORK_KEY = "framework";
13
+
14
+ /** 在根组件初始化时设置 Framework 实例到 Svelte context */
15
+ export function setFrameworkContext(framework: Framework): void {
16
+ setContext(FRAMEWORK_KEY, framework);
17
+ }
18
+
19
+ /** 获取 Framework 实例;SSR 或未设置时返回 undefined */
20
+ export function getFramework(): Framework | undefined {
21
+ return getContext<Framework | undefined>(FRAMEWORK_KEY);
22
+ }
23
+
24
+ /**
25
+ * 获取 perform 函数,供导航/操作类组件在事件处理器中调用。
26
+ * 在组件初始化阶段(<script> 顶层)调用,不能在事件回调内调用。
27
+ *
28
+ * @returns perform 函数;SSR 或无 context 时返回 undefined
29
+ */
30
+ export function getPerform(): ((action: Action) => Promise<void>) | undefined {
31
+ const fw = getFramework();
32
+ if (!fw) return undefined;
33
+ return (action: Action) => fw.perform(action);
34
+ }
@@ -0,0 +1,17 @@
1
+ import { type BeforeLoadGuard, next, redirect } from "@finesoft/front";
2
+
3
+ /**
4
+ * Auth guard — redirects unauthenticated users to login.
5
+ *
6
+ * Demonstrates beforeLoad middleware with `redirect()` and `next()`.
7
+ * In a real app, check a session cookie or token via `ctx.getCookie()`.
8
+ */
9
+ export const authGuard: BeforeLoadGuard = (ctx) => {
10
+ const token = ctx.getCookie("auth_token");
11
+
12
+ if (!token) {
13
+ return redirect(`/login?from=${encodeURIComponent(ctx.url)}`);
14
+ }
15
+
16
+ return next();
17
+ };
@@ -0,0 +1,15 @@
1
+ import { type AfterLoadGuard, next, rewrite } from "@finesoft/front";
2
+
3
+ /**
4
+ * SEO guard — normalizes URLs after data load.
5
+ *
6
+ * Demonstrates afterLoad middleware with `rewrite()`.
7
+ * Strips trailing slashes for canonical URL consistency.
8
+ */
9
+ export const seoGuard: AfterLoadGuard = (ctx) => {
10
+ if (ctx.path !== "/" && ctx.path.endsWith("/")) {
11
+ return rewrite(ctx.path.slice(0, -1));
12
+ }
13
+
14
+ return next();
15
+ };
@@ -0,0 +1,27 @@
1
+ import { makeFlowAction, mapEach, type Mapper } from "@finesoft/front";
2
+ import type { ProductItem } from "../models/product";
3
+
4
+ /** Raw API response shape */
5
+ interface ApiProduct {
6
+ id: number;
7
+ title: string;
8
+ price: number;
9
+ thumbnail?: string;
10
+ }
11
+
12
+ /** Transform raw API product → domain ProductItem */
13
+ const toProductItem: Mapper<ApiProduct, ProductItem> = (raw) => ({
14
+ id: String(raw.id),
15
+ itemType: "product",
16
+ name: raw.title,
17
+ price: raw.price,
18
+ imageUrl: raw.thumbnail ?? "/img/placeholder.svg",
19
+ clickAction: makeFlowAction(`/products/${raw.id}`),
20
+ });
21
+
22
+ /**
23
+ * Composed mapper pipeline: ApiProduct[] → ProductItem[]
24
+ *
25
+ * Demonstrates pipe() + mapEach() for data transformation.
26
+ */
27
+ export const mapProducts: Mapper<ApiProduct[], ProductItem[]> = mapEach(toProductItem);
@@ -0,0 +1,47 @@
1
+ import type { BaseItem, BasePage, BaseShelf } from "@finesoft/front";
2
+
3
+ export interface ProductItem extends BaseItem {
4
+ itemType: "product";
5
+ name: string;
6
+ price: number;
7
+ imageUrl: string;
8
+ }
9
+
10
+ export interface ProductShelf extends BaseShelf {
11
+ shelfType: "products";
12
+ items: ProductItem[];
13
+ }
14
+
15
+ export interface HomePage extends BasePage {
16
+ pageType: "home";
17
+ shelves: ProductShelf[];
18
+ }
19
+
20
+ export interface ProductPage extends BasePage {
21
+ pageType: "product";
22
+ product: {
23
+ id: string;
24
+ name: string;
25
+ price: number;
26
+ description: string;
27
+ imageUrl: string;
28
+ };
29
+ }
30
+
31
+ export interface SearchPage extends BasePage {
32
+ pageType: "search";
33
+ query: string;
34
+ results: ProductItem[];
35
+ }
36
+
37
+ export interface AboutPage extends BasePage {
38
+ pageType: "about";
39
+ content: string;
40
+ }
41
+
42
+ export interface ErrorPage extends BasePage {
43
+ pageType: "error";
44
+ status: number;
45
+ }
46
+
47
+ export type AppPage = HomePage | ProductPage | SearchPage | AboutPage | ErrorPage;
@@ -0,0 +1,15 @@
1
+ import type { BasePage } from "@finesoft/front";
2
+ import { render } from "svelte/server";
3
+ import App from "../App.svelte";
4
+ import type { AppPage } from "./models/product";
5
+
6
+ export function renderApp(page: BasePage, _locale: string) {
7
+ const result = render(App, { props: { page: page as AppPage } });
8
+ return {
9
+ html: result.body ?? "",
10
+ head: `<title>${page.title}</title><meta name="description" content="${
11
+ page.description ?? ""
12
+ }">${result.head ?? ""}`,
13
+ css: "",
14
+ };
15
+ }
@@ -0,0 +1,28 @@
1
+ import { HttpClient, type HttpClientConfig } from "@finesoft/front";
2
+
3
+ /**
4
+ * Demo API client — extends HttpClient for typed endpoint access.
5
+ *
6
+ * Showcases HttpClient subclass pattern with base URL and custom fetch.
7
+ */
8
+ export class ApiClient extends HttpClient {
9
+ constructor(config?: Partial<HttpClientConfig>) {
10
+ super({
11
+ baseUrl: config?.baseUrl ?? "/api",
12
+ defaultHeaders: { "Content-Type": "application/json", ...config?.defaultHeaders },
13
+ fetch: config?.fetch,
14
+ });
15
+ }
16
+
17
+ async getProducts(): Promise<unknown[]> {
18
+ return this.get<unknown[]>("/products");
19
+ }
20
+
21
+ async getProduct(id: string): Promise<unknown> {
22
+ return this.get<unknown>(`/products/${id}`);
23
+ }
24
+
25
+ async searchProducts(query: string): Promise<unknown[]> {
26
+ return this.get<unknown[]>("/products", { q: query });
27
+ }
28
+ }