@@ -28,13 +28,15 @@ import {
2828} from "@/db/widgets" ;
2929
3030const execAsync = promisify ( execCb ) ;
31+ const TEMPLATE_INSTALL_CMD = "npm install --include=dev" ;
3132
3233// ── Types ──
3334
3435interface WidgetStatus {
3536 status : "building" | "ready" | "error" ;
3637 port : number ;
3738 startedAt ?: number ;
39+ error ?: string ;
3840}
3941
4042interface WidgetSandbox {
@@ -111,6 +113,18 @@ const PREBAKED_DIR = join(process.cwd(), ".cache", "widget-base-template");
111113let baseTemplateDir : string | null = null ;
112114let baseTemplatePromise : Promise < string > | null = null ;
113115
116+ function getBuildErrorMessage ( error : unknown ) : string {
117+ if ( error && typeof error === "object" ) {
118+ const err = error as { stderr ?: string | Buffer ; stdout ?: string | Buffer ; message ?: string } ;
119+ const detail = err . stderr ?? err . stdout ?? err . message ;
120+ if ( detail ) {
121+ return String ( detail ) . trim ( ) . split ( "\n" ) . slice ( - 8 ) . join ( "\n" ) ;
122+ }
123+ }
124+ if ( typeof error === "string" ) return error ;
125+ return "Unknown widget build error" ;
126+ }
127+
114128async function ensureBaseTemplate ( ) : Promise < string > {
115129 if ( baseTemplateDir && existsSync ( join ( baseTemplateDir , "node_modules" ) ) ) {
116130 return baseTemplateDir ;
@@ -137,7 +151,7 @@ async function ensureBaseTemplate(): Promise<string> {
137151 writeFileSync ( full , content ) ;
138152 }
139153
140- await execAsync ( "npm install" , { cwd : dir , timeout : 120_000 } ) ;
154+ await execAsync ( TEMPLATE_INSTALL_CMD , { cwd : dir , timeout : 120_000 } ) ;
141155 console . log ( "[secure-exec] npm install done" ) ;
142156
143157 try {
@@ -328,7 +342,12 @@ async function doBuild(widgetId: string): Promise<void> {
328342 try {
329343 const files = getWidgetFiles ( widgetId ) ;
330344 if ( ! files [ "src/App.tsx" ] ) {
331- widgetStatuses . set ( widgetId , { status : "error" , port } ) ;
345+ widgetStatuses . set ( widgetId , {
346+ status : "error" ,
347+ port,
348+ startedAt : Date . now ( ) ,
349+ error : "Missing src/App.tsx" ,
350+ } ) ;
332351 console . error ( `[secure-exec] No src/App.tsx for ${ widgetId } ` ) ;
333352 return ;
334353 }
@@ -377,7 +396,12 @@ async function doBuild(widgetId: string): Promise<void> {
377396 console . log ( `[secure-exec] Widget ${ widgetId } serving on port ${ port } ` ) ;
378397 } catch ( err ) {
379398 console . error ( `[secure-exec] Build error for ${ widgetId } :` , err ) ;
380- widgetStatuses . set ( widgetId , { status : "error" , port } ) ;
399+ widgetStatuses . set ( widgetId , {
400+ status : "error" ,
401+ port,
402+ startedAt : Date . now ( ) ,
403+ error : getBuildErrorMessage ( err ) ,
404+ } ) ;
381405 }
382406}
383407
@@ -392,12 +416,18 @@ export async function buildWidget(widgetId: string): Promise<void> {
392416}
393417
394418const BUILD_TIMEOUT_MS = 120_000 ;
419+ const ERROR_RETRY_MS = 30_000 ;
395420
396421export async function ensureWidget ( widgetId : string ) : Promise < WidgetStatus > {
397422 const existing = widgetStatuses . get ( widgetId ) ;
398423 if ( existing ?. status === "ready" && widgetSandboxes . has ( widgetId ) ) return existing ;
399424 const isStale = existing ?. status === "building" && existing . startedAt && Date . now ( ) - existing . startedAt > BUILD_TIMEOUT_MS ;
425+ const shouldRetryError =
426+ existing ?. status === "error" &&
427+ existing . startedAt &&
428+ Date . now ( ) - existing . startedAt > ERROR_RETRY_MS ;
400429 if ( existing ?. status === "building" && ! isStale ) return existing ;
430+ if ( existing ?. status === "error" && ! shouldRetryError ) return existing ;
401431
402432 const port = await getPort ( { port : portNumbers ( 4100 , 4999 ) } ) ;
403433 const status : WidgetStatus = { status : "building" , port, startedAt : Date . now ( ) } ;
@@ -408,7 +438,7 @@ export async function ensureWidget(widgetId: string): Promise<WidgetStatus> {
408438
409439export async function rebuildWidget ( widgetId : string ) : Promise < WidgetStatus > {
410440 const port = await getPort ( { port : portNumbers ( 4100 , 4999 ) } ) ;
411- const status : WidgetStatus = { status : "building" , port } ;
441+ const status : WidgetStatus = { status : "building" , port, startedAt : Date . now ( ) } ;
412442 widgetStatuses . set ( widgetId , status ) ;
413443 buildWidget ( widgetId ) . catch ( ( err ) => console . error ( `[secure-exec] Rebuild failed for ${ widgetId } :` , err ) ) ;
414444 return status ;
0 commit comments