246 lines
7.4 KiB
JavaScript
Executable File
246 lines
7.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const { spawn } = require('child_process');
|
|
const chokidar = require('chokidar');
|
|
const path = require('path');
|
|
|
|
// Colors for output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
red: '\x1b[31m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
magenta: '\x1b[35m',
|
|
cyan: '\x1b[36m',
|
|
white: '\x1b[37m'
|
|
};
|
|
|
|
// App configurations - now only need paths, colors, and nx project names
|
|
const apps = {
|
|
shade: { path: 'apps/shade', color: colors.white, nxName: '@tryghost/shade' },
|
|
design: { path: 'apps/admin-x-design-system', color: colors.cyan, nxName: '@tryghost/admin-x-design-system' },
|
|
framework: { path: 'apps/admin-x-framework', color: colors.magenta, nxName: '@tryghost/admin-x-framework' },
|
|
activitypub: { path: 'apps/admin-x-activitypub', color: colors.red, nxName: '@tryghost/admin-x-activitypub' },
|
|
settings: { path: 'apps/admin-x-settings', color: colors.green, nxName: '@tryghost/admin-x-settings' },
|
|
posts: { path: 'apps/posts', color: colors.yellow, nxName: '@tryghost/posts' },
|
|
stats: { path: 'apps/stats', color: colors.blue, nxName: '@tryghost/stats' }
|
|
};
|
|
|
|
// Track all child processes and watchers for cleanup
|
|
const activeProcesses = new Set();
|
|
const activeWatchers = new Set();
|
|
|
|
function log(appName, message) {
|
|
const app = apps[appName];
|
|
console.log(`${app.color}[${appName}]${colors.reset} ${message}`);
|
|
}
|
|
|
|
function buildAllProjects(triggerApp) {
|
|
return new Promise((resolve) => {
|
|
log(triggerApp, 'Running nx run-many to rebuild all projects...');
|
|
const allProjects = Object.values(apps).map(app => app.nxName).join(',');
|
|
|
|
const child = spawn('pnpm', ['nx', 'run-many', '-t', 'build', `--projects=${allProjects}`], {
|
|
cwd: '/home/ghost',
|
|
stdio: 'pipe',
|
|
env: {
|
|
...process.env,
|
|
NX_DAEMON: 'false'
|
|
}
|
|
});
|
|
|
|
activeProcesses.add(child);
|
|
|
|
child.stdout.on('data', (data) => {
|
|
data.toString().split('\n').forEach(line => {
|
|
if (line.trim()) log(triggerApp, `nx: ${line}`);
|
|
});
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
data.toString().split('\n').forEach(line => {
|
|
if (line.trim()) log(triggerApp, `nx: ${line}`);
|
|
});
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
activeProcesses.delete(child);
|
|
if (code === 0) {
|
|
log(triggerApp, 'All builds complete');
|
|
} else {
|
|
log(triggerApp, `Some builds failed with code ${code}`);
|
|
}
|
|
resolve();
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
activeProcesses.delete(child);
|
|
log(triggerApp, `Build error: ${error.message}`);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function startWatching() {
|
|
const watchPaths = Object.values(apps).map(app => path.join('/home/ghost', app.path, 'src'));
|
|
|
|
console.log('Watching all project src folders for changes...');
|
|
|
|
const watcher = chokidar.watch(watchPaths, {
|
|
persistent: true,
|
|
ignoreInitial: true,
|
|
usePolling: true,
|
|
interval: 1000
|
|
});
|
|
|
|
// Track the watcher for cleanup
|
|
activeWatchers.add(watcher);
|
|
|
|
let rebuildTimer;
|
|
|
|
watcher.on('all', (event, filePath) => {
|
|
const relativePath = path.relative('/home/ghost', filePath);
|
|
|
|
// Find which project changed for better logging
|
|
const changedProject = Object.keys(apps).find(name =>
|
|
filePath.includes(apps[name].path)
|
|
) || 'unknown';
|
|
|
|
log(changedProject, `Change detected: ${event} ${relativePath}`);
|
|
|
|
// Debounce rebuilds
|
|
clearTimeout(rebuildTimer);
|
|
rebuildTimer = setTimeout(async () => {
|
|
await buildAllProjects(changedProject);
|
|
}, 500);
|
|
});
|
|
|
|
watcher.on('error', (error) => {
|
|
console.log(`Watcher error: ${error.message}`);
|
|
console.log('Exiting process - Docker will restart the service');
|
|
process.exit(1);
|
|
});
|
|
|
|
watcher.on('close', () => {
|
|
console.log('Watcher closed unexpectedly');
|
|
console.log('Exiting process - Docker will restart the service');
|
|
process.exit(1);
|
|
});
|
|
|
|
return watcher;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('Starting admin apps build and watch system...');
|
|
|
|
try {
|
|
// Phase 1: Build everything with nx handling dependency order and parallelization
|
|
const allProjects = Object.values(apps).map(app => app.nxName).join(',');
|
|
|
|
const child = spawn('pnpm', ['nx', 'run-many', '-t', 'build', `--projects=${allProjects}`], {
|
|
cwd: '/home/ghost',
|
|
stdio: 'pipe',
|
|
env: {
|
|
...process.env,
|
|
NX_DAEMON: 'false'
|
|
}
|
|
});
|
|
|
|
activeProcesses.add(child);
|
|
|
|
child.stdout.on('data', (data) => {
|
|
data.toString().split('\n').forEach(line => {
|
|
if (line.trim()) console.log(`[nx] ${line}`);
|
|
});
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
data.toString().split('\n').forEach(line => {
|
|
if (line.trim()) console.log(`[nx] ${line}`);
|
|
});
|
|
});
|
|
|
|
await new Promise((resolve, reject) => {
|
|
child.on('close', (code) => {
|
|
activeProcesses.delete(child);
|
|
if (code === 0) {
|
|
console.log('\nAll projects built successfully!');
|
|
resolve();
|
|
} else {
|
|
console.log(`\nSome builds failed, but continuing with watch processes...`);
|
|
resolve(); // Don't crash the watch system if some builds fail
|
|
}
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
activeProcesses.delete(child);
|
|
reject(error);
|
|
});
|
|
});
|
|
|
|
// Phase 2: Start single watcher for all projects
|
|
// Single watcher for all projects - any change triggers nx run-many (with caching)
|
|
const watcher = startWatching();
|
|
|
|
console.log('\nAll watch processes started. Press Ctrl+C to stop.');
|
|
|
|
// Keep the process alive
|
|
await new Promise(() => { });
|
|
|
|
} catch (error) {
|
|
console.error('Failed to start:', error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Graceful shutdown handler
|
|
function cleanup() {
|
|
console.log('\nShutting down...');
|
|
|
|
// Kill all active child processes
|
|
for (const child of activeProcesses) {
|
|
try {
|
|
child.kill('SIGTERM');
|
|
// Force kill after 1 second if still running
|
|
setTimeout(() => {
|
|
if (!child.killed) {
|
|
child.kill('SIGKILL');
|
|
}
|
|
}, 1000);
|
|
} catch (error) {
|
|
// Process might already be dead
|
|
}
|
|
}
|
|
|
|
// Close all watchers
|
|
for (const watcher of activeWatchers) {
|
|
try {
|
|
watcher.close();
|
|
} catch (error) {
|
|
// Watcher might already be closed
|
|
}
|
|
}
|
|
|
|
console.log('Cleanup complete.');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Handle various termination signals
|
|
process.on('SIGINT', cleanup);
|
|
process.on('SIGTERM', cleanup);
|
|
process.on('SIGQUIT', cleanup);
|
|
|
|
// Handle uncaught exceptions to ensure cleanup
|
|
process.on('uncaughtException', (error) => {
|
|
console.error('Uncaught exception:', error);
|
|
cleanup();
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason) => {
|
|
console.error('Unhandled rejection:', reason);
|
|
cleanup();
|
|
});
|
|
|
|
main();
|