Files
moreminimore-vibe/src/ipc/handlers/app_upgrade_handlers.ts
Will Chen 47f3ec460a Add Capacitor support (#483)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-06-24 14:35:05 -07:00

292 lines
8.7 KiB
TypeScript

import { createLoggedHandler } from "./safe_handle";
import log from "electron-log";
import { AppUpgrade } from "../ipc_types";
import { db } from "../../db";
import { apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths";
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { gitAddAll, gitCommit } from "../utils/git_utils";
import { simpleSpawn } from "../utils/simpleSpawn";
export const logger = log.scope("app_upgrade_handlers");
const handle = createLoggedHandler(logger);
const availableUpgrades: Omit<AppUpgrade, "isNeeded">[] = [
{
id: "component-tagger",
title: "Enable select component to edit",
description:
"Installs the Dyad component tagger Vite plugin and its dependencies.",
manualUpgradeUrl: "https://dyad.sh/docs/upgrades/select-component",
},
{
id: "capacitor",
title: "Upgrade to hybrid mobile app with Capacitor",
description:
"Adds Capacitor to your app lets it run on iOS and Android in addition to the web.",
manualUpgradeUrl: "https://dyad.sh/docs/guides/mobile-app#upgrade-your-app",
},
];
async function getApp(appId: number) {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});
if (!app) {
throw new Error(`App with id ${appId} not found`);
}
return app;
}
function isViteApp(appPath: string): boolean {
const viteConfigPathJs = path.join(appPath, "vite.config.js");
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
return fs.existsSync(viteConfigPathTs) || fs.existsSync(viteConfigPathJs);
}
function isComponentTaggerUpgradeNeeded(appPath: string): boolean {
const viteConfigPathJs = path.join(appPath, "vite.config.js");
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
let viteConfigPath;
if (fs.existsSync(viteConfigPathTs)) {
viteConfigPath = viteConfigPathTs;
} else if (fs.existsSync(viteConfigPathJs)) {
viteConfigPath = viteConfigPathJs;
} else {
return false;
}
try {
const viteConfigContent = fs.readFileSync(viteConfigPath, "utf-8");
return !viteConfigContent.includes("@dyad-sh/react-vite-component-tagger");
} catch (e) {
logger.error("Error reading vite config", e);
return false;
}
}
function isCapacitorUpgradeNeeded(appPath: string): boolean {
// Check if it's a Vite app first
if (!isViteApp(appPath)) {
return false;
}
// Check if Capacitor is already installed
const capacitorConfigJs = path.join(appPath, "capacitor.config.js");
const capacitorConfigTs = path.join(appPath, "capacitor.config.ts");
const capacitorConfigJson = path.join(appPath, "capacitor.config.json");
// If any Capacitor config exists, the upgrade is not needed
if (
fs.existsSync(capacitorConfigJs) ||
fs.existsSync(capacitorConfigTs) ||
fs.existsSync(capacitorConfigJson)
) {
return false;
}
return true;
}
async function applyComponentTagger(appPath: string) {
const viteConfigPathJs = path.join(appPath, "vite.config.js");
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
let viteConfigPath;
if (fs.existsSync(viteConfigPathTs)) {
viteConfigPath = viteConfigPathTs;
} else if (fs.existsSync(viteConfigPathJs)) {
viteConfigPath = viteConfigPathJs;
} else {
throw new Error("Could not find vite.config.js or vite.config.ts");
}
let content = await fs.promises.readFile(viteConfigPath, "utf-8");
// Add import statement if not present
if (
!content.includes(
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
)
) {
// Add it after the last import statement
const lines = content.split("\n");
let lastImportIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("import ")) {
lastImportIndex = i;
break;
}
}
lines.splice(
lastImportIndex + 1,
0,
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
);
content = lines.join("\n");
}
// Add plugin to plugins array
if (content.includes("plugins: [")) {
if (!content.includes("dyadComponentTagger()")) {
content = content.replace(
"plugins: [",
"plugins: [dyadComponentTagger(), ",
);
}
} else {
throw new Error(
"Could not find `plugins: [` in vite.config.ts. Manual installation required.",
);
}
await fs.promises.writeFile(viteConfigPath, content);
// Install the dependency
await new Promise<void>((resolve, reject) => {
logger.info("Installing component-tagger dependency");
const process = spawn(
"pnpm add -D @dyad-sh/react-vite-component-tagger || npm install --save-dev --legacy-peer-deps @dyad-sh/react-vite-component-tagger",
{
cwd: appPath,
shell: true,
stdio: "pipe",
},
);
process.stdout?.on("data", (data) => logger.info(data.toString()));
process.stderr?.on("data", (data) => logger.error(data.toString()));
process.on("close", (code) => {
if (code === 0) {
logger.info("component-tagger dependency installed successfully");
resolve();
} else {
logger.error(`Failed to install dependency, exit code ${code}`);
reject(new Error("Failed to install dependency"));
}
});
process.on("error", (err) => {
logger.error("Failed to spawn pnpm", err);
reject(err);
});
});
// Commit changes
try {
logger.info("Staging and committing changes");
await gitAddAll({ path: appPath });
await gitCommit({
path: appPath,
message: "[dyad] add Dyad component tagger",
});
logger.info("Successfully committed changes");
} catch (err) {
logger.warn(
`Failed to commit changes. This may happen if the project is not in a git repository, or if there are no changes to commit.`,
err,
);
}
}
async function applyCapacitor({
appName,
appPath,
}: {
appName: string;
appPath: string;
}) {
// Install Capacitor dependencies
await simpleSpawn({
command:
"pnpm add @capacitor/core @capacitor/cli @capacitor/ios @capacitor/android || npm install @capacitor/core @capacitor/cli @capacitor/ios @capacitor/android --legacy-peer-deps",
cwd: appPath,
successMessage: "Capacitor dependencies installed successfully",
errorPrefix: "Failed to install Capacitor dependencies",
});
// Initialize Capacitor
await simpleSpawn({
command: `npx cap init "${appName}" "com.example.${appName.toLowerCase().replace(/[^a-z0-9]/g, "")}" --web-dir=dist`,
cwd: appPath,
successMessage: "Capacitor initialized successfully",
errorPrefix: "Failed to initialize Capacitor",
});
// Add iOS and Android platforms
await simpleSpawn({
command: "npx cap add ios && npx cap add android",
cwd: appPath,
successMessage: "iOS and Android platforms added successfully",
errorPrefix: "Failed to add iOS and Android platforms",
});
// Commit changes
try {
logger.info("Staging and committing Capacitor changes");
await gitAddAll({ path: appPath });
await gitCommit({
path: appPath,
message: "[dyad] add Capacitor for mobile app support",
});
logger.info("Successfully committed Capacitor changes");
} catch (err) {
logger.warn(
`Failed to commit changes. This may happen if the project is not in a git repository, or if there are no changes to commit.`,
err,
);
throw new Error(
"Failed to commit Capacitor changes. Please commit them manually. Error: " +
err,
);
}
}
export function registerAppUpgradeHandlers() {
handle(
"get-app-upgrades",
async (_, { appId }: { appId: number }): Promise<AppUpgrade[]> => {
const app = await getApp(appId);
const appPath = getDyadAppPath(app.path);
const upgradesWithStatus = availableUpgrades.map((upgrade) => {
let isNeeded = false;
if (upgrade.id === "component-tagger") {
isNeeded = isComponentTaggerUpgradeNeeded(appPath);
} else if (upgrade.id === "capacitor") {
isNeeded = isCapacitorUpgradeNeeded(appPath);
}
return { ...upgrade, isNeeded };
});
return upgradesWithStatus;
},
);
handle(
"execute-app-upgrade",
async (_, { appId, upgradeId }: { appId: number; upgradeId: string }) => {
if (!upgradeId) {
throw new Error("upgradeId is required");
}
const app = await getApp(appId);
const appPath = getDyadAppPath(app.path);
if (upgradeId === "component-tagger") {
await applyComponentTagger(appPath);
} else if (upgradeId === "capacitor") {
await applyCapacitor({ appName: app.name, appPath });
} else {
throw new Error(`Unknown upgrade id: ${upgradeId}`);
}
},
);
}