/** * @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, 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 }) } 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)) ) } compiler.hooks.compilation.tap( this.constructor.name, compilation => { compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tap( this.constructor.name, (htmlPluginData) => { if (skip(htmlPluginData)) { return } this.generateLinks(compilation, htmlPluginData) } ) compilation.hooks.htmlWebpackPluginAlterAssetTags.tap( this.constructor.name, (htmlPluginData) => { if (skip(htmlPluginData)) { return } if (this.resourceHints) { htmlPluginData.head = [ ...this.resourceHints, ...htmlPluginData.head ] } return htmlPluginData } ) } ) } } module.exports = PreloadPlugin