UNPKG

preload-webpack-plugin-stzhang

Version:

A webpack plugin for injecting <link rel='preload|prefecth'> into HtmlWebpackPlugin pages, with async chunk support

191 lines (177 loc) 6.82 kB
/** * @license * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const defaultOptions = require('./lib/default-options') const determineAsValue = require('./lib/determine-as-value') const doesChunkBelongToHTML = require('./lib/does-chunk-belong-to-html') const extractChunks = require('./lib/extract-chunks') class PreloadPlugin { constructor (options) { this.options = Object.assign({}, defaultOptions, options) } generateLinks (compilation, htmlPluginData) { const options = this.options const extractedChunks = extractChunks({ compilation, optionsInclude: options.include }) const htmlChunks = options.include === 'allAssets' // Handle all chunks. ? extractedChunks // Only handle chunks imported by this HtmlWebpackPlugin. : extractedChunks.filter((chunk) => doesChunkBelongToHTML({ chunk, compilation, htmlAssetsChunks: Object.values(htmlPluginData.assets.chunks) })) // Flatten the list of files. const allFiles = htmlChunks.reduce((accumulated, chunk) => { return accumulated.concat(chunk.files) }, []) const uniqueFiles = new Set(allFiles) const filteredFiles = [...uniqueFiles].filter(file => { return ( !this.options.fileWhitelist || this.options.fileWhitelist.some(regex => regex.test(file)) ) }).filter(file => { return ( !this.options.fileBlacklist || this.options.fileBlacklist.every(regex => !regex.test(file)) ) }) // Sort to ensure the output is predictable. const sortedFilteredFiles = filteredFiles.sort() const links = [] const publicPath = compilation.outputOptions.publicPath || '' for (const file of sortedFilteredFiles) { const href = `${publicPath}${file}` const attributes = { href, rel: options.rel } // If we're preloading this resource (as opposed to prefetching), // then we need to set the 'as' attribute correctly. if (options.rel === 'preload') { attributes.as = determineAsValue({ href, file, optionsAs: options.as }) // On the off chance that we have a cross-origin 'href' attribute, // set crossOrigin on the <link> to trigger CORS mode. Non-CORS // fonts can't be used. if (attributes.as === 'font') { attributes.crossorigin = '' } } links.push({ tagName: 'link', attributes, voidTag: true }) } this.resourceHints = links return htmlPluginData } apply (compiler) { const skip = data => { const htmlFilename = data.plugin.options.filename const exclude = this.options.excludeHtmlNames const include = this.options.includeHtmlNames return ( (include && !(include.includes(htmlFilename))) || (exclude && exclude.includes(htmlFilename)) ) } const appendHash = (url, hash) => { if (!url) { return url } return url + (url.indexOf('?') === -1 ? '?' : '&') + hash } const buildReducer = (compilation, htmlPluginData) => { const publicPath = compilation.outputOptions.publicPath || '' return (chunks_, chunk) => { // Prepend the public path to all chunk files let chunkFiles = chunk.files.map(chunkFile => publicPath + chunkFile) if (chunkFiles.some(file => ~htmlPluginData.assets.js.indexOf(file) || ~htmlPluginData.assets.css.indexOf(file))) { const chunkName = chunk.names[0] || chunk.idHints[0] chunks_[chunkName] = {} // Append a hash for cache busting if (htmlPluginData.plugin.options.hash) { chunkFiles = chunkFiles.map(chunkFile => appendHash(chunkFile, compilation.hash)) } // Webpack outputs an array for each chunk when using sourcemaps // or when one chunk hosts js and css simultaneously const js = chunkFiles.find(chunkFile => /.js($|\?)/.test(chunkFile)) if (js) { chunks_[chunkName].size = chunk.size chunks_[chunkName].entry = js chunks_[chunkName].hash = chunk.hash } // Gather all css files const css = chunkFiles.filter(chunkFile => /.css($|\?)/.test(chunkFile)) chunks_[chunkName].css = css } return chunks_ } } compiler.hooks.compilation.tap(this.constructor.name, compilation => { let beforeHook, afterHook if (typeof compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing === 'object') { beforeHook = compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing afterHook = compilation.hooks.htmlWebpackPluginAlterAssetTags } else { const HtmlWebpackPlugin = require('html-webpack-plugin') const hooks = HtmlWebpackPlugin.getHooks(compilation) beforeHook = hooks.beforeAssetTagGeneration afterHook = hooks.alterAssetTagGroups } beforeHook.tap(this.constructor.name, htmlPluginData => { if (htmlPluginData.plugin.version >= 4) { const { chunks } = compilation.getStats().toJson({ chunks: true }) htmlPluginData.assets.chunks = Object.assign(htmlPluginData.assets.chunks || {}, chunks.reduce(buildReducer(compilation, htmlPluginData), {})) } if (skip(htmlPluginData)) { return } this.generateLinks(compilation, htmlPluginData) }) afterHook.tap(this.constructor.name, htmlPluginData => { if (skip(htmlPluginData)) { return } if (this.resourceHints) { if (htmlPluginData.plugin.version >= 4) { htmlPluginData.headTags = [ ...this.resourceHints, ...htmlPluginData.headTags ] } else { htmlPluginData.head = [ ...this.resourceHints, ...htmlPluginData.head ] } } return htmlPluginData }) } ) } } module.exports = PreloadPlugin