Compare commits

...

1 Commits

Author SHA1 Message Date
joshhunt
ae32488285 Devenv: Improve webpack readiness check 2025-11-25 10:42:30 +00:00
3 changed files with 127 additions and 6 deletions

View File

@@ -10,16 +10,16 @@ local_resource(
local_resource(
'yarn start',
cmd='rm -rf public/build/assets-manifest.json',
serve_cmd='yarn start:noLint',
serve_cmd='yarn start:noLint --env httpReadinessPort=9080',
resource_deps=['yarn install'],
# Note: this doesn't seem to work as expected - the assets-manifest is somehow created before
# the webpack build is complete?
readiness_probe=probe(
initial_delay_secs=10,
period_secs=1,
exec=exec_action(["bash", "-c", "stat public/build/assets-manifest.json"]),
failure_threshold=1,
http_get=http_get_action(
host='localhost',
port=9080,
),
),
allow_parallel=True,
labels=["local"]

View File

@@ -0,0 +1,112 @@
const http = require('http');
/**
* Starts a HTTP server that provides a readiness endpoint indicating the build status.
* The server responds with:
* - 200 when the build is complete and successful
* - 503 when the build is in progress, for both the initial build and subsequent rebuilds when in watch mode
* - 500 when the build has failed
*
* This is used in the devenv/frontend-service Tiltfile to determine when the frontend build is ready.
*/
class HttpReadinessPlugin {
/**
* @param {Object} options
* @param {number} options.port
* @param {string} [options.host]
*/
constructor(options = {}) {
this.port = options.port;
this.host = options.host || 'localhost';
this.isReady = false;
this.server = null;
this.hasError = false;
}
/**
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
// Start HTTP server on first compilation
compiler.hooks.beforeCompile.tapAsync('HttpReadinessPlugin', (params, callback) => {
if (!this.server) {
this.startServer();
}
callback();
});
// Mark as building when compilation starts
compiler.hooks.compile.tap('HttpReadinessPlugin', () => {
this.isReady = false;
this.hasError = false;
console.log('[HttpReadinessPlugin] Build started - readiness endpoint returning 503');
});
// Mark as ready when compilation completes successfully
compiler.hooks.done.tap('HttpReadinessPlugin', (stats) => {
if (stats.hasErrors()) {
this.hasError = true;
this.isReady = false;
console.log('[HttpReadinessPlugin] Build completed with errors - readiness endpoint returning 500');
} else {
this.isReady = true;
this.hasError = false;
console.log('[HttpReadinessPlugin] Build completed successfully - readiness endpoint returning 200');
}
});
// Mark as error on compilation failure
compiler.hooks.failed.tap('HttpReadinessPlugin', () => {
this.hasError = true;
this.isReady = false;
console.log('[HttpReadinessPlugin] Build failed - readiness endpoint returning 500');
});
// Close server on shutdown
compiler.hooks.shutdown.tap('HttpReadinessPlugin', () => {
this.stopServer();
});
}
startServer() {
this.server = http.createServer((req, res) => {
// Only respond to GET requests
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method Not Allowed');
return;
}
// Respond based on build state
if (this.isReady) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK - Build ready');
} else if (this.hasError) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error - Build failed');
} else {
res.writeHead(503, { 'Content-Type': 'text/plain' });
res.end('Service Unavailable - Build in progress');
}
});
this.server.listen(this.port, this.host, () => {
console.log(`[HttpReadinessPlugin] Readiness server listening on http://${this.host}:${this.port}`);
});
this.server.on('error', (error) => {
console.error(`[HttpReadinessPlugin] Server error:`, error);
});
}
stopServer() {
if (this.server) {
this.server.close(() => {
console.log('[HttpReadinessPlugin] Readiness server stopped');
});
this.server = null;
}
}
}
module.exports = HttpReadinessPlugin;

View File

@@ -14,6 +14,7 @@ const { merge } = require('webpack-merge');
const WebpackBar = require('webpackbar');
const getEnvConfig = require('./env-util.js');
const HttpReadinessPlugin = require('./plugins/HttpReadinessPlugin.js');
const common = require('./webpack.common.js');
const esbuildTargets = resolveToEsbuildTarget(browserslist(), { printUnknownTargets: false });
// esbuild-loader 3.0.0+ requires format to be set to prevent it
@@ -48,6 +49,8 @@ function scenesModule() {
const envConfig = getEnvConfig();
module.exports = (env = {}) => {
console.log('env', env);
const httpReadinessPort = parseInt(env.httpReadinessPort, 10);
return merge(common, {
devtool: 'source-map',
mode: 'development',
@@ -170,6 +173,12 @@ module.exports = (env = {}) => {
name: 'Grafana',
}),
new EnvironmentPlugin(envConfig),
httpReadinessPort
? new HttpReadinessPlugin({
port: httpReadinessPort,
host: 'localhost',
})
: new DefinePlugin({}), // bogus plugin to satisfy webpack API
],
stats: 'minimal',