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.
- package/dist/cli/index.js +1673 -696
- package/dist/cli/index.js.map +1 -1
- package/package.json +16 -10
- package/template/.husky/pre-commit +1 -1
- package/template/modules.config.ts +29 -2
- package/template/package.json +12 -12
- package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
- package/template/playwright.config.ts +7 -1
- package/template/src/admin/App.tsx +2 -2
- package/template/src/admin/components/CaptchaModal.tsx +7 -9
- package/template/src/admin/layouts/Header.tsx +56 -22
- package/template/src/admin/layouts/Layout.tsx +14 -9
- package/template/src/admin/layouts/Sidebar.tsx +122 -105
- package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
- package/template/src/admin/pages/ContentPage.tsx +2 -0
- package/template/src/admin/pages/DashboardPage.tsx +2 -0
- package/template/src/admin/pages/DisputesPage.tsx +2 -0
- package/template/src/admin/pages/MediaTestPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +2 -0
- package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
- package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
- package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
- package/template/src/admin/pages/TicketsPage.tsx +2 -0
- package/template/src/admin/pages/UsersPage.tsx +3 -2
- package/template/src/admin/stores/adminStore.ts +5 -0
- package/template/src/cli/index.ts +17 -19
- package/template/src/cli/modules/auth/index.ts +65 -0
- package/template/src/cli/modules/config/index.ts +74 -77
- package/template/src/cli/modules/index.ts +28 -6
- package/template/src/cli/modules/notification/index.ts +95 -79
- package/template/src/cli/modules/plugin/index.ts +111 -0
- package/template/src/cli/modules/todo/index.ts +99 -51
- package/template/src/cli/utils/auto-command.ts +7 -25
- package/template/src/cli/utils/index.ts +3 -1
- package/template/src/client/App.tsx +36 -16
- package/template/src/client/Layout.tsx +67 -10
- package/template/src/client/components/AuthButton.tsx +25 -18
- package/template/src/client/components/BottomTabBar.tsx +107 -0
- package/template/src/client/components/Navigation.tsx +162 -52
- package/template/src/client/components/__tests__/App.test.tsx +48 -32
- package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
- package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
- package/template/src/client/components/index.ts +1 -0
- package/template/src/client/contexts/PresetContext.tsx +10 -0
- package/template/src/client/main.tsx +63 -8
- package/template/src/client/pages/CartPage.tsx +244 -0
- package/template/src/client/pages/CategoriesPage.tsx +100 -0
- package/template/src/client/pages/ContentDetailPage.tsx +9 -14
- package/template/src/client/pages/ContentListPage.tsx +4 -11
- package/template/src/client/pages/DashboardPage.tsx +261 -0
- package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
- package/template/src/client/pages/LoginPage.tsx +127 -0
- package/template/src/client/pages/OrdersPage.tsx +196 -0
- package/template/src/client/pages/PluginDetailPage.tsx +345 -0
- package/template/src/client/pages/PluginsPage.tsx +223 -0
- package/template/src/client/pages/ProfilePage.tsx +206 -0
- package/template/src/client/pages/PublishPage.tsx +336 -0
- package/template/src/client/pages/RegisterPage.tsx +136 -0
- package/template/src/client/pages/SearchPage.tsx +204 -0
- package/template/src/client/pages/SettingsPage.tsx +220 -0
- package/template/src/client/pages/TopicsPage.tsx +180 -0
- package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
- package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
- package/template/src/client/preset-ui-config.ts +492 -0
- package/template/src/client/services/apiClient.ts +14 -4
- package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
- package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
- package/template/src/client/stores/authStore.ts +58 -5
- package/template/src/client/stores/chatWSStore.ts +9 -0
- package/template/src/client/stores/notificationStore.ts +4 -0
- package/template/src/client/stores/pluginStore.ts +279 -0
- package/template/src/client/stores/todoStore.ts +2 -6
- package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
- package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
- package/template/src/server/core/isr-cache.ts +239 -0
- package/template/src/server/core/isr-invalidation.ts +45 -0
- package/template/src/server/core/module-loader.ts +14 -7
- package/template/src/server/core/ssr-renderer.ts +240 -0
- package/template/src/server/db/init.ts +257 -10
- package/template/src/server/db/schema/developers.ts +20 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/plugins.ts +114 -0
- package/template/src/server/db/test-setup.ts +91 -0
- package/template/src/server/entries/cloudflare.ts +79 -7
- package/template/src/server/entries/node.ts +48 -5
- package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
- package/template/src/server/middleware/auth.ts +8 -1
- package/template/src/server/middleware/captcha.ts +14 -4
- package/template/src/server/middleware/rate-limit.ts +7 -2
- package/template/src/server/module-admin/module.ts +9 -5
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
- package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
- package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
- package/template/src/server/module-admin/services/admin-service.ts +68 -11
- package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
- package/template/src/server/module-auth/index.ts +7 -0
- package/template/src/server/module-auth/module.ts +40 -0
- package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
- package/template/src/server/module-auth/services/auth-service.ts +100 -0
- package/template/src/server/module-content/module.ts +10 -4
- package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
- package/template/src/server/module-content/routes/topics-routes.ts +205 -0
- package/template/src/server/module-content/services/content-service.ts +18 -2
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
- package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
- package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
- package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
- package/template/src/server/module-order/module.ts +15 -0
- package/template/src/server/module-order/routes/cart-routes.ts +103 -0
- package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
- package/template/src/server/module-order/services/order-service.ts +1 -1
- package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
- package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
- package/template/src/server/module-plugin/index.ts +2 -0
- package/template/src/server/module-plugin/module.ts +52 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
- package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
- package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
- package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
- package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
- package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
- package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
- package/template/src/server/route-registry.ts +16 -1
- package/template/src/server/test-utils/test-client.ts +1 -2
- package/template/src/server/utils/auth.ts +6 -0
- package/template/src/server/utils/json.ts +13 -0
- package/template/src/shared/core/module-manifest.ts +3 -6
- package/template/src/shared/modules/auth/index.ts +12 -0
- package/template/src/shared/modules/auth/schemas.ts +50 -0
- package/template/src/shared/modules/cart/index.ts +1 -0
- package/template/src/shared/modules/cart/schemas.ts +41 -0
- package/template/src/shared/modules/community/index.ts +1 -0
- package/template/src/shared/modules/community/schemas.ts +58 -0
- package/template/src/shared/modules/dashboard/index.ts +1 -0
- package/template/src/shared/modules/dashboard/schemas.ts +35 -0
- package/template/src/shared/modules/index.ts +41 -0
- package/template/src/shared/modules/order/schemas.ts +29 -0
- package/template/src/shared/modules/plugins/index.ts +48 -0
- package/template/src/shared/modules/plugins/schemas.ts +227 -0
- package/template/src/shared/schemas/index.ts +130 -0
- package/template/tests/e2e/todo.spec.ts +23 -18
- package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
- package/template/uploads/.gitkeep +0 -0
- package/template/vite.config.ts +2 -1
- package/template/vitest.setup.ts +11 -0
- package/template/wrangler.toml +4 -3
- package/template/package-lock.json +0 -14554
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { apiClient } from '@client/services/apiClient'
|
|
3
|
+
import type {
|
|
4
|
+
Plugin,
|
|
5
|
+
Category,
|
|
6
|
+
Review,
|
|
7
|
+
MarketplaceStats,
|
|
8
|
+
CreatePluginInput,
|
|
9
|
+
CreateReviewInput,
|
|
10
|
+
PluginListResponse,
|
|
11
|
+
} from '@shared/schemas'
|
|
12
|
+
|
|
13
|
+
type Pagination = Pick<PluginListResponse, 'page' | 'limit' | 'total'>
|
|
14
|
+
|
|
15
|
+
interface PluginState {
|
|
16
|
+
plugins: Plugin[]
|
|
17
|
+
currentPlugin: Plugin | null
|
|
18
|
+
reviews: Review[]
|
|
19
|
+
myPlugins: Plugin[]
|
|
20
|
+
categories: Category[]
|
|
21
|
+
stats: MarketplaceStats | null
|
|
22
|
+
searchQuery: string
|
|
23
|
+
selectedCategory: string | null
|
|
24
|
+
loading: boolean
|
|
25
|
+
error: string | null
|
|
26
|
+
pagination: Pagination
|
|
27
|
+
|
|
28
|
+
fetchPlugins: (page?: number) => Promise<void>
|
|
29
|
+
fetchPlugin: (slug: string) => Promise<void>
|
|
30
|
+
fetchReviews: (slug: string) => Promise<void>
|
|
31
|
+
searchPlugins: (query: string, page?: number) => Promise<void>
|
|
32
|
+
fetchCategories: () => Promise<void>
|
|
33
|
+
fetchStats: () => Promise<void>
|
|
34
|
+
fetchMyPlugins: () => Promise<void>
|
|
35
|
+
createPlugin: (data: CreatePluginInput) => Promise<string | null>
|
|
36
|
+
deletePlugin: (slug: string) => Promise<void>
|
|
37
|
+
submitReview: (slug: string, data: CreateReviewInput) => Promise<void>
|
|
38
|
+
trackInstall: (slug: string) => Promise<void>
|
|
39
|
+
setSearchQuery: (query: string) => void
|
|
40
|
+
setSelectedCategory: (category: string | null) => void
|
|
41
|
+
clearError: () => void
|
|
42
|
+
clearCurrentPlugin: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getErrorMessage(error: unknown): string {
|
|
46
|
+
if (error instanceof Error) return error.message
|
|
47
|
+
return 'Unknown error'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const usePluginStore = create<PluginState>((set, get) => ({
|
|
51
|
+
plugins: [],
|
|
52
|
+
currentPlugin: null,
|
|
53
|
+
reviews: [],
|
|
54
|
+
myPlugins: [],
|
|
55
|
+
categories: [],
|
|
56
|
+
stats: null,
|
|
57
|
+
searchQuery: '',
|
|
58
|
+
selectedCategory: null,
|
|
59
|
+
loading: false,
|
|
60
|
+
error: null,
|
|
61
|
+
pagination: { page: 1, limit: 20, total: 0 },
|
|
62
|
+
|
|
63
|
+
fetchPlugins: async (page = 1) => {
|
|
64
|
+
set({ loading: true, error: null })
|
|
65
|
+
try {
|
|
66
|
+
const { selectedCategory } = get()
|
|
67
|
+
const query: Record<string, string | number | boolean | undefined> = {
|
|
68
|
+
page,
|
|
69
|
+
limit: 20,
|
|
70
|
+
sort: 'newest',
|
|
71
|
+
}
|
|
72
|
+
if (selectedCategory) query.category = selectedCategory
|
|
73
|
+
|
|
74
|
+
const response = await apiClient.api.plugins.$get({ query })
|
|
75
|
+
const result = await response.json()
|
|
76
|
+
if (result.success) {
|
|
77
|
+
set({
|
|
78
|
+
plugins: result.data.plugins,
|
|
79
|
+
pagination: {
|
|
80
|
+
page: result.data.page,
|
|
81
|
+
limit: result.data.limit,
|
|
82
|
+
total: result.data.total,
|
|
83
|
+
},
|
|
84
|
+
loading: false,
|
|
85
|
+
})
|
|
86
|
+
} else {
|
|
87
|
+
set({
|
|
88
|
+
loading: false,
|
|
89
|
+
error: (result as { error?: string }).error ?? 'Failed to fetch plugins',
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
fetchPlugin: async (slug: string) => {
|
|
98
|
+
set({ loading: true, error: null, currentPlugin: null })
|
|
99
|
+
try {
|
|
100
|
+
const response = await apiClient.api.plugins[':slug'].$get({ param: { slug } })
|
|
101
|
+
const result = await response.json()
|
|
102
|
+
if (result.success) {
|
|
103
|
+
set({ currentPlugin: result.data, loading: false })
|
|
104
|
+
} else {
|
|
105
|
+
set({ loading: false, error: (result as { error?: string }).error ?? 'Plugin not found' })
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
fetchReviews: async (slug: string) => {
|
|
113
|
+
try {
|
|
114
|
+
const response = await apiClient.api.plugins[':slug'].reviews.$get({ param: { slug } })
|
|
115
|
+
const result = await response.json()
|
|
116
|
+
if (result.success) {
|
|
117
|
+
set({ reviews: result.data })
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Failed to fetch reviews:', error)
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
searchPlugins: async (query: string, page = 1) => {
|
|
125
|
+
set({ loading: true, error: null })
|
|
126
|
+
try {
|
|
127
|
+
const { selectedCategory } = get()
|
|
128
|
+
|
|
129
|
+
const response = await apiClient.api.plugins.search.$get({
|
|
130
|
+
query: {
|
|
131
|
+
q: query,
|
|
132
|
+
page,
|
|
133
|
+
limit: 20,
|
|
134
|
+
...(selectedCategory ? { category: selectedCategory } : {}),
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
const result = await response.json()
|
|
138
|
+
if (result.success) {
|
|
139
|
+
set({
|
|
140
|
+
plugins: result.data.plugins,
|
|
141
|
+
pagination: {
|
|
142
|
+
page: result.data.page,
|
|
143
|
+
limit: result.data.limit,
|
|
144
|
+
total: result.data.total,
|
|
145
|
+
},
|
|
146
|
+
loading: false,
|
|
147
|
+
})
|
|
148
|
+
} else {
|
|
149
|
+
set({ loading: false, error: (result as { error?: string }).error ?? 'Search failed' })
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
fetchCategories: async () => {
|
|
157
|
+
try {
|
|
158
|
+
const response = await apiClient.api.categories.$get()
|
|
159
|
+
const result = await response.json()
|
|
160
|
+
if (result.success) {
|
|
161
|
+
set({ categories: result.data })
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Failed to fetch categories:', error)
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
fetchStats: async () => {
|
|
169
|
+
try {
|
|
170
|
+
const response = await apiClient.api.stats.$get()
|
|
171
|
+
const result = await response.json()
|
|
172
|
+
if (result.success) {
|
|
173
|
+
set({ stats: result.data })
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Failed to fetch stats:', error)
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
fetchMyPlugins: async () => {
|
|
181
|
+
set({ loading: true, error: null })
|
|
182
|
+
try {
|
|
183
|
+
const response = await apiClient.api.plugins.mine.$get()
|
|
184
|
+
const result = await response.json()
|
|
185
|
+
if (result.success) {
|
|
186
|
+
set({ myPlugins: result.data, loading: false })
|
|
187
|
+
} else {
|
|
188
|
+
set({
|
|
189
|
+
loading: false,
|
|
190
|
+
error: (result as { error?: string }).error ?? 'Failed to fetch your plugins',
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
createPlugin: async (data: CreatePluginInput): Promise<string | null> => {
|
|
199
|
+
set({ loading: true, error: null })
|
|
200
|
+
try {
|
|
201
|
+
const response = await apiClient.api.plugins.$post({ json: data })
|
|
202
|
+
const result = await response.json()
|
|
203
|
+
if (result.success) {
|
|
204
|
+
set(state => ({
|
|
205
|
+
myPlugins: [result.data, ...state.myPlugins],
|
|
206
|
+
loading: false,
|
|
207
|
+
}))
|
|
208
|
+
return result.data.slug
|
|
209
|
+
}
|
|
210
|
+
set({
|
|
211
|
+
loading: false,
|
|
212
|
+
error: (result as { error?: string }).error ?? 'Failed to create plugin',
|
|
213
|
+
})
|
|
214
|
+
return null
|
|
215
|
+
} catch (error) {
|
|
216
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
deletePlugin: async (slug: string) => {
|
|
222
|
+
set({ loading: true, error: null })
|
|
223
|
+
try {
|
|
224
|
+
const response = await apiClient.api.plugins[':slug'].$delete({ param: { slug } })
|
|
225
|
+
const result = await response.json()
|
|
226
|
+
if (result.success) {
|
|
227
|
+
set(state => ({
|
|
228
|
+
myPlugins: state.myPlugins.filter(p => p.slug !== slug),
|
|
229
|
+
plugins: state.plugins.filter(p => p.slug !== slug),
|
|
230
|
+
loading: false,
|
|
231
|
+
}))
|
|
232
|
+
} else {
|
|
233
|
+
set({
|
|
234
|
+
loading: false,
|
|
235
|
+
error: (result as { error?: string }).error ?? 'Failed to delete plugin',
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
submitReview: async (slug: string, data: CreateReviewInput) => {
|
|
244
|
+
set({ loading: true, error: null })
|
|
245
|
+
try {
|
|
246
|
+
const response = await apiClient.api.plugins[':slug'].reviews.$post({
|
|
247
|
+
param: { slug },
|
|
248
|
+
json: data,
|
|
249
|
+
})
|
|
250
|
+
const result = await response.json()
|
|
251
|
+
if (result.success) {
|
|
252
|
+
set(state => ({
|
|
253
|
+
reviews: [result.data, ...state.reviews],
|
|
254
|
+
loading: false,
|
|
255
|
+
}))
|
|
256
|
+
} else {
|
|
257
|
+
set({
|
|
258
|
+
loading: false,
|
|
259
|
+
error: (result as { error?: string }).error ?? 'Failed to submit review',
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
set({ error: getErrorMessage(error), loading: false })
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
trackInstall: async (slug: string) => {
|
|
268
|
+
try {
|
|
269
|
+
await apiClient.api.plugins[':slug'].install.$post({ param: { slug } })
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Failed to track install:', error)
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
setSearchQuery: (query: string) => set({ searchQuery: query }),
|
|
276
|
+
setSelectedCategory: (category: string | null) => set({ selectedCategory: category }),
|
|
277
|
+
clearError: () => set({ error: null }),
|
|
278
|
+
clearCurrentPlugin: () => set({ currentPlugin: null, reviews: [] }),
|
|
279
|
+
}))
|
|
@@ -30,9 +30,7 @@ export const useTodoStore = create<TodoState>(set => ({
|
|
|
30
30
|
const response = await apiClient.api.todos.$get()
|
|
31
31
|
const result = await response.json()
|
|
32
32
|
if (result.success) {
|
|
33
|
-
|
|
34
|
-
const items = Array.isArray(data) ? data : data.items || []
|
|
35
|
-
set({ todos: items, loading: false })
|
|
33
|
+
set({ todos: result.data, loading: false })
|
|
36
34
|
} else {
|
|
37
35
|
set({ error: result.error, loading: false })
|
|
38
36
|
}
|
|
@@ -162,9 +160,7 @@ export const useTodoStore = create<TodoState>(set => ({
|
|
|
162
160
|
if (result.success) {
|
|
163
161
|
set(state => {
|
|
164
162
|
const newAttachments = new Map(state.attachments)
|
|
165
|
-
|
|
166
|
-
const items = Array.isArray(data) ? data : data.items || []
|
|
167
|
-
newAttachments.set(todoId, items)
|
|
163
|
+
newAttachments.set(todoId, result.data)
|
|
168
164
|
return { attachments: newAttachments }
|
|
169
165
|
})
|
|
170
166
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @framework-baseline dd5cd4fe46320b03
|
|
3
|
+
* @framework-modify
|
|
4
|
+
* @reason 移除未使用的 vi import,修复 TypeScript strict 检查
|
|
5
|
+
* @impact 不影响功能,仅清理代码
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
9
|
+
import { isISRRoute, generateCacheKey, createISRCache, type ISRCache } from '@server/core/isr-cache'
|
|
10
|
+
|
|
11
|
+
describe('isISRRoute', () => {
|
|
12
|
+
it('matches static ISR routes', () => {
|
|
13
|
+
expect(isISRRoute('/')).toBe(true)
|
|
14
|
+
expect(isISRRoute('/todos')).toBe(true)
|
|
15
|
+
expect(isISRRoute('/content')).toBe(true)
|
|
16
|
+
expect(isISRRoute('/notifications')).toBe(true)
|
|
17
|
+
expect(isISRRoute('/websocket')).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('matches dynamic content routes', () => {
|
|
21
|
+
expect(isISRRoute('/content/123')).toBe(true)
|
|
22
|
+
expect(isISRRoute('/content/content-456')).toBe(true)
|
|
23
|
+
expect(isISRRoute('/content/abc-def')).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('does not match non-ISR routes', () => {
|
|
27
|
+
expect(isISRRoute('/api/todos')).toBe(false)
|
|
28
|
+
expect(isISRRoute('/api/health')).toBe(false)
|
|
29
|
+
expect(isISRRoute('/assets/main.js')).toBe(false)
|
|
30
|
+
expect(isISRRoute('/admin/dashboard')).toBe(false)
|
|
31
|
+
expect(isISRRoute('/health')).toBe(false)
|
|
32
|
+
expect(isISRRoute('/vite.svg')).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('generateCacheKey', () => {
|
|
37
|
+
it('normalizes root path', () => {
|
|
38
|
+
expect(generateCacheKey('/')).toBe('isr:/index')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('generates keys for regular paths', () => {
|
|
42
|
+
expect(generateCacheKey('/todos')).toBe('isr:/todos')
|
|
43
|
+
expect(generateCacheKey('/content/123')).toBe('isr:/content/123')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('removes trailing slash', () => {
|
|
47
|
+
expect(generateCacheKey('/todos/')).toBe('isr:/todos')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('ISRCache (memory)', () => {
|
|
52
|
+
let cache: ISRCache
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
cache = createISRCache({ maxAge: 1, staleWhileRevalidate: 1 })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns miss for uncached routes', async () => {
|
|
59
|
+
const result = await cache.lookup('/todos')
|
|
60
|
+
expect(result.status).toBe('miss')
|
|
61
|
+
expect(result.html).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('stores and retrieves pages', async () => {
|
|
65
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
66
|
+
const result = await cache.lookup('/todos')
|
|
67
|
+
expect(result.status).toBe('fresh')
|
|
68
|
+
expect(result.html).toBe('<html>todos</html>')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('serves stale content after maxAge', async () => {
|
|
72
|
+
cache = createISRCache({ maxAge: 0, staleWhileRevalidate: 60 })
|
|
73
|
+
|
|
74
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
75
|
+
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
77
|
+
|
|
78
|
+
const result = await cache.lookup('/todos')
|
|
79
|
+
expect(result.status).toBe('stale')
|
|
80
|
+
expect(result.html).toBe('<html>todos</html>')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns miss after maxAge + staleWhileRevalidate', async () => {
|
|
84
|
+
cache = createISRCache({ maxAge: 0, staleWhileRevalidate: 0 })
|
|
85
|
+
|
|
86
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
87
|
+
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
89
|
+
|
|
90
|
+
const result = await cache.lookup('/todos')
|
|
91
|
+
expect(result.status).toBe('miss')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('purges specific pages', async () => {
|
|
95
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
96
|
+
await cache.store('/content', '<html>content</html>')
|
|
97
|
+
|
|
98
|
+
await cache.purge('/todos')
|
|
99
|
+
|
|
100
|
+
expect((await cache.lookup('/todos')).status).toBe('miss')
|
|
101
|
+
expect((await cache.lookup('/content')).status).toBe('fresh')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('purges by pattern', async () => {
|
|
105
|
+
await cache.store('/content', '<html>list</html>')
|
|
106
|
+
await cache.store('/content/123', '<html>detail 123</html>')
|
|
107
|
+
await cache.store('/content/456', '<html>detail 456</html>')
|
|
108
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
109
|
+
|
|
110
|
+
await cache.purgePattern('isr:/content/*')
|
|
111
|
+
|
|
112
|
+
expect((await cache.lookup('/content')).status).toBe('fresh')
|
|
113
|
+
expect((await cache.lookup('/content/123')).status).toBe('miss')
|
|
114
|
+
expect((await cache.lookup('/content/456')).status).toBe('miss')
|
|
115
|
+
expect((await cache.lookup('/todos')).status).toBe('fresh')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @framework-baseline 202ba4a1c53d59e3
|
|
3
|
+
* @framework-modify
|
|
4
|
+
* @reason 移除未使用的 vi import,修复 TypeScript strict 检查
|
|
5
|
+
* @impact 不影响功能,仅清理代码
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
purgePage,
|
|
11
|
+
purgeContentPages,
|
|
12
|
+
purgeAllPages,
|
|
13
|
+
setISRCache,
|
|
14
|
+
getISRCache,
|
|
15
|
+
} from '@server/core/isr-invalidation'
|
|
16
|
+
import { createISRCache } from '@server/core/isr-cache'
|
|
17
|
+
|
|
18
|
+
describe('isr-invalidation', () => {
|
|
19
|
+
it('returns null cache when not set', () => {
|
|
20
|
+
expect(getISRCache()).toBeNull()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('sets and gets cache', () => {
|
|
24
|
+
const cache = createISRCache()
|
|
25
|
+
setISRCache(cache)
|
|
26
|
+
expect(getISRCache()).toBe(cache)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('with cache set', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
const cache = createISRCache()
|
|
32
|
+
setISRCache(cache)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('purgePage purges specific page', async () => {
|
|
36
|
+
const cache = getISRCache()!
|
|
37
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
38
|
+
expect((await cache.lookup('/todos')).status).toBe('fresh')
|
|
39
|
+
|
|
40
|
+
await purgePage('/todos')
|
|
41
|
+
expect((await cache.lookup('/todos')).status).toBe('miss')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('purgeContentPages purges content list and details', async () => {
|
|
45
|
+
const cache = getISRCache()!
|
|
46
|
+
await cache.store('/content', '<html>list</html>')
|
|
47
|
+
await cache.store('/content/123', '<html>detail</html>')
|
|
48
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
49
|
+
|
|
50
|
+
await purgeContentPages()
|
|
51
|
+
|
|
52
|
+
expect((await cache.lookup('/content')).status).toBe('miss')
|
|
53
|
+
expect((await cache.lookup('/content/123')).status).toBe('miss')
|
|
54
|
+
expect((await cache.lookup('/todos')).status).toBe('fresh')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('purgeAllPages purges everything', async () => {
|
|
58
|
+
const cache = getISRCache()!
|
|
59
|
+
await cache.store('/', '<html>home</html>')
|
|
60
|
+
await cache.store('/todos', '<html>todos</html>')
|
|
61
|
+
await cache.store('/content', '<html>content</html>')
|
|
62
|
+
await cache.store('/content/123', '<html>detail</html>')
|
|
63
|
+
|
|
64
|
+
await purgeAllPages()
|
|
65
|
+
|
|
66
|
+
expect((await cache.lookup('/')).status).toBe('miss')
|
|
67
|
+
expect((await cache.lookup('/todos')).status).toBe('miss')
|
|
68
|
+
expect((await cache.lookup('/content')).status).toBe('miss')
|
|
69
|
+
expect((await cache.lookup('/content/123')).status).toBe('miss')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @framework-baseline 351df7aadf410369
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { renderPage, matchRoute } from '@server/core/ssr-renderer'
|
|
7
|
+
|
|
8
|
+
describe('matchRoute', () => {
|
|
9
|
+
it('matches static routes', () => {
|
|
10
|
+
expect(matchRoute('/')).not.toBeNull()
|
|
11
|
+
expect(matchRoute('/todos')).not.toBeNull()
|
|
12
|
+
expect(matchRoute('/notifications')).not.toBeNull()
|
|
13
|
+
expect(matchRoute('/websocket')).not.toBeNull()
|
|
14
|
+
expect(matchRoute('/content')).not.toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('matches dynamic content routes', () => {
|
|
18
|
+
const result = matchRoute('/content/123')
|
|
19
|
+
expect(result).not.toBeNull()
|
|
20
|
+
expect(result!.params.id).toBe('123')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('extracts params from dynamic routes', () => {
|
|
24
|
+
const result = matchRoute('/content/content-42')
|
|
25
|
+
expect(result).not.toBeNull()
|
|
26
|
+
expect(result!.params.id).toBe('content-42')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns null for non-matching routes', () => {
|
|
30
|
+
expect(matchRoute('/api/todos')).toBeNull()
|
|
31
|
+
expect(matchRoute('/admin')).toBeNull()
|
|
32
|
+
expect(matchRoute('/unknown')).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('renderPage', () => {
|
|
37
|
+
it('renders todos page with correct meta', async () => {
|
|
38
|
+
const result = await renderPage('/todos')
|
|
39
|
+
expect(result.status).toBe(200)
|
|
40
|
+
expect(result.html).toContain('<title>Todo List - Biomimic App</title>')
|
|
41
|
+
expect(result.html).toContain('meta name="description"')
|
|
42
|
+
expect(result.html).toContain('meta property="og:title"')
|
|
43
|
+
expect(result.html).toContain('<div id="root"></div>')
|
|
44
|
+
expect(result.html).toContain('window.__SSR_DATA__')
|
|
45
|
+
expect(result.headers['X-ISR-Rendered']).toBe('true')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('renders root path', async () => {
|
|
49
|
+
const result = await renderPage('/')
|
|
50
|
+
expect(result.status).toBe(200)
|
|
51
|
+
expect(result.html).toContain('<title>')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders content list page', async () => {
|
|
55
|
+
const result = await renderPage('/content')
|
|
56
|
+
expect(result.status).toBe(200)
|
|
57
|
+
expect(result.html).toContain('内容中心')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('renders content detail page', async () => {
|
|
61
|
+
const result = await renderPage('/content/123')
|
|
62
|
+
expect(result.status).toBe(200)
|
|
63
|
+
expect(result.html).toContain('window.__SSR_DATA__')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('renders fallback for unknown routes', async () => {
|
|
67
|
+
const result = await renderPage('/unknown-page')
|
|
68
|
+
expect(result.status).toBe(200)
|
|
69
|
+
expect(result.html).toContain('<div id="root"></div>')
|
|
70
|
+
expect(result.html).toContain('Biomimic App')
|
|
71
|
+
expect(result.headers['X-ISR-Rendered']).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('produces valid HTML structure', async () => {
|
|
75
|
+
const result = await renderPage('/todos')
|
|
76
|
+
expect(result.html).toContain('<!DOCTYPE html>')
|
|
77
|
+
expect(result.html).toContain('<html lang="en">')
|
|
78
|
+
expect(result.html).toContain('</html>')
|
|
79
|
+
expect(result.html).toContain('<head>')
|
|
80
|
+
expect(result.html).toContain('</head>')
|
|
81
|
+
expect(result.html).toContain('<body>')
|
|
82
|
+
expect(result.html).toContain('</body>')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('includes generator meta tag', async () => {
|
|
86
|
+
const result = await renderPage('/todos')
|
|
87
|
+
expect(result.html).toContain('name="generator" content="ISR"')
|
|
88
|
+
})
|
|
89
|
+
})
|