create-fullstack-scaffold 0.5.1 → 0.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fullstack-scaffold": "dist/cli/index.js"
@@ -14,6 +14,8 @@ export default defineConfig({
14
14
  out: './drizzle',
15
15
  dialect: 'sqlite',
16
16
  dbCredentials: {
17
+ // 与运行时保持同一库文件:drizzle-kit 在无 NODE_ENV 下执行时
18
+ // config.sqlitePath 会解析为 development.db,这里不能写死 app.db
17
19
  url: config.sqlitePath || './data/app.db',
18
20
  },
19
21
  })
@@ -114,6 +114,7 @@ export default tseslint.config(
114
114
  },
115
115
  {
116
116
  files: ['src/server/**/*.ts'],
117
+ ignores: ['src/server/route-registry.ts'],
117
118
  rules: {
118
119
  'no-console': 'error',
119
120
  'local-rules/require-hono-chain-syntax': 'error',
@@ -128,7 +129,9 @@ export default tseslint.config(
128
129
  'local-rules/no-new-old-service-naming': 'error',
129
130
  'local-rules/no-cross-module-service-import': 'error',
130
131
  'local-rules/route-location': 'error',
131
- 'local-rules/limit-type-complexity': ['warn', { maxRouteChainLength: 15 }],
132
+ // TS2589 防线收紧:类型出口已由 no-merged-api-type-export 封死,
133
+ // 链长阈值从 warn/15 收紧到 error/5(超出即架构回归信号)
134
+ 'local-rules/limit-type-complexity': ['error', { maxRouteChainLength: 5 }],
132
135
  'local-rules/no-merged-api-type-export': 'error',
133
136
  },
134
137
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biomimic-todo-app",
3
3
  "private": true,
4
- "version": "0.5.1",
4
+ "version": "0.5.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "biomimic": "./dist/cli/index.js"
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @framework-baseline 1a361bf1bb0e5343
2
+ * @framework-baseline 00b6a72a77e03864
3
3
  */
4
4
 
5
5
  /**
@@ -17,6 +17,7 @@ import { StaticRouter } from 'react-router-dom'
17
17
  import { HelmetProvider } from 'react-helmet-async'
18
18
 
19
19
  import { AppRoutes } from './AppRoutes'
20
+ import { getSsrPage } from './ssr-pages'
20
21
  import {
21
22
  snapshotEntryStores,
22
23
  seedEntryStores,
@@ -50,17 +51,25 @@ export function renderSSR(pathname: string, data: SSRData): SSRRenderResult {
50
51
  const helmetContext: Record<string, unknown> = {}
51
52
 
52
53
  try {
53
- const html = renderToString(
54
- React.createElement(
55
- HelmetProvider,
56
- { context: helmetContext },
57
- React.createElement(
58
- StaticRouter,
59
- { location: pathname },
60
- React.createElement(AppRoutes, { presetId: preset })
54
+ // 页面级 SSR:ISR 路由有静态同构组件(ssr-pages)——渲染真实页面
55
+ // 内容而非 lazy Loading 壳(SEO 核心);其余路由回退 AppRoutes 壳。
56
+ const SsrPage = getSsrPage(pathname)
57
+ const body = SsrPage
58
+ ? React.createElement(
59
+ HelmetProvider,
60
+ { context: helmetContext },
61
+ React.createElement(StaticRouter, { location: pathname }, React.createElement(SsrPage))
61
62
  )
62
- )
63
- )
63
+ : React.createElement(
64
+ HelmetProvider,
65
+ { context: helmetContext },
66
+ React.createElement(
67
+ StaticRouter,
68
+ { location: pathname },
69
+ React.createElement(AppRoutes, { presetId: preset })
70
+ )
71
+ )
72
+ const html = renderToString(body)
64
73
 
65
74
  // 5. Extract helmet data
66
75
  const helmet = (helmetContext as { helmet?: Record<string, unknown> }).helmet || {}
@@ -67,11 +67,24 @@ const RootApp = () => {
67
67
  )
68
68
  }
69
69
 
70
- ReactDOM.createRoot(document.getElementById('root')!).render(
71
- <React.StrictMode>
72
- <RootApp />
73
- </React.StrictMode>
74
- )
70
+ // 水合:SSR(ISR 管线)已输出 #root 内的 HTML——hydrateRoot 复用而非
71
+ // 重画(SEO/首屏收益成立的前提)。无 SSR 的普通访问 fallback createRoot。
72
+ const rootEl = document.getElementById('root')!
73
+ const hasSsrMarkup = rootEl.hasChildNodes()
74
+ if (hasSsrMarkup) {
75
+ ReactDOM.hydrateRoot(
76
+ rootEl,
77
+ <React.StrictMode>
78
+ <RootApp />
79
+ </React.StrictMode>
80
+ )
81
+ } else {
82
+ ReactDOM.createRoot(rootEl).render(
83
+ <React.StrictMode>
84
+ <RootApp />
85
+ </React.StrictMode>
86
+ )
87
+ }
75
88
 
76
89
  if (typeof window !== 'undefined') {
77
90
  requestAnimationFrame(() => {
@@ -4,10 +4,21 @@ import { Helmet } from 'react-helmet-async'
4
4
  import type { Content } from '@shared/modules/content'
5
5
  import { apiClient } from '@client/services/apiClient'
6
6
 
7
+ // SSR 首帧数据:ISR 服务端写入 __SSR_DATA__.content(详情页按 id 匹配)
8
+ function ssrInitialContent(id?: string): Content | null {
9
+ try {
10
+ const d = (window as unknown as { __SSR_DATA__?: { content?: Content | null } }).__SSR_DATA__
11
+ const c = d?.content ?? null
12
+ return c && (!id || c.id === id) ? c : null
13
+ } catch {
14
+ return null
15
+ }
16
+ }
17
+
7
18
  export const ContentDetailPage: React.FC = () => {
8
19
  const { id } = useParams<{ id: string }>()
9
- const [content, setContent] = useState<Content | null>(null)
10
- const [loading, setLoading] = useState(true)
20
+ const [content, setContent] = useState<Content | null>(() => ssrInitialContent(id))
21
+ const [loading, setLoading] = useState(() => ssrInitialContent(id) === null)
11
22
  const [error, setError] = useState<string | null>(null)
12
23
 
13
24
  const fetchContent = useCallback(async (contentId: string) => {
@@ -13,9 +13,20 @@ const CATEGORIES: { value: ContentCategory | ''; label: string }[] = [
13
13
  { value: 'policy', label: '政策' },
14
14
  ]
15
15
 
16
+ // SSR 首帧数据:ISR 管线在服务端把数据写进 __SSR_DATA__,客户端首帧
17
+ // 直接用它渲染(水合一致、爬虫可见真实内容),effect 再刷新
18
+ function ssrInitialContents(): Content[] {
19
+ try {
20
+ const d = (window as unknown as { __SSR_DATA__?: { contents?: Content[] } }).__SSR_DATA__
21
+ return d?.contents ?? []
22
+ } catch {
23
+ return []
24
+ }
25
+ }
26
+
16
27
  export const ContentListPage: React.FC = () => {
17
- const [contents, setContents] = useState<Content[]>([])
18
- const [loading, setLoading] = useState(true)
28
+ const [contents, setContents] = useState<Content[]>(ssrInitialContents)
29
+ const [loading, setLoading] = useState(ssrInitialContents().length === 0)
19
30
  const [error, setError] = useState<string | null>(null)
20
31
  const [category, setCategory] = useState<ContentCategory | ''>('')
21
32
  const [search, setSearch] = useState('')
@@ -0,0 +1,44 @@
1
+ /**
2
+ * SSR 静态页面注册表(替代 preset-ui-config 的 lazy 组件用于服务端渲染)。
3
+ *
4
+ * 为什么需要它:renderToString 是同步的,解析不了 React.lazy 的动态
5
+ * import——直接用 preset-ui-config 会让 SSR body 永远是 Suspense 的
6
+ * Loading 壳。这里用静态 import 提供同构组件映射,页面内容在服务端
7
+ * 真正渲染进 HTML(ISR/SEO 的核心价值所在)。
8
+ *
9
+ * 客户端不走这里(CSR 仍用 lazy 代码分割),水合由 entry-server 渲染的
10
+ * HTML + main.tsx 的 hydrateRoot 完成——两侧组件树来自同一源码,静态
11
+ * 导入版多打一份包只进 server bundle(tsup 的 CF 构建),不影响 client
12
+ * chunk 体积。
13
+ */
14
+
15
+ import type { ComponentType } from 'react'
16
+
17
+ // 按需静态导入 ISR 场景的页面(内容型、需要 SEO 的页面)
18
+ import { TodoPage } from './pages/TodoPage'
19
+ import { ContentListPage } from './pages/ContentListPage'
20
+ import { ContentDetailPage } from './pages/ContentDetailPage'
21
+ import { TopicsPage } from './pages/TopicsPage'
22
+
23
+ export type SsrPageComponent = ComponentType
24
+
25
+ /**
26
+ * 路由 → 同构组件映射。
27
+ * 未列出的路由(管理后台、登录页等)SSR 输出布局壳——它们本来就不
28
+ * 需要 SEO(登录后才可见),客户端 lazy 照常工作。
29
+ */
30
+ export const SSR_PAGES: Record<string, SsrPageComponent> = {
31
+ '/todos': TodoPage,
32
+ '/content': ContentListPage,
33
+ '/content/:id': ContentDetailPage,
34
+ '/topics': TopicsPage,
35
+ }
36
+
37
+ /** ISR 路径是否有关联的静态组件(决定 body 渲染还是仅 meta) */
38
+ export function getSsrPage(pathname: string): SsrPageComponent | null {
39
+ if (SSR_PAGES[pathname]) return SSR_PAGES[pathname]
40
+ // 详情页模式 /content/123
41
+ const m = pathname.match(/^\/content\/[^/]+$/)
42
+ if (m) return SSR_PAGES['/content/:id']
43
+ return null
44
+ }
@@ -12,6 +12,9 @@ import type { Todo } from '@shared/schemas'
12
12
 
13
13
  export interface SSRData {
14
14
  todos?: Todo[]
15
+ /** content 模块 ISR 数据(列表/详情首帧,页面从 __SSR_DATA__ 读取) */
16
+ contents?: unknown[]
17
+ content?: Record<string, unknown> | null
15
18
  [key: string]: unknown
16
19
  }
17
20
 
@@ -66,9 +66,7 @@ export function getAppConfig(): AppConfig {
66
66
  database: {
67
67
  driver: isCloudflare ? 'd1' : dbDriver,
68
68
  sqlitePath:
69
- typeof process !== 'undefined'
70
- ? process.env.SQLITE_PATH || `./data/${nodeEnv}.db`
71
- : undefined,
69
+ typeof process !== 'undefined' ? process.env.SQLITE_PATH || './data/app.db' : undefined,
72
70
  mysqlHost: typeof process !== 'undefined' ? process.env.MYSQL_HOST || 'localhost' : undefined,
73
71
  mysqlPort:
74
72
  typeof process !== 'undefined' ? parseInt(process.env.MYSQL_PORT || '3306', 10) : undefined,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @framework-baseline cec621fae360ba2b
2
+ * @framework-baseline 108b720f9d939fcd
3
3
  *
4
4
  *
5
5
  * @framework-modify
@@ -32,6 +32,8 @@ export interface ISRRenderOptions {
32
32
  body?: string
33
33
  /** Meta tags */
34
34
  meta: ISRPageMeta
35
+ /** ISR 抓取数据:序列化进 window.__SSR_DATA__ 供客户端首帧渲染(水合一致) */
36
+ data?: unknown
35
37
  }
36
38
 
37
39
  /**
@@ -39,7 +41,7 @@ export interface ISRRenderOptions {
39
41
  * If template is null, generates a minimal standalone HTML.
40
42
  */
41
43
  export function renderISRPage(opts: ISRRenderOptions): string {
42
- const { template, body = '', meta } = opts
44
+ const { template, body = '', meta, data } = opts
43
45
  const safeTitle = escapeHtml(meta.title)
44
46
  const safeDesc = escapeHtml(meta.description)
45
47
 
@@ -54,6 +56,10 @@ export function renderISRPage(opts: ISRRenderOptions): string {
54
56
  ` <meta name="description" content="${safeDesc}" />\n <meta property="og:title" content="${safeTitle}" />\n <meta property="og:description" content="${safeDesc}" />\n <meta name="generator" content="ISR-SSG" />\n </head>`
55
57
  )
56
58
  html = html.replace('<div id="root"></div>', `<div id="root">${body}</div>`)
59
+ if (data !== undefined) {
60
+ const json = JSON.stringify(data).replace(/</g, '\\u003c')
61
+ html = html.replace('</body>', `<script>window.__SSR_DATA__=${json};</script></body>`)
62
+ }
57
63
  return html
58
64
  }
59
65
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @framework-baseline e350401421193896
2
+ * @framework-baseline e6d62c9969957f22
3
3
  * @framework-modify
4
4
  * @reason 模块化 ISR 改造 + SSR 渲染:调用 renderSSR 生成 React body,注入到 ISR 模板
5
5
  * @impact CF 入口集成 React SSR,ISR 同时负责 SEO meta 标签和 body 渲染
@@ -212,7 +212,7 @@ async function renderISRForRoute(
212
212
  // Fallback: empty body, SPA will hydrate
213
213
  }
214
214
 
215
- return renderISRPage({ template: cachedTemplate, body, meta })
215
+ return renderISRPage({ template: cachedTemplate, body, meta, data })
216
216
  }
217
217
 
218
218
  export { isrCache }
@@ -202,7 +202,9 @@ export async function startServer() {
202
202
  await initializeDatabase()
203
203
  bootstrapLog.info({}, 'Database ready')
204
204
  } catch (err) {
205
- bootstrapLog.error({ err }, 'Database initialization failed')
205
+ // pino 走异步 thread-stream transport,process.exit 前不 flush——
206
+ // 生产致命错误必须同步 console.error,否则静默死(exit 1 零输出)
207
+ console.error('Database initialization failed:', err)
206
208
  process.exit(1)
207
209
  }
208
210