@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.
- 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
|
@@ -1,75 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type MountEntry,
|
|
6
|
-
type NavigationSnapshot,
|
|
7
|
-
type SessionStateProvider,
|
|
8
|
-
} from "@finesoft/front";
|
|
9
|
-
import { createApp, createSSRApp, markRaw, reactive, type Component } from "vue";
|
|
1
|
+
import { createBrowserApp } from "@finesoft/front";
|
|
2
|
+
import { createApp, createSSRApp } from "vue";
|
|
3
|
+
import { app } from "./app-definition";
|
|
4
|
+
import { appId } from "./config";
|
|
10
5
|
import App from "./App.vue";
|
|
11
|
-
import
|
|
12
|
-
import DetailView from "./views/DetailView.vue";
|
|
13
|
-
import NotesView from "./views/NotesView.vue";
|
|
14
|
-
import { bootstrap, navigation } from "./bootstrap";
|
|
6
|
+
import "./styles.css";
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
const root = (handle.shouldHydrate ? createSSRApp : createApp)(App, { app: handle });
|
|
24
|
+
root.mount(target);
|
|
25
|
+
const originalDispose = handle.dispose.bind(handle);
|
|
26
|
+
let disposal: Promise<void> | undefined;
|
|
27
|
+
Object.assign(handle, {
|
|
28
|
+
dispose: () => (disposal ??= originalDispose().finally(() => root.unmount())),
|
|
29
|
+
});
|
|
30
|
+
await handle.ready;
|
|
31
|
+
return handle;
|
|
20
32
|
}
|
|
21
|
-
export type AppController = AppHandle; // 组件 prop 类型 = 框架统一句柄
|
|
22
33
|
|
|
23
|
-
const
|
|
34
|
+
export const started = mountApplication(document.getElementById("app")!);
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
state.name = (data as { name?: string }).name ?? "";
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** islands 闭包引用:mount 回调赋值,islands(mount 后挂)读时已就绪。 */
|
|
35
|
-
let controller: AppHandle | undefined;
|
|
36
|
-
|
|
37
|
-
/** intent → 视图组件。islands 按 entry 挂为独立 Vue app。 */
|
|
38
|
-
const VIEWS: Record<string, Component> = { home: HomeView, detail: DetailView, notes: NotesView };
|
|
39
|
-
|
|
40
|
-
const mountEntry: MountEntry = (entry, container) => {
|
|
41
|
-
const view = VIEWS[entry.intent] ?? HomeView;
|
|
42
|
-
// 首屏 island 已由 SSR 渲入容器(编排器收养并置 hydrate:true)→ 水合;否则新建。
|
|
43
|
-
const factory = entry.hydrate ? createSSRApp : createApp;
|
|
44
|
-
const app = factory(view, { page: entry.page, controller });
|
|
45
|
-
app.mount(container);
|
|
46
|
-
return { unmount: () => app.unmount() };
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
void startBrowserApp({
|
|
50
|
-
bootstrap,
|
|
51
|
-
mount(target: HTMLElement, ctx) {
|
|
52
|
-
controller = ctx.app; // islands(mount 后挂)与 chrome 共用
|
|
53
|
-
const nav = ctx.navigation;
|
|
54
|
-
if (nav) {
|
|
55
|
-
state.snapshot = nav.getSnapshot(); // mount 时 tree 已就绪
|
|
56
|
-
nav.subscribe((s) => (state.snapshot = s));
|
|
57
|
-
}
|
|
58
|
-
// 方案 C:chrome 挂到 sibling chrome-root(不含 outlet);outlet 由框架编排器独占。
|
|
59
|
-
const { chromeRoot, hydrate } = resolveIslandsShell(target);
|
|
60
|
-
(hydrate ? createSSRApp : createApp)(App, { state, controller: markRaw(ctx.app!) }).mount(
|
|
61
|
-
chromeRoot,
|
|
62
|
-
);
|
|
63
|
-
return () => undefined;
|
|
64
|
-
},
|
|
65
|
-
callbacks: {
|
|
66
|
-
onNavigate() {},
|
|
67
|
-
onModal() {},
|
|
68
|
-
},
|
|
69
|
-
// 结构化导航 + islands:每屏 per-entry 挂为独立 root、保活。
|
|
70
|
-
navigation: { ...navigation.toBrowserConfig(), mountEntry },
|
|
71
|
-
// 重载 DOM 自动恢复:data-restore-root 内字段/滚动自动捕获回填。
|
|
72
|
-
domRestore: true,
|
|
73
|
-
// 会话恢复:注册全局切片 provider;导航位置 + 作用域状态由框架自动捕获。
|
|
74
|
-
session: { providers: [profileProvider] },
|
|
75
|
-
});
|
|
36
|
+
if (import.meta.hot) {
|
|
37
|
+
import.meta.hot.dispose(async () => {
|
|
38
|
+
await (await started).dispose();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DetailPage } from "../lib/models/page";
|
|
3
|
+
defineOptions({ inheritAttrs: false });
|
|
4
|
+
const { page } = defineProps<{ page: DetailPage }>();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<section class="page">
|
|
9
|
+
<h1>{{ page.title }}</h1>
|
|
10
|
+
<p>{{ page.description }}</p>
|
|
11
|
+
<!-- The entry owns this draft; data-restore-root also restores it after reload. -->
|
|
12
|
+
<div class="draft" data-restore-root>
|
|
13
|
+
<label
|
|
14
|
+
>Detail note<input
|
|
15
|
+
name="note"
|
|
16
|
+
placeholder="Kept while this screen is alive; lost when popped"
|
|
17
|
+
/></label>
|
|
18
|
+
</div>
|
|
19
|
+
</section>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ViewProps } from "@finesoft/front";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import type { HomePage } from "../lib/models/page";
|
|
5
|
+
import { getHomeLocale } from "../lib/locale";
|
|
6
|
+
|
|
7
|
+
defineOptions({ inheritAttrs: false });
|
|
8
|
+
const { page, app } = defineProps<{
|
|
9
|
+
page: HomePage;
|
|
10
|
+
app: ViewProps["app"];
|
|
11
|
+
}>();
|
|
12
|
+
const locale = computed(() => getHomeLocale(app));
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<section class="page">
|
|
17
|
+
<h1>{{ page.title }}</h1>
|
|
18
|
+
<p>{{ page.description }}</p>
|
|
19
|
+
<ul class="feed">
|
|
20
|
+
<li v-for="item in page.items" :key="item.id">
|
|
21
|
+
<button @click="app.perform({ kind: 'flow', url: `/item/${item.id}` })">
|
|
22
|
+
{{ item.title }}
|
|
23
|
+
</button>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
<section class="locale-info" aria-label="Locale">
|
|
27
|
+
<p>
|
|
28
|
+
<strong>{{ locale.label }}:</strong> {{ locale.lang }}
|
|
29
|
+
</p>
|
|
30
|
+
<p>{{ locale.badge }}</p>
|
|
31
|
+
<p>{{ locale.hint }}</p>
|
|
32
|
+
</section>
|
|
33
|
+
</section>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ErrorPage } from "../lib/models/page";
|
|
3
|
+
defineOptions({ inheritAttrs: false });
|
|
4
|
+
const { page } = defineProps<{ page: ErrorPage }>();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<section class="page">
|
|
9
|
+
<h1>{{ page.title }}</h1>
|
|
10
|
+
<p>{{ page.description }}</p>
|
|
11
|
+
<a href="/">← Go Home</a>
|
|
12
|
+
</section>
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NotesPage } from "../lib/models/page";
|
|
3
|
+
defineOptions({ inheritAttrs: false });
|
|
4
|
+
const { page } = defineProps<{ page: NotesPage }>();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<section class="page">
|
|
9
|
+
<h1>{{ page.title }}</h1>
|
|
10
|
+
<p>{{ page.description }}</p>
|
|
11
|
+
<!-- The entry owns this draft; data-restore-root also restores it after reload. -->
|
|
12
|
+
<div class="draft" data-restore-root>
|
|
13
|
+
<label
|
|
14
|
+
>Notes draft<textarea
|
|
15
|
+
name="notes"
|
|
16
|
+
rows="6"
|
|
17
|
+
placeholder="Write a note; switch tabs or reload to restore it"
|
|
18
|
+
></textarea>
|
|
19
|
+
</label>
|
|
20
|
+
</div>
|
|
21
|
+
</section>
|
|
22
|
+
</template>
|
|
@@ -1,50 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createSSRNavigationRender,
|
|
3
|
-
renderIslandsHtml,
|
|
4
|
-
serializeServerData,
|
|
5
|
-
type ResolvedEntry,
|
|
6
|
-
} from "@finesoft/front";
|
|
7
|
-
import { createSSRApp, type Component } from "vue";
|
|
1
|
+
import { createSSRApp } from "vue";
|
|
8
2
|
import { renderToString } from "vue/server-renderer";
|
|
3
|
+
import { createSSRRender } from "@finesoft/front";
|
|
4
|
+
import { app } from "./app-definition";
|
|
9
5
|
import App from "./App.vue";
|
|
10
|
-
import HomeView from "./views/HomeView.vue";
|
|
11
|
-
import DetailView from "./views/DetailView.vue";
|
|
12
|
-
import NotesView from "./views/NotesView.vue";
|
|
13
|
-
import { bootstrap, navigation } from "./bootstrap";
|
|
14
6
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* islands 架构 SSR(方案 C):
|
|
19
|
-
* - chrome(App.vue,header + tabbar)渲进 `<div data-fs-chrome>`。
|
|
20
|
-
* - 可见 island 内容由 `renderIslandsHtml` 渲进 sibling `<main data-fs-outlet>`,客户端按 key 收养水合。
|
|
21
|
-
*
|
|
22
|
-
* chrome 水合 props parity:SSR 必须用与客户端 hydrate 时**相同**的初始 state 渲 App,否则
|
|
23
|
-
* App.vue 的 `v-if="state"`(name label 等)server/client 不一致 → hydration mismatch。
|
|
24
|
-
* 客户端 hydrate 时 state = { snapshot: null, name: "" }(onNavigationReady / session 恢复都在
|
|
25
|
-
* 水合**之后**才填)。controller 仅用于事件处理器(`controller?.`),不影响渲染 DOM,SSR 可省。
|
|
26
|
-
*/
|
|
27
|
-
export const render = createSSRNavigationRender({
|
|
28
|
-
bootstrap,
|
|
29
|
-
getErrorPage(status, message) {
|
|
30
|
-
return { id: "error", pageType: "error", title: `Error ${status}`, description: message };
|
|
31
|
-
},
|
|
32
|
-
async renderApp(page, _framework, snapshot) {
|
|
33
|
-
// 客户端 mount 时 navigation.getSnapshot() 即此 URL 推导 snapshot(tree 一致)→ chrome 水合无失配,
|
|
34
|
-
// 且 nav bar 首屏即被 SSR 渲出。name 仍默认 ""(会话恢复在 mount 后,水合后才生效)。
|
|
35
|
-
const chromeHtml = await renderToString(
|
|
36
|
-
createSSRApp(App, { state: { snapshot, name: "" } }),
|
|
37
|
-
);
|
|
38
|
-
const islandsHtml = await renderIslandsHtml(snapshot, (entry: ResolvedEntry) =>
|
|
39
|
-
renderToString(createSSRApp(VIEWS[entry.intent] ?? HomeView, { page: entry.page })),
|
|
40
|
-
);
|
|
41
|
-
return {
|
|
42
|
-
html: `<div data-fs-chrome>${chromeHtml}</div><main data-fs-outlet>${islandsHtml}</main>`,
|
|
43
|
-
head: `<title>${page.title}</title>`,
|
|
44
|
-
css: "",
|
|
45
|
-
};
|
|
46
|
-
},
|
|
47
|
-
navigation: navigation.toSSRDefinition(),
|
|
7
|
+
export const render = createSSRRender({
|
|
8
|
+
definition: app,
|
|
9
|
+
render: (app) => renderToString(createSSRApp(App, { app })),
|
|
48
10
|
});
|
|
49
|
-
|
|
50
|
-
export { serializeServerData };
|
|
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": "preserve",
|
|
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 vue from "@vitejs/plugin-vue";
|
|
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.ts" },
|
|
10
|
-
}),
|
|
11
|
-
],
|
|
12
|
+
lint: { options: { typeAware: true, typeCheck: true } },
|
|
13
|
+
plugins: lazyPlugins(() => [front, vue()]),
|
|
12
14
|
});
|
|
@@ -1,14 +0,0 @@
|
|
|
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;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { type Framework, defineRoutes, int, route } 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
|
-
route("/products/:id", {
|
|
15
|
-
intentId: "product-detail",
|
|
16
|
-
controller: new ProductDetailController(),
|
|
17
|
-
params: { id: int() },
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { Action } from "@finesoft/front";
|
|
2
|
-
import type { AppPage } from "../lib/models/product";
|
|
3
|
-
import About from "../pages/About";
|
|
4
|
-
import Home from "../pages/Home";
|
|
5
|
-
import NotFound from "../pages/NotFound";
|
|
6
|
-
import ProductDetail from "../pages/ProductDetail";
|
|
7
|
-
import Search from "../pages/Search";
|
|
8
|
-
|
|
9
|
-
interface PageRendererProps {
|
|
10
|
-
page: AppPage;
|
|
11
|
-
onAction?: (action: Action) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** 根据 pageType 分发渲染对应组件 */
|
|
15
|
-
export function PageRenderer({ page, onAction }: PageRendererProps) {
|
|
16
|
-
switch (page.pageType) {
|
|
17
|
-
case "home":
|
|
18
|
-
return <Home page={page} onAction={onAction} />;
|
|
19
|
-
case "product":
|
|
20
|
-
return <ProductDetail page={page} onAction={onAction} />;
|
|
21
|
-
case "search":
|
|
22
|
-
return <Search page={page} onAction={onAction} />;
|
|
23
|
-
case "about":
|
|
24
|
-
return <About page={page} onAction={onAction} />;
|
|
25
|
-
case "error":
|
|
26
|
-
return <NotFound page={page} onAction={onAction} />;
|
|
27
|
-
default:
|
|
28
|
-
return (
|
|
29
|
-
<NotFound
|
|
30
|
-
page={{
|
|
31
|
-
id: "unknown",
|
|
32
|
-
pageType: "error",
|
|
33
|
-
title: "Unknown Page",
|
|
34
|
-
description: "Page not found",
|
|
35
|
-
status: 404,
|
|
36
|
-
}}
|
|
37
|
-
onAction={onAction}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { startBrowserApp, type Action, type BasePage, type Framework } from "@finesoft/front";
|
|
2
|
-
import { createRoot } from "react-dom/client";
|
|
3
|
-
import App from "./App";
|
|
4
|
-
import { bootstrap } from "./bootstrap";
|
|
5
|
-
import type { AppPage } from "./lib/models/product";
|
|
6
|
-
|
|
7
|
-
void startBrowserApp({
|
|
8
|
-
bootstrap,
|
|
9
|
-
mountId: "app",
|
|
10
|
-
mount: (target: HTMLElement, { framework }: { framework: Framework }) => {
|
|
11
|
-
let root: ReturnType<typeof createRoot> | null = null;
|
|
12
|
-
let currentPage: AppPage | null = null;
|
|
13
|
-
|
|
14
|
-
function render(page: AppPage | null, loading: boolean) {
|
|
15
|
-
const handleAction = (action: Action) => {
|
|
16
|
-
void framework.perform(action);
|
|
17
|
-
};
|
|
18
|
-
if (!root) root = createRoot(target);
|
|
19
|
-
root.render(<App page={page} loading={loading} onAction={handleAction} />);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return ({
|
|
23
|
-
page,
|
|
24
|
-
isFirstPage,
|
|
25
|
-
}: {
|
|
26
|
-
page: Promise<BasePage> | BasePage;
|
|
27
|
-
isFirstPage?: boolean;
|
|
28
|
-
}) => {
|
|
29
|
-
if (page instanceof Promise) {
|
|
30
|
-
if (!isFirstPage) render(currentPage, true);
|
|
31
|
-
void page.then((p) => {
|
|
32
|
-
currentPage = p as AppPage;
|
|
33
|
-
render(currentPage, false);
|
|
34
|
-
});
|
|
35
|
-
} else {
|
|
36
|
-
currentPage = page as AppPage;
|
|
37
|
-
render(currentPage, false);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
callbacks: {
|
|
42
|
-
onNavigate(pathname) {
|
|
43
|
-
console.log("[navigate]", pathname);
|
|
44
|
-
},
|
|
45
|
-
onModal(page) {
|
|
46
|
-
console.log("[modal]", page.title);
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createSSRRender, serializeServerData } from "@finesoft/front";
|
|
2
|
-
import { renderToString } from "react-dom/server";
|
|
3
|
-
import App from "./App";
|
|
4
|
-
import { bootstrap } from "./bootstrap";
|
|
5
|
-
import { getErrorPage } from "./lib/controllers/error";
|
|
6
|
-
import type { AppPage } from "./lib/models/product";
|
|
7
|
-
|
|
8
|
-
export { serializeServerData };
|
|
9
|
-
|
|
10
|
-
export const render = createSSRRender({
|
|
11
|
-
bootstrap,
|
|
12
|
-
getErrorPage,
|
|
13
|
-
renderApp(page, _framework) {
|
|
14
|
-
const html = renderToString(<App page={page as AppPage} />);
|
|
15
|
-
return {
|
|
16
|
-
html,
|
|
17
|
-
head: `<title>${page.title}</title><meta name="description" content="${
|
|
18
|
-
page.description ?? ""
|
|
19
|
-
}">`,
|
|
20
|
-
css: "",
|
|
21
|
-
};
|
|
22
|
-
},
|
|
23
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Framework, defineNavigation, defineRoutes, leaf, stack, tabs } from "@finesoft/front";
|
|
2
|
-
import { DetailController } from "./lib/controllers/detail";
|
|
3
|
-
import { HomeController } from "./lib/controllers/home";
|
|
4
|
-
import { NotesController } from "./lib/controllers/notes";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 同一份 bootstrap 在浏览器与服务器都会执行:注册 controllers + 路由。
|
|
8
|
-
* 路由让 URL 能 resolve(SSR 首屏 / codec / 扁平回退都需要)。
|
|
9
|
-
*/
|
|
10
|
-
export function bootstrap(framework: Framework): void {
|
|
11
|
-
defineRoutes(framework, [
|
|
12
|
-
{ path: "/", intentId: "home", controller: new HomeController() },
|
|
13
|
-
{ path: "/item/:id", intentId: "detail", controller: new DetailController() },
|
|
14
|
-
{ path: "/notes", intentId: "notes", controller: new NotesController() },
|
|
15
|
-
]);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 结构化导航:一个 TabView(Home / Notes),每个 tab 是一个 NavigationStack。
|
|
20
|
-
* - Home 栈可 push 到 detail(栈深 +1),返回 pop(栈深 -1)。
|
|
21
|
-
* - 切 tab 保活另一分支的栈深与作用域状态(对标 SwiftUI TabView)。
|
|
22
|
-
*
|
|
23
|
-
* `initial` 用 **URL→树 工厂**(而非静态树):这样深链 / 刷新 `/item/2` 能重建出
|
|
24
|
-
* 「Home 栈压着 detail」的结构,active-leaf codec 也把它回写成 `/item/2`,刷新不再跳回 `/`。
|
|
25
|
-
* (静态树会忽略 URL、永远从 home 起,刷新被改回 `/`。)
|
|
26
|
-
*/
|
|
27
|
-
export const navigation = defineNavigation({
|
|
28
|
-
initial: (url) => {
|
|
29
|
-
const path = url.split("?")[0].split("#")[0];
|
|
30
|
-
const detail = /^\/item\/(.+)$/.exec(path);
|
|
31
|
-
const home = detail
|
|
32
|
-
? stack([leaf("home"), leaf("detail", { id: detail[1] })])
|
|
33
|
-
: stack(leaf("home"));
|
|
34
|
-
return tabs({
|
|
35
|
-
active: path === "/notes" ? "notes" : "home",
|
|
36
|
-
branches: { home, notes: stack(leaf("notes")) },
|
|
37
|
-
});
|
|
38
|
-
},
|
|
39
|
-
});
|