first pass
This commit is contained in:
210
cli/detectors/nodejs.js
Normal file
210
cli/detectors/nodejs.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Detect Node.js project type and configuration
|
||||
*/
|
||||
export function detectNodeJS(projectPath) {
|
||||
const packageJsonPath = join(projectPath, 'package.json');
|
||||
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pkg;
|
||||
try {
|
||||
pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deps = pkg.dependencies || {};
|
||||
const devDeps = pkg.devDependencies || {};
|
||||
const scripts = pkg.scripts || {};
|
||||
|
||||
// Check for Electron (not dockerizable)
|
||||
if (deps.electron || devDeps.electron) {
|
||||
return {
|
||||
type: 'nodejs-electron',
|
||||
dockerizable: false,
|
||||
reason: 'Electron apps are desktop applications and cannot be containerized',
|
||||
template: null
|
||||
};
|
||||
}
|
||||
|
||||
// Detect project subtype
|
||||
const hasVite = !!devDeps.vite;
|
||||
const hasReact = !!(deps.react || devDeps.react);
|
||||
const hasExpress = !!deps.express;
|
||||
const hasSocketIO = !!(deps['socket.io'] || deps['socket.io-client']);
|
||||
const hasConcurrently = !!devDeps.concurrently;
|
||||
|
||||
// Check for server-side rendering setup (Vite + React + Express with concurrent dev)
|
||||
if (hasVite && hasReact && hasExpress && (hasConcurrently || scripts.dev?.includes('concurrently'))) {
|
||||
return {
|
||||
type: 'nodejs-vite-react-ssr',
|
||||
dockerizable: true,
|
||||
template: 'nodejs/vite-react-ssr',
|
||||
port: detectPort(pkg) || 3000,
|
||||
entryPoint: detectEntryPoint(pkg, projectPath, 'ssr'),
|
||||
buildCommand: 'npm run build',
|
||||
description: 'Vite + React with Express SSR'
|
||||
};
|
||||
}
|
||||
|
||||
// Vite + React SPA (no backend)
|
||||
if (hasVite && hasReact && !hasExpress) {
|
||||
return {
|
||||
type: 'nodejs-vite-react',
|
||||
dockerizable: true,
|
||||
template: 'nodejs/vite-react',
|
||||
port: 80, // Nginx serves static files on port 80
|
||||
entryPoint: null,
|
||||
buildCommand: 'npm run build',
|
||||
description: 'Vite + React SPA (served by Nginx)'
|
||||
};
|
||||
}
|
||||
|
||||
// Express server (with or without Socket.io)
|
||||
if (hasExpress) {
|
||||
return {
|
||||
type: 'nodejs-express',
|
||||
dockerizable: true,
|
||||
template: 'nodejs/express',
|
||||
port: detectPort(pkg) || 3000,
|
||||
entryPoint: detectEntryPoint(pkg, projectPath, 'express'),
|
||||
buildCommand: detectBuildCommand(pkg),
|
||||
description: hasSocketIO ? 'Express + Socket.io server' : 'Express server'
|
||||
};
|
||||
}
|
||||
|
||||
// Generic Node.js project (has package.json but no clear type)
|
||||
if (pkg.main || scripts.start) {
|
||||
return {
|
||||
type: 'nodejs-generic',
|
||||
dockerizable: true,
|
||||
template: 'nodejs/express',
|
||||
port: detectPort(pkg) || 3000,
|
||||
entryPoint: detectEntryPoint(pkg, projectPath, 'generic'),
|
||||
buildCommand: detectBuildCommand(pkg),
|
||||
description: 'Generic Node.js application'
|
||||
};
|
||||
}
|
||||
|
||||
// Has package.json but nothing to run
|
||||
return {
|
||||
type: 'nodejs-unknown',
|
||||
dockerizable: false,
|
||||
reason: 'No start script or main entry point found',
|
||||
template: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the default port from package.json or environment
|
||||
*/
|
||||
function detectPort(pkg) {
|
||||
const scripts = pkg.scripts || {};
|
||||
|
||||
// Check start script for port
|
||||
const startScript = scripts.start || '';
|
||||
const portMatch = startScript.match(/PORT[=\s]+(\d+)/i) ||
|
||||
startScript.match(/-p\s+(\d+)/) ||
|
||||
startScript.match(/--port\s+(\d+)/);
|
||||
if (portMatch) {
|
||||
return parseInt(portMatch[1], 10);
|
||||
}
|
||||
|
||||
// Check for common port patterns in dev script
|
||||
const devScript = scripts.dev || '';
|
||||
const devPortMatch = devScript.match(/:(\d{4})/);
|
||||
if (devPortMatch) {
|
||||
return parseInt(devPortMatch[1], 10);
|
||||
}
|
||||
|
||||
// Default based on common frameworks
|
||||
if (pkg.dependencies?.['next']) return 3000;
|
||||
if (pkg.devDependencies?.vite) return 5173;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the entry point file
|
||||
*/
|
||||
function detectEntryPoint(pkg, projectPath, type) {
|
||||
// Check package.json main field
|
||||
if (pkg.main) {
|
||||
return pkg.main;
|
||||
}
|
||||
|
||||
// Check for common entry points
|
||||
const commonEntries = [
|
||||
'server.js',
|
||||
'server.mjs',
|
||||
'src/server.js',
|
||||
'src/server.mjs',
|
||||
'src/index.js',
|
||||
'src/index.mjs',
|
||||
'index.js',
|
||||
'app.js',
|
||||
'src/app.js'
|
||||
];
|
||||
|
||||
for (const entry of commonEntries) {
|
||||
if (existsSync(join(projectPath, entry))) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
// For SSR projects, look for server files
|
||||
if (type === 'ssr') {
|
||||
const ssrEntries = ['server.mjs', 'server.js', 'src/server.mjs', 'src/server.js'];
|
||||
for (const entry of ssrEntries) {
|
||||
if (existsSync(join(projectPath, entry))) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default
|
||||
return 'index.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if project has a build command
|
||||
*/
|
||||
function detectBuildCommand(pkg) {
|
||||
const scripts = pkg.scripts || {};
|
||||
|
||||
if (scripts.build) {
|
||||
return 'npm run build';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional info about the Node.js project
|
||||
*/
|
||||
export function getNodeJSInfo(projectPath) {
|
||||
const packageJsonPath = join(projectPath, 'package.json');
|
||||
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
|
||||
return {
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
hasLockFile: existsSync(join(projectPath, 'package-lock.json')),
|
||||
hasYarnLock: existsSync(join(projectPath, 'yarn.lock')),
|
||||
hasPnpmLock: existsSync(join(projectPath, 'pnpm-lock.yaml')),
|
||||
nodeVersion: pkg.engines?.node || null,
|
||||
scripts: Object.keys(pkg.scripts || {}),
|
||||
dependencies: Object.keys(pkg.dependencies || {}),
|
||||
devDependencies: Object.keys(pkg.devDependencies || {})
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user