diff --git a/package-lock.json b/package-lock.json index 9581f2a3..4c1f1f27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,7 +2,6 @@ "name": "extract-text-webpack-plugin", "version": "3.0.2", "lockfileVersion": 1, - "requires": true, "dependencies": { "JSONStream": { "version": "1.3.2", diff --git a/package.json b/package.json index 4b1e3d29..253364f1 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "start": "npm run build -- -w", "appveyor:test": "npm run test", - "build": "cross-env NODE_ENV=production babel src -d dist --ignore 'src/**/*.test.js'", + "build": "cross-env NODE_ENV=production babel src -d dist --ignore 'src/**/*.test.js' --copy-files", "build:example": "(cd example && webpack)", "clean": "del-cli dist", "lint": "eslint --cache src test", diff --git a/src/index.js b/src/index.js index 15c7e3e8..687dc9e3 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ import { mergeOptions, isString, isFunction, + cloneModule, } from './lib/helpers'; const NS = path.dirname(fs.realpathSync(__filename)); @@ -26,7 +27,7 @@ class ExtractTextPlugin { if (isString(options)) { options = { filename: options }; } else { - validateOptions(path.resolve(__dirname, '../schema/plugin.json'), options, 'Extract Text Plugin'); + validateOptions(path.resolve(__dirname, './plugin.json'), options, 'Extract Text Plugin'); } this.filename = options.filename; this.id = options.id != null ? options.id : ++nextId; @@ -88,7 +89,7 @@ class ExtractTextPlugin { if (Array.isArray(options) || isString(options) || typeof options.options === 'object' || typeof options.query === 'object') { options = { use: options }; } else { - validateOptions(path.resolve(__dirname, '../schema/loader.json'), options, 'Extract Text Plugin (Loader)'); + validateOptions(path.resolve(__dirname, './loader.json'), options, 'Extract Text Plugin (Loader)'); } let loader = options.use; let before = options.fallback || []; @@ -115,7 +116,9 @@ class ExtractTextPlugin { compilation.plugin('normal-module-loader', (loaderContext, module) => { loaderContext[NS] = (content, opt) => { if (options.disable) { return false; } - if (!Array.isArray(content) && content != null) { throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(content)}`); } + if (!Array.isArray(content) && content != null) { + throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(content)}`); + } module[NS] = { content, options: opt || {}, @@ -126,8 +129,12 @@ class ExtractTextPlugin { const filename = this.filename; const id = this.id; let extractedChunks; + let toRemoveModules; + compilation.plugin('optimize-tree', (chunks, modules, callback) => { extractedChunks = chunks.map(() => new Chunk()); + toRemoveModules = []; + chunks.forEach((chunk, i) => { const extractedChunk = extractedChunks[i]; extractedChunk.index = i; @@ -161,24 +168,40 @@ class ExtractTextPlugin { } } if (shouldExtract || (!module.extracted && !wasExtracted)) { - module[`${NS}/extract`] = true; // eslint-disable-line no-path-concat - compilation.rebuildModule(module, (err) => { + const newModule = cloneModule(module); + newModule[`${NS}/extract`] = shouldExtract; // eslint-disable-line no-path-concat + compilation.buildModule(newModule, false, newModule, null, (err) => { if (err) { compilation.errors.push(err); return callback(); } - meta = module[NS]; + meta = newModule[NS]; + + const identifier = module.identifier(); // Error out if content is not an array and is not null if (!Array.isArray(meta.content) && meta.content != null) { - err = new Error(`${module.identifier()} doesn't export content`); + err = new Error(`${identifier} doesn't export content`); compilation.errors.push(err); return callback(); } - if (meta.content) { extractCompilation.addResultToChunk(module.identifier(), meta.content, module, extractedChunk); } + if (meta.content) { + extractCompilation.addResultToChunk(identifier, meta.content, module, extractedChunk); + if (toRemoveModules[identifier]) { + toRemoveModules[identifier].chunks.push(chunk); + } else { + toRemoveModules[identifier] = { + module: newModule, + moduleToRemove: module, + chunks: [chunk], + }; + } + } callback(); }); } else { - if (meta.content) { extractCompilation.addResultToChunk(module.identifier(), meta.content, module, extractedChunk); } + if (meta.content) { + extractCompilation.addResultToChunk(module.identifier(), meta.content, module, extractedChunk); + } callback(); } } else callback(); @@ -202,6 +225,30 @@ class ExtractTextPlugin { callback(); }); }); + + compilation.plugin('optimize-module-ids', (modules) => { + modules.forEach((module) => { + const data = toRemoveModules[module.identifier()]; + + if (data) { + const oldModuleId = module.id; + const newModule = cloneModule(module); + newModule.id = oldModuleId; + newModule._source = data.module._source; // eslint-disable-line no-underscore-dangle + data.chunks.forEach((chunk) => { + chunk.removeModule(data.moduleToRemove); + const deps = data.moduleToRemove.dependencies; + deps.forEach((d) => { + if (d.module && d.module.loaders.length > 0) { + chunk.removeModule(d.module); + } + }); + chunk.addModule(newModule); + }); + } + }); + }); + compilation.plugin('additional-assets', (callback) => { extractedChunks.forEach((extractedChunk) => { if (extractedChunk.getNumberOfModules()) { diff --git a/src/lib/helpers.js b/src/lib/helpers.js index 2fbe3d61..54f76ea1 100644 --- a/src/lib/helpers.js +++ b/src/lib/helpers.js @@ -1,3 +1,5 @@ +import NormalModule from 'webpack/lib/NormalModule'; + export function isInitialOrHasNoParents(chunk) { return chunk.isInitial() || chunk.parents.length === 0; } @@ -28,6 +30,17 @@ export function getOrder(a, b) { return 0; } +export function cloneModule(module) { + return new NormalModule( + module.request, + module.userRequest, + module.rawRequest, + module.loaders, + module.resource, + module.parser, + ); +} + export function getLoaderObject(loader) { if (isString(loader)) { return { loader }; diff --git a/schema/loader.json b/src/loader.json similarity index 100% rename from schema/loader.json rename to src/loader.json diff --git a/schema/plugin.json b/src/plugin.json similarity index 100% rename from schema/plugin.json rename to src/plugin.json diff --git a/test/__snapshots__/webpack-integration.test.js.snap b/test/__snapshots__/webpack-integration.test.js.snap index b18a0245..e3446c0c 100644 --- a/test/__snapshots__/webpack-integration.test.js.snap +++ b/test/__snapshots__/webpack-integration.test.js.snap @@ -90,6 +90,232 @@ c " `; +exports[`Webpack Integration Tests multiple-entries-all-async 1`] = ` +"webpackJsonp([0],{ + +/***/ 1: +/***/ (function(module, exports, __webpack_require__) { + +__webpack_require__(7); + +modules.export = function() { + return 'Route Homepage'; +}; + + +/***/ }), + +/***/ 7: +/***/ (function(module, exports) { + +// removed by extract-text-webpack-plugin + +/***/ }) + +});" +`; + +exports[`Webpack Integration Tests multiple-entries-all-async 2`] = ` +"webpackJsonp([1],{ + +/***/ 0: +/***/ (function(module, exports, __webpack_require__) { + +__webpack_require__(6); + +modules.export = function() { + return 'Route Contact'; +}; + + +/***/ }), + +/***/ 6: +/***/ (function(module, exports) { + +// removed by extract-text-webpack-plugin + +/***/ }) + +});" +`; + +exports[`Webpack Integration Tests multiple-entries-all-async 3`] = ` +"body { + background: red; +} +.contact { + color: black; +} +.homepage { + color: black; +} +" +`; + +exports[`Webpack Integration Tests multiple-entries-all-async 4`] = ` +"body { + background: red; +} +.contact { + color: black; +} +.homepage { + color: black; +} +" +`; + +exports[`Webpack Integration Tests multiple-entries-async 1`] = ` +"webpackJsonp([0],{ + +/***/ 12: +/***/ (function(module, exports, __webpack_require__) { + +// style-loader: Adds some css to the DOM by adding a