├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── example ├── index.html ├── main.js └── test.css ├── index.js ├── index.node6-compatible.js ├── index.test.js ├── jest.config.js ├── jest.init.js ├── package.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | [ 7 | "@babel/plugin-transform-runtime", 8 | { 9 | "regenerator": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "jest" 4 | ], 5 | "parser": "babel-eslint", 6 | "env": { 7 | "node": true, 8 | "jest": true, 9 | "es6": true 10 | }, 11 | "extends": "airbnb-base" 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .idea 5 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mark Shapiro 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 | # MERGE INTO SINGLE FILE PLUGIN FOR WEBPACK 2 | 3 | Webpack plugin to merge your source files together into single file, to be included in index.html, and achieving same effect as you would by including them all separately through ` 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | because your `node_modules` is not available in production. 25 |
with this plugin you can achieve the desired effect this way: 26 | ```javascript 27 | 28 | const MergeIntoSingleFilePlugin = require('webpack-merge-and-include-globally'); 29 | 30 | module.exports = { 31 | ... 32 | plugins: [ 33 | new MergeIntoSingleFilePlugin({ 34 | files: { 35 | "vendor.js": [ 36 | 'node_modules/jquery/dist/jquery.min.js', 37 | // will work too 38 | // 'node_modules/jquery/**/*.min.js', 39 | 'node_modules/moment/moment.js', 40 | 'node_modules/moment/locale/cs.js', 41 | 'node_modules/moment/locale/de.js', 42 | 'node_modules/moment/locale/nl.js', 43 | 'node_modules/toastr/build/toastr.min.js' 44 | ], 45 | "vendor.css": [ 46 | 'node_modules/toastr/build/toastr.min.css' 47 | ] 48 | } 49 | }), 50 | ] 51 | 52 | ``` 53 | this generates 2 files with merged js and css content, include them into your `index.html` to take effect: 54 | ``` html 55 | 56 | 57 | ``` 58 | now `jQuery`, `moment` and `toastr` are available globally throughout your application. 59 | 60 | ### Options 61 | 62 | #### files (as object) 63 | 64 | Object that maps file names to array of all files (can also be defined by wildcard path) that will be merged together and saved under each file name. 65 |
For example to merge `jquery`, `classnames` and `humps` into `vendor.js`, do: 66 | ```javascript 67 | new MergeIntoSingle({ 68 | files: { 69 | 'vendor.js': [ 70 | 'node_modules/jquery/**/*.min.js', 71 | 'node_modules/classnames/index.js', 72 | 'node_modules/humps/humps.js' 73 | ], 74 | 'style.css': [ 75 | 'example/test.css' 76 | ] 77 | } 78 | }) 79 | ``` 80 | 81 | #### transform 82 | 83 | Object that maps resulting file names to tranform methods that will be applied on merged content before saving. Use to minify / uglify the result. 84 |
For example to minify the final merge result of `vendor.js`, do: 85 | ```javascript 86 | new MergeIntoSingle({ 87 | files: { 'vendor.js': [...] }, 88 | transform: { 89 | 'vendor.js': code => require("uglify-js").minify(code).code 90 | } 91 | }) 92 | ``` 93 | 94 | #### files (as array) 95 | 96 | Alternative way to specify files as array of `src` & `dest`, for flexibility to transform and create multiple destination files for same source when you need to generate additional map file for example. 97 | ```javascript 98 | new MergeIntoSingle({ 99 | files: [{ 100 | src:[ 101 | 'node_modules/jquery/**/*.min.js', 102 | 'node_modules/classnames/index.js', 103 | 'node_modules/humps/humps.js' 104 | ], 105 | dest: code => { 106 | const min = uglifyJS.minify(code, {sourceMap: { 107 | filename: 'vendor.js', 108 | url: 'vendor.js.map' 109 | }}); 110 | return { 111 | 'vendor.js':min.code, 112 | 'vendor.js.map': min.map 113 | } 114 | }, 115 | 116 | // also possible: 117 | // 118 | // dest: 'vendor.js' 119 | },{ 120 | src: ['example/test.css'], 121 | dest: 'style.css' 122 | 123 | // also possible: 124 | // 125 | // dest: code => ({ 126 | // 'style.css':new CleanCSS({}).minify(code).styles 127 | // }) 128 | }] 129 | }) 130 | ``` 131 | 132 | #### hash 133 | default: false 134 | 135 | set `true` to append version hash before file extension. 136 | 137 | you can get names of generated files mapped to original by passing callback function as second argument to plugin: 138 | ```javascript 139 | new MergeIntoSingle({ ... }, filesMap => { ... }), 140 | ``` 141 | 142 | #### transformFileName 143 | default: undefined 144 | 145 | also you can pass function for change output file name with hash 146 | ```javascript 147 | new MergeIntoSingle({ 148 | ..., 149 | transformFileName: (fileNameBase, extension, hash) => `${fileName}.[${hash}]${extension}`, 150 | // bundle.[somehash].js 151 | }), 152 | 153 | //or 154 | 155 | new MergeIntoSingle({ 156 | ..., 157 | transformFileName: (fileNameBase, extension, hash) => `${fileNameBase}${extension}?hash=${hash}`, 158 | // bundle.js?hash=somehash 159 | }), 160 | 161 | ``` 162 | 163 | #### encoding 164 | 165 | default: 'utf-8' 166 | 167 | encoding of node.js reading 168 | 169 | #### chunks 170 | 171 | default: undefined 172 | 173 | array of entry points (strings) for which this plugin should run only 174 | 175 | #### separator 176 | 177 | default: '\n' 178 | 179 | string used between files when joining them together 180 | 181 | ### Working Example 182 | 183 | working example already included in project. 184 |
to test first install `npm i`, then run `npm run start` to see it in action 185 |
and `npm run build` to build prod files with vendor file and `index.html`. 186 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | document.write( jQuery ) = 14 |
15 | 16 |
17 | document.write( humps ) = 18 |
19 | 20 |
21 | document.write( classNames ) = 22 |
23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /example/test.css: -------------------------------------------------------------------------------- 1 | h1, h2, p { 2 | text-align: center; 3 | color: red; 4 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const glob = require('glob'); 3 | const { promisify } = require('es6-promisify'); 4 | const revHash = require('rev-hash'); 5 | const { sources, Compilation } = require('webpack'); 6 | 7 | const plugin = { name: 'MergeIntoFile' }; 8 | 9 | const readFile = promisify(fs.readFile); 10 | const listFiles = promisify(glob); 11 | 12 | const joinContent = async (promises, separator) => promises 13 | .reduce(async (acc, curr) => `${await acc}${(await acc).length ? separator : ''}${await curr}`, ''); 14 | 15 | class MergeIntoFile { 16 | constructor(options, onComplete) { 17 | this.options = options; 18 | this.onComplete = onComplete; 19 | } 20 | 21 | apply(compiler) { 22 | if (compiler.hooks) { 23 | let emitHookSet = false; 24 | compiler.hooks.thisCompilation.tap( 25 | plugin.name, 26 | (compilation) => { 27 | if (compilation.hooks.processAssets) { 28 | compilation.hooks.processAssets.tapAsync( 29 | { 30 | name: plugin.name, 31 | stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, 32 | }, 33 | (_, callback) => this.run(compilation, callback), 34 | ); 35 | } else if (!emitHookSet) { 36 | emitHookSet = true; 37 | compiler.hooks.emit.tapAsync(plugin.name, this.run.bind(this)); 38 | } 39 | }, 40 | ); 41 | } else { 42 | compiler.plugin('emit', this.run.bind(this)); 43 | } 44 | } 45 | 46 | static getHashOfRelatedFile(assets, fileName) { 47 | let hashPart = null; 48 | Object.keys(assets).forEach((existingFileName) => { 49 | const match = existingFileName.match(/-([0-9a-f]+)(\.min)?(\.\w+)(\.map)?$/); 50 | const fileHashPart = match && match.length && match[1]; 51 | if (fileHashPart) { 52 | const canonicalFileName = existingFileName.replace(`-${fileHashPart}`, '').replace(/\.map$/, ''); 53 | if (canonicalFileName === fileName.replace(/\.map$/, '')) { 54 | hashPart = fileHashPart; 55 | } 56 | } 57 | }); 58 | return hashPart; 59 | } 60 | 61 | run(compilation, callback) { 62 | const { 63 | files, 64 | transform, 65 | encoding, 66 | chunks, 67 | hash, 68 | transformFileName, 69 | } = this.options; 70 | if (chunks && compilation.chunks && compilation.chunks 71 | .filter((chunk) => chunks.indexOf(chunk.name) >= 0 && chunk.rendered).length === 0) { 72 | if (typeof (callback) === 'function') { 73 | callback(); 74 | } 75 | return; 76 | } 77 | const generatedFiles = {}; 78 | let filesCanonical = []; 79 | if (!Array.isArray(files)) { 80 | Object.keys(files).forEach((newFile) => { 81 | filesCanonical.push({ 82 | src: files[newFile], 83 | dest: newFile, 84 | }); 85 | }); 86 | } else { 87 | filesCanonical = files; 88 | } 89 | filesCanonical.forEach((fileTransform) => { 90 | if (typeof fileTransform.dest === 'string') { 91 | const destFileName = fileTransform.dest; 92 | fileTransform.dest = (code) => ({ // eslint-disable-line no-param-reassign 93 | [destFileName]: (transform && transform[destFileName]) 94 | ? transform[destFileName](code) 95 | : code, 96 | }); 97 | } 98 | }); 99 | const finalPromises = filesCanonical.map(async (fileTransform) => { 100 | const { separator = '\n' } = this.options; 101 | const listOfLists = await Promise.all(fileTransform.src.map((path) => listFiles(path, null))); 102 | const flattenedList = Array.prototype.concat.apply([], listOfLists); 103 | const filesContentPromises = flattenedList.map((path) => readFile(path, encoding || 'utf-8')); 104 | const content = await joinContent(filesContentPromises, separator); 105 | const resultsFiles = await fileTransform.dest(content); 106 | // eslint-disable-next-line no-restricted-syntax 107 | for (const resultsFile in resultsFiles) { 108 | if (typeof resultsFiles[resultsFile] === 'object') { 109 | // eslint-disable-next-line no-await-in-loop 110 | resultsFiles[resultsFile] = await resultsFiles[resultsFile]; 111 | } 112 | } 113 | Object.keys(resultsFiles).forEach((newFileName) => { 114 | let newFileNameHashed = newFileName; 115 | const hasTransformFileNameFn = typeof transformFileName === 'function'; 116 | 117 | if (hash || hasTransformFileNameFn) { 118 | const hashPart = MergeIntoFile.getHashOfRelatedFile(compilation.assets, newFileName) 119 | || revHash(resultsFiles[newFileName]); 120 | 121 | if (hasTransformFileNameFn) { 122 | const extensionPattern = /\.[^.]*$/g; 123 | const fileNameBase = newFileName.replace(extensionPattern, ''); 124 | const [extension] = newFileName.match(extensionPattern); 125 | 126 | newFileNameHashed = transformFileName(fileNameBase, extension, hashPart); 127 | } else { 128 | newFileNameHashed = newFileName.replace(/(\.min)?\.\w+(\.map)?$/, (suffix) => `-${hashPart}${suffix}`); 129 | } 130 | 131 | const fileId = newFileName.replace(/\.map$/, '').replace(/\.\w+$/, ''); 132 | 133 | if (typeof compilation.addChunk === 'function') { 134 | const chunk = compilation.addChunk(fileId); 135 | chunk.id = fileId; 136 | chunk.ids = [chunk.id]; 137 | chunk.files.push(newFileNameHashed); 138 | } 139 | } 140 | generatedFiles[newFileName] = newFileNameHashed; 141 | 142 | let rawSource; 143 | if (sources && sources.RawSource) { 144 | rawSource = new sources.RawSource(resultsFiles[newFileName]); 145 | } else { 146 | rawSource = { 147 | source() { 148 | return resultsFiles[newFileName]; 149 | }, 150 | size() { 151 | return resultsFiles[newFileName].length; 152 | }, 153 | }; 154 | } 155 | 156 | if (compilation.emitAsset) { 157 | compilation.emitAsset(newFileNameHashed, rawSource); 158 | } else { 159 | // eslint-disable-next-line no-param-reassign 160 | compilation.assets[newFileNameHashed] = rawSource; 161 | } 162 | }); 163 | }); 164 | 165 | Promise.all(finalPromises) 166 | .then(() => { 167 | if (this.onComplete) { 168 | this.onComplete(generatedFiles); 169 | } 170 | if (typeof (callback) === 'function') { 171 | callback(); 172 | } 173 | }) 174 | .catch((error) => { 175 | if (typeof (callback) === 'function') { 176 | callback(error); 177 | } else { 178 | throw new Error(error); 179 | } 180 | }); 181 | } 182 | } 183 | 184 | module.exports = MergeIntoFile; 185 | -------------------------------------------------------------------------------- /index.node6-compatible.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); 6 | 7 | var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); 8 | 9 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); 10 | 11 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 12 | 13 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); 14 | 15 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 16 | 17 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 18 | 19 | var fs = require('fs'); 20 | 21 | var glob = require('glob'); 22 | 23 | var _require = require('es6-promisify'), 24 | promisify = _require.promisify; 25 | 26 | var revHash = require('rev-hash'); 27 | 28 | var _require2 = require('webpack'), 29 | sources = _require2.sources, 30 | Compilation = _require2.Compilation; 31 | 32 | var plugin = { 33 | name: 'MergeIntoFile' 34 | }; 35 | var readFile = promisify(fs.readFile); 36 | var listFiles = promisify(glob); 37 | 38 | var joinContent = /*#__PURE__*/function () { 39 | var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(promises, separator) { 40 | return _regenerator["default"].wrap(function _callee2$(_context2) { 41 | while (1) { 42 | switch (_context2.prev = _context2.next) { 43 | case 0: 44 | return _context2.abrupt("return", promises.reduce( /*#__PURE__*/function () { 45 | var _ref2 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(acc, curr) { 46 | return _regenerator["default"].wrap(function _callee$(_context) { 47 | while (1) { 48 | switch (_context.prev = _context.next) { 49 | case 0: 50 | _context.t2 = ""; 51 | _context.next = 3; 52 | return acc; 53 | 54 | case 3: 55 | _context.t3 = _context.sent; 56 | _context.t1 = _context.t2.concat.call(_context.t2, _context.t3); 57 | _context.next = 7; 58 | return acc; 59 | 60 | case 7: 61 | if (!_context.sent.length) { 62 | _context.next = 11; 63 | break; 64 | } 65 | 66 | _context.t4 = separator; 67 | _context.next = 12; 68 | break; 69 | 70 | case 11: 71 | _context.t4 = ''; 72 | 73 | case 12: 74 | _context.t5 = _context.t4; 75 | _context.t0 = _context.t1.concat.call(_context.t1, _context.t5); 76 | _context.next = 16; 77 | return curr; 78 | 79 | case 16: 80 | _context.t6 = _context.sent; 81 | return _context.abrupt("return", _context.t0.concat.call(_context.t0, _context.t6)); 82 | 83 | case 18: 84 | case "end": 85 | return _context.stop(); 86 | } 87 | } 88 | }, _callee); 89 | })); 90 | 91 | return function (_x3, _x4) { 92 | return _ref2.apply(this, arguments); 93 | }; 94 | }(), '')); 95 | 96 | case 1: 97 | case "end": 98 | return _context2.stop(); 99 | } 100 | } 101 | }, _callee2); 102 | })); 103 | 104 | return function joinContent(_x, _x2) { 105 | return _ref.apply(this, arguments); 106 | }; 107 | }(); 108 | 109 | var MergeIntoFile = /*#__PURE__*/function () { 110 | function MergeIntoFile(options, onComplete) { 111 | (0, _classCallCheck2["default"])(this, MergeIntoFile); 112 | this.options = options; 113 | this.onComplete = onComplete; 114 | } 115 | 116 | (0, _createClass2["default"])(MergeIntoFile, [{ 117 | key: "apply", 118 | value: function apply(compiler) { 119 | var _this = this; 120 | 121 | if (compiler.hooks) { 122 | var emitHookSet = false; 123 | compiler.hooks.thisCompilation.tap(plugin.name, function (compilation) { 124 | if (compilation.hooks.processAssets) { 125 | compilation.hooks.processAssets.tapAsync({ 126 | name: plugin.name, 127 | stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL 128 | }, function (_, callback) { 129 | return _this.run(compilation, callback); 130 | }); 131 | } else if (!emitHookSet) { 132 | emitHookSet = true; 133 | compiler.hooks.emit.tapAsync(plugin.name, _this.run.bind(_this)); 134 | } 135 | }); 136 | } else { 137 | compiler.plugin('emit', this.run.bind(this)); 138 | } 139 | } 140 | }, { 141 | key: "run", 142 | value: function run(compilation, callback) { 143 | var _this2 = this; 144 | 145 | var _this$options = this.options, 146 | files = _this$options.files, 147 | transform = _this$options.transform, 148 | encoding = _this$options.encoding, 149 | chunks = _this$options.chunks, 150 | hash = _this$options.hash, 151 | transformFileName = _this$options.transformFileName; 152 | 153 | if (chunks && compilation.chunks && compilation.chunks.filter(function (chunk) { 154 | return chunks.indexOf(chunk.name) >= 0 && chunk.rendered; 155 | }).length === 0) { 156 | if (typeof callback === 'function') { 157 | callback(); 158 | } 159 | 160 | return; 161 | } 162 | 163 | var generatedFiles = {}; 164 | var filesCanonical = []; 165 | 166 | if (!Array.isArray(files)) { 167 | Object.keys(files).forEach(function (newFile) { 168 | filesCanonical.push({ 169 | src: files[newFile], 170 | dest: newFile 171 | }); 172 | }); 173 | } else { 174 | filesCanonical = files; 175 | } 176 | 177 | filesCanonical.forEach(function (fileTransform) { 178 | if (typeof fileTransform.dest === 'string') { 179 | var destFileName = fileTransform.dest; 180 | 181 | fileTransform.dest = function (code) { 182 | return (0, _defineProperty2["default"])({}, destFileName, transform && transform[destFileName] ? transform[destFileName](code) : code); 183 | }; 184 | } 185 | }); 186 | var finalPromises = filesCanonical.map( /*#__PURE__*/function () { 187 | var _ref4 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3(fileTransform) { 188 | var _this2$options$separa, separator, listOfLists, flattenedList, filesContentPromises, content, resultsFiles, resultsFile; 189 | 190 | return _regenerator["default"].wrap(function _callee3$(_context3) { 191 | while (1) { 192 | switch (_context3.prev = _context3.next) { 193 | case 0: 194 | _this2$options$separa = _this2.options.separator, separator = _this2$options$separa === void 0 ? '\n' : _this2$options$separa; 195 | _context3.next = 3; 196 | return Promise.all(fileTransform.src.map(function (path) { 197 | return listFiles(path, null); 198 | })); 199 | 200 | case 3: 201 | listOfLists = _context3.sent; 202 | flattenedList = Array.prototype.concat.apply([], listOfLists); 203 | filesContentPromises = flattenedList.map(function (path) { 204 | return readFile(path, encoding || 'utf-8'); 205 | }); 206 | _context3.next = 8; 207 | return joinContent(filesContentPromises, separator); 208 | 209 | case 8: 210 | content = _context3.sent; 211 | _context3.next = 11; 212 | return fileTransform.dest(content); 213 | 214 | case 11: 215 | resultsFiles = _context3.sent; 216 | _context3.t0 = _regenerator["default"].keys(resultsFiles); 217 | 218 | case 13: 219 | if ((_context3.t1 = _context3.t0()).done) { 220 | _context3.next = 21; 221 | break; 222 | } 223 | 224 | resultsFile = _context3.t1.value; 225 | 226 | if (!((0, _typeof2["default"])(resultsFiles[resultsFile]) === 'object')) { 227 | _context3.next = 19; 228 | break; 229 | } 230 | 231 | _context3.next = 18; 232 | return resultsFiles[resultsFile]; 233 | 234 | case 18: 235 | resultsFiles[resultsFile] = _context3.sent; 236 | 237 | case 19: 238 | _context3.next = 13; 239 | break; 240 | 241 | case 21: 242 | Object.keys(resultsFiles).forEach(function (newFileName) { 243 | var newFileNameHashed = newFileName; 244 | var hasTransformFileNameFn = typeof transformFileName === 'function'; 245 | 246 | if (hash || hasTransformFileNameFn) { 247 | var hashPart = MergeIntoFile.getHashOfRelatedFile(compilation.assets, newFileName) || revHash(resultsFiles[newFileName]); 248 | 249 | if (hasTransformFileNameFn) { 250 | var extensionPattern = /\.[^.]*$/g; 251 | var fileNameBase = newFileName.replace(extensionPattern, ''); 252 | 253 | var _newFileName$match = newFileName.match(extensionPattern), 254 | _newFileName$match2 = (0, _slicedToArray2["default"])(_newFileName$match, 1), 255 | extension = _newFileName$match2[0]; 256 | 257 | newFileNameHashed = transformFileName(fileNameBase, extension, hashPart); 258 | } else { 259 | newFileNameHashed = newFileName.replace(/(\.min)?\.\w+(\.map)?$/, function (suffix) { 260 | return "-".concat(hashPart).concat(suffix); 261 | }); 262 | } 263 | 264 | var fileId = newFileName.replace(/\.map$/, '').replace(/\.\w+$/, ''); 265 | 266 | if (typeof compilation.addChunk === 'function') { 267 | var chunk = compilation.addChunk(fileId); 268 | chunk.id = fileId; 269 | chunk.ids = [chunk.id]; 270 | chunk.files.push(newFileNameHashed); 271 | } 272 | } 273 | 274 | generatedFiles[newFileName] = newFileNameHashed; 275 | var rawSource; 276 | 277 | if (sources && sources.RawSource) { 278 | rawSource = new sources.RawSource(resultsFiles[newFileName]); 279 | } else { 280 | rawSource = { 281 | source: function source() { 282 | return resultsFiles[newFileName]; 283 | }, 284 | size: function size() { 285 | return resultsFiles[newFileName].length; 286 | } 287 | }; 288 | } 289 | 290 | if (compilation.emitAsset) { 291 | compilation.emitAsset(newFileNameHashed, rawSource); 292 | } else { 293 | // eslint-disable-next-line no-param-reassign 294 | compilation.assets[newFileNameHashed] = rawSource; 295 | } 296 | }); 297 | 298 | case 22: 299 | case "end": 300 | return _context3.stop(); 301 | } 302 | } 303 | }, _callee3); 304 | })); 305 | 306 | return function (_x5) { 307 | return _ref4.apply(this, arguments); 308 | }; 309 | }()); 310 | Promise.all(finalPromises).then(function () { 311 | if (_this2.onComplete) { 312 | _this2.onComplete(generatedFiles); 313 | } 314 | 315 | if (typeof callback === 'function') { 316 | callback(); 317 | } 318 | })["catch"](function (error) { 319 | if (typeof callback === 'function') { 320 | callback(error); 321 | } else { 322 | throw new Error(error); 323 | } 324 | }); 325 | } 326 | }], [{ 327 | key: "getHashOfRelatedFile", 328 | value: function getHashOfRelatedFile(assets, fileName) { 329 | var hashPart = null; 330 | Object.keys(assets).forEach(function (existingFileName) { 331 | var match = existingFileName.match(/-([0-9a-f]+)(\.min)?(\.\w+)(\.map)?$/); 332 | var fileHashPart = match && match.length && match[1]; 333 | 334 | if (fileHashPart) { 335 | var canonicalFileName = existingFileName.replace("-".concat(fileHashPart), '').replace(/\.map$/, ''); 336 | 337 | if (canonicalFileName === fileName.replace(/\.map$/, '')) { 338 | hashPart = fileHashPart; 339 | } 340 | } 341 | }); 342 | return hashPart; 343 | } 344 | }]); 345 | return MergeIntoFile; 346 | }(); 347 | 348 | module.exports = MergeIntoFile; 349 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('fs'); 2 | jest.mock('glob'); 3 | 4 | const fs = require('fs'); 5 | const glob = require('glob'); 6 | 7 | const MergeIntoSingle = require('./index.node6-compatible.js'); 8 | // const MergeIntoSingle = require('./index.js'); 9 | 10 | describe('MergeIntoFile', () => { 11 | const pathToFiles = { 12 | 'file1.js': ['1.js'], 13 | 'file2.js': ['2.js'], 14 | '*.css': ['3.css', '4.css'], 15 | }; 16 | 17 | const fileToContent = { 18 | '1.js': 'FILE_1_TEXT', 19 | '2.js': 'FILE_2_TEXT', 20 | '3.css': 'FILE_3_TEXT', 21 | '4.css': 'FILE_4_TEXT', 22 | }; 23 | 24 | fs.readFile.mockImplementation((fileName, options, cb) => cb(null, fileToContent[fileName])); 25 | glob.mockImplementation((path, options, cb) => cb(null, pathToFiles[path])); 26 | 27 | it('should succeed merging using mock content', (done) => { 28 | const instance = new MergeIntoSingle({ 29 | files: { 30 | 'script.js': [ 31 | 'file1.js', 32 | 'file2.js', 33 | ], 34 | 'style.css': [ 35 | '*.css', 36 | ], 37 | }, 38 | }); 39 | instance.apply({ 40 | plugin: (event, fun) => { 41 | const obj = { 42 | assets: {}, 43 | }; 44 | fun(obj, (err) => { 45 | expect(err).toEqual(undefined); 46 | expect(obj.assets['script.js'].source()).toEqual('FILE_1_TEXT\nFILE_2_TEXT'); 47 | expect(obj.assets['style.css'].source()).toEqual('FILE_3_TEXT\nFILE_4_TEXT'); 48 | done(); 49 | }); 50 | }, 51 | }); 52 | }); 53 | 54 | it('should succeed merging using mock content with a custom separator', (done) => { 55 | const instance = new MergeIntoSingle({ 56 | separator: '\n;\n', 57 | files: { 58 | 'script.js': [ 59 | 'file1.js', 60 | 'file2.js', 61 | ], 62 | }, 63 | }); 64 | instance.apply({ 65 | plugin: (event, fun) => { 66 | const obj = { 67 | assets: {}, 68 | }; 69 | fun(obj, (err) => { 70 | expect(err).toEqual(undefined); 71 | expect(obj.assets['script.js'].source()).toEqual('FILE_1_TEXT\n;\nFILE_2_TEXT'); 72 | done(); 73 | }); 74 | }, 75 | }); 76 | }); 77 | 78 | it('should succeed merging using mock content with transform', (done) => { 79 | const instance = new MergeIntoSingle({ 80 | files: { 81 | 'script.js': [ 82 | 'file1.js', 83 | 'file2.js', 84 | ], 85 | 'style.css': [ 86 | '*.css', 87 | ], 88 | }, 89 | transform: { 90 | 'script.js': (val) => `${val.toLowerCase()}`, 91 | }, 92 | }); 93 | instance.apply({ 94 | plugin: (event, fun) => { 95 | const obj = { 96 | assets: {}, 97 | }; 98 | fun(obj, (err) => { 99 | expect(err).toEqual(undefined); 100 | expect(obj.assets['script.js'].source()).toEqual('file_1_text\nfile_2_text'); 101 | expect(obj.assets['style.css'].source()).toEqual('FILE_3_TEXT\nFILE_4_TEXT'); 102 | done(); 103 | }); 104 | }, 105 | }); 106 | }); 107 | 108 | it('should succeed merging using mock content with async transform', (done) => { 109 | const instance = new MergeIntoSingle({ 110 | files: { 111 | 'script.js': [ 112 | 'file1.js', 113 | 'file2.js', 114 | ], 115 | 'style.css': [ 116 | '*.css', 117 | ], 118 | }, 119 | transform: { 120 | 'script.js': async (val) => `${val.toLowerCase()}`, 121 | }, 122 | }); 123 | instance.apply({ 124 | plugin: (event, fun) => { 125 | const obj = { 126 | assets: {}, 127 | }; 128 | fun(obj, (err) => { 129 | expect(err).toEqual(undefined); 130 | expect(obj.assets['script.js'].source()).toEqual('file_1_text\nfile_2_text'); 131 | expect(obj.assets['style.css'].source()).toEqual('FILE_3_TEXT\nFILE_4_TEXT'); 132 | done(); 133 | }); 134 | }, 135 | }); 136 | }); 137 | 138 | it('should succeed merging using mock content by using array instead of object', (done) => { 139 | const instance = new MergeIntoSingle({ 140 | files: [ 141 | { 142 | src: ['file1.js', 'file2.js'], 143 | dest: (val) => ({ 144 | 'script.js': `${val.toLowerCase()}`, 145 | }), 146 | }, 147 | { 148 | src: ['*.css'], 149 | dest: 'style.css', 150 | }, 151 | ], 152 | }); 153 | instance.apply({ 154 | plugin: (event, fun) => { 155 | const obj = { 156 | assets: {}, 157 | }; 158 | fun(obj, (err) => { 159 | expect(err).toEqual(undefined); 160 | expect(obj.assets['script.js'].source()).toEqual('file_1_text\nfile_2_text'); 161 | expect(obj.assets['style.css'].source()).toEqual('FILE_3_TEXT\nFILE_4_TEXT'); 162 | done(); 163 | }); 164 | }, 165 | }); 166 | }); 167 | 168 | it('should succeed merging using transform file name function', (done) => { 169 | const mockHash = 'xyz'; 170 | const instance = new MergeIntoSingle({ 171 | files: { 172 | 'script.js': [ 173 | 'file1.js', 174 | 'file2.js', 175 | ], 176 | 'other.deps.js': [ 177 | 'file1.js', 178 | ], 179 | 'style.css': [ 180 | '*.css', 181 | ], 182 | }, 183 | transformFileName: (fileNameBase, extension) => `${fileNameBase}${extension}?hash=${mockHash}`, 184 | }); 185 | instance.apply({ 186 | plugin: (event, fun) => { 187 | const obj = { 188 | assets: {}, 189 | }; 190 | fun(obj, (err) => { 191 | expect(err).toEqual(undefined); 192 | expect(obj.assets[`script.js?hash=${mockHash}`]).toBeDefined(); 193 | expect(obj.assets[`other.deps.js?hash=${mockHash}`]).toBeDefined(); 194 | expect(obj.assets[`style.css?hash=${mockHash}`]).toBeDefined(); 195 | done(); 196 | }); 197 | }, 198 | }); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markshapiro/webpack-merge-and-include-globally/2bb5182ff950dd4081ed1e2707a2697438e9c402/jest.config.js -------------------------------------------------------------------------------- /jest.init.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | setupTestFrameworkScriptFile: './jest.init.js', 4 | roots: [], 5 | moduleDirectories: ['node_modules'], 6 | testEnvironment: 'node', 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-merge-and-include-globally", 3 | "version": "2.3.4", 4 | "description": "Merge multiple files (js,css..) into single file to include somewhere", 5 | "main": "index.node6-compatible.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "webpack --progress && cp example/index.html dist/index.html", 9 | "start": "webpack serve", 10 | "lint": "eslint index.js index.test.js webpack.config.js --fix", 11 | "build-src": "babel index.js --out-file index.node6-compatible.js" 12 | }, 13 | "contributors": [], 14 | "devDependencies": { 15 | "@babel/cli": "^7.12.10", 16 | "@babel/core": "^7.12.10", 17 | "@babel/plugin-transform-runtime": "7.12.10", 18 | "@babel/preset-env": "^7.12.11", 19 | "babel-core": "^7.0.0-bridge.0", 20 | "babel-eslint": "^10.1.0", 21 | "babel-jest": "^26.6.3", 22 | "babel-loader": "^8.2.2", 23 | "classnames": "^2.2.6", 24 | "clean-css": "^4.2.3", 25 | "eslint": "^7.16.0", 26 | "eslint-config-airbnb-base": "^14.2.1", 27 | "eslint-plugin-import": "^2.22.1", 28 | "eslint-plugin-jest": "^24.1.3", 29 | "humps": "^2.0.1", 30 | "install": "^0.13.0", 31 | "jest": "^26.6.3", 32 | "jquery": "^3.5.1", 33 | "raw-loader": "^4.0.2", 34 | "uglify-js": "^3.12.3", 35 | "webpack": "^5.11.1", 36 | "webpack-cli": "4.3.0", 37 | "webpack-dev-server": "^3.11.1" 38 | }, 39 | "peerDependencies": { 40 | "webpack": ">=1.0.0" 41 | }, 42 | "dependencies": { 43 | "es6-promisify": "^6.1.1", 44 | "glob": "^7.1.6", 45 | "rev-hash": "^3.0.0" 46 | }, 47 | "keywords": [ 48 | "webpack", 49 | "global", 50 | "js", 51 | "merge", 52 | "concat", 53 | "global library", 54 | "expose" 55 | ], 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/markshapiro/webpack-merge-and-include-globally.git" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/markshapiro/webpack-merge-and-include-globally/issues" 62 | }, 63 | "homepage": "https://github.com/markshapiro/webpack-merge-and-include-globally#readme", 64 | "author": "Mark Shapiro", 65 | "license": "MIT" 66 | } 67 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const uglifyJS = require('uglify-js'); 3 | const CleanCSS = require('clean-css'); 4 | const MergeIntoSingle = require('./index.node6-compatible.js'); 5 | // const MergeIntoSingle = require('./index.js'); 6 | 7 | // Webpack Config 8 | const webpackConfig = { 9 | devServer: { 10 | contentBase: path.join(__dirname, 'example'), 11 | compress: true, 12 | port: 3000, 13 | open: true, 14 | }, 15 | mode: 'none', 16 | entry: ['./example/main.js'], 17 | devtool: 'cheap-module-source-map', 18 | output: { 19 | filename: 'bundle.js', 20 | path: path.resolve(__dirname, './dist'), 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.css'], 24 | }, 25 | plugins: [ 26 | 27 | new MergeIntoSingle({ 28 | files: [{ 29 | src: [ 30 | 'node_modules/jquery/**/*.min.js', 31 | 'node_modules/classnames/index.js', 32 | 'node_modules/humps/humps.js', 33 | ], 34 | dest: (code) => { 35 | const min = uglifyJS.minify(code, { 36 | sourceMap: { 37 | filename: 'vendor.js', 38 | url: 'vendor.js.map', 39 | }, 40 | }); 41 | return { 42 | 'vendor.js': min.code, 43 | 'vendor.js.map': min.map, 44 | }; 45 | }, 46 | }, { 47 | src: ['example/test.css'], 48 | dest: (code) => ({ 49 | 'style.css': new CleanCSS({}).minify(code).styles, 50 | }), 51 | }], 52 | 53 | // also possible: 54 | 55 | // files:{ 56 | // 'vendor.js':[ 57 | // 'node_modules/jquery/**/*.min.js', 58 | // 'node_modules/classnames/index.js', 59 | // 'node_modules/humps/humps.js', 60 | // ], 61 | // 'style.css':[ 62 | // 'example/test.css', 63 | // ] 64 | // }, 65 | // transform:{ 66 | // 'vendor.js': code => uglifyJS.minify(code).code, 67 | // 'style.css': code => new CleanCSS({}).minify(code).styles, 68 | // }, 69 | 70 | hash: false, 71 | }, (filesMap) => { 72 | console.log('generated files: ', filesMap); // eslint-disable-line no-console 73 | }), 74 | ], 75 | module: { 76 | rules: [ 77 | { test: /\.html$/, loader: 'raw-loader' }, 78 | ], 79 | }, 80 | }; 81 | 82 | module.exports = webpackConfig; 83 | --------------------------------------------------------------------------------