From 849e6d020ff84bfd5d9530d5ba00e7fc7a76ee7e Mon Sep 17 00:00:00 2001 From: Dvvarf Date: Tue, 15 Oct 2019 14:51:56 +0300 Subject: [PATCH] added an ability to generate source map for merged files --- index.js | 59 ++++++++++++++++++++++---- index.node6-compatible.js | 88 ++++++++++++++++++++++++++++++++------- index.test.js | 31 ++++++++++++++ package.json | 3 +- 4 files changed, 157 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index a84e084..693218c 100755 --- a/index.js +++ b/index.js @@ -2,12 +2,52 @@ 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 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) { @@ -40,7 +80,7 @@ class MergeIntoFile { } run(compilation, callback) { - const { files, transform, encoding, hash } = this.options; + const { files, transform, encoding, hash, sourceMap } = this.options; const generatedFiles = {}; let filesCanonical = []; if (!Array.isArray(files)) { @@ -56,19 +96,22 @@ 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 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, '\n'); - const resultsFiles = await fileTransform.dest(content); + const filesContentPromises = flattenedList.map(path => ({ path, content: readFile(path, 'utf-8') })); + const content = sourceMapEnabled ? await joinContentWithMap(filesContentPromises, '\n', sourceMapRoot, sourceMapInlineSources) : await joinContent(filesContentPromises, '\n'); + const resultsFiles = await fileTransform.dest(content.code, content.map); Object.keys(resultsFiles).forEach((newFileName) => { let newFileNameHashed = newFileName; if (hash) { diff --git a/index.node6-compatible.js b/index.node6-compatible.js index 3b0fc61..ce744e2 100644 --- a/index.node6-compatible.js +++ b/index.node6-compatible.js @@ -6,28 +6,83 @@ 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 readFile = promisify(fs.readFile); const listFiles = promisify(glob); -const joinContent = (() => { - var _ref = _asyncToGenerator(function* (promises, separator) { +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 = (() => { + var _ref = _asyncToGenerator(function* (promises, separator, sourceRoot, inlineSources) { return promises.reduce((() => { var _ref2 = _asyncToGenerator(function* (acc, curr) { - return `${yield acc}${(yield acc).length ? separator : ''}${yield curr}`; + const lines = getLineNumber((yield curr.content)); + const relativePath = sourceRoot ? path.relative(sourceRoot, curr.path) : curr.path; + + if (inlineSources) { + (yield acc).map.setSourceContent(relativePath, (yield curr.content)); + } + + for (let offset = 0; offset < lines; offset++) { + (yield acc).map.addMapping({ + source: relativePath, + original: { line: 1 + offset, column: 0 }, + generated: { line: (yield acc).lines + offset, column: 0 } + }); + } + + return { + code: `${(yield acc).code}${(yield acc).code.length ? separator : ''}${yield curr.content}`, + lines: (yield acc).lines + lines, + map: (yield acc).map + }; }); - return function (_x3, _x4) { + return function (_x5, _x6) { return _ref2.apply(this, arguments); }; - })(), ''); + })(), { + code: '', + lines: 1, + map: new SourceMapGenerator() + }); }); - return function joinContent(_x, _x2) { + return function joinContentWithMap(_x, _x2, _x3, _x4) { return _ref.apply(this, arguments); }; })(); +const joinContent = (() => { + var _ref3 = _asyncToGenerator(function* (promises, separator) { + return promises.reduce((() => { + var _ref4 = _asyncToGenerator(function* (acc, curr) { + return { + code: `${(yield acc).code}${(yield acc).code.length ? separator : ''}${yield curr.content}` + }; + }); + + return function (_x9, _x10) { + return _ref4.apply(this, arguments); + }; + })(), { + code: '' + }); + }); + + return function joinContent(_x7, _x8) { + return _ref3.apply(this, arguments); + }; +})(); + class MergeIntoFile { constructor(options, onComplete) { this.options = options; @@ -59,7 +114,7 @@ class MergeIntoFile { } run(compilation, callback) { - const { files, transform, encoding, hash } = this.options; + const { files, transform, encoding, hash, sourceMap } = this.options; const generatedFiles = {}; let filesCanonical = []; if (!Array.isArray(files)) { @@ -75,22 +130,25 @@ class MergeIntoFile { filesCanonical.forEach(fileTransform => { if (typeof fileTransform.dest === 'string') { const destFileName = fileTransform.dest; - fileTransform.dest = code => ({ // eslint-disable-line no-param-reassign - [destFileName]: transform && transform[destFileName] ? transform[destFileName](code) : code + fileTransform.dest = (code, map) => ({ // eslint-disable-line no-param-reassign + [destFileName]: transform && transform[destFileName] ? transform[destFileName](code, map) : code }); } }); + const sourceMapEnabled = !!sourceMap; + const sourceMapRoot = sourceMap && sourceMap.sourceRoot; + const sourceMapInlineSources = sourceMap && sourceMap.inlineSources; const finalPromises = filesCanonical.map((() => { - var _ref3 = _asyncToGenerator(function* (fileTransform) { + var _ref5 = _asyncToGenerator(function* (fileTransform) { const listOfLists = yield Promise.all(fileTransform.src.map(function (path) { return listFiles(path, null); })); const flattenedList = Array.prototype.concat.apply([], listOfLists); const filesContentPromises = flattenedList.map(function (path) { - return readFile(path, encoding || 'utf-8'); + return { path, content: readFile(path, 'utf-8') }; }); - const content = yield joinContent(filesContentPromises, '\n'); - const resultsFiles = yield fileTransform.dest(content); + const content = sourceMapEnabled ? yield joinContentWithMap(filesContentPromises, '\n', sourceMapRoot, sourceMapInlineSources) : yield joinContent(filesContentPromises, '\n'); + const resultsFiles = yield fileTransform.dest(content.code, content.map); Object.keys(resultsFiles).forEach(function (newFileName) { let newFileNameHashed = newFileName; if (hash) { @@ -117,8 +175,8 @@ class MergeIntoFile { }); }); - return function (_x5) { - return _ref3.apply(this, arguments); + return function (_x11) { + return _ref5.apply(this, arguments); }; })()); diff --git a/index.test.js b/index.test.js index a91fa0b..c7e624a 100755 --- a/index.test.js +++ b/index.test.js @@ -82,6 +82,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 index 9c1c91a..a6d9e4e 100755 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "dependencies": { "es6-promisify": "^6.0.0", "glob": "^7.1.2", - "rev-hash": "^2.0.0" + "rev-hash": "^2.0.0", + "source-map": "^0.7.3" }, "keywords": [ "webpack",