diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js new file mode 100644 index 0000000000000000000000000000000000000000..8cd1d2eb2caa89a0fc44ba33e337bb0201e8e4c6 --- /dev/null +++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js @@ -0,0 +1,24 @@ +const div = document.createElement('div'); + +Object.assign(div.style, { + width: '100vw', + height: '100vh', + position: 'fixed', + top: 0, + left: 0, + 'z-index': 100000, + background: 'rgba(0,0,0,0.9)', + 'font-size': '25px', + 'font-family': 'monospace', + color: 'white', + padding: '2.5em', + 'text-align': 'center', +}); + +div.innerHTML = ` +<h1 style="color:white">🧙 Webpack is doing its magic 🧙</h1> +<p>If you use Hot Module reloading, the page will reload in a few seconds.</p> +<p>If you do not use Hot Module reloading, please <a href="">reload the page manually in a few seconds</a></p> +`; + +document.body.append(div); diff --git a/config/helpers/incremental_webpack_compiler.js b/config/helpers/incremental_webpack_compiler.js new file mode 100644 index 0000000000000000000000000000000000000000..9af52248d02d4a8041447532c8833d6c1bfc886a --- /dev/null +++ b/config/helpers/incremental_webpack_compiler.js @@ -0,0 +1,128 @@ +const fs = require('fs'); +const path = require('path'); + +const log = (msg, ...rest) => console.log(`IncrementalWebpackCompiler: ${msg}`, ...rest); + +// If we force a recompile immediately, the page reload doesn't seem to work. +// Five seconds seem to work fine and the user can read the message +const TIMEOUT = 5000; + +class NoopCompiler { + enabled = false; + + filterEntryPoints(entryPoints) { + return entryPoints; + } + + logStatus() {} + + setupMiddleware() {} +} + +class IncrementalWebpackCompiler extends NoopCompiler { + enabled = true; + + constructor(historyFilePath) { + super(); + this.history = {}; + this.compiledEntryPoints = new Set([ + // Login page + 'pages.sessions.new', + // Explore page + 'pages.root', + ]); + this.historyFilePath = historyFilePath; + this.loadFromHistory(); + } + + filterEntryPoints(entrypoints) { + return Object.fromEntries( + Object.entries(entrypoints).map(([key, val]) => { + if (this.compiledEntryPoints.has(key)) { + return [key, val]; + } + return [key, ['./webpack_non_compiled_placeholder.js']]; + }), + ); + } + + logStatus(totalCount) { + const current = this.compiledEntryPoints.size; + log(`Currently compiling route entrypoints: ${current} of ${totalCount}`); + } + + setupMiddleware(app, server) { + app.use((req, res, next) => { + const fileName = path.basename(req.url); + + /** + * We are only interested in files that have a name like `pages.foo.bar.chunk.js` + * because those are the ones corresponding to our entry points. + * + * This filters out hot update files that are for example named "pages.foo.bar.[hash].hot-update.js" + */ + if (fileName.startsWith('pages.') && fileName.endsWith('.chunk.js')) { + const chunk = fileName.replace(/\.chunk\.js$/, ''); + + this.addToHistory(chunk); + + if (!this.compiledEntryPoints.has(chunk)) { + log(`First time we are seeing ${chunk}. Adding to compilation.`); + + this.compiledEntryPoints.add(chunk); + + setTimeout(() => { + server.middleware.invalidate(() => { + if (server.sockets) { + server.sockWrite(server.sockets, 'content-changed'); + } + }); + }, TIMEOUT); + } + } + + next(); + }); + } + + // private methods + + addToHistory(chunk) { + if (!this.history[chunk]) { + this.history[chunk] = { lastVisit: null, count: 0 }; + } + this.history[chunk].lastVisit = Date.now(); + this.history[chunk].count += 1; + + try { + fs.writeFileSync(this.historyFilePath, JSON.stringify(this.history), 'utf8'); + } catch (e) { + log('Warning – Could not write to history', e.message); + } + } + + loadFromHistory() { + try { + this.history = JSON.parse(fs.readFileSync(this.historyFilePath, 'utf8')); + const entryPoints = Object.keys(this.history); + log(`Successfully loaded history containing ${entryPoints.length} entry points`); + /* + TODO: Let's ask a few folks to give us their history file after a milestone of usage + Then we can make smarter decisions on when to throw out rather than rendering everything + Something like top 20/30/40 entries visited in the last 7/10/15 days might be sufficient + */ + this.compiledEntryPoints = new Set([...this.compiledEntryPoints, ...entryPoints]); + } catch (e) { + log(`No history found...`); + } + } +} + +module.exports = (enabled, historyFilePath) => { + log(`Status – ${enabled ? 'enabled' : 'disabled'}`); + + if (enabled) { + return new IncrementalWebpackCompiler(historyFilePath); + } + return new NoopCompiler(); +}; diff --git a/config/webpack.config.js b/config/webpack.config.js index 958b27d674dc5bf41ddcc962a54d807410777938..19059c35c4670d9d67011a9a2abd88d8a76ee8fb 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -9,6 +9,7 @@ const MonacoWebpackPlugin = require('./plugins/monaco_webpack'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const CopyWebpackPlugin = require('copy-webpack-plugin'); const vendorDllHash = require('./helpers/vendor_dll_hash'); +const createIncrementalWebpackCompiler = require('./helpers/incremental_webpack_compiler'); const ROOT_PATH = path.resolve(__dirname, '..'); const VENDOR_DLL = process.env.WEBPACK_VENDOR_DLL && process.env.WEBPACK_VENDOR_DLL !== 'false'; @@ -23,6 +24,10 @@ const DEV_SERVER_ALLOWED_HOSTS = process.env.DEV_SERVER_ALLOWED_HOSTS && process.env.DEV_SERVER_ALLOWED_HOSTS.split(','); const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false'; const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false'; +const INCREMENTAL_COMPILER_ENABLED = + IS_DEV_SERVER && + process.env.DEV_SERVER_INCREMENTAL && + process.env.DEV_SERVER_INCREMENTAL !== 'false'; const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false'; const WEBPACK_MEMORY_TEST = process.env.WEBPACK_MEMORY_TEST && process.env.WEBPACK_MEMORY_TEST !== 'false'; @@ -48,6 +53,11 @@ let autoEntriesCount = 0; let watchAutoEntries = []; const defaultEntries = ['./main']; +const incrementalCompiler = createIncrementalWebpackCompiler( + INCREMENTAL_COMPILER_ENABLED, + path.join(CACHE_PATH, 'incremental-webpack-compiler-history.json'), +); + function generateEntries() { // generate automatic entry points const autoEntries = {}; @@ -97,7 +107,7 @@ function generateEntries() { jira_connect_app: './jira_connect/index.js', }; - return Object.assign(manualEntries, autoEntries); + return Object.assign(manualEntries, incrementalCompiler.filterEntryPoints(autoEntries)); } const alias = { @@ -495,9 +505,13 @@ module.exports = { watchAutoEntries.forEach((watchPath) => compilation.contextDependencies.add(watchPath)); // report our auto-generated bundle count - console.log( - `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`, - ); + if (incrementalCompiler.enabled) { + incrementalCompiler.logStatus(autoEntriesCount); + } else { + console.log( + `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`, + ); + } callback(); }); @@ -576,8 +590,10 @@ module.exports = { */ new webpack.IgnorePlugin(/moment/, /pikaday/), ].filter(Boolean), - devServer: { + before(app, server) { + incrementalCompiler.setupMiddleware(app, server); + }, host: DEV_SERVER_HOST, port: DEV_SERVER_PORT, public: DEV_SERVER_PUBLIC_ADDR,