diff --git a/package-lock.json b/package-lock.json index 8af4e953..1e5066dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13319,9 +13319,8 @@ }, "tapable": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true + "resolved": "https://registry.npm.taobao.org/tapable/download/tapable-1.1.3.tgz", + "integrity": "sha1-ofzMBrWNth/XpF2i2kT186Pme6I=" }, "term-size": { "version": "1.2.0", diff --git a/package.json b/package.json index e82590f2..f51acb3a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "loader-utils": "^1.1.0", "normalize-url": "1.9.1", "schema-utils": "^1.0.0", + "tapable": "^1.1.3", "webpack-sources": "^1.1.0" }, "devDependencies": { diff --git a/src/index.js b/src/index.js index d9f6bf2d..85f8e23f 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ const { Template, util: { createHash }, } = webpack; +const { SyncWaterfallHook } = require('tapable'); const MODULE_TYPE = 'css/mini-extract'; @@ -129,6 +130,11 @@ class MiniCssExtractPlugin { apply(compiler) { compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { + // eslint-disable-next-line no-param-reassign + compilation.hooks.miniCssExtractPluginBeforeLinkAppend = new SyncWaterfallHook( + ['source', 'chunk', 'hash'] + ); + compilation.dependencyFactories.set( CssDependency, new CssModuleFactory() @@ -377,6 +383,11 @@ class MiniCssExtractPlugin { ]) : '', 'var head = document.getElementsByTagName("head")[0];', + compilation.hooks.miniCssExtractPluginBeforeLinkAppend.call( + source, + chunk, + hash + ), 'head.appendChild(linkTag);', ]), '}).then(function() {', diff --git a/test/BeforeLinkAppendHook.test.js b/test/BeforeLinkAppendHook.test.js new file mode 100644 index 00000000..8c2720cd --- /dev/null +++ b/test/BeforeLinkAppendHook.test.js @@ -0,0 +1,49 @@ +import MiniCssExtractPlugin from '../src'; + +import { getCompiler, compile } from './helpers'; + +it('It should be able to inject successfully', async (done) => { + class RewriteErrorHandler { + // eslint-disable-next-line class-methods-use-this + apply(compiler) { + compiler.hooks.thisCompilation.tap( + 'RewriteErrorHandler', + (compilation) => { + compilation.hooks.miniCssExtractPluginBeforeLinkAppend.tap( + 'RewriteErrorHandler', + (source, chunk, hash) => { + expect(source).toBeDefined(); + expect(chunk).toBeDefined(); + expect(hash).toBeDefined(); + return 'head.onerror = console.error;'; + } + ); + } + ); + } + } + + const compiler = getCompiler( + './esmAsync.js', + { esModule: true }, + { + mode: 'production', + optimization: { minimize: false }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: '[id].css', + }), + new RewriteErrorHandler(), + ], + } + ); + + const stats = await compile(compiler); + const content = stats.compilation.assets['main.bundle.js'].source(); + + expect(stats.hasErrors()).toBe(false); + expect(content).toMatchSnapshot(); + + done(); +}); diff --git a/test/__snapshots__/BeforeLinkAppendHook.test.js.snap b/test/__snapshots__/BeforeLinkAppendHook.test.js.snap new file mode 100644 index 00000000..09cb1008 --- /dev/null +++ b/test/__snapshots__/BeforeLinkAppendHook.test.js.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`It should be able to inject successfully 1`] = ` +"/******/ (function(modules) { // webpackBootstrap +/******/ // install a JSONP callback for chunk loading +/******/ function webpackJsonpCallback(data) { +/******/ var chunkIds = data[0]; +/******/ var moreModules = data[1]; +/******/ +/******/ +/******/ // add \\"moreModules\\" to the modules object, +/******/ // then flag all \\"chunkIds\\" as loaded and fire callback +/******/ var moduleId, chunkId, i = 0, resolves = []; +/******/ for(;i < chunkIds.length; i++) { +/******/ chunkId = chunkIds[i]; +/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { +/******/ resolves.push(installedChunks[chunkId][0]); +/******/ } +/******/ installedChunks[chunkId] = 0; +/******/ } +/******/ for(moduleId in moreModules) { +/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { +/******/ modules[moduleId] = moreModules[moduleId]; +/******/ } +/******/ } +/******/ if(parentJsonpFunction) parentJsonpFunction(data); +/******/ +/******/ while(resolves.length) { +/******/ resolves.shift()(); +/******/ } +/******/ +/******/ }; +/******/ +/******/ +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // object to store loaded CSS chunks +/******/ var installedCssChunks = { +/******/ 0: 0 +/******/ } +/******/ +/******/ // object to store loaded and loading chunks +/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched +/******/ // Promise = chunk loading, 0 = chunk loaded +/******/ var installedChunks = { +/******/ 0: 0 +/******/ }; +/******/ +/******/ +/******/ +/******/ // script path function +/******/ function jsonpScriptSrc(chunkId) { +/******/ return __webpack_require__.p + \\"\\" + ({}[chunkId]||chunkId) + \\".chunk.js\\" +/******/ } +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ // This file contains only the entry chunk. +/******/ // The chunk loading function for additional chunks +/******/ __webpack_require__.e = function requireEnsure(chunkId) { +/******/ var promises = []; +/******/ +/******/ +/******/ // mini-css-extract-plugin CSS loading +/******/ var cssChunks = {\\"1\\":1}; +/******/ if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]); +/******/ else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) { +/******/ promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) { +/******/ var href = \\"\\" + chunkId + \\".css\\"; +/******/ var fullhref = __webpack_require__.p + href; +/******/ var existingLinkTags = document.getElementsByTagName(\\"link\\"); +/******/ for(var i = 0; i < existingLinkTags.length; i++) { +/******/ var tag = existingLinkTags[i]; +/******/ var dataHref = tag.getAttribute(\\"data-href\\") || tag.getAttribute(\\"href\\"); +/******/ if(tag.rel === \\"stylesheet\\" && (dataHref === href || dataHref === fullhref)) return resolve(); +/******/ } +/******/ var existingStyleTags = document.getElementsByTagName(\\"style\\"); +/******/ for(var i = 0; i < existingStyleTags.length; i++) { +/******/ var tag = existingStyleTags[i]; +/******/ var dataHref = tag.getAttribute(\\"data-href\\"); +/******/ if(dataHref === href || dataHref === fullhref) return resolve(); +/******/ } +/******/ var linkTag = document.createElement(\\"link\\"); +/******/ linkTag.rel = \\"stylesheet\\"; +/******/ linkTag.type = \\"text/css\\"; +/******/ linkTag.onload = resolve; +/******/ linkTag.onerror = function(event) { +/******/ var request = event && event.target && event.target.src || fullhref; +/******/ var err = new Error(\\"Loading CSS chunk \\" + chunkId + \\" failed.\\\\n(\\" + request + \\")\\"); +/******/ err.code = \\"CSS_CHUNK_LOAD_FAILED\\"; +/******/ err.request = request; +/******/ delete installedCssChunks[chunkId] +/******/ linkTag.parentNode.removeChild(linkTag) +/******/ reject(err); +/******/ }; +/******/ linkTag.href = fullhref; +/******/ +/******/ var head = document.getElementsByTagName(\\"head\\")[0]; +/******/ head.onerror = console.error; +/******/ head.appendChild(linkTag); +/******/ }).then(function() { +/******/ installedCssChunks[chunkId] = 0; +/******/ })); +/******/ } +/******/ +/******/ // JSONP chunk loading for javascript +/******/ +/******/ var installedChunkData = installedChunks[chunkId]; +/******/ if(installedChunkData !== 0) { // 0 means \\"already installed\\". +/******/ +/******/ // a Promise means \\"currently loading\\". +/******/ if(installedChunkData) { +/******/ promises.push(installedChunkData[2]); +/******/ } else { +/******/ // setup Promise in chunk cache +/******/ var promise = new Promise(function(resolve, reject) { +/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject]; +/******/ }); +/******/ promises.push(installedChunkData[2] = promise); +/******/ +/******/ // start chunk loading +/******/ var script = document.createElement('script'); +/******/ var onScriptComplete; +/******/ +/******/ script.charset = 'utf-8'; +/******/ script.timeout = 120; +/******/ if (__webpack_require__.nc) { +/******/ script.setAttribute(\\"nonce\\", __webpack_require__.nc); +/******/ } +/******/ script.src = jsonpScriptSrc(chunkId); +/******/ +/******/ // create error before stack unwound to get useful stacktrace later +/******/ var error = new Error(); +/******/ onScriptComplete = function (event) { +/******/ // avoid mem leaks in IE. +/******/ script.onerror = script.onload = null; +/******/ clearTimeout(timeout); +/******/ var chunk = installedChunks[chunkId]; +/******/ if(chunk !== 0) { +/******/ if(chunk) { +/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type); +/******/ var realSrc = event && event.target && event.target.src; +/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\\\\n(' + errorType + ': ' + realSrc + ')'; +/******/ error.name = 'ChunkLoadError'; +/******/ error.type = errorType; +/******/ error.request = realSrc; +/******/ chunk[1](error); +/******/ } +/******/ installedChunks[chunkId] = undefined; +/******/ } +/******/ }; +/******/ var timeout = setTimeout(function(){ +/******/ onScriptComplete({ type: 'timeout', target: script }); +/******/ }, 120000); +/******/ script.onerror = script.onload = onScriptComplete; +/******/ document.head.appendChild(script); +/******/ } +/******/ } +/******/ return Promise.all(promises); +/******/ }; +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = \\"\\"; +/******/ +/******/ // on error function for async loading +/******/ __webpack_require__.oe = function(err) { console.error(err); throw err; }; +/******/ +/******/ var jsonpArray = window[\\"webpackJsonp\\"] = window[\\"webpackJsonp\\"] || []; +/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); +/******/ jsonpArray.push = webpackJsonpCallback; +/******/ jsonpArray = jsonpArray.slice(); +/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); +/******/ var parentJsonpFunction = oldJsonpFunction; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)); + + +/***/ }) +/******/ ]);" +`; diff --git a/test/fixtures/esmAsync.js b/test/fixtures/esmAsync.js new file mode 100644 index 00000000..a536a608 --- /dev/null +++ b/test/fixtures/esmAsync.js @@ -0,0 +1 @@ +import("./simple.css");