blob: 6816575e41d612f7c605baac007c0321bd431502 [file]
/*
* Licensed to Apache Software Foundation (ASF) under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Apache Software Foundation (ASF) licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Dev-mode wrapper for the Canopy BFF.
//
// Watches `canopy/web/dist/` and respawns the BFF whenever the production
// web bundle is rebuilt. This is the missing piece: the BFF caches
// in-memory references to the bundle HTML + asset paths at startup, so
// rebuilding `web/dist/` without restarting the BFF served stale references.
//
// Usage:
// node scripts/dev-bff.mjs
//
// Press Ctrl+C once to stop everything.
import { spawn } from 'node:child_process';
import { watch, existsSync } from 'node:fs';
import { resolve, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..', '..');
const DIST_DIR = resolve(REPO_ROOT, 'canopy', 'web', 'dist');
const SERVER_CWD = resolve(REPO_ROOT, 'canopy', 'server');
let child = null;
let restarting = false;
let pendingRestart = false;
let userStopping = false;
function startBff() {
console.log(`[dev-bff] starting BFF (port 4000, watching ${DIST_DIR})`);
child = spawn('node', ['--loader', 'ts-node/esm', 'src/index.ts'], {
cwd: SERVER_CWD,
env: {
...process.env,
SESSION_SECRET: process.env.SESSION_SECRET ?? 'canopy-e2e-test-secret-32chars!!',
CANOPY_DEV_NOAUTH: process.env.CANOPY_DEV_NOAUTH ?? 'true',
BANYANDB_TARGET: process.env.BANYANDB_TARGET ?? 'http://127.0.0.1:17913',
PORT: '4000',
LOG_LEVEL: process.env.LOG_LEVEL ?? 'warn',
},
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (userStopping) {
console.log(`[dev-bff] BFF exited (${signal}) — user-requested stop, exiting wrapper`);
process.exit(0);
}
if (signal === 'SIGINT' || signal === 'SIGTERM') {
// Killed by the watcher-triggered restart path; the once('exit', ...)
// handler in restartBff is responsible for respawning. Do nothing here.
return;
}
if (!restarting) {
console.log(`[dev-bff] BFF exited unexpectedly (code=${code}); respawning in 1s`);
setTimeout(() => startBff(), 1000);
}
});
}
function restartBff(reason) {
if (restarting) { pendingRestart = true; return; }
restarting = true;
console.log(`[dev-bff] ${reason} — restarting BFF`);
if (child) {
child.once('exit', () => {
restarting = false;
startBff();
if (pendingRestart) { pendingRestart = false; /* subsequent change already queued by the watcher */ }
});
child.kill('SIGTERM');
} else {
restarting = false;
startBff();
}
}
if (!existsSync(DIST_DIR)) {
console.error(`[dev-bff] ${DIST_DIR} does not exist — run \`npm run -w web build\` first`);
process.exit(1);
}
startBff();
// Watch dist for rebuilds. Use recursive + a debounce so multi-file writes
// (Vite writes index.html + assets/*.js + assets/*.css) trigger one restart.
let timer = null;
function notifyChange(filename) {
if (!filename) return;
if (timer) clearTimeout(timer);
timer = setTimeout(() => restartBff(`dist changed: ${filename}`), 200);
}
try {
watch(DIST_DIR, { recursive: true }, (_event, filename) => notifyChange(filename));
console.log('[dev-bff] watching dist/ for changes');
} catch (e) {
console.error(`[dev-bff] failed to watch ${DIST_DIR}: ${e.message}`);
console.error('[dev-bff] (recursive watch is required; Node 20+ on Linux is supported)');
}
process.on('SIGINT', () => {
userStopping = true;
console.log('[dev-bff] SIGINT — stopping BFF and exiting wrapper');
if (child) child.kill('SIGTERM');
// The child.on('exit') handler will call process.exit(0) once userStopping is true.
setTimeout(() => process.exit(0), 2000).unref();
});
process.on('SIGTERM', () => {
userStopping = true;
if (child) child.kill('SIGTERM');
setTimeout(() => process.exit(0), 2000).unref();
});