create-fullstack-scaffold 0.1.1 → 0.2.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 (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
@@ -3,7 +3,7 @@ import { OpenAPIHono } from '@hono/zod-openapi'
3
3
  import * as contentService from '../services/content-service'
4
4
  import { successResponse, errorResponse } from '@server/utils/route-helpers'
5
5
  import { NotFoundError } from '@server/utils/app-error'
6
- import { success, list } from '@server/utils/response'
6
+ import { success } from '@server/utils/response'
7
7
  import { z } from '@hono/zod-openapi'
8
8
  import { ContentSchema, ContentCategorySchema } from '@shared/modules/content'
9
9
 
@@ -46,7 +46,7 @@ export const publicContentRoutes = new OpenAPIHono()
46
46
  search,
47
47
  })
48
48
  const items = result.slice(offset, offset + limit)
49
- return c.json(list(items, result.length), 200)
49
+ return c.json(success(items), 200)
50
50
  })
51
51
  .openapi(getPublicRoute, async c => {
52
52
  const { id } = c.req.valid('param')
@@ -0,0 +1,205 @@
1
+ import { createRoute } from '@hono/zod-openapi'
2
+ import { OpenAPIHono } from '@hono/zod-openapi'
3
+ import { successResponse } from '@server/utils/route-helpers'
4
+ import { TopicsResponseSchema, ProfileResponseSchema } from '@shared/schemas'
5
+
6
+ const MOCK_TOPICS = [
7
+ {
8
+ id: '1',
9
+ title: 'How to implement real-time notifications with SSE?',
10
+ excerpt:
11
+ 'I am trying to set up server-sent events for my community app. The connection keeps dropping after a few minutes. Has anyone dealt with this issue before?',
12
+ votes: 24,
13
+ replyCount: 8,
14
+ viewCount: 342,
15
+ status: 'solved' as const,
16
+ tags: [
17
+ { label: 'SSE', color: 'bg-emerald-100 text-emerald-700' },
18
+ { label: 'Real-time', color: 'bg-blue-100 text-blue-700' },
19
+ ],
20
+ author: { name: 'Sarah Chen', initials: 'SC' },
21
+ createdAt: '1h ago',
22
+ },
23
+ {
24
+ id: '2',
25
+ title: 'Best practices for WebSocket reconnection logic',
26
+ excerpt:
27
+ 'My WebSocket client disconnects when the user switches tabs. What reconnection strategies do you recommend for a production environment?',
28
+ votes: 18,
29
+ replyCount: 0,
30
+ viewCount: 156,
31
+ status: 'unanswered' as const,
32
+ tags: [
33
+ { label: 'WebSocket', color: 'bg-purple-100 text-purple-700' },
34
+ { label: 'Architecture', color: 'bg-orange-100 text-orange-700' },
35
+ ],
36
+ author: { name: 'Alex Rivera', initials: 'AR' },
37
+ createdAt: '2h ago',
38
+ },
39
+ {
40
+ id: '3',
41
+ title: 'Type-safe API routes with Hono RPC — a complete guide',
42
+ excerpt:
43
+ 'After months of using Hono RPC in production, here is my comprehensive guide to achieving full end-to-end type safety across your stack.',
44
+ votes: 56,
45
+ replyCount: 12,
46
+ viewCount: 891,
47
+ status: 'hot' as const,
48
+ tags: [
49
+ { label: 'Hono', color: 'bg-sky-100 text-sky-700' },
50
+ { label: 'TypeScript', color: 'bg-blue-100 text-blue-700' },
51
+ { label: 'Guide', color: 'bg-emerald-100 text-emerald-700' },
52
+ ],
53
+ author: { name: 'Jordan Park', initials: 'JP' },
54
+ createdAt: '3h ago',
55
+ },
56
+ {
57
+ id: '4',
58
+ title: 'Deploying Hono apps to Cloudflare Workers with D1',
59
+ excerpt:
60
+ 'Step-by-step walkthrough of deploying a full-stack Hono application to Cloudflare Workers, including database setup with D1 and R2 storage.',
61
+ votes: 31,
62
+ replyCount: 5,
63
+ viewCount: 478,
64
+ status: 'solved' as const,
65
+ tags: [
66
+ { label: 'Cloudflare', color: 'bg-amber-100 text-amber-700' },
67
+ { label: 'Deployment', color: 'bg-rose-100 text-rose-700' },
68
+ ],
69
+ author: { name: 'Mika Tanaka', initials: 'MT' },
70
+ createdAt: '5h ago',
71
+ },
72
+ {
73
+ id: '5',
74
+ title: 'Zustand vs Jotai — which state manager for community apps?',
75
+ excerpt:
76
+ 'Comparing Zustand and Jotai for a medium-complexity community application. Performance benchmarks and developer experience included.',
77
+ votes: 42,
78
+ replyCount: 0,
79
+ viewCount: 267,
80
+ status: 'unanswered' as const,
81
+ tags: [
82
+ { label: 'React', color: 'bg-cyan-100 text-cyan-700' },
83
+ { label: 'State Management', color: 'bg-violet-100 text-violet-700' },
84
+ ],
85
+ author: { name: 'Liam Nguyen', initials: 'LN' },
86
+ createdAt: '8h ago',
87
+ },
88
+ {
89
+ id: '6',
90
+ title: 'Building a plugin system with module manifests',
91
+ excerpt:
92
+ 'How we designed a declarative module manifest system that lets users scaffold apps with only the features they need. Patterns and lessons learned.',
93
+ votes: 67,
94
+ replyCount: 19,
95
+ viewCount: 1204,
96
+ status: 'hot' as const,
97
+ tags: [
98
+ { label: 'Architecture', color: 'bg-orange-100 text-orange-700' },
99
+ { label: 'Plugins', color: 'bg-pink-100 text-pink-700' },
100
+ ],
101
+ author: { name: 'Emma Wilson', initials: 'EW' },
102
+ createdAt: '12h ago',
103
+ },
104
+ ]
105
+
106
+ const MOCK_PROFILE_ACTIVITY = [
107
+ {
108
+ id: '1',
109
+ type: 'reply' as const,
110
+ text: 'Replied to',
111
+ target: 'How to implement real-time notifications with SSE?',
112
+ time: '2h ago',
113
+ },
114
+ {
115
+ id: '2',
116
+ type: 'topic' as const,
117
+ text: 'Created topic',
118
+ target: 'Best practices for WebSocket reconnection logic',
119
+ time: '5h ago',
120
+ },
121
+ {
122
+ id: '3',
123
+ type: 'like' as const,
124
+ text: 'Liked',
125
+ target: 'Type-safe API routes with Hono RPC — a complete guide',
126
+ time: '8h ago',
127
+ },
128
+ {
129
+ id: '4',
130
+ type: 'reply' as const,
131
+ text: 'Replied to',
132
+ target: 'Deploying Hono apps to Cloudflare Workers with D1',
133
+ time: '1d ago',
134
+ },
135
+ {
136
+ id: '5',
137
+ type: 'topic' as const,
138
+ text: 'Created topic',
139
+ target: 'Zustand vs Jotai — which state manager for community apps?',
140
+ time: '2d ago',
141
+ },
142
+ {
143
+ id: '6',
144
+ type: 'like' as const,
145
+ text: 'Liked',
146
+ target: 'Building a plugin system with module manifests',
147
+ time: '3d ago',
148
+ },
149
+ {
150
+ id: '7',
151
+ type: 'reply' as const,
152
+ text: 'Replied to',
153
+ target: 'How to set up CI/CD for monorepo projects',
154
+ time: '4d ago',
155
+ },
156
+ {
157
+ id: '8',
158
+ type: 'topic' as const,
159
+ text: 'Created topic',
160
+ target: 'Tailwind CSS v4 migration guide and tips',
161
+ time: '5d ago',
162
+ },
163
+ ]
164
+
165
+ const getTopicsRoute = createRoute({
166
+ method: 'get',
167
+ path: '/topics',
168
+ responses: {
169
+ 200: successResponse(TopicsResponseSchema, 'List community topics'),
170
+ },
171
+ })
172
+
173
+ const getPopularTopicsRoute = createRoute({
174
+ method: 'get',
175
+ path: '/topics/popular',
176
+ responses: {
177
+ 200: successResponse(TopicsResponseSchema, 'Popular topics'),
178
+ },
179
+ })
180
+
181
+ const getProfileRoute = createRoute({
182
+ method: 'get',
183
+ path: '/profile',
184
+ responses: {
185
+ 200: successResponse(ProfileResponseSchema, 'User profile'),
186
+ },
187
+ })
188
+
189
+ export const topicsRoutes = new OpenAPIHono()
190
+ .openapi(getTopicsRoute, async c => {
191
+ return c.json({ success: true as const, data: MOCK_TOPICS })
192
+ })
193
+ .openapi(getPopularTopicsRoute, async c => {
194
+ const popular = [...MOCK_TOPICS].sort((a, b) => b.votes - a.votes)
195
+ return c.json({ success: true as const, data: popular })
196
+ })
197
+ .openapi(getProfileRoute, async c => {
198
+ return c.json({
199
+ success: true as const,
200
+ data: {
201
+ stats: { topics: 12, replies: 48, likes: 156 },
202
+ activity: MOCK_PROFILE_ACTIVITY,
203
+ },
204
+ })
205
+ })
@@ -11,10 +11,12 @@ import { contents, type ContentTable } from '@server/db/schema'
11
11
  import { toISOString } from '@server/utils/date'
12
12
  import { randomDate, randomElement } from '@server/utils/generate'
13
13
  import { parseModuleId } from '@server/utils/id-helpers'
14
+ // @framework-import ISR 缓存失效,内容变更时清除页面缓存
15
+ import { purgeContentPages } from '@server/core/isr-invalidation'
14
16
 
15
17
  export async function seedContentsIfEmpty(): Promise<void> {
16
18
  const db = await getDb()
17
- const existing = await db.select().from(contents).all()
19
+ const existing = await db.select().from(contents)
18
20
  if (existing.length === 0) {
19
21
  const CATEGORIES: ContentCategory[] = ['article', 'announcement', 'tutorial', 'news', 'policy']
20
22
  const STATUSES: ContentStatus[] = ['draft', 'published', 'archived']
@@ -48,7 +50,9 @@ export async function seedContentsIfEmpty(): Promise<void> {
48
50
 
49
51
  await db.insert(contents).values({
50
52
  title: TITLES[i % TITLES.length],
51
- body: `这是${TITLES[i % TITLES.length]}的详细内容。这里包含了完整的文章内容,用户可以阅读和学习相关知识。`,
53
+ body: `这是${
54
+ TITLES[i % TITLES.length]
55
+ }的详细内容。这里包含了完整的文章内容,用户可以阅读和学习相关知识。`,
52
56
  category,
53
57
  status,
54
58
  author: randomElement(AUTHORS),
@@ -173,6 +177,9 @@ export async function updateContent(id: string, data: UpdateContentInput): Promi
173
177
  const result = await db.update(contents).set(updateData).where(eq(contents.id, numId)).returning()
174
178
 
175
179
  if (result.length === 0) return null
180
+ purgeContentPages().catch(e => {
181
+ console.warn('ISR content pages purge failed after update:', e)
182
+ })
176
183
  return mapContentRow(result[0])
177
184
  }
178
185
 
@@ -183,6 +190,9 @@ export async function deleteContent(id: string): Promise<{ success: boolean; mes
183
190
 
184
191
  const result = await db.delete(contents).where(eq(contents.id, numId)).returning()
185
192
  if (result.length === 0) return { success: false, message: '内容不存在' }
193
+ purgeContentPages().catch(e => {
194
+ console.warn('ISR content pages purge failed after delete:', e)
195
+ })
186
196
  return { success: true, message: '内容已删除' }
187
197
  }
188
198
 
@@ -202,6 +212,9 @@ export async function publishContent(id: string): Promise<Content | null> {
202
212
  .where(eq(contents.id, numId))
203
213
  .returning()
204
214
 
215
+ purgeContentPages().catch(e => {
216
+ console.warn('ISR content pages purge failed after publish:', e)
217
+ })
205
218
  return mapContentRow(result[0])
206
219
  }
207
220
 
@@ -220,5 +233,8 @@ export async function archiveContent(id: string): Promise<Content | null> {
220
233
  .where(eq(contents.id, numId))
221
234
  .returning()
222
235
 
236
+ purgeContentPages().catch(e => {
237
+ console.warn('ISR content pages purge failed after archive:', e)
238
+ })
223
239
  return mapContentRow(result[0])
224
240
  }
@@ -17,6 +17,7 @@ import {
17
17
  UpdateDisputeSchema,
18
18
  DisputeListSchema,
19
19
  ResolveDisputeSchema,
20
+ DeleteResultSchema,
20
21
  } from '@shared/modules/dispute'
21
22
  import { NotFoundError, BusinessError } from '@server/utils/app-error'
22
23
 
@@ -91,7 +92,7 @@ const deleteRoute = createRoute({
91
92
  middleware: [authMiddleware({ requiredPermissions: [Permission.DISPUTE_DELETE] })],
92
93
  request: idRequest,
93
94
  responses: {
94
- 200: successResponse(DisputeSchema, 'Delete dispute'),
95
+ 200: successResponse(DeleteResultSchema, 'Delete dispute'),
95
96
  401: errorResponse('Unauthorized'),
96
97
  403: errorResponse('Forbidden'),
97
98
  404: errorResponse('Dispute not found'),
@@ -15,7 +15,7 @@ import { parseModuleId } from '@server/utils/id-helpers'
15
15
 
16
16
  export async function seedDisputesIfEmpty(): Promise<void> {
17
17
  const db = await getDb()
18
- const existing = await db.select().from(disputes).all()
18
+ const existing = await db.select().from(disputes)
19
19
  if (existing.length === 0) {
20
20
  const DISPUTE_TYPES: DisputeType[] = [
21
21
  'refund',
@@ -13,8 +13,8 @@ setRuntimeAdapter(getNodeRuntimeAdapter())
13
13
  let sseTestContext: { port: number; close: () => Promise<void> }
14
14
  let client: ReturnType<typeof createTestClient>
15
15
 
16
- function connectSSE() {
17
- const conn = client.api.notifications.stream.$sse()
16
+ async function connectSSE() {
17
+ const conn = await client.api.notifications.stream.$sse()
18
18
  return conn as unknown as SSEClientImpl<AppSSEProtocol>
19
19
  }
20
20
 
@@ -44,7 +44,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
44
44
 
45
45
  describe('GET /api/notifications/stream via $sse()', () => {
46
46
  it('should use patched $sse() method for type-safe SSE connection', async () => {
47
- const conn = connectSSE()
47
+ const conn = await connectSSE()
48
48
 
49
49
  expect(['connecting', 'open', 'closed']).toContain(conn.status)
50
50
 
@@ -83,7 +83,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
83
83
  })
84
84
 
85
85
  it('should receive typed notification events', async () => {
86
- const conn = connectSSE()
86
+ const conn = await connectSSE()
87
87
 
88
88
  const receivedNotifications: AppSSEProtocol['events']['notification'][] = []
89
89
 
@@ -110,7 +110,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
110
110
  })
111
111
 
112
112
  it('should receive typed ping events', async () => {
113
- const conn = connectSSE()
113
+ const conn = await connectSSE()
114
114
 
115
115
  const receivedPings: AppSSEProtocol['events']['ping'][] = []
116
116
 
@@ -130,7 +130,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
130
130
  })
131
131
 
132
132
  it('should receive typed connected events', async () => {
133
- const conn = connectSSE()
133
+ const conn = await connectSSE()
134
134
 
135
135
  const receivedConnected: AppSSEProtocol['events']['connected'][] = []
136
136
 
@@ -150,7 +150,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
150
150
  })
151
151
 
152
152
  it('should handle connection status changes', async () => {
153
- const conn = connectSSE()
153
+ const conn = await connectSSE()
154
154
 
155
155
  const statusHistory: ('connecting' | 'open' | 'closed')[] = []
156
156
 
@@ -170,7 +170,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
170
170
  })
171
171
 
172
172
  it('should handle errors gracefully', async () => {
173
- const conn = connectSSE()
173
+ const conn = await connectSSE()
174
174
 
175
175
  const unsubscribe = conn.onError((error: Error) => {
176
176
  console.error('SSE error:', error)
@@ -187,7 +187,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
187
187
 
188
188
  describe('Error Scenarios', () => {
189
189
  it('should handle connection abort during event reception', async () => {
190
- const conn = connectSSE()
190
+ const conn = await connectSSE()
191
191
 
192
192
  const receivedEvents: unknown[] = []
193
193
  const unsubscribe = conn.on('notification', (event: unknown) => {
@@ -206,7 +206,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
206
206
  })
207
207
 
208
208
  it('should handle multiple abort calls gracefully', async () => {
209
- const conn = connectSSE()
209
+ const conn = await connectSSE()
210
210
 
211
211
  await new Promise(resolve => setTimeout(resolve, 500))
212
212
 
@@ -218,7 +218,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
218
218
  })
219
219
 
220
220
  it('should handle unsubscribe before connection close', async () => {
221
- const conn = connectSSE()
221
+ const conn = await connectSSE()
222
222
 
223
223
  const unsubscribe = conn.on('notification', () => {})
224
224
 
@@ -232,7 +232,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
232
232
  })
233
233
 
234
234
  it('should handle error events', async () => {
235
- const conn = connectSSE()
235
+ const conn = await connectSSE()
236
236
 
237
237
  let errorReceived = false
238
238
  const unsubscribe = conn.onError((error: Error) => {
@@ -253,7 +253,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
253
253
  })
254
254
 
255
255
  it('should handle non-existent event type gracefully', async () => {
256
- const conn = connectSSE()
256
+ const conn = await connectSSE()
257
257
 
258
258
  let received = false
259
259
  const unsubscribe = conn.on('nonExistentEvent' as 'notification', () => {
@@ -273,7 +273,7 @@ describe('SSE Routes with Patched $sse() RPC Method', () => {
273
273
  })
274
274
 
275
275
  it('should test error assertion pattern', async () => {
276
- const conn = connectSSE()
276
+ const conn = await connectSSE()
277
277
 
278
278
  await new Promise(resolve => setTimeout(resolve, 500))
279
279
 
@@ -18,6 +18,30 @@ import {
18
18
  import { NotFoundError } from '@server/utils/app-error'
19
19
  import { getRuntimeAdapter } from '@server/core/runtime'
20
20
 
21
+ function createFallbackSSEResponse(): Response {
22
+ const stream = new ReadableStream({
23
+ start(controller) {
24
+ const encoder = new TextEncoder()
25
+ const sendPing = () => {
26
+ controller.enqueue(encoder.encode(`event: ping\ndata: {"timestamp":${Date.now()}}\n\n`))
27
+ }
28
+ sendPing()
29
+ const interval = setInterval(sendPing, 30000)
30
+ setTimeout(() => {
31
+ clearInterval(interval)
32
+ controller.close()
33
+ }, 300000)
34
+ },
35
+ })
36
+ return new Response(stream, {
37
+ headers: {
38
+ 'Content-Type': 'text/event-stream',
39
+ 'Cache-Control': 'no-cache',
40
+ Connection: 'keep-alive',
41
+ },
42
+ })
43
+ }
44
+
21
45
  const streamRoute = createRoute({
22
46
  method: 'get',
23
47
  path: '/notifications/stream',
@@ -126,12 +150,10 @@ const deleteRoute = createRoute({
126
150
 
127
151
  export const notificationRoutes = new OpenAPIHono()
128
152
  .openapi(streamRoute, async c => {
129
- // In Cloudflare environment, route SSE to Durable Object for proper broadcast support
130
153
  const env = c.env as { REALTIME_DO?: DurableObjectNamespace } | undefined
131
154
  if (env?.REALTIME_DO) {
132
155
  const id = env.REALTIME_DO.idFromName('global')
133
156
  const stub = env.REALTIME_DO.get(id)
134
- // Forward the SSE request to the Durable Object
135
157
  const doRequest = new Request(c.req.url, {
136
158
  method: c.req.method,
137
159
  headers: c.req.raw.headers,
@@ -139,13 +161,17 @@ export const notificationRoutes = new OpenAPIHono()
139
161
  return stub.fetch(doRequest)
140
162
  }
141
163
 
142
- // Fallback for Node environment
143
- const adapter = getRuntimeAdapter()
144
- if (adapter.handleSSERequest) {
145
- const response = await adapter.handleSSERequest()
146
- return response
164
+ try {
165
+ const adapter = getRuntimeAdapter()
166
+ if (adapter.handleSSERequest) {
167
+ const response = await adapter.handleSSERequest()
168
+ return response
169
+ }
170
+ } catch {
171
+ return createFallbackSSEResponse()
147
172
  }
148
- return c.json({ success: false as const, error: 'SSE not supported' }, 500)
173
+
174
+ return createFallbackSSEResponse()
149
175
  })
150
176
  .openapi(listRoute, async c => {
151
177
  const query = c.req.valid('query')
@@ -65,8 +65,18 @@ describe('Order Routes', () => {
65
65
  it('should filter orders by both status and customerName', async () => {
66
66
  const client = createTestClient(undefined, { headers: authHeaders })
67
67
 
68
+ const createRes = await client.api['orders'].$post({
69
+ json: {
70
+ customerName: 'Filter Test',
71
+ customerEmail: 'filter@example.com',
72
+ productName: 'Filter Product',
73
+ amount: 100,
74
+ },
75
+ })
76
+ expect(createRes.status).toBe(201)
77
+
68
78
  const res = await client.api['orders'].$get({
69
- query: { status: 'pending', customerName: '李四' },
79
+ query: { status: 'pending', customerName: 'Filter Test' },
70
80
  })
71
81
  expect(res.status).toBe(200)
72
82
 
@@ -76,7 +86,7 @@ describe('Order Routes', () => {
76
86
  expect(Array.isArray(data.data)).toBe(true)
77
87
  data.data.forEach((order: { status: string; customerName: string }) => {
78
88
  expect(order.status).toBe('pending')
79
- expect(order.customerName).toContain('李四')
89
+ expect(order.customerName).toContain('Filter Test')
80
90
  })
81
91
  }
82
92
  })
@@ -84,6 +94,16 @@ describe('Order Routes', () => {
84
94
  it('should return empty array when no orders match filter', async () => {
85
95
  const client = createTestClient(undefined, { headers: authHeaders })
86
96
 
97
+ const createRes = await client.api['orders'].$post({
98
+ json: {
99
+ customerName: 'Exists Test',
100
+ customerEmail: 'exists@example.com',
101
+ productName: 'Exists Product',
102
+ amount: 100,
103
+ },
104
+ })
105
+ expect(createRes.status).toBe(201)
106
+
87
107
  const res = await client.api['orders'].$get({
88
108
  query: { customerName: 'NonExistentCustomer12345' },
89
109
  })
@@ -7,6 +7,16 @@ const orderManifest: ModuleManifest = {
7
7
  dependsOn: ['permission'],
8
8
 
9
9
  routes: {
10
+ client: [
11
+ {
12
+ importPath: './routes/cart-routes',
13
+ exportName: 'cartRoutes',
14
+ },
15
+ {
16
+ importPath: './routes/orders-mock-routes',
17
+ exportName: 'ordersMockRoutes',
18
+ },
19
+ ],
10
20
  admin: [
11
21
  {
12
22
  importPath: './routes/order-routes',
@@ -19,6 +29,11 @@ const orderManifest: ModuleManifest = {
19
29
  path: 'order',
20
30
  },
21
31
 
32
+ clientPages: [
33
+ { name: 'CartPage', route: '/cart' },
34
+ { name: 'OrdersPage', route: '/orders' },
35
+ ],
36
+
22
37
  adminPages: [{ name: 'OrdersPage', route: '/orders', requiredPermission: 'ORDER_VIEW' }],
23
38
 
24
39
  dbSchemas: {
@@ -0,0 +1,103 @@
1
+ import { createRoute } from '@hono/zod-openapi'
2
+ import { OpenAPIHono } from '@hono/zod-openapi'
3
+ import { successResponse, errorResponse } from '@server/utils/route-helpers'
4
+ import { z } from '@hono/zod-openapi'
5
+ import {
6
+ CartItemSchema,
7
+ CartResponseSchema,
8
+ AddCartItemSchema,
9
+ RemoveCartItemResponseSchema,
10
+ } from '@shared/schemas'
11
+
12
+ const SHIPPING_THRESHOLD = 50
13
+ const TAX_RATE = 0.08
14
+
15
+ const MOCK_CART_ITEMS = [
16
+ {
17
+ id: 1,
18
+ name: 'Wireless Headphones',
19
+ variant: 'Black',
20
+ price: 79.99,
21
+ quantity: 1,
22
+ color: '#374151',
23
+ },
24
+ {
25
+ id: 2,
26
+ name: 'Organic Cotton T-Shirt',
27
+ variant: 'Medium / Sage',
28
+ price: 34.99,
29
+ quantity: 2,
30
+ color: '#6ee7b7',
31
+ },
32
+ {
33
+ id: 3,
34
+ name: 'Ceramic Travel Mug',
35
+ variant: 'Amber / 350ml',
36
+ price: 24.99,
37
+ quantity: 1,
38
+ color: '#f59e0b',
39
+ },
40
+ ]
41
+
42
+ function computeSummary(items: typeof MOCK_CART_ITEMS) {
43
+ const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
44
+ const shipping = subtotal >= SHIPPING_THRESHOLD ? 0 : 5.99
45
+ const tax = subtotal * TAX_RATE
46
+ const total = subtotal + shipping + tax
47
+ const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
48
+ return { subtotal, shipping, tax, total, totalItems }
49
+ }
50
+
51
+ const getCartRoute = createRoute({
52
+ method: 'get',
53
+ path: '/cart',
54
+ responses: {
55
+ 200: successResponse(CartResponseSchema, 'Get cart'),
56
+ },
57
+ })
58
+
59
+ const addCartItemRoute = createRoute({
60
+ method: 'post',
61
+ path: '/cart/items',
62
+ request: {
63
+ body: {
64
+ content: {
65
+ 'application/json': { schema: AddCartItemSchema },
66
+ },
67
+ },
68
+ },
69
+ responses: {
70
+ 201: successResponse(CartItemSchema, 'Item added to cart'),
71
+ 400: errorResponse('Invalid input'),
72
+ },
73
+ })
74
+
75
+ const removeCartItemRoute = createRoute({
76
+ method: 'delete',
77
+ path: '/cart/items/{id}',
78
+ request: {
79
+ params: z.object({ id: z.string() }),
80
+ },
81
+ responses: {
82
+ 200: successResponse(RemoveCartItemResponseSchema, 'Item removed'),
83
+ 404: errorResponse('Item not found'),
84
+ },
85
+ })
86
+
87
+ export const cartRoutes = new OpenAPIHono()
88
+ .openapi(getCartRoute, async c => {
89
+ const summary = computeSummary(MOCK_CART_ITEMS)
90
+ return c.json({ success: true as const, data: { items: MOCK_CART_ITEMS, summary } })
91
+ })
92
+ .openapi(addCartItemRoute, async c => {
93
+ const body = c.req.valid('json')
94
+ return c.json({ success: true as const, data: body }, 201)
95
+ })
96
+ .openapi(removeCartItemRoute, async c => {
97
+ const { id } = c.req.valid('param')
98
+ const exists = MOCK_CART_ITEMS.some(item => String(item.id) === id)
99
+ if (!exists) {
100
+ return c.json({ success: false as const, error: 'Item not found' }, 404)
101
+ }
102
+ return c.json({ success: true as const, data: { removedId: id } })
103
+ })