Unverified 提交 cc811a40 authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Aggregating logs accross sources and improving logs viewer (#1956)

<!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Aggregates logs from client console, network requests, and Supabase Edge Functions into one console with filters and virtualization for faster debugging. Improves the preview console UX with better filtering, auto-scroll, and “send to chat”. - **New Features** - Unified log stream: new appConsoleEntriesAtom and combined, time-sorted entries (client console, network requests, edge-function, build-time). - Client capture: injects console interceptor and a Service Worker to track fetch requests/responses/errors; forwarded via postMessage. - Supabase Edge Function logs: new IPC handler and admin client query; incremental polling via last timestamp; function name extracted for source filtering. - Console UI: level/type/source filters, log count, virtualized list with react-virtuoso, auto-scroll when near bottom, clear filters, and “send to chat” per entry. - Preview iframe: forwards console logs, network events, and build/runtime errors into unified logs with proper levels. - **Dependencies** - Added react-virtuoso. <sup>Written for commit c8390c5ef9ab2def1135052f04156368b2b9d5d0. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Brings a unified, performant logs console and multi-source aggregation for easier debugging. > > - Introduces `appConsoleEntriesAtom` and removes `appOutputAtom`; resets logs on app switch > - Captures client logs via injected `worker/dyad_logs.js` and forwards to parent; displays in `Console` with `ConsoleEntry` UI > - Intercepts network requests/responses/errors via Service Worker (`worker/dyad-sw.js`, registered by `dyad-sw-register.js`); proxy serves SW and injects scripts > - Adds Supabase Edge Function logs: IPC handler `supabase:get-edge-logs`, admin query `getSupabaseProjectLogs`, polling via `useSupabase.loadEdgeLogs`, and `lastLogTimestampAtom`; function name extracted for source tags > - Updates `PreviewIframe` to forward console/network events and runtime errors into unified log stream > - New console UI (`Console.tsx`, `ConsoleFilters.tsx`, `ConsoleEntry.tsx`) with level/type/source filters, auto-scroll, and list virtualization via `react-virtuoso` (disabled in E2E via `E2E_TEST_BUILD`) > - Adds "Send to chat" action per log and "Clear" filter control; increases CI timeout; adds e2e tests for logs, network, chat export, and filter clearing > - Exposes `E2E_TEST_BUILD` via `get-env-vars`; adds dependency `react-virtuoso` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0c10230f707c2e6f1968bad428b16fe1f56c039f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
上级 3e051bab
import { testSkipIfWindows, Timeout } from "./helpers/test_helper";
import { expect } from "@playwright/test";
testSkipIfWindows(
"console logs should appear in the console",
async ({ po }) => {
await po.setUp();
await po.sendPrompt("tc=console-logs");
await po.approveProposal();
// Wait for app to run
const picker = po.page.getByTestId("preview-pick-element-button");
await expect(picker).toBeEnabled({ timeout: Timeout.EXTRA_LONG });
// Wait for iframe to load and app to render
const iframe = po.getPreviewIframeElement();
await expect(
iframe.contentFrame().getByText("Console Logs Test App"),
).toBeVisible({ timeout: Timeout.MEDIUM });
// Open the system messages console
// Logs are generated in useEffect when component mounts, so they may already exist
const consoleHeader = po.page.locator('text="System Messages"').first();
await consoleHeader.click();
// Wait for console to be visible and auto-scroll to complete
// Wait for at least one log entry to appear, then wait for the last one to be visible
// This ensures auto-scroll has completed
await expect(po.page.getByTestId("console-entry").first()).toBeVisible({
timeout: Timeout.MEDIUM,
});
// Wait for the last log entry to be visible (ensures auto-scroll to bottom)
await expect(async () => {
const allLogs = po.page.getByTestId("console-entry");
const count = await allLogs.count();
expect(count).toBeGreaterThan(0);
await expect(allLogs.last()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Wait for all console logs to appear - use retry logic
// Verify console.log appears
await expect(async () => {
const consoleEntry = po.page
.getByTestId("console-entry")
.filter({ hasText: "[LOG] Hello from console.log" });
const count = await consoleEntry.count();
expect(count).toBeGreaterThan(0);
await expect(consoleEntry.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Verify console.info appears
await expect(async () => {
const infoEntry = po.page
.getByTestId("console-entry")
.filter({ hasText: "[INFO] Info message" });
const count = await infoEntry.count();
expect(count).toBeGreaterThan(0);
await expect(infoEntry.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Verify console.warn appears
await expect(async () => {
const warnEntry = po.page
.getByTestId("console-entry")
.filter({ hasText: "[WARN] Warning message" });
const count = await warnEntry.count();
expect(count).toBeGreaterThan(0);
await expect(warnEntry.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Verify console.error appears
await expect(async () => {
const errorEntry = po.page
.getByTestId("console-entry")
.filter({ hasText: "[ERROR] Test error message" });
const count = await errorEntry.count();
expect(count).toBeGreaterThan(0);
await expect(errorEntry.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
},
);
testSkipIfWindows(
"network requests and responses should appear in the console",
async ({ po }) => {
await po.setUp();
await po.sendPrompt("tc=network-requests");
await po.approveProposal();
// Wait for app to run
const picker = po.page.getByTestId("preview-pick-element-button");
await expect(picker).toBeEnabled({ timeout: Timeout.EXTRA_LONG });
// Wait for iframe to load - wait for content to appear
const iframe = po.getPreviewIframeElement();
const iframeFrame = iframe.contentFrame();
await expect(
iframeFrame.getByText("Network Requests Test App"),
).toBeVisible({ timeout: Timeout.MEDIUM });
// Wait for service worker to be ready
// Service worker registration is async, so we wait for it to be active
// We check by waiting for network request logs to appear, which indicates SW is ready
// If SW isn't ready, network requests will still work but won't be logged
// Open the system messages console
// Network requests happen in useEffect, so they may already be in progress or complete
const consoleHeader = po.page.locator('text="System Messages"').first();
await consoleHeader.click();
// Wait for console to be visible and auto-scroll to complete
// Wait for at least one log entry to appear, then wait for the last one to be visible
// This ensures auto-scroll has completed
await expect(po.page.getByTestId("console-entry").first()).toBeVisible({
timeout: Timeout.MEDIUM,
});
// Wait for the last log entry to be visible (ensures auto-scroll to bottom)
await expect(async () => {
const allLogs = po.page.getByTestId("console-entry");
const count = await allLogs.count();
expect(count).toBeGreaterThan(0);
await expect(allLogs.last()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Wait for network requests to appear - use retry logic with proper conditions
// Network requests happen in useEffect, so they may take a moment
// Wait for the GET request log to appear
// Format: "→ GET https://jsonplaceholder.typicode.com/posts/1"
await expect(async () => {
const getRequestLocator = po.page
.getByTestId("console-entry")
.filter({ hasText: /→ GET.*jsonplaceholder\.typicode\.com\/posts\/1/ });
const count = await getRequestLocator.count();
expect(count).toBeGreaterThan(0);
await expect(getRequestLocator.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Wait for the GET response log to appear
// Format: "[200] GET https://jsonplaceholder.typicode.com/posts/1 (durationms)"
await expect(async () => {
const getResponseLocator = po.page.getByTestId("console-entry").filter({
hasText: /\[200\].*GET.*jsonplaceholder\.typicode\.com\/posts\/1/,
});
const count = await getResponseLocator.count();
expect(count).toBeGreaterThan(0);
await expect(getResponseLocator.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Wait for the POST request log to appear
// Format: "→ POST https://jsonplaceholder.typicode.com/posts"
await expect(async () => {
const postRequestLocator = po.page
.getByTestId("console-entry")
.filter({ hasText: /→ POST.*jsonplaceholder\.typicode\.com\/posts/ });
const count = await postRequestLocator.count();
expect(count).toBeGreaterThan(0);
await expect(postRequestLocator.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
// Wait for the POST response log to appear
// Format: "[201] POST https://jsonplaceholder.typicode.com/posts (durationms)"
await expect(async () => {
const postResponseLocator = po.page.getByTestId("console-entry").filter({
hasText: /\[201\].*POST.*jsonplaceholder\.typicode\.com\/posts/,
});
const count = await postResponseLocator.count();
expect(count).toBeGreaterThan(0);
await expect(postResponseLocator.first()).toBeVisible();
}).toPass({ timeout: Timeout.MEDIUM });
},
);
testSkipIfWindows(
"clicking send to chat button adds log to chat input",
async ({ po }) => {
await po.setUp();
// Create an app with console output using fixture
await po.sendPrompt("tc=write-index");
await po.approveProposal();
// Wait for app to run
const picker = po.page.getByTestId("preview-pick-element-button");
await expect(picker).toBeEnabled({ timeout: Timeout.EXTRA_LONG });
// Open the system messages console
const consoleHeader = po.page.locator('text="System Messages"').first();
await consoleHeader.click();
// Wait for the log entry to appear
const consoleEntry = await po.page.getByTestId("console-entry").last();
await expect(consoleEntry).toBeVisible({ timeout: Timeout.EXTRA_LONG });
// Hover over the log entry to reveal the send to chat button
await consoleEntry.hover();
// Click the send to chat button (MessageSquare icon)
const sendToChatButton = consoleEntry.getByTestId("send-to-chat");
await sendToChatButton.click({ timeout: Timeout.EXTRA_LONG });
// Check that the chat input now contains the log information
const chatInput = po.getChatInput();
const inputValue = await chatInput.textContent();
// Verify the log was added to chat input
expect(inputValue).toContain("```");
},
);
testSkipIfWindows("clear filters button works", async ({ po }) => {
await po.setUp();
// Create a basic app using fixture
await po.sendPrompt("tc=write-index");
await po.approveProposal();
// Wait for app to run
await po.page
.getByTestId("preview-pick-element-button")
.click({ timeout: Timeout.EXTRA_LONG });
// Open the system messages console
const consoleHeader = po.page.locator('text="System Messages"').first();
await consoleHeader.click();
// Apply a filter
const levelFilter = po.page
.locator("select")
.filter({ hasText: "All Levels" });
await levelFilter.selectOption("error");
// Check that clear button appears
const clearButton = po.page.getByRole("button", { name: "Clear" });
await expect(clearButton).toBeVisible();
// Click clear button
await clearButton.click();
// Verify filters are reset
const filterValue = await levelFilter.inputValue();
expect(filterValue).toBe("all");
});
Creating a React app with console logging examples.
<dyad-write path="src/pages/Index.tsx" description="adding console logs">
import { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('Hello from console.log');
console.info('Info message');
console.warn('Warning message');
console.error('Test error message');
}, []);
return (
<div>
<h1>Console Logs Test App</h1>
<p>Check the System Messages console for logs.</p>
</div>
);
}
export default App;
</dyad-write>
Creating a React app that makes network requests to test network logging.
<dyad-write path="src/pages/Index.tsx" description="adding network requests">
import { useEffect } from 'react';
function App() {
useEffect(() => {
// Make a GET request
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => {
console.log('Fetched data:', data);
})
.catch(error => {
console.error('Fetch error:', error);
});
// Make a POST request
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Test Post',
body: 'This is a test post',
userId: 1,
}),
})
.then(response => response.json())
.then(data => {
console.log('Posted data:', data);
});
}, []);
return (
<div>
<h1>Network Requests Test App</h1>
<p>Check the System Messages console for network logs.</p>
</div>
);
}
export default App;
</dyad-write>
......@@ -88,6 +88,7 @@
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
"react-shiki": "^0.9.0",
"react-virtuoso": "^4.17.0",
"recast": "^0.23.11",
"remark-gfm": "^4.0.1",
"shell-env": "^4.0.1",
......@@ -18061,6 +18062,16 @@
}
}
},
"node_modules/react-virtuoso": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.17.0.tgz",
"integrity": "sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w==",
"license": "MIT",
"peerDependencies": {
"react": ">=16 || >=17 || >= 18 || >= 19",
"react-dom": ">=16 || >=17 || >= 18 || >=19"
}
},
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
......
......@@ -164,6 +164,7 @@
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
"react-shiki": "^0.9.0",
"react-virtuoso": "^4.17.0",
"recast": "^0.23.11",
"remark-gfm": "^4.0.1",
"shell-env": "^4.0.1",
......
......@@ -7,7 +7,11 @@ import { TitleBar } from "./TitleBar";
import { useEffect, type ReactNode } from "react";
import { useRunApp } from "@/hooks/useRunApp";
import { useAtomValue, useSetAtom } from "jotai";
import { previewModeAtom, selectedAppIdAtom } from "@/atoms/appAtoms";
import {
appConsoleEntriesAtom,
previewModeAtom,
selectedAppIdAtom,
} from "@/atoms/appAtoms";
import { useSettings } from "@/hooks/useSettings";
import type { ZoomLevel } from "@/lib/schemas";
import { selectedComponentsPreviewAtom } from "@/atoms/previewAtoms";
......@@ -24,6 +28,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
);
const setChatInput = useSetAtom(chatInputValueAtom);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const setConsoleEntries = useSetAtom(appConsoleEntriesAtom);
useEffect(() => {
const zoomLevel = settings?.zoomLevel ?? DEFAULT_ZOOM_LEVEL;
......@@ -73,6 +78,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
useEffect(() => {
setChatInput("");
setSelectedComponentsPreview([]);
setConsoleEntries([]);
}, [selectedAppId]);
return (
......
import { atom } from "jotai";
import type { App, AppOutput, Version } from "@/ipc/ipc_types";
import type { App, Version } from "@/ipc/ipc_types";
import type { UserSettings } from "@/lib/schemas";
export const currentAppAtom = atom<App | null>(null);
......@@ -11,7 +11,22 @@ export const previewModeAtom = atom<
"preview" | "code" | "problems" | "configure" | "publish" | "security"
>("preview");
export const selectedVersionIdAtom = atom<string | null>(null);
export const appOutputAtom = atom<AppOutput[]>([]);
export interface ConsoleEntry {
level: "info" | "warn" | "error";
type:
| "server"
| "client"
| "edge-function"
| "network-requests"
| "build-time";
message: string;
timestamp: number;
sourceName?: string;
appId: number;
}
export const appConsoleEntriesAtom = atom<ConsoleEntry[]>([]);
export const appUrlAtom = atom<
| { appUrl: string; appId: number; originalUrl: string }
| { appUrl: null; appId: null; originalUrl: null }
......
......@@ -13,3 +13,6 @@ export const supabaseErrorAtom = atom<Error | null>(null);
// Define atom for storing the currently selected Supabase project
export const selectedSupabaseProjectAtom = atom<string | null>(null);
// Define atom for tracking the last log timestamp per project (for incremental log loading)
export const lastLogTimestampAtom = atom<Record<string, number>>({});
import {
MessageSquare,
AlertCircle,
AlertTriangle,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { chatInputValueAtom } from "@/atoms/chatAtoms";
interface ConsoleEntryProps {
type:
| "server"
| "client"
| "edge-function"
| "network-requests"
| "build-time";
level: "info" | "warn" | "error";
timestamp: number;
message: string;
sourceName?: string;
typeFilter?: string;
isExpanded?: boolean;
onToggleExpand?: () => void;
}
const formatTimestamp = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleTimeString("en-US", { hour12: false });
};
const MAX_MESSAGE_LENGTH = 300;
export const ConsoleEntryComponent = (props: ConsoleEntryProps) => {
const {
timestamp,
message,
sourceName,
level,
type,
typeFilter,
isExpanded = false,
onToggleExpand,
} = props;
const setChatInput = useSetAtom(chatInputValueAtom);
const isTruncated = message.length > MAX_MESSAGE_LENGTH;
const displayMessage =
isTruncated && !isExpanded
? message.slice(0, MAX_MESSAGE_LENGTH) + "..."
: message;
const handleSendToChat = () => {
const time = new Date(timestamp).toLocaleTimeString("en-US", {
hour12: false,
});
const prefix = sourceName ? `[${sourceName}]` : "";
const formattedLog = `[${time}] ${level.toUpperCase()} ${prefix}: ${message}`;
setChatInput((prev) => {
return `${prev}\n\`\`\`\n${formattedLog}\n\`\`\``;
});
};
// Determine styling based on log level
const getBackgroundClass = () => {
if (level === "error") {
return "bg-red-50 dark:bg-red-950/30 hover:bg-red-100 dark:hover:bg-red-950/50";
}
if (level === "warn") {
return "bg-yellow-50 dark:bg-yellow-950/30 hover:bg-yellow-100 dark:hover:bg-yellow-950/50";
}
return "hover:bg-gray-100 dark:hover:bg-gray-800";
};
return (
<div
data-testid="console-entry"
className={`relative pr-8 px-2 py-1 my-0.5 rounded transition-colors group ${getBackgroundClass()}`}
>
<div className="flex items-start gap-2 flex-wrap">
{level === "error" && (
<AlertCircle size={14} className="text-red-500 shrink-0 mt-0.5" />
)}
{level === "warn" && (
<AlertTriangle
size={14}
className="text-yellow-500 shrink-0 mt-0.5"
/>
)}
<span
className="text-gray-400 shrink-0"
title={new Date(timestamp).toLocaleString()}
>
{formatTimestamp(timestamp)}
</span>
<span className="flex-1 whitespace-pre-wrap break-all">
{sourceName && (
<span className="text-gray-500 shrink-0 text-[10px] px-1 py-0.5 mr-2 bg-gray-200 dark:bg-gray-700 rounded">
{sourceName}
</span>
)}
{typeFilter == "all" && type && (
<span className="text-purple-500 shrink-0 text-[10px] px-1 py-0.5 mr-2 bg-gray-200 dark:bg-gray-700 rounded">
{type}
</span>
)}
{displayMessage}
{isTruncated && (
<button
onClick={onToggleExpand}
className="ml-2 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center gap-1 text-xs"
>
{isExpanded ? (
<>
Show less <ChevronUp size={12} />
</>
) : (
<>
Show more <ChevronDown size={12} />
</>
)}
</button>
)}
</span>
</div>
<button
onClick={handleSendToChat}
title="Send to chat"
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
data-testid="send-to-chat"
>
<MessageSquare size={12} className="text-gray-500" />
</button>
</div>
);
};
import { Filter, X } from "lucide-react";
interface ConsoleFiltersProps {
levelFilter: "all" | "info" | "warn" | "error";
typeFilter:
| "all"
| "server"
| "client"
| "edge-function"
| "network-requests"
| "build-time";
sourceFilter: string;
onLevelFilterChange: (value: "all" | "info" | "warn" | "error") => void;
onTypeFilterChange: (
value:
| "all"
| "server"
| "client"
| "edge-function"
| "network-requests"
| "build-time",
) => void;
onSourceFilterChange: (value: string) => void;
onClearFilters: () => void;
uniqueSources: string[];
totalLogs: number;
showFilters: boolean;
}
export const ConsoleFilters = ({
levelFilter,
typeFilter,
sourceFilter,
onLevelFilterChange,
onTypeFilterChange,
onSourceFilterChange,
onClearFilters,
uniqueSources,
totalLogs,
showFilters,
}: ConsoleFiltersProps) => {
const hasActiveFilters =
levelFilter !== "all" || typeFilter !== "all" || sourceFilter !== "";
if (!showFilters) return null;
return (
<div className="bg-white dark:bg-gray-950 border-b border-border p-2 flex flex-wrap gap-2 items-center animate-in fade-in slide-in-from-top-2 duration-300">
<Filter size={14} className="text-gray-500" />
{/* Level filter */}
<select
value={levelFilter}
onChange={(e) =>
onLevelFilterChange(
e.target.value as "all" | "info" | "warn" | "error",
)
}
className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<option value="all">All Levels</option>
<option value="info">Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
{/* Type filter */}
<select
value={typeFilter}
onChange={(e) =>
onTypeFilterChange(
e.target.value as
| "all"
| "server"
| "client"
| "edge-function"
| "network-requests"
| "build-time",
)
}
className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<option value="all">All Types</option>
<option value="server">Server</option>
<option value="client">Client</option>
<option value="edge-function">Edge Function</option>
<option value="network-requests">Network Requests</option>
<option value="build-time">Build Time</option>
</select>
{/* Source filter */}
{uniqueSources.length > 0 && (
<select
value={sourceFilter}
onChange={(e) => onSourceFilterChange(e.target.value)}
className="text-xs px-2 py-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<option value="">All Sources</option>
{uniqueSources.map((source) => (
<option key={source} value={source}>
{source}
</option>
))}
</select>
)}
{/* Clear filters button */}
{hasActiveFilters && (
<button
onClick={onClearFilters}
className="text-xs px-2 py-1 flex items-center gap-1 border border-border rounded bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<X size={12} />
Clear
</button>
)}
<div className="ml-auto text-xs text-gray-500">{totalLogs} logs</div>
</div>
);
};
import {
selectedAppIdAtom,
appUrlAtom,
appOutputAtom,
appConsoleEntriesAtom,
previewErrorMessageAtom,
} from "@/atoms/appAtoms";
import { useAtomValue, useSetAtom, useAtom } from "jotai";
......@@ -171,7 +171,7 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
export const PreviewIframe = ({ loading }: { loading: boolean }) => {
const selectedAppId = useAtomValue(selectedAppIdAtom);
const { appUrl, originalUrl } = useAtomValue(appUrlAtom);
const setAppOutput = useSetAtom(appOutputAtom);
const setConsoleEntries = useSetAtom(appConsoleEntriesAtom);
// State to trigger iframe reload
const [reloadKey, setReloadKey] = useState(0);
const [errorMessage, setErrorMessage] = useAtom(previewErrorMessageAtom);
......@@ -346,6 +346,78 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
return;
}
// Handle console logs from the iframe
if (event.data?.type === "console-log") {
const { level, args } = event.data;
const formattedMessage = `[${level.toUpperCase()}] ${args.join(" ")}`;
const logLevel =
level === "error" ? "error" : level === "warn" ? "warn" : "info";
setConsoleEntries((prev) => [
...prev,
{
level: logLevel,
type: "client",
message: formattedMessage,
timestamp: Date.now(),
appId: selectedAppId!,
},
]);
return;
}
// Handle network requests from the iframe
if (event.data?.type === "network-request") {
const { method, url } = event.data;
const formattedMessage = `→ ${method} ${url}`;
setConsoleEntries((prev) => [
...prev,
{
level: "info",
type: "network-requests",
message: formattedMessage,
timestamp: Date.now(),
appId: selectedAppId!,
},
]);
return;
}
// Handle network responses from the iframe
if (event.data?.type === "network-response") {
const { method, url, status, duration } = event.data;
const formattedMessage = `[${status}] ${method} ${url} (${duration}ms)`;
const level = status >= 400 ? "error" : status >= 300 ? "warn" : "info";
setConsoleEntries((prev) => [
...prev,
{
level,
type: "network-requests",
message: formattedMessage,
timestamp: Date.now(),
appId: selectedAppId!,
},
]);
return;
}
// Handle network errors from the iframe
if (event.data?.type === "network-error") {
const { method, url, status, error, duration } = event.data;
const statusCode = status && status !== 0 ? `[${status}] ` : "";
const formattedMessage = `${statusCode}${method} ${url} - ${error} (${duration}ms)`;
setConsoleEntries((prev) => [
...prev,
{
level: "error",
type: "network-requests",
message: formattedMessage,
timestamp: Date.now(),
appId: selectedAppId!,
},
]);
return;
}
if (event.data?.type === "dyad-component-selector-initialized") {
setIsComponentSelectorInitialized(true);
iframeRef.current?.contentWindow?.postMessage(
......@@ -480,26 +552,28 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
}\nStack trace: ${stack}`;
console.error("Iframe error:", errorMessage);
setErrorMessage({ message: errorMessage, source: "preview-app" });
setAppOutput((prev) => [
setConsoleEntries((prev) => [
...prev,
{
level: "error",
type: "client",
message: `Iframe error: ${errorMessage}`,
type: "client-error",
appId: selectedAppId!,
timestamp: Date.now(),
appId: selectedAppId!,
},
]);
} else if (type === "build-error-report") {
console.debug(`Build error report: ${payload}`);
const errorMessage = `${payload?.message} from file ${payload?.file}.\n\nSource code:\n${payload?.frame}`;
setErrorMessage({ message: errorMessage, source: "preview-app" });
setAppOutput((prev) => [
setConsoleEntries((prev) => [
...prev,
{
level: "error",
type: "client",
message: `Build error report: ${JSON.stringify(payload)}`,
type: "client-error",
appId: selectedAppId!,
timestamp: Date.now(),
appId: selectedAppId!,
},
]);
} else if (type === "pushState" || type === "replaceState") {
......
import { useAtom, useAtomValue } from "jotai";
import {
appOutputAtom,
appConsoleEntriesAtom,
previewModeAtom,
previewPanelKeyAtom,
selectedAppIdAtom,
......@@ -17,6 +17,7 @@ import { Console } from "./Console";
import { useRunApp } from "@/hooks/useRunApp";
import { PublishPanel } from "./PublishPanel";
import { SecurityPanel } from "./SecurityPanel";
import { useSupabase } from "@/hooks/useSupabase";
interface ConsoleHeaderProps {
isOpen: boolean;
......@@ -54,13 +55,15 @@ export function PreviewPanel() {
const selectedAppId = useAtomValue(selectedAppIdAtom);
const [isConsoleOpen, setIsConsoleOpen] = useState(false);
const { runApp, stopApp, loading, app } = useRunApp();
const { loadEdgeLogs } = useSupabase();
const runningAppIdRef = useRef<number | null>(null);
const key = useAtomValue(previewPanelKeyAtom);
const appOutput = useAtomValue(appOutputAtom);
const consoleEntries = useAtomValue(appConsoleEntriesAtom);
const messageCount = appOutput.length;
const latestMessage =
messageCount > 0 ? appOutput[messageCount - 1]?.message : undefined;
consoleEntries.length > 0
? consoleEntries[consoleEntries.length - 1]?.message
: undefined;
useEffect(() => {
const previousAppId = runningAppIdRef.current;
......@@ -106,6 +109,27 @@ export function PreviewPanel() {
// Dependencies: run effect when selectedAppId changes.
// runApp/stopApp are stable due to useCallback.
}, [selectedAppId, runApp, stopApp]);
// Load edge logs if app has Supabase project configured
useEffect(() => {
const projectId = app?.supabaseProjectId;
if (!projectId) return;
// Load logs immediately
loadEdgeLogs(projectId).catch((error) => {
console.error("Failed to load edge logs:", error);
});
// Poll for new logs every 5 seconds
const intervalId = setInterval(() => {
loadEdgeLogs(projectId).catch((error) => {
console.error("Failed to load edge logs:", error);
});
}, 5000);
return () => clearInterval(intervalId);
}, [app?.supabaseProjectId, loadEdgeLogs]);
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
......
......@@ -2,7 +2,7 @@ import { useCallback } from "react";
import { atom } from "jotai";
import { IpcClient } from "@/ipc/ipc_client";
import {
appOutputAtom,
appConsoleEntriesAtom,
appUrlAtom,
currentAppAtom,
previewPanelKeyAtom,
......@@ -18,7 +18,7 @@ const useRunAppLoadingAtom = atom(false);
export function useRunApp() {
const [loading, setLoading] = useAtom(useRunAppLoadingAtom);
const [app, setApp] = useAtom(currentAppAtom);
const setAppOutput = useSetAtom(appOutputAtom);
const setConsoleEntries = useSetAtom(appConsoleEntriesAtom);
const [, setAppUrlObj] = useAtom(appUrlAtom);
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
const appId = useAtomValue(selectedAppIdAtom);
......@@ -65,13 +65,26 @@ export function useRunApp() {
return; // Don't add to regular output
}
// Add to regular app output
setAppOutput((prev) => [...prev, output]);
// Add to console entries
const level =
output.type === "stderr" || output.type === "client-error"
? "error"
: "info";
setConsoleEntries((prev) => [
...prev,
{
level,
type: "build-time",
message: output.message,
timestamp: output.timestamp,
appId: output.appId,
},
]);
// Process proxy server output
processProxyServerOutput(output);
},
[setAppOutput],
[setConsoleEntries],
);
const runApp = useCallback(
async (appId: number) => {
......@@ -88,13 +101,14 @@ export function useRunApp() {
return prevAppUrlObj; // No change needed
});
setAppOutput((prev) => [
setConsoleEntries((prev) => [
...prev,
{
level: "info",
type: "build-time",
message: "Trying to restart app...",
type: "stdout",
appId,
timestamp: Date.now(),
appId,
},
]);
const app = await ipcClient.getApp(appId);
......@@ -166,13 +180,14 @@ export function useRunApp() {
// Clear the URL and add restart message
setAppUrlObj({ appUrl: null, appId: null, originalUrl: null });
setAppOutput((prev) => [
setConsoleEntries((prev) => [
...prev,
{
level: "info",
type: "build-time",
message: "Restarting app...",
type: "stdout",
appId,
timestamp: Date.now(),
appId,
},
]);
......@@ -211,7 +226,7 @@ export function useRunApp() {
[
appId,
setApp,
setAppOutput,
setConsoleEntries,
setAppUrlObj,
setPreviewPanelKey,
processAppOutput,
......
import { useCallback } from "react";
import { useAtom } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import {
supabaseProjectsAtom,
supabaseBranchesAtom,
supabaseLoadingAtom,
supabaseErrorAtom,
selectedSupabaseProjectAtom,
lastLogTimestampAtom,
} from "@/atoms/supabaseAtoms";
import { appConsoleEntriesAtom, selectedAppIdAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { SetSupabaseAppProjectParams } from "@/ipc/ipc_types";
......@@ -18,6 +20,9 @@ export function useSupabase() {
const [selectedProject, setSelectedProject] = useAtom(
selectedSupabaseProjectAtom,
);
const setConsoleEntries = useSetAtom(appConsoleEntriesAtom);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const [lastLogTimestamp, setLastLogTimestamp] = useAtom(lastLogTimestampAtom);
const ipcClient = IpcClient.getInstance();
......@@ -98,6 +103,70 @@ export function useSupabase() {
[ipcClient, setError, setLoading],
);
/**
* Load edge function logs for a Supabase project
* Uses timestamp tracking to only fetch new logs on subsequent calls
*/
const loadEdgeLogs = useCallback(
async (projectId: string) => {
if (!selectedAppId) return;
// Use last timestamp if available, otherwise fetch logs from the past 10 minutes
const lastTimestamp = lastLogTimestamp[projectId];
const timestampStart = lastTimestamp ?? Date.now() - 10 * 60 * 1000; // 10 minutes ago
setLoading(true);
try {
// Fetch logs - handler returns ConsoleEntry[] already formatted
const logs = await ipcClient.getSupabaseEdgeLogs({
projectId,
timestampStart,
appId: selectedAppId,
});
if (logs.length === 0) {
// Even if no logs, set the timestamp so we don't keep looking back 10 minutes
if (!lastTimestamp) {
setLastLogTimestamp((prev) => ({
...prev,
[projectId]: Date.now(),
}));
}
setError(null);
return;
}
// Logs are already in ConsoleEntry format, just append them
setConsoleEntries((prev) => [...prev, ...logs]);
// Update the last timestamp for this project
const latestLog = logs.reduce((latest, log) =>
log.timestamp > latest.timestamp ? log : latest,
);
setLastLogTimestamp((prev) => ({
...prev,
[projectId]: latestLog.timestamp,
}));
setError(null);
} catch (error) {
console.error("Error loading Supabase edge logs:", error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);
}
},
[
ipcClient,
setConsoleEntries,
setError,
setLoading,
selectedAppId,
lastLogTimestamp,
setLastLogTimestamp,
],
);
/**
* Select a project for current use
*/
......@@ -116,6 +185,7 @@ export function useSupabase() {
selectedProject,
loadProjects,
loadBranches,
loadEdgeLogs,
setAppProject,
unsetAppProject,
selectProject,
......
......@@ -5,7 +5,9 @@ import { apps } from "../../db/schema";
import {
getSupabaseClient,
listSupabaseBranches,
getSupabaseProjectLogs,
} from "../../supabase_admin/supabase_management_client";
import { extractFunctionName } from "../../supabase_admin/supabase_utils";
import {
createLoggedHandler,
createTestOnlyLoggedHandler,
......@@ -14,6 +16,7 @@ import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_
import { safeSend } from "../utils/safe_sender";
import { SetSupabaseAppProjectParams, SupabaseBranch } from "../ipc_types";
import type { ConsoleEntry } from "../../atoms/appAtoms";
const logger = log.scope("supabase_handlers");
const handle = createLoggedHandler(logger);
......@@ -45,6 +48,49 @@ export function registerSupabaseHandlers() {
},
);
// Get edge function logs for a Supabase project
handle(
"supabase:get-edge-logs",
async (
_,
{
projectId,
timestampStart,
appId,
}: { projectId: string; timestampStart?: number; appId: number },
): Promise<Array<ConsoleEntry>> => {
const response = await getSupabaseProjectLogs(projectId, timestampStart);
if (response.error) {
const errorMsg =
typeof response.error === "string"
? response.error
: JSON.stringify(response.error);
throw new Error(`Failed to fetch logs: ${errorMsg}`);
}
const rawLogs = response.result || [];
// Transform to ConsoleEntry format
return rawLogs.map((log: any) => {
const metadata = log.metadata?.[0] || {};
const level = metadata.level || "info";
const eventMessage = log.event_message || "";
const functionName = extractFunctionName(eventMessage);
return {
level:
level === "error" ? "error" : level === "warn" ? "warn" : "info",
type: "edge-function" as const,
message: eventMessage,
timestamp: log.timestamp / 1000, // Convert from microseconds to milliseconds
sourceName: functionName,
appId,
};
});
},
);
// Set app project - links a Dyad app to a Supabase project
handle(
"supabase:set-app-project",
......
......@@ -77,6 +77,7 @@ import type {
AgentToolConsentRequestPayload,
AgentToolConsentResponseParams,
} from "./ipc_types";
import type { ConsoleEntry } from "../atoms/appAtoms";
import type { Template } from "../shared/templates";
import type {
AppChatContext,
......@@ -1024,6 +1025,14 @@ export class IpcClient {
return this.ipcRenderer.invoke("supabase:list-branches", params);
}
public async getSupabaseEdgeLogs(params: {
projectId: string;
timestampStart?: number;
appId: number;
}): Promise<Array<ConsoleEntry>> {
return this.ipcRenderer.invoke("supabase:get-edge-logs", params);
}
public async setSupabaseAppProject(
params: SetSupabaseAppProjectParams,
): Promise<void> {
......
......@@ -542,6 +542,36 @@ export interface SetSupabaseAppProjectParams {
parentProjectId?: string;
appId: number;
}
// Supabase Logs
export interface LogMetadata {
// For Edge Functions
function?: string;
request_id?: string;
status?: number;
// For Database logs
query?: string;
table?: string;
rows_affected?: number;
// For Auth logs
user_id?: string;
event?: string;
// Additional dynamic fields
[key: string]: any;
}
export interface SupabaseLog {
id: string;
timestamp: string;
log_type: "function" | "database" | "auth" | "api" | "realtime" | "system";
event_message: string;
metadata?: LogMetadata;
body?: any;
}
export interface SetNodePathParams {
nodePath: string;
}
......
......@@ -80,6 +80,7 @@ const validInvokeChannels = [
"get-system-debug-info",
"supabase:list-projects",
"supabase:list-branches",
"supabase:get-edge-logs",
"supabase:set-app-project",
"supabase:unset-app-project",
"local-models:list-ollama",
......
......@@ -356,7 +356,16 @@ const token = authHeader.replace('Bearer ', '')
- Use <resource-link> for guidance
9. Logging:
- Implement comprehensive logging for debugging purposes
- Implement comprehensive logging for debugging purposes.
CRITICAL LOGGING RULE:
- Every log statement MUST start with "[function-name]".
- This applies to ALL console methods: console.log, console.error, console.warn, console.debug, console.info.
- Do NOT add any console statements that do not follow this format under any circumstances.
Examples:
- Example: console.log("[function-name] message", { data });
- Example: console.error("[function-name] error message", { error });
10. Linking:
Use <resource-link> to link to the relevant edge function
......
......@@ -171,6 +171,64 @@ export async function getSupabaseProjectName(
return project?.name || `<project not found for: ${projectId}>`;
}
export async function getSupabaseProjectLogs(
projectId: string,
timestampStart?: number,
): Promise<any> {
const supabase = await getSupabaseClient();
// Build SQL query with optional timestamp filter
let sqlQuery = `
SELECT
timestamp,
event_message,
metadata
FROM function_logs`;
if (timestampStart) {
// Convert milliseconds to microseconds and wrap in TIMESTAMP_MICROS for BigQuery
sqlQuery += `\nWHERE timestamp > TIMESTAMP_MICROS(${timestampStart * 1000})`;
}
sqlQuery += `\nORDER BY timestamp ASC
LIMIT 1000`;
// Calculate time range for API parameters
const now = new Date();
const isoTimestampEnd = now.toISOString();
// Default to last 10 minutes if no start timestamp provided
const isoTimestampStart = timestampStart
? new Date(timestampStart).toISOString()
: new Date(now.getTime() - 10 * 60 * 1000).toISOString();
// Encode SQL query for URL
const encodedSql = encodeURIComponent(sqlQuery);
const url = `https://api.supabase.com/v1/projects/${projectId}/analytics/endpoints/logs.all?sql=${encodedSql}&iso_timestamp_start=${isoTimestampStart}&iso_timestamp_end=${isoTimestampEnd}`;
logger.info(`Fetching logs from: ${url}`);
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${(supabase as any).options.accessToken}`,
},
});
if (response.status !== 200) {
const errorText = await response.text();
logger.error(`Failed to fetch logs (${response.status}): ${errorText}`);
throw new Error(
`Failed to fetch logs: ${response.statusText} (${response.status}) - ${errorText}`,
);
}
const jsonResponse = await response.json();
logger.info(`Received ${jsonResponse.result?.length || 0} logs`);
return jsonResponse;
}
export async function executeSupabaseSql({
supabaseProjectId,
query,
......
......@@ -5,6 +5,17 @@ import { deploySupabaseFunction } from "./supabase_management_client";
const logger = log.scope("supabase_utils");
/**
* Extracts function name from Supabase edge function log event_message
* Example: "[todo-activity] fetched 0 recent todos\n" -> "todo-activity"
* @param eventMessage - The event_message string from the log
* @returns The function name or undefined if not found
*/
export function extractFunctionName(eventMessage: string): string | undefined {
const match = eventMessage.match(/^\[([^\]]+)\]/);
return match ? match[1] : undefined;
}
/**
* Checks if a file path is a Supabase edge function
* (i.e., inside supabase/functions/ but NOT in _shared/)
......
/**
* dyad-sw-register.js – Service Worker registration script
* This script is injected into the HTML to register the Service Worker
* and forward messages to the parent window
*/
(function () {
// Check if Service Workers are supported
if (!("serviceWorker" in navigator)) {
console.warn("[Dyad] Service Workers are not supported in this browser");
return;
}
// Register the Service Worker
navigator.serviceWorker
.register("/dyad-sw.js", { scope: "/" })
.then((registration) => {
console.log("[Dyad] Service Worker registered:", registration.scope);
// Handle updates
registration.addEventListener("updatefound", () => {
console.log("[Dyad] Service Worker update found");
});
})
.catch((error) => {
console.error("[Dyad] Service Worker registration failed:", error);
});
// Listen for messages from the Service Worker
navigator.serviceWorker.addEventListener("message", (event) => {
// Forward all messages to the parent window
try {
window.parent.postMessage(event.data, "*");
} catch (e) {
console.error("[Dyad] Failed to forward message to parent:", e);
}
});
// Also listen for messages from the active Service Worker controller
if (navigator.serviceWorker.controller) {
console.log("[Dyad] Service Worker controller already active");
}
})();
/**
* dyad-sw.js – Service Worker for network request interception
* Intercepts all fetch requests and reports them to the client
*/
// Service Worker installation
self.addEventListener("install", (_event) => {
console.log("[Dyad SW] Installing...");
// Skip waiting to activate immediately
self.skipWaiting();
});
// Service Worker activation
self.addEventListener("activate", (event) => {
console.log("[Dyad SW] Activating...");
// Claim all clients immediately
event.waitUntil(self.clients.claim());
});
// Intercept all fetch requests
self.addEventListener("fetch", (event) => {
const request = event.request;
// ---- Guardrails: avoid breaking things we shouldn't touch ----
// Skip navigations (HTML document loads) to reduce dev-time weirdness.
if (request.mode === "navigate") return;
// Only handle http(s)
let urlObj;
try {
urlObj = new URL(request.url);
} catch {
return;
}
if (urlObj.protocol !== "http:" && urlObj.protocol !== "https:") return;
// Chrome SW footgun: only-if-cached must be same-origin or it throws.
if (request.cache === "only-if-cached" && request.mode !== "same-origin")
return;
// Skip noisy Vite and Next.js development module requests
const pathname = urlObj.pathname;
if (
// Vite
pathname.includes("/node_modules") || // Vite deps
pathname.includes("/@vite/") || // Vite client/HMR
pathname.includes("/__vite_ping") || // Vite ping
// Next.js
pathname.includes("/_next/static/") || // Static assets (chunks, CSS, media)
pathname.includes("/_next/webpack-hmr") || // Next.js HMR
pathname.includes("/__nextjs_original-stack-frame") || // Error overlay internals
pathname.includes("/__webpack_hmr") || // Webpack HMR
pathname.includes(".hot-update.") // HMR update chunks
) {
return;
}
const startTime = Date.now();
const url = request.url;
const method = request.method;
// Helper to send message to the initiating client or broadcast as fallback
const postMessage = (message) => {
const sendMessage = async () => {
// Prefer sending to the initiating client
if (event.clientId) {
const client = await self.clients.get(event.clientId);
if (client) {
client.postMessage(message);
return;
}
}
// Fallback: broadcast to all clients within SW scope
const clients = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
});
for (const client of clients) {
client.postMessage(message);
}
};
// Wrap with event.waitUntil to ensure completion
event.waitUntil(sendMessage());
};
// Send initial request info
postMessage({
type: "network-request",
method,
url,
requestType: "fetch",
timestamp: new Date().toISOString(),
});
// Pass through the request and monitor the response
event.respondWith(
fetch(event.request)
.then((response) => {
const duration = Date.now() - startTime;
// Send response info
postMessage({
type: "network-response",
method,
url,
status: response.status,
statusText: response.statusText,
duration,
requestType: "fetch",
timestamp: new Date().toISOString(),
});
// Return the response unchanged
return response;
})
.catch((error) => {
const duration = Date.now() - startTime;
// Send error info
postMessage({
type: "network-error",
method,
url,
status: 0,
error: error.message,
duration,
requestType: "fetch",
timestamp: new Date().toISOString(),
});
// Re-throw the error
throw error;
}),
);
});
/**
* dyad_logs.js – Console interception script
* Intercepts all console methods and forwards them to the parent window
*/
(function () {
// Store original console methods
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
const originalInfo = console.info;
const originalDebug = console.debug;
// Helper function to safely stringify arguments
function stringifyArgs(args) {
return args.map((arg) => {
if (arg === null) return "null";
if (arg === undefined) return "undefined";
if (typeof arg === "object") {
try {
return JSON.stringify(arg, null, 2);
} catch {
return "[Object: unable to stringify]";
}
}
return String(arg);
});
}
// Intercept console.log
console.log = function (...args) {
window.parent.postMessage(
{
type: "console-log",
level: "log",
args: stringifyArgs(args),
timestamp: new Date().toISOString(),
},
"*",
);
originalLog.apply(console, args);
};
// Intercept console.warn
console.warn = function (...args) {
window.parent.postMessage(
{
type: "console-log",
level: "warn",
args: stringifyArgs(args),
timestamp: new Date().toISOString(),
},
"*",
);
originalWarn.apply(console, args);
};
// Intercept console.error
console.error = function (...args) {
window.parent.postMessage(
{
type: "console-log",
level: "error",
args: stringifyArgs(args),
timestamp: new Date().toISOString(),
},
"*",
);
originalError.apply(console, args);
};
// Intercept console.info
console.info = function (...args) {
window.parent.postMessage(
{
type: "console-log",
level: "info",
args: stringifyArgs(args),
timestamp: new Date().toISOString(),
},
"*",
);
originalInfo.apply(console, args);
};
// Intercept console.debug
console.debug = function (...args) {
window.parent.postMessage(
{
type: "console-log",
level: "debug",
args: stringifyArgs(args),
timestamp: new Date().toISOString(),
},
"*",
);
originalDebug.apply(console, args);
};
})();
......@@ -41,6 +41,7 @@ let dyadComponentSelectorClientContent = null;
let dyadScreenshotClientContent = null;
let htmlToImageContent = null;
let dyadVisualEditorClientContent = null;
let dyadLogsContent = null;
try {
const htmlToImagePath = path.join(
......@@ -140,6 +141,40 @@ try {
);
}
try {
const dyadLogsPath = path.join(__dirname, "dyad_logs.js");
dyadLogsContent = fs.readFileSync(dyadLogsPath, "utf-8");
parentPort?.postMessage("[proxy-worker] dyad_logs.js loaded.");
} catch (error) {
parentPort?.postMessage(
`[proxy-worker] Failed to read dyad_logs.js: ${error.message}`,
);
}
// Load Service Worker files
let dyadSwContent = null;
let dyadSwRegisterContent = null;
try {
const dyadSwPath = path.join(__dirname, "dyad-sw.js");
dyadSwContent = fs.readFileSync(dyadSwPath, "utf-8");
parentPort?.postMessage("[proxy-worker] dyad-sw.js loaded.");
} catch (error) {
parentPort?.postMessage(
`[proxy-worker] Failed to read dyad-sw.js: ${error.message}`,
);
}
try {
const dyadSwRegisterPath = path.join(__dirname, "dyad-sw-register.js");
dyadSwRegisterContent = fs.readFileSync(dyadSwRegisterPath, "utf-8");
parentPort?.postMessage("[proxy-worker] dyad-sw-register.js loaded.");
} catch (error) {
parentPort?.postMessage(
`[proxy-worker] Failed to read dyad-sw-register.js: ${error.message}`,
);
}
/* ---------------------- helper: need to inject? ------------------------ */
function needsInjection(pathname) {
// Inject for routes without a file extension (e.g., "/foo", "/foo/bar", "/")
......@@ -208,6 +243,20 @@ function injectHTML(buf) {
'<script>console.warn("[proxy-worker] dyad visual editor client was not injected.");</script>',
);
}
if (dyadLogsContent) {
scripts.push(`<script>${dyadLogsContent}</script>`);
} else {
scripts.push(
'<script>console.warn("[proxy-worker] dyad_logs.js was not injected.");</script>',
);
}
if (dyadSwRegisterContent) {
scripts.push(`<script>${dyadSwRegisterContent}</script>`);
} else {
scripts.push(
'<script>console.warn("[proxy-worker] dyad-sw-register.js was not injected.");</script>',
);
}
const allScripts = scripts.join("\n");
const headRegex = /<head[^>]*>/i;
......@@ -235,6 +284,23 @@ function buildTargetURL(clientReq) {
/* ----------------------------------------------------------------------- */
const server = http.createServer((clientReq, clientRes) => {
// Special handling for Service Worker file
if (clientReq.url === "/dyad-sw.js") {
if (dyadSwContent) {
clientRes.writeHead(200, {
"content-type": "application/javascript",
"service-worker-allowed": "/",
"cache-control": "no-cache",
});
clientRes.end(dyadSwContent);
return;
} else {
clientRes.writeHead(404, { "content-type": "text/plain" });
clientRes.end("Service Worker file not found");
return;
}
}
let target;
try {
target = buildTargetURL(clientReq);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论