├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── webpackdemo ├── .babelrc ├── package.json ├── src ├── a.css ├── a.js ├── a1.js ├── b.css ├── b.js ├── c.js ├── jquery.js ├── pageA.js └── pageB.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | 10 | [*.js] 11 | indent_size = 4 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | webpackdemo/build 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Huang ZhenYong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE:** Using this plugin with 'expose' plugin may cause problem. [2 modules gets the same ID with expose-loader · Issue #12 · zhenyong/webpack-stable-module-id-and-hash](https://github.com/zhenyong/webpack-stable-module-id-and-hash/issues/12) 2 | 3 | # WebpackStableModuleIdAndHash 4 | 5 | To provide stable module id and reliable content chunkhash in webpack 1.x, help u make long term cache easily. 6 | 7 | ## Usage 8 | 9 | 10 | ``` 11 | npm install webpack-stable-module-id-and-hash --save-dev 12 | 13 | ``` 14 | 15 | Then new a plugin in `plugins` webpack options, only use it in **`Production`** stage build,use OccurrenceOrderPlugin for `Development` is enough. 16 | 17 | ``` 18 | var WebpackStableModuleIdAndHash = require('webpack-stable-module-id-and-hash'); 19 | ... 20 | plugins: [ 21 | ... 22 | new WebpackStableModuleIdAndHash() 23 | ... 24 | ] 25 | ``` 26 | 27 | ## Why 28 | 29 | Here related discuss from github issue 30 | 31 | - [Vendor chunkhash changes when app code changes · Issue #1315 · webpack/webpack](https://github.com/webpack/webpack/issues/1315) 32 | 33 | ## Target 34 | 35 | - Every output `[chunkhash]` should be calcuated (md5) by its **dependencies module contents**. 36 | - Every `module ID` should be stable and **only if** changes because correspond module **file path (or content)** changes. 37 | 38 | ## How 39 | 40 | ### Chunkhash 41 | 42 | Like [webpack-md5-hash](https://github.com/erm0l0v/webpack-md5-hash),it calcuate chunkhash by dependen module content. 43 | 44 | But webpack-md5-hash has a shortcoming: 45 | 46 | Since module id is not stable, `webpack-md5-hash` sort modules by id may lead some unexpect output, e.p. `chunkhash` is not stable or same chunkhash for different content of output chunk(module id change). 47 | 48 | ### Stable Module Id 49 | 50 | Here some option or plugins support by webpack 1.x, like [OccurrenceOrderPlugin](http://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin), [`recordsPath`](http://webpack.github.io/docs/configuration.html#recordspath-recordsinputpath-recordsoutputpath), [DllPlugin & DllReferencePlugin 51 | ](http://webpack.github.io/docs/list-of-plugins.html#dllplugin),they all try to give stable module id,but can not 100% fix problem or require you to check in extra files. 52 | 53 | Webpack 2 may fix most part of them with [HashedModuleIdsPlugin](https://github.com/webpack/webpack/blob/master/lib/HashedModuleIdsPlugin.js) 54 | 55 | Like what HashedModuleIdsPlugin to do, juse one more thing that it converts the hash to num because webpack 1.x just accept num as module id. 56 | 57 | OMG!! Forgive my poor English. Just checkout the source code. 58 | 59 | ### Module ID collisions may cause builds to fail 60 | 61 | This plugin calculates a predictable hash values based on the module file 62 | names. A hashin algorithm based on MD5 is used to calculate the hash value. 63 | As with any hash value, collisions are rare, but possible. In the event of a 64 | collision, a `webpack-stable-module-id-and-hash module id collision` error is 65 | thrown during the Webpack build. 66 | 67 | In such a situation you can try to choose a different `seed` value to get 68 | different module IDs that may not collide. 69 | 70 | The probability of a collision depends on the `hashSize` option. By default 71 | 53 bits are used, which allow 9007199254740992 possible module IDs. This means 72 | that the probability of a collision is 0.0000000000000011102% multiplied by 73 | the number of modules in your project. 74 | 75 | ### chunk-hash length 76 | 77 | This plugin adds the values of `hashSize` and `seed` to the chunk-hash string. 78 | That's necessary because these settings influence the module ids contained 79 | within the chunks, but *not* the MD5 chunk hash itself. Therefore, the values 80 | must become part of the chunk-hash used in the filename to make it unique. 81 | 82 | Webpack by default shortens the resulting hash string to 20 characters, reducing 83 | the entropy of the MD5 hash. 84 | 85 | In practice, that shouldn't be an issue. But if you want to have a better 86 | hash then you can adjust the `output.hashDigestLength` Webpack setting to a 87 | value higher than 20. 88 | 89 | For example, to compensate the reduced entropy caused by 90 | `webpack-stable-module-id-and-hash` you might use a setting like this: 91 | 92 | ``` 93 | //... 94 | output: { 95 | //... 96 | hashDigestLength: 30 97 | } 98 | ``` 99 | 100 | ### Options 101 | 102 | The plugin acceps an object with these optional properties: 103 | 104 | - `hashSize` = Number of bits to use for the module ID. Defaults to the 105 | maximum, 53 bits. Large hash sizes greatly reduce the probability of a 106 | collision but lead also to very large module ID numbers for the generated 107 | code, which might *slightly* incrase Webpack chunk sizes. 108 | - `seed` = Any number between 0 and 31. Different "seed" values cause 109 | completely different module IDs. This is useful in the event of a collision. 110 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var crypto = require('crypto'); 4 | var pathUtil = require('path'); 5 | 6 | var md5Cache = {} 7 | 8 | function md5(content) { 9 | if (!md5Cache[content]) { 10 | md5Cache[content] = crypto.createHash('md5').update(content, 'utf-8').digest('hex') 11 | } 12 | return md5Cache[content]; 13 | } 14 | 15 | function compareMod(a, b) { 16 | return a.resource < b.resource ? -1 : a.resource > b.resource ? 1 : 0; 17 | } 18 | 19 | function getModSrc(module) { 20 | return module._source && module._source._value || ''; 21 | } 22 | 23 | function hashToModuleId(hash, seed, hashSize) { 24 | // Generate a unsigned integer sized bits using a part of the MD5 25 | // hash. Seed is a number 0..31 and the hash is expected to be 32 chars 26 | // (nibbles) long. 27 | 28 | // double the hash to allow overflow 29 | hash = hash + hash; 30 | 31 | // get lower and upper 28 bits 32 | var lsb = parseInt(hash.substr(seed, 7), 16); 33 | var msb = parseInt(hash.substr(seed + 7, 7), 16); 34 | 35 | // combine them to get the ID 36 | // NOTE: Logical operators only work up to 31 bits (because values will be 37 | // casted to 32bit signed integer), so we use classic arithmetic! 38 | var lsbBits = Math.min(28, hashSize); 39 | var msbBits = Math.max(0, hashSize - 28); 40 | var lsbMask = Math.pow(2, lsbBits) - 1; 41 | var msbMask = Math.pow(2, msbBits) - 1; 42 | return (lsb & lsbMask) + ((msb & msbMask) * Math.pow(2, 28)); 43 | } 44 | 45 | 46 | function WebpackStableModuleIdAndHash(options) { 47 | this.options = options || {}; 48 | } 49 | 50 | WebpackStableModuleIdAndHash.prototype.apply = function(compiler) { 51 | 52 | var usedIds = {}; 53 | var context = compiler.options.context; 54 | var seed = (+this.options.seed || 0) % 32; 55 | var hashSize = (+this.options.hashSize || 53); 56 | 57 | if (hashSize > 53) { 58 | // In JavaScript, only integers up to 2^53 (exclusive) can be considered 59 | // safe, see http://www.2ality.com/2013/10/safe-integers.html 60 | throw new Error("hashSize too large"); 61 | } 62 | 63 | function genModuleId(modulePath) { 64 | var hash = md5(modulePath); 65 | // generate a 28 bit integer using a part of the MD5 hash 66 | var id = hashToModuleId(hash, seed, hashSize); 67 | if (usedIds.hasOwnProperty(id) && usedIds[id] !== modulePath) 68 | throw new Error("webpack-stable-module-id-and-hash module id collision"); 69 | return id 70 | } 71 | 72 | // Generate module id by md5 value of file path. 73 | 74 | // Since webpack 1.x can not use a non-number as a module id, 75 | // convert the md5 (hex) to unique number. 76 | 77 | // spooned from https://github.com/webpack/webpack/blob/master/lib/HashedModuleIdsPlugin.js 78 | compiler.plugin("compilation", function(compilation) { 79 | compilation.plugin("before-module-ids", function(modules) { 80 | modules.forEach(function(module) { 81 | if (module.libIdent && module.id === null) { 82 | var modulePath = module.libIdent({ 83 | context: context 84 | }); 85 | module.id = genModuleId(modulePath); 86 | usedIds[module.id] = modulePath; 87 | } 88 | }); 89 | }); 90 | }); 91 | 92 | 93 | // spooned from https://github.com/erm0l0v/webpack-md5-hash 94 | compiler.plugin("compilation", function(compilation) { 95 | compilation.plugin("chunk-hash", function(chunk, chunkHash) { 96 | var source = chunk.modules.sort(compareMod).map(getModSrc).join(''); 97 | chunkHash.digest = function() { 98 | return seed + '-' + hashSize + '-' + md5(source); 99 | }; 100 | }); 101 | }); 102 | 103 | }; 104 | 105 | module.exports = WebpackStableModuleIdAndHash; 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-stable-module-id-and-hash", 3 | "version": "1.0.3", 4 | "description": "To provide stable module id and reliable content chunkhash", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zhenyong/webpack-stable-module-id-and-hash.git" 12 | }, 13 | "author": "zhenyong <116982086@qq.com>", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/zhenyong/webpack-stable-module-id-and-hash/issues" 17 | }, 18 | "homepage": "https://github.com/zhenyong/webpack-stable-module-id-and-hash#readme" 19 | } 20 | -------------------------------------------------------------------------------- /webpackdemo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /webpackdemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpackdemo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "0.js", 6 | "dependencies": { 7 | "css-loader": "^0.21.0", 8 | "decamelize": "^1.1.1", 9 | "icss-replace-symbols": "^1.0.2", 10 | "defined": "^1.0.0", 11 | "postcss-svgo": "^2.0.4", 12 | "postcss-reduce-transforms": "^1.0.3", 13 | "postcss-discard-comments": "^2.0.2", 14 | "style-loader": "^0.13.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.0.0", 18 | "babel-eslint": "^7.0.0", 19 | "babel-loader": "^6.0.0", 20 | "babel-plugin-add-module-exports": "^0.2.1", 21 | "babel-plugin-component": "^0.4.3", 22 | "babel-plugin-lodash": "^3.2.9", 23 | "babel-plugin-transform-runtime": "^6.0.0", 24 | "babel-preset-es2015": "^6.0.0", 25 | "babel-preset-stage-2": "^6.0.0", 26 | "babel-register": "^6.0.0", 27 | "extract-text-webpack-plugin": "^1.0.1", 28 | "webpack": "^1.13.3" 29 | }, 30 | "scripts": { 31 | "build": "rm -rf build/** && webpack", 32 | "test": "echo \"Error: no test specified\" && exit 1" 33 | }, 34 | "author": "", 35 | "license": "ISC" 36 | } 37 | -------------------------------------------------------------------------------- /webpackdemo/src/a.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: '#AAA'; 3 | } -------------------------------------------------------------------------------- /webpackdemo/src/a.js: -------------------------------------------------------------------------------- 1 | console.log('aaaaaaa') -------------------------------------------------------------------------------- /webpackdemo/src/a1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenyong/webpack-stable-module-id-and-hash/88153bb8e38e38a6e13153c8f50c4f3eec0a7cd2/webpackdemo/src/a1.js -------------------------------------------------------------------------------- /webpackdemo/src/b.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: '#BBB'; 3 | } -------------------------------------------------------------------------------- /webpackdemo/src/b.js: -------------------------------------------------------------------------------- 1 | console.log('bbbbbb') -------------------------------------------------------------------------------- /webpackdemo/src/c.js: -------------------------------------------------------------------------------- 1 | console.log('ccccc') -------------------------------------------------------------------------------- /webpackdemo/src/jquery.js: -------------------------------------------------------------------------------- 1 | module.exports ='this is jquery jquery jquery'; -------------------------------------------------------------------------------- /webpackdemo/src/pageA.js: -------------------------------------------------------------------------------- 1 | // 移除对 a.js 的依赖 2 | // require('./a.js') // a.js 3 | require('./b.js') // b.js 4 | var a = 'this is pageA'; -------------------------------------------------------------------------------- /webpackdemo/src/pageB.js: -------------------------------------------------------------------------------- 1 | require('./b.js') // b.js' 2 | require('./c.js') // c.js 3 | var b = 'this is pageB'; -------------------------------------------------------------------------------- /webpackdemo/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 3 | var StableModuleIdAndHash = require('../index'); 4 | 5 | module.exports = { 6 | entry: { 7 | pageA: './src/pageA.js', 8 | pageB: './src/pageB.js', 9 | }, 10 | output: { 11 | path: __dirname + '/build', 12 | filename: '[name].[chunkhash:4].js', 13 | chunkFilename: "[id].[chunkhash:4].bundle.js", 14 | }, 15 | module: { 16 | loaders: [{ 17 | test: /\.css$/, 18 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader') 19 | }], 20 | xloaders: [{ 21 | test: /\.css$/, 22 | loader: 'style!css', 23 | }] 24 | }, 25 | plugins: [ 26 | new webpack.optimize.CommonsChunkPlugin({ 27 | name: "commons", 28 | minChunks: 2, 29 | chunks: ["pageA", "pageB"], 30 | }), 31 | 32 | // new webpack.optimize.CommonsChunkPlugin({ 33 | // name: "manifestA", 34 | // chunks:['pageA'] 35 | // }), 36 | 37 | // new webpack.optimize.CommonsChunkPlugin({ 38 | // name: "manifestB", 39 | // chunks:['pageB'] 40 | // }), 41 | new StableModuleIdAndHash(), 42 | new ExtractTextPlugin('[name].[contenthash:4].css'), 43 | ], 44 | }; 45 | --------------------------------------------------------------------------------