create-fullstack-scaffold 0.4.24 → 0.5.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 (146) hide show
  1. package/dist/cli/index.js +439 -15
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +10 -13
  4. package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
  5. package/template/eslint-rules/no-cross-module-service-import.js +4 -0
  6. package/template/eslint-rules/no-merged-api-type-export.js +190 -0
  7. package/template/eslint.config.js +3 -0
  8. package/template/package.json +9 -6
  9. package/template/scripts/sync-agent-hooks.mjs +189 -0
  10. package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
  11. package/template/src/admin/pages/ContentPage.tsx +1 -1
  12. package/template/src/admin/pages/DisputesPage.tsx +1 -1
  13. package/template/src/admin/pages/OrdersPage.tsx +1 -1
  14. package/template/src/admin/pages/TicketsPage.tsx +1 -1
  15. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
  16. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
  17. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
  18. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
  19. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
  20. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
  21. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
  22. package/template/src/admin/services/apiClient.ts +2 -3
  23. package/template/src/cli/modules/content/index.ts +3 -3
  24. package/template/src/cli/modules/dispute/index.ts +3 -3
  25. package/template/src/cli/modules/ticket/index.ts +3 -3
  26. package/template/src/cli/modules/todo/index.ts +1 -1
  27. package/template/src/cli/rpc/client.ts +3 -4
  28. package/template/src/cli/rpc/index.ts +1 -1
  29. package/template/src/client/App.tsx +3 -39
  30. package/template/src/client/AppRoutes.tsx +53 -0
  31. package/template/src/client/entry-server.tsx +75 -0
  32. package/template/src/client/main.tsx +3 -1
  33. package/template/src/client/pages/SearchPage.tsx +1 -1
  34. package/template/src/client/services/apiClient.ts +12 -4
  35. package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
  36. package/template/src/client/stores/entry-stores.ts +36 -0
  37. package/template/src/client/stores/notificationStore.ts +4 -4
  38. package/template/src/client/stores/todoStore.ts +2 -2
  39. package/template/src/merchant/pages/DisputesPage.tsx +3 -3
  40. package/template/src/merchant/pages/OrdersPage.tsx +1 -1
  41. package/template/src/merchant/pages/ProductsPage.tsx +1 -1
  42. package/template/src/merchant/pages/SettingsPage.tsx +1 -1
  43. package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
  44. package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
  45. package/template/src/server/app.ts +6 -4
  46. package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
  47. package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
  48. package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
  49. package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
  50. package/template/src/server/core/isr-cache.ts +6 -12
  51. package/template/src/server/core/isr-invalidation.ts +6 -12
  52. package/template/src/server/core/isr-registry.ts +104 -0
  53. package/template/src/server/core/isr-renderer.ts +75 -0
  54. package/template/src/server/db/schema/contents.ts +28 -20
  55. package/template/src/server/db/schema/disputes.ts +30 -22
  56. package/template/src/server/db/schema/notifications.ts +20 -14
  57. package/template/src/server/db/schema/orders.ts +23 -16
  58. package/template/src/server/db/schema/plugins.ts +56 -38
  59. package/template/src/server/db/schema/products.ts +24 -17
  60. package/template/src/server/db/schema/tickets.ts +44 -31
  61. package/template/src/server/db/schema/todos.ts +26 -18
  62. package/template/src/server/entries/cloudflare.ts +82 -19
  63. package/template/src/server/entries/node.ts +0 -2
  64. package/template/src/server/index.ts +0 -1
  65. package/template/src/server/isr-modules.ts +10 -0
  66. package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
  67. package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
  68. package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
  69. package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
  70. package/template/src/server/module-admin/services/admin-service.ts +57 -13
  71. package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
  72. package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
  73. package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
  74. package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
  75. package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
  76. package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
  77. package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
  78. package/template/src/server/module-content/isr.ts +63 -0
  79. package/template/src/server/module-content/routes/content-routes.ts +10 -10
  80. package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
  81. package/template/src/server/module-content/routes/topics-routes.ts +3 -0
  82. package/template/src/server/module-content/services/content-service.ts +23 -10
  83. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
  84. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
  85. package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
  86. package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
  87. package/template/src/server/module-file/routes/file-routes.ts +3 -0
  88. package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
  89. package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
  90. package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
  91. package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
  92. package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
  93. package/template/src/server/module-order/routes/cart-routes.ts +3 -0
  94. package/template/src/server/module-order/routes/order-routes.ts +9 -4
  95. package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
  96. package/template/src/server/module-order/services/order-service.ts +25 -13
  97. package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
  98. package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
  99. package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
  100. package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
  101. package/template/src/server/module-permission/routes/role-routes.ts +3 -0
  102. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
  103. package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
  104. package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
  105. package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
  106. package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
  107. package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
  108. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
  109. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
  110. package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
  111. package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
  112. package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
  113. package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
  114. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
  115. package/template/src/server/module-todos/isr.ts +41 -0
  116. package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
  117. package/template/src/server/module-todos/services/todo-service.ts +31 -10
  118. package/template/src/server/route-registry.ts +0 -4
  119. package/template/src/server/rpc-merge.ts +27 -0
  120. package/template/src/server/rpc-surface.ts +117 -0
  121. package/template/src/server/rpc-type-canary.ts +80 -0
  122. package/template/src/server/test-utils/test-client.ts +7 -9
  123. package/template/src/server/test-utils/test-isr-helper.ts +140 -0
  124. package/template/src/shared/modules/content/schemas.ts +14 -0
  125. package/template/src/shared/modules/dispute/schemas.ts +14 -0
  126. package/template/src/shared/modules/order/schemas.ts +9 -1
  127. package/template/src/shared/modules/ticket/schemas.ts +14 -0
  128. package/template/src/shared/modules/todos/index.ts +4 -0
  129. package/template/src/shared/modules/todos/schemas.ts +14 -0
  130. package/template/src/shared/schemas/index.ts +18 -0
  131. package/template/tsup.config.ts +48 -1
  132. package/template/vitest.config.ts +19 -3
  133. package/template/vitest.setup.ts +68 -5
  134. package/template/drizzle/0000_rainy_boomer.sql +0 -101
  135. package/template/drizzle/0001_add_todo_attachments.sql +0 -12
  136. package/template/drizzle/0002_chilly_magneto.sql +0 -89
  137. package/template/drizzle/0003_ambiguous_magdalene.sql +0 -100
  138. package/template/drizzle/0004_add_merchants_products.sql +0 -30
  139. package/template/drizzle/meta/0000_snapshot.json +0 -652
  140. package/template/drizzle/meta/0001_snapshot.json +0 -735
  141. package/template/drizzle/meta/0002_snapshot.json +0 -1198
  142. package/template/drizzle/meta/0003_snapshot.json +0 -1837
  143. package/template/drizzle/meta/_journal.json +0 -41
  144. package/template/patches/typescript+5.9.3.patch +0 -24
  145. package/template/pnpm-lock.yaml +0 -7137
  146. /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @framework-baseline 779069c2d2bb618c
