create-next-imagicma 0.0.1
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 +42 -0
- package/bin/create-next-imagicma.mjs +220 -0
- package/package.json +19 -0
- package/template/.env.example +10 -0
- package/template/AGENTS.md +146 -0
- package/template/README.md +36 -0
- package/template/app/_components/DevPreviewShield.tsx +638 -0
- package/template/app/api/greeting/route.ts +27 -0
- package/template/app/error.tsx +93 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +145 -0
- package/template/app/hello/_components/HelloClient.tsx +94 -0
- package/template/app/hello/page.tsx +23 -0
- package/template/app/layout.tsx +29 -0
- package/template/app/page.tsx +49 -0
- package/template/app/providers.tsx +25 -0
- package/template/components/ui/accordion.tsx +58 -0
- package/template/components/ui/alert-dialog.tsx +141 -0
- package/template/components/ui/alert.tsx +61 -0
- package/template/components/ui/aspect-ratio.tsx +7 -0
- package/template/components/ui/avatar.tsx +51 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/breadcrumb.tsx +117 -0
- package/template/components/ui/button.tsx +64 -0
- package/template/components/ui/calendar.tsx +72 -0
- package/template/components/ui/card.tsx +87 -0
- package/template/components/ui/carousel.tsx +262 -0
- package/template/components/ui/chart.tsx +365 -0
- package/template/components/ui/checkbox.tsx +30 -0
- package/template/components/ui/collapsible.tsx +11 -0
- package/template/components/ui/command.tsx +153 -0
- package/template/components/ui/context-menu.tsx +200 -0
- package/template/components/ui/dialog.tsx +122 -0
- package/template/components/ui/drawer.tsx +118 -0
- package/template/components/ui/dropdown-menu.tsx +200 -0
- package/template/components/ui/form.tsx +178 -0
- package/template/components/ui/hover-card.tsx +29 -0
- package/template/components/ui/input-otp.tsx +71 -0
- package/template/components/ui/input.tsx +25 -0
- package/template/components/ui/label.tsx +26 -0
- package/template/components/ui/menubar.tsx +256 -0
- package/template/components/ui/navigation-menu.tsx +130 -0
- package/template/components/ui/pagination.tsx +119 -0
- package/template/components/ui/popover.tsx +31 -0
- package/template/components/ui/progress.tsx +28 -0
- package/template/components/ui/radio-group.tsx +44 -0
- package/template/components/ui/resizable.tsx +45 -0
- package/template/components/ui/scroll-area.tsx +48 -0
- package/template/components/ui/select.tsx +160 -0
- package/template/components/ui/separator.tsx +31 -0
- package/template/components/ui/sheet.tsx +140 -0
- package/template/components/ui/sidebar.tsx +732 -0
- package/template/components/ui/skeleton.tsx +17 -0
- package/template/components/ui/slider.tsx +28 -0
- package/template/components/ui/switch.tsx +29 -0
- package/template/components/ui/table.tsx +119 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/components/ui/textarea.tsx +24 -0
- package/template/components/ui/toast.tsx +129 -0
- package/template/components/ui/toaster.tsx +35 -0
- package/template/components/ui/toggle-group.tsx +61 -0
- package/template/components/ui/toggle.tsx +45 -0
- package/template/components/ui/tooltip.tsx +30 -0
- package/template/drizzle.config.ts +50 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-greeting.ts +15 -0
- package/template/hooks/use-mobile.ts +21 -0
- package/template/hooks/use-toast.ts +194 -0
- package/template/lib/queryClient.ts +59 -0
- package/template/lib/utils.ts +6 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +81 -0
- package/template/pnpm-lock.yaml +6937 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/server/db.ts +24 -0
- package/template/server/storage.ts +41 -0
- package/template/shared/routes.ts +13 -0
- package/template/shared/schema.ts +17 -0
- package/template/tailwind.config.mjs +96 -0
- package/template/tsconfig.json +35 -0
- package/template/types/pg.d.ts +19 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type OverlayKind = "none" | "building" | "error";
|
|
6
|
+
type ErrorSource = "compile" | "server" | "runtime" | "unknown";
|
|
7
|
+
|
|
8
|
+
type ErrorInfo = {
|
|
9
|
+
source: ErrorSource;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const SHOW_DELAY_MS = 120;
|
|
14
|
+
const MIN_VISIBLE_MS = 260;
|
|
15
|
+
const TOAST_VISIBLE_MS = 800;
|
|
16
|
+
const MAX_ERROR_LINES = 3;
|
|
17
|
+
|
|
18
|
+
function stripAnsi(input: string) {
|
|
19
|
+
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toErrorMessage(value: unknown): string {
|
|
23
|
+
if (typeof value === "string") return value;
|
|
24
|
+
if (value instanceof Error) return value.message || "发生未知错误";
|
|
25
|
+
if (value && typeof value === "object" && "message" in value) {
|
|
26
|
+
const message = (value as { message?: unknown }).message;
|
|
27
|
+
if (typeof message === "string" && message.trim()) return message;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
} catch {
|
|
32
|
+
return "发生未知错误";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatErrorSnippet(message: string) {
|
|
37
|
+
const cleaned = stripAnsi(message).trim();
|
|
38
|
+
const lines = cleaned.split(/\r?\n/).filter(Boolean);
|
|
39
|
+
return lines.slice(0, MAX_ERROR_LINES).join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getHmrWebSocketUrl() {
|
|
43
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
44
|
+
const host = window.location.host;
|
|
45
|
+
return `${protocol}//${host}/_next/webpack-hmr`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function UILoader() {
|
|
49
|
+
return (
|
|
50
|
+
<div className="relative w-24 h-20 bg-white/90 dark:bg-zinc-800 rounded-lg shadow-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden flex flex-col p-2 gap-2">
|
|
51
|
+
{/* Header simulate */}
|
|
52
|
+
<div className="flex gap-1 mb-1">
|
|
53
|
+
<div className="w-2 h-2 rounded-full bg-red-400" />
|
|
54
|
+
<div className="w-2 h-2 rounded-full bg-yellow-400" />
|
|
55
|
+
<div className="w-2 h-2 rounded-full bg-green-400" />
|
|
56
|
+
</div>
|
|
57
|
+
{/* Content lines simulate */}
|
|
58
|
+
<div className="w-full h-2 bg-zinc-200 dark:bg-zinc-700 rounded animate-[pulse_1s_ease-in-out_infinite]" />
|
|
59
|
+
<div className="w-3/4 h-2 bg-indigo-100 dark:bg-indigo-900/50 rounded animate-[pulse_1.5s_ease-in-out_infinite]" />
|
|
60
|
+
<div className="w-1/2 h-2 bg-zinc-200 dark:bg-zinc-700 rounded animate-[pulse_2s_ease-in-out_infinite]" />
|
|
61
|
+
{/* Shimmer effect overlay */}
|
|
62
|
+
<div className="absolute inset-0 -translate-x-full animate-[shimmer_1.5s_infinite] bg-gradient-to-r from-transparent via-white/40 to-transparent" />
|
|
63
|
+
<style
|
|
64
|
+
dangerouslySetInnerHTML={{
|
|
65
|
+
__html: `@keyframes shimmer { 100% { transform: translateX(100%); } }`,
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function RefreshCwIcon({ className }: { className?: string }) {
|
|
73
|
+
return (
|
|
74
|
+
<svg
|
|
75
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
76
|
+
width="12"
|
|
77
|
+
height="12"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
strokeWidth="2"
|
|
82
|
+
strokeLinecap="round"
|
|
83
|
+
strokeLinejoin="round"
|
|
84
|
+
className={className}
|
|
85
|
+
aria-hidden
|
|
86
|
+
>
|
|
87
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
|
88
|
+
<path d="M3 3v5h5" />
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const HMR_EXIT_DURATION_MS = 400;
|
|
94
|
+
|
|
95
|
+
function HMRStatusOverlay({
|
|
96
|
+
isVisible,
|
|
97
|
+
message = "正在更新预览…",
|
|
98
|
+
}: {
|
|
99
|
+
isVisible: boolean;
|
|
100
|
+
message?: string;
|
|
101
|
+
}) {
|
|
102
|
+
const exitUntilRef = useRef<number | null>(null);
|
|
103
|
+
const prevVisibleRef = useRef(false);
|
|
104
|
+
const [, setTick] = useState(0);
|
|
105
|
+
|
|
106
|
+
if (isVisible) {
|
|
107
|
+
prevVisibleRef.current = true;
|
|
108
|
+
exitUntilRef.current = null;
|
|
109
|
+
} else {
|
|
110
|
+
if (prevVisibleRef.current && exitUntilRef.current === null) {
|
|
111
|
+
// eslint-disable-next-line react-hooks/purity -- intentional ref mutation for exit timing
|
|
112
|
+
exitUntilRef.current = Date.now() + HMR_EXIT_DURATION_MS;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
prevVisibleRef.current = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (isVisible || exitUntilRef.current === null) return;
|
|
120
|
+
const timer = setTimeout(
|
|
121
|
+
() => setTick((n) => n + 1),
|
|
122
|
+
HMR_EXIT_DURATION_MS
|
|
123
|
+
);
|
|
124
|
+
return () => clearTimeout(timer);
|
|
125
|
+
}, [isVisible]);
|
|
126
|
+
// eslint-disable-next-line react-hooks/purity -- intentional ref mutation for exit timing
|
|
127
|
+
const shouldRender =isVisible || (exitUntilRef.current !== null && Date.now() < exitUntilRef.current);
|
|
128
|
+
if (!shouldRender) {
|
|
129
|
+
exitUntilRef.current = null;
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className={`fixed inset-0 z-[9999] flex flex-col items-center justify-center transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
|
137
|
+
isVisible
|
|
138
|
+
? "opacity-100 backdrop-blur-xl bg-white/60 dark:bg-black/60"
|
|
139
|
+
: "opacity-0 backdrop-blur-none pointer-events-none"
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
<div
|
|
143
|
+
className={`transform transition-all duration-500 ${
|
|
144
|
+
isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"
|
|
145
|
+
}`}
|
|
146
|
+
>
|
|
147
|
+
<div className="flex flex-col items-center">
|
|
148
|
+
<div className="mb-6 p-6 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl ring-1 ring-black/5 dark:ring-white/10 relative overflow-hidden">
|
|
149
|
+
<div className="absolute inset-0 bg-gradient-to-tr from-indigo-500/10 via-purple-500/10 to-pink-500/10 opacity-50" />
|
|
150
|
+
<div className="relative z-10">
|
|
151
|
+
<UILoader />
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-zinc-800/80 rounded-full border border-zinc-200 dark:border-zinc-700 shadow-sm backdrop-blur-md">
|
|
155
|
+
<RefreshCwIcon className="text-indigo-500 animate-spin shrink-0" />
|
|
156
|
+
<span className="text-sm font-medium text-zinc-600 dark:text-zinc-300 font-mono tracking-tight">
|
|
157
|
+
{message}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class DevRuntimeErrorBoundary extends React.PureComponent<
|
|
167
|
+
{
|
|
168
|
+
children: React.ReactNode;
|
|
169
|
+
resetKey: number;
|
|
170
|
+
onRuntimeError: (error: unknown) => void;
|
|
171
|
+
},
|
|
172
|
+
{ hasError: boolean }
|
|
173
|
+
> {
|
|
174
|
+
static getDerivedStateFromError() {
|
|
175
|
+
return { hasError: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
componentDidCatch(error: unknown) {
|
|
179
|
+
this.props.onRuntimeError(error);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
componentDidUpdate(prevProps: Readonly<{ resetKey: number }>) {
|
|
183
|
+
if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) {
|
|
184
|
+
this.setState({ hasError: false });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
state = { hasError: false };
|
|
189
|
+
|
|
190
|
+
render() {
|
|
191
|
+
if (this.state.hasError) return null;
|
|
192
|
+
return this.props.children;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function DevPreviewShield({ children }: { children: React.ReactNode }) {
|
|
197
|
+
const [overlay, setOverlay] = useState<OverlayKind>("none");
|
|
198
|
+
const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
|
|
199
|
+
const [debugMode, setDebugMode] = useState(false);
|
|
200
|
+
const [toastVisible, setToastVisible] = useState(false);
|
|
201
|
+
const [resetKey, setResetKey] = useState(0);
|
|
202
|
+
|
|
203
|
+
const overlayRef = useRef<OverlayKind>("none");
|
|
204
|
+
const debugModeRef = useRef(false);
|
|
205
|
+
const visibleSinceRef = useRef<number | null>(null);
|
|
206
|
+
|
|
207
|
+
const showTimerRef = useRef<number | null>(null);
|
|
208
|
+
const hideTimerRef = useRef<number | null>(null);
|
|
209
|
+
const toastTimerRef = useRef<number | null>(null);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
overlayRef.current = overlay;
|
|
213
|
+
}, [overlay]);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
debugModeRef.current = debugMode;
|
|
217
|
+
}, [debugMode]);
|
|
218
|
+
|
|
219
|
+
const setPreviewShieldAttr = useCallback((on: boolean) => {
|
|
220
|
+
if (on) {
|
|
221
|
+
document.documentElement.dataset.previewShield = "on";
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
delete document.documentElement.dataset.previewShield;
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
const shouldRenderOverlay = overlay !== "none" && !debugMode;
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
setPreviewShieldAttr(shouldRenderOverlay);
|
|
231
|
+
return () => setPreviewShieldAttr(false);
|
|
232
|
+
}, [setPreviewShieldAttr, shouldRenderOverlay]);
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!shouldRenderOverlay) return;
|
|
236
|
+
if (overlayRef.current === "none") return;
|
|
237
|
+
if (visibleSinceRef.current == null) visibleSinceRef.current = Date.now();
|
|
238
|
+
}, [shouldRenderOverlay]);
|
|
239
|
+
|
|
240
|
+
const clearTimer = useCallback((ref: React.MutableRefObject<number | null>) => {
|
|
241
|
+
if (ref.current == null) return;
|
|
242
|
+
window.clearTimeout(ref.current);
|
|
243
|
+
ref.current = null;
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
const showToast = useCallback(() => {
|
|
247
|
+
clearTimer(toastTimerRef);
|
|
248
|
+
setToastVisible(true);
|
|
249
|
+
toastTimerRef.current = window.setTimeout(() => {
|
|
250
|
+
toastTimerRef.current = null;
|
|
251
|
+
setToastVisible(false);
|
|
252
|
+
}, TOAST_VISIBLE_MS);
|
|
253
|
+
}, [clearTimer]);
|
|
254
|
+
|
|
255
|
+
const showBuildingOverlay = useCallback(() => {
|
|
256
|
+
setErrorInfo(null);
|
|
257
|
+
setOverlay("building");
|
|
258
|
+
visibleSinceRef.current = Date.now();
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
const enterBuilding = useCallback(() => {
|
|
262
|
+
clearTimer(hideTimerRef);
|
|
263
|
+
clearTimer(toastTimerRef);
|
|
264
|
+
|
|
265
|
+
if (overlayRef.current !== "none") {
|
|
266
|
+
showBuildingOverlay();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (showTimerRef.current != null) return;
|
|
271
|
+
showTimerRef.current = window.setTimeout(() => {
|
|
272
|
+
showTimerRef.current = null;
|
|
273
|
+
showBuildingOverlay();
|
|
274
|
+
}, SHOW_DELAY_MS);
|
|
275
|
+
}, [clearTimer, showBuildingOverlay]);
|
|
276
|
+
|
|
277
|
+
const enterError = useCallback(
|
|
278
|
+
(message: string, source: ErrorSource) => {
|
|
279
|
+
clearTimer(showTimerRef);
|
|
280
|
+
clearTimer(hideTimerRef);
|
|
281
|
+
|
|
282
|
+
setOverlay("error");
|
|
283
|
+
setErrorInfo({ source, message: formatErrorSnippet(message) });
|
|
284
|
+
if (visibleSinceRef.current == null) visibleSinceRef.current = Date.now();
|
|
285
|
+
},
|
|
286
|
+
[clearTimer]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const hideOverlayWithMinVisible = useCallback(() => {
|
|
290
|
+
clearTimer(showTimerRef);
|
|
291
|
+
|
|
292
|
+
const since = visibleSinceRef.current;
|
|
293
|
+
const elapsed = since == null ? MIN_VISIBLE_MS : Date.now() - since;
|
|
294
|
+
const remaining = Math.max(0, MIN_VISIBLE_MS - elapsed);
|
|
295
|
+
|
|
296
|
+
const doHide = () => {
|
|
297
|
+
hideTimerRef.current = null;
|
|
298
|
+
setOverlay("none");
|
|
299
|
+
setErrorInfo(null);
|
|
300
|
+
visibleSinceRef.current = null;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (remaining === 0) {
|
|
304
|
+
doHide();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
clearTimer(hideTimerRef);
|
|
308
|
+
hideTimerRef.current = window.setTimeout(doHide, remaining);
|
|
309
|
+
}, [clearTimer]);
|
|
310
|
+
|
|
311
|
+
const handleBuildOk = useCallback(() => {
|
|
312
|
+
const hadPendingShow = showTimerRef.current != null;
|
|
313
|
+
|
|
314
|
+
clearTimer(showTimerRef);
|
|
315
|
+
clearTimer(hideTimerRef);
|
|
316
|
+
|
|
317
|
+
setDebugMode(false);
|
|
318
|
+
setResetKey((k) => k + 1);
|
|
319
|
+
|
|
320
|
+
if (hadPendingShow && !debugModeRef.current) {
|
|
321
|
+
showToast();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (overlayRef.current === "none") {
|
|
325
|
+
setErrorInfo(null);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
hideOverlayWithMinVisible();
|
|
330
|
+
}, [clearTimer, hideOverlayWithMinVisible, showToast]);
|
|
331
|
+
|
|
332
|
+
const handleBuildMessage = useCallback(
|
|
333
|
+
(msg: unknown) => {
|
|
334
|
+
if (!msg || typeof msg !== "object") return;
|
|
335
|
+
const type = (msg as { type?: unknown }).type;
|
|
336
|
+
if (type === "building") {
|
|
337
|
+
enterBuilding();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (type === "built" || type === "sync") {
|
|
342
|
+
const errors = (msg as { errors?: unknown }).errors;
|
|
343
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
344
|
+
const first = errors[0];
|
|
345
|
+
enterError(
|
|
346
|
+
typeof first === "string" ? first : toErrorMessage(first),
|
|
347
|
+
"compile"
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
handleBuildOk();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (type === "serverError") {
|
|
356
|
+
const errorJSON = (msg as { errorJSON?: unknown }).errorJSON;
|
|
357
|
+
if (typeof errorJSON === "string" && errorJSON) {
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(errorJSON) as { message?: unknown };
|
|
360
|
+
enterError(toErrorMessage(parsed), "server");
|
|
361
|
+
return;
|
|
362
|
+
} catch {
|
|
363
|
+
enterError(errorJSON, "server");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
enterError("服务器发生错误。", "server");
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
[enterBuilding, enterError, handleBuildOk]
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const onRuntimeError = useCallback(
|
|
374
|
+
(error: unknown) => {
|
|
375
|
+
enterError(toErrorMessage(error), "runtime");
|
|
376
|
+
},
|
|
377
|
+
[enterError]
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const shortcutLabel = useMemo(() => {
|
|
381
|
+
const isMac =
|
|
382
|
+
typeof navigator !== "undefined" &&
|
|
383
|
+
/Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
|
384
|
+
return isMac ? "⌘⇧D" : "Ctrl⇧D";
|
|
385
|
+
}, []);
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
389
|
+
const key = e.key.toLowerCase();
|
|
390
|
+
if (!e.shiftKey || key !== "d") return;
|
|
391
|
+
if (!(e.ctrlKey || e.metaKey)) return;
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
setDebugMode((v) => !v);
|
|
394
|
+
};
|
|
395
|
+
window.addEventListener("keydown", onKeyDown);
|
|
396
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
397
|
+
}, []);
|
|
398
|
+
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
const onError = (e: ErrorEvent) => {
|
|
401
|
+
if (e.error) {
|
|
402
|
+
enterError(toErrorMessage(e.error), "runtime");
|
|
403
|
+
} else if (e.message) {
|
|
404
|
+
enterError(e.message, "runtime");
|
|
405
|
+
} else {
|
|
406
|
+
enterError("发生运行时错误。", "runtime");
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const onRejection = (e: PromiseRejectionEvent) => {
|
|
410
|
+
enterError(toErrorMessage(e.reason), "runtime");
|
|
411
|
+
};
|
|
412
|
+
window.addEventListener("error", onError);
|
|
413
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
414
|
+
return () => {
|
|
415
|
+
window.removeEventListener("error", onError);
|
|
416
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
417
|
+
};
|
|
418
|
+
}, [enterError]);
|
|
419
|
+
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
let alive = true;
|
|
422
|
+
let ws: WebSocket | null = null;
|
|
423
|
+
let retryCount = 0;
|
|
424
|
+
let retryTimer: number | null = null;
|
|
425
|
+
|
|
426
|
+
const connect = () => {
|
|
427
|
+
if (!alive) return;
|
|
428
|
+
if (retryTimer != null) {
|
|
429
|
+
window.clearTimeout(retryTimer);
|
|
430
|
+
retryTimer = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const url = getHmrWebSocketUrl();
|
|
434
|
+
try {
|
|
435
|
+
ws = new WebSocket(url);
|
|
436
|
+
ws.binaryType = "arraybuffer";
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
ws.onmessage = (event) => {
|
|
442
|
+
if (!alive) return;
|
|
443
|
+
|
|
444
|
+
if (event.data instanceof ArrayBuffer) {
|
|
445
|
+
try {
|
|
446
|
+
const t = new DataView(event.data).getUint8(0);
|
|
447
|
+
if (t === 1) {
|
|
448
|
+
enterError("检测到编译错误。", "compile");
|
|
449
|
+
} else {
|
|
450
|
+
enterError("检测到错误。", "unknown");
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
enterError("检测到错误。", "unknown");
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (typeof event.data !== "string") return;
|
|
459
|
+
try {
|
|
460
|
+
const msg = JSON.parse(event.data) as unknown;
|
|
461
|
+
handleBuildMessage(msg);
|
|
462
|
+
} catch {
|
|
463
|
+
// ignore non-JSON messages
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
ws.onopen = () => {
|
|
468
|
+
retryCount = 0;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
ws.onclose = () => {
|
|
472
|
+
if (!alive) return;
|
|
473
|
+
retryCount += 1;
|
|
474
|
+
const backoffMs = Math.min(5000, 250 * retryCount);
|
|
475
|
+
retryTimer = window.setTimeout(connect, backoffMs);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
ws.onerror = () => {
|
|
479
|
+
// let onclose handle reconnection
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
connect();
|
|
484
|
+
|
|
485
|
+
return () => {
|
|
486
|
+
alive = false;
|
|
487
|
+
if (retryTimer != null) window.clearTimeout(retryTimer);
|
|
488
|
+
if (ws) ws.close();
|
|
489
|
+
clearTimer(showTimerRef);
|
|
490
|
+
clearTimer(hideTimerRef);
|
|
491
|
+
clearTimer(toastTimerRef);
|
|
492
|
+
setPreviewShieldAttr(false);
|
|
493
|
+
};
|
|
494
|
+
}, [
|
|
495
|
+
clearTimer,
|
|
496
|
+
enterError,
|
|
497
|
+
handleBuildMessage,
|
|
498
|
+
setPreviewShieldAttr,
|
|
499
|
+
enterBuilding,
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
const title = overlay === "building" ? "正在更新预览…" : "预览暂时不可用";
|
|
503
|
+
const description =
|
|
504
|
+
overlay === "building"
|
|
505
|
+
? "请稍候,改动即将生效。"
|
|
506
|
+
: "检测到错误,修复后将自动恢复。";
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<>
|
|
510
|
+
<DevRuntimeErrorBoundary resetKey={resetKey} onRuntimeError={onRuntimeError}>
|
|
511
|
+
{children}
|
|
512
|
+
</DevRuntimeErrorBoundary>
|
|
513
|
+
|
|
514
|
+
{!debugMode && toastVisible ? (
|
|
515
|
+
<div className="fixed left-1/2 top-6 z-[2147483646] -translate-x-1/2">
|
|
516
|
+
<div className="flex items-center gap-2 rounded-full border border-black/10 bg-white/80 px-4 py-2 text-xs font-medium text-black/70 shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-zinc-950/60 dark:text-white/80">
|
|
517
|
+
<svg
|
|
518
|
+
viewBox="0 0 20 20"
|
|
519
|
+
className="h-4 w-4 text-emerald-600 dark:text-emerald-400"
|
|
520
|
+
fill="none"
|
|
521
|
+
aria-hidden="true"
|
|
522
|
+
>
|
|
523
|
+
<path
|
|
524
|
+
d="M16.25 5.75 8.25 13.75 3.75 9.25"
|
|
525
|
+
stroke="currentColor"
|
|
526
|
+
strokeWidth="2"
|
|
527
|
+
strokeLinecap="round"
|
|
528
|
+
strokeLinejoin="round"
|
|
529
|
+
/>
|
|
530
|
+
</svg>
|
|
531
|
+
<span>已更新</span>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
) : null}
|
|
535
|
+
|
|
536
|
+
<HMRStatusOverlay
|
|
537
|
+
isVisible={overlay === "building"}
|
|
538
|
+
message="正在更新预览…"
|
|
539
|
+
/>
|
|
540
|
+
|
|
541
|
+
{shouldRenderOverlay && overlay === "error" ? (
|
|
542
|
+
<div
|
|
543
|
+
className="fixed inset-0 z-[2147483646] flex items-center justify-center bg-white/70 px-6 py-16 backdrop-blur-md dark:bg-black/40"
|
|
544
|
+
role="alertdialog"
|
|
545
|
+
aria-modal="true"
|
|
546
|
+
>
|
|
547
|
+
<div className="w-full max-w-2xl">
|
|
548
|
+
<div className="mx-auto w-full max-w-xl rounded-3xl border border-black/10 bg-white/80 shadow-[0_24px_80px_-40px_rgba(0,0,0,0.45)] backdrop-blur-xl dark:border-white/10 dark:bg-zinc-950/60">
|
|
549
|
+
<div className="flex items-center justify-between px-5 py-4">
|
|
550
|
+
<div className="flex items-center gap-1.5">
|
|
551
|
+
<span className="h-2.5 w-2.5 rounded-full bg-red-400/90" />
|
|
552
|
+
<span className="h-2.5 w-2.5 rounded-full bg-amber-400/90" />
|
|
553
|
+
<span className="h-2.5 w-2.5 rounded-full bg-emerald-400/90" />
|
|
554
|
+
</div>
|
|
555
|
+
<div className="flex items-center gap-2 rounded-full border border-black/10 bg-black/[0.03] px-3 py-1 text-xs font-medium text-black/60 shadow-sm dark:border-white/10 dark:bg-white/5 dark:text-white/70">
|
|
556
|
+
<span className="h-1.5 w-1.5 rounded-full bg-rose-400" />
|
|
557
|
+
<span>Error</span>
|
|
558
|
+
</div>
|
|
559
|
+
<div className="w-10" />
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div className="px-6 pb-8 pt-2">
|
|
563
|
+
<div className="rounded-2xl border border-black/5 bg-white/60 p-6 shadow-sm dark:border-white/10 dark:bg-white/5">
|
|
564
|
+
<div className="flex items-center justify-between">
|
|
565
|
+
<div className="h-3 w-28 rounded-full bg-black/10 dark:bg-white/10" />
|
|
566
|
+
<div className="h-3 w-10 rounded-full bg-black/10 dark:bg-white/10" />
|
|
567
|
+
</div>
|
|
568
|
+
{errorInfo?.message ? (
|
|
569
|
+
<div className="mt-6">
|
|
570
|
+
<div className="mb-3 text-xs font-medium text-black/60 dark:text-white/70">
|
|
571
|
+
{errorInfo.source === "compile"
|
|
572
|
+
? "编译错误"
|
|
573
|
+
: errorInfo.source === "server"
|
|
574
|
+
? "服务端错误"
|
|
575
|
+
: errorInfo.source === "runtime"
|
|
576
|
+
? "运行时错误"
|
|
577
|
+
: "错误"}
|
|
578
|
+
</div>
|
|
579
|
+
<pre className="w-full max-h-44 overflow-auto rounded-2xl border border-black/5 bg-white/70 p-4 text-left text-sm leading-6 text-black/80 shadow-inner dark:border-white/10 dark:bg-zinc-950/40 dark:text-white/85">
|
|
580
|
+
<code>{errorInfo.message}</code>
|
|
581
|
+
</pre>
|
|
582
|
+
</div>
|
|
583
|
+
) : null}
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div className="mt-10 text-center">
|
|
589
|
+
<h2 className="text-2xl font-semibold tracking-tight text-black/90 dark:text-white">
|
|
590
|
+
{title}
|
|
591
|
+
</h2>
|
|
592
|
+
<p className="mt-3 text-sm leading-6 text-black/60 dark:text-white/70">
|
|
593
|
+
{description}
|
|
594
|
+
</p>
|
|
595
|
+
|
|
596
|
+
<div className="mt-8 flex w-full flex-col items-center gap-3 sm:flex-row sm:justify-center">
|
|
597
|
+
<button
|
|
598
|
+
type="button"
|
|
599
|
+
className="inline-flex h-11 w-full items-center justify-center rounded-full bg-black px-5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-black/90 sm:w-auto dark:bg-white dark:text-black dark:hover:bg-white/90"
|
|
600
|
+
onClick={() => window.location.reload()}
|
|
601
|
+
>
|
|
602
|
+
刷新预览
|
|
603
|
+
</button>
|
|
604
|
+
<button
|
|
605
|
+
type="button"
|
|
606
|
+
className="inline-flex h-11 w-full items-center justify-center rounded-full border border-black/10 bg-white/70 px-5 text-sm font-medium text-black/80 shadow-sm transition-colors hover:bg-white sm:w-auto dark:border-white/10 dark:bg-zinc-950/40 dark:text-white/80 dark:hover:bg-zinc-950/60"
|
|
607
|
+
onClick={() => setDebugMode(true)}
|
|
608
|
+
>
|
|
609
|
+
查看错误详情
|
|
610
|
+
</button>
|
|
611
|
+
</div>
|
|
612
|
+
|
|
613
|
+
<p className="mt-6 text-xs text-black/45 dark:text-white/55">
|
|
614
|
+
快捷键切换遮罩 / Next 面板:{shortcutLabel}
|
|
615
|
+
</p>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
) : null}
|
|
620
|
+
|
|
621
|
+
{debugMode && overlay !== "none" ? (
|
|
622
|
+
<div className="fixed bottom-4 right-4 z-[2147483647]">
|
|
623
|
+
<button
|
|
624
|
+
type="button"
|
|
625
|
+
className="inline-flex items-center gap-2 rounded-full border border-black/10 bg-white/80 px-4 py-2 text-xs font-medium text-black/70 shadow-lg backdrop-blur-md transition-colors hover:bg-white dark:border-white/10 dark:bg-zinc-950/60 dark:text-white/80 dark:hover:bg-zinc-950/70"
|
|
626
|
+
onClick={() => setDebugMode(false)}
|
|
627
|
+
title={`返回预览遮罩(${shortcutLabel})`}
|
|
628
|
+
>
|
|
629
|
+
返回预览遮罩
|
|
630
|
+
<span className="rounded-full border border-black/10 bg-black/[0.03] px-2 py-0.5 text-[10px] text-black/55 dark:border-white/10 dark:bg-white/5 dark:text-white/70">
|
|
631
|
+
{shortcutLabel}
|
|
632
|
+
</span>
|
|
633
|
+
</button>
|
|
634
|
+
</div>
|
|
635
|
+
) : null}
|
|
636
|
+
</>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { api } from "@shared/routes";
|
|
3
|
+
import { storage } from "@/server/storage";
|
|
4
|
+
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
try {
|
|
10
|
+
const message = await storage.getMessage();
|
|
11
|
+
const body = { message };
|
|
12
|
+
|
|
13
|
+
// Keep server/client contract in sync.
|
|
14
|
+
api.greeting.get.responses[200].parse(body);
|
|
15
|
+
|
|
16
|
+
return NextResponse.json(body);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("GET /api/greeting failed:", error);
|
|
19
|
+
|
|
20
|
+
const message =
|
|
21
|
+
error instanceof Error
|
|
22
|
+
? error.message
|
|
23
|
+
: "Internal Server Error";
|
|
24
|
+
|
|
25
|
+
return NextResponse.json({ message }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|