Skip to content

Commit

Permalink
feat(compiler): Use timestamps to verify cache validity
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon committed Jul 11, 2018
1 parent dfe1d10 commit 0ebcd17
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
/node_modules/
/dist/
/coverage/
npm-debug.*.log
13 changes: 7 additions & 6 deletions index.js
Expand Up @@ -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);
});

Expand All @@ -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();
Expand All @@ -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);
Expand Down
67 changes: 60 additions & 7 deletions lib/compiler.js
Expand Up @@ -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<WebpackCompilation, boolean>}}
*/
this.cacheVerifiedForCompilation = new WeakMap();
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -226,7 +266,7 @@ function getChildCompiler (mainCompiler) {
* @param {WebpackCompiler} mainCompiler
*/
function clearCache (mainCompiler) {
delete (childCompilerCache[mainCompiler]);
childCompilerCache.delete(mainCompiler);
}

/**
Expand All @@ -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, {
Expand All @@ -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
};
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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"
},
Expand Down
45 changes: 45 additions & 0 deletions spec/caching.spec.js
Expand Up @@ -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: '<!-- 1 -->'});
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: '<!-- 2 -->'});
return compiler.waitForWatchRunComplete();
})
.then(() => {
// Verify that the html was processed trice
expect(htmlWebpackPlugin.evaluateCompilationResult.mock.calls.length)
.toBe(3);
})
.then(() => compiler.stopWatching())
.then(done);
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Expand Up @@ -31,6 +31,7 @@
"node_modules",
"spec",
"examples",
"dist"
"dist",
"coverage"
]
}

0 comments on commit 0ebcd17

Please sign in to comment.