Files
open-lovable/app/api/detect-and-install-packages/route.ts
T
Developers Digest 1629e12079 initial
2025-08-08 09:04:33 -04:00

260 lines
8.0 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
declare global {
var activeSandbox: any;
}
export async function POST(request: NextRequest) {
try {
const { files } = await request.json();
if (!files || typeof files !== 'object') {
return NextResponse.json({
success: false,
error: 'Files object is required'
}, { status: 400 });
}
if (!global.activeSandbox) {
return NextResponse.json({
success: false,
error: 'No active sandbox'
}, { status: 404 });
}
console.log('[detect-and-install-packages] Processing files:', Object.keys(files));
// Extract all import statements from the files
const imports = new Set<string>();
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*(?:from\s+)?['"]([^'"]+)['"]/g;
const requireRegex = /require\s*\(['"]([^'"]+)['"]\)/g;
for (const [filePath, content] of Object.entries(files)) {
if (typeof content !== 'string') continue;
// Skip non-JS/JSX/TS/TSX files
if (!filePath.match(/\.(jsx?|tsx?)$/)) continue;
// Find ES6 imports
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.add(match[1]);
}
// Find CommonJS requires
while ((match = requireRegex.exec(content)) !== null) {
imports.add(match[1]);
}
}
console.log('[detect-and-install-packages] Found imports:', Array.from(imports));
// Log specific heroicons imports
const heroiconImports = Array.from(imports).filter(imp => imp.includes('heroicons'));
if (heroiconImports.length > 0) {
console.log('[detect-and-install-packages] Heroicon imports:', heroiconImports);
}
// Filter out relative imports and built-in modules
const packages = Array.from(imports).filter(imp => {
// Skip relative imports
if (imp.startsWith('.') || imp.startsWith('/')) return false;
// Skip built-in Node modules
const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process'];
if (builtins.includes(imp)) return false;
// Extract package name (handle scoped packages and subpaths)
const parts = imp.split('/');
if (imp.startsWith('@')) {
// Scoped package like @vitejs/plugin-react
return true;
} else {
// Regular package, return just the first part
return true;
}
});
// Extract just the package names (without subpaths)
const packageNames = packages.map(pkg => {
if (pkg.startsWith('@')) {
// Scoped package: @scope/package or @scope/package/subpath
const parts = pkg.split('/');
return parts.slice(0, 2).join('/');
} else {
// Regular package: package or package/subpath
return pkg.split('/')[0];
}
});
// Remove duplicates
const uniquePackages = [...new Set(packageNames)];
console.log('[detect-and-install-packages] Packages to install:', uniquePackages);
if (uniquePackages.length === 0) {
return NextResponse.json({
success: true,
packagesInstalled: [],
message: 'No new packages to install'
});
}
// Check which packages are already installed
const checkResult = await global.activeSandbox.runCode(`
import os
import json
installed = []
missing = []
packages = ${JSON.stringify(uniquePackages)}
for package in packages:
# Handle scoped packages
if package.startswith('@'):
package_path = f"/home/user/app/node_modules/{package}"
else:
package_path = f"/home/user/app/node_modules/{package}"
if os.path.exists(package_path):
installed.append(package)
else:
missing.append(package)
result = {
'installed': installed,
'missing': missing
}
print(json.dumps(result))
`);
const status = JSON.parse(checkResult.logs.stdout.join(''));
console.log('[detect-and-install-packages] Package status:', status);
if (status.missing.length === 0) {
return NextResponse.json({
success: true,
packagesInstalled: [],
packagesAlreadyInstalled: status.installed,
message: 'All packages already installed'
});
}
// Install missing packages
console.log('[detect-and-install-packages] Installing packages:', status.missing);
const installResult = await global.activeSandbox.runCode(`
import subprocess
import os
import json
os.chdir('/home/user/app')
packages_to_install = ${JSON.stringify(status.missing)}
# Join packages into a single install command
packages_str = ' '.join(packages_to_install)
cmd = f'npm install {packages_str} --save'
print(f"Running: {cmd}")
# Run npm install with explicit save flag
result = subprocess.run(['npm', 'install', '--save'] + packages_to_install,
capture_output=True,
text=True,
cwd='/home/user/app',
timeout=60)
print("stdout:", result.stdout)
if result.stderr:
print("stderr:", result.stderr)
# Verify installation
installed = []
failed = []
for package in packages_to_install:
# Handle scoped packages correctly
if package.startswith('@'):
# For scoped packages like @heroicons/react
package_path = f"/home/user/app/node_modules/{package}"
else:
package_path = f"/home/user/app/node_modules/{package}"
if os.path.exists(package_path):
installed.append(package)
print(f"✓ Verified installation of {package}")
else:
# Check if it's a submodule of an installed package
base_package = package.split('/')[0]
if package.startswith('@'):
# For @scope/package, the base is @scope/package
base_package = '/'.join(package.split('/')[:2])
base_path = f"/home/user/app/node_modules/{base_package}"
if os.path.exists(base_path):
installed.append(package)
print(f"✓ Verified installation of {package} (via {base_package})")
else:
failed.append(package)
print(f"✗ Failed to verify installation of {package}")
result_data = {
'installed': installed,
'failed': failed,
'returncode': result.returncode
}
print("\\nResult:", json.dumps(result_data))
`, { timeout: 60000 });
// Parse the result more safely
let installStatus;
try {
const stdout = installResult.logs.stdout.join('');
const resultMatch = stdout.match(/Result:\s*({.*})/);
if (resultMatch) {
installStatus = JSON.parse(resultMatch[1]);
} else {
// Fallback parsing
const lines = stdout.split('\n');
const resultLine = lines.find((line: string) => line.includes('Result:'));
if (resultLine) {
installStatus = JSON.parse(resultLine.split('Result:')[1].trim());
} else {
throw new Error('Could not find Result in output');
}
}
} catch (parseError) {
console.error('[detect-and-install-packages] Failed to parse install result:', parseError);
console.error('[detect-and-install-packages] stdout:', installResult.logs.stdout.join(''));
// Fallback to assuming all packages were installed
installStatus = {
installed: status.missing,
failed: [],
returncode: 0
};
}
if (installStatus.failed.length > 0) {
console.error('[detect-and-install-packages] Failed to install:', installStatus.failed);
}
return NextResponse.json({
success: true,
packagesInstalled: installStatus.installed,
packagesFailed: installStatus.failed,
packagesAlreadyInstalled: status.installed,
message: `Installed ${installStatus.installed.length} packages`,
logs: installResult.logs.stdout.join('\n')
});
} catch (error) {
console.error('[detect-and-install-packages] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
}