336 lines
8.5 KiB
JavaScript
Executable File
336 lines
8.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import { spawn } from 'child_process';
|
|
import { watch } from 'fs';
|
|
import { existsSync } from 'fs';
|
|
import { dirname, resolve } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import treeKill from 'tree-kill';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Configuration
|
|
const CE_TYPINGS_FLAG_PATH = resolve(__dirname, '../../meet-ce/typings/dist/typings-ready.flag');
|
|
const CE_TYPINGS_DIST = resolve(__dirname, '../../meet-ce/typings/dist');
|
|
const PRO_TYPINGS_FLAG_PATH = resolve(__dirname, '../../meet-pro/typings/dist/typings-ready.flag');
|
|
const PRO_TYPINGS_DIST = resolve(__dirname, '../../meet-pro/typings/dist');
|
|
const DEBOUNCE_MS = 500; // Wait 500ms after flag appears before restarting
|
|
const KILL_TIMEOUT_MS = 5000; // Max time to wait for process to die
|
|
const POLL_DIR_MS = 1000; // Polling interval for directories that don't yet exist
|
|
|
|
// Get command from arguments
|
|
const command = process.argv.slice(2).join(' ');
|
|
|
|
if (!command) {
|
|
console.error('❌ Error: No command provided');
|
|
console.error('Usage: watch-with-typings-guard.mjs <command>');
|
|
process.exit(1);
|
|
}
|
|
|
|
let childProcess = null;
|
|
// We'll support multiple targets (CE and PRO). Each target has its own ready state.
|
|
const targets = [];
|
|
let pendingRestart = null;
|
|
let hasStartedOnce = false;
|
|
let isKilling = false;
|
|
|
|
/**
|
|
* Start the child process
|
|
*/
|
|
async function startProcess() {
|
|
// Process should start only when all required targets report ready
|
|
const allReady = targets.length > 0 && targets.every((t) => t.isReady);
|
|
if (!allReady) {
|
|
if (!hasStartedOnce) {
|
|
const names = targets.map((t) => t.name).join(', ');
|
|
console.log(`Waiting for typings to be ready for: ${names}...`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (childProcess) {
|
|
console.log('Restarting process...');
|
|
await killProcess();
|
|
}
|
|
|
|
if (isKilling) {
|
|
console.log('Waiting for previous process to terminate...');
|
|
return;
|
|
}
|
|
|
|
console.log(`Starting: ${command}`);
|
|
childProcess = spawn(command, {
|
|
shell: true,
|
|
stdio: 'inherit',
|
|
env: { ...process.env },
|
|
detached: false
|
|
});
|
|
|
|
hasStartedOnce = true;
|
|
|
|
childProcess.on('exit', (code) => {
|
|
if (code !== null && code !== 0 && !isKilling) {
|
|
console.error(`❌ Process exited with code ${code}`);
|
|
}
|
|
childProcess = null;
|
|
isKilling = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Kill the child process gracefully (and wait for it to die)
|
|
*/
|
|
function killProcess() {
|
|
return new Promise((resolve) => {
|
|
if (!childProcess || isKilling) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
isKilling = true;
|
|
const pid = childProcess.pid;
|
|
|
|
// Set a timeout in case the process doesn't die
|
|
const timeout = setTimeout(() => {
|
|
console.error(`⚠️ Process ${pid} didn't terminate gracefully, force killing...`);
|
|
if (childProcess) {
|
|
try {
|
|
treeKill(pid, 'SIGKILL');
|
|
} catch (err) {
|
|
console.error('Error force killing process:', err.message);
|
|
}
|
|
}
|
|
isKilling = false;
|
|
childProcess = null;
|
|
resolve();
|
|
}, KILL_TIMEOUT_MS);
|
|
|
|
// Try graceful shutdown first
|
|
childProcess.once('exit', () => {
|
|
clearTimeout(timeout);
|
|
isKilling = false;
|
|
childProcess = null;
|
|
resolve();
|
|
});
|
|
|
|
try {
|
|
// Kill the entire process tree (important for shells that spawn subprocesses)
|
|
treeKill(pid, 'SIGTERM', (err) => {
|
|
if (err) {
|
|
console.error(`⚠️ Error killing process tree: ${err.message}`);
|
|
// Fallback to direct kill
|
|
try {
|
|
childProcess?.kill('SIGTERM');
|
|
} catch (e) {
|
|
console.error('Error in fallback kill:', e.message);
|
|
}
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error('Error killing process:', err.message);
|
|
clearTimeout(timeout);
|
|
isKilling = false;
|
|
childProcess = null;
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Schedule a restart with debouncing
|
|
*/
|
|
function scheduleRestart() {
|
|
// Don't schedule if we're currently killing a process
|
|
if (isKilling) {
|
|
console.log('Process is being killed, will restart when done...');
|
|
// Will retry after kill completes
|
|
setTimeout(scheduleRestart, 100);
|
|
return;
|
|
}
|
|
|
|
if (pendingRestart) {
|
|
clearTimeout(pendingRestart);
|
|
}
|
|
|
|
pendingRestart = setTimeout(async () => {
|
|
pendingRestart = null;
|
|
await startProcess();
|
|
}, DEBOUNCE_MS);
|
|
}
|
|
|
|
/**
|
|
* Check if typings are ready
|
|
*/
|
|
function checkTypingsReady() {
|
|
let changed = false;
|
|
for (const t of targets) {
|
|
const wasReady = t.isReady;
|
|
t.isReady = existsSync(t.flagPath);
|
|
if (!wasReady && t.isReady) {
|
|
console.log(`✅ Typings ready for ${t.name}!`);
|
|
changed = true;
|
|
} else if (wasReady && !t.isReady) {
|
|
console.log(`Typings recompiling for ${t.name}... (process will restart when ready)`);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
scheduleRestart();
|
|
}
|
|
|
|
return targets.every((t) => t.isReady);
|
|
}
|
|
|
|
/**
|
|
* Watch the flag file
|
|
*/
|
|
/**
|
|
* Create a watcher for a single target that watches its flag dir and dist dir.
|
|
* If the directories don't exist yet, we poll until they appear then create watchers.
|
|
*/
|
|
function watchTarget(target) {
|
|
console.log(`Setting up watchers for ${target.name}`);
|
|
|
|
// Initial check
|
|
checkTypingsReady();
|
|
|
|
const flagDir = dirname(target.flagPath);
|
|
const distDir = target.distPath;
|
|
|
|
function createWatchers() {
|
|
try {
|
|
if (!target.flagWatcher && existsSync(flagDir)) {
|
|
target.flagWatcher = watch(flagDir, { recursive: false }, (eventType, filename) => {
|
|
if (filename === 'typings-ready.flag') {
|
|
checkTypingsReady();
|
|
}
|
|
});
|
|
|
|
target.flagWatcher.on('error', (error) => {
|
|
console.error(`❌ Watcher error for ${target.name} flag:`, error);
|
|
});
|
|
}
|
|
|
|
if (!target.distWatcher && existsSync(distDir)) {
|
|
target.distWatcher = watch(distDir, { recursive: true }, (eventType, filename) => {
|
|
if (filename === 'typings-ready.flag') return;
|
|
if (childProcess && target.isReady) {
|
|
console.log(`Detected change in ${target.name} typings: ${filename} (will restart when compilation finishes)`);
|
|
}
|
|
});
|
|
|
|
target.distWatcher.on('error', (error) => {
|
|
console.error(`❌ Watcher error for ${target.name} dist:`, error);
|
|
});
|
|
}
|
|
|
|
// If we have at least one watcher, stop polling
|
|
if ((target.flagWatcher || target.distWatcher) && target.poller) {
|
|
clearInterval(target.poller);
|
|
target.poller = null;
|
|
}
|
|
} catch (err) {
|
|
// Rare race - ignore and keep polling
|
|
// console.error(`Error creating watcher for ${target.name}:`, err.message);
|
|
}
|
|
}
|
|
|
|
// If directories are already present, create watchers immediately
|
|
if (existsSync(flagDir) || existsSync(distDir)) {
|
|
createWatchers();
|
|
}
|
|
|
|
// Start polling for directory creation if watchers not yet created
|
|
if (!target.flagWatcher && !target.distWatcher) {
|
|
target.poller = setInterval(() => {
|
|
createWatchers();
|
|
}, POLL_DIR_MS);
|
|
}
|
|
|
|
return () => {
|
|
if (target.flagWatcher) {
|
|
try { target.flagWatcher.close(); } catch (e) {}
|
|
target.flagWatcher = null;
|
|
}
|
|
if (target.distWatcher) {
|
|
try { target.distWatcher.close(); } catch (e) {}
|
|
target.distWatcher = null;
|
|
}
|
|
if (target.poller) {
|
|
clearInterval(target.poller);
|
|
target.poller = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Watch typings/dist for changes (to trigger restart when ready)
|
|
*/
|
|
// Setup targets depending on the provided command. If command mentions meet-pro we include PRO
|
|
function setupTargetsFromCommand(cmd) {
|
|
const normalized = (cmd || '').toLowerCase();
|
|
const usePro = /\b(?:meet-pro|pro)\b/.test(normalized);
|
|
|
|
// Always include CE by default unless the command explicitly targets only pro and not CE.
|
|
// If both are mentioned include both.
|
|
const includeCE = true; // keep CE by default
|
|
|
|
if (includeCE) {
|
|
targets.push({
|
|
name: 'CE',
|
|
flagPath: CE_TYPINGS_FLAG_PATH,
|
|
distPath: CE_TYPINGS_DIST,
|
|
isReady: false,
|
|
flagWatcher: null,
|
|
distWatcher: null,
|
|
poller: null,
|
|
});
|
|
}
|
|
|
|
if (usePro) {
|
|
targets.push({
|
|
name: 'PRO',
|
|
flagPath: PRO_TYPINGS_FLAG_PATH,
|
|
distPath: PRO_TYPINGS_DIST,
|
|
isReady: false,
|
|
flagWatcher: null,
|
|
distWatcher: null,
|
|
poller: null,
|
|
});
|
|
}
|
|
|
|
// If the command only mentions PRO and you don't want CE, you could tweak logic here.
|
|
}
|
|
|
|
// Setup targets based on command
|
|
setupTargetsFromCommand(command);
|
|
|
|
// Create watchers for each target and keep cleanup functions
|
|
const cleanupFns = [];
|
|
for (const t of targets) {
|
|
const cleanup = watchTarget(t);
|
|
cleanupFns.push(cleanup);
|
|
}
|
|
|
|
// Kick an initial check/start attempt (in case flags already ready)
|
|
checkTypingsReady();
|
|
|
|
// Cleanup on exit
|
|
async function doCleanupAndExit(code = 0) {
|
|
console.log('\n🛑 Stopping...');
|
|
await killProcess();
|
|
for (const fn of cleanupFns) {
|
|
try { fn(); } catch (e) {}
|
|
}
|
|
process.exit(code);
|
|
}
|
|
|
|
process.on('SIGINT', async () => {
|
|
await doCleanupAndExit(0);
|
|
});
|
|
|
|
process.on('SIGTERM', async () => {
|
|
await doCleanupAndExit(0);
|
|
});
|