diff --git a/src/index.js b/src/index.js index 73c34458..1e3ab17f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,14 @@ import { validate } from 'schema-utils'; import schema from './plugin-options.json'; -import { shared, MODULE_TYPE, compareModulesByIdentifier } from './utils'; +import { + MODULE_TYPE, + compareModulesByIdentifier, + provideLoaderContext, +} from './utils'; -const pluginName = 'mini-css-extract-plugin'; +export const pluginName = 'mini-css-extract-plugin'; +export const pluginSymbol = Symbol(pluginName); const REGEXP_CHUNKHASH = /\[chunkhash(?::(\d+))?\]/i; const REGEXP_CONTENTHASH = /\[contenthash(?::(\d+))?\]/i; @@ -18,8 +23,23 @@ const CODE_GENERATION_RESULT = { runtimeRequirements: new Set(), }; +/** + * @type WeakMap + */ +const cssModuleCache = new WeakMap(); +/** + * @type WeakMap + */ +const cssDependencyCache = new WeakMap(); + class MiniCssExtractPlugin { static getCssModule(webpack) { + /** + * Prevent creation of multiple CssModule classes to allow other integrations to get the current CssModule. + */ + if (cssModuleCache.has(webpack)) { + return cssModuleCache.get(webpack); + } class CssModule extends webpack.Module { constructor({ context, @@ -134,6 +154,8 @@ class MiniCssExtractPlugin { } } + cssModuleCache.set(webpack, CssModule); + if ( webpack.util && webpack.util.serialization && @@ -181,6 +203,12 @@ class MiniCssExtractPlugin { } static getCssDependency(webpack) { + /** + * Prevent creation of multiple CssDependency classes to allow other integrations to get the current CssDependency. + */ + if (cssDependencyCache.has(webpack)) { + return cssDependencyCache.get(webpack); + } // eslint-disable-next-line no-shadow class CssDependency extends webpack.Dependency { constructor( @@ -231,6 +259,8 @@ class MiniCssExtractPlugin { } } + cssDependencyCache.set(webpack, CssDependency); + if ( webpack.util && webpack.util.serialization && @@ -356,16 +386,18 @@ class MiniCssExtractPlugin { } } - // initializeCssDependency - // eslint-disable-next-line no-shadow - const { CssModule, CssDependency } = shared(webpack, (webpack) => { - // eslint-disable-next-line no-shadow - const CssModule = MiniCssExtractPlugin.getCssModule(webpack); - // eslint-disable-next-line no-shadow - const CssDependency = MiniCssExtractPlugin.getCssDependency(webpack); - - return { CssModule, CssDependency }; - }); + const CssModule = MiniCssExtractPlugin.getCssModule(webpack); + const CssDependency = MiniCssExtractPlugin.getCssDependency(webpack); + + provideLoaderContext( + compiler, + `${pluginName} loader context`, + (loaderContext) => { + // eslint-disable-next-line no-param-reassign + loaderContext[pluginSymbol] = true; + }, + false + ); compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { class CssModuleFactory { @@ -1093,7 +1125,7 @@ class MiniCssExtractPlugin { return usedModules; } - modules = [...modules]; + const modulesList = [...modules]; const [chunkGroup] = chunk.groupsIterable; const moduleIndexFunctionName = @@ -1103,16 +1135,18 @@ class MiniCssExtractPlugin { if (typeof chunkGroup[moduleIndexFunctionName] === 'function') { // Store dependencies for modules - const moduleDependencies = new Map(modules.map((m) => [m, new Set()])); + const moduleDependencies = new Map( + modulesList.map((m) => [m, new Set()]) + ); const moduleDependenciesReasons = new Map( - modules.map((m) => [m, new Map()]) + modulesList.map((m) => [m, new Map()]) ); // Get ordered list of modules per chunk group // This loop also gathers dependencies from the ordered lists // Lists are in reverse order to allow to use Array.pop() const modulesByChunkGroup = Array.from(chunk.groupsIterable, (cg) => { - const sortedModules = modules + const sortedModules = modulesList .map((m) => { return { module: m, @@ -1145,7 +1179,7 @@ class MiniCssExtractPlugin { const unusedModulesFilter = (m) => !usedModules.has(m); - while (usedModules.size < modules.length) { + while (usedModules.size < modulesList.length) { let success = false; let bestMatch; let bestMatchDeps; @@ -1227,8 +1261,8 @@ class MiniCssExtractPlugin { // (to avoid a breaking change) // TODO remove this in next major version // and increase minimum webpack version to 4.12.0 - modules.sort((a, b) => a.index2 - b.index2); - usedModules = modules; + modulesList.sort((a, b) => a.index2 - b.index2); + usedModules = modulesList; } this._sortedModulesCache.set(chunk, usedModules); diff --git a/src/loader.js b/src/loader.js index a72366ac..686dfcff 100644 --- a/src/loader.js +++ b/src/loader.js @@ -3,10 +3,10 @@ import path from 'path'; import loaderUtils from 'loader-utils'; import { validate } from 'schema-utils'; -import { shared, findModuleById, evalModuleCode } from './utils'; +import { findModuleById, evalModuleCode, provideLoaderContext } from './utils'; import schema from './loader-options.json'; -const pluginName = 'mini-css-extract-plugin'; +import MiniCssExtractPlugin, { pluginName, pluginSymbol } from './index'; function hotLoader(content, context) { const accept = context.locals @@ -37,6 +37,12 @@ export function pitch(request) { baseDataPath: 'options', }); + if (!this[pluginSymbol]) { + throw new Error( + "You forgot to add 'mini-css-extract-plugin' plugin (i.e. `{ plugins: [new MiniCssExtractPlugin()] }`), please read https://github.com/webpack-contrib/mini-css-extract-plugin#getting-started" + ); + } + const loaders = this.loaders.slice(this.loaderIndex + 1); this.addDependency(this.resourcePath); @@ -108,33 +114,18 @@ export function pitch(request) { new LimitChunkCountPlugin({ maxChunks: 1 }).apply(childCompiler); - const NormalModule = webpack.NormalModule - ? webpack.NormalModule - : // eslint-disable-next-line global-require - require('webpack/lib/NormalModule'); - - childCompiler.hooks.thisCompilation.tap( - `${pluginName} loader`, - (compilation) => { - const normalModuleHook = - typeof NormalModule.getCompilationHooks !== 'undefined' - ? NormalModule.getCompilationHooks(compilation).loader - : compilation.hooks.normalModuleLoader; - - normalModuleHook.tap(`${pluginName} loader`, (loaderContext, module) => { - if (module.request === request) { - // eslint-disable-next-line no-param-reassign - module.loaders = loaders.map((loader) => { - return { - loader: loader.path, - options: loader.options, - ident: loader.ident, - }; - }); - } + provideLoaderContext(childCompiler, `${pluginName} loader`, (_, module) => { + if (module.request === request) { + // eslint-disable-next-line no-param-reassign + module.loaders = loaders.map((loader) => { + return { + loader: loader.path, + options: loader.options, + ident: loader.ident, + }; }); } - ); + }); let source; @@ -205,15 +196,7 @@ export function pitch(request) { } const count = identifierCountMap.get(dependency.identifier) || 0; - const { CssDependency } = shared(webpack, () => { - return {}; - }); - - if (!CssDependency) { - throw new Error( - "You forgot to add 'mini-css-extract-plugin' plugin (i.e. `{ plugins: [new MiniCssExtractPlugin()] }`), please read https://github.com/webpack-contrib/mini-css-extract-plugin#getting-started" - ); - } + const CssDependency = MiniCssExtractPlugin.getCssDependency(webpack); this._module.addDependency( (lastDep = new CssDependency(dependency, dependency.context, count)) diff --git a/src/utils.js b/src/utils.js index 98e5140e..ae0fc7ff 100644 --- a/src/utils.js +++ b/src/utils.js @@ -49,26 +49,30 @@ function compareModulesByIdentifier(a, b) { return compareIds(a.identifier(), b.identifier()); } -const initializeCache = new WeakMap(); - -function shared(webpack, initializer) { - const cacheEntry = initializeCache.get(webpack); - - // eslint-disable-next-line no-undefined - if (cacheEntry !== undefined) { - return cacheEntry; - } - - const constructors = initializer(webpack); - const result = { ...constructors }; - - initializeCache.set(webpack, result); - - return result; +function provideLoaderContext(compiler, name, handler, thisCompilation = true) { + const NormalModule = + compiler.webpack && compiler.webpack.NormalModule + ? compiler.webpack.NormalModule + : // eslint-disable-next-line global-require + require('webpack/lib/NormalModule'); + + compiler.hooks[thisCompilation ? 'thisCompilation' : 'compilation'].tap( + name, + (compilation) => { + const normalModuleHook = + typeof NormalModule.getCompilationHooks !== 'undefined' + ? NormalModule.getCompilationHooks(compilation).loader + : compilation.hooks.normalModuleLoader; + + normalModuleHook.tap(name, (loaderContext, module) => + handler(loaderContext, module, compilation) + ); + } + ); } export { - shared, + provideLoaderContext, MODULE_TYPE, findModuleById, evalModuleCode, diff --git a/test/api.test.js b/test/api.test.js new file mode 100644 index 00000000..a6e79a6c --- /dev/null +++ b/test/api.test.js @@ -0,0 +1,17 @@ +import webpack from 'webpack'; + +import MiniCssExtractPlugin from '../src'; + +describe('API', () => { + it('should return the same CssModule when same webpack instance provided', () => { + expect(MiniCssExtractPlugin.getCssModule(webpack)).toEqual( + MiniCssExtractPlugin.getCssModule(webpack) + ); + }); + + it('should return the same CssDependency when same webpack instance provided', () => { + expect(MiniCssExtractPlugin.getCssDependency(webpack)).toEqual( + MiniCssExtractPlugin.getCssDependency(webpack) + ); + }); +});