diff --git a/hotModuleReplacement.js b/hotModuleReplacement.js index 2d965960..543cf06c 100644 --- a/hotModuleReplacement.js +++ b/hotModuleReplacement.js @@ -1,7 +1,8 @@ module.exports = function(publicPath, outputFilename) { if (document) { - var origin = document.location.protocol + '//' + document.location.hostname + (document.location.port ? ':' + document.location.port: ''); - var newHref = origin + publicPath + outputFilename + var origin = document.location.protocol + + '//' + document.location.hostname + (document.location.port ? ':' + document.location.port: ''); + var newHref = origin + publicPath + outputFilename; var styleSheets = document.getElementsByTagName('link'); //update the stylesheet corresponding to `outputFilename` @@ -20,4 +21,4 @@ module.exports = function(publicPath, outputFilename) { } } } -} +}; diff --git a/index.js b/index.js index f1760d7a..663f7826 100644 --- a/index.js +++ b/index.js @@ -1,66 +1,64 @@ /* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ var fs = require('fs'); var ConcatSource = require("webpack-sources").ConcatSource; -var CachedSource = require("webpack-sources").CachedSource; var async = require("async"); var ExtractedModule = require("./ExtractedModule"); var Chunk = require("webpack/lib/Chunk"); +var NormalModule = require("webpack/lib/NormalModule"); var OrderUndefinedError = require("./OrderUndefinedError"); var loaderUtils = require("loader-utils"); -var schemaTester = require('./schema/validator'); -var loaderSchema = require('./schema/loader-schema'); -var pluginSchema = require('./schema/plugin-schema.json'); +var validateOptions = require('schema-utils'); +var path = require('path'); var NS = fs.realpathSync(__dirname); -var DEV = process.env.NODE_ENV === 'development'; + var nextId = 0; function ExtractTextPluginCompilation() { this.modulesByIdentifier = {}; } -// ExtractTextPlugin.prototype.mergeNonInitialChunks = function(chunk, intoChunk, checkedChunks) { -// if (chunk.chunks) { -// // Fix error when hot module replacement used with CommonsChunkPlugin -// chunk.chunks = chunk.chunks.filter(function(c) { -// return typeof c !== 'undefined'; -// }) -// } +function isInitialOrHasNoParents(chunk) { + return chunk.isInitial() || chunk.parents.length === 0; +} -// if(!intoChunk) { -// checkedChunks = []; -// chunk.chunks.forEach(function(c) { -// if(c.isInitial()) return; -// this.mergeNonInitialChunks(c, chunk, checkedChunks); -// }, this); -// } else if(checkedChunks.indexOf(chunk) < 0) { -// checkedChunks.push(chunk); -// chunk.modules.slice().forEach(function(module) { -// intoChunk.addModule(module); -// module.addChunk(intoChunk); -// }); -// chunk.chunks.forEach(function(c) { -// if(c.isInitial()) return; -// this.mergeNonInitialChunks(c, intoChunk, checkedChunks); -// }, this); -// } -// }; +ExtractTextPlugin.prototype.mergeNonInitialChunks = function(chunk, intoChunk, checkedChunks) { + if(!intoChunk) { + checkedChunks = []; + chunk.chunks.forEach(function(c) { + if(isInitialOrHasNoParents(c)) return; + this.mergeNonInitialChunks(c, chunk, checkedChunks); + }, this); + } else if(checkedChunks.indexOf(chunk) < 0) { + checkedChunks.push(chunk); + chunk.modules.slice().forEach(function(module) { + intoChunk.addModule(module); + module.addChunk(intoChunk); + }); + chunk.chunks.forEach(function(c) { + if(isInitialOrHasNoParents(c)) return; + this.mergeNonInitialChunks(c, intoChunk, checkedChunks); + }, this); + } +}; -ExtractTextPluginCompilation.prototype.addModule = function(identifier, originalModule, source, additionalInformation, sourceMap, prevModules) { - var m; - if(!this.modulesByIdentifier[identifier]) { - m = this.modulesByIdentifier[identifier] = new ExtractedModule(identifier, originalModule, source, sourceMap, additionalInformation, prevModules); - } else { - m = this.modulesByIdentifier[identifier]; - m.addPrevModules(prevModules); - if(originalModule.index2 < m.getOriginalModule().index2) { - m.setOriginalModule(originalModule); +ExtractTextPluginCompilation.prototype.addModule = + function(identifier, originalModule, source, additionalInformation, sourceMap, prevModules) { + var m; + if(!this.modulesByIdentifier[identifier]) { + m = this.modulesByIdentifier[identifier] = + new ExtractedModule(identifier, originalModule, source, sourceMap, additionalInformation, prevModules); + } else { + m = this.modulesByIdentifier[identifier]; + m.addPrevModules(prevModules); + if(originalModule.index2 < m.getOriginalModule().index2) { + m.setOriginalModule(originalModule); + } } - } - return m; + return m; }; ExtractTextPluginCompilation.prototype.addResultToChunk = function(identifier, result, originalModule, extractedChunk) { @@ -115,26 +113,25 @@ function getOrder(a, b) { } function ExtractTextPlugin(options) { - options = options || {} - if(arguments.length > 1) { throw new Error("Breaking change: ExtractTextPlugin now only takes a single argument. Either an options " + - "object *or* the name of the result file.\n" + - "Example: if your old code looked like this:\n" + - " new ExtractTextPlugin('css/[name].css', { disable: false, allChunks: true })\n\n" + - "You would change it to:\n" + - " new ExtractTextPlugin({ filename: 'css/[name].css', disable: false, allChunks: true })\n\n" + - "The available options are:\n" + - " filename: string\n" + - " allChunks: boolean\n" + - " disable: boolean\n"); + "object *or* the name of the result file.\n" + + "Example: if your old code looked like this:\n" + + " new ExtractTextPlugin('css/[name].css', { disable: false, allChunks: true })\n\n" + + "You would change it to:\n" + + " new ExtractTextPlugin({ filename: 'css/[name].css', disable: false, allChunks: true })\n\n" + + "The available options are:\n" + + " filename: string\n" + + " allChunks: boolean\n" + + " disable: boolean\n" + + " ignoreOrder: boolean\n"); } if(isString(options)) { options = { filename: options }; } else { - schemaTester(pluginSchema, options); + validateOptions(path.resolve(__dirname, './schema/plugin.json'), options, 'Extract Text Plugin'); } - this.filename = options.filename || (DEV ? '[name].css' : '[name].[contenthash].css'); + this.filename = options.filename; this.id = options.id != null ? options.id : ++nextId; this.options = {}; mergeOptions(this.options, options); @@ -189,18 +186,19 @@ ExtractTextPlugin.prototype.loader = function(options) { return ExtractTextPlugin.loader(mergeOptions({id: this.id}, options)); }; + ExtractTextPlugin.prototype.extract = function(options) { if(arguments.length > 1) { throw new Error("Breaking change: extract now only takes a single argument. Either an options " + - "object *or* the loader(s).\n" + - "Example: if your old code looked like this:\n" + - " ExtractTextPlugin.extract('style-loader', 'css-loader')\n\n" + - "You would change it to:\n" + - " ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' })\n\n" + - "The available options are:\n" + - " use: string | object | loader[]\n" + - " fallback: string | object | loader[]\n" + - " publicPath: string\n"); + "object *or* the loader(s).\n" + + "Example: if your old code looked like this:\n" + + " ExtractTextPlugin.extract('style-loader', 'css-loader')\n\n" + + "You would change it to:\n" + + " ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' })\n\n" + + "The available options are:\n" + + " use: string | object | loader[]\n" + + " fallback: string | object | loader[]\n" + + " publicPath: string\n"); } if(options.fallbackLoader) { console.warn('fallbackLoader option has been deprecated - replace with "fallback"'); @@ -208,10 +206,11 @@ ExtractTextPlugin.prototype.extract = function(options) { if(options.loader) { console.warn('loader option has been deprecated - replace with "use"'); } - if(Array.isArray(options) || isString(options) || typeof options.options === "object" || typeof options.query === 'object') { + if(Array.isArray(options) || + isString(options) || typeof options.options === "object" || typeof options.query === 'object') { options = { loader: options }; } else { - schemaTester(loaderSchema, options); + validateOptions(path.resolve(__dirname, './schema/loader.json'), options, 'Extract Text Plugin (Loader)'); } var loader = options.use ||  options.loader; var before = options.fallback || options.fallbackLoader || []; @@ -231,7 +230,7 @@ ExtractTextPlugin.prototype.extract = function(options) { return [this.loader(options)] .concat(before, loader) .map(getLoaderObject); -} +}; ExtractTextPlugin.extract = ExtractTextPlugin.prototype.extract.bind(ExtractTextPlugin); @@ -239,6 +238,7 @@ ExtractTextPlugin.prototype.apply = function(compiler) { var options = this.options; compiler.plugin("this-compilation", function(compilation) { var extractCompilation = new ExtractTextPluginCompilation(); + var toRemoveModules = {}; compilation.plugin("normal-module-loader", function(loaderContext, module) { loaderContext[NS] = function(content, opt) { if(options.disable) @@ -255,6 +255,22 @@ ExtractTextPlugin.prototype.apply = function(compiler) { var filename = this.filename; var id = this.id; var extractedChunks, entryChunks, initialChunks; + + const getPath = (source, chunk) => (format) => compilation.getPath(format, { + chunk: chunk + }).replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, function() { + return loaderUtils.getHashDigest(source, arguments[1], arguments[2], parseInt(arguments[3], 10)); + }); + + var getFile =(module, chunk) => (isFunction(filename)) ? + filename(getPath(module.source(), chunk)) : + getPath(module.source(), chunk)(filename); + + var replaceTemplate = (module, chunk, toModifyModule) => { + var file = getFile(module, chunk); + toModifyModule._source._value = toModifyModule._source._value.replace("%%extracted-file%%", file); + }; + compilation.plugin("optimize-tree", function(chunks, modules, callback) { extractedChunks = chunks.map(function() { return new Chunk(); @@ -274,34 +290,57 @@ ExtractTextPlugin.prototype.apply = function(compiler) { }); async.forEach(chunks, function(chunk, callback) { var extractedChunk = extractedChunks[chunks.indexOf(chunk)]; - - // SETTING THIS TO TRUE INSURES ALL CHUNKS ARE HANDLED: - var shouldExtract = true; //!!(options.allChunks || chunk.isInitial()); - + var shouldExtract = !!(options.allChunks || isInitialOrHasNoParents(chunk)); async.forEach(chunk.modules.slice(), function(module, callback) { var meta = module[NS]; if(meta && (!meta.options.id || meta.options.id === id)) { var wasExtracted = Array.isArray(meta.content); if(shouldExtract !== wasExtracted) { - module[NS + "/extract"] = shouldExtract; // eslint-disable-line no-path-concat - compilation.rebuildModule(module, function(err) { + var newModule = new NormalModule( + module.request, + module.userRequest, + module.rawRequest, + module.loaders, + module.resource, + module.parser + ); + newModule[NS + "/extract"] = shouldExtract; // eslint-disable-line no-path-concat + // build a new module and save result to extracted compilations + compilation.buildModule(newModule, false, newModule, null, function(err) { if(err) { compilation.errors.push(err); return callback(); } - meta = module[NS]; + meta = newModule[NS]; + // 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(newModule.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) { + var ident = module.identifier(); + extractCompilation.addResultToChunk(ident, meta.content, module, extractedChunk); + // remove generated result from chunk + if(toRemoveModules[ident]) { + toRemoveModules[ident].chunks.push(chunk) + } else { + toRemoveModules[ident] = { + 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(); @@ -311,22 +350,61 @@ ExtractTextPlugin.prototype.apply = function(compiler) { }); }, function(err) { if(err) return callback(err); - // REMOVING THIS CODE IS ALL THAT'S NEEDED TO CREATE CSS FILES PER CHUNK: - // extractedChunks.forEach(function(extractedChunk) { - // if(extractedChunk.isInitial()) - // this.mergeNonInitialChunks(extractedChunk); - // }, this); - // extractedChunks.forEach(function(extractedChunk) { - // if(!extractedChunk.isInitial()) { - // extractedChunk.modules.slice().forEach(function(module) { - // extractedChunk.removeModule(module); - // }); - // } - // }); + extractedChunks.forEach(function(extractedChunk) { + if(isInitialOrHasNoParents(extractedChunk)) + this.mergeNonInitialChunks(extractedChunk); + }, this); + extractedChunks.forEach(function(extractedChunk) { + if(!isInitialOrHasNoParents(extractedChunk)) { + extractedChunk.modules.slice().forEach(function(module) { + extractedChunk.removeModule(module); + }); + } + }); compilation.applyPlugins("optimize-extracted-chunks", extractedChunks); callback(); }.bind(this)); }.bind(this)); + compilation.plugin("optimize-module-ids", function(modules){ + + // HMR: inject file name into corresponding javascript modules in order to trigger + // appropriate hot module reloading of CSS + extractedChunks.forEach(function(extractedChunk) { + extractedChunk.modules.forEach(function (module) { + if(module.__fileInjected) { + return; + } + module.__fileInjected = true; + var originalModule = module.getOriginalModule(); + replaceTemplate(module, extractedChunk, originalModule); + }); + }); + modules.forEach(function (module) { + var data = toRemoveModules[module.identifier()]; + if (data) { + var id = module.id; + var newModule = new NormalModule( + module.request, + module.userRequest, + module.rawRequest, + module.loaders, + module.resource, + module.parser + ); + newModule.id = id; + newModule._source = data.module._source; + data.chunks.forEach(function (chunk) { + chunk.removeModule(data.moduleToRemove); + data.moduleToRemove.dependencies.forEach(d => { + chunk.removeModule(d.module); + }); + replaceTemplate(newModule, chunk, newModule); + chunk.addModule(newModule); + }); + } + }); + + }); compilation.plugin("additional-assets", function(callback) { extractedChunks.forEach(function(extractedChunk) { @@ -339,56 +417,14 @@ ExtractTextPlugin.prototype.apply = function(compiler) { return getOrder(a, b); }); var chunk = extractedChunk.originalChunk; - var source = this.renderExtractedChunk(extractedChunk); - - var getPath = (format) => compilation.getPath(format, { - chunk: chunk - }).replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, function() { - return loaderUtils.getHashDigest(source.source(), arguments[1], arguments[2], parseInt(arguments[3], 10)); - }); - - var file = (isFunction(filename)) ? filename(getPath) : getPath(filename); - + var module = this.renderExtractedChunk(extractedChunk); + var file = getFile(module, extractedChunk); // add the css files to assets and the files array corresponding to its chunk - compilation.assets[file] = source; + compilation.assets[file] = module; chunk.files.push(file); - - // HMR: inject file name into corresponding javascript modules in order to trigger - // appropriate hot module reloading of CSS - extractedChunk.modules.forEach(function(module){ - var originalModule = module.getOriginalModule(); - originalModule._source._value = originalModule._source._value.replace('%%extracted-file%%', file); - }); } }, this); - - // duplicate js chunks into secondary files that don't have css injection, - // giving the additional js files the extension: `.no_css.js` - Object.keys(compilation.assets).forEach(function(name) { - var asset = compilation.assets[name]; - - if (/\.js$/.test(name) && asset._source) { - var newName = name.replace(/\.js/, '.no_css.js'); - var newAsset = new CachedSource(asset._source); - var regex = /\/\*__START_CSS__\*\/[\s\S]*?\/\*__END_CSS__\*\//g - - // remove js that adds css to DOM via style-loader, so that React Loadable - // can serve smaller files (without css) in initial request. - newAsset._cachedSource = asset.source().replace(regex, ''); - - compilation.assets[newName] = newAsset; - - // add no_css file to files associated with chunk so that they are minified, - // and receive source maps, and can be found by React Loadable - extractedChunks.forEach(function(extractedChunk) { - var chunk = extractedChunk.originalChunk; - if (chunk.files.indexOf(name) > -1) { - chunk.files.push(newName); - } - }) - } - }) - callback() + callback(); }.bind(this)); }.bind(this)); }; diff --git a/loader.js b/loader.js index 16ef2efd..6a7f49a5 100644 --- a/loader.js +++ b/loader.js @@ -1,50 +1,43 @@ /* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -var path = require("path"); + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ var fs = require("fs"); var loaderUtils = require("loader-utils"); -var jsesc = require("jsesc"); var NodeTemplatePlugin = require("webpack/lib/node/NodeTemplatePlugin"); var NodeTargetPlugin = require("webpack/lib/node/NodeTargetPlugin"); var LibraryTemplatePlugin = require("webpack/lib/LibraryTemplatePlugin"); var SingleEntryPlugin = require("webpack/lib/SingleEntryPlugin"); var LimitChunkCountPlugin = require("webpack/lib/optimize/LimitChunkCountPlugin"); - +var path = require('path'); var NS = fs.realpathSync(__dirname); -module.exports = function(source) { +module.exports = function (source) { if(this.cacheable) this.cacheable(); - // Even though this gets overwritten if extract+remove are true, without it, the runtime doesn't get added to the chunk - return `require("style-loader/lib/addStyles.js"); - if (module.hot) { require('${require.resolve("./hotModuleReplacement.js")}'); } - ${source}`; + return source; }; -module.exports.pitch = function(request) { - var self = this; - var remainingRequest = request; +module.exports.pitch = function (request) { if(this.cacheable) this.cacheable(); var query = loaderUtils.getOptions(this) || {}; var loaders = this.loaders.slice(this.loaderIndex + 1); this.addDependency(this.resourcePath); // We already in child compiler, return empty bundle - if(this[NS] === undefined) { + if (this[NS] === undefined) { throw new Error( '"extract-text-webpack-plugin" loader is used without the corresponding plugin, ' + 'refer to https://github.com/webpack/extract-text-webpack-plugin for the usage example' ); - } else if(this[NS] === false) { + } else if (this[NS] === false) { return ""; - } else if(this[NS](null, query)) { - if(query.omit) { + } else if (this[NS](null, query)) { + if (query.omit) { this.loaderIndex += +query.omit + 1; request = request.split("!").slice(+query.omit).join("!"); loaders = loaders.slice(+query.omit); } var resultSource; - if(query.remove) { + if (query.remove) { resultSource = "// removed by extract-text-webpack-plugin"; } else { resultSource = undefined; @@ -61,22 +54,22 @@ module.exports.pitch = function(request) { childCompiler.apply(new LibraryTemplatePlugin(null, "commonjs2")); childCompiler.apply(new NodeTargetPlugin()); childCompiler.apply(new SingleEntryPlugin(this.context, "!!" + request)); - childCompiler.apply(new LimitChunkCountPlugin({ maxChunks: 1 })); + childCompiler.apply(new LimitChunkCountPlugin({maxChunks: 1})); var subCache = "subcache " + NS + " " + request; // eslint-disable-line no-path-concat - childCompiler.plugin("compilation", function(compilation) { - if(compilation.cache) { - if(!compilation.cache[subCache]) + childCompiler.plugin("compilation", function (compilation) { + if (compilation.cache) { + if (!compilation.cache[subCache]) compilation.cache[subCache] = {}; compilation.cache = compilation.cache[subCache]; } }); // We set loaderContext[NS] = false to indicate we already in // a child compiler so we don't spawn another child compilers from there. - childCompiler.plugin("this-compilation", function(compilation) { - compilation.plugin("normal-module-loader", function(loaderContext, module) { + childCompiler.plugin("this-compilation", function (compilation) { + compilation.plugin("normal-module-loader", function (loaderContext, module) { loaderContext[NS] = false; if (module.request === request) { - module.loaders = loaders.map(function(loader) { + module.loaders = loaders.map(function (loader) { return { loader: loader.path, options: loader.options @@ -87,12 +80,12 @@ module.exports.pitch = function(request) { }); var source; - childCompiler.plugin("after-compile", function(compilation, callback) { + childCompiler.plugin("after-compile", function (compilation, callback) { source = compilation.assets[childFilename] && compilation.assets[childFilename].source(); // Remove all chunk assets - compilation.chunks.forEach(function(chunk) { - chunk.files.forEach(function(file) { + compilation.chunks.forEach(function (chunk) { + chunk.files.forEach(function (file) { delete compilation.assets[file]; }); }); @@ -100,63 +93,54 @@ module.exports.pitch = function(request) { callback(); }); var callback = this.async(); - childCompiler.runAsChild(function(err, entries, compilation) { - if(err) return callback(err); + childCompiler.runAsChild(function (err, entries, compilation) { + if (err) return callback(err); - if(compilation.errors.length > 0) { + if (compilation.errors.length > 0) { return callback(compilation.errors[0]); } - compilation.fileDependencies.forEach(function(dep) { + compilation.fileDependencies.forEach(function (dep) { this.addDependency(dep); }, this); - compilation.contextDependencies.forEach(function(dep) { + compilation.contextDependencies.forEach(function (dep) { this.addContextDependency(dep); }, this); - if(!source) { + if (!source) { return callback(new Error("Didn't get a result from child compiler")); } try { var text = this.exec(source, request); - if(typeof text === "string") + if (typeof text === "string") text = [[0, text]]; - text.forEach(function(item) { + text.forEach(function (item) { var id = item[0]; - compilation.modules.forEach(function(module) { - if(module.id === id) + compilation.modules.forEach(function (module) { + if (module.id === id) item[0] = module.identifier(); }); }); this[NS](text, query); - if(typeof resultSource !== "undefined") { + + if (typeof resultSource !== "undefined") { if (text.locals) { resultSource += "\nmodule.exports = " + JSON.stringify(text.locals) + ";"; } - - // module.hot.data is undefined on initial load, and an object in hot updates - var jsescOpts = { wrap: true, quotes: "double" }; resultSource += ` -/*__START_CSS__*/ -var moduleId = ${jsesc(text[0][0], jsescOpts)}; -var css = ${jsesc(text[0][1], jsescOpts)}; -var addStyles = require("style-loader/lib/addStyles.js"); -addStyles([[moduleId, css]], ""); -/*__END_CSS__*/ - if (module.hot) { module.hot.accept(); if (module.hot.data) { - require("${require.resolve('./hotModuleReplacement.js')}")("${publicPath}", "%%extracted-file%%"); + require(${loaderUtils.stringifyRequest(this, path.join(__dirname, "hotModuleReplacement.js"))})("${publicPath}", "%%extracted-file%%"); } }`; } - } catch(e) { + } catch (e) { return callback(e); } - - if(resultSource) + if (resultSource) { callback(null, resultSource); - else + } else { callback(); + } }.bind(this)); } }; diff --git a/package.json b/package.json index e8cff4c1..3eaf73e6 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,7 @@ "webpack": "^2.2.0" }, "dependencies": { - "ajv": "^4.11.2", "async": "^2.1.2", - "jsesc": "^2.5.1", "loader-utils": "^1.0.2", "schema-utils": "^0.3.0", "style-loader": "^0.18.2", @@ -48,10 +46,11 @@ "mocha": "^3.2.0", "mocha-lcov-reporter": "1.3.0", "raw-loader": "^0.5.1", + "rimraf": "^2.6.1", "semantic-release": "^6.3.2", "should": "^11.1.2", "standard-version": "^4.0.0", - "webpack": "^2.2.0" + "webpack": "~2.6.0" }, "homepage": "http://github.com/faceyspacey/extract-css-chunks-webpack-plugin", "repository": { diff --git a/schema/loader-schema.js b/schema/loader-schema.js deleted file mode 100644 index 80266761..00000000 --- a/schema/loader-schema.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "allChunks": { "type": "boolean"}, - "disable": { "type": "boolean" }, - "omit": { "type": "boolean" }, - "remove": { "type": "boolean" }, - "fallback": { "type": ["string", "array", "object"] }, - "filename": { "type": "string" }, - "use": { "type": ["string", "array", "object"] }, - "publicPath": { "type": "string" }, - - // deprecated - "fallbackLoader": { "type": ["string", "array", "object"] }, - "loader": { "type": ["string", "array", "object"] } - } -}; diff --git a/schema/loader.json b/schema/loader.json new file mode 100644 index 00000000..bca660af --- /dev/null +++ b/schema/loader.json @@ -0,0 +1,36 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "allChunks": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + }, + "omit": { + "type": "boolean" + }, + "remove": { + "type": "boolean" + }, + "fallback": { + "type": ["string", "array", "object"] + }, + "filename": { + "type": "string" + }, + "use": { + "type": ["string", "array", "object"] + }, + "publicPath": { + "type": "string" + }, + "fallbackLoader": { + "type": ["string", "array", "object"] + }, + "loader": { + "type": ["string", "array", "object"] + } + } +} diff --git a/schema/plugin-schema.json b/schema/plugin.json similarity index 76% rename from schema/plugin-schema.json rename to schema/plugin.json index 7b507d17..4fcf2e66 100644 --- a/schema/plugin-schema.json +++ b/schema/plugin.json @@ -1,5 +1,4 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "additionalProperties": false, "properties": { @@ -14,16 +13,13 @@ "fallback": { "description": "A loader that webpack can fall back to if the original one fails.", "modes": { - "type": "string", - "type": "object", - "type": "array" + "type": ["string", "object", "array"] } }, "filename": { "description": "The filename and path that ExtractTextPlugin will extract to", "modes": { - "type": "string", - "type": "function" + "type": ["string", "function"] } }, "ignoreOrder": { @@ -33,9 +29,7 @@ "loader": { "description": "The loader that ExtractTextPlugin will attempt to load through.", "modes": { - "type": "string", - "type": "object", - "type": "array" + "type": ["string", "object", "array"] } }, "publicPath": { diff --git a/schema/validator.js b/schema/validator.js deleted file mode 100644 index 5e5f9174..00000000 --- a/schema/validator.js +++ /dev/null @@ -1,13 +0,0 @@ -var Ajv = require('ajv'); -var ajv = new Ajv({allErrors: true}); - -module.exports = function validate(schema, data) { - var ajv = new Ajv({ - errorDataPath: 'property' - }); - var isValid = ajv.validate(schema, data); - - if(!isValid) { - throw new Error(ajv.errorsText()); - } -} diff --git a/test/TestCases.test.js b/test/TestCases.test.js index bbcb3712..415d1094 100644 --- a/test/TestCases.test.js +++ b/test/TestCases.test.js @@ -3,6 +3,7 @@ var vm = require("vm"); var path = require("path"); var webpack = require("webpack"); var should = require("should"); +var rimraf = require('rimraf'); var ExtractTextPlugin = require("../"); var cases = process.env.CASES ? process.env.CASES.split(",") : fs.readdirSync(path.join(__dirname, "cases")); @@ -12,36 +13,39 @@ describe("TestCases", function() { it(testCase, function(done) { var testDirectory = path.join(__dirname, "cases", testCase); var outputDirectory = path.join(__dirname, "js", testCase); - var options = { entry: { test: "./index.js" } }; - var configFile = path.join(testDirectory, "webpack.config.js"); - if(fs.existsSync(configFile)) - options = require(configFile); - options.context = testDirectory; - if(!options.module) options.module = {}; - if(!options.module.loaders) options.module.loaders = [ - { test: /\.txt$/, loader: ExtractTextPlugin.extract("raw-loader") } - ]; - if(!options.output) options.output = { filename: "[name].js" }; - if(!options.output.path) options.output.path = outputDirectory; - if(process.env.CASES) { - console.log("\nwebpack." + testCase + ".config.js " + JSON.stringify(options, null, 2)); - } - webpack(options, function(err, stats) { - if(err) return done(err); - if(stats.hasErrors()) return done(new Error(stats.toString())); - var testFile = path.join(outputDirectory, "test.js"); - if(fs.existsSync(testFile)) - require(testFile)(suite); - var expectedDirectory = path.join(testDirectory, "expected"); - fs.readdirSync(expectedDirectory).forEach(function(file) { - var filePath = path.join(expectedDirectory, file); - var actualPath = path.join(outputDirectory, file); - readFileOrEmpty(actualPath).should.be.eql( - readFileOrEmpty(filePath), - file + " should be correct"); + function test() { + var options = { entry: { test: "./index.js" } }; + var configFile = path.join(testDirectory, "webpack.config.js"); + if (fs.existsSync(configFile)) + options = require(configFile); + options.context = testDirectory; + if (!options.module) options.module = {}; + if (!options.module.loaders) options.module.loaders = [ + { test: /\.txt$/, loader: ExtractTextPlugin.extract("raw-loader") } + ]; + if (!options.output) options.output = { filename: "[name].js" }; + if (!options.output.path) options.output.path = outputDirectory; + if (process.env.CASES) { + console.log("\nwebpack." + testCase + ".config.js " + JSON.stringify(options, null, 2)); + } + webpack(options, function (err, stats) { + if (err) return done(err); + if (stats.hasErrors()) return done(new Error(stats.toString())); + var testFile = path.join(outputDirectory, "test.js"); + if (fs.existsSync(testFile)) + require(testFile)(suite); + var expectedDirectory = path.join(testDirectory, "expected"); + fs.readdirSync(expectedDirectory).forEach(function (file) { + var filePath = path.join(expectedDirectory, file); + var actualPath = path.join(outputDirectory, file); + readFileOrEmpty(actualPath).should.be.eql( + readFileOrEmpty(filePath), + file + " should be correct"); + }); + done(); }); - done(); - }); + } + rimraf(outputDirectory, test); }); }); }); diff --git a/test/cases/common-async/a.js b/test/cases/common-async/a.js new file mode 100644 index 00000000..2d7d820b --- /dev/null +++ b/test/cases/common-async/a.js @@ -0,0 +1,2 @@ +require("./a.txt"); +require("./b.txt"); diff --git a/test/cases/common-async/a.txt b/test/cases/common-async/a.txt new file mode 100644 index 00000000..78981922 --- /dev/null +++ b/test/cases/common-async/a.txt @@ -0,0 +1 @@ +a diff --git a/test/cases/common-async/b.js b/test/cases/common-async/b.js new file mode 100644 index 00000000..caac53f4 --- /dev/null +++ b/test/cases/common-async/b.js @@ -0,0 +1,3 @@ +require("./a.txt"); +require("./c.txt"); + diff --git a/test/cases/common-async/b.txt b/test/cases/common-async/b.txt new file mode 100644 index 00000000..61780798 --- /dev/null +++ b/test/cases/common-async/b.txt @@ -0,0 +1 @@ +b diff --git a/test/cases/common-async/c.txt b/test/cases/common-async/c.txt new file mode 100644 index 00000000..f2ad6c76 --- /dev/null +++ b/test/cases/common-async/c.txt @@ -0,0 +1 @@ +c diff --git a/test/cases/common-async/expected/file.css b/test/cases/common-async/expected/file.css new file mode 100644 index 00000000..de980441 --- /dev/null +++ b/test/cases/common-async/expected/file.css @@ -0,0 +1,3 @@ +a +b +c diff --git a/test/cases/common-async/index.js b/test/cases/common-async/index.js new file mode 100644 index 00000000..624fa419 --- /dev/null +++ b/test/cases/common-async/index.js @@ -0,0 +1,14 @@ +require.ensure( + [], + function() { + require("./a.js"); + }, + 'async-chunk-a' +); +require.ensure( + [], + function() { + require("./b.js"); + }, + 'async-chunk-b' +); diff --git a/test/cases/common-async/webpack.config.js b/test/cases/common-async/webpack.config.js new file mode 100644 index 00000000..758d08a1 --- /dev/null +++ b/test/cases/common-async/webpack.config.js @@ -0,0 +1,17 @@ +var webpack = require('webpack'); +var ExtractTextPlugin = require("../../../index"); + +module.exports = { + entry: "./index", + plugins: [ + new webpack.optimize.CommonsChunkPlugin({ + name: 'common', + filename: 'common.js', + chunks: ['async-chunk-a', 'async-chunk-b'] + }), + new ExtractTextPlugin({ + filename: "file.css", + allChunks: true + }) + ] +}; diff --git a/test/cases/multiple-entries-all-async-eval/default-styles.css b/test/cases/multiple-entries-all-async-eval/default-styles.css new file mode 100644 index 00000000..f0d5b13b --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/default-styles.css @@ -0,0 +1,3 @@ +body { + background: red; +} diff --git a/test/cases/multiple-entries-all-async-eval/entries/contact.js b/test/cases/multiple-entries-all-async-eval/entries/contact.js new file mode 100644 index 00000000..db962fb6 --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/entries/contact.js @@ -0,0 +1,2 @@ +require('../router'); +require('../routes/contact/index'); diff --git a/test/cases/multiple-entries-all-async-eval/entries/homepage.js b/test/cases/multiple-entries-all-async-eval/entries/homepage.js new file mode 100644 index 00000000..0cf6a0ff --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/entries/homepage.js @@ -0,0 +1,2 @@ +require('../router'); +require('../routes/homepage/index'); diff --git a/test/cases/multiple-entries-all-async-eval/expected/0.js b/test/cases/multiple-entries-all-async-eval/expected/0.js new file mode 100644 index 00000000..6fad5f29 --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/expected/0.js @@ -0,0 +1,17 @@ +webpackJsonp([0],{ + +/***/ 2: +/***/ (function(module, exports, __webpack_require__) { + +eval("__webpack_require__(7);\n\nmodules.export = function() {\n\treturn 'Route Homepage';\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./routes/homepage/index.js\n// module id = 2\n// module chunks = 0 2\n\n//# sourceURL=webpack:///./routes/homepage/index.js?"); + +/***/ }), + +/***/ 7: +/***/ (function(module, exports, __webpack_require__) { + +eval("// removed by extract-text-webpack-plugin\nif (false) {\n\tmodule.hot.accept();\n\tif (module.hot.data) {\n\t\trequire(\"../../../../../hotModuleReplacement.js\")(\"undefined\", \"homepage.css\");\n\t}\n}\n\n//////////////////\n// WEBPACK FOOTER\n// ./routes/homepage/styles.css\n// module id = 7\n// module chunks = 0 2\n\n//# sourceURL=webpack:///./routes/homepage/styles.css?"); + +/***/ }) + +}); \ No newline at end of file diff --git a/test/cases/multiple-entries-all-async-eval/expected/1.js b/test/cases/multiple-entries-all-async-eval/expected/1.js new file mode 100644 index 00000000..ede5d2e2 --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/expected/1.js @@ -0,0 +1,17 @@ +webpackJsonp([1],{ + +/***/ 1: +/***/ (function(module, exports, __webpack_require__) { + +eval("__webpack_require__(6);\n\nmodules.export = function() {\n\treturn 'Route Contact';\n};\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./routes/contact/index.js\n// module id = 1\n// module chunks = 1 3\n\n//# sourceURL=webpack:///./routes/contact/index.js?"); + +/***/ }), + +/***/ 6: +/***/ (function(module, exports, __webpack_require__) { + +eval("// removed by extract-text-webpack-plugin\nif (false) {\n\tmodule.hot.accept();\n\tif (module.hot.data) {\n\t\trequire(\"../../../../../hotModuleReplacement.js\")(\"undefined\", \"homepage.css\");\n\t}\n}\n\n//////////////////\n// WEBPACK FOOTER\n// ./routes/contact/styles.css\n// module id = 6\n// module chunks = 1 3\n\n//# sourceURL=webpack:///./routes/contact/styles.css?"); + +/***/ }) + +}); \ No newline at end of file diff --git a/test/cases/multiple-entries-all-async-eval/expected/contact.css b/test/cases/multiple-entries-all-async-eval/expected/contact.css new file mode 100644 index 00000000..c952ccfd --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/expected/contact.css @@ -0,0 +1,9 @@ +body { + background: red; +} +.contact { + color: black; +} +.homepage { + color: black; +} diff --git a/test/cases/multiple-entries-all-async-eval/expected/homepage.css b/test/cases/multiple-entries-all-async-eval/expected/homepage.css new file mode 100644 index 00000000..c952ccfd --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/expected/homepage.css @@ -0,0 +1,9 @@ +body { + background: red; +} +.contact { + color: black; +} +.homepage { + color: black; +} diff --git a/test/cases/multiple-entries-all-async-eval/router.js b/test/cases/multiple-entries-all-async-eval/router.js new file mode 100644 index 00000000..d9855317 --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/router.js @@ -0,0 +1,6 @@ +require('./default-styles.css'); +module.export = function (route) { + return import(/* webpackChunkName: "route-[request]" */ './routes/' + route + 'index.js').then(function (route) { + return route; + }); +}; diff --git a/test/cases/multiple-entries-all-async-eval/routes/contact/index.js b/test/cases/multiple-entries-all-async-eval/routes/contact/index.js new file mode 100644 index 00000000..c16daacb --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/routes/contact/index.js @@ -0,0 +1,5 @@ +require('./styles.css'); + +modules.export = function() { + return 'Route Contact'; +}; diff --git a/test/cases/multiple-entries-all-async-eval/routes/contact/styles.css b/test/cases/multiple-entries-all-async-eval/routes/contact/styles.css new file mode 100644 index 00000000..fd8a667d --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/routes/contact/styles.css @@ -0,0 +1,3 @@ +.contact { + color: black; +} diff --git a/test/cases/multiple-entries-all-async-eval/routes/homepage/index.js b/test/cases/multiple-entries-all-async-eval/routes/homepage/index.js new file mode 100644 index 00000000..bb86d26e --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/routes/homepage/index.js @@ -0,0 +1,5 @@ +require('./styles.css'); + +modules.export = function() { + return 'Route Homepage'; +}; diff --git a/test/cases/multiple-entries-all-async-eval/routes/homepage/styles.css b/test/cases/multiple-entries-all-async-eval/routes/homepage/styles.css new file mode 100644 index 00000000..1fc1fc5a --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/routes/homepage/styles.css @@ -0,0 +1,3 @@ +.homepage { + color: black; +} diff --git a/test/cases/multiple-entries-all-async-eval/webpack.config.js b/test/cases/multiple-entries-all-async-eval/webpack.config.js new file mode 100644 index 00000000..b1db2d89 --- /dev/null +++ b/test/cases/multiple-entries-all-async-eval/webpack.config.js @@ -0,0 +1,25 @@ +var ExtractTextPlugin = require("../../../"); + +module.exports = { + devtool: 'eval', + entry: { + 'homepage': "./entries/homepage.js", + 'contact': "./entries/contact.js" + }, + module: { + loaders: [ + { test: /\.css$/, use: ExtractTextPlugin.extract({ + fallback: "style-loader", + use: { loader: "css-loader", options: { + sourceMap: false + } } + }) } + ] + }, + plugins: [ + new ExtractTextPlugin({ + filename: '[name].css', + allChunks: true + }) + ] +}; diff --git a/test/cases/multiple-entries-all-async/default-styles.css b/test/cases/multiple-entries-all-async/default-styles.css new file mode 100644 index 00000000..f0d5b13b --- /dev/null +++ b/test/cases/multiple-entries-all-async/default-styles.css @@ -0,0 +1,3 @@ +body { + background: red; +} diff --git a/test/cases/multiple-entries-all-async/entries/contact.js b/test/cases/multiple-entries-all-async/entries/contact.js new file mode 100644 index 00000000..66b458c2 --- /dev/null +++ b/test/cases/multiple-entries-all-async/entries/contact.js @@ -0,0 +1,2 @@ +require('../router'); +require('../routes/contact'); diff --git a/test/cases/multiple-entries-all-async/entries/homepage.js b/test/cases/multiple-entries-all-async/entries/homepage.js new file mode 100644 index 00000000..7a8e5aea --- /dev/null +++ b/test/cases/multiple-entries-all-async/entries/homepage.js @@ -0,0 +1,2 @@ +require('../router'); +require('../routes/homepage'); diff --git a/test/cases/multiple-entries-all-async/expected/0.js b/test/cases/multiple-entries-all-async/expected/0.js new file mode 100644 index 00000000..5b5ca98c --- /dev/null +++ b/test/cases/multiple-entries-all-async/expected/0.js @@ -0,0 +1,28 @@ +webpackJsonp([0],{ + +/***/ 2: +/***/ (function(module, exports, __webpack_require__) { + +__webpack_require__(7); + +modules.export = function() { + return 'Route Homepage'; +}; + + +/***/ }), + +/***/ 7: +/***/ (function(module, exports, __webpack_require__) { + +// removed by extract-text-webpack-plugin +if (false) { + module.hot.accept(); + if (module.hot.data) { + require("../../../../../hotModuleReplacement.js")("undefined", "homepage.css"); + } +} + +/***/ }) + +}); \ No newline at end of file diff --git a/test/cases/multiple-entries-all-async/expected/1.js b/test/cases/multiple-entries-all-async/expected/1.js new file mode 100644 index 00000000..a37f0e60 --- /dev/null +++ b/test/cases/multiple-entries-all-async/expected/1.js @@ -0,0 +1,28 @@ +webpackJsonp([1],{ + +/***/ 1: +/***/ (function(module, exports, __webpack_require__) { + +__webpack_require__(6); + +modules.export = function() { + return 'Route Contact'; +}; + + +/***/ }), + +/***/ 6: +/***/ (function(module, exports, __webpack_require__) { + +// removed by extract-text-webpack-plugin +if (false) { + module.hot.accept(); + if (module.hot.data) { + require("../../../../../hotModuleReplacement.js")("undefined", "homepage.css"); + } +} + +/***/ }) + +}); \ No newline at end of file diff --git a/test/cases/multiple-entries-all-async/expected/contact.css b/test/cases/multiple-entries-all-async/expected/contact.css new file mode 100644 index 00000000..c952ccfd --- /dev/null +++ b/test/cases/multiple-entries-all-async/expected/contact.css @@ -0,0 +1,9 @@ +body { + background: red; +} +.contact { + color: black; +} +.homepage { + color: black; +} diff --git a/test/cases/multiple-entries-all-async/expected/homepage.css b/test/cases/multiple-entries-all-async/expected/homepage.css new file mode 100644 index 00000000..c952ccfd --- /dev/null +++ b/test/cases/multiple-entries-all-async/expected/homepage.css @@ -0,0 +1,9 @@ +body { + background: red; +} +.contact { + color: black; +} +.homepage { + color: black; +} diff --git a/test/cases/multiple-entries-all-async/router.js b/test/cases/multiple-entries-all-async/router.js new file mode 100644 index 00000000..d9855317 --- /dev/null +++ b/test/cases/multiple-entries-all-async/router.js @@ -0,0 +1,6 @@ +require('./default-styles.css'); +module.export = function (route) { + return import(/* webpackChunkName: "route-[request]" */ './routes/' + route + 'index.js').then(function (route) { + return route; + }); +}; diff --git a/test/cases/multiple-entries-all-async/routes/contact/index.js b/test/cases/multiple-entries-all-async/routes/contact/index.js new file mode 100644 index 00000000..c16daacb --- /dev/null +++ b/test/cases/multiple-entries-all-async/routes/contact/index.js @@ -0,0 +1,5 @@ +require('./styles.css'); + +modules.export = function() { + return 'Route Contact'; +}; diff --git a/test/cases/multiple-entries-all-async/routes/contact/styles.css b/test/cases/multiple-entries-all-async/routes/contact/styles.css new file mode 100644 index 00000000..fd8a667d --- /dev/null +++ b/test/cases/multiple-entries-all-async/routes/contact/styles.css @@ -0,0 +1,3 @@ +.contact { + color: black; +} diff --git a/test/cases/multiple-entries-all-async/routes/homepage/index.js b/test/cases/multiple-entries-all-async/routes/homepage/index.js new file mode 100644 index 00000000..bb86d26e --- /dev/null +++ b/test/cases/multiple-entries-all-async/routes/homepage/index.js @@ -0,0 +1,5 @@ +require('./styles.css'); + +modules.export = function() { + return 'Route Homepage'; +}; diff --git a/test/cases/multiple-entries-all-async/routes/homepage/styles.css b/test/cases/multiple-entries-all-async/routes/homepage/styles.css new file mode 100644 index 00000000..1fc1fc5a --- /dev/null +++ b/test/cases/multiple-entries-all-async/routes/homepage/styles.css @@ -0,0 +1,3 @@ +.homepage { + color: black; +} diff --git a/test/cases/multiple-entries-all-async/webpack.config.js b/test/cases/multiple-entries-all-async/webpack.config.js new file mode 100644 index 00000000..d94f0093 --- /dev/null +++ b/test/cases/multiple-entries-all-async/webpack.config.js @@ -0,0 +1,23 @@ +var ExtractTextPlugin = require("../../../"); +module.exports = { + entry: { + 'homepage': "./entries/homepage.js", + 'contact': "./entries/contact.js" + }, + module: { + loaders: [ + { test: /\.css$/, use: ExtractTextPlugin.extract({ + fallback: "style-loader", + use: { loader: "css-loader", options: { + sourceMap: false + } } + }) } + ] + }, + plugins: [ + new ExtractTextPlugin({ + filename: '[name].css', + allChunks: true + }) + ] +}; diff --git a/test/cases/multiple-entries-async/default-styles.css b/test/cases/multiple-entries-async/default-styles.css new file mode 100644 index 00000000..f0d5b13b --- /dev/null +++ b/test/cases/multiple-entries-async/default-styles.css @@ -0,0 +1,3 @@ +body { + background: red; +} diff --git a/test/cases/multiple-entries-async/entries/contact.js b/test/cases/multiple-entries-async/entries/contact.js new file mode 100644 index 00000000..66b458c2 --- /dev/null +++ b/test/cases/multiple-entries-async/entries/contact.js @@ -0,0 +1,2 @@ +require('../router'); +require('../routes/contact'); diff --git a/test/cases/multiple-entries-async/entries/homepage.js b/test/cases/multiple-entries-async/entries/homepage.js new file mode 100644 index 00000000..7a8e5aea --- /dev/null +++ b/test/cases/multiple-entries-async/entries/homepage.js @@ -0,0 +1,2 @@ +require('../router'); +require('../routes/homepage'); diff --git a/test/cases/multiple-entries-async/expected/0.js b/test/cases/multiple-entries-async/expected/0.js new file mode 100644 index 00000000..0d707b04 --- /dev/null +++ b/test/cases/multiple-entries-async/expected/0.js @@ -0,0 +1,62 @@ +webpackJsonp([0],{ + +/***/ 16: +/***/ (function(module, exports, __webpack_require__) { + +// style-loader: Adds some css to the DOM by adding a