diff --git a/README.md b/README.md index 033e73e..ca61a57 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Allowed values are as follows: - `cache`: `true | false` if `true` (default) try to emit the file only if it was changed. - `showErrors`: `true | false` if `true` (default) errors details will be written into the html page. - `chunks`: Allows you to add only some chunks (e.g. only the unit-test chunk) -- `chunksSortMode`: Allows to controll how chunks should be sorted before they are included to the html. Allowed values: 'none' | 'default' | {function} - default: 'auto' +- `chunksSortMode`: Allows to control how chunks should be sorted before they are included to the html. Allowed values: 'none' | 'auto' | 'dependency' | {function} - default: 'auto' - `excludeChunks`: Allows you to skip some chunks (e.g. don't add the unit-test chunk) Here's an example webpack config illustrating how to use these options: diff --git a/index.js b/index.js index 2217c3e..df2fbef 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ var Promise = require('bluebird'); var path = require('path'); var childCompiler = require('./lib/compiler.js'); var prettyError = require('./lib/errors.js'); +var chunkSorter = require('./lib/chunksorter.js'); Promise.promisifyAll(fs); function HtmlWebpackPlugin (options) { @@ -280,17 +281,21 @@ HtmlWebpackPlugin.prototype.addFileToAssets = function (filename, compilation) { HtmlWebpackPlugin.prototype.sortChunks = function (chunks, sortMode) { // Sort mode auto by default: if (typeof sortMode === 'undefined' || sortMode === 'auto') { - return chunks.sort(function orderEntryLast (a, b) { - if (a.entry !== b.entry) { - return b.entry ? 1 : -1; - } else { - return b.id - a.id; - } - }); + return chunkSorter.auto(chunks); + } + // Sort mode 'dependency': + if (sortMode === 'dependency') { + var sortResult = chunkSorter.dependency(chunks); + + if (!sortResult) { + throw new Error('Chunk sorting based on dependencies failed. Please consider custom sort mode.'); + } + + return sortResult; } // Disabled sorting: if (sortMode === 'none') { - return chunks; + return chunkSorter.none(chunks); } // Custom function if (typeof sortMode === 'function') { diff --git a/lib/chunksorter.js b/lib/chunksorter.js new file mode 100644 index 0000000..0ef52d2 --- /dev/null +++ b/lib/chunksorter.js @@ -0,0 +1,96 @@ +'use strict'; + +var toposort = require('toposort'); + +/* + Sorts dependencies between chunks by their "parents" attribute. + + This function sorts chunks based on their dependencies with each other. + The parent relation between chunks as generated by Webpack for each chunk + is used to define a directed (and hopefully acyclic) graph, which is then + topologically sorted in order to retrieve the correct order in which + chunks need to be embedded into HTML. A directed edge in this graph is + describing a "is parent of" relationship from a chunk to another (distinct) + chunk. Thus topological sorting orders chunks from bottom-layer chunks to + highest level chunks that use the lower-level chunks. + + @param {Array} chunks an array of chunks as generated by the html-webpack-plugin. + It is assumed that each entry contains at least the properties "id" + (containing the chunk id) and "parents" (array containing the ids of the + parent chunks). Must not be null/undefined or empty + + @return {Array} A topologically sorted version of the input chunks, or null if + no such order could be calculated (e.g. because the chunks and their + parent relations did not define an directed acyclic graph). +*/ +module.exports.dependency = function (chunks) { + if (!chunks) { + return null; + } + + // We build a map (chunk-id -> chunk) for faster access during graph building. + var nodeMap = []; + + chunks.forEach(function (chunk) { + nodeMap[chunk.id] = chunk; + }); + + // Next, we add an edge for each parent relationship into the graph + var edges = []; + + chunks.forEach(function (chunk) { + if (chunk.parents) { + // Add an edge for each parent (parent -> child) + chunk.parents.forEach(function (parentId) { + var parentChunk = nodeMap[parentId]; + + if (!parentChunk) { + return null; // We haven't found the referenced chunk in our map! + } + + edges.push([parentChunk, chunk]); + }); + } + }); + + // We now perform a topological sorting on the input chunks and built edges + var sortedVertices = null; + + try { + sortedVertices = toposort.array(chunks, edges); + } catch (err) { + return null; // Error during sort + } + + return sortedVertices; +}; + +/** + * Sorts the chunks based on the chunk id. + * + * @param {Array} chunks the list of chunks to sort + * @return {Array} The sorted list of chunks + */ +module.exports.id = function (chunks) { + return chunks.sort(function orderEntryLast (a, b) { + if (a.entry !== b.entry) { + return b.entry ? 1 : -1; + } else { + return b.id - a.id; + } + }); +}; + +/** + * Performs identity mapping (no-sort). + * @param {Array} chunks the chunks to sort + * @return {Array} The sorted chunks + */ +module.exports.none = function (chunks) { + return chunks; +}; + +/** + * Defines the default sorter. + */ +module.exports.auto = module.exports.id; diff --git a/package.json b/package.json index d9cc045..a6b9c5a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "html-minifier": "^1.1.1", "loader-utils": "^0.2.12", "lodash": "^3.10.1", - "pretty-error": "^2.0.0" + "pretty-error": "^2.0.0", + "toposort": "^0.2.12" } } diff --git a/spec/HtmlWebpackPluginSpec.js b/spec/HtmlWebpackPluginSpec.js index c74e942..bdcbb24 100644 --- a/spec/HtmlWebpackPluginSpec.js +++ b/spec/HtmlWebpackPluginSpec.js @@ -842,7 +842,7 @@ describe('HtmlWebpackPlugin', function () { }, ["Child compilation failed:\n Entry module not found: Error: Cannot resolve 'file' or 'directory'"], null, done, true); }); - it('should short the chunks', function (done) { + it('should sort the chunks', function (done) { testHtmlPlugin({ entry: { util: path.join(__dirname, 'fixtures/util.js'), @@ -865,7 +865,7 @@ describe('HtmlWebpackPlugin', function () { /