html-bundler-webpack-plugin
Version:
Generates complete single-page or multi-page website from source assets. Built-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.
194 lines (160 loc) • 6.25 kB
JavaScript
const fs = require('fs/promises');
const path = require('path');
const { favicons, config } = require('favicons');
const { black, blueBright, yellow } = require('ansis');
const PluginService = require('../../src/Plugin/PluginService');
const BundlerPlugin = require('../../src/');
const { joinUrl, outToConsole } = require('../../src/Common/Helpers');
class FaviconsBundlerPlugin {
pluginName = 'favicons-bundler-plugin';
/** {FaviconResponse} */
faviconResponse = null;
/** {string} The source favicon file. */
faviconFile = '';
/**
* The output path to save generated files.
*
* @type {string}
* */
outputPath = '';
/**
* The full output public path..
* It will be used if Webpack output.publicPath ia a URL.
*
* @type {string}
* */
outputUrl = '';
/**
* The key is the source HTML file, where was found the original `faviconFile` parsed in the `faviconTag`.
* The `faviconFile` is the source file of the parsed favicon file.
* The `faviconTag` is a link tag string, used as a placeholder which will be replaced with the generated favicon tags.
*
* @type {Map<string, {faviconFile: string, faviconTag: string}>}
*/
pages = new Map();
/**
* @param {{}} options
*/
constructor(options = {}) {
this.options = options;
// favicons configuration options, see https://github.com/itgalaxy/favicons#usage
this.options.faviconsConfig = { ...config.defaults, ...(options.faviconOptions || {}) };
this.outputPath = this.options.faviconsConfig?.path ?? '';
}
apply(compiler) {
const bundlerPluginOption = PluginService.getPluginContext(compiler).pluginOption;
const enabled = bundlerPluginOption.toBool(this.options?.enabled, true, 'auto');
const isUrlPublicPath = bundlerPluginOption.isUrlPublicPath();
if (isUrlPublicPath) {
let publicPath = bundlerPluginOption.getPublicPath();
this.outputUrl = joinUrl(publicPath, this.outputPath);
}
if (!enabled) {
return;
}
const { pluginName } = this;
const { webpack } = compiler;
const { Compilation } = webpack;
this.fs = compiler.inputFileSystem.fileSystem;
this.webpack = webpack;
const { RawSource } = this.webpack.sources;
compiler.hooks.compilation.tap(pluginName, (compilation) => {
const hooks = BundlerPlugin.getHooks(compilation);
// get favicon source file
hooks.resolveSource.tap(pluginName, (source, info) => {
const { tag, attribute, resolvedFile, value, issuer } = info;
if (tag === 'link' && attribute === 'href' && source.includes('rel="icon"')) {
// use the first founded icon file as favicon file
if (!this.faviconFile) {
this.faviconFile = resolvedFile;
}
this.pages.set(issuer, {
faviconFile: resolvedFile,
faviconTag: source,
});
// disable processing in webpack, keep original value
return value;
}
});
// get favicons response
compilation.hooks.processAssets.tapPromise(
{
name: pluginName,
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
() => {
if (!this.faviconFile) {
return Promise.resolve();
}
return favicons(this.faviconFile, this.options.faviconsConfig).then((response) => {
response.html = response?.html.sort().join('\n');
this.faviconResponse = response;
// save favicon manifest files on disk
response.files.forEach(({ name, contents }) => {
let outputFile = path.posix.join(this.options.faviconsConfig.path, name);
compilation.emitAsset(outputFile, new RawSource(contents));
});
});
}
);
// injection generated favicon tags into html
// the usage example of the sync hook, sync hook must return a string
hooks.postprocess.tap(pluginName, (content, info) => {
const page = this.pages.get(info.sourceFile);
if (page && this.faviconResponse) {
let { outputUrl, outputPath } = this;
let faviconHtml = this.faviconResponse.html;
if (outputUrl) {
faviconHtml = faviconHtml.replaceAll(outputPath, outputUrl);
}
// inject generated <link> tags
content = content.replace(page.faviconTag, faviconHtml);
}
return content;
});
// injection generated favicon tags into html
// the usage example of the async hook, async hook must return a promise
// hooks.postprocess.tapPromise(pluginName, (content, info) => {
// const page = this.pages.get(info.sourceFile);
//
// if (page && this.faviconResponse) {
// content = content.replace(page.faviconTag, this.faviconResponse.html);
// }
//
// return Promise.resolve(content);
// });
});
// save favicon images on disk
compiler.hooks.afterEmit.tapPromise(
pluginName,
() =>
new Promise((resolve, reject) => {
if (this.faviconResponse?.images.length > 0) {
const { images } = this.faviconResponse;
const outputPath = bundlerPluginOption.getWebpackOutputPath();
const saveDir = path.join(outputPath, this.options.faviconsConfig.path);
return fs
.mkdir(saveDir, { recursive: true })
.then(() =>
Promise.all(
images.map(async (image) => await fs.writeFile(path.join(saveDir, image.name), image.contents))
).then(resolve)
);
} else {
this.warningFaviconNotFound();
resolve();
}
})
);
}
warningFaviconNotFound() {
const header = `\n${black.bgYellow` ${this.pluginName} `}${black.bg(227)` WARNING `} `;
let warning = `Favicon file is not found!
If the ${blueBright(
this.constructor.name
)} is used, at last one favicon source file should be defined in the template, e.g.:
${yellow`<link rel="icon" href="path/to/source/favicon.png">`}`;
outToConsole(header + warning);
}
}
module.exports = FaviconsBundlerPlugin;