1 | /**
|
---|
2 | * Copyright (c) 2015-present, Waysact Pty Ltd
|
---|
3 | *
|
---|
4 | * This source code is licensed under the MIT license found in the
|
---|
5 | * LICENSE file in the root directory of this source tree.
|
---|
6 | */
|
---|
7 |
|
---|
8 | var crypto = require('crypto');
|
---|
9 | var path = require('path');
|
---|
10 | var webpack = require('webpack');
|
---|
11 | // eslint-disable-next-line global-require
|
---|
12 | var ReplaceSource = (webpack.sources || require('webpack-sources')).ReplaceSource;
|
---|
13 | var util = require('./util');
|
---|
14 | var WebIntegrityJsonpMainTemplatePlugin = require('./jmtp');
|
---|
15 | var HtmlWebpackPlugin;
|
---|
16 |
|
---|
17 | // https://www.w3.org/TR/2016/REC-SRI-20160623/#cryptographic-hash-functions
|
---|
18 | var standardHashFuncNames = ['sha256', 'sha384', 'sha512'];
|
---|
19 |
|
---|
20 | try {
|
---|
21 | // eslint-disable-next-line global-require
|
---|
22 | HtmlWebpackPlugin = require('html-webpack-plugin');
|
---|
23 | } catch (e) {
|
---|
24 | if (!(e instanceof Error) || e.code !== 'MODULE_NOT_FOUND') {
|
---|
25 | throw e;
|
---|
26 | }
|
---|
27 | }
|
---|
28 |
|
---|
29 | function SubresourceIntegrityPlugin(options) {
|
---|
30 | var useOptions;
|
---|
31 | if (options === null || typeof options === 'undefined') {
|
---|
32 | useOptions = {};
|
---|
33 | } else if (typeof options === 'object') {
|
---|
34 | useOptions = options;
|
---|
35 | } else {
|
---|
36 | throw new Error('webpack-subresource-integrity: argument must be an object');
|
---|
37 | }
|
---|
38 |
|
---|
39 | this.options = {
|
---|
40 | enabled: true
|
---|
41 | };
|
---|
42 |
|
---|
43 | Object.assign(this.options, useOptions);
|
---|
44 |
|
---|
45 | this.emittedMessages = {};
|
---|
46 |
|
---|
47 | this.assetIntegrity = new Map();
|
---|
48 | }
|
---|
49 |
|
---|
50 | SubresourceIntegrityPlugin.prototype.emitMessage = function emitMessage(messages, message) {
|
---|
51 | messages.push(new Error('webpack-subresource-integrity: ' + message));
|
---|
52 | };
|
---|
53 |
|
---|
54 | SubresourceIntegrityPlugin.prototype.emitMessageOnce = function emitMessageOnce(messages, message) {
|
---|
55 | if (!this.emittedMessages[message]) {
|
---|
56 | this.emittedMessages[message] = true;
|
---|
57 | this.emitMessage(messages, message);
|
---|
58 | }
|
---|
59 | };
|
---|
60 |
|
---|
61 | SubresourceIntegrityPlugin.prototype.warnOnce = function warn(compilation, message) {
|
---|
62 | this.emitMessageOnce(compilation.warnings, message);
|
---|
63 | };
|
---|
64 |
|
---|
65 | SubresourceIntegrityPlugin.prototype.error = function error(compilation, message) {
|
---|
66 | this.emitMessage(compilation.errors, message);
|
---|
67 | };
|
---|
68 |
|
---|
69 | SubresourceIntegrityPlugin.prototype.errorOnce = function error(compilation, message) {
|
---|
70 | this.emitMessageOnce(compilation.errors, message);
|
---|
71 | };
|
---|
72 |
|
---|
73 | SubresourceIntegrityPlugin.prototype.validateOptions = function validateOptions(compilation) {
|
---|
74 | if (this.optionsValidated) {
|
---|
75 | return;
|
---|
76 | }
|
---|
77 | this.optionsValidated = true;
|
---|
78 |
|
---|
79 | if (this.options.enabled && !compilation.compiler.options.output.crossOriginLoading) {
|
---|
80 | this.warnOnce(
|
---|
81 | compilation,
|
---|
82 | 'SRI requires a cross-origin policy, defaulting to "anonymous". ' +
|
---|
83 | 'Set webpack option output.crossOriginLoading to a value other than false ' +
|
---|
84 | 'to make this warning go away. ' +
|
---|
85 | 'See https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data-leakage'
|
---|
86 | );
|
---|
87 | }
|
---|
88 | this.validateHashFuncNames(compilation);
|
---|
89 | };
|
---|
90 |
|
---|
91 | SubresourceIntegrityPlugin.prototype.validateHashFuncNames =
|
---|
92 | function validateHashFuncNames(compilation) {
|
---|
93 | if (!Array.isArray(this.options.hashFuncNames)) {
|
---|
94 | this.error(
|
---|
95 | compilation,
|
---|
96 | 'options.hashFuncNames must be an array of hash function names, ' +
|
---|
97 | 'instead got \'' + this.options.hashFuncNames + '\'.');
|
---|
98 | this.options.enabled = false;
|
---|
99 | } else if (
|
---|
100 | !this.options.hashFuncNames.every(this.validateHashFuncName.bind(this, compilation))
|
---|
101 | ) {
|
---|
102 | this.options.enabled = false;
|
---|
103 | } else {
|
---|
104 | this.warnStandardHashFunc(compilation);
|
---|
105 | }
|
---|
106 | };
|
---|
107 |
|
---|
108 | SubresourceIntegrityPlugin.prototype.warnStandardHashFunc =
|
---|
109 | function warnStandardHashFunc(compilation) {
|
---|
110 | var foundStandardHashFunc = false;
|
---|
111 | var i;
|
---|
112 | for (i = 0; i < this.options.hashFuncNames.length; i += 1) {
|
---|
113 | if (standardHashFuncNames.indexOf(this.options.hashFuncNames[i]) >= 0) {
|
---|
114 | foundStandardHashFunc = true;
|
---|
115 | }
|
---|
116 | }
|
---|
117 | if (!foundStandardHashFunc) {
|
---|
118 | this.warnOnce(
|
---|
119 | compilation,
|
---|
120 | 'It is recommended that at least one hash function is part of the set ' +
|
---|
121 | 'for which support is mandated by the specification. ' +
|
---|
122 | 'These are: ' + standardHashFuncNames.join(', ') + '. ' +
|
---|
123 | 'See http://www.w3.org/TR/SRI/#cryptographic-hash-functions for more information.');
|
---|
124 | }
|
---|
125 | };
|
---|
126 |
|
---|
127 | SubresourceIntegrityPlugin.prototype.validateHashFuncName =
|
---|
128 | function validateHashFuncName(compilation, hashFuncName) {
|
---|
129 | if (typeof hashFuncName !== 'string' &&
|
---|
130 | !(hashFuncName instanceof String)) {
|
---|
131 | this.error(
|
---|
132 | compilation,
|
---|
133 | 'options.hashFuncNames must be an array of hash function names, ' +
|
---|
134 | 'but contained ' + hashFuncName + '.');
|
---|
135 | return false;
|
---|
136 | }
|
---|
137 | try {
|
---|
138 | crypto.createHash(hashFuncName);
|
---|
139 | } catch (error) {
|
---|
140 | this.error(
|
---|
141 | compilation,
|
---|
142 | 'Cannot use hash function \'' + hashFuncName + '\': ' +
|
---|
143 | error.message);
|
---|
144 | return false;
|
---|
145 | }
|
---|
146 | return true;
|
---|
147 | };
|
---|
148 |
|
---|
149 | /* Given a public URL path to an asset, as generated by
|
---|
150 | * HtmlWebpackPlugin for use as a `<script src>` or `<link href`> URL
|
---|
151 | * in `index.html`, return the path to the asset, suitable as a key
|
---|
152 | * into `compilation.assets`.
|
---|
153 | */
|
---|
154 | SubresourceIntegrityPlugin.prototype.hwpAssetPath = function hwpAssetPath(src) {
|
---|
155 | return path.relative(this.hwpPublicPath, src);
|
---|
156 | };
|
---|
157 |
|
---|
158 | SubresourceIntegrityPlugin.prototype.warnIfHotUpdate = function warnIfHotUpdate(
|
---|
159 | compilation, source
|
---|
160 | ) {
|
---|
161 | if (source.indexOf('webpackHotUpdate') >= 0) {
|
---|
162 | this.warnOnce(
|
---|
163 | compilation,
|
---|
164 | 'webpack-subresource-integrity may interfere with hot reloading. ' +
|
---|
165 | 'Consider disabling this plugin in development mode.'
|
---|
166 | );
|
---|
167 | }
|
---|
168 | };
|
---|
169 |
|
---|
170 | SubresourceIntegrityPlugin.prototype.replaceAsset = function replaceAsset(
|
---|
171 | assets,
|
---|
172 | hashByChunkId,
|
---|
173 | chunkFile
|
---|
174 | ) {
|
---|
175 | var oldSource = assets[chunkFile].source();
|
---|
176 | var newAsset;
|
---|
177 | var magicMarker;
|
---|
178 | var magicMarkerPos;
|
---|
179 | var hashFuncNames = this.options.hashFuncNames;
|
---|
180 |
|
---|
181 | newAsset = new ReplaceSource(assets[chunkFile]);
|
---|
182 |
|
---|
183 | Array.from(hashByChunkId.entries()).forEach(function replaceMagicMarkers(idAndHash) {
|
---|
184 | magicMarker = util.makePlaceholder(hashFuncNames, idAndHash[0]);
|
---|
185 | magicMarkerPos = oldSource.indexOf(magicMarker);
|
---|
186 | if (magicMarkerPos >= 0) {
|
---|
187 | newAsset.replace(
|
---|
188 | magicMarkerPos,
|
---|
189 | (magicMarkerPos + magicMarker.length) - 1,
|
---|
190 | idAndHash[1]);
|
---|
191 | }
|
---|
192 | });
|
---|
193 |
|
---|
194 | // eslint-disable-next-line no-param-reassign
|
---|
195 | assets[chunkFile] = newAsset;
|
---|
196 |
|
---|
197 | newAsset.integrity = util.computeIntegrity(hashFuncNames, newAsset.source());
|
---|
198 | return newAsset;
|
---|
199 | };
|
---|
200 |
|
---|
201 | SubresourceIntegrityPlugin.prototype.processChunk = function processChunk(
|
---|
202 | chunk, compilation, assets
|
---|
203 | ) {
|
---|
204 | var self = this;
|
---|
205 | var newAsset;
|
---|
206 | var hashByChunkId = new Map();
|
---|
207 |
|
---|
208 | Array.from(util.findChunks(chunk)).reverse().forEach(childChunk => {
|
---|
209 | var sourcePath;
|
---|
210 | var files = Array.isArray(childChunk.files) ? new Set(childChunk.files) : childChunk.files;
|
---|
211 |
|
---|
212 | // This can happen with invalid Webpack configurations
|
---|
213 | if (files.size === 0) return;
|
---|
214 |
|
---|
215 | sourcePath = compilation.sriChunkAssets[childChunk.id];
|
---|
216 |
|
---|
217 | if (!files.has(sourcePath)) {
|
---|
218 | self.warnOnce(
|
---|
219 | compilation,
|
---|
220 | 'Cannot determine asset for chunk ' + childChunk.id + ', computed="' + sourcePath +
|
---|
221 | '", available=' + Array.from(childChunk.files)[0] + '. Please report this full error message ' +
|
---|
222 | 'along with your Webpack configuration at ' +
|
---|
223 | 'https://github.com/waysact/webpack-subresource-integrity/issues/new'
|
---|
224 | );
|
---|
225 | sourcePath = Array.from(files)[0];
|
---|
226 | }
|
---|
227 |
|
---|
228 | self.warnIfHotUpdate(compilation, assets[sourcePath].source());
|
---|
229 | newAsset = self.replaceAsset(assets, hashByChunkId, sourcePath);
|
---|
230 | hashByChunkId.set(childChunk.id, newAsset.integrity);
|
---|
231 | this.assetIntegrity.set(sourcePath, newAsset.integrity);
|
---|
232 | });
|
---|
233 | };
|
---|
234 |
|
---|
235 | SubresourceIntegrityPlugin.prototype.chunkAsset =
|
---|
236 | function chunkAsset(compilation, chunk, asset) {
|
---|
237 | if (compilation.assets[asset]) {
|
---|
238 | // eslint-disable-next-line no-param-reassign
|
---|
239 | compilation.sriChunkAssets[chunk.id] = asset;
|
---|
240 | }
|
---|
241 | };
|
---|
242 |
|
---|
243 | SubresourceIntegrityPlugin.prototype.statsFactory =
|
---|
244 | function stats(compilation, statsFactory) {
|
---|
245 | statsFactory.hooks.extract.for("asset").tap('SriPlugin', (object, asset) => {
|
---|
246 | if (this.assetIntegrity.has(asset.name)) {
|
---|
247 | // eslint-disable-next-line no-param-reassign
|
---|
248 | object.integrity = String(this.assetIntegrity.get(asset.name));
|
---|
249 | }
|
---|
250 | });
|
---|
251 | };
|
---|
252 |
|
---|
253 | SubresourceIntegrityPlugin.prototype.addMissingIntegrityHashes =
|
---|
254 | function addMissingIntegrityHashes(assets) {
|
---|
255 | var self = this;
|
---|
256 | Object.keys(assets).forEach(function loop(assetKey) {
|
---|
257 | var asset = assets[assetKey];
|
---|
258 | if (!asset.integrity) {
|
---|
259 | asset.integrity = util.computeIntegrity(self.options.hashFuncNames, asset.source());
|
---|
260 | }
|
---|
261 | });
|
---|
262 | };
|
---|
263 |
|
---|
264 | /*
|
---|
265 | * Calculate SRI values for each chunk and replace the magic
|
---|
266 | * placeholders by the actual values.
|
---|
267 | */
|
---|
268 | SubresourceIntegrityPlugin.prototype.afterOptimizeAssets =
|
---|
269 | function afterOptimizeAssets(compilation, assets) {
|
---|
270 | var self = this;
|
---|
271 |
|
---|
272 | Array.from(compilation.chunks).filter(util.isRuntimeChunk).forEach(function forEachChunk(chunk) {
|
---|
273 | self.processChunk(chunk, compilation, assets);
|
---|
274 | });
|
---|
275 |
|
---|
276 | this.addMissingIntegrityHashes(assets);
|
---|
277 | };
|
---|
278 |
|
---|
279 | SubresourceIntegrityPlugin.prototype.processTag =
|
---|
280 | function processTag(compilation, tag) {
|
---|
281 | var src = this.hwpAssetPath(util.getTagSrc(tag));
|
---|
282 | /* eslint-disable no-param-reassign */
|
---|
283 | var integrity = util.getIntegrityChecksumForAsset(compilation.assets, src);
|
---|
284 | if (!Object.prototype.hasOwnProperty.call(tag.attributes, "integrity")) {
|
---|
285 | tag.attributes.integrity = integrity;
|
---|
286 | tag.attributes.crossorigin = compilation.compiler.options.output.crossOriginLoading || 'anonymous';
|
---|
287 | }
|
---|
288 | /* eslint-enable no-param-reassign */
|
---|
289 | };
|
---|
290 |
|
---|
291 | SubresourceIntegrityPlugin.prototype.alterAssetTags =
|
---|
292 | function alterAssetTags(compilation, pluginArgs, callback) {
|
---|
293 | /* html-webpack-plugin has added an event so we can pre-process the html tags before they
|
---|
294 | inject them. This does the work.
|
---|
295 | */
|
---|
296 | var processTag = this.processTag.bind(this, compilation);
|
---|
297 | pluginArgs.head.filter(util.filterTag).forEach(processTag);
|
---|
298 | pluginArgs.body.filter(util.filterTag).forEach(processTag);
|
---|
299 | callback(null, pluginArgs);
|
---|
300 | };
|
---|
301 |
|
---|
302 |
|
---|
303 | /* Add jsIntegrity and cssIntegrity properties to pluginArgs, to
|
---|
304 | * go along with js and css properties. These are later
|
---|
305 | * accessible on `htmlWebpackPlugin.files`.
|
---|
306 | */
|
---|
307 | SubresourceIntegrityPlugin.prototype.beforeHtmlGeneration =
|
---|
308 | function beforeHtmlGeneration(compilation, pluginArgs, callback) {
|
---|
309 | var self = this;
|
---|
310 | this.hwpPublicPath = pluginArgs.assets.publicPath;
|
---|
311 | this.addMissingIntegrityHashes(compilation.assets);
|
---|
312 |
|
---|
313 | ['js', 'css'].forEach(function addIntegrity(fileType) {
|
---|
314 | // eslint-disable-next-line no-param-reassign
|
---|
315 | pluginArgs.assets[fileType + 'Integrity'] =
|
---|
316 | pluginArgs.assets[fileType].map(function assetIntegrity(filePath) {
|
---|
317 | return util.getIntegrityChecksumForAsset(compilation.assets, self.hwpAssetPath(filePath));
|
---|
318 | });
|
---|
319 | });
|
---|
320 | callback(null, pluginArgs);
|
---|
321 | };
|
---|
322 |
|
---|
323 | SubresourceIntegrityPlugin.prototype.registerJMTP = function registerJMTP(compilation) {
|
---|
324 | var plugin = new WebIntegrityJsonpMainTemplatePlugin(this, compilation);
|
---|
325 | if (plugin.apply) {
|
---|
326 | plugin.apply(compilation.mainTemplate);
|
---|
327 | } else {
|
---|
328 | compilation.mainTemplate.apply(plugin);
|
---|
329 | }
|
---|
330 | };
|
---|
331 |
|
---|
332 | SubresourceIntegrityPlugin.prototype.registerHwpHooks =
|
---|
333 | function registerHwpHooks(alterAssetTags, beforeHtmlGeneration, hwpCompilation) {
|
---|
334 | var self = this;
|
---|
335 | if (HtmlWebpackPlugin && HtmlWebpackPlugin.getHooks) {
|
---|
336 | // HtmlWebpackPlugin >= 4
|
---|
337 | HtmlWebpackPlugin.getHooks(hwpCompilation).beforeAssetTagGeneration.tapAsync(
|
---|
338 | 'sri',
|
---|
339 | this.beforeHtmlGeneration.bind(this, hwpCompilation)
|
---|
340 | );
|
---|
341 |
|
---|
342 | HtmlWebpackPlugin.getHooks(hwpCompilation).alterAssetTags.tapAsync(
|
---|
343 | 'sri',
|
---|
344 | function cb(data, callback) {
|
---|
345 | var processTag = self.processTag.bind(self, hwpCompilation);
|
---|
346 | data.assetTags.scripts.filter(util.filterTag).forEach(processTag);
|
---|
347 | data.assetTags.styles.filter(util.filterTag).forEach(processTag);
|
---|
348 | callback(null, data);
|
---|
349 | }
|
---|
350 | );
|
---|
351 | } else if (hwpCompilation.hooks.htmlWebpackPluginAlterAssetTags &&
|
---|
352 | hwpCompilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {
|
---|
353 | // HtmlWebpackPlugin 3
|
---|
354 | hwpCompilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('SriPlugin', alterAssetTags);
|
---|
355 | hwpCompilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync('SriPlugin', beforeHtmlGeneration);
|
---|
356 | }
|
---|
357 | };
|
---|
358 |
|
---|
359 | SubresourceIntegrityPlugin.prototype.thisCompilation = function thisCompilation(
|
---|
360 | compiler,
|
---|
361 | compilation
|
---|
362 | ) {
|
---|
363 | this.validateOptions(compilation);
|
---|
364 |
|
---|
365 | if (!this.options.enabled) {
|
---|
366 | return;
|
---|
367 | }
|
---|
368 |
|
---|
369 | this.registerJMTP(compilation);
|
---|
370 |
|
---|
371 | // FIXME: refactor into separate per-compilation state
|
---|
372 | // eslint-disable-next-line no-param-reassign
|
---|
373 | compilation.sriChunkAssets = {};
|
---|
374 |
|
---|
375 | /*
|
---|
376 | * html-webpack support:
|
---|
377 | * Modify the asset tags before webpack injects them for anything with an integrity value.
|
---|
378 | */
|
---|
379 | if (compiler.hooks) {
|
---|
380 | this.setupHooks(compiler, compilation);
|
---|
381 | } else {
|
---|
382 | this.setupLegacyHooks(compiler, compilation);
|
---|
383 | }
|
---|
384 | };
|
---|
385 |
|
---|
386 | SubresourceIntegrityPlugin.prototype.setupHooks = function setupHooks(
|
---|
387 | compiler,
|
---|
388 | compilation
|
---|
389 | ) {
|
---|
390 | var afterOptimizeAssets = this.afterOptimizeAssets.bind(this, compilation);
|
---|
391 | var beforeChunkAssets = this.beforeChunkAssets.bind(this, compilation);
|
---|
392 | var alterAssetTags = this.alterAssetTags.bind(this, compilation);
|
---|
393 | var beforeHtmlGeneration = this.beforeHtmlGeneration.bind(this, compilation);
|
---|
394 |
|
---|
395 | if (compilation.hooks.processAssets) {
|
---|
396 | compilation.hooks.processAssets.tap(
|
---|
397 | {
|
---|
398 | name: 'SriPlugin',
|
---|
399 | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH
|
---|
400 | },
|
---|
401 | afterOptimizeAssets
|
---|
402 | );
|
---|
403 | } else {
|
---|
404 | compilation.hooks.afterOptimizeAssets.tap('SriPlugin', afterOptimizeAssets);
|
---|
405 | }
|
---|
406 | compilation.hooks.beforeChunkAssets.tap('SriPlugin', beforeChunkAssets);
|
---|
407 | compiler.hooks.compilation.tap(
|
---|
408 | 'HtmlWebpackPluginHooks',
|
---|
409 | this.registerHwpHooks.bind(this, alterAssetTags, beforeHtmlGeneration)
|
---|
410 | );
|
---|
411 | };
|
---|
412 |
|
---|
413 | SubresourceIntegrityPlugin.prototype.setupLegacyHooks = function setupLegacyHooks(
|
---|
414 | compiler,
|
---|
415 | compilation
|
---|
416 | ) {
|
---|
417 | var afterOptimizeAssets = this.afterOptimizeAssets.bind(this, compilation);
|
---|
418 | var beforeChunkAssets = this.beforeChunkAssets.bind(this, compilation);
|
---|
419 | var alterAssetTags = this.alterAssetTags.bind(this, compilation);
|
---|
420 | var beforeHtmlGeneration = this.beforeHtmlGeneration.bind(this, compilation);
|
---|
421 |
|
---|
422 | compilation.plugin('after-optimize-assets', afterOptimizeAssets);
|
---|
423 | compilation.plugin('before-chunk-assets', beforeChunkAssets);
|
---|
424 | compilation.plugin('html-webpack-plugin-alter-asset-tags', alterAssetTags);
|
---|
425 | compilation.plugin(
|
---|
426 | 'html-webpack-plugin-before-html-generation',
|
---|
427 | beforeHtmlGeneration
|
---|
428 | );
|
---|
429 | };
|
---|
430 |
|
---|
431 | SubresourceIntegrityPlugin.prototype.beforeChunkAssets = function afterPlugins(compilation) {
|
---|
432 | var chunkAsset = this.chunkAsset.bind(this, compilation);
|
---|
433 | if (compilation.hooks) {
|
---|
434 | compilation.hooks.chunkAsset.tap('SriPlugin', chunkAsset);
|
---|
435 | } else {
|
---|
436 | compilation.plugin('chunk-asset', chunkAsset);
|
---|
437 | }
|
---|
438 | };
|
---|
439 |
|
---|
440 | SubresourceIntegrityPlugin.prototype.afterPlugins = function afterPlugins(compiler) {
|
---|
441 | if (compiler.hooks) {
|
---|
442 | compiler.hooks.thisCompilation.tap('SriPlugin', this.thisCompilation.bind(this, compiler));
|
---|
443 |
|
---|
444 | compiler.hooks.compilation.tap("DefaultStatsFactoryPlugin", compilation => {
|
---|
445 | if (compilation.hooks.statsFactory) {
|
---|
446 | compilation.hooks.statsFactory.tap('SriPlugin', this.statsFactory.bind(this, compilation));
|
---|
447 | }
|
---|
448 | });
|
---|
449 |
|
---|
450 | } else {
|
---|
451 | compiler.plugin('this-compilation', this.thisCompilation.bind(this, compiler));
|
---|
452 | }
|
---|
453 | };
|
---|
454 |
|
---|
455 | SubresourceIntegrityPlugin.prototype.apply = function apply(compiler) {
|
---|
456 | if (compiler.hooks) {
|
---|
457 | compiler.hooks.afterPlugins.tap('SriPlugin', this.afterPlugins.bind(this));
|
---|
458 | } else {
|
---|
459 | compiler.plugin('after-plugins', this.afterPlugins.bind(this));
|
---|
460 | }
|
---|
461 | };
|
---|
462 |
|
---|
463 | module.exports = SubresourceIntegrityPlugin;
|
---|