diff --git a/index.js b/index.js index 0d30b0d..3e98400 100755 --- a/index.js +++ b/index.js @@ -2,6 +2,8 @@ const fs = require('fs'); const glob = require('glob'); const { promisify } = require('es6-promisify'); const revHash = require('rev-hash'); +const { SourceMapGenerator } = require('source-map'); +const path = require('path'); const { sources, Compilation } = require('webpack'); const plugin = { name: 'MergeIntoFile' }; @@ -9,8 +11,46 @@ const plugin = { name: 'MergeIntoFile' }; const readFile = promisify(fs.readFile); const listFiles = promisify(glob); -const joinContent = async (promises, separator) => promises - .reduce(async (acc, curr) => `${await acc}${(await acc).length ? separator : ''}${await curr}`, ''); +const getLineNumber = function (str, start, stop) { + const i1 = (start === undefined || start < 0) ? 0 : start; + const i2 = (stop === undefined || stop >= str.length) ? str.length - 1 : stop; + let ret = 1; + for (let i = i1; i <= i2; i++) if (str.charAt(i) === '\n') ret++; + return ret; +}; + +const joinContentWithMap = async (promises, separator, sourceRoot, inlineSources) => promises.reduce(async (acc, curr) => { + const lines = getLineNumber(await curr.content); + const relativePath = sourceRoot ? path.relative(sourceRoot, curr.path) : curr.path; + + if (inlineSources) { + (await acc).map.setSourceContent(relativePath, await curr.content); + } + + for (let offset = 0; offset < lines; offset++) { + (await acc).map.addMapping({ + source: relativePath, + original: { line: 1 + offset, column: 0 }, + generated: { line: (await acc).lines + offset, column: 0 }, + }); + } + + return { + code: `${(await acc).code}${(await acc).code.length ? separator : ''}${await curr.content}`, + lines: (await acc).lines + lines, + map: (await acc).map, + }; +}, { + code: '', + lines: 1, + map: new SourceMapGenerator(), +}); + +const joinContent = async (promises, separator) => promises.reduce(async (acc, curr) => ({ + code: `${(await acc).code}${(await acc).code.length ? separator : ''}${await curr.content}`, +}), { + code: '', +}); class MergeIntoFile { constructor(options, onComplete) { @@ -66,6 +106,7 @@ class MergeIntoFile { chunks, hash, transformFileName, + sourceMap, } = this.options; if (chunks && compilation.chunks && compilation.chunks .filter((chunk) => chunks.indexOf(chunk.name) >= 0 && chunk.rendered).length === 0) { @@ -89,20 +130,23 @@ class MergeIntoFile { filesCanonical.forEach((fileTransform) => { if (typeof fileTransform.dest === 'string') { const destFileName = fileTransform.dest; - fileTransform.dest = (code) => ({ // eslint-disable-line no-param-reassign + fileTransform.dest = (code, map) => ({ // eslint-disable-line no-param-reassign [destFileName]: (transform && transform[destFileName]) - ? transform[destFileName](code) + ? transform[destFileName](code, map) : code, }); } }); + const sourceMapEnabled = !!sourceMap; + const sourceMapRoot = sourceMap && sourceMap.sourceRoot; + const sourceMapInlineSources = sourceMap && sourceMap.inlineSources; const finalPromises = filesCanonical.map(async (fileTransform) => { const { separator = '\n' } = this.options; const listOfLists = await Promise.all(fileTransform.src.map((path) => listFiles(path, null))); const flattenedList = Array.prototype.concat.apply([], listOfLists); - const filesContentPromises = flattenedList.map((path) => readFile(path, encoding || 'utf-8')); - const content = await joinContent(filesContentPromises, separator); - const resultsFiles = await fileTransform.dest(content); + const filesContentPromises = flattenedList.map(path => ({ path, content: readFile(path, encoding || 'utf-8') })); + const content = sourceMapEnabled ? await joinContentWithMap(filesContentPromises, separator, sourceMapRoot, sourceMapInlineSources) : await joinContent(filesContentPromises, separator); + const resultsFiles = await fileTransform.dest(content.code, content.map); // eslint-disable-next-line no-restricted-syntax for (const resultsFile in resultsFiles) { if (typeof resultsFiles[resultsFile] === 'object') { diff --git a/index.node6-compatible.js b/index.node6-compatible.js index efddee7..32dbeaf 100644 --- a/index.node6-compatible.js +++ b/index.node6-compatible.js @@ -25,9 +25,14 @@ var _require = require('es6-promisify'), var revHash = require('rev-hash'); -var _require2 = require('webpack'), - sources = _require2.sources, - Compilation = _require2.Compilation; +var _require2 = require('source-map'), + SourceMapGenerator = _require2.SourceMapGenerator; + +var path = require('path'); + +var _require3 = require('webpack'), + sources = _require3.sources, + Compilation = _require3.Compilation; var plugin = { name: 'MergeIntoFile' @@ -35,52 +40,154 @@ var plugin = { var readFile = promisify(fs.readFile); var listFiles = promisify(glob); -var joinContent = /*#__PURE__*/function () { - var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(promises, separator) { +var getLineNumber = function getLineNumber(str, start, stop) { + var i1 = start === undefined || start < 0 ? 0 : start; + var i2 = stop === undefined || stop >= str.length ? str.length - 1 : stop; + var ret = 1; + + for (var i = i1; i <= i2; i++) { + if (str.charAt(i) === '\n') ret++; + } + + return ret; +}; + +var joinContentWithMap = /*#__PURE__*/function () { + var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(promises, separator, sourceRoot, inlineSources) { return _regenerator["default"].wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: return _context2.abrupt("return", promises.reduce( /*#__PURE__*/function () { var _ref2 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(acc, curr) { + var lines, relativePath, offset; return _regenerator["default"].wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: - _context.t2 = ""; + _context.t0 = getLineNumber; _context.next = 3; - return acc; + return curr.content; case 3: - _context.t3 = _context.sent; - _context.t1 = _context.t2.concat.call(_context.t2, _context.t3); - _context.next = 7; + _context.t1 = _context.sent; + lines = (0, _context.t0)(_context.t1); + relativePath = sourceRoot ? path.relative(sourceRoot, curr.path) : curr.path; + + if (!inlineSources) { + _context.next = 15; + break; + } + + _context.next = 9; return acc; - case 7: - if (!_context.sent.length) { - _context.next = 11; + case 9: + _context.t2 = _context.sent.map; + _context.t3 = relativePath; + _context.next = 13; + return curr.content; + + case 13: + _context.t4 = _context.sent; + + _context.t2.setSourceContent.call(_context.t2, _context.t3, _context.t4); + + case 15: + offset = 0; + + case 16: + if (!(offset < lines)) { + _context.next = 33; break; } - _context.t4 = separator; - _context.next = 12; - break; + _context.next = 19; + return acc; - case 11: - _context.t4 = ''; + case 19: + _context.t5 = _context.sent.map; + _context.t6 = relativePath; + _context.t7 = { + line: 1 + offset, + column: 0 + }; + _context.next = 24; + return acc; - case 12: - _context.t5 = _context.t4; - _context.t0 = _context.t1.concat.call(_context.t1, _context.t5); + case 24: + _context.t8 = _context.sent.lines; + _context.t9 = offset; + _context.t10 = _context.t8 + _context.t9; + _context.t11 = { + line: _context.t10, + column: 0 + }; + _context.t12 = { + source: _context.t6, + original: _context.t7, + generated: _context.t11 + }; + + _context.t5.addMapping.call(_context.t5, _context.t12); + + case 30: + offset++; _context.next = 16; - return curr; + break; - case 16: - _context.t6 = _context.sent; - return _context.abrupt("return", _context.t0.concat.call(_context.t0, _context.t6)); + case 33: + _context.t15 = ""; + _context.next = 36; + return acc; + + case 36: + _context.t16 = _context.sent.code; + _context.t14 = _context.t15.concat.call(_context.t15, _context.t16); + _context.next = 40; + return acc; + + case 40: + if (!_context.sent.code.length) { + _context.next = 44; + break; + } + + _context.t17 = separator; + _context.next = 45; + break; + + case 44: + _context.t17 = ''; - case 18: + case 45: + _context.t18 = _context.t17; + _context.t13 = _context.t14.concat.call(_context.t14, _context.t18); + _context.next = 49; + return curr.content; + + case 49: + _context.t19 = _context.sent; + _context.t20 = _context.t13.concat.call(_context.t13, _context.t19); + _context.next = 53; + return acc; + + case 53: + _context.t21 = _context.sent.lines; + _context.t22 = lines; + _context.t23 = _context.t21 + _context.t22; + _context.next = 58; + return acc; + + case 58: + _context.t24 = _context.sent.map; + return _context.abrupt("return", { + code: _context.t20, + lines: _context.t23, + map: _context.t24 + }); + + case 60: case "end": return _context.stop(); } @@ -88,10 +195,14 @@ var joinContent = /*#__PURE__*/function () { }, _callee); })); - return function (_x3, _x4) { + return function (_x5, _x6) { return _ref2.apply(this, arguments); }; - }(), '')); + }(), { + code: '', + lines: 1, + map: new SourceMapGenerator() + })); case 1: case "end": @@ -101,11 +212,87 @@ var joinContent = /*#__PURE__*/function () { }, _callee2); })); - return function joinContent(_x, _x2) { + return function joinContentWithMap(_x, _x2, _x3, _x4) { return _ref.apply(this, arguments); }; }(); +var joinContent = /*#__PURE__*/function () { + var _ref3 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee4(promises, separator) { + return _regenerator["default"].wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + return _context4.abrupt("return", promises.reduce( /*#__PURE__*/function () { + var _ref4 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3(acc, curr) { + return _regenerator["default"].wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + _context3.t2 = ""; + _context3.next = 3; + return acc; + + case 3: + _context3.t3 = _context3.sent.code; + _context3.t1 = _context3.t2.concat.call(_context3.t2, _context3.t3); + _context3.next = 7; + return acc; + + case 7: + if (!_context3.sent.code.length) { + _context3.next = 11; + break; + } + + _context3.t4 = separator; + _context3.next = 12; + break; + + case 11: + _context3.t4 = ''; + + case 12: + _context3.t5 = _context3.t4; + _context3.t0 = _context3.t1.concat.call(_context3.t1, _context3.t5); + _context3.next = 16; + return curr.content; + + case 16: + _context3.t6 = _context3.sent; + _context3.t7 = _context3.t0.concat.call(_context3.t0, _context3.t6); + return _context3.abrupt("return", { + code: _context3.t7 + }); + + case 19: + case "end": + return _context3.stop(); + } + } + }, _callee3); + })); + + return function (_x9, _x10) { + return _ref4.apply(this, arguments); + }; + }(), { + code: '' + })); + + case 1: + case "end": + return _context4.stop(); + } + } + }, _callee4); + })); + + return function joinContent(_x7, _x8) { + return _ref3.apply(this, arguments); + }; +}(); + var MergeIntoFile = /*#__PURE__*/function () { function MergeIntoFile(options, onComplete) { (0, _classCallCheck2["default"])(this, MergeIntoFile); @@ -148,7 +335,8 @@ var MergeIntoFile = /*#__PURE__*/function () { encoding = _this$options.encoding, chunks = _this$options.chunks, hash = _this$options.hash, - transformFileName = _this$options.transformFileName; + transformFileName = _this$options.transformFileName, + sourceMap = _this$options.sourceMap; if (chunks && compilation.chunks && compilation.chunks.filter(function (chunk) { return chunks.indexOf(chunk.name) >= 0 && chunk.rendered; @@ -178,67 +366,91 @@ var MergeIntoFile = /*#__PURE__*/function () { if (typeof fileTransform.dest === 'string') { var destFileName = fileTransform.dest; - fileTransform.dest = function (code) { - return (0, _defineProperty2["default"])({}, destFileName, transform && transform[destFileName] ? transform[destFileName](code) : code); + fileTransform.dest = function (code, map) { + return (0, _defineProperty2["default"])({}, destFileName, transform && transform[destFileName] ? transform[destFileName](code, map) : code); }; } }); + var sourceMapEnabled = !!sourceMap; + var sourceMapRoot = sourceMap && sourceMap.sourceRoot; + var sourceMapInlineSources = sourceMap && sourceMap.inlineSources; var finalPromises = filesCanonical.map( /*#__PURE__*/function () { - var _ref4 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3(fileTransform) { + var _ref6 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee5(fileTransform) { var _this2$options$separa, separator, listOfLists, flattenedList, filesContentPromises, content, resultsFiles, resultsFile; - return _regenerator["default"].wrap(function _callee3$(_context3) { + return _regenerator["default"].wrap(function _callee5$(_context5) { while (1) { - switch (_context3.prev = _context3.next) { + switch (_context5.prev = _context5.next) { case 0: _this2$options$separa = _this2.options.separator, separator = _this2$options$separa === void 0 ? '\n' : _this2$options$separa; - _context3.next = 3; + _context5.next = 3; return Promise.all(fileTransform.src.map(function (path) { return listFiles(path, null); })); case 3: - listOfLists = _context3.sent; + listOfLists = _context5.sent; flattenedList = Array.prototype.concat.apply([], listOfLists); filesContentPromises = flattenedList.map(function (path) { - return readFile(path, encoding || 'utf-8'); + return { + path: path, + content: readFile(path, encoding || 'utf-8') + }; }); - _context3.next = 8; + + if (!sourceMapEnabled) { + _context5.next = 12; + break; + } + + _context5.next = 9; + return joinContentWithMap(filesContentPromises, separator, sourceMapRoot, sourceMapInlineSources); + + case 9: + _context5.t0 = _context5.sent; + _context5.next = 15; + break; + + case 12: + _context5.next = 14; return joinContent(filesContentPromises, separator); - case 8: - content = _context3.sent; - _context3.next = 11; - return fileTransform.dest(content); + case 14: + _context5.t0 = _context5.sent; - case 11: - resultsFiles = _context3.sent; - _context3.t0 = _regenerator["default"].keys(resultsFiles); + case 15: + content = _context5.t0; + _context5.next = 18; + return fileTransform.dest(content.code, content.map); - case 13: - if ((_context3.t1 = _context3.t0()).done) { - _context3.next = 21; + case 18: + resultsFiles = _context5.sent; + _context5.t1 = _regenerator["default"].keys(resultsFiles); + + case 20: + if ((_context5.t2 = _context5.t1()).done) { + _context5.next = 28; break; } - resultsFile = _context3.t1.value; + resultsFile = _context5.t2.value; if (!((0, _typeof2["default"])(resultsFiles[resultsFile]) === 'object')) { - _context3.next = 19; + _context5.next = 26; break; } - _context3.next = 18; + _context5.next = 25; return resultsFiles[resultsFile]; - case 18: - resultsFiles[resultsFile] = _context3.sent; + case 25: + resultsFiles[resultsFile] = _context5.sent; - case 19: - _context3.next = 13; + case 26: + _context5.next = 20; break; - case 21: + case 28: Object.keys(resultsFiles).forEach(function (newFileName) { var newFileNameHashed = newFileName; var hasTransformFileNameFn = typeof transformFileName === 'function'; @@ -295,16 +507,16 @@ var MergeIntoFile = /*#__PURE__*/function () { } }); - case 22: + case 29: case "end": - return _context3.stop(); + return _context5.stop(); } } - }, _callee3); + }, _callee5); })); - return function (_x5) { - return _ref4.apply(this, arguments); + return function (_x11) { + return _ref6.apply(this, arguments); }; }()); Promise.all(finalPromises).then(function () { diff --git a/index.test.js b/index.test.js index 637498f..b476b0f 100755 --- a/index.test.js +++ b/index.test.js @@ -135,6 +135,37 @@ describe('MergeIntoFile', () => { }); }); + it('should succeed merging using mock content with transform and map', (done) => { + const instance = new MergeIntoSingle({ + files: { + 'script.js.map': [ + 'file1.js', + 'file2.js', + ], + 'style.css': [ + '*.css', + ], + }, + transform: { + 'script.js.map': (val, map) => `${map.toString()}`, + }, + sourceMap: true, + }); + instance.apply({ + plugin: (event, fun) => { + const obj = { + assets: {}, + }; + fun(obj, (err) => { + expect(err).toEqual(undefined); + expect(obj.assets['script.js.map'].source()).toEqual('{"version":3,"sources":["1.js","2.js"],"names":[],"mappings":"AAAA;ACAA"}'); + expect(obj.assets['style.css'].source()).toEqual('FILE_3_TEXT\nFILE_4_TEXT'); + done(); + }); + }, + }); + }); + it('should succeed merging using mock content by using array instead of object', (done) => { const instance = new MergeIntoSingle({ files: [ diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 0e8a88d..5edf2f0 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "dependencies": { "es6-promisify": "^6.1.1", "glob": "^7.1.6", - "rev-hash": "^3.0.0" + "rev-hash": "^3.0.0", + "source-map": "^0.7.3" }, "keywords": [ "webpack",