blob: 7da301f220d767f8ee360ff35da2d45f17896fc2 [file]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const chokidar = require('chokidar');
const path = require('path');
const childProcess = require('child_process');
const fs = require('fs');
const cwd = process.cwd();
const frontEndDir = path.join(cwd, 'front_end');
const testsDir = path.join(cwd, 'test');
const {WebSocketServer} = require('ws');
const env = process.env;
const extractArgument = argName => {
const arg = process.argv.find(value => value.startsWith(`${argName}`));
if (!arg) {
return;
}
return arg.slice(`${argName}=`.length);
};
const relativeFileName = absoluteName => {
return path.relative(cwd, absoluteName);
};
const currentTimeString = () => {
return (new Date())
.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'});
};
const connections = {};
let lastConnectionId = 0;
// Extract the target if it's provided.
const target = extractArgument('--target') || 'Default';
const PORT = 8080;
let restartBuild = false;
let autoninja;
const changedFiles = new Set();
const startWebSocketServerForCssChanges = () => {
const wss = new WebSocketServer({port: PORT});
wss.on('listening', () => {
console.log(`Listening connections for CSS changes at ${PORT}\n`);
});
wss.on('connection', ws => {
const connection = {
id: ++lastConnectionId,
ws,
};
connections[connection.id] = connection;
ws.on('close', () => {
delete connections[connection.id];
});
});
};
const onFileChange = fileName => {
// Some filesystems emit multiple events in quick succession for a
// single file change. Here we track the changed files, and reset
// after a short timeout.
if (!fileName || changedFiles.has(fileName)) {
return;
}
changedFiles.add(fileName);
setTimeout(() => {
changedFiles.delete(fileName);
}, 250);
// If the exitCode is null, autoninja is still running so stop it
// and try to restart it again.
const ninjaProcessExists = Boolean(autoninja && autoninja.pid);
if (ninjaProcessExists) {
const isRunning = ninjaProcessExists && autoninja.exitCode === null;
if (isRunning) {
autoninja.kill();
restartBuild = true;
}
return;
}
if (fileName.endsWith('.css')) {
console.log(`${currentTimeString()} - ${relativeFileName(fileName)} changed, notifying frontend`);
const content = fs.readFileSync(fileName, {encoding: 'utf8', flag: 'r'});
Object.values(connections).forEach(connection => {
connection.ws.send(JSON.stringify({file: fileName, content}));
});
return;
}
autoninja = childProcess.spawn('autoninja', ['-C', `out/${target}`], {cwd, env, stdio: 'inherit'});
autoninja.on('close', () => {
autoninja = null;
if (restartBuild) {
restartBuild = false;
console.log(`${currentTimeString()} - ${relativeFileName(fileName)} changed, restarting ninja`);
onFileChange();
}
});
};
// Run build initially before starting to watch for changes.
console.log('Running initial build before watching changes');
childProcess.spawnSync('autoninja', ['-C', `out/${target}`], {cwd, env, stdio: 'inherit'});
// Watch the front_end and test folder and build on any change.
console.log(`Watching for changes in ${frontEndDir} and ${testsDir}`);
chokidar.watch(frontEndDir).on('change', onFileChange);
chokidar.watch(testsDir).on('change', onFileChange);
startWebSocketServerForCssChanges();