@finesoft/create-app 0.1.33 → 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.
- package/dist/index.mjs +22 -26
- package/package.json +14 -5
- package/templates/react/README.md +35 -0
- package/templates/react/index.html +2 -3
- package/templates/react/package.json +12 -8
- package/templates/react/src/App.tsx +7 -15
- package/templates/react/src/app-definition.ts +35 -0
- package/templates/react/src/components/Layout.tsx +5 -52
- package/templates/react/src/components/Loading.tsx +1 -1
- package/templates/react/src/components/Navigation.tsx +10 -37
- package/templates/react/src/components/ProductCard.tsx +5 -23
- package/templates/react/src/config.ts +2 -0
- package/templates/react/src/lib/controllers/about.ts +18 -13
- package/templates/react/src/lib/controllers/error.ts +11 -7
- package/templates/react/src/lib/controllers/home.ts +36 -41
- package/templates/react/src/lib/controllers/product-detail.ts +71 -40
- package/templates/react/src/lib/controllers/search.ts +33 -54
- package/templates/react/src/lib/data/products.ts +36 -0
- package/templates/react/src/lib/mappers/product.ts +3 -3
- package/templates/react/src/lib/models/product.ts +11 -1
- package/templates/react/src/lib/services/api-client.ts +7 -4
- package/templates/react/src/main.ts +34 -0
- package/templates/react/src/pages/About.tsx +2 -4
- package/templates/react/src/pages/Home.tsx +6 -15
- package/templates/react/src/pages/NotFound.tsx +4 -14
- package/templates/react/src/pages/ProductDetail.tsx +5 -23
- package/templates/react/src/pages/Search.tsx +5 -7
- package/templates/react/src/ssr.ts +11 -0
- package/templates/react/src/styles.css +71 -0
- package/templates/react/src/views.ts +12 -0
- package/templates/react/tsconfig.json +14 -9
- package/templates/react/vite.config.ts +9 -13
- package/templates/react-minimal/README.md +39 -0
- package/templates/react-minimal/index.html +2 -3
- package/templates/react-minimal/package.json +12 -8
- package/templates/react-minimal/src/App.tsx +40 -85
- package/templates/react-minimal/src/app-definition.ts +48 -0
- package/templates/react-minimal/src/config.ts +2 -0
- package/templates/react-minimal/src/lib/controllers/detail.ts +19 -18
- package/templates/react-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/react-minimal/src/lib/controllers/home.ts +19 -24
- package/templates/react-minimal/src/lib/controllers/notes.ts +7 -5
- package/templates/react-minimal/src/lib/locale.ts +16 -0
- package/templates/react-minimal/src/lib/models/page.ts +27 -0
- package/templates/react-minimal/src/lib/navigation.ts +11 -0
- package/templates/react-minimal/src/locales/en-US.json +5 -0
- package/templates/react-minimal/src/locales/zh-Hans.json +5 -0
- package/templates/react-minimal/src/main.ts +43 -0
- package/templates/react-minimal/src/pages/Detail.tsx +20 -0
- package/templates/react-minimal/src/pages/Home.tsx +38 -0
- package/templates/react-minimal/src/pages/NotFound.tsx +11 -0
- package/templates/react-minimal/src/pages/Notes.tsx +21 -0
- package/templates/react-minimal/src/ssr.ts +11 -0
- package/templates/react-minimal/src/styles.css +74 -0
- package/templates/react-minimal/src/views.ts +6 -0
- package/templates/react-minimal/src/vite-env.d.ts +3 -0
- package/templates/react-minimal/tsconfig.json +14 -9
- package/templates/react-minimal/vite.config.ts +9 -7
- package/templates/svelte/README.md +35 -0
- package/templates/svelte/index.html +1 -2
- package/templates/svelte/package.json +9 -5
- package/templates/svelte/src/App.svelte +8 -80
- package/templates/svelte/src/app-definition.ts +35 -0
- package/templates/svelte/src/components/Layout.svelte +2 -2
- package/templates/svelte/src/components/Loading.svelte +1 -1
- package/templates/svelte/src/components/Navigation.svelte +5 -66
- package/templates/svelte/src/components/ProductCard.svelte +3 -24
- package/templates/svelte/src/config.ts +2 -0
- package/templates/svelte/src/lib/controllers/about.ts +18 -13
- package/templates/svelte/src/lib/controllers/error.ts +11 -7
- package/templates/svelte/src/lib/controllers/home.ts +36 -41
- package/templates/svelte/src/lib/controllers/product-detail.ts +71 -40
- package/templates/svelte/src/lib/controllers/search.ts +33 -54
- package/templates/svelte/src/lib/data/products.ts +36 -0
- package/templates/svelte/src/lib/mappers/product.ts +3 -3
- package/templates/svelte/src/lib/models/product.ts +12 -1
- package/templates/svelte/src/lib/services/api-client.ts +7 -4
- package/templates/svelte/src/main.ts +28 -24
- package/templates/svelte/src/pages/About.svelte +2 -2
- package/templates/svelte/src/pages/Home.svelte +3 -15
- package/templates/svelte/src/pages/NotFound.svelte +3 -12
- package/templates/svelte/src/pages/ProductDetail.svelte +4 -21
- package/templates/svelte/src/pages/Search.svelte +2 -10
- package/templates/svelte/src/ssr.ts +10 -9
- package/templates/svelte/src/styles.css +71 -0
- package/templates/svelte/src/views.ts +12 -0
- package/templates/svelte/svelte.config.js +2 -0
- package/templates/svelte/tsconfig.json +13 -8
- package/templates/svelte/vite.config.ts +9 -13
- package/templates/svelte-minimal/README.md +39 -0
- package/templates/svelte-minimal/index.html +1 -2
- package/templates/svelte-minimal/package.json +9 -5
- package/templates/svelte-minimal/src/App.svelte +49 -61
- package/templates/svelte-minimal/src/app-definition.ts +48 -0
- package/templates/svelte-minimal/src/config.ts +2 -0
- package/templates/svelte-minimal/src/lib/controllers/detail.ts +23 -0
- package/templates/svelte-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/svelte-minimal/src/lib/controllers/home.ts +24 -23
- package/templates/svelte-minimal/src/lib/controllers/notes.ts +18 -0
- package/templates/svelte-minimal/src/lib/locale.ts +16 -0
- package/templates/svelte-minimal/src/lib/models/page.ts +27 -0
- package/templates/svelte-minimal/src/lib/navigation.ts +11 -0
- package/templates/svelte-minimal/src/locales/en-US.json +1 -3
- package/templates/svelte-minimal/src/locales/zh-Hans.json +1 -3
- package/templates/svelte-minimal/src/main.ts +37 -19
- package/templates/svelte-minimal/src/pages/Detail.svelte +11 -0
- package/templates/svelte-minimal/src/pages/Home.svelte +17 -21
- package/templates/svelte-minimal/src/pages/NotFound.svelte +10 -0
- package/templates/svelte-minimal/src/pages/Notes.svelte +11 -0
- package/templates/svelte-minimal/src/ssr.ts +9 -17
- package/templates/svelte-minimal/src/styles.css +74 -0
- package/templates/svelte-minimal/src/views.ts +6 -0
- package/templates/svelte-minimal/src/vite-env.d.ts +3 -0
- package/templates/svelte-minimal/svelte.config.js +2 -0
- package/templates/svelte-minimal/tsconfig.json +13 -8
- package/templates/svelte-minimal/vite.config.ts +9 -10
- package/templates/vue/README.md +35 -0
- package/templates/vue/index.html +1 -2
- package/templates/vue/package.json +9 -5
- package/templates/vue/src/App.vue +7 -22
- package/templates/vue/src/app-definition.ts +35 -0
- package/templates/vue/src/components/Layout.vue +4 -6
- package/templates/vue/src/components/Loading.vue +1 -1
- package/templates/vue/src/components/Navigation.vue +10 -43
- package/templates/vue/src/components/ProductCard.vue +4 -28
- package/templates/vue/src/config.ts +2 -0
- package/templates/vue/src/lib/controllers/about.ts +18 -13
- package/templates/vue/src/lib/controllers/error.ts +11 -7
- package/templates/vue/src/lib/controllers/home.ts +36 -41
- package/templates/vue/src/lib/controllers/product-detail.ts +71 -40
- package/templates/vue/src/lib/controllers/search.ts +33 -54
- package/templates/vue/src/lib/data/products.ts +36 -0
- package/templates/vue/src/lib/mappers/product.ts +3 -3
- package/templates/vue/src/lib/models/product.ts +12 -1
- package/templates/vue/src/lib/services/api-client.ts +7 -7
- package/templates/vue/src/main.ts +28 -43
- package/templates/vue/src/pages/About.vue +3 -5
- package/templates/vue/src/pages/Home.vue +5 -23
- package/templates/vue/src/pages/NotFound.vue +4 -13
- package/templates/vue/src/pages/ProductDetail.vue +5 -23
- package/templates/vue/src/pages/Search.vue +4 -18
- package/templates/vue/src/ssr.ts +5 -18
- package/templates/vue/src/styles.css +71 -0
- package/templates/vue/src/views.ts +12 -0
- package/templates/vue/tsconfig.json +14 -9
- package/templates/vue/vite.config.ts +9 -13
- package/templates/vue-minimal/README.md +39 -0
- package/templates/vue-minimal/index.html +1 -2
- package/templates/vue-minimal/package.json +9 -5
- package/templates/vue-minimal/src/App.vue +46 -43
- package/templates/vue-minimal/src/app-definition.ts +48 -0
- package/templates/vue-minimal/src/config.ts +2 -0
- package/templates/vue-minimal/src/lib/controllers/detail.ts +19 -18
- package/templates/vue-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/vue-minimal/src/lib/controllers/home.ts +19 -24
- package/templates/vue-minimal/src/lib/controllers/notes.ts +7 -5
- package/templates/vue-minimal/src/lib/locale.ts +16 -0
- package/templates/vue-minimal/src/lib/models/page.ts +27 -0
- package/templates/vue-minimal/src/lib/navigation.ts +11 -0
- package/templates/vue-minimal/src/locales/en-US.json +5 -0
- package/templates/vue-minimal/src/locales/zh-Hans.json +5 -0
- package/templates/vue-minimal/src/main.ts +35 -70
- package/templates/vue-minimal/src/pages/Detail.vue +20 -0
- package/templates/vue-minimal/src/pages/Home.vue +34 -0
- package/templates/vue-minimal/src/pages/NotFound.vue +13 -0
- package/templates/vue-minimal/src/pages/Notes.vue +22 -0
- package/templates/vue-minimal/src/ssr.ts +7 -46
- package/templates/vue-minimal/src/styles.css +74 -0
- package/templates/vue-minimal/src/views.ts +6 -0
- package/templates/vue-minimal/src/vite-env.d.ts +3 -0
- package/templates/vue-minimal/tsconfig.json +14 -9
- package/templates/vue-minimal/vite.config.ts +9 -7
- package/templates/react/src/actions.ts +0 -14
- package/templates/react/src/bootstrap.ts +0 -36
- package/templates/react/src/components/PageRenderer.tsx +0 -41
- package/templates/react/src/main.tsx +0 -49
- package/templates/react/src/ssr.tsx +0 -23
- package/templates/react-minimal/src/bootstrap.ts +0 -39
- package/templates/react-minimal/src/main.tsx +0 -99
- package/templates/react-minimal/src/ssr.tsx +0 -51
- package/templates/react-minimal/src/views/DetailView.tsx +0 -28
- package/templates/react-minimal/src/views/HomeView.tsx +0 -27
- package/templates/react-minimal/src/views/NotesView.tsx +0 -11
- package/templates/svelte/src/actions.ts +0 -14
- package/templates/svelte/src/bootstrap.ts +0 -36
- package/templates/svelte/src/components/PageRenderer.svelte +0 -22
- package/templates/svelte/src/lib/framework-svelte.ts +0 -34
- package/templates/svelte/src/lib/render.ts +0 -15
- package/templates/svelte-minimal/src/bootstrap.ts +0 -6
- package/templates/svelte-minimal/src/lib/framework-svelte.ts +0 -34
- package/templates/svelte-minimal/src/lib/render.ts +0 -12
- package/templates/vue/src/actions.ts +0 -14
- package/templates/vue/src/bootstrap.ts +0 -42
- package/templates/vue/src/components/PageRenderer.vue +0 -26
- package/templates/vue-minimal/src/bootstrap.ts +0 -39
- package/templates/vue-minimal/src/views/DetailView.vue +0 -29
- package/templates/vue-minimal/src/views/HomeView.vue +0 -26
- package/templates/vue-minimal/src/views/NotesView.vue +0 -12
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NotesPage } from "../lib/models/page";
|
|
2
|
+
|
|
3
|
+
export default function Notes({ page }: { page: NotesPage }) {
|
|
4
|
+
return (
|
|
5
|
+
<section className="page">
|
|
6
|
+
<h1>{page.title}</h1>
|
|
7
|
+
<p>{page.description}</p>
|
|
8
|
+
{/* The entry owns this draft; data-restore-root also restores it after reload. */}
|
|
9
|
+
<div className="draft" data-restore-root>
|
|
10
|
+
<label>
|
|
11
|
+
Notes draft
|
|
12
|
+
<textarea
|
|
13
|
+
name="notes"
|
|
14
|
+
rows={6}
|
|
15
|
+
placeholder="Write a note; switch tabs or reload to restore it"
|
|
16
|
+
/>
|
|
17
|
+
</label>
|
|
18
|
+
</div>
|
|
19
|
+
</section>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { createSSRRender } from "@finesoft/front";
|
|
4
|
+
import { app } from "./app-definition";
|
|
5
|
+
import App from "./App";
|
|
6
|
+
|
|
7
|
+
export const render = createSSRRender({
|
|
8
|
+
definition: app,
|
|
9
|
+
render: (app) => renderToString(createElement(App, { app })),
|
|
10
|
+
});
|
|
11
|
+
export { serializeServerData } from "@finesoft/front";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: system-ui, sans-serif;
|
|
3
|
+
color: #202124;
|
|
4
|
+
background: #fff;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
}
|
|
10
|
+
button,
|
|
11
|
+
input,
|
|
12
|
+
textarea {
|
|
13
|
+
font: inherit;
|
|
14
|
+
}
|
|
15
|
+
button {
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app-chrome,
|
|
20
|
+
.page {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
max-width: 34rem;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
padding: 1rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.profile,
|
|
28
|
+
.tabs {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 0.5rem;
|
|
31
|
+
align-items: center;
|
|
32
|
+
margin-bottom: 1rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.profile label {
|
|
36
|
+
flex: 1;
|
|
37
|
+
}
|
|
38
|
+
.profile input {
|
|
39
|
+
display: block;
|
|
40
|
+
width: 100%;
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
}
|
|
43
|
+
.tabs button[aria-current="true"] {
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
}
|
|
46
|
+
.feed {
|
|
47
|
+
list-style: none;
|
|
48
|
+
padding: 0;
|
|
49
|
+
}
|
|
50
|
+
.feed li + li {
|
|
51
|
+
margin-top: 0.5rem;
|
|
52
|
+
}
|
|
53
|
+
.feed button {
|
|
54
|
+
width: 100%;
|
|
55
|
+
padding: 0.75rem;
|
|
56
|
+
text-align: left;
|
|
57
|
+
}
|
|
58
|
+
.draft label,
|
|
59
|
+
.draft input,
|
|
60
|
+
.draft textarea {
|
|
61
|
+
display: block;
|
|
62
|
+
width: 100%;
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
}
|
|
65
|
+
.draft input,
|
|
66
|
+
.draft textarea {
|
|
67
|
+
margin-top: 0.5rem;
|
|
68
|
+
padding: 0.5rem;
|
|
69
|
+
}
|
|
70
|
+
.locale-info {
|
|
71
|
+
margin-top: 2rem;
|
|
72
|
+
color: #555;
|
|
73
|
+
font-size: 0.875rem;
|
|
74
|
+
}
|
|
@@ -4,20 +4,25 @@
|
|
|
4
4
|
"target": "ESNext",
|
|
5
5
|
"module": "ESNext",
|
|
6
6
|
"moduleResolution": "bundler",
|
|
7
|
-
"
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
8
11
|
"esModuleInterop": true,
|
|
9
12
|
"forceConsistentCasingInFileNames": true,
|
|
10
13
|
"skipLibCheck": true,
|
|
11
14
|
"noEmit": true,
|
|
15
|
+
"rootDir": ".",
|
|
16
|
+
"jsx": "react-jsx",
|
|
12
17
|
"paths": {
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"@finesoft/core": ["../../packages/core/src/index.ts"],
|
|
17
|
-
"@finesoft/browser": ["../../packages/browser/src/index.ts"],
|
|
18
|
-
"@finesoft/ssr": ["../../packages/ssr/src/index.ts"],
|
|
19
|
-
"@finesoft/server": ["../../packages/server/src/index.ts"]
|
|
18
|
+
"@finesoft/front": [
|
|
19
|
+
"./.finesoft/front.d.ts"
|
|
20
|
+
]
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
|
-
"include": [
|
|
23
|
+
"include": [
|
|
24
|
+
"src/**/*",
|
|
25
|
+
"env.d.ts",
|
|
26
|
+
"vite.config.ts"
|
|
27
|
+
]
|
|
23
28
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
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
|
+
i18n: { messagesDir: "src/locales" },
|
|
9
|
+
});
|
|
4
10
|
|
|
5
11
|
export default defineConfig({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
finesoftFrontViteConfig({
|
|
9
|
-
ssr: { entry: "src/ssr.tsx" },
|
|
10
|
-
}),
|
|
11
|
-
],
|
|
12
|
+
lint: { options: { typeAware: true, typeCheck: true } },
|
|
13
|
+
plugins: lazyPlugins(() => [front, react()]),
|
|
12
14
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Finesoft Front full application
|
|
2
|
+
|
|
3
|
+
Home, product detail, search and About demonstrate SSR, CSR, URL actions, typed route parameters, DI, data mapping and guards. 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 product, try `/search?q=Vite`, and navigate to `/about` (CSR). `/admin` demonstrates an authentication guard: without an `auth_token` cookie it redirects to `/login?from=...`. Add your real login page and authentication service when adopting this example.
|
|
17
|
+
|
|
18
|
+
## Server controller example
|
|
19
|
+
|
|
20
|
+
`src/lib/controllers/product-detail.ts` extends `BaseServerController` from `@finesoft/front`. Open `/products/1`, then navigate to another product. The controller reads the incoming `last_product` cookie and explicitly writes a new HttpOnly cookie when the product changes. It also sets `Cache-Control: private, no-store`. This is an application-owned browsing preference; the framework provides request/response tools and does not manage login state or automatically forward credentials.
|
|
21
|
+
|
|
22
|
+
SSR executes the controller directly. Browser navigation invokes it on the server, and hydration reuses the SSR result. Its implementation stays out of the browser bundle; `markPublic` selects the page data sent to the browser. Home, search and About keep ordinary `BaseController` examples. Route types still come from `src/app-definition.ts`; the Vite plugin maintains the generated `Input` / `Failure` types and provides the server context automatically.
|
|
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. The template runs locally without external API credentials.
|
|
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
|
+
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.
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
"preview": "vp preview"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@finesoft/front": "^0.5.
|
|
12
|
-
"svelte": "^5.
|
|
11
|
+
"@finesoft/front": "^0.5.2",
|
|
12
|
+
"svelte": "^5.56.8"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@
|
|
16
|
-
"vite": "
|
|
17
|
-
"
|
|
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,85 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
import { untrack } from "svelte";
|
|
3
|
+
import { Outlet as selectOutlet, useSnapshot, type WebAppView } from "@finesoft/front";
|
|
4
4
|
import Layout from "./components/Layout.svelte";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
}
|
|
5
|
+
import { views } from "./views";
|
|
6
|
+
const Outlet = selectOutlet("svelte");
|
|
7
|
+
let { app }: { app: WebAppView } = $props();
|
|
8
|
+
const snapshot = untrack(() => useSnapshot("svelte", app));
|
|
73
9
|
</script>
|
|
74
10
|
|
|
75
|
-
<Layout {
|
|
76
|
-
{
|
|
77
|
-
{#await currentPage}
|
|
78
|
-
<Loading />
|
|
79
|
-
{:then page}
|
|
80
|
-
<PageRenderer {page} />
|
|
81
|
-
{/await}
|
|
82
|
-
{:else if currentPage}
|
|
83
|
-
<PageRenderer page={currentPage} />
|
|
84
|
-
{/if}
|
|
11
|
+
<Layout currentPath={$snapshot.entries.find((entry) => entry.visible)?.page.url ?? "/"}>
|
|
12
|
+
<Outlet {app} {views} />
|
|
85
13
|
</Layout>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { appId } from "./config";
|
|
2
|
+
import { definePage, defineWebApp, int, str, withDefault } from "@finesoft/front";
|
|
3
|
+
import { AboutController } from "./lib/controllers/about";
|
|
4
|
+
import { HomeController } from "./lib/controllers/home";
|
|
5
|
+
import { ProductDetailController } from "./lib/controllers/product-detail";
|
|
6
|
+
import { SearchController } from "./lib/controllers/search";
|
|
7
|
+
import { authGuard } from "./lib/guards/auth";
|
|
8
|
+
import { seoGuard } from "./lib/guards/seo";
|
|
9
|
+
import { getErrorPage } from "./lib/controllers/error";
|
|
10
|
+
export const homePage = definePage({
|
|
11
|
+
id: "home",
|
|
12
|
+
create: () => new HomeController(),
|
|
13
|
+
routes: ["/", { path: "/admin", beforeLoad: [authGuard] }],
|
|
14
|
+
});
|
|
15
|
+
export const productDetailPage = definePage({
|
|
16
|
+
id: "product-detail",
|
|
17
|
+
routes: [{ path: "/products/:id", params: { id: int() } }],
|
|
18
|
+
create: () => new ProductDetailController(),
|
|
19
|
+
});
|
|
20
|
+
export const searchPage = definePage({
|
|
21
|
+
id: "search",
|
|
22
|
+
create: () => new SearchController(),
|
|
23
|
+
routes: [{ path: "/search", query: { q: withDefault(str(), "") } }],
|
|
24
|
+
});
|
|
25
|
+
export const aboutPage = definePage({
|
|
26
|
+
id: "about",
|
|
27
|
+
create: () => new AboutController(),
|
|
28
|
+
routes: [{ path: "/about", renderMode: "csr" }],
|
|
29
|
+
});
|
|
30
|
+
export const app = defineWebApp({
|
|
31
|
+
id: appId,
|
|
32
|
+
pages: [homePage, productDetailPage, searchPage, aboutPage],
|
|
33
|
+
getErrorPage,
|
|
34
|
+
afterLoad: [seoGuard],
|
|
35
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<div
|
|
1
|
+
<div class="loading">Loading…</div>
|
|
@@ -1,75 +1,14 @@
|
|
|
1
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
2
|
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
|
-
}
|
|
3
|
+
const links = [{ label: "Home", href: "/" }, { label: "Search", href: "/search" }, { label: "About", href: "/about" }, { label: "GitHub", href: "https://github.com/nicepkg/finesoft" }];
|
|
45
4
|
</script>
|
|
46
5
|
|
|
47
|
-
<nav>
|
|
6
|
+
<nav class="navigation">
|
|
48
7
|
{#each links as link}
|
|
49
8
|
<a
|
|
50
|
-
href={link.
|
|
51
|
-
class
|
|
52
|
-
|
|
9
|
+
href={link.href}
|
|
10
|
+
class="navigation-link"
|
|
11
|
+
class:active={currentPath === link.href}>{link.label}</a
|
|
53
12
|
>
|
|
54
13
|
{/each}
|
|
55
14
|
</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>
|
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getPerform } from "../lib/framework-svelte";
|
|
3
2
|
import type { ProductItem } from "../lib/models/product";
|
|
4
3
|
|
|
5
4
|
let { item }: { item: ProductItem } = $props();
|
|
6
|
-
|
|
7
|
-
const perform = getPerform();
|
|
8
5
|
</script>
|
|
9
6
|
|
|
10
|
-
<
|
|
7
|
+
<article class="product-card">
|
|
11
8
|
<h3>{item.name}</h3>
|
|
12
9
|
<p class="price">${item.price.toFixed(2)}</p>
|
|
13
10
|
{#if item.clickAction && "url" in item.clickAction}
|
|
14
11
|
<a
|
|
15
12
|
href={item.clickAction.url}
|
|
16
|
-
|
|
17
|
-
if (perform && item.clickAction) {
|
|
18
|
-
e.preventDefault();
|
|
19
|
-
void perform(item.clickAction);
|
|
20
|
-
}
|
|
21
|
-
}}>View Details →</a
|
|
13
|
+
>View Details →</a
|
|
22
14
|
>
|
|
23
15
|
{/if}
|
|
24
|
-
</
|
|
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>
|
|
16
|
+
</article>
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { BaseController } from "@finesoft/front";
|
|
1
|
+
import { BaseController, type ControllerInput } from "@finesoft/front";
|
|
2
|
+
import { markPublic } from "@finesoft/front";
|
|
2
3
|
import type { AboutPage } from "../models/product";
|
|
3
4
|
|
|
4
|
-
export class AboutController extends BaseController<
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export class AboutController extends BaseController<
|
|
6
|
+
ControllerInput<Record<string, string>>,
|
|
7
|
+
AboutPage
|
|
8
|
+
> {
|
|
7
9
|
execute(): AboutPage {
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
return markPublic(
|
|
11
|
+
{
|
|
12
|
+
id: "about",
|
|
13
|
+
pageType: "about",
|
|
14
|
+
title: "About",
|
|
15
|
+
description: "About Finesoft Front",
|
|
16
|
+
url: "/about",
|
|
17
|
+
content:
|
|
18
|
+
"Finesoft Front is a full-stack TypeScript framework with router, DI, middleware, SSR, and multi-platform deployment.",
|
|
19
|
+
},
|
|
20
|
+
["content"],
|
|
21
|
+
);
|
|
17
22
|
}
|
|
18
23
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { markPublic } from "@finesoft/front";
|
|
1
2
|
import type { ErrorPage } from "../models/product";
|
|
2
3
|
|
|
3
4
|
export function getErrorPage(status: number, message: string): ErrorPage {
|
|
4
|
-
return
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
return markPublic(
|
|
6
|
+
{
|
|
7
|
+
id: "error",
|
|
8
|
+
pageType: "error",
|
|
9
|
+
title: `Error ${status}`,
|
|
10
|
+
description: message,
|
|
11
|
+
status,
|
|
12
|
+
},
|
|
13
|
+
["status"],
|
|
14
|
+
);
|
|
11
15
|
}
|