create-fullstack-scaffold 0.4.22 → 0.4.24
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/README.md +231 -104
- package/dist/cli/index.js +4063 -0
- package/dist/cli/index.js.map +1 -0
- package/package.json +1 -1
- package/template/package.json +1 -0
- package/template/src/admin/i18n/locales/en-US.json +10 -1
- package/template/src/admin/i18n/locales/zh-CN.json +10 -1
- package/template/src/admin/pages/SettingsPage.tsx +96 -47
- package/template/src/server/module-admin/routes/system-routes.ts +53 -0
- package/template/src/server/module-auth/__tests__/profile-routes.test.ts +27 -17
- package/template/src/shared/modules/admin/schemas.ts +16 -0
- package/template/vite.config.ts +26 -1
package/package.json
CHANGED
package/template/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"dev:forum": "VITE_PRESET=community vite",
|
|
16
16
|
"dev:saas": "VITE_PRESET=saas vite",
|
|
17
17
|
"build": "npm run build:client && npm run build:server",
|
|
18
|
+
"build:analyze": "ANALYZE=true vite build",
|
|
18
19
|
"build:client": "vite build",
|
|
19
20
|
"build:server": "tsup",
|
|
20
21
|
"build:cli": "tsup --config tsup.config.ts",
|
|
@@ -131,7 +131,16 @@
|
|
|
131
131
|
"confirmNewPassword": "Confirm New Password",
|
|
132
132
|
"confirmNewPasswordPlaceholder": "Confirm new password",
|
|
133
133
|
"saveChanges": "Save Changes",
|
|
134
|
-
"saved": "Settings saved successfully!"
|
|
134
|
+
"saved": "Settings saved successfully!",
|
|
135
|
+
"email": "Email Settings",
|
|
136
|
+
"smtpHost": "SMTP Host",
|
|
137
|
+
"smtpHostPlaceholder": "e.g. smtp.example.com",
|
|
138
|
+
"smtpPort": "SMTP Port",
|
|
139
|
+
"emailFrom": "From Address",
|
|
140
|
+
"emailFromPlaceholder": "e.g. noreply@example.com",
|
|
141
|
+
"sessionTimeout": "Session Timeout",
|
|
142
|
+
"maxLoginAttempts": "Max Login Attempts",
|
|
143
|
+
"minutes": "minutes"
|
|
135
144
|
},
|
|
136
145
|
"roles": {
|
|
137
146
|
"title": "Role Management",
|
|
@@ -131,7 +131,16 @@
|
|
|
131
131
|
"confirmNewPassword": "确认新密码",
|
|
132
132
|
"confirmNewPasswordPlaceholder": "请确认新密码",
|
|
133
133
|
"saveChanges": "保存更改",
|
|
134
|
-
"saved": "设置保存成功!"
|
|
134
|
+
"saved": "设置保存成功!",
|
|
135
|
+
"email": "邮件设置",
|
|
136
|
+
"smtpHost": "SMTP 主机",
|
|
137
|
+
"smtpHostPlaceholder": "例如 smtp.example.com",
|
|
138
|
+
"smtpPort": "SMTP 端口",
|
|
139
|
+
"emailFrom": "发件地址",
|
|
140
|
+
"emailFromPlaceholder": "例如 noreply@example.com",
|
|
141
|
+
"sessionTimeout": "会话超时",
|
|
142
|
+
"maxLoginAttempts": "最大登录尝试次数",
|
|
143
|
+
"minutes": "分钟"
|
|
135
144
|
},
|
|
136
145
|
"roles": {
|
|
137
146
|
"title": "角色管理",
|
|
@@ -1,73 +1,122 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { Card, Form, Input, Button, Switch, Divider, message, InputNumber } from 'antd'
|
|
3
|
+
import { apiClient, api } from '../services/apiClient'
|
|
2
4
|
import { useLanguage } from '../i18n/useLanguage'
|
|
3
|
-
|
|
4
|
-
interface SettingsFormValues {
|
|
5
|
-
siteName?: string
|
|
6
|
-
siteDescription?: string
|
|
7
|
-
currentPassword?: string
|
|
8
|
-
newPassword?: string
|
|
9
|
-
confirmPassword?: string
|
|
10
|
-
}
|
|
5
|
+
import type { Settings } from '@shared/modules/admin'
|
|
11
6
|
|
|
12
7
|
export const SettingsPage: React.FC = () => {
|
|
13
8
|
const { t } = useLanguage()
|
|
14
|
-
const [form] = Form.useForm<
|
|
9
|
+
const [form] = Form.useForm<Settings>()
|
|
10
|
+
const [loading, setLoading] = useState(false)
|
|
11
|
+
|
|
12
|
+
const fetchSettings = useCallback(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const data = await api(apiClient.api.admin.settings.$get())
|
|
15
|
+
.withLoading(t('common.loading'))
|
|
16
|
+
.json()
|
|
17
|
+
form.setFieldsValue(data)
|
|
18
|
+
} catch {
|
|
19
|
+
// handled by api-request
|
|
20
|
+
}
|
|
21
|
+
}, [form, t])
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
fetchSettings()
|
|
25
|
+
}, [fetchSettings])
|
|
26
|
+
|
|
27
|
+
const handleSave = async (values: Settings) => {
|
|
28
|
+
setLoading(true)
|
|
29
|
+
try {
|
|
30
|
+
await api(
|
|
31
|
+
apiClient.api.admin.settings.$put({
|
|
32
|
+
json: values,
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
.withLoading(t('common.loading'))
|
|
36
|
+
.json()
|
|
37
|
+
message.success(t('settings.saved'))
|
|
38
|
+
} catch {
|
|
39
|
+
// handled by api-request
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false)
|
|
42
|
+
}
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
return (
|
|
21
46
|
<div>
|
|
22
47
|
<h1 className="text-2xl font-bold text-gray-900 mb-6">{t('settings.title')}</h1>
|
|
23
48
|
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
<Form.Item label={t('settings.siteName')} name="siteName">
|
|
49
|
+
<Form form={form} layout="vertical" onFinish={handleSave}>
|
|
50
|
+
<Card title={t('settings.general')} className="mb-6">
|
|
51
|
+
<Form.Item label={t('settings.siteName')} name="siteName" rules={[{ required: true }]}>
|
|
27
52
|
<Input placeholder={t('settings.siteNamePlaceholder')} />
|
|
28
53
|
</Form.Item>
|
|
29
54
|
<Form.Item label={t('settings.siteDescription')} name="siteDescription">
|
|
30
55
|
<Input.TextArea rows={4} placeholder={t('settings.siteDescriptionPlaceholder')} />
|
|
31
56
|
</Form.Item>
|
|
32
|
-
</
|
|
33
|
-
</Card>
|
|
57
|
+
</Card>
|
|
34
58
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
<Card title={t('settings.email')} className="mb-6">
|
|
60
|
+
<Form.Item label={t('settings.smtpHost')} name="smtpHost" rules={[{ required: true }]}>
|
|
61
|
+
<Input placeholder={t('settings.smtpHostPlaceholder')} />
|
|
62
|
+
</Form.Item>
|
|
63
|
+
<Form.Item label={t('settings.smtpPort')} name="smtpPort" rules={[{ required: true }]}>
|
|
64
|
+
<InputNumber min={1} max={65535} className="w-full" />
|
|
65
|
+
</Form.Item>
|
|
66
|
+
<Form.Item
|
|
67
|
+
label={t('settings.emailFrom')}
|
|
68
|
+
name="emailFrom"
|
|
69
|
+
rules={[{ required: true, type: 'email' }]}
|
|
70
|
+
>
|
|
71
|
+
<Input placeholder={t('settings.emailFromPlaceholder')} />
|
|
72
|
+
</Form.Item>
|
|
73
|
+
</Card>
|
|
74
|
+
|
|
75
|
+
<Card title={t('settings.notification')} className="mb-6">
|
|
76
|
+
<div className="space-y-4">
|
|
77
|
+
<div className="flex items-center justify-between">
|
|
78
|
+
<div>
|
|
79
|
+
<p className="font-medium">{t('settings.emailNotifications')}</p>
|
|
80
|
+
<p className="text-sm text-gray-500">{t('settings.emailNotificationsDesc')}</p>
|
|
81
|
+
</div>
|
|
82
|
+
<Form.Item name="emailNotifications" valuePropName="checked" noStyle>
|
|
83
|
+
<Switch />
|
|
84
|
+
</Form.Item>
|
|
41
85
|
</div>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<
|
|
86
|
+
<Divider />
|
|
87
|
+
<div className="flex items-center justify-between">
|
|
88
|
+
<div>
|
|
89
|
+
<p className="font-medium">{t('settings.pushNotifications')}</p>
|
|
90
|
+
<p className="text-sm text-gray-500">{t('settings.pushNotificationsDesc')}</p>
|
|
91
|
+
</div>
|
|
92
|
+
<Form.Item name="pushNotifications" valuePropName="checked" noStyle>
|
|
93
|
+
<Switch />
|
|
94
|
+
</Form.Item>
|
|
49
95
|
</div>
|
|
50
|
-
<Switch />
|
|
51
96
|
</div>
|
|
52
|
-
</
|
|
53
|
-
</Card>
|
|
97
|
+
</Card>
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
99
|
+
<Card title={t('settings.security')} className="mb-6">
|
|
100
|
+
<Form.Item
|
|
101
|
+
label={t('settings.sessionTimeout')}
|
|
102
|
+
name="sessionTimeout"
|
|
103
|
+
rules={[{ required: true }]}
|
|
104
|
+
>
|
|
105
|
+
<InputNumber min={5} max={1440} addonAfter={t('settings.minutes')} className="w-full" />
|
|
59
106
|
</Form.Item>
|
|
60
|
-
<Form.Item
|
|
61
|
-
|
|
107
|
+
<Form.Item
|
|
108
|
+
label={t('settings.maxLoginAttempts')}
|
|
109
|
+
name="maxLoginAttempts"
|
|
110
|
+
rules={[{ required: true }]}
|
|
111
|
+
>
|
|
112
|
+
<InputNumber min={1} max={20} className="w-full" />
|
|
62
113
|
</Form.Item>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</Form>
|
|
70
|
-
</Card>
|
|
114
|
+
</Card>
|
|
115
|
+
|
|
116
|
+
<Button type="primary" htmlType="submit" loading={loading} size="large">
|
|
117
|
+
{t('settings.saveChanges')}
|
|
118
|
+
</Button>
|
|
119
|
+
</Form>
|
|
71
120
|
</div>
|
|
72
121
|
)
|
|
73
122
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
HealthCheckSchema,
|
|
10
10
|
RecentActivitySchema,
|
|
11
11
|
ClearTodosResultSchema,
|
|
12
|
+
SettingsSchema,
|
|
13
|
+
UpdateSettingsSchema,
|
|
12
14
|
} from '@shared/modules/admin'
|
|
13
15
|
|
|
14
16
|
const getStatsRoute = createRoute({
|
|
@@ -68,7 +70,58 @@ const clearAllTodosRoute = createRoute({
|
|
|
68
70
|
},
|
|
69
71
|
})
|
|
70
72
|
|
|
73
|
+
const getSettingsRoute = createRoute({
|
|
74
|
+
method: 'get',
|
|
75
|
+
path: '/admin/settings',
|
|
76
|
+
tags: ['admin'],
|
|
77
|
+
security: [{ Bearer: [] }],
|
|
78
|
+
middleware: [authMiddleware({ requiredRole: Role.SUPER_ADMIN })],
|
|
79
|
+
responses: {
|
|
80
|
+
200: successResponse(SettingsSchema, 'Get system settings'),
|
|
81
|
+
401: errorResponse('Unauthorized'),
|
|
82
|
+
403: errorResponse('Forbidden'),
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const updateSettingsRoute = createRoute({
|
|
87
|
+
method: 'put',
|
|
88
|
+
path: '/admin/settings',
|
|
89
|
+
tags: ['admin'],
|
|
90
|
+
security: [{ Bearer: [] }],
|
|
91
|
+
middleware: [authMiddleware({ requiredRole: Role.SUPER_ADMIN })],
|
|
92
|
+
request: {
|
|
93
|
+
body: {
|
|
94
|
+
content: { 'application/json': { schema: UpdateSettingsSchema } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
responses: {
|
|
98
|
+
200: successResponse(SettingsSchema, 'Settings updated'),
|
|
99
|
+
401: errorResponse('Unauthorized'),
|
|
100
|
+
403: errorResponse('Forbidden'),
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
let settingsState: z.infer<typeof SettingsSchema> = {
|
|
105
|
+
siteName: 'Biomimic Admin',
|
|
106
|
+
siteDescription: 'A full-stack admin dashboard',
|
|
107
|
+
smtpHost: 'smtp.example.com',
|
|
108
|
+
smtpPort: 587,
|
|
109
|
+
emailFrom: 'noreply@example.com',
|
|
110
|
+
sessionTimeout: 30,
|
|
111
|
+
maxLoginAttempts: 5,
|
|
112
|
+
emailNotifications: true,
|
|
113
|
+
pushNotifications: false,
|
|
114
|
+
}
|
|
115
|
+
|
|
71
116
|
export const systemRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUser } }>()
|
|
117
|
+
.openapi(getSettingsRoute, async c => {
|
|
118
|
+
return c.json(success(settingsState), 200)
|
|
119
|
+
})
|
|
120
|
+
.openapi(updateSettingsRoute, async c => {
|
|
121
|
+
const body = c.req.valid('json')
|
|
122
|
+
settingsState = { ...settingsState, ...body }
|
|
123
|
+
return c.json(success(settingsState), 200)
|
|
124
|
+
})
|
|
72
125
|
.openapi(getStatsRoute, async c => {
|
|
73
126
|
const stats = await adminService.getSystemStats()
|
|
74
127
|
return c.json(success(stats), 200)
|
|
@@ -2,19 +2,36 @@
|
|
|
2
2
|
import { describe, it, expect } from 'vitest'
|
|
3
3
|
import { profileRoutes } from '../routes/profile-routes'
|
|
4
4
|
|
|
5
|
+
interface ProfileResponse {
|
|
6
|
+
success: boolean
|
|
7
|
+
data: {
|
|
8
|
+
id: string
|
|
9
|
+
username: string
|
|
10
|
+
email: string
|
|
11
|
+
bio: string
|
|
12
|
+
joinDate: string
|
|
13
|
+
stats: { posts: number; followers: number; following: number }
|
|
14
|
+
}
|
|
15
|
+
timestamp: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function fetchProfile(): Promise<ProfileResponse> {
|
|
19
|
+
const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
|
|
20
|
+
return (await res.json()) as ProfileResponse
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
describe('Profile Routes', () => {
|
|
6
24
|
describe('GET /profile', () => {
|
|
7
25
|
it('should return profile with 200', async () => {
|
|
8
26
|
const res = await profileRoutes.fetch(new Request('http://localhost/profile'))
|
|
9
27
|
|
|
10
28
|
expect(res.status).toBe(200)
|
|
11
|
-
const data =
|
|
29
|
+
const data = await fetchProfile()
|
|
12
30
|
expect(data.success).toBe(true)
|
|
13
31
|
})
|
|
14
32
|
|
|
15
33
|
it('should return profile with required fields', async () => {
|
|
16
|
-
const
|
|
17
|
-
const data = (await res.json()) as Record<string, any>
|
|
34
|
+
const data = await fetchProfile()
|
|
18
35
|
|
|
19
36
|
expect(data.success).toBe(true)
|
|
20
37
|
expect(data.data.id).toBeDefined()
|
|
@@ -25,16 +42,14 @@ describe('Profile Routes', () => {
|
|
|
25
42
|
})
|
|
26
43
|
|
|
27
44
|
it('should return valid email format', async () => {
|
|
28
|
-
const
|
|
29
|
-
const data = (await res.json()) as Record<string, any>
|
|
45
|
+
const data = await fetchProfile()
|
|
30
46
|
|
|
31
47
|
expect(data.success).toBe(true)
|
|
32
48
|
expect(data.data.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
|
|
33
49
|
})
|
|
34
50
|
|
|
35
51
|
it('should return stats with numeric values', async () => {
|
|
36
|
-
const
|
|
37
|
-
const data = (await res.json()) as Record<string, any>
|
|
52
|
+
const data = await fetchProfile()
|
|
38
53
|
|
|
39
54
|
expect(data.success).toBe(true)
|
|
40
55
|
expect(typeof data.data.stats.posts).toBe('number')
|
|
@@ -43,18 +58,15 @@ describe('Profile Routes', () => {
|
|
|
43
58
|
})
|
|
44
59
|
|
|
45
60
|
it('should return a valid ISO date for joinDate', async () => {
|
|
46
|
-
const
|
|
47
|
-
const data = (await res.json()) as Record<string, any>
|
|
61
|
+
const data = await fetchProfile()
|
|
48
62
|
|
|
49
63
|
expect(data.success).toBe(true)
|
|
50
64
|
expect(data.data.joinDate).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
|
51
65
|
})
|
|
52
66
|
|
|
53
67
|
it('should return consistent data across requests', async () => {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const res2 = await profileRoutes.fetch(new Request('http://localhost/profile'))
|
|
57
|
-
const data2 = (await res2.json()) as Record<string, any>
|
|
68
|
+
const data1 = await fetchProfile()
|
|
69
|
+
const data2 = await fetchProfile()
|
|
58
70
|
|
|
59
71
|
expect(data1.success).toBe(true)
|
|
60
72
|
expect(data2.success).toBe(true)
|
|
@@ -64,8 +76,7 @@ describe('Profile Routes', () => {
|
|
|
64
76
|
})
|
|
65
77
|
|
|
66
78
|
it('should include timestamp in response', async () => {
|
|
67
|
-
const
|
|
68
|
-
const data = (await res.json()) as Record<string, any>
|
|
79
|
+
const data = await fetchProfile()
|
|
69
80
|
|
|
70
81
|
expect(data.success).toBe(true)
|
|
71
82
|
expect(data.timestamp).toBeDefined()
|
|
@@ -73,8 +84,7 @@ describe('Profile Routes', () => {
|
|
|
73
84
|
})
|
|
74
85
|
|
|
75
86
|
it('should return bio field', async () => {
|
|
76
|
-
const
|
|
77
|
-
const data = (await res.json()) as Record<string, any>
|
|
87
|
+
const data = await fetchProfile()
|
|
78
88
|
|
|
79
89
|
expect(data.success).toBe(true)
|
|
80
90
|
expect(data.data.bio).toBeDefined()
|
|
@@ -79,6 +79,20 @@ export const ClearTodosResultSchema = z.object({
|
|
|
79
79
|
deletedCount: z.number(),
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
+
export const SettingsSchema = z.object({
|
|
83
|
+
siteName: z.string(),
|
|
84
|
+
siteDescription: z.string(),
|
|
85
|
+
smtpHost: z.string(),
|
|
86
|
+
smtpPort: z.number(),
|
|
87
|
+
emailFrom: z.string(),
|
|
88
|
+
sessionTimeout: z.number(),
|
|
89
|
+
maxLoginAttempts: z.number(),
|
|
90
|
+
emailNotifications: z.boolean(),
|
|
91
|
+
pushNotifications: z.boolean(),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const UpdateSettingsSchema = SettingsSchema
|
|
95
|
+
|
|
82
96
|
export const AdminSuccessSchema = z.object({})
|
|
83
97
|
|
|
84
98
|
export const DownloadTokenSchema = z.object({
|
|
@@ -98,3 +112,5 @@ export type RegisterRequest = z.infer<typeof RegisterRequestSchema>
|
|
|
98
112
|
export type User = z.infer<typeof UserSchema>
|
|
99
113
|
export type UpdateUserRequest = z.infer<typeof UpdateUserRequestSchema>
|
|
100
114
|
export type ClearTodosResult = z.infer<typeof ClearTodosResultSchema>
|
|
115
|
+
export type Settings = z.infer<typeof SettingsSchema>
|
|
116
|
+
export type UpdateSettings = z.infer<typeof UpdateSettingsSchema>
|
package/template/vite.config.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import { existsSync } from 'fs'
|
|
3
|
-
import { defineConfig } from 'vite'
|
|
3
|
+
import { defineConfig, type Plugin } from 'vite'
|
|
4
4
|
import devServer from '@hono/vite-dev-server'
|
|
5
5
|
import { websocketPlugin, dbPlugin } from './vite-plugins'
|
|
6
|
+
// Bundle analysis: npm install -D rollup-plugin-visualizer && npm run build:analyze
|
|
7
|
+
let visualizerPlugin: (() => Plugin) | undefined
|
|
8
|
+
if (process.env.ANALYZE === 'true') {
|
|
9
|
+
try {
|
|
10
|
+
// @ts-expect-error — optional dev dependency, installed via: npm install -D rollup-plugin-visualizer
|
|
11
|
+
const mod = await import('rollup-plugin-visualizer')
|
|
12
|
+
visualizerPlugin = () =>
|
|
13
|
+
mod.visualizer({
|
|
14
|
+
open: true,
|
|
15
|
+
filename: 'stats.html',
|
|
16
|
+
gzipSize: true,
|
|
17
|
+
brotliSize: true,
|
|
18
|
+
}) as Plugin
|
|
19
|
+
} catch {
|
|
20
|
+
// rollup-plugin-visualizer not installed — run: npm install -D rollup-plugin-visualizer
|
|
21
|
+
}
|
|
22
|
+
}
|
|
6
23
|
// Prerender is optional — only needed during production build with puppeteer + Chrome installed
|
|
7
24
|
let prerender: typeof import('@prerenderer/rollup-plugin').default | undefined
|
|
8
25
|
let puppeteerRenderer: typeof import('@prerenderer/renderer-puppeteer').default | undefined
|
|
@@ -85,6 +102,7 @@ export default defineConfig({
|
|
|
85
102
|
],
|
|
86
103
|
build: {
|
|
87
104
|
outDir: 'dist/client',
|
|
105
|
+
chunkSizeWarningLimit: 1000,
|
|
88
106
|
rollupOptions: {
|
|
89
107
|
input: {
|
|
90
108
|
main: path.resolve(__dirname, 'index.html'),
|
|
@@ -93,6 +111,12 @@ export default defineConfig({
|
|
|
93
111
|
merchant: path.resolve(__dirname, 'merchant.html'),
|
|
94
112
|
},
|
|
95
113
|
output: {
|
|
114
|
+
manualChunks: {
|
|
115
|
+
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
|
|
116
|
+
'vendor-antd': ['antd', '@ant-design/icons'],
|
|
117
|
+
'vendor-hono': ['hono'],
|
|
118
|
+
'vendor-zustand': ['zustand'],
|
|
119
|
+
},
|
|
96
120
|
plugins: [
|
|
97
121
|
...(prerender && puppeteerRenderer
|
|
98
122
|
? [
|
|
@@ -105,6 +129,7 @@ export default defineConfig({
|
|
|
105
129
|
}),
|
|
106
130
|
]
|
|
107
131
|
: []),
|
|
132
|
+
...(visualizerPlugin ? [visualizerPlugin()] : []),
|
|
108
133
|
],
|
|
109
134
|
},
|
|
110
135
|
onwarn(warning, defaultHandler) {
|