Provide a rebuild option (restart w/ re-install node modules)

This commit is contained in:
Will Chen
2025-04-21 16:46:10 -07:00
parent 09b3bf3fee
commit ee3d2e7f4e
5 changed files with 125 additions and 57 deletions

View File

@@ -283,20 +283,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
} }
function mapActionToButton(action: SuggestedAction) { function mapActionToButton(action: SuggestedAction) {
const { restartApp } = useRunApp();
switch (action.id) { switch (action.id) {
case "restart-app":
return (
<Button
variant="outline"
size="sm"
className="rounded-xl"
key={action.id}
onClick={restartApp}
>
Restart app
</Button>
);
default: default:
console.error(`Unsupported action: ${action.id}`); console.error(`Unsupported action: ${action.id}`);
return ( return (

View File

@@ -15,12 +15,23 @@ import {
ChevronUp, ChevronUp,
Logs, Logs,
RefreshCw, RefreshCw,
MoreVertical,
Trash2,
Cog,
CirclePower,
Power,
} from "lucide-react"; } from "lucide-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useEffect, useRef, useState, useCallback } from "react"; import { useEffect, useRef, useState, useCallback } from "react";
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels"; import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { Console } from "./Console"; import { Console } from "./Console";
import { useRunApp } from "@/hooks/useRunApp"; import { useRunApp } from "@/hooks/useRunApp";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
type PreviewMode = "preview" | "code"; type PreviewMode = "preview" | "code";
@@ -28,6 +39,7 @@ interface PreviewHeaderProps {
previewMode: PreviewMode; previewMode: PreviewMode;
setPreviewMode: (mode: PreviewMode) => void; setPreviewMode: (mode: PreviewMode) => void;
onRestart: () => void; onRestart: () => void;
onCleanRestart: () => void;
} }
interface ConsoleHeaderProps { interface ConsoleHeaderProps {
@@ -41,6 +53,7 @@ const PreviewHeader = ({
previewMode, previewMode,
setPreviewMode, setPreviewMode,
onRestart, onRestart,
onCleanRestart,
}: PreviewHeaderProps) => ( }: PreviewHeaderProps) => (
<div className="flex items-center justify-between px-4 py-2 border-b border-border"> <div className="flex items-center justify-between px-4 py-2 border-b border-border">
<div className="relative flex space-x-2 bg-[var(--background-darkest)] rounded-md p-0.5"> <div className="relative flex space-x-2 bg-[var(--background-darkest)] rounded-md p-0.5">
@@ -73,14 +86,37 @@ const PreviewHeader = ({
<span>Code</span> <span>Code</span>
</button> </button>
</div> </div>
<button <div className="flex items-center">
onClick={onRestart} <button
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors" onClick={onRestart}
title="Restart App" className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
> title="Restart App"
<RefreshCw size={16} /> >
<span>Restart</span> <Power size={16} />
</button> <span>Restart</span>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
title="More options"
>
<MoreVertical size={16} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuItem onClick={onCleanRestart}>
<Cog size={16} />
<div className="flex flex-col">
<span>Rebuild</span>
<span className="text-xs text-muted-foreground">
Re-installs node_modules and restarts
</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div> </div>
); );
@@ -126,6 +162,10 @@ export function PreviewPanel() {
restartApp(); restartApp();
}, [restartApp]); }, [restartApp]);
const handleCleanRestart = useCallback(() => {
restartApp({ removeNodeModules: true });
}, [restartApp]);
useEffect(() => { useEffect(() => {
const previousAppId = runningAppIdRef.current; const previousAppId = runningAppIdRef.current;
@@ -176,6 +216,7 @@ export function PreviewPanel() {
previewMode={previewMode} previewMode={previewMode}
setPreviewMode={setPreviewMode} setPreviewMode={setPreviewMode}
onRestart={handleRestart} onRestart={handleRestart}
onCleanRestart={handleCleanRestart}
/> />
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<PanelGroup direction="vertical"> <PanelGroup direction="vertical">

View File

@@ -70,41 +70,56 @@ export function useRunApp() {
} }
}, []); }, []);
const restartApp = useCallback(async () => { const restartApp = useCallback(
if (appId === null) { async ({
return; removeNodeModules = false,
} }: { removeNodeModules?: boolean } = {}) => {
setLoading(true); if (appId === null) {
try { return;
const ipcClient = IpcClient.getInstance(); }
console.debug("Restarting app", appId); setLoading(true);
try {
const ipcClient = IpcClient.getInstance();
console.debug(
"Restarting app",
appId,
removeNodeModules ? "with node_modules cleanup" : ""
);
// Clear the URL and add restart message // Clear the URL and add restart message
setAppUrlObj({ appUrl: null, appId: null }); setAppUrlObj({ appUrl: null, appId: null });
setAppOutput((prev) => [ setAppOutput((prev) => [
...prev, ...prev,
{ message: "Restarting app...", type: "stdout", appId }, { message: "Restarting app...", type: "stdout", appId },
]); ]);
const app = await ipcClient.getApp(appId); const app = await ipcClient.getApp(appId);
setApp(app); setApp(app);
await ipcClient.restartApp(appId, (output) => { await ipcClient.restartApp(
setAppOutput((prev) => [...prev, output]); appId,
// Check if the output contains a localhost URL (output) => {
const urlMatch = output.message.match(/(https?:\/\/localhost:\d+\/?)/); setAppOutput((prev) => [...prev, output]);
if (urlMatch) { // Check if the output contains a localhost URL
setAppUrlObj({ appUrl: urlMatch[1], appId }); const urlMatch = output.message.match(
} /(https?:\/\/localhost:\d+\/?)/
}); );
setError(null); if (urlMatch) {
} catch (error) { setAppUrlObj({ appUrl: urlMatch[1], appId });
console.error(`Error restarting app ${appId}:`, error); }
setError(error instanceof Error ? error : new Error(String(error))); },
} finally { removeNodeModules
setPreviewPanelKey((prevKey) => prevKey + 1); );
setLoading(false); setError(null);
} } catch (error) {
}, []); console.error(`Error restarting app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setPreviewPanelKey((prevKey) => prevKey + 1);
setLoading(false);
}
},
[appId, setApp, setAppOutput, setAppUrlObj, setError, setPreviewPanelKey]
);
const refreshAppIframe = useCallback(async () => { const refreshAppIframe = useCallback(async () => {
setPreviewPanelKey((prevKey) => prevKey + 1); setPreviewPanelKey((prevKey) => prevKey + 1);

View File

@@ -378,7 +378,10 @@ export function registerAppHandlers() {
"restart-app", "restart-app",
async ( async (
event: Electron.IpcMainInvokeEvent, event: Electron.IpcMainInvokeEvent,
{ appId }: { appId: number } {
appId,
removeNodeModules,
}: { appId: number; removeNodeModules?: boolean }
) => { ) => {
logger.log(`Restarting app ${appId}`); logger.log(`Restarting app ${appId}`);
return withLock(appId, async () => { return withLock(appId, async () => {
@@ -410,6 +413,24 @@ export function registerAppHandlers() {
} }
const appPath = getDyadAppPath(app.path); const appPath = getDyadAppPath(app.path);
// Remove node_modules if requested
if (removeNodeModules) {
const nodeModulesPath = path.join(appPath, "node_modules");
logger.log(
`Removing node_modules for app ${appId} at ${nodeModulesPath}`
);
if (fs.existsSync(nodeModulesPath)) {
await fsPromises.rm(nodeModulesPath, {
recursive: true,
force: true,
});
logger.log(`Successfully removed node_modules for app ${appId}`);
} else {
logger.log(`No node_modules directory found for app ${appId}`);
}
}
logger.debug( logger.debug(
`Executing app ${appId} in path ${app.path} after restart request` `Executing app ${appId} in path ${app.path} after restart request`
); // Adjusted log ); // Adjusted log

View File

@@ -317,10 +317,14 @@ export class IpcClient {
// Restart a running app // Restart a running app
public async restartApp( public async restartApp(
appId: number, appId: number,
onOutput: (output: AppOutput) => void onOutput: (output: AppOutput) => void,
removeNodeModules?: boolean
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
try { try {
const result = await this.ipcRenderer.invoke("restart-app", { appId }); const result = await this.ipcRenderer.invoke("restart-app", {
appId,
removeNodeModules,
});
this.appStreams.set(appId, { onOutput }); this.appStreams.set(appId, { onOutput });
return result; return result;
} catch (error) { } catch (error) {