3
+ */
4
+
5
+ /**
6
+ * TS2589 哨兵(编译期早期预警)
7
+ *
8
+ * 历史:route-registry 曾把 N 个模块链式 merge 成巨型类型导出,hc<T> 实例化时
9
+ * 深度展开整条链触发 TS2589(Type instantiation is excessively deep),
10
+ * 甚至一度 patch 了 TypeScript 编译器(已被 no-ts-patch validator 禁止)。
11
+ * 现行架构:每个模块导出窄类型(深度 = 1),rpc-surface 按模块实例化。
12
+ *
13
+ * 本文件的职责:对每个模块的窄类型做一次真实的 hc 值级实例化。
14
+ * 若将来有人把某个模块类型重新指到 merge 链上(例如让 XxxApiType 依赖
15
+ * route-registry 的合并结果),深度爆炸会先在这里报错——错误落在哨兵文件、
16
+ * 语义明确,而不是散落在业务代码里让推导"莫名其妙地失败"。
17
+ *
18
+ * 配套防线:
19
+ * - eslint 规则 no-merged-api-type-export(禁止导出 merge 类型,形态级拦截)
20
+ * - no-ts-patch validator(禁止放宽编译器保险丝)
21
+ * - CI template-checks 的 tsc 耗时预算(拦截"不报错但变慢"的慢性回退)
22
+ */
23
+
24
+ import { hc } from 'hono/client'
25
+ import type { AuthApiType } from './module-auth/routes/auth-routes'
26
+ import type { ProfileApiType } from './module-auth/routes/profile-routes'
27
+ import type { ChatApiType } from './module-chat/routes/chat-routes'
28
+ import type { NotificationsApiType } from './module-notifications/routes/notification-routes'
29
+ import type { TodosApiType } from './module-todos/routes/todos-routes'
30
+ import type { PluginsApiType } from './module-plugin/routes/plugin-routes'
31
+ import type { PluginAdminApiType } from './module-plugin/routes/plugin-admin-routes'
32
+ import type { PublicContentApiType } from './module-content/routes/public-content-routes'
33
+ import type { ContentsApiType } from './module-content/routes/content-routes'
34
+ import type { TopicsApiType } from './module-content/routes/topics-routes'
35
+ import type { CartApiType } from './module-order/routes/cart-routes'
36
+ import type { OrdersApiType } from './module-order/routes/order-routes'
37
+ import type { OrdersMockApiType } from './module-order/routes/orders-mock-routes'
38
+ import type { MerchantApiType } from './module-merchant/routes/merchant-routes'
39
+ import type { TicketsApiType } from './module-ticket/routes/ticket-routes'
40
+ import type { DisputesApiType } from './module-dispute/routes/dispute-routes'
41
+ import type { CaptchaApiType } from './module-captcha/routes/captcha-routes'
42
+ import type { PermissionsApiType } from './module-permission/routes/permission-routes'
43
+ import type { RolesApiType } from './module-permission/routes/role-routes'
44
+ import type { AuditLogsApiType } from './module-permission/routes/audit-log-routes'
45
+ import type { AdminRoutesApiType } from './module-admin/routes/admin-routes'
46
+ import type { DashboardApiType } from './module-admin/routes/dashboard-routes'
47
+ import type { TenantApiType } from './module-tenant/routes/tenant-routes'
48
+ import type { FilesApiType } from './module-file/routes/file-routes'
49
+ import { createApiFacade } from './rpc-surface'
50
+
51
+ const api = '/api-canary'
52
+
53
+ // 每个模块一次值级实例化:深度爆炸在此处定点报错(TS2589)
54
+ export const canaryAuth = hc<AuthApiType>(api)
55
+ export const canaryProfile = hc<ProfileApiType>(api)
56
+ export const canaryChat = hc<ChatApiType>(api)
57
+ export const canaryNotifications = hc<NotificationsApiType>(api)
58
+ export const canaryTodos = hc<TodosApiType>(api)
59
+ export const canaryPlugins = hc<PluginsApiType>(api)
60
+ export const canaryPluginAdmin = hc<PluginAdminApiType>(api)
61
+ export const canaryPublicContent = hc<PublicContentApiType>(api)
62
+ export const canaryContents = hc<ContentsApiType>(api)
63
+ export const canaryTopics = hc<TopicsApiType>(api)
64
+ export const canaryCart = hc<CartApiType>(api)
65
+ export const canaryOrders = hc<OrdersApiType>(api)
66
+ export const canaryOrdersMock = hc<OrdersMockApiType>(api)
67
+ export const canaryMerchant = hc<MerchantApiType>(api)
68
+ export const canaryTickets = hc<TicketsApiType>(api)
69
+ export const canaryDisputes = hc<DisputesApiType>(api)
70
+ export const canaryCaptcha = hc<CaptchaApiType>(api)
71
+ export const canaryPermissions = hc<PermissionsApiType>(api)
72
+ export const canaryRoles = hc<RolesApiType>(api)
73
+ export const canaryAuditLogs = hc<AuditLogsApiType>(api)
74
+ export const canaryAdminRoutes = hc<AdminRoutesApiType>(api)
75
+ export const canaryDashboard = hc<DashboardApiType>(api)
76
+ export const canaryTenant = hc<TenantApiType>(api)
77
+ export const canaryFiles = hc<FilesApiType>(api)
78
+
79
+ // 门面整体形状哨兵:所有段必须在门面上可访问
80
+ export const canaryFacade = createApiFacade('/api-canary')
@@ -6,8 +6,7 @@
6
6
  * @impact 测试客户端现在支持自定义 headers,用于认证测试
