From ae32488285ecb912c86eb2e22465efaf7a2cd6c7 Mon Sep 17 00:00:00 2001 From: joshhunt Date: Sat, 15 Nov 2025 14:46:16 +0000 Subject: [PATCH] Devenv: Improve webpack readiness check --- devenv/frontend-service/Tiltfile | 12 +- .../webpack/plugins/HttpReadinessPlugin.js | 112 ++++++++++++++++++ scripts/webpack/webpack.dev.js | 9 ++ 3 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 scripts/webpack/plugins/HttpReadinessPlugin.js diff --git a/devenv/frontend-service/Tiltfile b/devenv/frontend-service/Tiltfile index 9c443cf2ece..c045fd1799c 100644 --- a/devenv/frontend-service/Tiltfile +++ b/devenv/frontend-service/Tiltfile @@ -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"] diff --git a/scripts/webpack/plugins/HttpReadinessPlugin.js b/scripts/webpack/plugins/HttpReadinessPlugin.js new file mode 100644 index 00000000000..e2735b3442f --- /dev/null +++ b/scripts/webpack/plugins/HttpReadinessPlugin.js @@ -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; diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index f48aada0ed7..90faecb9f71 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -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',