From 50e34e85f329e85282ece2b72c42040a72243e75 Mon Sep 17 00:00:00 2001 From: Timur Sufiev Date: Tue, 3 Apr 2018 16:09:33 +0300 Subject: [PATCH 1/2] Update inputSourceMap according to loader transformations Since bem-loader expands certain require()-s in the beginning of the module, all subsequent source maps need to be adjusted by the introduced line offset. Do a correct mapping as well for the transformed require()-s themselves. Source map adjustments take place when the 'devtool' option passed into webpack toplevel config is not `false`. --- index.js | 19 +++++-- package.json | 3 +- source-map-utils.js | 122 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 source-map-utils.js diff --git a/index.js b/index.js index d9554b9..44b7f08 100644 --- a/index.js +++ b/index.js @@ -10,12 +10,14 @@ const path = require('path'), requiredPath = require('required-path'), falafel = require('falafel'), loaderUtils = require('loader-utils'), - getGenerators = require('./generators'); + getGenerators = require('./generators'), + updateSourceMapOffsets = require('./source-map-utils'); module.exports = function(source, inputSourceMap) { this.cacheable && this.cacheable(); const callback = this.async(), + sourceMapsEnabled = Boolean(this.options.devtool), options = Object.assign({}, this.options.bemLoader, loaderUtils.getOptions(this)), levelsMap = options.levels || bemConfig.levelMapSync(), levels = Array.isArray(levelsMap) ? levelsMap : Object.keys(levelsMap), @@ -44,7 +46,13 @@ module.exports = function(source, inputSourceMap) { generators.i18n = require('./generators/i18n').generate(langs); - const result = falafel(source, { ecmaVersion : 8, sourceType : 'module' }, node => { + const modifiedNodes = []; + const parserOptions = { + ecmaVersion : 8, + sourceType : 'module', + locations : sourceMapsEnabled + }; + const result = falafel(source, parserOptions, node => { // match `require('b:button')` if(!( node.type === 'CallExpression' && @@ -154,11 +162,16 @@ module.exports = function(source, inputSourceMap) { }); node.update(`[${res.join(',')}][0]`); + modifiedNodes.push(node); }) ); }); Promise.all(allPromises) - .then(() => callback(null, result.toString(), inputSourceMap)) + .then(() => { + const updatedSourceMap = sourceMapsEnabled && inputSourceMap ? + updateSourceMapOffsets(inputSourceMap, modifiedNodes) : inputSourceMap; + callback(null, result.toString(), updatedSourceMap); + }) .catch(callback); }; diff --git a/package.json b/package.json index 62d244f..342fb73 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "bem-config": "3.2.3", "falafel": "2.1.0", "loader-utils": "1.1.0", - "required-path": "1.0.1" + "required-path": "1.0.1", + "source-map": "0.5.7" }, "devDependencies": { "common-tags": "^1.7.2", diff --git a/source-map-utils.js b/source-map-utils.js new file mode 100644 index 0000000..23b735f --- /dev/null +++ b/source-map-utils.js @@ -0,0 +1,122 @@ +const SourceMapConsumer = require('source-map').SourceMapConsumer, + SourceMapGenerator = require('source-map').SourceMapGenerator; + +// NOTE: taken from source-map/util.js +/** + * Make a path relative to a URL or another path. + * + * @param {string} aRoot The root path or URL. + * @param {string} aPath The path or URL to be made relative to aRoot. + * @returns {string} + */ +function relative(aRoot, aPath) { + if(aRoot === '') { + aRoot = '.'; + } + + aRoot = aRoot.replace(/\/$/, ''); + + // It is possible for the path to be above the root. In this case, simply + // checking whether the root is a prefix of the path won't work. Instead, we + // need to remove components from the root one by one, until either we find + // a prefix that fits, or we run out of components to remove. + let level = 0; + while(aPath.indexOf(aRoot + '/') !== 0) { + const index = aRoot.lastIndexOf('/'); + if(index < 0) { + return aPath; + } + + // If the only part of the root that is left is the scheme (i.e. http://, + // file:///, etc.), one or more slashes (/), or simply nothing at all, we + // have exhausted all components, so the path is not relative to the root. + aRoot = aRoot.slice(0, index); + if(aRoot.match(/^([^\/]+:\/)?\/*$/)) { + return aPath; + } + + ++level; + } + + // Make sure we add a "../" for each component we removed from the root. + return Array(level + 1).join('../') + aPath.substr(aRoot.length + 1); +} + + +/** + * Take a raw source map from previous loader and apply adjustments related to the modifications + * made to `modifiedNodes`. Each node in `modifiedNodes` is expected to have 'loc' entry containing + * the original source's coordinates, while the transformed source is retrieved via node.source(). + * @param {Object} inputSourceMap + * @param {Array} modifiedNodes + * @returns {Object} + */ +function updateSourceMapOffsets(inputSourceMap, modifiedNodes) { + const sourceMapConsumer = new SourceMapConsumer(inputSourceMap); + const sourceRoot = sourceMapConsumer.sourceRoot; + const sourceMapGenerator = new SourceMapGenerator({ + file : sourceMapConsumer.file, + sourceRoot : sourceRoot + }); + + const copyMapping = (srcMapping, lineOffset) => { + const newMapping = { + generated : { + line : srcMapping.generatedLine + lineOffset, + column : srcMapping.generatedColumn + } + }; + + if(srcMapping.source !== null && srcMapping.originalLine !== null) { + newMapping.source = srcMapping.source; + if(sourceRoot != null) { + newMapping.source = relative(sourceRoot, newMapping.source); + } + + newMapping.original = { + line : srcMapping.originalLine, + column : srcMapping.originalColumn + }; + + if(srcMapping.name != null) { + newMapping.name = srcMapping.name; + } + } + + sourceMapGenerator.addMapping(newMapping); + }; + + modifiedNodes = modifiedNodes.slice(); + + // Since we're dealing with async operations, ensure that modified nodes are properly sorted + modifiedNodes.sort((node1, node2) => { + if(node1.loc.start.line === node2.loc.start.line) { + return node1.loc.start.column - node2.loc.start.column; + } else { + return node1.loc.start.line - node2.loc.start.line; + } + }); + + let lineOffset = 0; + let currentNode = modifiedNodes.shift(); + + sourceMapConsumer.eachMapping((inputMapping) => { + copyMapping(inputMapping, lineOffset); + if(currentNode && currentNode.loc.start.line === inputMapping.generatedLine) { + // When one-line require() is expanded into N require()-s, each new generated line + // should point to the original one-liner. We don't care about column transformations + // since there is one import/require per line. + let additionalLines = currentNode.source().split('\n').length - 1; + while(additionalLines > 0) { + lineOffset++; + copyMapping(inputMapping, lineOffset); + additionalLines--; + } + currentNode = modifiedNodes.shift(); + } + }); + + return Object.assign({}, sourceMapGenerator.toJSON(), { sourcesContent : inputSourceMap.sourcesContent }); +} + +module.exports = updateSourceMapOffsets; From 9a9978b6fdd2cfe3e5a449a488a8b7891bf949fe Mon Sep 17 00:00:00 2001 From: Timur Sufiev Date: Sun, 15 Apr 2018 16:52:26 +0300 Subject: [PATCH 2/2] Ongoing work with: * creating sourcemap from scratch (no input sourcemap) * writing unit tests --- index.js | 12 +++++---- source-map-utils.js | 66 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 44b7f08..a234931 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,6 @@ module.exports = function(source, inputSourceMap) { this.cacheable && this.cacheable(); const callback = this.async(), - sourceMapsEnabled = Boolean(this.options.devtool), options = Object.assign({}, this.options.bemLoader, loaderUtils.getOptions(this)), levelsMap = options.levels || bemConfig.levelMapSync(), levels = Array.isArray(levelsMap) ? levelsMap : Object.keys(levelsMap), @@ -50,7 +49,7 @@ module.exports = function(source, inputSourceMap) { const parserOptions = { ecmaVersion : 8, sourceType : 'module', - locations : sourceMapsEnabled + locations : this.sourceMap }; const result = falafel(source, parserOptions, node => { // match `require('b:button')` @@ -169,9 +168,12 @@ module.exports = function(source, inputSourceMap) { Promise.all(allPromises) .then(() => { - const updatedSourceMap = sourceMapsEnabled && inputSourceMap ? - updateSourceMapOffsets(inputSourceMap, modifiedNodes) : inputSourceMap; - callback(null, result.toString(), updatedSourceMap); + const sourceMap = this.sourceMap ? + updateSourceMapOffsets.call(this, source, inputSourceMap, modifiedNodes) : undefined; + //if(inputSourceMap.file === 'AttributesButton.js') { + // console.log('\n', source, '\n', result.toString(), '\n'); + //} + callback(null, result.toString(), sourceMap); }) .catch(callback); }; diff --git a/source-map-utils.js b/source-map-utils.js index 23b735f..29ad82d 100644 --- a/source-map-utils.js +++ b/source-map-utils.js @@ -1,5 +1,7 @@ const SourceMapConsumer = require('source-map').SourceMapConsumer, - SourceMapGenerator = require('source-map').SourceMapGenerator; + SourceMapGenerator = require('source-map').SourceMapGenerator, + loaderUtils = require('loader-utils'), + path = require('path'); // NOTE: taken from source-map/util.js /** @@ -47,15 +49,29 @@ function relative(aRoot, aPath) { * Take a raw source map from previous loader and apply adjustments related to the modifications * made to `modifiedNodes`. Each node in `modifiedNodes` is expected to have 'loc' entry containing * the original source's coordinates, while the transformed source is retrieved via node.source(). + * @param {string} source * @param {Object} inputSourceMap * @param {Array} modifiedNodes * @returns {Object} */ -function updateSourceMapOffsets(inputSourceMap, modifiedNodes) { - const sourceMapConsumer = new SourceMapConsumer(inputSourceMap); - const sourceRoot = sourceMapConsumer.sourceRoot; +function updateSourceMapOffsets(source, inputSourceMap, modifiedNodes) { + const webpackRemainingChain = loaderUtils.getRemainingRequest(this).split('!'); + const filename = webpackRemainingChain[webpackRemainingChain.length - 1]; + let sourceMapConsumer, sourceRoot, sourceFile, sourcesContent; + + if(inputSourceMap) { + sourceMapConsumer = new SourceMapConsumer(inputSourceMap); + sourceRoot = sourceMapConsumer.sourceRoot; + sourceFile = sourceMapConsumer.file; + sourcesContent = inputSourceMap.sourcesContent; + } else { + sourceFile = path.basename(filename); + sourceRoot = process.cwd(); + sourcesContent = [source]; + } + const sourceMapGenerator = new SourceMapGenerator({ - file : sourceMapConsumer.file, + file : sourceFile, sourceRoot : sourceRoot }); @@ -100,23 +116,45 @@ function updateSourceMapOffsets(inputSourceMap, modifiedNodes) { let lineOffset = 0; let currentNode = modifiedNodes.shift(); - sourceMapConsumer.eachMapping((inputMapping) => { - copyMapping(inputMapping, lineOffset); - if(currentNode && currentNode.loc.start.line === inputMapping.generatedLine) { - // When one-line require() is expanded into N require()-s, each new generated line - // should point to the original one-liner. We don't care about column transformations - // since there is one import/require per line. + if(sourceMapConsumer) { + sourceMapConsumer.eachMapping((inputMapping) => { + copyMapping(inputMapping, lineOffset); + if(currentNode && currentNode.loc.start.line === inputMapping.generatedLine) { + // When one-line require() is expanded into N require()-s, each new generated line + // should point to the original one-liner. We don't care about column transformations + // since there is one import/require per line. + let additionalLines = currentNode.source().split('\n').length - 1; + while(additionalLines > 0) { + lineOffset++; + copyMapping(inputMapping, lineOffset); + additionalLines--; + } + currentNode = modifiedNodes.shift(); + } + }); + } else { + while(currentNode) { let additionalLines = currentNode.source().split('\n').length - 1; while(additionalLines > 0) { lineOffset++; - copyMapping(inputMapping, lineOffset); + sourceMapGenerator.addMapping({ + original : { + line : currentNode.loc.start.line, + column : currentNode.loc.start.column + }, + generated : { + line : currentNode.loc.start.line + lineOffset, + column : currentNode.loc.start.column + }, + source : relative(sourceRoot, filename) + }); additionalLines--; } currentNode = modifiedNodes.shift(); } - }); + } - return Object.assign({}, sourceMapGenerator.toJSON(), { sourcesContent : inputSourceMap.sourcesContent }); + return Object.assign({}, sourceMapGenerator.toJSON(), { sourcesContent : sourcesContent }); } module.exports = updateSourceMapOffsets;