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) {
const { restartApp } = useRunApp();
switch (action.id) {
case "restart-app":
return (
<Button
variant="outline"
size="sm"
className="rounded-xl"
key={action.id}
onClick={restartApp}
>
Restart app
</Button>
);
default:
console.error(`Unsupported action: ${action.id}`);
return (

View File

@@ -15,12 +15,23 @@ import {
ChevronUp,
Logs,
RefreshCw,
MoreVertical,
Trash2,
Cog,
CirclePower,
Power,
} from "lucide-react";
import { motion } from "framer-motion";
import { useEffect, useRef, useState, useCallback } from "react";
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { Console } from "./Console";
import { useRunApp } from "@/hooks/useRunApp";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
type PreviewMode = "preview" | "code";
@@ -28,6 +39,7 @@ interface PreviewHeaderProps {
previewMode: PreviewMode;
setPreviewMode: (mode: PreviewMode) => void;
onRestart: () => void;
onCleanRestart: () => void;
}
interface ConsoleHeaderProps {
@@ -41,6 +53,7 @@ const PreviewHeader = ({
previewMode,
setPreviewMode,
onRestart,
onCleanRestart,
}: PreviewHeaderProps) => (
<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">
@@ -73,14 +86,37 @@ const PreviewHeader = ({
<span>Code</span>
</button>
</div>
<button
onClick={onRestart}
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>
</button>
<div className="flex items-center">
<button
onClick={onRestart}
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"
>
<Power size={16} />
<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>
);
@@ -126,6 +162,10 @@ export function PreviewPanel() {
restartApp();
}, [restartApp]);
const handleCleanRestart = useCallback(() => {
restartApp({ removeNodeModules: true });
}, [restartApp]);
useEffect(() => {
const previousAppId = runningAppIdRef.current;
@@ -176,6 +216,7 @@ export function PreviewPanel() {
previewMode={previewMode}
setPreviewMode={setPreviewMode}
onRestart={handleRestart}
onCleanRestart={handleCleanRestart}
/>
<div className="flex-1 overflow-hidden">
<PanelGroup direction="vertical">

View File

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

View File

@@ -378,7 +378,10 @@ export function registerAppHandlers() {
"restart-app",
async (
event: Electron.IpcMainInvokeEvent,
{ appId }: { appId: number }
{
appId,
removeNodeModules,
}: { appId: number; removeNodeModules?: boolean }
) => {
logger.log(`Restarting app ${appId}`);
return withLock(appId, async () => {
@@ -410,6 +413,24 @@ export function registerAppHandlers() {
}
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(
`Executing app ${appId} in path ${app.path} after restart request`
); // Adjusted log

View File

@@ -317,10 +317,14 @@ export class IpcClient {
// Restart a running app
public async restartApp(
appId: number,
onOutput: (output: AppOutput) => void
onOutput: (output: AppOutput) => void,
removeNodeModules?: boolean
): Promise<{ success: boolean }> {
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 });
return result;
} catch (error) {