Files
mygit/docker/watch-admin-apps.js
T
DuckQ1u 93d1b7c3d3
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
first commit
2026-04-22 19:51:20 +07:00

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();