helpers.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. 'use strict';
  2. var path = require('path');
  3. var fs = require('graceful-fs');
  4. var nal = require('now-and-later');
  5. var File = require('vinyl');
  6. var convert = require('convert-source-map');
  7. var removeBOM = require('remove-bom-buffer');
  8. var appendBuffer = require('append-buffer');
  9. var normalizePath = require('normalize-path');
  10. var urlRegex = /^(https?|webpack(-[^:]+)?):\/\//;
  11. function isRemoteSource(source) {
  12. return source.match(urlRegex);
  13. }
  14. function parse(data) {
  15. try {
  16. return JSON.parse(removeBOM(data));
  17. } catch (err) {
  18. // TODO: should this log a debug?
  19. }
  20. }
  21. function loadSourceMap(file, state, callback) {
  22. // Try to read inline source map
  23. state.map = convert.fromSource(state.content);
  24. if (state.map) {
  25. state.map = state.map.toObject();
  26. // Sources in map are relative to the source file
  27. state.path = file.dirname;
  28. state.content = convert.removeComments(state.content);
  29. // Remove source map comment from source
  30. file.contents = new Buffer(state.content, 'utf8');
  31. return callback();
  32. }
  33. // Look for source map comment referencing a source map file
  34. var mapComment = convert.mapFileCommentRegex.exec(state.content);
  35. var mapFile;
  36. if (mapComment) {
  37. mapFile = path.resolve(file.dirname, mapComment[1] || mapComment[2]);
  38. state.content = convert.removeMapFileComments(state.content);
  39. // Remove source map comment from source
  40. file.contents = new Buffer(state.content, 'utf8');
  41. } else {
  42. // If no comment try map file with same name as source file
  43. mapFile = file.path + '.map';
  44. }
  45. // Sources in external map are relative to map file
  46. state.path = path.dirname(mapFile);
  47. fs.readFile(mapFile, onRead);
  48. function onRead(err, data) {
  49. if (err) {
  50. return callback();
  51. }
  52. state.map = parse(data);
  53. callback();
  54. }
  55. }
  56. // Fix source paths and sourceContent for imported source map
  57. function fixImportedSourceMap(file, state, callback) {
  58. if (!state.map) {
  59. return callback();
  60. }
  61. state.map.sourcesContent = state.map.sourcesContent || [];
  62. nal.map(state.map.sources, normalizeSourcesAndContent, callback);
  63. function assignSourcesContent(sourceContent, idx) {
  64. state.map.sourcesContent[idx] = sourceContent;
  65. }
  66. function normalizeSourcesAndContent(sourcePath, idx, cb) {
  67. var sourceRoot = state.map.sourceRoot || '';
  68. var sourceContent = state.map.sourcesContent[idx] || null;
  69. if (isRemoteSource(sourcePath)) {
  70. assignSourcesContent(sourceContent, idx);
  71. return cb();
  72. }
  73. if (state.map.sourcesContent[idx]) {
  74. return cb();
  75. }
  76. if (sourceRoot && isRemoteSource(sourceRoot)) {
  77. assignSourcesContent(sourceContent, idx);
  78. return cb();
  79. }
  80. var basePath = path.resolve(file.base, sourceRoot);
  81. var absPath = path.resolve(state.path, sourceRoot, sourcePath);
  82. var relPath = path.relative(basePath, absPath);
  83. var unixRelPath = normalizePath(relPath);
  84. state.map.sources[idx] = unixRelPath;
  85. if (absPath !== file.path) {
  86. // Load content from file async
  87. return fs.readFile(absPath, onRead);
  88. }
  89. // If current file: use content
  90. assignSourcesContent(state.content, idx);
  91. cb();
  92. function onRead(err, data) {
  93. if (err) {
  94. assignSourcesContent(null, idx);
  95. return cb();
  96. }
  97. assignSourcesContent(removeBOM(data).toString('utf8'), idx);
  98. cb();
  99. }
  100. }
  101. }
  102. function mapsLoaded(file, state, callback) {
  103. if (!state.map) {
  104. state.map = {
  105. version: 3,
  106. names: [],
  107. mappings: '',
  108. sources: [normalizePath(file.relative)],
  109. sourcesContent: [state.content],
  110. };
  111. }
  112. state.map.file = normalizePath(file.relative);
  113. file.sourceMap = state.map;
  114. callback();
  115. }
  116. function addSourceMaps(file, state, callback) {
  117. var tasks = [
  118. loadSourceMap,
  119. fixImportedSourceMap,
  120. mapsLoaded,
  121. ];
  122. function apply(fn, key, cb) {
  123. fn(file, state, cb);
  124. }
  125. nal.mapSeries(tasks, apply, done);
  126. function done() {
  127. callback(null, file);
  128. }
  129. }
  130. /* Write Helpers */
  131. function createSourceMapFile(opts) {
  132. return new File({
  133. cwd: opts.cwd,
  134. base: opts.base,
  135. path: opts.path,
  136. contents: new Buffer(JSON.stringify(opts.content)),
  137. stat: {
  138. isFile: function() {
  139. return true;
  140. },
  141. isDirectory: function() {
  142. return false;
  143. },
  144. isBlockDevice: function() {
  145. return false;
  146. },
  147. isCharacterDevice: function() {
  148. return false;
  149. },
  150. isSymbolicLink: function() {
  151. return false;
  152. },
  153. isFIFO: function() {
  154. return false;
  155. },
  156. isSocket: function() {
  157. return false;
  158. },
  159. },
  160. });
  161. }
  162. var needsMultiline = ['.css'];
  163. function getCommentOptions(extname) {
  164. var opts = {
  165. multiline: (needsMultiline.indexOf(extname) !== -1),
  166. };
  167. return opts;
  168. }
  169. function writeSourceMaps(file, destPath, callback) {
  170. var sourceMapFile;
  171. var commentOpts = getCommentOptions(file.extname);
  172. var comment;
  173. if (destPath == null) {
  174. // Encode source map into comment
  175. comment = convert.fromObject(file.sourceMap).toComment(commentOpts);
  176. } else {
  177. var mapFile = path.join(destPath, file.relative) + '.map';
  178. var sourceMapPath = path.join(file.base, mapFile);
  179. // Create new sourcemap File
  180. sourceMapFile = createSourceMapFile({
  181. cwd: file.cwd,
  182. base: file.base,
  183. path: sourceMapPath,
  184. content: file.sourceMap,
  185. });
  186. var sourcemapLocation = path.relative(file.dirname, sourceMapPath);
  187. sourcemapLocation = normalizePath(sourcemapLocation);
  188. comment = convert.generateMapFileComment(sourcemapLocation, commentOpts);
  189. }
  190. // Append source map comment
  191. file.contents = appendBuffer(file.contents, comment);
  192. callback(null, file, sourceMapFile);
  193. }
  194. module.exports = {
  195. addSourceMaps: addSourceMaps,
  196. writeSourceMaps: writeSourceMaps,
  197. };