'use strict'; var through = require('through2'); var fs = require('graceful-fs'); var path = require('path'); var File = require('vinyl'); var convert = require('convert-source-map'); var stripBom = require('strip-bom'); var PLUGIN_NAME = 'gulp-sourcemap'; var urlRegex = /^(https?|webpack(-[^:]+)?):\/\//; /** * Initialize source mapping chain */ module.exports.init = function init(options) { function sourceMapInit(file, encoding, callback) { /*jshint validthis:true */ // pass through if file is null or already has a source map if (file.isNull() || file.sourceMap) { this.push(file); return callback(); } if (file.isStream()) { return callback(new Error(PLUGIN_NAME + '-init: Streaming not supported')); } var fileContent = file.contents.toString(); var sourceMap; if (options && options.loadMaps) { var sourcePath = ''; //root path for the sources in the map // Try to read inline source map sourceMap = convert.fromSource(fileContent); if (sourceMap) { sourceMap = sourceMap.toObject(); // sources in map are relative to the source file sourcePath = path.dirname(file.path); fileContent = convert.removeComments(fileContent); } else { // look for source map comment referencing a source map file var mapComment = convert.mapFileCommentRegex.exec(fileContent); var mapFile; if (mapComment) { mapFile = path.resolve(path.dirname(file.path), mapComment[1] || mapComment[2]); fileContent = convert.removeMapFileComments(fileContent); // if no comment try map file with same name as source file } else { mapFile = file.path + '.map'; } // sources in external map are relative to map file sourcePath = path.dirname(mapFile); try { sourceMap = JSON.parse(stripBom(fs.readFileSync(mapFile, 'utf8'))); } catch(e) {} } // fix source paths and sourceContent for imported source map if (sourceMap) { sourceMap.sourcesContent = sourceMap.sourcesContent || []; sourceMap.sources.forEach(function(source, i) { if (source.match(urlRegex)) { sourceMap.sourcesContent[i] = sourceMap.sourcesContent[i] || null; return; } var absPath = path.resolve(sourcePath, source); sourceMap.sources[i] = unixStylePath(path.relative(file.base, absPath)); if (!sourceMap.sourcesContent[i]) { var sourceContent = null; if (sourceMap.sourceRoot) { if (sourceMap.sourceRoot.match(urlRegex)) { sourceMap.sourcesContent[i] = null; return; } absPath = path.resolve(sourcePath, sourceMap.sourceRoot, source); } // if current file: use content if (absPath === file.path) { sourceContent = fileContent; // else load content from file } else { try { if (options.debug) console.log(PLUGIN_NAME + '-init: No source content for "' + source + '". Loading from file.'); sourceContent = stripBom(fs.readFileSync(absPath, 'utf8')); } catch (e) { if (options.debug) console.warn(PLUGIN_NAME + '-init: source file not found: ' + absPath); } } sourceMap.sourcesContent[i] = sourceContent; } }); // remove source map comment from source file.contents = new Buffer(fileContent, 'utf8'); } } if (!sourceMap) { // Make an empty source map sourceMap = { version : 3, names: [], mappings: '', sources: [unixStylePath(file.relative)], sourcesContent: [fileContent] }; } sourceMap.file = unixStylePath(file.relative); file.sourceMap = sourceMap; this.push(file); callback(); } return through.obj(sourceMapInit); }; /** * Write the source map * * @param options options to change the way the source map is written * */ module.exports.write = function write(destPath, options) { if (options === undefined && Object.prototype.toString.call(destPath) === '[object Object]') { options = destPath; destPath = undefined; } options = options || {}; // set defaults for options if unset if (options.includeContent === undefined) options.includeContent = true; if (options.addComment === undefined) options.addComment = true; function sourceMapWrite(file, encoding, callback) { /*jshint validthis:true */ if (file.isNull() || !file.sourceMap) { this.push(file); return callback(); } if (file.isStream()) { return callback(new Error(PLUGIN_NAME + '-write: Streaming not supported')); } var sourceMap = file.sourceMap; // fix paths if Windows style paths sourceMap.file = unixStylePath(file.relative); sourceMap.sources = sourceMap.sources.map(function(filePath) { return unixStylePath(filePath); }); if (typeof options.sourceRoot === 'function') { sourceMap.sourceRoot = options.sourceRoot(file); } else { sourceMap.sourceRoot = options.sourceRoot; } if (options.includeContent) { sourceMap.sourcesContent = sourceMap.sourcesContent || []; // load missing source content for (var i = 0; i < file.sourceMap.sources.length; i++) { if (!sourceMap.sourcesContent[i]) { var sourcePath = path.resolve(sourceMap.sourceRoot || file.base, sourceMap.sources[i]); try { if (options.debug) console.log(PLUGIN_NAME + '-write: No source content for "' + sourceMap.sources[i] + '". Loading from file.'); sourceMap.sourcesContent[i] = stripBom(fs.readFileSync(sourcePath, 'utf8')); } catch (e) { if (options.debug) console.warn(PLUGIN_NAME + '-write: source file not found: ' + sourcePath); } } } if (sourceMap.sourceRoot === undefined) { sourceMap.sourceRoot = '/source/'; } else if (sourceMap.sourceRoot === null) { sourceMap.sourceRoot = undefined; } } else { delete sourceMap.sourcesContent; } var extension = file.relative.split('.').pop(); var commentFormatter; switch (extension) { case 'css': commentFormatter = function(url) { return "\n/*# sourceMappingURL=" + url + " */\n"; }; break; case 'js': commentFormatter = function(url) { return "\n//# sourceMappingURL=" + url + "\n"; }; break; default: commentFormatter = function(url) { return ""; }; } var comment, sourceMappingURLPrefix; if (!destPath) { // encode source map into comment var base64Map = new Buffer(JSON.stringify(sourceMap)).toString('base64'); comment = commentFormatter('data:application/json;base64,' + base64Map); } else { var sourceMapPath = path.join(file.base, destPath, file.relative) + '.map'; // add new source map file to stream var sourceMapFile = new File({ cwd: file.cwd, base: file.base, path: sourceMapPath, contents: new Buffer(JSON.stringify(sourceMap)), stat: { isFile: function () { return true; }, isDirectory: function () { return false; }, isBlockDevice: function () { return false; }, isCharacterDevice: function () { return false; }, isSymbolicLink: function () { return false; }, isFIFO: function () { return false; }, isSocket: function () { return false; } } }); this.push(sourceMapFile); var sourceMapPathRelative = path.relative(path.dirname(file.path), sourceMapPath); if (options.sourceMappingURLPrefix) { var prefix = ''; if (typeof options.sourceMappingURLPrefix === 'function') { prefix = options.sourceMappingURLPrefix(file); } else { prefix = options.sourceMappingURLPrefix; } sourceMapPathRelative = prefix+path.join('/', sourceMapPathRelative); } comment = commentFormatter(unixStylePath(sourceMapPathRelative)); if (options.sourceMappingURL && typeof options.sourceMappingURL === 'function') { comment = commentFormatter(options.sourceMappingURL(file)); } } // append source map comment if (options.addComment) file.contents = Buffer.concat([file.contents, new Buffer(comment)]); this.push(file); callback(); } return through.obj(sourceMapWrite); }; function unixStylePath(filePath) { return filePath.split(path.sep).join('/'); }