draft: refactor project so it sits on top of a standalone app

This commit is contained in:
2020-05-11 15:34:47 +02:00
parent a9ee5acc1c
commit f97e8455fc
13068 changed files with 1735502 additions and 866 deletions
+40
View File
@@ -0,0 +1,40 @@
// @ts-check
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
'use strict';
/**
* @type {{[sortmode: string] : (entryPointNames: Array<string>, compilation, htmlWebpackPluginOptions) => Array<string> }}
* This file contains different sort methods for the entry chunks names
*/
module.exports = {};
/**
* Performs identity mapping (no-sort).
* @param {Array} chunks the chunks to sort
* @return {Array} The sorted chunks
*/
module.exports.none = chunks => chunks;
/**
* Sort manually by the chunks
* @param {string[]} entryPointNames the chunks to sort
* @param {WebpackCompilation} compilation the webpack compilation
* @param htmlWebpackPluginOptions the plugin options
* @return {string[]} The sorted chunks
*/
module.exports.manual = (entryPointNames, compilation, htmlWebpackPluginOptions) => {
const chunks = htmlWebpackPluginOptions.chunks;
if (!Array.isArray(chunks)) {
return entryPointNames;
}
// Remove none existing entries from
// htmlWebpackPluginOptions.chunks
return chunks.filter((entryPointName) => {
return compilation.entrypoints.has(entryPointName);
});
};
/**
* Defines the default sorter.
*/
module.exports.auto = module.exports.none;
+356
View File
@@ -0,0 +1,356 @@
// @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
};
+31
View File
@@ -0,0 +1,31 @@
// @ts-nocheck
'use strict';
const PrettyError = require('pretty-error');
const prettyError = new PrettyError();
prettyError.withoutColors();
prettyError.skipPackage('html-plugin-evaluation');
prettyError.skipNodeFiles();
prettyError.skip(function (traceLine) {
return traceLine.path === 'html-plugin-evaluation';
});
module.exports = function (err, context) {
return {
toHtml: function () {
return 'Html Webpack Plugin:\n<pre>\n' + this.toString() + '</pre>';
},
toJsonHtml: function () {
return JSON.stringify(this.toHtml());
},
toString: function () {
try {
return prettyError.render(err).replace(/webpack:\/\/\/\./g, context);
} catch (e) {
// This can sometimes fail. We don't know why, but returning the
// original error is better than returning the error thrown by
// pretty-error.
return err;
}
}
};
};
+106
View File
@@ -0,0 +1,106 @@
// @ts-check
/** @typedef {import("../typings").Hooks} HtmlWebpackPluginHooks */
'use strict';
/**
* This file provides access to all public htmlWebpackPlugin hooks
*/
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
/** @typedef {import("../index.js")} HtmlWebpackPlugin */
const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;
// The following is the API definition for all available hooks
// For the TypeScript definition, see the Hooks type in typings.d.ts
/**
beforeAssetTagGeneration:
AsyncSeriesWaterfallHook<{
assets: {
publicPath: string,
js: Array<string>,
css: Array<string>,
favicon?: string | undefined,
manifest?: string | undefined
},
outputName: string,
plugin: HtmlWebpackPlugin
}>,
alterAssetTags:
AsyncSeriesWaterfallHook<{
assetTags: {
scripts: Array<HtmlTagObject>,
styles: Array<HtmlTagObject>,
meta: Array<HtmlTagObject>,
},
outputName: string,
plugin: HtmlWebpackPlugin
}>,
alterAssetTagGroups:
AsyncSeriesWaterfallHook<{
headTags: Array<HtmlTagObject | HtmlTagObject>,
bodyTags: Array<HtmlTagObject | HtmlTagObject>,
outputName: string,
plugin: HtmlWebpackPlugin
}>,
afterTemplateExecution:
AsyncSeriesWaterfallHook<{
html: string,
headTags: Array<HtmlTagObject | HtmlTagObject>,
bodyTags: Array<HtmlTagObject | HtmlTagObject>,
outputName: string,
plugin: HtmlWebpackPlugin,
}>,
beforeEmit:
AsyncSeriesWaterfallHook<{
html: string,
outputName: string,
plugin: HtmlWebpackPlugin,
}>,
afterEmit:
AsyncSeriesWaterfallHook<{
outputName: string,
plugin: HtmlWebpackPlugin
}>
*/
/**
* @type {WeakMap<WebpackCompilation, HtmlWebpackPluginHooks>}}
*/
const htmlWebpackPluginHooksMap = new WeakMap();
/**
* Returns all public hooks of the html webpack plugin for the given compilation
*
* @param {WebpackCompilation} compilation
* @returns {HtmlWebpackPluginHooks}
*/
function getHtmlWebpackPluginHooks (compilation) {
let hooks = htmlWebpackPluginHooksMap.get(compilation);
// Setup the hooks only once
if (hooks === undefined) {
hooks = createHtmlWebpackPluginHooks();
htmlWebpackPluginHooksMap.set(compilation, hooks);
}
return hooks;
}
/**
* Add hooks to the webpack compilation object to allow foreign plugins to
* extend the HtmlWebpackPlugin
*
* @returns {HtmlWebpackPluginHooks}
*/
function createHtmlWebpackPluginHooks () {
return {
beforeAssetTagGeneration: new AsyncSeriesWaterfallHook(['pluginArgs']),
alterAssetTags: new AsyncSeriesWaterfallHook(['pluginArgs']),
alterAssetTagGroups: new AsyncSeriesWaterfallHook(['pluginArgs']),
afterTemplateExecution: new AsyncSeriesWaterfallHook(['pluginArgs']),
beforeEmit: new AsyncSeriesWaterfallHook(['pluginArgs']),
afterEmit: new AsyncSeriesWaterfallHook(['pluginArgs'])
};
}
module.exports = {
getHtmlWebpackPluginHooks
};
+71
View File
@@ -0,0 +1,71 @@
// @ts-check
/** @typedef {import("../typings").HtmlTagObject} HtmlTagObject */
/**
* @file
* This file provides to helper to create html as a object repesentation as
* thoses objects are easier to modify than pure string representations
*
* Usage:
* ```
* const element = createHtmlTagObject('h1', {class: 'demo'}, 'Hello World');
* const html = htmlTagObjectToString(element);
* console.log(html) // -> <h1 class="demo">Hello World</h1>
* ```
*/
/**
* All html tag elements which must not contain innerHTML
* @see https://www.w3.org/TR/html5/syntax.html#void-elements
*/
const voidTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
/**
* Turn a tag definition into a html string
* @param {HtmlTagObject} tagDefinition
* A tag element according to the htmlWebpackPlugin object notation
*
* @param xhtml {boolean}
* Wether the generated html should add closing slashes to be xhtml compliant
*/
function htmlTagObjectToString (tagDefinition, xhtml) {
const attributes = Object.keys(tagDefinition.attributes || {})
.filter(function (attributeName) {
return tagDefinition.attributes[attributeName] !== false;
})
.map(function (attributeName) {
if (tagDefinition.attributes[attributeName] === true) {
return xhtml ? attributeName + '="' + attributeName + '"' : attributeName;
}
return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';
});
return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (tagDefinition.voidTag && xhtml ? '/' : '') + '>' +
(tagDefinition.innerHTML || '') +
(tagDefinition.voidTag ? '' : '</' + tagDefinition.tagName + '>');
}
/**
* Static helper to create a tag object to be get injected into the dom
*
* @param {string} tagName
* the name of the tage e.g. 'div'
*
* @param {{[attributeName: string]: string|boolean}} [attributes]
* tag attributes e.g. `{ 'class': 'example', disabled: true }`
*
* @param {string} [innerHTML]
*
* @returns {HtmlTagObject}
*/
function createHtmlTagObject (tagName, attributes, innerHTML) {
return {
tagName: tagName,
voidTag: voidTags.indexOf(tagName) !== -1,
attributes: attributes || {},
innerHTML: innerHTML
};
}
module.exports = {
createHtmlTagObject: createHtmlTagObject,
htmlTagObjectToString: htmlTagObjectToString
};
+34
View File
@@ -0,0 +1,34 @@
/* This loader renders the template with underscore if no other loader was found */
// @ts-nocheck
'use strict';
const _ = require('lodash');
const loaderUtils = require('loader-utils');
module.exports = function (source) {
// Get templating options
const options = this.query !== '' ? loaderUtils.getOptions(this) : {};
const force = options.force || false;
const allLoadersButThisOne = this.loaders.filter(function (loader) {
return loader.normal !== module.exports;
});
// This loader shouldn't kick in if there is any other loader (unless it's explicitly enforced)
if (allLoadersButThisOne.length > 0 && !force) {
return source;
}
// Skip .js files (unless it's explicitly enforced)
if (/\.js$/.test(this.resourcePath) && !force) {
return source;
}
// The following part renders the template with lodash as a minimalistic loader
//
const template = _.template(source, _.defaults(options, { interpolate: /<%=([\s\S]+?)%>/g, variable: 'data' }));
// Use __non_webpack_require__ to enforce using the native nodejs require
// during template execution
return 'var _ = __non_webpack_require__(' + JSON.stringify(require.resolve('lodash')) + ');' +
'module.exports = function (templateParams) { with(templateParams) {' +
// Execute the lodash template
'return (' + template.source + ')();' +
'}}';
};