diff --git a/lib/Compilation.js b/lib/Compilation.js index 07f195ba4b6..aa0621c8a51 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -289,10 +289,122 @@ class Compilation { */ constructor(compiler) { const getNormalModuleLoader = () => deprecatedNormalModuleLoaderHook(this); - /** @type {AsyncSeriesHook<[CompilationAssets]>} */ + /** @typedef {{ additionalAssets?: true | Function }} ProcessAssetsAdditionalOptions */ + /** @type {AsyncSeriesHook<[CompilationAssets], ProcessAssetsAdditionalOptions>} */ const processAssetsHook = new AsyncSeriesHook(["assets"]); + + let savedAssets = new Set(); + const popNewAssets = assets => { + let newAssets = undefined; + for (const file of Object.keys(assets)) { + if (savedAssets.has(file)) continue; + if (newAssets === undefined) { + newAssets = Object.create(null); + } + newAssets[file] = assets[file]; + savedAssets.add(file); + } + return newAssets; + }; + processAssetsHook.intercept({ + name: "Compilation", + call: () => { + savedAssets.clear(); + }, + register: tap => { + const { type, name } = tap; + const { fn, additionalAssets, ...remainingTap } = tap; + const additionalAssetsFn = + additionalAssets === true ? fn : additionalAssets; + let processedAssets = undefined; + switch (type) { + case "sync": + if (additionalAssetsFn) { + this.hooks.processAdditionalAssets.tap(name, assets => { + if (processedAssets === this.assets) additionalAssetsFn(assets); + }); + } + return { + ...remainingTap, + type: "async", + fn: (assets, callback) => { + try { + fn(assets); + } catch (e) { + return callback(e); + } + processedAssets = this.assets; + const newAssets = popNewAssets(assets); + if (newAssets !== undefined) { + this.hooks.processAdditionalAssets.callAsync( + newAssets, + callback + ); + return; + } + callback(); + } + }; + case "async": + if (additionalAssetsFn) { + this.hooks.processAdditionalAssets.tapAsync( + name, + (assets, callback) => { + if (processedAssets === this.assets) + return additionalAssetsFn(assets, callback); + callback(); + } + ); + } + return { + ...remainingTap, + fn: (assets, callback) => { + fn(assets, err => { + if (err) return callback(err); + processedAssets = this.assets; + const newAssets = popNewAssets(assets); + if (newAssets !== undefined) { + this.hooks.processAdditionalAssets.callAsync( + newAssets, + callback + ); + return; + } + callback(); + }); + } + }; + case "promise": + if (additionalAssetsFn) { + this.hooks.processAdditionalAssets.tapPromise(name, assets => { + if (processedAssets === this.assets) + return additionalAssetsFn(assets); + return Promise.resolve(); + }); + } + return { + ...remainingTap, + fn: assets => { + const p = fn(assets); + if (!p || !p.then) return p; + return p.then(() => { + processedAssets = this.assets; + const newAssets = popNewAssets(assets); + if (newAssets !== undefined) { + return this.hooks.processAdditionalAssets.promise( + newAssets + ); + } + }); + } + }; + } + } + }); + /** @type {SyncHook<[CompilationAssets]>} */ const afterProcessAssetsHook = new SyncHook(["assets"]); + /** * @template T * @param {string} name name of the hook @@ -544,6 +656,8 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si processAssets: processAssetsHook, afterProcessAssets: afterProcessAssetsHook, + /** @type {AsyncSeriesHook<[CompilationAssets]>} */ + processAdditionalAssets: new AsyncSeriesHook(["assets"]), /** @type {SyncBailHook<[], boolean>} */ needAdditionalSeal: new SyncBailHook([]), @@ -3439,6 +3553,8 @@ Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE = 100; /** * Optimize the count of existing assets, e. g. by merging them. + * Only assets of the same type should be merged. + * For assets of different types see PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE. */ Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_COUNT = 200; @@ -3453,16 +3569,22 @@ Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_COMPATIBILITY = 300; Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE = 400; /** - * Summarize the list of existing assets. - * When creating new assets from this they should be fully optimized. - * e. g. creating an assets manifest of Service Workers. + * Add development tooling to assets, e. g. by extracting a SourceMap. */ -Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE = 1000; +Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING = 500; /** - * Add development tooling to assets, e. g. by extracting a SourceMap. + * Optimize the count of existing assets, e. g. by inlining assets of into other assets. + * Only assets of different types should be inlined. + * For assets of the same type see PROCESS_ASSETS_STAGE_OPTIMIZE_COUNT. */ -Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING = 2000; +Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE = 700; + +/** + * Summarize the list of existing assets + * e. g. creating an assets manifest of Service Workers. + */ +Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE = 1000; /** * Optimize the hashes of the assets, e. g. by generating real hashes of the asset content. diff --git a/lib/Compiler.js b/lib/Compiler.js index 51e7546f405..934f6e0a238 100644 --- a/lib/Compiler.js +++ b/lib/Compiler.js @@ -152,9 +152,9 @@ class Compiler { beforeCompile: new AsyncSeriesHook(["params"]), /** @type {SyncHook<[CompilationParams]>} */ compile: new SyncHook(["params"]), - /** @type {AsyncParallelHook<[Compilation], Module>} */ + /** @type {AsyncParallelHook<[Compilation]>} */ make: new AsyncParallelHook(["compilation"]), - /** @type {AsyncParallelHook<[Compilation], Module>} */ + /** @type {AsyncParallelHook<[Compilation]>} */ finishMake: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<[Compilation]>} */ afterCompile: new AsyncSeriesHook(["compilation"]), diff --git a/lib/SourceMapDevToolPlugin.js b/lib/SourceMapDevToolPlugin.js index f22ef0101fb..6562ecc2626 100644 --- a/lib/SourceMapDevToolPlugin.js +++ b/lib/SourceMapDevToolPlugin.js @@ -162,7 +162,8 @@ class SourceMapDevToolPlugin { compilation.hooks.processAssets.tapAsync( { name: "SourceMapDevToolPlugin", - stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING + stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING, + additionalAssets: true }, (assets, callback) => { const chunkGraph = compilation.chunkGraph; diff --git a/lib/index.js b/lib/index.js index cf9b4d6760d..dabb0abfcbe 100644 --- a/lib/index.js +++ b/lib/index.js @@ -355,6 +355,9 @@ module.exports = mergeExports(fn, { get ModuleConcatenationPlugin() { return require("./optimize/ModuleConcatenationPlugin"); }, + get RealContentHashPlugin() { + return require("./optimize/RealContentHashPlugin"); + }, get RuntimeChunkPlugin() { return require("./optimize/RuntimeChunkPlugin"); }, diff --git a/lib/optimize/RealContentHashPlugin.js b/lib/optimize/RealContentHashPlugin.js index 28d48162faf..5f9af527d6b 100644 --- a/lib/optimize/RealContentHashPlugin.js +++ b/lib/optimize/RealContentHashPlugin.js @@ -5,12 +5,15 @@ "use strict"; +const { SyncBailHook } = require("tapable"); const { RawSource, CachedSource, CompatSource } = require("webpack-sources"); const Compilation = require("../Compilation"); const WebpackError = require("../WebpackError"); const { compareSelect, compareStrings } = require("../util/comparators"); const createHash = require("../util/createHash"); +/** @typedef {import("webpack-sources").Source} Source */ +/** @typedef {import("../Compilation").AssetInfo} AssetInfo */ /** @typedef {import("../Compiler")} Compiler */ const EMPTY_SET = new Set(); @@ -47,7 +50,50 @@ const toCachedSource = source => { return newSource; }; +/** + * @typedef {Object} AssetInfoForRealContentHash + * @property {string} name + * @property {AssetInfo} info + * @property {Source} source + * @property {RawSource | undefined} newSource + * @property {RawSource | undefined} newSourceWithoutOwn + * @property {string} content + * @property {Set} ownHashes + * @property {Promise} contentComputePromise + * @property {Promise} contentComputeWithoutOwnPromise + * @property {Set} referencedHashes + * @property {Set} hashes + */ + +/** + * @typedef {Object} CompilationHooks + * @property {SyncBailHook<[Buffer[], string], string>} updateHash + */ + +/** @type {WeakMap} */ +const compilationHooksMap = new WeakMap(); + class RealContentHashPlugin { + /** + * @param {Compilation} compilation the compilation + * @returns {CompilationHooks} the attached hooks + */ + static getCompilationHooks(compilation) { + if (!(compilation instanceof Compilation)) { + throw new TypeError( + "The 'compilation' argument must be an instance of Compilation" + ); + } + let hooks = compilationHooksMap.get(compilation); + if (hooks === undefined) { + hooks = { + updateHash: new SyncBailHook(["content", "oldHash"]) + }; + compilationHooksMap.set(compilation, hooks); + } + return hooks; + } + constructor({ hashFunction, hashDigest }) { this._hashFunction = hashFunction; this._hashDigest = hashDigest; @@ -66,6 +112,7 @@ class RealContentHashPlugin { const cacheGenerate = compilation.getCache( "RealContentHashPlugin|generate" ); + const hooks = RealContentHashPlugin.getCompilationHooks(compilation); compilation.hooks.processAssets.tapPromise( { name: "RealContentHashPlugin", @@ -73,6 +120,7 @@ class RealContentHashPlugin { }, async () => { const assets = compilation.getAssets(); + /** @type {AssetInfoForRealContentHash[]} */ const assetsWithInfo = []; const hashToAssets = new Map(); for (const { source, info, name } of assets) { @@ -87,9 +135,13 @@ class RealContentHashPlugin { source: cachedSource, /** @type {RawSource | undefined} */ newSource: undefined, + /** @type {RawSource | undefined} */ + newSourceWithoutOwn: undefined, content, - hasOwnHash: false, - contentComputePromise: false, + /** @type {Set} */ + ownHashes: undefined, + contentComputePromise: undefined, + contentComputeWithoutOwnPromise: undefined, /** @type {Set} */ referencedHashes: undefined, hashes @@ -114,6 +166,7 @@ class RealContentHashPlugin { const { name, source, content, hashes } = asset; if (Buffer.isBuffer(content)) { asset.referencedHashes = EMPTY_SET; + asset.ownHashes = EMPTY_SET; return; } const etag = cacheAnalyse.mergeEtags( @@ -122,21 +175,21 @@ class RealContentHashPlugin { ); [ asset.referencedHashes, - asset.hasOwnHash + asset.ownHashes ] = await cacheAnalyse.providePromise(name, etag, () => { const referencedHashes = new Set(); - let hasOwnHash = false; + let ownHashes = new Set(); const inContent = content.match(hashRegExp); if (inContent) { for (const hash of inContent) { if (hashes.has(hash)) { - hasOwnHash = true; + ownHashes.add(hash); continue; } referencedHashes.add(hash); } } - return [referencedHashes, hasOwnHash]; + return [referencedHashes, ownHashes]; }); }) ); @@ -163,7 +216,12 @@ ${referencingAssets return undefined; } const hashes = new Set(); - for (const { referencedHashes } of assets) { + for (const { referencedHashes, ownHashes } of assets) { + if (!ownHashes.has(hash)) { + for (const hash of ownHashes) { + hashes.add(hash); + } + } for (const hash of referencedHashes) { hashes.add(hash); } @@ -199,32 +257,57 @@ ${referencingAssets add(hash, new Set()); } const hashToNewHash = new Map(); - const computeNewContent = (asset, includeOwn) => { + const getEtag = asset => + cacheGenerate.mergeEtags( + cacheGenerate.getLazyHashedEtag(asset.source), + Array.from(asset.referencedHashes, hash => + hashToNewHash.get(hash) + ).join("|") + ); + const computeNewContent = asset => { if (asset.contentComputePromise) return asset.contentComputePromise; return (asset.contentComputePromise = (async () => { if ( - asset.hasOwnHash || + asset.ownHashes.size > 0 || Array.from(asset.referencedHashes).some( hash => hashToNewHash.get(hash) !== hash ) ) { - const identifier = - asset.name + - (includeOwn && asset.hasOwnHash ? "|with-own" : ""); - const etag = cacheGenerate.mergeEtags( - cacheGenerate.getLazyHashedEtag(asset.source), - Array.from(asset.referencedHashes, hash => - hashToNewHash.get(hash) - ).join("|") - ); + const identifier = asset.name; + const etag = getEtag(asset); asset.newSource = await cacheGenerate.providePromise( + identifier, + etag, + () => { + const newContent = asset.content.replace(hashRegExp, hash => + hashToNewHash.get(hash) + ); + return new RawSource(newContent); + } + ); + } + })()); + }; + const computeNewContentWithoutOwn = asset => { + if (asset.contentComputeWithoutOwnPromise) + return asset.contentComputeWithoutOwnPromise; + return (asset.contentComputeWithoutOwnPromise = (async () => { + if ( + asset.ownHashes.size > 0 || + Array.from(asset.referencedHashes).some( + hash => hashToNewHash.get(hash) !== hash + ) + ) { + const identifier = asset.name + "|without-own"; + const etag = getEtag(asset); + asset.newSourceWithoutOwn = await cacheGenerate.providePromise( identifier, etag, () => { const newContent = asset.content.replace( hashRegExp, hash => { - if (!includeOwn && asset.hashes.has(hash)) { + if (asset.ownHashes.has(hash)) { return ""; } return hashToNewHash.get(hash); @@ -241,25 +324,37 @@ ${referencingAssets const assets = hashToAssets.get(oldHash); assets.sort(comparator); const hash = createHash(this._hashFunction); - await Promise.all(assets.map(computeNewContent)); - for (const asset of assets) { - hash.update( - asset.newSource + await Promise.all( + assets.map(asset => + asset.ownHashes.has(oldHash) + ? computeNewContentWithoutOwn(asset) + : computeNewContent(asset) + ) + ); + const assetsContent = assets.map(asset => { + if (asset.ownHashes.has(oldHash)) { + return asset.newSourceWithoutOwn + ? asset.newSourceWithoutOwn.buffer() + : asset.source.buffer(); + } else { + return asset.newSource ? asset.newSource.buffer() - : asset.source.buffer() - ); + : asset.source.buffer(); + } + }); + let newHash = hooks.updateHash.call(assetsContent, oldHash); + if (!newHash) { + for (const content of assetsContent) { + hash.update(content); + } + const digest = hash.digest(this._hashDigest); + newHash = /** @type {string} */ (digest.slice(0, oldHash.length)); } - const digest = hash.digest(this._hashDigest); - const newHash = digest.slice(0, oldHash.length); hashToNewHash.set(oldHash, newHash); } await Promise.all( assetsWithInfo.map(async asset => { - // recomputed content with it's own hash - if (asset.hasOwnHash) { - asset.contentComputePromise = undefined; - } - await computeNewContent(asset, true); + await computeNewContent(asset); const newName = asset.name.replace(hashRegExp, hash => hashToNewHash.get(hash) ); diff --git a/package.json b/package.json index 37710b8bd55..8a189fc87f6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "neo-async": "^2.6.2", "pkg-dir": "^4.2.0", "schema-utils": "^3.0.0", - "tapable": "^2.0.0", + "tapable": "^2.1.1", "terser-webpack-plugin": "^5.0.3", "watchpack": "^2.0.0", "webpack-sources": "^2.1.1" diff --git a/test/configCases/process-assets/html-plugin/index.js b/test/configCases/process-assets/html-plugin/index.js new file mode 100644 index 00000000000..057cb6a0576 --- /dev/null +++ b/test/configCases/process-assets/html-plugin/index.js @@ -0,0 +1,28 @@ +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); + +it("should result in the correct HTML", () => { + const content = fs.readFileSync( + path.resolve(__dirname, "index.html"), + "utf-8" + ); + + // check minimized + expect(content).toMatch(/<\/script> `; + }; + compilation.emitAsset( + "index.html", + new OriginalSource( + ` + +${files.map(file => ` ${toScriptTag(file)}`).join("\n")} + +`, + "index.html" + ) + ); + } + ); + }); + } +} + +class HtmlInlinePlugin { + constructor(inline) { + this.inline = inline; + } + + apply(compiler) { + compiler.hooks.compilation.tap("html-inline-plugin", compilation => { + compilation.hooks.processAssets.tap( + { + name: "html-inline-plugin", + stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE, + additionalAssets: true + }, + assets => { + const publicPath = compilation.outputOptions.publicPath; + for (const name of Object.keys(assets)) { + if (/\.html$/.test(name)) { + const asset = compilation.getAsset(name); + const content = asset.source.source(); + const matches = []; + const regExp = /<\/script>/g; + let match = regExp.exec(content); + while (match) { + let url = match[1]; + if (url.startsWith(publicPath)) + url = url.slice(publicPath.length); + if (this.inline.test(url)) { + const asset = compilation.getAsset(url); + matches.push({ + start: match.index, + length: match[0].length, + asset + }); + } + match = regExp.exec(content); + } + if (matches.length > 0) { + const newSource = new ReplaceSource(asset.source, name); + for (const { start, length, asset } of matches) { + newSource.replace( + start, + start + length - 1, + `` + ); + } + compilation.updateAsset(name, newSource); + } + } + } + } + ); + }); + } +} + +class SriHashSupportPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("sri-hash-support-plugin", compilation => { + RealContentHashPlugin.getCompilationHooks(compilation).updateHash.tap( + "sri-hash-support-plugin", + (input, oldHash) => { + if (/^sha512-.{88}$/.test(oldHash) && input.length === 1) { + const hash = createHash("sha512"); + hash.update(input[0]); + return `sha512-${hash.digest("base64")}`; + } + } + ); + }); + } +} + +class HtmlMinimizePlugin { + apply(compiler) { + compiler.hooks.compilation.tap("html-minimize-plugin", compilation => { + compilation.hooks.processAssets.tap( + { + name: "html-minimize-plugin", + stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, + additionalAssets: true + }, + assets => { + for (const name of Object.keys(assets)) { + if (/\.html$/.test(name)) { + compilation.updateAsset( + name, + source => new RawSource(source.source().replace(/\s+/g, " ")), + assetInfo => ({ + ...assetInfo, + minimized: true + }) + ); + } + } + } + ); + }); + } +} + +/** @type {import("../../../../").Configuration} */ +module.exports = { + mode: "production", + entry: { + test: { import: "./index.js", filename: "test.js" }, + inline: "./inline.js", + normal: "./normal.js" + }, + output: { + filename: "[name]-[contenthash].js" + }, + optimization: { + minimize: true, + minimizer: ["...", new HtmlMinimizePlugin()] + }, + node: { + __dirname: false, + __filename: false + }, + plugins: [ + new HtmlPlugin(["inline", "normal"]), + new HtmlInlinePlugin(/inline/), + new SriHashSupportPlugin() + ] +}; diff --git a/types.d.ts b/types.d.ts index b620a48eb29..d231ad59e86 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1229,10 +1229,17 @@ declare class Compilation { "tap" | "tapAsync" | "tapPromise" | "name" > & FakeHookMarker; - optimizeAssets: AsyncSeriesHook<[Record]>; + optimizeAssets: AsyncSeriesHook< + [Record], + { additionalAssets?: true | Function } + >; afterOptimizeAssets: SyncHook<[Record]>; - processAssets: AsyncSeriesHook<[Record]>; + processAssets: AsyncSeriesHook< + [Record], + { additionalAssets?: true | Function } + >; afterProcessAssets: SyncHook<[Record]>; + processAdditionalAssets: AsyncSeriesHook<[Record]>; needAdditionalSeal: SyncBailHook<[], boolean>; afterSeal: AsyncSeriesHook<[]>; renderManifest: SyncWaterfallHook< @@ -1536,6 +1543,8 @@ declare class Compilation { /** * Optimize the count of existing assets, e. g. by merging them. + * Only assets of the same type should be merged. + * For assets of different types see PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE. */ static PROCESS_ASSETS_STAGE_OPTIMIZE_COUNT: number; @@ -1550,16 +1559,22 @@ declare class Compilation { static PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE: number; /** - * Summarize the list of existing assets. - * When creating new assets from this they should be fully optimized. - * e. g. creating an assets manifest of Service Workers. + * Add development tooling to assets, e. g. by extracting a SourceMap. */ - static PROCESS_ASSETS_STAGE_SUMMARIZE: number; + static PROCESS_ASSETS_STAGE_DEV_TOOLING: number; /** - * Add development tooling to assets, e. g. by extracting a SourceMap. + * Optimize the count of existing assets, e. g. by inlining assets of into other assets. + * Only assets of different types should be inlined. + * For assets of the same type see PROCESS_ASSETS_STAGE_OPTIMIZE_COUNT. */ - static PROCESS_ASSETS_STAGE_DEV_TOOLING: number; + static PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE: number; + + /** + * Summarize the list of existing assets + * e. g. creating an assets manifest of Service Workers. + */ + static PROCESS_ASSETS_STAGE_SUMMARIZE: number; /** * Optimize the hashes of the assets, e. g. by generating real hashes of the asset content. @@ -1597,6 +1612,9 @@ declare interface CompilationHooksJavascriptModulesPlugin { chunkHash: SyncHook<[Chunk, Hash, ChunkHashContext]>; useSourceMap: SyncBailHook<[Chunk, RenderContextObject], boolean>; } +declare interface CompilationHooksRealContentHashPlugin { + updateHash: SyncBailHook<[Buffer[], string], string>; +} declare interface CompilationParams { normalModuleFactory: NormalModuleFactory; contextModuleFactory: ContextModuleFactory; @@ -5665,7 +5683,7 @@ declare abstract class NormalModuleFactory extends ModuleFactory { beforeResolve: AsyncSeriesBailHook<[ResolveData], any>; afterResolve: AsyncSeriesBailHook<[ResolveData], any>; createModule: AsyncSeriesBailHook<[any, ResolveData], any>; - module: SyncWaterfallHook<[Module, any, ResolveData]>; + module: SyncWaterfallHook<[Module, any, ResolveData], any>; createParser: HookMap>; parser: HookMap>; createGenerator: HookMap>; @@ -6888,6 +6906,17 @@ declare class ReadFileCompileWasmPlugin { */ apply(compiler: Compiler): void; } +declare class RealContentHashPlugin { + constructor(__0: { hashFunction: any; hashDigest: any }); + + /** + * Apply the plugin + */ + apply(compiler: Compiler): void; + static getCompilationHooks( + compilation: Compilation + ): CompilationHooksRealContentHashPlugin; +} declare interface RealDependencyLocation { start: SourcePosition; end?: SourcePosition; @@ -10220,6 +10249,7 @@ declare namespace exports { LimitChunkCountPlugin, MinChunkSizePlugin, ModuleConcatenationPlugin, + RealContentHashPlugin, RuntimeChunkPlugin, SideEffectsFlagPlugin, SplitChunksPlugin diff --git a/yarn.lock b/yarn.lock index b6b0dd915c0..e5551551c61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6221,10 +6221,10 @@ tapable@^1.0.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tapable@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.0.0.tgz#a49c3d6a8a2bb606e7db372b82904c970d537a08" - integrity sha512-bjzn0C0RWoffnNdTzNi7rNDhs1Zlwk2tRXgk8EiHKAOX1Mag3d6T0Y5zNa7l9CJ+EoUne/0UHdwS8tMbkh9zDg== +tapable@^2.0.0, tapable@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.1.1.tgz#b01cc1902d42a7bb30514e320ce21c456f72fd3f" + integrity sha512-Wib1S8m2wdpLbmQz0RBEVosIyvb/ykfKXf3ZIDqvWoMg/zTNm6G/tDSuUM61J1kNCDXWJrLHGSFeMhAG+gAGpQ== temp-dir@^1.0.0: version "1.0.0"