7
7
  */
8
8
 
9
- import { hc } from 'hono/client'
10
- import type { AppType } from '@server/index'
9
+ import { createApiFacade, type ApiFacade } from '@server/rpc-surface'
11
10
  import { createApp } from '@server/app'
12
11
  import { SSEClientImpl } from '@shared/core/sse-client'
13
12
  import { setRuntimeAdapter } from '@server/core/runtime'
@@ -16,12 +15,11 @@ import { getNodeRuntimeAdapter } from '@server/core/runtime-node'
16
15
  setRuntimeAdapter(getNodeRuntimeAdapter())
17
16
 
18
17
  /**
19
- * 测试客户端类型
20
- *
21
- * 注意:TypeScript 在推导 Hono Client 类型时可能触发 TS2589,
22
- * 该警告不影响运行时行为,测试客户端可正常工作。
18
+ * 测试客户端类型 —— 按模块拆分的门面(见 rpc-surface.ts)。
19
+ * 旧的 `ReturnType<typeof hc<AppType>>>` 会深度实例化整个 merge 链并触发
20
+ * TS2589,已被 eslint 规则 no-merged-api-type-export 禁止。
23
21
  */
24
- export type TestClient = ReturnType<typeof hc<AppType>>
22
+ export type TestClient = ApiFacade
25
23
 
