Skip to content

Commit e1bbe48

Browse files
committed
Added project type node
1 parent 21ee8a4 commit e1bbe48

7 files changed

Lines changed: 585 additions & 109 deletions

File tree

src/main/services/ProjectManager.js

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -242,25 +242,32 @@ class ProjectManager {
242242

243243
const id = uuidv4();
244244

245-
// Validate that required PHP version is installed before creating project
246-
const phpVersion = config.phpVersion || '8.3';
245+
// Detect project type early (needed for conditional PHP check)
246+
const projectType = config.type || (await this.detectProjectType(config.path));
247+
247248
const { app } = require('electron');
248249
const resourcePath = this.configStore.get('resourcePath') || path.join(app.getPath('userData'), 'resources');
249250
const platform = process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux';
250-
const phpDir = path.join(resourcePath, 'php', phpVersion, platform);
251-
const phpExe = platform === 'win' ? 'php.exe' : 'php';
252-
const phpCgiExe = platform === 'win' ? 'php-cgi.exe' : 'php-cgi';
253251

254-
const isPlaywright = process.env.PLAYWRIGHT_TEST === 'true';
252+
// Validate that required PHP version is installed before creating project.
253+
// Node.js projects do not require PHP.
254+
if (projectType !== 'nodejs') {
255+
const phpVersion = config.phpVersion || '8.3';
256+
const phpDir = path.join(resourcePath, 'php', phpVersion, platform);
257+
const phpExe = platform === 'win' ? 'php.exe' : 'php';
258+
const phpCgiExe = platform === 'win' ? 'php-cgi.exe' : 'php-cgi';
255259

256-
if (!isPlaywright && (!await fs.pathExists(path.join(phpDir, phpExe)) || !await fs.pathExists(path.join(phpDir, phpCgiExe)))) {
257-
throw new Error(`PHP ${phpVersion} is not installed. Please download it from the Binary Manager before creating a project.`);
260+
const isPlaywright = process.env.PLAYWRIGHT_TEST === 'true';
261+
262+
if (!isPlaywright && (!await fs.pathExists(path.join(phpDir, phpExe)) || !await fs.pathExists(path.join(phpDir, phpCgiExe)))) {
263+
throw new Error(`PHP ${phpVersion} is not installed. Please download it from the Binary Manager before creating a project.`);
264+
}
258265
}
259266

260267
// Re-fetch projects list (it may have changed after removing failed project)
261268
const currentProjects = this.configStore.get('projects', []);
262269

263-
// Find available port
270+
// Find available port (for the web-server proxy)
264271
const usedPorts = currentProjects.map((p) => p.port);
265272
let port = settings.portRangeStart || 8000;
266273
while (usedPorts.includes(port)) {
@@ -274,8 +281,15 @@ class ProjectManager {
274281
sslPort++;
275282
}
276283

277-
// Detect project type if not specified
278-
const projectType = config.type || (await this.detectProjectType(config.path));
284+
// Node.js app internal port (used as upstream for the reverse proxy).
285+
// Reserve a separate block starting at 3000 so it doesn't clash with web-server ports.
286+
let nodePort = config.nodePort || 3000;
287+
if (projectType === 'nodejs') {
288+
const usedNodePorts = currentProjects.map((p) => p.nodePort).filter(Boolean);
289+
while (usedNodePorts.includes(nodePort)) {
290+
nodePort++;
291+
}
292+
}
279293

280294
// Determine default web server version from installed versions
281295
let defaultWebServerVersion = '1.28';
@@ -316,15 +330,19 @@ class ProjectManager {
316330
redis: config.services?.redis || false,
317331
redisVersion: config.services?.redisVersion || '7.4',
318332
queue: config.services?.queue || false,
319-
// Node.js for projects that need it
320-
nodejs: config.services?.nodejs || false,
333+
// Node.js is always enabled for nodejs-type projects
334+
nodejs: projectType === 'nodejs' ? true : (config.services?.nodejs || false),
321335
nodejsVersion: config.services?.nodejsVersion || '20',
322336
},
323337
environment: this.getDefaultEnvironment(projectType, config.name, port),
324338
supervisor: {
325339
workers: config.supervisor?.workers || 1,
326340
processes: [],
327341
},
342+
// Node.js reverse-proxy port (only meaningful for nodejs-type projects)
343+
nodePort: projectType === 'nodejs' ? nodePort : undefined,
344+
// Node.js start command for supervisor (only for nodejs-type projects)
345+
nodeStartCommand: projectType === 'nodejs' ? (config.nodeStartCommand || 'npm start') : undefined,
328346
createdAt: new Date().toISOString(),
329347
lastStarted: null,
330348
// Compatibility warnings acknowledged by user
@@ -409,18 +427,33 @@ class ProjectManager {
409427
});
410428
}
411429

412-
// Save project first (before installation which might take time)
413-
// Re-fetch to ensure we have latest list
414-
const projectsToSave = this.configStore.get('projects', []);
415-
projectsToSave.push(project);
416-
this.configStore.set('projects', projectsToSave);
417-
418-
// Auto-install CLI if not already installed
419-
await this.ensureCliInstalled();
430+
// Set up Node.js start process for nodejs-type projects
431+
if (project.type === 'nodejs') {
432+
const nodejsVersion = project.services?.nodejsVersion || '20';
433+
const nodeResourcePath = this.configStore.get('resourcePath') || path.join(require('electron').app.getPath('userData'), 'resources');
434+
const nodePlatform = process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux';
435+
const nodeDir = path.join(nodeResourcePath, 'nodejs', nodejsVersion, nodePlatform);
436+
const nodeExe = process.platform === 'win32'
437+
? path.join(nodeDir, 'node.exe')
438+
: path.join(nodeDir, 'bin', 'node');
439+
project.supervisor.processes.push({
440+
name: 'nodejs-app',
441+
command: project.nodeStartCommand || 'npm start',
442+
autostart: true,
443+
autorestart: true,
444+
numprocs: 1,
445+
environment: {
446+
PORT: String(project.nodePort || 3000),
447+
NODE_PATH: nodeDir,
448+
},
449+
});
450+
}
420451

421452
// Install fresh framework OR clone from repository - run async without blocking
422453
if (config.installFresh || config.projectSource === 'clone') {
423-
// Mark project as installing
454+
// Mark project as installing BEFORE saving to store.
455+
// This way, if the app crashes during installation, the project is saved
456+
// with installing:true and can be retried on restart or re-creation.
424457
project.installing = true;
425458

426459
// Store clone config for runInstallation
@@ -429,8 +462,19 @@ class ProjectManager {
429462
authType: config.authType || 'public',
430463
accessToken: config.accessToken,
431464
} : null;
465+
}
432466

433-
// Run installation in background (don't await)
467+
// Save project first (before installation which might take time)
468+
// Re-fetch to ensure we have latest list
469+
const projectsToSave = this.configStore.get('projects', []);
470+
projectsToSave.push(project);
471+
this.configStore.set('projects', projectsToSave);
472+
473+
// Auto-install CLI if not already installed
474+
await this.ensureCliInstalled();
475+
476+
// Start the installation in background (don't await)
477+
if (config.installFresh || config.projectSource === 'clone') {
434478
this.runInstallation(project, mainWindow).catch(error => {
435479
this.managers.log?.systemError('Background installation failed', { project: project.name, error: error.message });
436480
});
@@ -499,13 +543,29 @@ class ProjectManager {
499543
if (await fs.pathExists(project.path)) {
500544
const files = await fs.readdir(project.path);
501545
if (files.length > 0) {
502-
sendOutput(`Warning: Directory ${project.path} is not empty. Skipping Laravel installation.`, 'warning');
503-
sendOutput('If you want a fresh installation, please choose an empty directory.', 'info');
504-
project.installError = 'Directory not empty';
505-
project.installing = false;
506-
this.updateProjectInStore(project);
507-
sendOutput('', 'complete'); // Signal completion
508-
return;
546+
// Allow retrying if it looks like a partial/failed Laravel install:
547+
// A real Laravel project has artisan, composer.json, app/, etc.
548+
// A partial install might only have a public/ folder or similar stubs.
549+
const laravelIndicators = ['artisan', 'composer.json', 'app', 'bootstrap', 'config'];
550+
const hasLaravelFiles = files.some(f => laravelIndicators.includes(f.toLowerCase()));
551+
552+
if (hasLaravelFiles) {
553+
sendOutput(`Warning: Directory ${project.path} already contains a Laravel project. Skipping installation.`, 'warning');
554+
sendOutput('If you want a fresh installation, please choose an empty directory.', 'info');
555+
project.installError = 'Directory not empty';
556+
project.installing = false;
557+
this.updateProjectInStore(project);
558+
sendOutput('', 'complete'); // Signal completion
559+
return;
560+
} else {
561+
// Partial/failed installation (e.g. only public/ was created). Clean it up.
562+
sendOutput(`Cleaning up partial installation at ${project.path}...`, 'info');
563+
try {
564+
await fs.remove(project.path);
565+
} catch (cleanErr) {
566+
sendOutput(`Warning: Could not clean up partial files: ${cleanErr.message}`, 'warning');
567+
}
568+
}
509569
}
510570
}
511571

src/main/services/WebServerManager.js

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,67 @@ class WebServerManager {
206206
? `${domain || 'localhost'} _` // _ is a catch-all server name
207207
: (domain || 'localhost');
208208

209-
let serverConfig = `
209+
const isNodeJs = project.type === 'nodejs';
210+
const nodePort = project.nodePort || 3000;
211+
212+
let serverConfig;
213+
214+
if (isNodeJs) {
215+
// Node.js reverse-proxy configuration
216+
serverConfig = `
217+
# DevBox Pro - ${name} (Node.js)
218+
# Auto-generated configuration${networkAccess ? '\n# Network Access: ENABLED - accessible from local network' : ''}
219+
${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
220+
221+
server {
222+
listen ${listenDirective}${usePort80 ? ' default_server' : ''};
223+
server_name ${serverName};
224+
225+
charset utf-8;
226+
227+
location / {
228+
proxy_pass http://127.0.0.1:${nodePort};
229+
proxy_http_version 1.1;
230+
proxy_set_header Upgrade $http_upgrade;
231+
proxy_set_header Connection 'upgrade';
232+
proxy_set_header Host $host;
233+
proxy_cache_bypass $http_upgrade;
234+
}
235+
236+
access_log "${this.dataPath.replace(/\\/g, '/')}/nginx/logs/${id}-access.log";
237+
error_log "${this.dataPath.replace(/\\/g, '/')}/nginx/logs/${id}-error.log";
238+
}
239+
`;
240+
// Add SSL block for Node.js if enabled
241+
if (ssl) {
242+
const certPath = path.join(this.dataPath, 'ssl', domain || id);
243+
serverConfig += `
244+
245+
server {
246+
listen ${listenDirectiveSsl};
247+
server_name ${serverName};
248+
249+
ssl_certificate "${certPath.replace(/\\/g, '/')}/cert.pem";
250+
ssl_certificate_key "${certPath.replace(/\\/g, '/')}/key.pem";
251+
ssl_protocols TLSv1.2 TLSv1.3;
252+
ssl_ciphers HIGH:!aNULL:!MD5;
253+
254+
charset utf-8;
255+
256+
location / {
257+
proxy_pass http://127.0.0.1:${nodePort};
258+
proxy_http_version 1.1;
259+
proxy_set_header Upgrade $http_upgrade;
260+
proxy_set_header Connection 'upgrade';
261+
proxy_set_header Host $host;
262+
proxy_cache_bypass $http_upgrade;
263+
}
264+
}
265+
`;
266+
}
267+
} else {
268+
// Standard PHP FastCGI configuration
269+
serverConfig = `
210270
# DevBox Pro - ${name}
211271
# Auto-generated configuration${networkAccess ? '\n# Network Access: ENABLED - accessible from local network' : ''}
212272
${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
@@ -250,10 +310,10 @@ server {
250310
}
251311
`;
252312

253-
// Add SSL server block if SSL is enabled
254-
if (ssl) {
255-
const certPath = path.join(this.dataPath, 'ssl', domain || id);
256-
serverConfig += `
313+
// Add SSL server block if SSL is enabled
314+
if (ssl) {
315+
const certPath = path.join(this.dataPath, 'ssl', domain || id);
316+
serverConfig += `
257317
258318
server {
259319
listen ${listenDirectiveSsl};
@@ -295,14 +355,15 @@ server {
295355
}
296356
}
297357
`;
358+
}
298359
}
299360

300361
// Save config
301362
const configPath = path.join(this.dataPath, 'nginx', 'sites', `${id}.conf`);
302363
await fs.ensureDir(path.dirname(configPath));
303364
await fs.writeFile(configPath, serverConfig);
304365

305-
return { configPath, phpFpmPort };
366+
return { configPath, phpFpmPort: isNodeJs ? null : phpFpmPort };
306367
}
307368

308369
// Generate Apache config for a project
@@ -353,7 +414,52 @@ server {
353414
// Add ServerAlias * when network access is enabled to accept any hostname
354415
const serverAliasDirective = networkAccess ? '\n ServerAlias *' : '';
355416

356-
let vhostConfig = `
417+
const isNodeJs = project.type === 'nodejs';
418+
const nodePort = project.nodePort || 3000;
419+
420+
let vhostConfig;
421+
422+
if (isNodeJs) {
423+
// Node.js reverse-proxy configuration
424+
vhostConfig = `
425+
# DevBox Pro - ${name} (Node.js)
426+
# Auto-generated configuration${networkAccess ? '\n# Network Access: ENABLED - accessible from local network' : ''}
427+
${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
428+
429+
<VirtualHost *:${finalPort}>
430+
ServerName ${domain || 'localhost'}${serverAliasDirective}
431+
432+
ProxyPreserveHost On
433+
ProxyPass / http://127.0.0.1:${nodePort}/
434+
ProxyPassReverse / http://127.0.0.1:${nodePort}/
435+
436+
ErrorLog "${this.dataPath}/apache/logs/${id}-error.log"
437+
CustomLog "${this.dataPath}/apache/logs/${id}-access.log" combined
438+
</VirtualHost>
439+
`;
440+
if (ssl) {
441+
const certPath = path.join(this.dataPath, 'ssl', domain || id);
442+
vhostConfig += `
443+
444+
<VirtualHost *:${sslPort}>
445+
ServerName ${domain || 'localhost'}${serverAliasDirective}
446+
447+
SSLEngine on
448+
SSLCertificateFile "${certPath}/cert.pem"
449+
SSLCertificateKeyFile "${certPath}/key.pem"
450+
451+
ProxyPreserveHost On
452+
ProxyPass / http://127.0.0.1:${nodePort}/
453+
ProxyPassReverse / http://127.0.0.1:${nodePort}/
454+
455+
ErrorLog "${this.dataPath}/apache/logs/${id}-ssl-error.log"
456+
CustomLog "${this.dataPath}/apache/logs/${id}-ssl-access.log" combined
457+
</VirtualHost>
458+
`;
459+
}
460+
} else {
461+
// Standard PHP FastCGI configuration
462+
vhostConfig = `
357463
# DevBox Pro - ${name}
358464
# Auto-generated configuration${networkAccess ? '\n# Network Access: ENABLED - accessible from local network' : ''}
359465
${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
@@ -375,7 +481,7 @@ ${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
375481
376482
# PHP-FPM/CGI proxy with timeout for long-running processes (0 = unlimited)
377483
ProxyTimeout 0
378-
<FilesMatch \\.php$>
484+
<FilesMatch \.php$>
379485
SetHandler "proxy:fcgi://127.0.0.1:${phpFpmPort}"
380486
</FilesMatch>
381487
@@ -386,10 +492,10 @@ ${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
386492
</VirtualHost>
387493
`;
388494

389-
// Add SSL virtual host if enabled
390-
if (ssl) {
391-
const certPath = path.join(this.dataPath, 'ssl', domain || id);
392-
vhostConfig += `
495+
// Add SSL virtual host if enabled
496+
if (ssl) {
497+
const certPath = path.join(this.dataPath, 'ssl', domain || id);
498+
vhostConfig += `
393499
394500
<VirtualHost *:${sslPort}>
395501
ServerName ${domain || 'localhost'}${serverAliasDirective}
@@ -412,7 +518,7 @@ ${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
412518
413519
# PHP-FPM/CGI proxy with timeout for long-running processes (0 = unlimited)
414520
ProxyTimeout 0
415-
<FilesMatch \\.php$>
521+
<FilesMatch \.php$>
416522
SetHandler "proxy:fcgi://127.0.0.1:${phpFpmPort}"
417523
</FilesMatch>
418524
@@ -422,14 +528,15 @@ ${usePort80 ? '# Port 80 enabled (Sole network access project)' : ''}
422528
CustomLog "${this.dataPath}/apache/logs/${id}-ssl-access.log" combined
423529
</VirtualHost>
424530
`;
531+
}
425532
}
426533

427534
// Save config
428535
const configPath = path.join(this.dataPath, 'apache', 'vhosts', `${id}.conf`);
429536
await fs.ensureDir(path.dirname(configPath));
430537
await fs.writeFile(configPath, vhostConfig);
431538

432-
return { configPath, phpFpmPort };
539+
return { configPath, phpFpmPort: isNodeJs ? null : phpFpmPort };
433540
}
434541

435542
// Start PHP-FPM/CGI for a project

0 commit comments

Comments
 (0)