├── .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 |
--------------------------------------------------------------------------------