123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- // @ts-check
- /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
- /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
- /** @typedef {import("webpack/lib/Chunk.js")} WebpackChunk */
- 'use strict';
- /**
- * @file
- * This file uses webpack to compile a template with a child compiler.
- *
- * [TEMPLATE] -> [JAVASCRIPT]
- *
- */
- 'use strict';
- const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
- const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
- const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
- const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
- const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
- /**
- * The HtmlWebpackChildCompiler is a helper to allow resusing one childCompiler
- * for multile HtmlWebpackPlugin instances to improve the compilation performance.
- */
- class HtmlWebpackChildCompiler {
- constructor () {
- /**
- * @type {string[]} templateIds
- * The template array will allow us to keep track which input generated which output
- */
- this.templates = [];
- /**
- * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
- */
- this.compilationPromise; // eslint-disable-line
- /**
- * @type {number}
- */
- this.compilationStartedTimestamp; // eslint-disable-line
- /**
- * @type {number}
- */
- this.compilationEndedTimestamp; // eslint-disable-line
- /**
- * All file dependencies of the child compiler
- * @type {string[]}
- */
- this.fileDependencies = [];
- }
- /**
- * Add a templatePath to the child compiler
- * The given template will be compiled by `compileTemplates`
- * @param {string} template - The webpack path to the template e.g. `'!!html-loader!index.html'`
- * @returns {boolean} true if the template is new
- */
- addTemplate (template) {
- const templateId = this.templates.indexOf(template);
- // Don't add the template to the compiler if a similar template was already added
- if (templateId !== -1) {
- return false;
- }
- // A child compiler can compile only once
- // throw an error if a new template is added after the compilation started
- if (this.isCompiling()) {
- throw new Error('New templates can only be added before `compileTemplates` was called.');
- }
- // Add the template to the childCompiler
- this.templates.push(template);
- // Mark the cache invalid
- return true;
- }
- /**
- * Returns true if the childCompiler is currently compiling
- * @retuns {boolean}
- */
- isCompiling () {
- return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
- }
- /**
- * Returns true if the childCOmpiler is done compiling
- */
- didCompile () {
- return this.compilationEndedTimestamp !== undefined;
- }
- /**
- * This function will start the template compilation
- * once it is started no more templates can be added
- *
- * @param {WebpackCompilation} mainCompilation
- * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
- */
- compileTemplates (mainCompilation) {
- // To prevent multiple compilations for the same template
- // the compilation is cached in a promise.
- // If it already exists return
- if (this.compilationPromise) {
- return this.compilationPromise;
- }
- // The entry file is just an empty helper as the dynamic template
- // require is added in "loader.js"
- const outputOptions = {
- filename: '__child-[name]',
- publicPath: mainCompilation.outputOptions.publicPath
- };
- const compilerName = 'HtmlWebpackCompiler';
- // Create an additional child compiler which takes the template
- // and turns it into an Node.JS html factory.
- // This allows us to use loaders during the compilation
- const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
- // The file path context which webpack uses to resolve all relative files to
- childCompiler.context = mainCompilation.compiler.context;
- // Compile the template to nodejs javascript
- new NodeTemplatePlugin(outputOptions).apply(childCompiler);
- new NodeTargetPlugin().apply(childCompiler);
- new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
- new LoaderTargetPlugin('node').apply(childCompiler);
- // Add all templates
- this.templates.forEach((template, index) => {
- new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
- });
- this.compilationStartedTimestamp = new Date().getTime();
- this.compilationPromise = new Promise((resolve, reject) => {
- childCompiler.runAsChild((err, entries, childCompilation) => {
- // Extract templates
- const compiledTemplates = entries
- ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
- : [];
- // Extract file dependencies
- if (entries) {
- this.fileDependencies = Array.from(childCompilation.fileDependencies);
- }
- // Reject the promise if the childCompilation contains error
- if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
- const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
- reject(new Error('Child compilation failed:\n' + errorDetails));
- return;
- }
- // Reject if the error object contains errors
- if (err) {
- reject(err);
- return;
- }
- /**
- * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
- */
- const result = {};
- compiledTemplates.forEach((templateSource, entryIndex) => {
- // The compiledTemplates are generated from the entries added in
- // the addTemplate function.
- // Therefore the array index of this.templates should be the as entryIndex.
- result[this.templates[entryIndex]] = {
- content: templateSource,
- hash: childCompilation.hash,
- entry: entries[entryIndex]
- };
- });
- this.compilationEndedTimestamp = new Date().getTime();
- resolve(result);
- });
- });
- return this.compilationPromise;
- }
- }
- /**
- * The webpack child compilation will create files as a side effect.
- * This function will extract them and clean them up so they won't be written to disk.
- *
- * Returns the source code of the compiled templates as string
- *
- * @returns Array<string>
- */
- function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
- const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
- return mainCompilation.mainTemplate.getAssetPath(filename, {
- hash: childCompilation.hash,
- chunk: entryChunk,
- name: `HtmlWebpackPlugin_${index}`
- });
- });
- helperAssetNames.forEach((helperFileName) => {
- delete mainCompilation.assets[helperFileName];
- });
- const helperContents = helperAssetNames.map((helperFileName) => {
- return childCompilation.assets[helperFileName].source();
- });
- return helperContents;
- }
- /**
- * @type {WeakMap<WebpackCompiler, HtmlWebpackChildCompiler>}}
- */
- const childCompilerCache = new WeakMap();
- /**
- * Get child compiler from cache or a new child compiler for the given mainCompilation
- *
- * @param {WebpackCompiler} mainCompiler
- */
- function getChildCompiler (mainCompiler) {
- const cachedChildCompiler = childCompilerCache.get(mainCompiler);
- if (cachedChildCompiler) {
- return cachedChildCompiler;
- }
- const newCompiler = new HtmlWebpackChildCompiler();
- childCompilerCache.set(mainCompiler, newCompiler);
- return newCompiler;
- }
- /**
- * Remove the childCompiler from the cache
- *
- * @param {WebpackCompiler} mainCompiler
- */
- function clearCache (mainCompiler) {
- const childCompiler = getChildCompiler(mainCompiler);
- // If this childCompiler was already used
- // remove the entire childCompiler from the cache
- if (childCompiler.isCompiling() || childCompiler.didCompile()) {
- childCompilerCache.delete(mainCompiler);
- }
- }
- /**
- * Register a template for the current main compiler
- * @param {WebpackCompiler} mainCompiler
- * @param {string} templatePath
- */
- function addTemplateToCompiler (mainCompiler, templatePath) {
- const childCompiler = getChildCompiler(mainCompiler);
- const isNew = childCompiler.addTemplate(templatePath);
- if (isNew) {
- clearCache(mainCompiler);
- }
- }
- /**
- * Starts the compilation for all templates.
- * This has to be called once all templates where added.
- *
- * If this function is called multiple times it will use a cache inside
- * the childCompiler
- *
- * @param {string} templatePath
- * @param {string} outputFilename
- * @param {WebpackCompilation} mainCompilation
- */
- 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.getAssetPath(outputFilename, {
- hash: compiledTemplate.hash,
- chunk: compiledTemplate.entry
- });
- return {
- // Hash of the template entry point
- hash: compiledTemplate.hash,
- // Output name
- outputName: outputName,
- // Compiled code
- content: compiledTemplate.content
- };
- });
- }
- /**
- * Return all file dependencies of the last child compilation
- *
- * @param {WebpackCompiler} compiler
- * @returns {Array<string>}
- */
- function getFileDependencies (compiler) {
- const childCompiler = getChildCompiler(compiler);
- return childCompiler.fileDependencies;
- }
- /**
- * @type {WeakMap<WebpackCompilation, WeakMap<HtmlWebpackChildCompiler, boolean>>}}
- */
- const hasOutdatedCompilationDependenciesMap = new WeakMap();
- /**
- * Returns `true` if the file dependencies of the current childCompiler
- * for the given mainCompilation are outdated.
- *
- * Uses the `hasOutdatedCompilationDependenciesMap` cache if possible.
- *
- * @param {WebpackCompilation} mainCompilation
- * @returns {boolean}
- */
- function hasOutDatedTemplateCache (mainCompilation) {
- const childCompiler = getChildCompiler(mainCompilation.compiler);
- /**
- * @type {WeakMap<HtmlWebpackChildCompiler, boolean>|undefined}
- */
- let hasOutdatedChildCompilerDependenciesMap = hasOutdatedCompilationDependenciesMap.get(mainCompilation);
- // Create map for childCompiler if none exist
- if (!hasOutdatedChildCompilerDependenciesMap) {
- hasOutdatedChildCompilerDependenciesMap = new WeakMap();
- hasOutdatedCompilationDependenciesMap.set(mainCompilation, hasOutdatedChildCompilerDependenciesMap);
- }
- // Try to get the `checkChildCompilerCache` result from cache
- let isOutdated = hasOutdatedChildCompilerDependenciesMap.get(childCompiler);
- if (isOutdated !== undefined) {
- return isOutdated;
- }
- // If `checkChildCompilerCache` has never been called for the given
- // `mainCompilation` and `childCompiler` combination call it:
- isOutdated = isChildCompilerCacheOutdated(mainCompilation, childCompiler);
- hasOutdatedChildCompilerDependenciesMap.set(childCompiler, isOutdated);
- return isOutdated;
- }
- /**
- * Returns `true` if the file dependencies of the given childCompiler are outdated.
- *
- * @param {WebpackCompilation} mainCompilation
- * @param {HtmlWebpackChildCompiler} childCompiler
- * @returns {boolean}
- */
- function isChildCompilerCacheOutdated (mainCompilation, childCompiler) {
- // If the compilation was never run there is no invalid cache
- if (!childCompiler.compilationStartedTimestamp) {
- return false;
- }
- // Check if any dependent file was changed after the last compilation
- const fileTimestamps = mainCompilation.fileTimestamps;
- const isCacheOutOfDate = childCompiler.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 > childCompiler.compilationStartedTimestamp;
- });
- return isCacheOutOfDate;
- }
- module.exports = {
- addTemplateToCompiler,
- compileTemplate,
- hasOutDatedTemplateCache,
- clearCache,
- getFileDependencies
- };
|