Unverified 提交 79b7f865 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Emit clean stack traces from iframe (#63)

上级 1bbfedc6
(function () {
const isInsideIframe = window.parent !== window;
if (!isInsideIframe) return;
let previousUrl = window.location.href;
const PARENT_TARGET_ORIGIN = "*";
// --- History API Overrides ---
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
const handleStateChangeAndNotify = (originalMethod, state, title, url) => {
const oldUrlForMessage = previousUrl;
let newUrl;
try {
newUrl = url
? new URL(url, window.location.href).href
: window.location.href;
} catch (e) {
newUrl = window.location.href;
}
const navigationType =
originalMethod === originalPushState ? "pushState" : "replaceState";
try {
// Pass the original state directly
originalMethod.call(history, state, title, url);
previousUrl = window.location.href;
window.parent.postMessage(
{
type: navigationType,
payload: { oldUrl: oldUrlForMessage, newUrl: newUrl },
},
PARENT_TARGET_ORIGIN
);
} catch (e) {
console.error(
`[vite-dev-plugin] Error calling original ${navigationType}: `,
e
);
window.parent.postMessage(
{
type: "navigation-error",
payload: {
operation: navigationType,
message: e.message,
error: e.toString(),
stateAttempted: state,
urlAttempted: url,
},
},
PARENT_TARGET_ORIGIN
);
}
};
history.pushState = function (state, title, url) {
handleStateChangeAndNotify(originalPushState, state, title, url);
};
history.replaceState = function (state, title, url) {
handleStateChangeAndNotify(originalReplaceState, state, title, url);
};
// --- Listener for Back/Forward Navigation (popstate event) ---
window.addEventListener("popstate", (event) => {
const currentUrl = window.location.href;
previousUrl = currentUrl;
});
// --- Listener for Commands from Parent ---
window.addEventListener("message", (event) => {
if (
event.source !== window.parent ||
!event.data ||
typeof event.data !== "object"
)
return;
if (event.data.type === "navigate") {
const direction = event.data.payload?.direction;
if (direction === "forward") history.forward();
else if (direction === "backward") history.back();
}
});
// --- Sourcemapped Error Handling ---
function sendSourcemappedErrorToParent(error, sourceType) {
if (typeof window.StackTrace === "undefined") {
console.error("[vite-dev-plugin] StackTrace object not found.");
// Send simplified raw data if StackTrace isn't available
window.parent.postMessage(
{
type: sourceType,
payload: {
message: error?.message || String(error),
stack:
error?.stack || "<no stack available - StackTrace.js missing>",
},
},
PARENT_TARGET_ORIGIN
);
return;
}
window.StackTrace.fromError(error)
.then((stackFrames) => {
const sourcemappedStack = stackFrames
.map((sf) => sf.toString())
.join("\n");
const payload = {
message: error?.message || String(error),
stack: sourcemappedStack,
};
window.parent.postMessage(
{
type: "iframe-sourcemapped-error",
payload: { ...payload, originalSourceType: sourceType },
},
PARENT_TARGET_ORIGIN
);
})
.catch((mappingError) => {
console.error(
"[vite-dev-plugin] Error during stacktrace sourcemapping:",
mappingError
);
const payload = {
message: error?.message || String(error),
// Provide the raw stack or an indication of mapping failure
stack: error?.stack
? `Sourcemapping failed: ${mappingError.message}\n--- Raw Stack ---\n${error.stack}`
: `Sourcemapping failed: ${mappingError.message}\n<no raw stack available>`,
};
window.parent.postMessage(
{
type: "iframe-sourcemapped-error",
payload: { ...payload, originalSourceType: sourceType },
},
PARENT_TARGET_ORIGIN
);
});
}
window.addEventListener("error", (event) => {
let error = event.error;
if (!(error instanceof Error)) {
window.parent.postMessage(
{
type: "window-error",
payload: {
message: error.toString(),
stack: "<no stack available - an improper error was thrown>",
},
},
PARENT_TARGET_ORIGIN
);
return;
}
sendSourcemappedErrorToParent(error, "window-error");
});
window.addEventListener("unhandledrejection", (event) => {
let error = event.reason;
if (!(error instanceof Error)) {
window.parent.postMessage(
{
type: "unhandled-rejection",
payload: {
message: event.reason.toString(),
stack:
"<no stack available - an improper error was thrown (promise)>",
},
},
PARENT_TARGET_ORIGIN
);
return;
}
sendSourcemappedErrorToParent(error, "unhandled-rejection");
});
})();
......@@ -56,6 +56,7 @@
"react-router-dom": "^6.26.2",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"stacktrace-js": "^2.0.2",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.3",
......@@ -67,7 +68,7 @@
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
......@@ -77,6 +78,6 @@
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
"vite": "^6.3.4"
}
}
\ No newline at end of file
差异被折叠。
差异被折叠。
......@@ -45,7 +45,9 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
{/* Error message in the middle */}
<div className="px-6 py-1 text-sm">
<div className="text-red-700 dark:text-red-300 text-wrap">{error}</div>
<div className="text-red-700 dark:text-red-300 text-wrap font-mono whitespace-pre-wrap break-words text-xs">
{error}
</div>
</div>
{/* Tip message */}
......@@ -159,8 +161,16 @@ export const PreviewIframe = ({
const { type, payload } = event.data;
if (type === "window-error") {
const errorMessage = `Error in ${payload.filename} (line ${payload.lineno}, col ${payload.colno}): ${payload.message}`;
if (
type === "window-error" ||
type === "unhandled-rejection" ||
type === "iframe-sourcemapped-error"
) {
const stack =
type === "iframe-sourcemapped-error"
? payload.stack.split("\n").slice(0, 1).join("\n")
: payload.stack;
const errorMessage = `Error ${payload.message}\nStack trace: ${stack}`;
console.error("Iframe error:", errorMessage);
setErrorMessage(errorMessage);
setAppOutput((prev) => [
......@@ -171,18 +181,6 @@ export const PreviewIframe = ({
appId: selectedAppId!,
},
]);
} else if (type === "unhandled-rejection") {
const errorMessage = `Unhandled Promise Rejection: ${payload.reason}`;
console.error("Iframe unhandled rejection:", errorMessage);
setErrorMessage(errorMessage);
setAppOutput((prev) => [
...prev,
{
message: `Iframe unhandled rejection: ${errorMessage}`,
type: "client-error",
appId: selectedAppId!,
},
]);
} else if (type === "pushState" || type === "replaceState") {
console.debug(`Navigation event: ${type}`, payload);
......@@ -201,10 +199,6 @@ export const PreviewIframe = ({
newHistory[currentHistoryPosition] = payload.newUrl;
setNavigationHistory(newHistory);
}
// Update navigation buttons state
setCanGoBack(currentHistoryPosition > 0);
setCanGoForward(currentHistoryPosition < navigationHistory.length - 1);
}
};
......@@ -212,6 +206,12 @@ export const PreviewIframe = ({
return () => window.removeEventListener("message", handleMessage);
}, [navigationHistory, currentHistoryPosition, selectedAppId]);
useEffect(() => {
// Update navigation buttons state
setCanGoBack(currentHistoryPosition > 0);
setCanGoForward(currentHistoryPosition < navigationHistory.length - 1);
}, [navigationHistory, currentHistoryPosition]);
// Initialize navigation history when iframe loads
useEffect(() => {
if (appUrl) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论