180 lines
4.9 KiB
TypeScript
180 lines
4.9 KiB
TypeScript
export interface BuildValidation {
|
|
success: boolean;
|
|
errors: string[];
|
|
isRendering: boolean;
|
|
warnings?: string[];
|
|
}
|
|
|
|
/**
|
|
* Validates that the sandbox build was successful
|
|
* Checks compilation status and verifies app is rendering
|
|
*/
|
|
export async function validateBuild(sandboxUrl: string, sandboxId: string): Promise<BuildValidation> {
|
|
try {
|
|
// Step 1: Wait for Vite to process files (give it time to compile)
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
|
// Step 2: Check if the sandbox is actually serving content
|
|
const response = await fetch(sandboxUrl, {
|
|
headers: {
|
|
'User-Agent': 'OpenLovable-Validator',
|
|
'Cache-Control': 'no-cache'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return {
|
|
success: false,
|
|
errors: [`Sandbox returned ${response.status}`],
|
|
isRendering: false
|
|
};
|
|
}
|
|
|
|
const html = await response.text();
|
|
|
|
// Step 3: Check if it's the default page or actual app
|
|
const isDefaultPage =
|
|
html.includes('Vercel Sandbox Ready') ||
|
|
html.includes('Start building your React app with Vite') ||
|
|
html.includes('Vite + React') ||
|
|
!html.includes('id="root"');
|
|
|
|
if (isDefaultPage) {
|
|
return {
|
|
success: false,
|
|
errors: ['Sandbox showing default page, app not rendered'],
|
|
isRendering: false
|
|
};
|
|
}
|
|
|
|
// Step 4: Check for Vite error overlay in HTML
|
|
const hasViteError = html.includes('vite-error-overlay');
|
|
if (hasViteError) {
|
|
// Try to extract error message
|
|
const errorMatch = html.match(/Failed to resolve import "([^"]+)"/);
|
|
const error = errorMatch
|
|
? `Missing package: ${errorMatch[1]}`
|
|
: 'Vite compilation error detected';
|
|
|
|
return {
|
|
success: false,
|
|
errors: [error],
|
|
isRendering: false
|
|
};
|
|
}
|
|
|
|
// Success! App is rendering
|
|
return {
|
|
success: true,
|
|
errors: [],
|
|
isRendering: true
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('[validateBuild] Error during validation:', error);
|
|
return {
|
|
success: false,
|
|
errors: [error instanceof Error ? error.message : 'Validation failed'],
|
|
isRendering: false
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts missing package names from error messages
|
|
*/
|
|
export function extractMissingPackages(error: any): string[] {
|
|
const message = error?.message || String(error);
|
|
const packages: string[] = [];
|
|
|
|
// Pattern 1: "Failed to resolve import 'package-name'"
|
|
const importMatches = message.matchAll(/Failed to resolve import ["']([^"']+)["']/g);
|
|
for (const match of importMatches) {
|
|
packages.push(match[1]);
|
|
}
|
|
|
|
// Pattern 2: "Cannot find module 'package-name'"
|
|
const moduleMatches = message.matchAll(/Cannot find module ["']([^"']+)["']/g);
|
|
for (const match of moduleMatches) {
|
|
packages.push(match[1]);
|
|
}
|
|
|
|
// Pattern 3: "Package 'package-name' not found"
|
|
const packageMatches = message.matchAll(/Package ["']([^"']+)["'] not found/g);
|
|
for (const match of packageMatches) {
|
|
packages.push(match[1]);
|
|
}
|
|
|
|
return [...new Set(packages)]; // Remove duplicates
|
|
}
|
|
|
|
/**
|
|
* Classifies error type for targeted recovery
|
|
*/
|
|
export type ErrorType = 'missing-package' | 'syntax-error' | 'sandbox-timeout' | 'not-rendered' | 'vite-error' | 'unknown';
|
|
|
|
export function classifyError(error: any): ErrorType {
|
|
const message = (error?.message || String(error)).toLowerCase();
|
|
|
|
if (message.includes('failed to resolve import') ||
|
|
message.includes('cannot find module') ||
|
|
message.includes('missing package')) {
|
|
return 'missing-package';
|
|
}
|
|
|
|
if (message.includes('syntax error') ||
|
|
message.includes('unexpected token') ||
|
|
message.includes('parsing error')) {
|
|
return 'syntax-error';
|
|
}
|
|
|
|
if (message.includes('timeout') ||
|
|
message.includes('not responding') ||
|
|
message.includes('timed out')) {
|
|
return 'sandbox-timeout';
|
|
}
|
|
|
|
if (message.includes('not rendered') ||
|
|
message.includes('sandbox ready') ||
|
|
message.includes('default page')) {
|
|
return 'not-rendered';
|
|
}
|
|
|
|
if (message.includes('vite') ||
|
|
message.includes('compilation')) {
|
|
return 'vite-error';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Calculates retry delay based on attempt number and error type
|
|
*/
|
|
export function calculateRetryDelay(attempt: number, errorType: ErrorType): number {
|
|
const baseDelay = 2000; // 2 seconds
|
|
|
|
// Different strategies for different errors
|
|
switch (errorType) {
|
|
case 'missing-package':
|
|
// Packages need time to install
|
|
return baseDelay * 2 * attempt; // 4s, 8s, 12s
|
|
|
|
case 'not-rendered':
|
|
// Vite needs time to compile
|
|
return baseDelay * 3 * attempt; // 6s, 12s, 18s
|
|
|
|
case 'vite-error':
|
|
// Vite restart needed
|
|
return baseDelay * 2 * attempt;
|
|
|
|
case 'sandbox-timeout':
|
|
// Sandbox might be slow
|
|
return baseDelay * 4 * attempt; // 8s, 16s, 24s
|
|
|
|
default:
|
|
// Standard exponential backoff
|
|
return baseDelay * attempt;
|
|
}
|
|
}
|