Skip to content

Instantly share code, notes, and snippets.

@ninetails
Last active March 28, 2018 12:27
Show Gist options
  • Save ninetails/86a22f9f258cdc2704592686f534f329 to your computer and use it in GitHub Desktop.
Save ninetails/86a22f9f258cdc2704592686f534f329 to your computer and use it in GitHub Desktop.
resource-hint-webpack-plugin
'use strict';
const assert = require('assert');
const objectAssign = require('object-assign');
const minimatch = require('minimatch');
const path = require('path');
const preloadDirective = {
'.js': 'script',
'.css': 'style',
'.woff': 'font',
'.woff2': 'font',
'.jpeg': 'image',
'.jpg': 'image',
'.gif': 'image',
'.png': 'image',
'.svg': 'image'
};
// By default all files are prefetched and preload
const defaultFilter = ['**/*.*'];
class ResourceHintWebpackPlugin {
constructor (options) {
assert.equal(options, undefined, 'The ResourceHintWebpackPlugin does not accept any options');
}
apply (compiler) {
// Hook into the html-webpack-plugin processing
if (compiler.hooks) {
// Webpack 4+ Plugin System
compiler.hooks.compilation.tap('ResourceHintWebpackPlugin', compilation => {
if (compilation.hooks.htmlWebpackPluginAlterAssetTags) {
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('ResourceHintWebpackPluginAlterAssetTags',
resourceHintWebpackPluginAlterAssetTags
);
}
});
} else {
// Webpack 1-3 Plugin System
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-alter-asset-tags',
resourceHintWebpackPluginAlterAssetTags
);
});
}
}
}
/**
* The main processing function
*/
function resourceHintWebpackPluginAlterAssetTags (htmlPluginData, callback) {
const htmlWebpackPluginOptions = htmlPluginData.plugin.options;
const pluginData = objectAssign({}, htmlPluginData);
const tags = {
prefetch: [],
// https://w3c.github.io/preload/#link-type-preload
preload: []
};
// Create Resource tags
Object.keys(tags).forEach(resourceHintType => {
// Check if it is disabled for the current htmlWebpackPlugin instance:
// e.g.
// new HtmlWebpackPlugin({
// prefetch: false
// })
if (htmlWebpackPluginOptions[resourceHintType] === false) {
return;
}
// If no options are found all files are prefetched / preload
const fileFilters = htmlWebpackPluginOptions[resourceHintType]
? [].concat(htmlWebpackPluginOptions[resourceHintType])
: defaultFilter;
// Process every filter
fileFilters.forEach(filter => {
if (filter.indexOf('*') !== -1) {
Array.prototype.push.apply(tags[resourceHintType], addResourceHintTags(
resourceHintType,
filter,
pluginData.body,
htmlWebpackPluginOptions
));
} else {
tags[resourceHintType].push(createResourceHintTag(filter, resourceHintType, htmlWebpackPluginOptions));
}
});
});
// Add all Resource tags to the head
Array.prototype.push.apply(pluginData.head, tags.preload.map(addPreloadType));
Array.prototype.push.apply(pluginData.head, tags.prefetch);
callback(null, pluginData);
}
/**
* Adds Resource hint tags
*/
function addResourceHintTags (resourceHintType, filter, assetTags, htmlWebpackPluginOptions) {
const urls = assetTags
.map(tag => tag.attributes.src || tag.attributes.href)
.filter(url => url)
.filter(minimatch.filter(filter));
// Add a ResourceHint for every match
return urls.map(url => createResourceHintTag(url, resourceHintType, htmlWebpackPluginOptions));
}
function createResourceHintTag (url, resourceHintType, htmlWebpackPluginOptions) {
return {
tagName: 'link',
selfClosingTag: !!htmlWebpackPluginOptions.xhtml,
attributes: {
rel: resourceHintType,
href: url
}
};
}
/**
* The as attribute's value must be a valid request destination.
* If the provided value is omitted, the value is initialized to the empty string.
*
* @see https://w3c.github.io/preload/#link-element-interface-extensions
* @param {[type]} tag [description]
*/
function addPreloadType (tag) {
const ext = path.extname(tag.attributes.href);
if (preloadDirective[ext]) {
tag.attributes.as = preloadDirective[ext];
}
return tag;
}
module.exports = ResourceHintWebpackPlugin;
/* eslint-env jasmine */
const fs = require('fs');
const path = require('path');
const MemoryFileSystem = require('memory-fs');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlResourceHintPlugin = require('../');
const OUTPUT_DIR = path.join(__dirname, '../dist');
describe('HtmlResourceHintPlugin', () => {
it('adds prefetch tags by default', (done) => {
const expected = fs.readFileSync(path.resolve(__dirname, 'fixtures/expected.html')).toString();
const compiler = webpack({
entry: {
main: path.join(__dirname, 'fixtures', 'entry.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin(),
new HtmlResourceHintPlugin()
]
}, (err, result) => {
expect(err).toBeFalsy();
expect(JSON.stringify(result.compilation.errors)).toBe('[]');
const html = result.compilation.assets['index.html'].source();
expect(html).toBe(expected);
done();
});
compiler.outputFileSystem = new MemoryFileSystem();
});
});
describe('HtmlResourceHintPlugin', () => {
it('adds prefetch tags', (done) => {
const expected = fs.readFileSync(path.resolve(__dirname, 'fixtures/expected.html')).toString();
const compiler = webpack({
entry: {
main: path.join(__dirname, 'fixtures', 'entry.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
prefetch: '*.js',
preload: '*.js'
}),
new HtmlResourceHintPlugin()
]
}, (err, result) => {
expect(err).toBeFalsy();
expect(JSON.stringify(result.compilation.errors)).toBe('[]');
const html = result.compilation.assets['index.html'].source();
expect(html).toBe(expected);
done();
});
compiler.outputFileSystem = new MemoryFileSystem();
});
});
describe('HtmlResourceHintPlugin', () => {
it('adds no file which do not match the filter', (done) => {
const compiler = webpack({
entry: {
main: path.join(__dirname, 'fixtures', 'entry.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
prefetch: '*.json',
preload: false
}),
new HtmlResourceHintPlugin()
]
}, (err, result) => {
expect(err).toBeFalsy();
expect(JSON.stringify(result.compilation.errors)).toBe('[]');
const html = result.compilation.assets['index.html'].source();
expect(html.indexOf('rel="prefetch"') === -1).toBe(true);
expect(html.indexOf('rel="preload"') === -1).toBe(true);
done();
});
compiler.outputFileSystem = new MemoryFileSystem();
});
});
describe('HtmlResourceHintPlugin', () => {
it('allows to add fixed prefetch url', (done) => {
const compiler = webpack({
entry: {
main: path.join(__dirname, 'fixtures', 'entry.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
prefetch: ['demo.json']
}),
new HtmlResourceHintPlugin()
]
}, (err, result) => {
expect(err).toBeFalsy();
expect(JSON.stringify(result.compilation.errors)).toBe('[]');
const html = result.compilation.assets['index.html'].source();
expect(!!html.indexOf('<link rel="prefetch" href="demo.json">')).toBe(true);
done();
});
compiler.outputFileSystem = new MemoryFileSystem();
});
});
describe('HtmlResourceHintPlugin', () => {
it('allows to add fixed preload url', (done) => {
const compiler = webpack({
entry: {
main: path.join(__dirname, 'fixtures', 'entry.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
preload: ['*.js', 'demo.json']
}),
new HtmlResourceHintPlugin()
]
}, (err, result) => {
expect(err).toBeFalsy();
expect(JSON.stringify(result.compilation.errors)).toBe('[]');
const html = result.compilation.assets['index.html'].source();
expect(!!html.indexOf('<link rel="preload" href="demo.json">')).toBe(true);
done();
});
compiler.outputFileSystem = new MemoryFileSystem();
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment