From 7f94377f4fbc74ed4388b12c5fd6a6cbbc135d0d Mon Sep 17 00:00:00 2001 From: Nicholas Charriere Date: Tue, 22 Oct 2024 18:17:09 -0700 Subject: [PATCH] Install npm packages after plan ran. External link option (#410) * Install npm packages after plan ran. External link option * Remove useless comment * Address comments --- packages/api/ai/plan-parser.mts | 9 +++ packages/api/apps/app.mts | 33 ++++++----- packages/web/src/components/apps/header.tsx | 62 ++++++++++++++------- 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/packages/api/ai/plan-parser.mts b/packages/api/ai/plan-parser.mts index cf3ca97a..0b593f2d 100644 --- a/packages/api/ai/plan-parser.mts +++ b/packages/api/ai/plan-parser.mts @@ -160,3 +160,12 @@ export async function parsePlan( throw new Error('Failed to parse XML response'); } } + +export function getPackagesToInstall(plan: Plan): string[] { + return plan.actions + .filter( + (action): action is NpmInstallCommand => + action.type === 'command' && action.command === 'npm install', + ) + .flatMap((action) => action.packages); +} diff --git a/packages/api/apps/app.mts b/packages/api/apps/app.mts index 626bda0c..edb707e3 100644 --- a/packages/api/apps/app.mts +++ b/packages/api/apps/app.mts @@ -7,7 +7,7 @@ import { asc, desc, eq } from 'drizzle-orm'; import { npmInstall } from '../exec.mjs'; import { generateApp } from '../ai/generate.mjs'; import { toValidPackageName } from '../apps/utils.mjs'; -import { parsePlan } from '../ai/plan-parser.mjs'; +import { getPackagesToInstall, parsePlan } from '../ai/plan-parser.mjs'; function toSecondsSinceEpoch(date: Date): number { return Math.floor(date.getTime() / 1000); @@ -54,19 +54,24 @@ export async function createAppWithAi(data: CreateAppWithAiSchemaType): Promise< const plan = await parsePlan(result, app, data.prompt, randomid()); await applyPlan(app, plan); - // Run npm install again since we don't have a good way of parsing the plan to know if we should... - npmInstall({ - cwd: pathToApp(app.externalId), - stdout(data) { - console.log(data.toString('utf8')); - }, - stderr(data) { - console.error(data.toString('utf8')); - }, - onExit(code) { - console.log(`npm install exit code: ${code}`); - }, - }); + const packagesToInstall = getPackagesToInstall(plan); + + if (packagesToInstall.length > 0) { + console.log('installing packages', packagesToInstall); + npmInstall({ + cwd: pathToApp(app.externalId), + packages: packagesToInstall, + stdout(data) { + console.log(data.toString('utf8')); + }, + stderr(data) { + console.error(data.toString('utf8')); + }, + onExit(code) { + console.log(`npm install exit code: ${code}`); + }, + }); + } return app; } diff --git a/packages/web/src/components/apps/header.tsx b/packages/web/src/components/apps/header.tsx index 5535733f..f6516140 100644 --- a/packages/web/src/components/apps/header.tsx +++ b/packages/web/src/components/apps/header.tsx @@ -8,6 +8,7 @@ import { CircleAlertIcon, PanelBottomOpenIcon, PanelBottomCloseIcon, + ExternalLinkIcon, } from 'lucide-react'; import { Link } from 'react-router-dom'; import { SrcbookLogo } from '@/components/logos'; @@ -46,7 +47,7 @@ type PropsType = { export default function EditorHeader(props: PropsType) { const { app, updateApp } = useApp(); - const { start: startPreview, stop: stopPreview, status: previewStatus } = usePreview(); + const { url, start: startPreview, stop: stopPreview, status: previewStatus } = usePreview(); const { status: npmInstallStatus, nodeModulesExists } = usePackageJson(); const [isExporting, setIsExporting] = useState(false); const { open, togglePane, panelIcon } = useLogs(); @@ -170,25 +171,46 @@ export default function EditorHeader(props: PropsType) { ) : null} {props.tab === 'preview' && previewStatus !== 'stopped' ? ( - - - - - - Stop dev server - - + <> + {url && ( + + + + + + Open in new tab + + + )} + + + + + + + Stop dev server + + + ) : null}