Proxy server to inject shim (#178)

things to test:

- [x] allow real URL to open in new window
- [x] packaging in electron?
- [ ] does it work on windows?
- [x] make sure it works with older apps
- [x] what about cache / reuse? - maybe use a bigger range of ports??
This commit is contained in:
Will Chen
2025-05-16 23:28:26 -07:00
committed by GitHub
parent 63e41454c7
commit 5966dd7f4b
15 changed files with 563 additions and 158 deletions

View File

@@ -1,268 +0,0 @@
(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) {
console.error("Could not parse URL", 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", () => {
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");
});
(function watchForViteErrorOverlay() {
// --- Configuration for the observer ---
// We only care about direct children being added or removed.
const config = {
childList: true, // Observe additions/removals of child nodes
subtree: false, // IMPORTANT: Do *not* observe descendants, only direct children
};
// --- Callback function executed when mutations are observed ---
const observerCallback = function (mutationsList) {
// Iterate through all mutations that just occurred
for (const mutation of mutationsList) {
// We are only interested in nodes that were added
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
// Check each added node
for (const node of mutation.addedNodes) {
// Check if it's an ELEMENT_NODE (type 1) and has the correct ID
if (
node.nodeType === Node.ELEMENT_NODE &&
node.tagName === "vite-error-overlay".toUpperCase()
) {
reportViteErrorOverlay(node);
}
}
}
}
};
function reportViteErrorOverlay(node) {
console.log(`Detected vite error overlay: ${node}`);
try {
window.parent.postMessage(
{
type: "build-error-report",
payload: {
message: node.shadowRoot.querySelector(".message").textContent,
file: node.shadowRoot.querySelector(".file").textContent,
frame: node.shadowRoot.querySelector(".frame").textContent,
},
},
PARENT_TARGET_ORIGIN,
);
} catch (error) {
console.error("Could not report vite error overlay", error);
}
}
// --- Wait for DOM ready logic ---
if (document.readyState === "loading") {
// The document is still loading, wait for DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
if (!document.body) {
console.error(
"document.body does not exist - something very weird happened",
);
return;
}
const node = document.body.querySelector("vite-error-overlay");
if (node) {
reportViteErrorOverlay(node);
}
const observer = new MutationObserver(observerCallback);
observer.observe(document.body, config);
});
console.log(
"Document loading, waiting for DOMContentLoaded to set up observer.",
);
} else {
if (!document.body) {
console.error(
"document.body does not exist - something very weird happened",
);
return;
}
// The DOM is already interactive or complete
console.log("DOM already ready, setting up observer immediately.");
const observer = new MutationObserver(observerCallback);
observer.observe(document.body, config);
}
})();
})();

View File

@@ -56,7 +56,6 @@
"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",

View File

@@ -143,9 +143,6 @@ importers:
sonner:
specifier: ^1.5.0
version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
stacktrace-js:
specifier: ^2.0.2
version: 2.0.2
tailwind-merge:
specifier: ^2.5.2
version: 2.6.0
@@ -1618,9 +1615,6 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
error-stack-parser@2.1.4:
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
esbuild@0.25.3:
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
engines: {node: '>=18'}
@@ -2230,22 +2224,6 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map@0.5.6:
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
engines: {node: '>=0.10.0'}
stack-generator@2.0.10:
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
stacktrace-gps@3.1.2:
resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==}
stacktrace-js@2.0.2:
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -3782,10 +3760,6 @@ snapshots:
emoji-regex@9.2.2: {}
error-stack-parser@2.1.4:
dependencies:
stackframe: 1.3.4
esbuild@0.25.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.3
@@ -4389,25 +4363,6 @@ snapshots:
source-map-js@1.2.1: {}
source-map@0.5.6: {}
stack-generator@2.0.10:
dependencies:
stackframe: 1.3.4
stackframe@1.3.4: {}
stacktrace-gps@3.1.2:
dependencies:
source-map: 0.5.6
stackframe: 1.3.4
stacktrace-js@2.0.2:
dependencies:
error-stack-parser: 2.1.4
stack-generator: 2.0.10
stacktrace-gps: 3.1.2
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0

View File

@@ -1,98 +1,13 @@
import { defineConfig, Plugin, HtmlTagDescriptor } from "vite";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import fs from "fs";
export function devErrorAndNavigationPlugin(): Plugin {
let stacktraceJsContent: string | null = null;
let dyadShimContent: string | null = null;
return {
name: "dev-error-and-navigation-handler",
apply: "serve",
configResolved() {
const stackTraceLibPath = path.join(
"node_modules",
"stacktrace-js",
"dist",
"stacktrace.min.js",
);
if (stackTraceLibPath) {
try {
stacktraceJsContent = fs.readFileSync(stackTraceLibPath, "utf-8");
} catch (error) {
console.error(
`[dyad-shim] Failed to read stacktrace.js from ${stackTraceLibPath}:`,
error,
);
stacktraceJsContent = null;
}
} else {
console.error(`[dyad-shim] stacktrace.js not found.`);
}
const dyadShimPath = path.join("dyad-shim.js");
if (dyadShimPath) {
try {
dyadShimContent = fs.readFileSync(dyadShimPath, "utf-8");
} catch (error) {
console.error(
`[dyad-shim] Failed to read dyad-shim from ${dyadShimPath}:`,
error,
);
dyadShimContent = null;
}
} else {
console.error(`[dyad-shim] stacktrace.js not found.`);
}
},
transformIndexHtml(html) {
const tags: HtmlTagDescriptor[] = [];
// 1. Inject stacktrace.js
if (stacktraceJsContent) {
tags.push({
tag: "script",
injectTo: "head-prepend",
children: stacktraceJsContent,
});
} else {
tags.push({
tag: "script",
injectTo: "head-prepend",
children:
"console.warn('[dyad-shim] stacktrace.js library was not injected.');",
});
}
// 2. Inject dyad shim
if (dyadShimContent) {
tags.push({
tag: "script",
injectTo: "head-prepend",
children: dyadShimContent,
});
} else {
tags.push({
tag: "script",
injectTo: "head-prepend",
children: "console.warn('[dyad-shim] dyad shim was not injected.');",
});
}
return { html, tags };
},
};
}
export default defineConfig(() => ({
server: {
host: "::",
port: 8080,
},
plugins: [react(), devErrorAndNavigationPlugin()],
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),