From 0ebcd1776132262b799f2814659f4d90c3f3c1b3 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Fri, 8 Jun 2018 10:02:42 +0200 Subject: [PATCH] feat(compiler): Use timestamps to verify cache validity --- .gitignore | 1 + index.js | 13 +++++---- lib/compiler.js | 67 +++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- spec/caching.spec.js | 45 +++++++++++++++++++++++++++++ tsconfig.json | 3 +- 6 files changed, 116 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index e1328041..4ca53526 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /dist/ +/coverage/ npm-debug.*.log diff --git a/index.js b/index.js index a8a87956..f3ae77df 100644 --- a/index.js +++ b/index.js @@ -97,7 +97,10 @@ class HtmlWebpackPlugin { // Clear the cache once a new HtmlWebpackPlugin is added childCompiler.clearCache(compiler); - compiler.hooks.compile.tap('HtmlWebpackPlugin', () => { + compiler.hooks.compilation.tap('HtmlWebpackPlugin', (compilation) => { + if (childCompiler.hasOutDatedTemplateCache(compilation)) { + childCompiler.clearCache(compiler); + } childCompiler.addTemplateToCompiler(compiler, this.options.template); }); @@ -111,12 +114,13 @@ class HtmlWebpackPlugin { compilation.errors.push(prettyError(err, compiler.context).toString()); return { content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR', - outputName: self.options.filename + outputName: self.options.filename, + hash: '' }; }) .then(compilationResult => { // If the compilation change didnt change the cache is valid - isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash; + isCompilationCached = Boolean(compilationResult.hash) && self.childCompilerHash === compilationResult.hash; self.childCompilerHash = compilationResult.hash; self.childCompilationOutputName = compilationResult.outputName; callback(); @@ -131,9 +135,6 @@ class HtmlWebpackPlugin { * @param {() => void} callback */ (compilation, callback) => { - // Clear the childCompilerCache - childCompiler.clearCache(compiler); - // Get all entry point names for this html file const entryNames = Array.from(compilation.entrypoints.keys()); const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks); diff --git a/lib/compiler.js b/lib/compiler.js index d9a871ed..7e431a73 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -33,14 +33,19 @@ class HtmlWebpackChildCompiler { */ this.compilationPromise; /** - * @type {Date} + * @type {number} */ - this.compilationStarted; + this.compilationStartedTimestamp; /** * All file dependencies of the child compiler * @type {string[]} */ this.fileDependencies = []; + /** + * Store if the cache was already verified for the given compilation + * @type {WeakMap}} + */ + this.cacheVerifiedForCompilation = new WeakMap(); } /** @@ -62,6 +67,7 @@ class HtmlWebpackChildCompiler { // Add the template to the childCompiler const newTemplateId = this.templates.length; this.templates.push(template); + // Mark the cache invalid return newTemplateId; } @@ -116,7 +122,7 @@ class HtmlWebpackChildCompiler { new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler); }); - this.compilationStarted = new Date(); + this.compilationStartedTimestamp = new Date().getTime(); this.compilationPromise = new Promise((resolve, reject) => { childCompiler.runAsChild((err, entries, childCompilation) => { // Extract templates @@ -158,6 +164,37 @@ class HtmlWebpackChildCompiler { return this.compilationPromise; } + + /** + * Returns `false` if any template file depenendencies has changed + * for the given main compilation + * + * @param {WebpackCompilation} mainCompilation + * @returns {boolean} + */ + hasOutDatedTemplateCache (mainCompilation) { + // Check if cache validation was already computed + const isCacheValid = this.cacheVerifiedForCompilation.get(mainCompilation); + if (isCacheValid !== undefined) { + return isCacheValid; + } + // If the compilation was never run there is no invalid cache + if (!this.compilationStartedTimestamp) { + this.cacheVerifiedForCompilation.set(mainCompilation, false); + return false; + } + // Check if any dependent file was changed after the last compilation + const fileTimestamps = mainCompilation.fileTimestamps; + const isCacheOutOfDate = this.fileDependencies.some((fileDependency) => { + const timestamp = fileTimestamps.get(fileDependency); + // If the timestamp is not known the file is new + // If the timestamp is larger then the file has changed + // Otherwise the file is still the same + return !timestamp || timestamp > this.compilationStartedTimestamp; + }); + this.cacheVerifiedForCompilation.set(mainCompilation, isCacheOutOfDate); + return isCacheOutOfDate; + } } /** @@ -214,10 +251,13 @@ const childCompilerCache = new WeakMap(); * @param {WebpackCompiler} mainCompiler */ function getChildCompiler (mainCompiler) { - if (!childCompilerCache[mainCompiler]) { - childCompilerCache[mainCompiler] = new HtmlWebpackChildCompiler(); + const cachedChildCompiler = childCompilerCache.get(mainCompiler); + if (cachedChildCompiler) { + return cachedChildCompiler; } - return childCompilerCache[mainCompiler]; + const newCompiler = new HtmlWebpackChildCompiler(); + childCompilerCache.set(mainCompiler, newCompiler); + return newCompiler; } /** @@ -226,7 +266,7 @@ function getChildCompiler (mainCompiler) { * @param {WebpackCompiler} mainCompiler */ function clearCache (mainCompiler) { - delete (childCompilerCache[mainCompiler]); + childCompilerCache.delete(mainCompiler); } /** @@ -252,6 +292,7 @@ function addTemplateToCompiler (mainCompiler, templatePath) { function compileTemplate (templatePath, outputFilename, mainCompilation) { const childCompiler = getChildCompiler(mainCompilation.compiler); return childCompiler.compileTemplates(mainCompilation).then((compiledTemplates) => { + if (!compiledTemplates[templatePath]) console.log(Object.keys(compiledTemplates), templatePath); const compiledTemplate = compiledTemplates[templatePath]; // Replace [hash] placeholders in filename const outputName = mainCompilation.mainTemplate.hooks.assetPath.call(outputFilename, { @@ -269,8 +310,20 @@ function compileTemplate (templatePath, outputFilename, mainCompilation) { }); } +/** + * Returns false if the cache is not valid anymore + * + * @param {WebpackCompilation} compilation + * @returns {boolean} + */ +function hasOutDatedTemplateCache (compilation) { + const childCompiler = childCompilerCache.get(compilation.compiler); + return childCompiler ? childCompiler.hasOutDatedTemplateCache(compilation) : false; +} + module.exports = { addTemplateToCompiler, compileTemplate, + hasOutDatedTemplateCache, clearCache }; diff --git a/package.json b/package.json index 3472e7b1..dd232467 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "posttest": "tsc", "commit": "git-cz", "build-examples": "node examples/build-examples.js", - "test": "jest --runInBand", + "test": "jest --runInBand --verbose --coverage", "test-watch": "jest --runInBand --watch", "release": "standard-version" }, diff --git a/spec/caching.spec.js b/spec/caching.spec.js index bab3d291..43f7fbec 100644 --- a/spec/caching.spec.js +++ b/spec/caching.spec.js @@ -203,4 +203,49 @@ describe('HtmlWebpackPluginCaching', function () { }) .then(done); }); + + it('should keep watching the webpack html if only a js file was changed', function (done) { + var template = path.join(__dirname, 'fixtures/plain.html'); + const jsFile = path.join(__dirname, 'fixtures/index.js'); + var htmlWebpackPlugin = new HtmlWebpackPlugin({ + template: template + }); + var compiler = setUpCompiler(htmlWebpackPlugin); + compiler.addTestFile(template); + compiler.addTestFile(jsFile); + // Build the template file for the first time + compiler.startWatching() + // Change the template file (second build) + .then(() => { + compiler.simulateFileChange(template, {footer: ''}); + return compiler.waitForWatchRunComplete(); + }) + // Change js + .then(() => { + compiler.simulateFileChange(jsFile, {footer: '// 1'}); + return compiler.waitForWatchRunComplete(); + }) + // Change js + .then(() => { + compiler.simulateFileChange(jsFile, {footer: '// 2'}); + return compiler.waitForWatchRunComplete(); + }) + // Change js + .then(() => { + compiler.simulateFileChange(jsFile, {footer: '// 3'}); + return compiler.waitForWatchRunComplete(); + }) + // Change the template file (third build) + .then(() => { + compiler.simulateFileChange(template, {footer: ''}); + return compiler.waitForWatchRunComplete(); + }) + .then(() => { + // Verify that the html was processed trice + expect(htmlWebpackPlugin.evaluateCompilationResult.mock.calls.length) + .toBe(3); + }) + .then(() => compiler.stopWatching()) + .then(done); + }); }); diff --git a/tsconfig.json b/tsconfig.json index ff013dfd..112ef7cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,7 @@ "node_modules", "spec", "examples", - "dist" + "dist", + "coverage" ] }