index.js 5.31 KB
'use strict';

var path            =  require('path');
var convert         =  require('convert-source-map');
var memoize         =  require('lodash.memoize');
var createGenerator =  require('inline-source-map');
var pathIsAbsolute  =  require('./lib/path-is-absolute');
var mappingsFromMap =  require('./lib/mappings-from-map');

var protocolRx = /^[a-z]+:\/\//;

/**
 * Rebases a relative path in 'sourceFile' to be relative
 * to the path where 'sourceFile' is located.
 *
 * This is necessary before adding relative paths to the
 * new combined map to ensure all paths are relative to their
 * original source.
 *
 * The 'sourceRoot' from the original source map is joined
 * as well to ensure the complete path.
 *
 * Resulting paths that are absolute are passed along directly.
 *
 * @param sourceFile {String} path to the original source file that references a map
 * @param relativeRoot {String} sourceRoot in sourceFile's map to combine with relativePath
 * @param relativePath {String} source path from sourceFile's map
 */
var rebaseRelativePath = memoize(function(sourceFile, relativeRoot, relativePath) {
  if (!relativePath) {
    return relativePath;
  }

  // join relative path to root (e.g. 'src/' + 'file.js')
  var relativeRootedPath = relativeRoot ? path.join(relativeRoot, relativePath) : relativePath;
  relativeRootedPath = relativeRootedPath.replace(/\\/g, '/');
  sourceFile = sourceFile.replace(/\\/g, '/');

  if (sourceFile === relativeRootedPath ||    // same path,
      pathIsAbsolute(relativeRootedPath) ||   // absolute path, nor
      protocolRx.test(relativeRootedPath)) {  // absolute protocol need rebasing
    return relativeRootedPath;
  }

  // make relative to source file
  return path.join(path.dirname(sourceFile), relativeRootedPath).replace(/\\/g, '/');
}, function(a, b, c) {
  return a + '::' + b + '::' + c;
});

function resolveMap(source) {
  var gen = convert.fromSource(source);
  return gen ? gen.toObject() : null;
}

function hasInlinedSource(existingMap) {
  return existingMap.sourcesContent && !!existingMap.sourcesContent[0];
}

function Combiner(file, sourceRoot) {
  // since we include the original code in the map sourceRoot actually not needed
  this.generator = createGenerator({ file: file || 'generated.js', sourceRoot: sourceRoot });
}

Combiner.prototype._addGeneratedMap = function (sourceFile, source, offset) {
  this.generator.addGeneratedMappings(sourceFile, source, offset);
  this.generator.addSourceContent(sourceFile, source);
  return this;
};

Combiner.prototype._addExistingMap = function (sourceFile, source, existingMap, offset) {
  var mappings = mappingsFromMap(existingMap);

  // add all of the sources from the map
  for (var i = 0, len = existingMap.sources.length; i < len; i++) {
    if (!existingMap.sourcesContent) continue;

    this.generator.addSourceContent(
      rebaseRelativePath(sourceFile, existingMap.sourceRoot, existingMap.sources[i]),
      existingMap.sourcesContent[i]);
  }

  // add the mappings, preserving the original mapping 'source'
  mappings.forEach(function(mapping) {
    // Add the mappings one at a time because 'inline-source-map' doesn't handle
    // mapping source filenames. The mapping.source already takes sourceRoot into account
    // per the SMConsumer.eachMapping function, so pass null for the root here.
    this.generator.addMappings(
      rebaseRelativePath(sourceFile, null, mapping.source), [mapping], offset);
  }, this);

  return this;
};

/**
 * Adds map to underlying source map.
 * If source contains a source map comment that has the source of the original file inlined it will offset these
 * mappings and include them.
 * If no source map comment is found or it has no source inlined, mappings for the file will be generated and included
 *
 * @name addMap
 * @function
 * @param opts {Object} { sourceFile: {String}, source: {String} }
 * @param offset {Object} { line: {Number}, column: {Number} }
 */
Combiner.prototype.addFile = function (opts, offset) {

  offset = offset || {};
  if (!offset.hasOwnProperty('line'))  offset.line    =  0;
  if (!offset.hasOwnProperty('column')) offset.column =  0;

  var existingMap = resolveMap(opts.source);

  return existingMap && hasInlinedSource(existingMap)
    ? this._addExistingMap(opts.sourceFile, opts.source, existingMap, offset)
    : this._addGeneratedMap(opts.sourceFile, opts.source, offset);
};

/**
* @name base64
* @function
* @return {String} base64 encoded combined source map
*/
Combiner.prototype.base64 = function () {
  return this.generator.base64Encode();
};

/**
 * @name comment
 * @function
 * @return {String} base64 encoded sourceMappingUrl comment of the combined source map
 */
Combiner.prototype.comment = function () {
  return this.generator.inlineMappingUrl();
};

/**
 * @name create
 * @function
 * @param file {String} optional name of the generated file
 * @param sourceRoot {String} optional sourceRoot of the map to be generated
 * @return {Object} Combiner instance to which source maps can be added and later combined
 */
exports.create = function (file, sourceRoot) { return new Combiner(file, sourceRoot); };

/**
 * @name removeComments
 * @function
 * @param src
 * @return {String} src with all sourceMappingUrl comments removed
 */
exports.removeComments = function (src) {
  if (!src.replace) return src;
  return src.replace(convert.commentRegex, '').replace(convert.mapFileCommentRegex, '');
};