'use strict'; var path = require('path'); var fs = require('graceful-fs'); var nal = require('now-and-later'); var File = require('vinyl'); var convert = require('convert-source-map'); var removeBOM = require('remove-bom-buffer'); var appendBuffer = require('append-buffer'); var normalizePath = require('normalize-path'); var urlRegex = /^(https?|webpack(-[^:]+)?):\/\//; function isRemoteSource(source) { return source.match(urlRegex); } function parse(data) { try { return JSON.parse(removeBOM(data)); } catch (err) { // TODO: should this log a debug? } } function loadSourceMap(file, state, callback) { // Try to read inline source map state.map = convert.fromSource(state.content); if (state.map) { state.map = state.map.toObject(); // Sources in map are relative to the source file state.path = file.dirname; state.content = convert.removeComments(state.content); // Remove source map comment from source file.contents = new Buffer(state.content, 'utf8'); return callback(); } // Look for source map comment referencing a source map file var mapComment = convert.mapFileCommentRegex.exec(state.content); var mapFile; if (mapComment) { mapFile = path.resolve(file.dirname, mapComment[1] || mapComment[2]); state.content = convert.removeMapFileComments(state.content); // Remove source map comment from source file.contents = new Buffer(state.content, 'utf8'); } else { // If no comment try map file with same name as source file mapFile = file.path + '.map'; } // Sources in external map are relative to map file state.path = path.dirname(mapFile); fs.readFile(mapFile, onRead); function onRead(err, data) { if (err) { return callback(); } state.map = parse(data); callback(); } } // Fix source paths and sourceContent for imported source map function fixImportedSourceMap(file, state, callback) { if (!state.map) { return callback(); } state.map.sourcesContent = state.map.sourcesContent || []; nal.map(state.map.sources, normalizeSourcesAndContent, callback); function assignSourcesContent(sourceContent, idx) { state.map.sourcesContent[idx] = sourceContent; } function normalizeSourcesAndContent(sourcePath, idx, cb) { var sourceRoot = state.map.sourceRoot || ''; var sourceContent = state.map.sourcesContent[idx] || null; if (isRemoteSource(sourcePath)) { assignSourcesContent(sourceContent, idx); return cb(); } if (state.map.sourcesContent[idx]) { return cb(); } if (sourceRoot && isRemoteSource(sourceRoot)) { assignSourcesContent(sourceContent, idx); return cb(); } var basePath = path.resolve(file.base, sourceRoot); var absPath = path.resolve(state.path, sourceRoot, sourcePath); var relPath = path.relative(basePath, absPath); var unixRelPath = normalizePath(relPath); state.map.sources[idx] = unixRelPath; if (absPath !== file.path) { // Load content from file async return fs.readFile(absPath, onRead); } // If current file: use content assignSourcesContent(state.content, idx); cb(); function onRead(err, data) { if (err) { assignSourcesContent(null, idx); return cb(); } assignSourcesContent(removeBOM(data).toString('utf8'), idx); cb(); } } } function mapsLoaded(file, state, callback) { if (!state.map) { state.map = { version: 3, names: [], mappings: '', sources: [normalizePath(file.relative)], sourcesContent: [state.content], }; } state.map.file = normalizePath(file.relative); file.sourceMap = state.map; callback(); } function addSourceMaps(file, state, callback) { var tasks = [ loadSourceMap, fixImportedSourceMap, mapsLoaded, ]; function apply(fn, key, cb) { fn(file, state, cb); } nal.mapSeries(tasks, apply, done); function done() { callback(null, file); } } /* Write Helpers */ function createSourceMapFile(opts) { return new File({ cwd: opts.cwd, base: opts.base, path: opts.path, contents: new Buffer(JSON.stringify(opts.content)), 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; }, }, }); } var needsMultiline = ['.css']; function getCommentOptions(extname) { var opts = { multiline: (needsMultiline.indexOf(extname) !== -1), }; return opts; } function writeSourceMaps(file, destPath, callback) { var sourceMapFile; var commentOpts = getCommentOptions(file.extname); var comment; if (destPath == null) { // Encode source map into comment comment = convert.fromObject(file.sourceMap).toComment(commentOpts); } else { var mapFile = path.join(destPath, file.relative) + '.map'; var sourceMapPath = path.join(file.base, mapFile); // Create new sourcemap File sourceMapFile = createSourceMapFile({ cwd: file.cwd, base: file.base, path: sourceMapPath, content: file.sourceMap, }); var sourcemapLocation = path.relative(file.dirname, sourceMapPath); sourcemapLocation = normalizePath(sourcemapLocation); comment = convert.generateMapFileComment(sourcemapLocation, commentOpts); } // Append source map comment file.contents = appendBuffer(file.contents, comment); callback(null, file, sourceMapFile); } module.exports = { addSourceMaps: addSourceMaps, writeSourceMaps: writeSourceMaps, };