UNPKG

html-bundler-webpack-plugin

Version:

Generates complete single-page or multi-page website from source assets. Build-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.

233 lines (196 loc) 6.56 kB
const path = require('path'); const { detectIndent, getFileExtension } = require('../Common/Helpers'); const { optionPreloadAsException } = require('./Messages/Exception'); /** * Media types that differ from file extension. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types */ const mimeType = { audio: { oga: 'audio/ogg', mp3: 'audio/mpeg', weba: 'audio/webm', }, video: { ogv: 'video/ogg', webm: 'video/webm', }, image: { svg: 'image/svg+xml', jpg: 'image/jpeg', tif: 'image/tiff', }, font: { svg: 'image/svg+xml', }, }; /** * The valid type values of content for 'as' attribute. * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link * * @type {Array<string>} */ const contentType = [ 'audio', 'document', 'embed', 'font', 'image', 'object', 'script', 'style', 'track', 'video', 'worker', ]; /** * Content types whose 'type' attribute can be omitted. * * @type {Set<string>} */ const optionalTypeBy = new Set(['script', 'style']); class Preload { pluginOption = null; constructor(pluginOption) { this.pluginOption = pluginOption; } /** * * @param {AssetEntryOptions} entry The entry where is specified the resource. * @param {FileInfo|null} issuer The issuer file info. * @param {string} assetFile The asset output filename. * @return {string} */ getPreloadFile(entry, issuer, assetFile) { if (issuer && issuer.resource !== entry.resource) { // recovery preload output file of an asset relative by entry point const issuerDir = path.dirname(issuer.filename); const webRootPath = path.posix.join(issuerDir, assetFile); assetFile = this.pluginOption.getAssetOutputFile(webRootPath, entry.filename); } return assetFile; } /** * Generates and injects preload tags in the head for all matching source files resolved in templates and styles. * * @param {string} content The template content. * @param {string} entryAsset The output filename of template. * @param {Map} collection The reference to `Collection data`. * @throws */ insertPreloadAssets(content, entryAsset, collection) { const data = collection.get(entryAsset); if (!data) return; const options = this.pluginOption.getPreload(); if (!options || !content) return; const insertPos = this.#findInsertPos(content); if (insertPos < 0) { // TODO: show warning - the template must contain the <head></head> tag to inject preloading assets. return; } const preloadAssets = new Map(); const LF = this.pluginOption.getLF(); const indent = LF + detectIndent(content, insertPos - 1); const groupBy = {}; // normalize preload attributes and create sorted groups in the order of the specified 'preload' options for (const conf of options) { const as = conf.as || conf.attributes?.as; // note: `as` property is a required valid value if (!as || contentType.indexOf(as) < 0) { optionPreloadAsException(conf, as, contentType); } // determine the order of the attributes in the link tag const attrs = { rel: 'preload', href: undefined, as, type: undefined, ...(conf.attributes || {}) }; // override attributes with the main properties if (conf.rel) attrs.rel = conf.rel; if (conf.type) attrs.type = conf.type; // whether the 'type' property exist regardless of a value; // if the property exists and have the undefined value, exclude this attribute in generating preload tag const hasType = 'type' in conf || (conf.attributes && 'type' in conf.attributes) || optionalTypeBy.has(attrs.as); // save normalized attributes conf._opts = { attrs, hasType }; groupBy[as] = []; } // prepare a flat array with preload assets for (let item of data.assets) { if (item.inline) continue; const assets = item.assets; const conf = options.find(({ test }) => test.test(item.resource)); if (conf) { if (Array.isArray(item.chunks)) { // js for (let { chunkFile, assetFile } of item.chunks) { preloadAssets.set(assetFile, conf._opts); } } else { // css preloadAssets.set(item.assetFile, conf._opts); } } // assets in css if (Array.isArray(assets)) { for (const assetItem of assets) { if (assetItem.inline) continue; const conf = options.find(({ test }) => test.test(assetItem.resource)); if (!conf) continue; let preloadFile = assetItem.issuer ? this.getPreloadFile(data.entry, assetItem.issuer, assetItem.assetFile) : assetItem.assetFile; preloadAssets.set(preloadFile, conf._opts); } } } // save generated preload tags for verbose data.preloads = []; for (const [filename, opts] of preloadAssets.entries()) { const attrs = { ...opts.attrs }; attrs.href = filename; if (!opts.hasType) { const ext = getFileExtension(filename); attrs.type = mimeType[attrs.as]?.[ext] || attrs.as + '/' + ext; } let tag = `<link`; for (const [key, value] of Object.entries(attrs)) { if (value != null) tag += ` ${key}="${value}"`; } tag += '>'; data.preloads.push({ filename, type: attrs.as || attrs.type, tag, }); groupBy[attrs.as].push(tag); } // inject preload tags into the template const tags = Object.values(groupBy).flat(); if (tags.length) { const str = tags.join(indent) + indent; return content.slice(0, insertPos) + str + content.slice(insertPos); } } /** * Find start position in the content to insert generated tags. * * @param {string} content * @return {number} Returns the position to insert place or -1 if the head tag not found. */ #findInsertPos(content) { let headStartPos = content.indexOf('<head'); if (headStartPos < 0) { return -1; } let headEndPos = content.indexOf('</head>', headStartPos); if (headEndPos < 0) { return -1; } let startPos = content.indexOf('<link', headStartPos); if (startPos < 0 || startPos > headEndPos) { startPos = content.indexOf('<script', headStartPos); } if (startPos < 0 || startPos > headEndPos) { startPos = headEndPos; } return startPos; } } module.exports = Preload;