create-fullstack-scaffold 0.5.6 → 0.6.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 +80 -22
- package/dist/cli/index.js.map +1 -1
- package/package.json +12 -12
- package/template/package.json +10 -14
- package/template/src/cli/modules/tenant/index.ts +89 -0
- package/template/src/client/entry-server.tsx +7 -1
- package/template/src/client/main.tsx +0 -8
- package/template/src/client/pages/ContentDetailPage.tsx +1 -1
- package/template/src/client/pages/ContentListPage.tsx +1 -1
- package/template/src/client/pages/TodoPage.tsx +20 -4
- package/template/src/client/ssr-pages.ts +10 -21
- package/template/src/server/db/init.ts +0 -52
- package/template/src/server/db/schema/index.ts +3 -0
- package/template/src/server/db/schema/tenant-invitations.ts +33 -0
- package/template/src/server/db/schema/tenant-members.ts +38 -0
- package/template/src/server/db/schema/tenant-roles.ts +32 -0
- package/template/src/server/db/schema/todos.ts +4 -0
- package/template/src/server/db/test-setup.ts +128 -0
- package/template/src/server/entries/node.ts +38 -19
- package/template/src/server/index.ts +12 -0
- package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +55 -9
- package/template/src/server/middleware/auth.ts +17 -19
- package/template/src/server/middleware/tenant-isolation.ts +4 -2
- package/template/src/server/module-auth/module.ts +2 -1
- package/template/src/server/module-auth/services/auth-service.ts +39 -0
- package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +97 -0
- package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +201 -22
- package/template/src/server/module-tenant/module.ts +1 -1
- package/template/src/server/module-tenant/routes/tenant-routes.ts +339 -2
- package/template/src/server/module-tenant/services/tenant-service.ts +581 -17
- package/template/src/server/module-todos/routes/todos-routes.ts +10 -2
- package/template/src/server/module-todos/services/todo-service.ts +12 -2
- package/template/src/server/ssr-bridge.ts +10 -0
- package/template/src/server/utils/__tests__/captcha.test.ts +6 -4
- package/template/src/server/utils/id-helpers.ts +13 -0
- package/template/src/shared/modules/index.ts +29 -6
- package/template/src/shared/modules/tenant/index.ts +30 -0
- package/template/src/shared/modules/tenant/permissions.ts +54 -0
- package/template/src/shared/modules/tenant/role-templates.ts +66 -0
- package/template/src/shared/modules/tenant/schemas.ts +113 -1
- package/template/src/shared/modules/todos/schemas.ts +1 -0
- package/template/src/shared/schemas/index.ts +28 -0
- package/template/src/tenant/App.tsx +30 -23
- package/template/src/tenant/components/TenantGuard.tsx +12 -2
- package/template/src/tenant/layouts/Header.tsx +6 -2
- package/template/src/tenant/pages/InviteAcceptPage.tsx +97 -0
- package/template/src/tenant/pages/LoginPage.tsx +82 -0
- package/template/src/tenant/pages/SubscriptionPage.tsx +32 -55
- package/template/src/tenant/pages/UsersPage.tsx +134 -88
- package/template/src/tenant/services/tenantApi.ts +61 -0
- package/template/src/tenant/stores/tenantStore.ts +172 -49
- package/template/src/test/setup-db-path.ts +20 -0
- package/template/vite.config.ts +4 -73
- package/template/vitest.config.ts +3 -1
|
@@ -6,32 +6,44 @@ import type {
|
|
|
6
6
|
CreateTodoInput,
|
|
7
7
|
UpdateTodoInput,
|
|
8
8
|
Topic,
|
|
9
|
+
TenantMember,
|
|
10
|
+
TenantRole,
|
|
9
11
|
} from '@shared/schemas'
|
|
12
|
+
import { api, setToken, setSlug, getToken } from '../services/tenantApi'
|
|
10
13
|
|
|
11
14
|
interface TenantState {
|
|
12
15
|
isAuthenticated: boolean
|
|
13
16
|
currentTenant: Tenant | null
|
|
14
17
|
loading: boolean
|
|
15
|
-
users:
|
|
18
|
+
users: TenantMember[]
|
|
19
|
+
roles: TenantRole[]
|
|
16
20
|
todos: Todo[]
|
|
17
21
|
topics: Topic[]
|
|
18
22
|
stats: TenantStats
|
|
19
|
-
subscription:
|
|
23
|
+
subscription: TenantSubscription | null
|
|
20
24
|
|
|
25
|
+
login: (
|
|
26
|
+
account: string,
|
|
27
|
+
password: string
|
|
28
|
+
) => Promise<{ ok: boolean; hasTenant: boolean; error?: string }>
|
|
29
|
+
/** 有 token 无租户上下文时(如邀请接受后)从 /tenants/mine 恢复 */
|
|
30
|
+
restoreFromToken: () => Promise<void>
|
|
31
|
+
logout: () => void
|
|
21
32
|
setCurrentTenant: (tenant: Tenant | null) => void
|
|
22
33
|
fetchCurrentTenant: (slug: string) => Promise<void>
|
|
23
34
|
fetchUsers: () => Promise<void>
|
|
24
|
-
|
|
25
|
-
updateUser: (userId:
|
|
26
|
-
deleteUser: (userId:
|
|
35
|
+
inviteUser: (email: string, roleId: string) => Promise<boolean>
|
|
36
|
+
updateUser: (userId: string, data: { roleId: string }) => Promise<boolean>
|
|
37
|
+
deleteUser: (userId: string) => Promise<boolean>
|
|
38
|
+
fetchRoles: () => Promise<void>
|
|
27
39
|
fetchTodos: () => Promise<void>
|
|
28
40
|
createTodo: (data: CreateTodoInput) => Promise<boolean>
|
|
29
41
|
updateTodo: (todoId: number, data: UpdateTodoInput) => Promise<boolean>
|
|
30
42
|
deleteTodo: (todoId: number) => Promise<boolean>
|
|
31
43
|
fetchTopics: () => Promise<void>
|
|
32
44
|
createTopic: (data: unknown) => Promise<boolean>
|
|
33
|
-
updateTopic: (topicId: number, data: unknown) => Promise<boolean>
|
|
34
|
-
deleteTopic: (topicId: number) => Promise<boolean>
|
|
45
|
+
updateTopic: (topicId: string | number, data: unknown) => Promise<boolean>
|
|
46
|
+
deleteTopic: (topicId: string | number) => Promise<boolean>
|
|
35
47
|
fetchSubscription: () => Promise<void>
|
|
36
48
|
updateTenant: (tenantId: number, data: UpdateTenantInput) => Promise<boolean>
|
|
37
49
|
fetchStats: () => Promise<void>
|
|
@@ -47,11 +59,19 @@ interface TenantStats {
|
|
|
47
59
|
monthlyRevenue: number
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
/** 订阅信息由租户 plan/maxUsers 派生(P4 在服务端执行配额) */
|
|
63
|
+
interface TenantSubscription {
|
|
64
|
+
plan: string
|
|
65
|
+
maxUsers: number
|
|
66
|
+
currentUsers: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const useTenantStore = create<TenantState>((set, getState) => ({
|
|
70
|
+
isAuthenticated: !!getToken(),
|
|
52
71
|
currentTenant: null,
|
|
53
72
|
loading: false,
|
|
54
73
|
users: [],
|
|
74
|
+
roles: [],
|
|
55
75
|
todos: [],
|
|
56
76
|
topics: [],
|
|
57
77
|
stats: {
|
|
@@ -62,82 +82,185 @@ export const useTenantStore = create<TenantState>(set => ({
|
|
|
62
82
|
},
|
|
63
83
|
subscription: null,
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
// 登录 = 平台认证 + mine 选定租户(取第一个成员租户)
|
|
86
|
+
login: async (account, password) => {
|
|
87
|
+
set({ loading: true })
|
|
88
|
+
try {
|
|
89
|
+
const loginRes = await api<{ token: string }>('/auth/login', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: { account, password },
|
|
92
|
+
})
|
|
93
|
+
if (!loginRes.success || !loginRes.data?.token) {
|
|
94
|
+
return { ok: false, hasTenant: false, error: 'Invalid credentials' }
|
|
95
|
+
}
|
|
96
|
+
setToken(loginRes.data.token)
|
|
66
97
|
|
|
67
|
-
|
|
98
|
+
const mineRes = await api<Tenant[]>('/tenants/mine')
|
|
99
|
+
// 认证已成功——无租户账号保留 token(受邀新用户需登录态接受邀请),
|
|
100
|
+
// 由调用方引导回邀请落地页而非硬拒
|
|
101
|
+
if (!mineRes.success || !mineRes.data || mineRes.data.length === 0) {
|
|
102
|
+
return { ok: true, hasTenant: false }
|
|
103
|
+
}
|
|
68
104
|
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
const tenant = mineRes.data[0]
|
|
106
|
+
setSlug(tenant.slug)
|
|
107
|
+
set({ isAuthenticated: true, currentTenant: tenant })
|
|
108
|
+
return { ok: true, hasTenant: true }
|
|
109
|
+
} finally {
|
|
110
|
+
set({ loading: false })
|
|
111
|
+
}
|
|
71
112
|
},
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
114
|
+
restoreFromToken: async () => {
|
|
115
|
+
if (!getToken()) return
|
|
116
|
+
const mineRes = await api<Tenant[]>('/tenants/mine')
|
|
117
|
+
if (mineRes.success && mineRes.data && mineRes.data.length > 0) {
|
|
118
|
+
const tenant = mineRes.data[0]
|
|
119
|
+
setSlug(tenant.slug)
|
|
120
|
+
set({ isAuthenticated: true, currentTenant: tenant })
|
|
121
|
+
}
|
|
76
122
|
},
|
|
77
123
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
124
|
+
logout: () => {
|
|
125
|
+
setToken(null)
|
|
126
|
+
setSlug(null)
|
|
127
|
+
set({ isAuthenticated: false, currentTenant: null, users: [], todos: [] })
|
|
81
128
|
},
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
130
|
+
setCurrentTenant: tenant => set({ currentTenant: tenant }),
|
|
131
|
+
|
|
132
|
+
setLoading: loading => set({ loading }),
|
|
133
|
+
|
|
134
|
+
fetchCurrentTenant: async slug => {
|
|
135
|
+
if (!getToken()) {
|
|
136
|
+
set({ isAuthenticated: false })
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
const res = await api<Tenant>(`/tenants/slug/${slug}`)
|
|
140
|
+
if (res.success && res.data) {
|
|
141
|
+
set({ isAuthenticated: true, currentTenant: res.data })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
// 仅认证确实失效(401)才回登录;429/网络错误按瞬态跳过,
|
|
145
|
+
// 否则限流抖动会把已登录用户弹出去
|
|
146
|
+
if (res.status === 401) {
|
|
147
|
+
set({ isAuthenticated: false })
|
|
148
|
+
}
|
|
86
149
|
},
|
|
87
150
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
151
|
+
fetchRoles: async () => {
|
|
152
|
+
const tenant = getState().currentTenant
|
|
153
|
+
if (!tenant) return
|
|
154
|
+
const res = await api<TenantRole[]>(`/tenants/${tenant.id}/roles`)
|
|
155
|
+
if (res.success && res.data) set({ roles: res.data })
|
|
91
156
|
},
|
|
92
157
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
158
|
+
fetchUsers: async () => {
|
|
159
|
+
const tenant = getState().currentTenant
|
|
160
|
+
if (!tenant) return
|
|
161
|
+
const res = await api<TenantMember[]>(`/tenants/${tenant.id}/members`)
|
|
162
|
+
if (res.success && res.data) set({ users: res.data })
|
|
96
163
|
},
|
|
97
164
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return false
|
|
165
|
+
inviteUser: async (email, roleId) => {
|
|
166
|
+
const tenant = getState().currentTenant
|
|
167
|
+
if (!tenant) return false
|
|
168
|
+
const res = await api(`/tenants/${tenant.id}/members/invite`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body: { email, roleId },
|
|
171
|
+
})
|
|
172
|
+
return res.success
|
|
101
173
|
},
|
|
102
174
|
|
|
103
|
-
|
|
104
|
-
|
|
175
|
+
updateUser: async (memberId, data) => {
|
|
176
|
+
const tenant = getState().currentTenant
|
|
177
|
+
if (!tenant) return false
|
|
178
|
+
const res = await api(`/tenants/${tenant.id}/members/${memberId}`, {
|
|
179
|
+
method: 'PUT',
|
|
180
|
+
body: data,
|
|
181
|
+
})
|
|
182
|
+
return res.success
|
|
105
183
|
},
|
|
106
184
|
|
|
107
|
-
|
|
108
|
-
|
|
185
|
+
deleteUser: async memberId => {
|
|
186
|
+
const tenant = getState().currentTenant
|
|
187
|
+
if (!tenant) return false
|
|
188
|
+
const res = await api(`/tenants/${tenant.id}/members/${memberId}`, { method: 'DELETE' })
|
|
189
|
+
return res.success
|
|
109
190
|
},
|
|
110
191
|
|
|
111
192
|
fetchTodos: async () => {
|
|
112
|
-
|
|
193
|
+
const res = await api<{ todos: Todo[]; total: number }>('/todos?limit=100')
|
|
194
|
+
if (res.success && res.data) set({ todos: res.data.todos })
|
|
113
195
|
},
|
|
114
196
|
|
|
115
|
-
createTodo: async
|
|
116
|
-
|
|
117
|
-
|
|
197
|
+
createTodo: async data => {
|
|
198
|
+
const res = await api<Todo>('/todos', { method: 'POST', body: data })
|
|
199
|
+
if (res.success) await getState().fetchTodos()
|
|
200
|
+
return res.success
|
|
118
201
|
},
|
|
119
202
|
|
|
120
|
-
updateTodo: async () => {
|
|
121
|
-
|
|
122
|
-
|
|
203
|
+
updateTodo: async (todoId, data) => {
|
|
204
|
+
const res = await api<Todo>(`/todos/${todoId}`, { method: 'PUT', body: data })
|
|
205
|
+
if (res.success) await getState().fetchTodos()
|
|
206
|
+
return res.success
|
|
123
207
|
},
|
|
124
208
|
|
|
125
|
-
deleteTodo: async
|
|
126
|
-
|
|
127
|
-
|
|
209
|
+
deleteTodo: async todoId => {
|
|
210
|
+
const res = await api(`/todos/${todoId}`, { method: 'DELETE' })
|
|
211
|
+
if (res.success) await getState().fetchTodos()
|
|
212
|
+
return res.success
|
|
128
213
|
},
|
|
129
214
|
|
|
130
215
|
fetchTopics: async () => {
|
|
131
|
-
|
|
216
|
+
const res = await api<Topic[]>('/public/topics?limit=50')
|
|
217
|
+
if (res.success && res.data) set({ topics: res.data })
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
createTopic: async () => false,
|
|
221
|
+
|
|
222
|
+
updateTopic: async () => false,
|
|
223
|
+
|
|
224
|
+
deleteTopic: async () => false,
|
|
225
|
+
|
|
226
|
+
fetchSubscription: async () => {
|
|
227
|
+
const tenant = getState().currentTenant
|
|
228
|
+
if (!tenant) return
|
|
229
|
+
const members = await api<TenantMember[]>(`/tenants/${tenant.id}/members`)
|
|
230
|
+
set({
|
|
231
|
+
subscription: {
|
|
232
|
+
plan: tenant.plan,
|
|
233
|
+
maxUsers: tenant.maxUsers,
|
|
234
|
+
currentUsers: members.data?.length ?? 0,
|
|
235
|
+
},
|
|
236
|
+
})
|
|
132
237
|
},
|
|
133
238
|
|
|
134
|
-
updateTenant: async () => {
|
|
135
|
-
|
|
239
|
+
updateTenant: async (tenantId, data) => {
|
|
240
|
+
const res = await api<Tenant>(`/tenants/${tenantId}`, { method: 'PUT', body: data })
|
|
241
|
+
if (res.success && res.data) {
|
|
242
|
+
set({ currentTenant: res.data })
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
136
245
|
return false
|
|
137
246
|
},
|
|
138
247
|
|
|
139
248
|
fetchStats: async () => {
|
|
140
|
-
|
|
249
|
+
const tenant = getState().currentTenant
|
|
250
|
+
if (!tenant) return
|
|
251
|
+
const [members, todos] = await Promise.all([
|
|
252
|
+
api<TenantMember[]>(`/tenants/${tenant.id}/members`),
|
|
253
|
+
api<{ todos: Todo[]; total: number }>('/todos?limit=100'),
|
|
254
|
+
])
|
|
255
|
+
const activeTodos = (todos.data?.todos ?? []).filter(t => t.status !== 'completed').length
|
|
256
|
+
set({
|
|
257
|
+
stats: {
|
|
258
|
+
totalUsers: members.data?.length ?? 0,
|
|
259
|
+
activeTodos,
|
|
260
|
+
contentCount: 0,
|
|
261
|
+
monthlyRevenue: 0,
|
|
262
|
+
},
|
|
263
|
+
})
|
|
141
264
|
},
|
|
142
265
|
|
|
143
266
|
startLoading: (_text?: string) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitest setupFile:把测试库从 :memory: 换成每进程独立的临时文件。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不能用 :memory:——@libsql/client 的 transaction() 会置空内部连接
|
|
5
|
+
* (#db = null,事务跑在旧连接上),下一次普通查询惰性新建连接;文件库
|
|
6
|
+
* 会重开同一文件,而 :memory: 会得到一个全新的空库(后续所有查询
|
|
7
|
+
* "no such table")。租户开通等真实事务依赖此修正。
|
|
8
|
+
*/
|
|
9
|
+
import { tmpdir } from 'os'
|
|
10
|
+
import { join } from 'path'
|
|
11
|
+
import { existsSync, rmSync } from 'fs'
|
|
12
|
+
|
|
13
|
+
if (!process.env.SQLITE_PATH || process.env.SQLITE_PATH === ':memory:') {
|
|
14
|
+
const dbPath = join(tmpdir(), `cfs-test-${process.pid}.db`)
|
|
15
|
+
// 旧运行可能残留旧 DDL 的库(IF NOT EXISTS 不会升级表结构)——先删干净
|
|
16
|
+
for (const f of [dbPath, `${dbPath}-journal`, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
17
|
+
if (existsSync(f)) rmSync(f)
|
|
18
|
+
}
|
|
19
|
+
process.env.SQLITE_PATH = dbPath
|
|
20
|
+
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import { existsSync } from 'fs'
|
|
3
2
|
import { defineConfig, type Plugin } from 'vite'
|
|
4
3
|
import devServer from '@hono/vite-dev-server'
|
|
5
4
|
import { websocketPlugin, dbPlugin } from './vite-plugins'
|
|
@@ -20,64 +19,9 @@ if (process.env.ANALYZE === 'true') {
|
|
|
20
19
|
// rollup-plugin-visualizer not installed — run: npm install -D rollup-plugin-visualizer
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const prerenderMod = await import('@prerenderer/rollup-plugin')
|
|
28
|
-
prerender = prerenderMod.default
|
|
29
|
-
const rendererMod = await import('@prerenderer/renderer-puppeteer')
|
|
30
|
-
puppeteerRenderer = rendererMod.default
|
|
31
|
-
// Verify Chrome is actually available at runtime
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
33
|
-
// @ts-ignore — puppeteer is an optional dependency, may not be installed
|
|
34
|
-
const pupMod = await import('puppeteer')
|
|
35
|
-
const chromePath: string = await pupMod.executablePath()
|
|
36
|
-
if (!existsSync(chromePath)) {
|
|
37
|
-
prerender = undefined
|
|
38
|
-
puppeteerRenderer = undefined
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// puppeteer/Chrome not available — prerendering disabled
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function getPrerenderRoutes(): Promise<string[]> {
|
|
45
|
-
const staticRoutes = ['/', '/todos', '/notifications', '/websocket', '/content']
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const { createClient } = await import('@libsql/client')
|
|
49
|
-
const dbCandidates = [
|
|
50
|
-
path.resolve(process.cwd(), 'data/production.db'),
|
|
51
|
-
path.resolve(process.cwd(), 'data/app.db'),
|
|
52
|
-
path.resolve(process.cwd(), 'data/development.db'),
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
for (const dbPath of dbCandidates) {
|
|
56
|
-
if (!existsSync(dbPath)) continue
|
|
57
|
-
|
|
58
|
-
const client = createClient({ url: `file:${dbPath}` })
|
|
59
|
-
try {
|
|
60
|
-
const rs = await client.execute(
|
|
61
|
-
"SELECT id FROM contents WHERE status = 'published' ORDER BY created_at DESC"
|
|
62
|
-
)
|
|
63
|
-
client.close()
|
|
64
|
-
|
|
65
|
-
if (rs.rows.length > 0) {
|
|
66
|
-
const contentRoutes = rs.rows.map(row => `/content/content-${row.id}`)
|
|
67
|
-
return [...staticRoutes, ...contentRoutes]
|
|
68
|
-
}
|
|
69
|
-
} catch {
|
|
70
|
-
client.close()
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
// fallback
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return staticRoutes
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const routes = await getPrerenderRoutes()
|
|
22
|
+
// 说明:构建期 puppeteer 预渲染已移除——ISR 运行时管线(registry fetch +
|
|
23
|
+
// renderSSR + __SSR_DATA__ 注入)在请求时产出新鲜 HTML,构建期快照反而会
|
|
24
|
+
// 以空数据/错误文本固化页面并在 serveStatic 下遮蔽运行时 ISR。
|
|
81
25
|
|
|
82
26
|
export default defineConfig({
|
|
83
27
|
server: {
|
|
@@ -117,20 +61,7 @@ export default defineConfig({
|
|
|
117
61
|
'vendor-hono': ['hono'],
|
|
118
62
|
'vendor-zustand': ['zustand'],
|
|
119
63
|
},
|
|
120
|
-
plugins: [
|
|
121
|
-
...(prerender && puppeteerRenderer
|
|
122
|
-
? [
|
|
123
|
-
prerender({
|
|
124
|
-
routes,
|
|
125
|
-
renderer: new puppeteerRenderer({
|
|
126
|
-
renderAfterDocumentEvent: 'prerender-ready',
|
|
127
|
-
renderAfterTime: 5000,
|
|
128
|
-
}),
|
|
129
|
-
}),
|
|
130
|
-
]
|
|
131
|
-
: []),
|
|
132
|
-
...(visualizerPlugin ? [visualizerPlugin()] : []),
|
|
133
|
-
],
|
|
64
|
+
plugins: [...(visualizerPlugin ? [visualizerPlugin()] : [])],
|
|
134
65
|
},
|
|
135
66
|
onwarn(warning, defaultHandler) {
|
|
136
67
|
// Suppress antd "use client" directive warnings (React Server Components marker)
|
|
@@ -23,7 +23,6 @@ export default defineConfig({
|
|
|
23
23
|
return 'node'
|
|
24
24
|
}
|
|
25
25
|
})(),
|
|
26
|
-
setupFiles: ['./vitest.setup.ts'],
|
|
27
26
|
include: [
|
|
28
27
|
'**/__tests__/**/*.test.ts',
|
|
29
28
|
'**/__tests__/**/*.test.tsx',
|
|
@@ -32,6 +31,9 @@ export default defineConfig({
|
|
|
32
31
|
exclude: ['**/node_modules/**', '**/dist/**'],
|
|
33
32
|
testTimeout: 60000,
|
|
34
33
|
hookTimeout: 60000,
|
|
34
|
+
// setup-db-path 必须在前:把 :memory: 换成临时文件(libsql 事务与
|
|
35
|
+
// :memory: 不兼容),vitest.setup 再注册 jest-dom 等
|
|
36
|
+
setupFiles: ['./src/test/setup-db-path.ts', './vitest.setup.ts'],
|
|
35
37
|
env: {
|
|
36
38
|
NODE_ENV: 'test',
|
|
37
39
|
SQLITE_PATH: ':memory:',
|