26
24
  export interface TestClientOptions {
27
25
  webSocket?: (url: string | URL) => WebSocket
@@ -44,13 +42,13 @@ export function createTestClient(baseUrl?: string, options?: TestClientOptions)
44
42
  : (url: string | URL) => new SSEClientImpl(url, defaultHeaders)
45
43
 
46
44
  if (baseUrl) {
47
- return hc<AppType>(baseUrl, {
45
+ return createApiFacade(baseUrl, {
48
46
  headers: defaultHeaders,
49
47
  webSocket: options?.webSocket ? (url: string | URL) => options.webSocket!(url) : undefined,
50
48
  sse: sseFactory as (url: string) => unknown,
51
49
  })
52
50
  }
53
- return hc<AppType>('http://localhost', {
51
+ return createApiFacade('http://localhost', {
54
52
  fetch: (input: RequestInfo | URL, init?: RequestInit) => {
55
53
  const request = new Request(input, init)
56
54
  Object.entries(defaultHeaders).forEach(([key, value]) => {
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @framework-baseline 2c3eaa60a64308e8
3
+ *
4
+ *
5
+ * @framework-modify
6
+ * @reason prettier 格式化与注释结构整理;MockContent 字段收窄为字面量联合
7
+ * @impact 框架文件维护性修改,行为见测试
8
+ */
9
+
10
+ /**
11
+ * ISR test utilities — mock DB, mock service responses, time manipulation.
12
+ */
13
+
14
+ import { createISRCache } from '@server/core/isr-cache'
15
+ import { createISRRegistry, type ISRRouteEntry } from '@server/core/isr-registry'
16
+
17
+ export interface MockTodo {
18
+ id: number
19
+ title: string
20
+ description?: string
21
+ status: 'pending' | 'in_progress' | 'completed'
22
+ createdAt: string
23
+ updatedAt: string
24
+ }
25
+
26
+ export interface MockContent {
27
+ id: string
28
+ title: string
29
+ content: string
30
+ category: 'article' | 'announcement' | 'tutorial' | 'news' | 'policy'
31
+ status: 'draft' | 'published' | 'archived'
32
+ author: string
33
+ tags: string[]
34
+ viewCount: number
35
+ likeCount: number
36
+ createdAt: string
37
+ updatedAt: string
38
+ publishedAt?: string
39
+ }
40
+
41
+ export const mockTodos: MockTodo[] = [
42
+ {
43
+ id: 1,
44
+ title: '学习 ISR',
45
+ status: 'completed',
46
+ createdAt: '2024-01-01T00:00:00.000Z',
47
+ updatedAt: '2024-01-02T00:00:00.000Z',
48
+ },
49
+ {
50
+ id: 2,
51
+ title: '部署 CF',
52
+ status: 'completed',
53
+ createdAt: '2024-01-03T00:00:00.000Z',
54
+ updatedAt: '2024-01-04T00:00:00.000Z',
55
+ },
56
+ {
57
+ id: 3,
58
+ title: '写测试',
59
+ status: 'in_progress',
60
+ createdAt: '2024-01-05T00:00:00.000Z',
61
+ updatedAt: '2024-01-06T00:00:00.000Z',
62
+ },
63
+ {
64
+ id: 4,
65
+ title: '优化 SEO',
66
+ status: 'pending',
67
+ createdAt: '2024-01-07T00:00:00.000Z',
68
+ updatedAt: '2024-01-08T00:00:00.000Z',
69
+ },
70
+ ]
71
+
72
+ export const mockContents: MockContent[] = [
73
+ {
74
+ id: 'content-1',
75
+ title: 'ISR 完全指南',
76
+ content: 'ISR 是一种页面渲染策略...',
77
+ category: 'article',
78
+ status: 'published',
79
+ author: '技术团队',
80
+ tags: ['ISR', 'Cloudflare'],
81
+ viewCount: 128,
82
+ likeCount: 32,
83
+ createdAt: '2024-01-01T00:00:00.000Z',
84
+ updatedAt: '2024-01-02T00:00:00.000Z',
85
+ publishedAt: '2024-01-02T00:00:00.000Z',
86
+ },
87
+ {
88
+ id: 'content-2',
89
+ title: 'CF Workers 最佳实践',
90
+ content: '本文总结 CF Workers 部署经验...',
91
+ category: 'tutorial',
92
+ status: 'published',
93
+ author: '运维团队',
94
+ tags: ['Cloudflare', 'DevOps'],
95
+ viewCount: 256,
96
+ likeCount: 48,
97
+ createdAt: '2024-01-03T00:00:00.000Z',
98
+ updatedAt: '2024-01-04T00:00:00.000Z',
99
+ publishedAt: '2024-01-04T00:00:00.000Z',
100
+ },
101
+ ]
102
+
103
+ /**
104
+ * Create a test ISR setup with isolated registry and cache.
105
+ */
106
+ export function createTestISR() {
107
+ const registry = createISRRegistry()
108
+ const cache = createISRCache({ maxAge: 1, staleWhileRevalidate: 2 })
109
+
110
+ return { registry, cache }
111
+ }
112
+
113
+ /**
114
+ * Create mock ISR route entries for testing.
115
+ */
116
+ export function createMockTodoEntries(todos: MockTodo[] = mockTodos): ISRRouteEntry[] {
117
+ return [
118
+ {
119
+ module: 'todos',
120
+ match: '/todos',
121
+ fetch: async () => ({ todos }),
122
+ meta: () => ({ title: 'Todo List', description: 'Todos page' }),
123
+ },
124
+ ]
125
+ }
126
+
127
+ /**
128
+ * Advance virtual time (for testing cache expiry without real waits).
129
+ * Returns a promise that resolves after the specified ms.
130
+ */
131
+ export async function advanceTime(ms: number): Promise<void> {
132
+ await new Promise(resolve => setTimeout(resolve, ms))
133
+ }
134
+
135
+ /**
136
+ * Extract text content from HTML tags.
137
+ */
138
+ export function extractFromHTML(html: string, pattern: RegExp): string[] {
139
+ return html.match(pattern) ?? []
140
+ }
@@ -41,6 +41,18 @@ export const UpdateContentSchema = z.object({
41
41
 
42
42
  export const ContentListSchema = z.array(ContentSchema)
43
43
 
44
+ export const ContentListResponseSchema = z.object({
45
+ contents: z.array(ContentSchema),
46
+ total: z.number(),
47
+ page: z.number(),
48
+ limit: z.number(),
49
+ })
50
+
51
+ export const ContentListQuerySchema = z.object({
52
+ page: z.coerce.number().int().positive().default(1),
53
+ limit: z.coerce.number().int().positive().max(100).default(20),
54
+ })
55
+
44
56
  export const ContentDeleteResultSchema = z.object({
45
57
  message: z.string(),
46
58
  })
@@ -50,4 +62,6 @@ export type ContentStatus = z.infer<typeof ContentStatusSchema>
50
62
  export type Content = z.infer<typeof ContentSchema>
51
63
  export type CreateContentInput = z.infer<typeof CreateContentSchema>
52
64
  export type UpdateContentInput = z.infer<typeof UpdateContentSchema>
65
+ export type ContentListResponse = z.infer<typeof ContentListResponseSchema>
66
+ export type ContentListQuery = z.infer<typeof ContentListQuerySchema>
53
67
  export type ContentDeleteResult = z.infer<typeof ContentDeleteResultSchema>
@@ -49,6 +49,18 @@ export const ResolveDisputeSchema = z.object({
49
49
 
50
50
  export const DisputeListSchema = z.array(DisputeSchema)
51
51
 
52
+ export const DisputeListResponseSchema = z.object({
53
+ disputes: z.array(DisputeSchema),
54
+ total: z.number(),
55
+ page: z.number(),
56
+ limit: z.number(),
57
+ })
58
+
59
+ export const DisputeListQuerySchema = z.object({
60
+ page: z.coerce.number().int().positive().default(1),
61
+ limit: z.coerce.number().int().positive().max(100).default(20),
62
+ })
63
+
52
64
  export const DisputeDeleteResultSchema = z.object({
53
65
  message: z.string(),
54
66
  })
@@ -59,4 +71,6 @@ export type Dispute = z.infer<typeof DisputeSchema>
59
71
  export type CreateDisputeInput = z.infer<typeof CreateDisputeSchema>
60
72
  export type UpdateDisputeInput = z.infer<typeof UpdateDisputeSchema>
61
73
  export type ResolveDisputeInput = z.infer<typeof ResolveDisputeSchema>
74
+ export type DisputeListResponse = z.infer<typeof DisputeListResponseSchema>
75
+ export type DisputeListQuery = z.infer<typeof DisputeListQuerySchema>
62
76
  export type DisputeDeleteResult = z.infer<typeof DisputeDeleteResultSchema>
@@ -33,11 +33,18 @@ export const UpdateOrderSchema = z.object({
33
33
 
34
34
  export const OrderListSchema = z.array(OrderSchema)
35
35
 
36
+ export const OrderListResponseSchema = z.object({
37
+ orders: z.array(OrderSchema),
38
+ total: z.number(),
39
+ page: z.number(),
40
+ limit: z.number(),
41
+ })
42
+
36
43
  export const OrderQuerySchema = z.object({
37
44
  status: OrderStatusSchema.nullish(),
38
45
  customerName: z.string().nullish(),
46
+ page: z.coerce.number().int().positive().default(1),
39
47
  limit: z.coerce.number().int().positive().max(100).default(20),
40
- offset: z.coerce.number().int().min(0).default(0),
41
48
  })
42
49
 
43
50
  export const OrderDeleteResultSchema = z.object({
@@ -60,6 +67,7 @@ export type UpdateOrderInput = z.infer<typeof UpdateOrderSchema>
60
67
  export type OrderDeleteResult = z.infer<typeof OrderDeleteResultSchema>
61
68
  export type ProcessOrderInput = z.infer<typeof ProcessOrderSchema>
62
69
  export type CancelOrderInput = z.infer<typeof CancelOrderSchema>
70
+ export type OrderListResponse = z.infer<typeof OrderListResponseSchema>
63
71
  export type OrderQueryInput = z.infer<typeof OrderQuerySchema>
64
72
 
65
73
  export const RemoveCartItemResponseSchema = z.object({ removedId: z.string() })
@@ -62,6 +62,18 @@ export const ReplyTicketSchema = z.object({
62
62
 
63
63
  export const TicketListSchema = z.array(TicketSchema)
64
64
 
65
+ export const TicketListResponseSchema = z.object({
66
+ tickets: z.array(TicketSchema),
67
+ total: z.number(),
68
+ page: z.number(),
69
+ limit: z.number(),
70
+ })
71
+
72
+ export const TicketListQuerySchema = z.object({
73
+ page: z.coerce.number().int().positive().default(1),
74
+ limit: z.coerce.number().int().positive().max(100).default(20),
75
+ })
76
+
65
77
  export const TicketDeleteResultSchema = z.object({
66
78
  message: z.string(),
67
79
  })
@@ -74,4 +86,6 @@ export type Ticket = z.infer<typeof TicketSchema>
74
86
  export type CreateTicketInput = z.infer<typeof CreateTicketSchema>
75
87
  export type UpdateTicketInput = z.infer<typeof UpdateTicketSchema>
76
88
  export type ReplyTicketInput = z.infer<typeof ReplyTicketSchema>
89
+ export type TicketListResponse = z.infer<typeof TicketListResponseSchema>
90
+ export type TicketListQuery = z.infer<typeof TicketListQuerySchema>
77
91
  export type TicketDeleteResult = z.infer<typeof TicketDeleteResultSchema>
@@ -10,6 +10,8 @@ export {
10
10
  TodoWithAttachmentsSchema,
11
11
  UploadFileSchema,
12
12
  AttachmentIdResponseSchema,
13
+ TodoListResponseSchema,
14
+ TodoListQuerySchema,
13
15
  type Todo,
14
16
  type TodoStatus,
15
17
  type CreateTodoInput,
@@ -17,4 +19,6 @@ export {
17
19
  type TodoIdResponse,
18
20
  type TodoAttachment,
19
21
  type TodoWithAttachments,
22
+ type TodoListResponse,
23
+ type TodoListQuery,
20
24
  } from './schemas'
@@ -56,6 +56,18 @@ export const AttachmentIdResponseSchema = z.object({
56
56
  id: z.number(),
57
57
  })
58
58
 
59
+ export const TodoListResponseSchema = z.object({
60
+ todos: z.array(TodoSchema),
61
+ total: z.number(),
62
+ page: z.number(),
63
+ limit: z.number(),
64
+ })
65
+
66
+ export const TodoListQuerySchema = z.object({
67
+ page: z.coerce.number().int().positive().default(1),
68
+ limit: z.coerce.number().int().positive().max(100).default(20),
69
+ })
70
+
59
71
  export type TodoStatus = z.infer<typeof TodoStatusSchema>
60
72
  export type Todo = z.infer<typeof TodoSchema>
61
73
  export type CreateTodoInput = z.infer<typeof CreateTodoSchema>
@@ -63,3 +75,5 @@ export type UpdateTodoInput = z.infer<typeof UpdateTodoSchema>
63
75
  export type TodoIdResponse = z.infer<typeof TodoIdResponseSchema>
64
76
  export type TodoAttachment = z.infer<typeof TodoAttachmentSchema>
65
77
  export type TodoWithAttachments = z.infer<typeof TodoWithAttachmentsSchema>
78
+ export type TodoListResponse = z.infer<typeof TodoListResponseSchema>
79
+ export type TodoListQuery = z.infer<typeof TodoListQuerySchema>
@@ -49,6 +49,8 @@ export {
49
49
  TodoWithAttachmentsSchema,
50
50
  UploadFileSchema,
51
51
  AttachmentIdResponseSchema,
52
+ TodoListResponseSchema,
53
+ TodoListQuerySchema,
52
54
  type Todo,
53
55
  type TodoStatus,
54
56
  type CreateTodoInput,
@@ -56,6 +58,8 @@ export {
56
58
  type TodoIdResponse,
57
59
  type TodoAttachment,
58
60
  type TodoWithAttachments,
61
+ type TodoListResponse,
62
+ type TodoListQuery,
59
63
  } from '../modules/todos'
60
64
  export {
61
65
  NotificationSchema,
@@ -171,6 +175,7 @@ export {
171
175
  CreateOrderSchema,
172
176
  UpdateOrderSchema,
173
177
  OrderListSchema,
178
+ OrderListResponseSchema,
174
179
  OrderQuerySchema,
175
180
  OrderDeleteResultSchema,
176
181
  ProcessOrderSchema,
@@ -184,6 +189,7 @@ export {
184
189
  type CreateOrderInput,
185
190
  type UpdateOrderInput,
186
191
  type OrderDeleteResult,
192
+ type OrderListResponse,
187
193
  type ProcessOrderInput,
188
194
  type CancelOrderInput,
189
195
  type OrderQueryInput,
@@ -239,12 +245,16 @@ export {
239
245
  UpdateDisputeSchema,
240
246
  ResolveDisputeSchema,
241
247
  DisputeListSchema,
248
+ DisputeListResponseSchema,
249
+ DisputeListQuerySchema,
242
250
  type DisputeType,
243
251
  type DisputeStatus,
244
252
  type Dispute,
245
253
  type CreateDisputeInput,
246
254
  type UpdateDisputeInput,
247
255
  type ResolveDisputeInput,
256
+ type DisputeListResponse,
257
+ type DisputeListQuery,
248
258
  } from '../modules/dispute'
249
259
  export {
250
260
  ProductSchema,
@@ -277,6 +287,8 @@ export {
277
287
  UpdateTicketSchema,
278
288
  ReplyTicketSchema,
279
289
  TicketListSchema,
290
+ TicketListResponseSchema,
291
+ TicketListQuerySchema,
280
292
  TicketDeleteResultSchema,
281
293
  type TicketStatus,
282
294
  type TicketPriority,
@@ -287,6 +299,8 @@ export {
287
299
  type UpdateTicketInput,
288
300
  type ReplyTicketInput,
289
301
  type TicketDeleteResult,
302
+ type TicketListResponse,
303
+ type TicketListQuery,
290
304
  } from '../modules/ticket'
291
305
  export {
292
306
  ContentCategorySchema,
@@ -295,6 +309,8 @@ export {
295
309
  CreateContentSchema,
296
310
  UpdateContentSchema,
297
311
  ContentListSchema,
312
+ ContentListResponseSchema,
313
+ ContentListQuerySchema,
298
314
  ContentDeleteResultSchema,
299
315
  type ContentCategory,
300
316
  type ContentStatus,
@@ -302,6 +318,8 @@ export {
302
318
  type CreateContentInput,
303
319
  type UpdateContentInput,
304
320
  type ContentDeleteResult,
321
+ type ContentListResponse,
322
+ type ContentListQuery,
305
323
  } from '../modules/content'
306
324
  export {
307
325
  CaptchaResponseSchema,
@@ -1,10 +1,14 @@
1
1
  import { defineConfig, type Options } from 'tsup'
2
2
  import { existsSync } from 'fs'
3
3
  import { join, resolve } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = fileURLToPath(new URL('.', import.meta.url))
4
7
 
5
8
  const root = process.cwd()
6
9
 
7
- const serverBuildConfigs: Options[] = [
10
+ // 按入口文件存在性过滤(cli-only 等无 client preset 会排除 cloudflare 入口)
11
+ const rawServerBuildConfigs: Options[] = [
8
12
  {
9
13
  entry: ['src/server/entries/node.ts'],
10
14
  outDir: 'dist/server',
@@ -48,6 +52,12 @@ const serverBuildConfigs: Options[] = [
48
52
  'zod',
49
53
  'drizzle-orm',
50
54
  'drizzle-orm/d1',
55
+ 'react',
56
+ 'react-dom',
57
+ 'react-dom/server',
58
+ 'react-router-dom',
59
+ 'react-helmet-async',
60
+ 'zustand',
51
61
  ],
52
62
  external: [
53
63
  'pino',
@@ -72,10 +82,47 @@ const serverBuildConfigs: Options[] = [
72
82
  ],
73
83
  define: {
74
84
  'process.env.NODE_ENV': '"production"',
85
+ 'import.meta.env.VITE_PRESET': '"todo"',
86
+ },
87
+ esbuildOptions(options) {
88
+ // Strip CSS imports for SSR (CSS is handled by client build)
89
+ options.loader = {
90
+ ...options.loader,
91
+ '.css': 'empty',
92
+ '.svg': 'empty',
93
+ '.png': 'empty',
94
+ '.jpg': 'empty',
95
+ '.jpeg': 'empty',
96
+ '.gif': 'empty',
97
+ '.woff': 'empty',
98
+ '.woff2': 'empty',
99
+ '.ttf': 'empty',
100
+ '.eot': 'empty',
101
+ }
102
+ // Path aliases for client/shared modules
103
+ options.alias = {
104
+ '@shared': resolve(root, 'src/shared'),
105
+ '@client': resolve(root, 'src/client'),
106
+ '@server': resolve(root, 'src/server'),
107
+ '@admin': resolve(root, 'src/admin'),
108
+ // Use browser variant of react-dom/server for Cloudflare Workers
109
+ // (Node variant requires 'util' builtin which is unavailable in Workers)
110
+ 'react-dom/server': 'react-dom/server.browser',
111
+ }
75
112
  },
76
113
  },
77
114
  ]
78
115
 
116
+ // 按入口文件存在性过滤(cli-only 等无 client 的 preset 会排除 cloudflare 入口文件)
117
+ const serverBuildConfigs: Options[] = rawServerBuildConfigs.filter(c => {
118
+ const raw = c.entry
119
+ const entries = Array.isArray(raw) ? raw : raw ? [raw] : []
120
+ return (
121
+ entries.length === 0 ||
122
+ entries.every(e => typeof e === 'string' && existsSync(resolve(__dirname, e)))
123
+ )
124
+ })
125
+
79
126
  // Only build CLI if CLI module is included in preset
80
127
  const cliEntryPath = join(process.cwd(), 'src/cli/index.ts')
81
128
  if (existsSync(cliEntryPath)) {
@@ -1,12 +1,28 @@
1
1
  import { defineConfig } from 'vitest/config'
2
- import react from '@vitejs/plugin-react'
3
2
  import { resolve } from 'node:path'
3
+ import { createRequire } from 'node:module'
4
+
5
+ // react 插件仅含 client 的 preset 安装;cli-only 等服务端 preset 跳过
6
+ // 变量说明符:tsc 不做模块类型解析(cli-only 未安装该依赖)
7
+ const reactPluginPkg = '@vitejs/plugin-react'
8
+ const reactPlugin = (await import(/* @vite-ignore */ reactPluginPkg)
9
+ .then((m: { default: () => unknown }) => m.default())
10
+ .catch(() => null)) as unknown
4
11
 
5
12
  export default defineConfig({
6
- plugins: [react()],
13
+ plugins: reactPlugin ? [reactPlugin as never] : [],
7
14
  test: {
15
+ passWithNoTests: true,
8
16
  globals: true,
9
- environment: 'jsdom',
17
+ // jsdom 仅含 client 的 preset 安装;缺失时回退 node(服务端测试同样适用)
18
+ environment: (() => {
19
+ try {
20
+ createRequire(import.meta.url).resolve('jsdom')
21
+ return 'jsdom'
22
+ } catch {
23
+ return 'node'
24
+ }
25
+ })(),
10
26
  setupFiles: ['./vitest.setup.ts'],
11
27
  include: [
12
28
  '**/__tests__/**/*.test.ts',