index.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /**
  2. * espower-source - Power Assert instrumentor from source to source.
  3. *
  4. * https://github.com/power-assert-js/espower-source
  5. *
  6. * Copyright (c) 2014-2018 Takuto Wada
  7. * Licensed under the MIT license.
  8. * https://github.com/power-assert-js/espower-source/blob/master/MIT-LICENSE.txt
  9. */
  10. 'use strict';
  11. var espower = require('espower');
  12. var acorn = require('acorn');
  13. require('acorn-es7-plugin')(acorn);
  14. var estraverse = require('estraverse');
  15. var mergeVisitors = require('merge-estraverse-visitors');
  16. var empowerAssert = require('empower-assert');
  17. var escodegen = require('escodegen');
  18. var extend = require('xtend');
  19. var convert = require('convert-source-map');
  20. var transfer = require('multi-stage-sourcemap').transfer;
  21. var _path = require('path');
  22. var isAbsolute = require('path-is-absolute');
  23. function mergeSourceMap (incomingSourceMap, outgoingSourceMap) {
  24. if (typeof outgoingSourceMap === 'string' || outgoingSourceMap instanceof String) {
  25. outgoingSourceMap = JSON.parse(outgoingSourceMap);
  26. }
  27. if (!incomingSourceMap) {
  28. return outgoingSourceMap;
  29. }
  30. return JSON.parse(transfer({fromSourceMap: outgoingSourceMap, toSourceMap: incomingSourceMap}));
  31. }
  32. function copyPropertyIfExists (name, from, to) {
  33. if (from[name]) {
  34. to.setProperty(name, from[name]);
  35. }
  36. }
  37. function reconnectSourceMap (inMap, outMap) {
  38. var mergedRawMap = mergeSourceMap(inMap, outMap.toObject());
  39. var reMap = convert.fromObject(mergedRawMap);
  40. copyPropertyIfExists('sources', inMap, reMap);
  41. copyPropertyIfExists('sourceRoot', inMap, reMap);
  42. copyPropertyIfExists('sourcesContent', inMap, reMap);
  43. return reMap;
  44. }
  45. function handleIncomingSourceMap (originalCode, options) {
  46. var inMap;
  47. if (options.sourceMap) {
  48. if (typeof options.sourceMap === 'string' || options.sourceMap instanceof String) {
  49. options.sourceMap = JSON.parse(options.sourceMap);
  50. }
  51. inMap = options.sourceMap;
  52. } else {
  53. var sourceMappingURL = retrieveSourceMapURL(originalCode);
  54. var commented;
  55. // relative file sourceMap
  56. // //# sourceMappingURL=foo.js.map or /*# sourceMappingURL=foo.js.map */
  57. if (sourceMappingURL && !/^data:application\/json[^,]+base64,/.test(sourceMappingURL)) {
  58. commented = convert.fromMapFileSource(originalCode, _path.dirname(options.path));
  59. } else {
  60. // inline sourceMap or none sourceMap
  61. commented = convert.fromSource(originalCode);
  62. }
  63. if (commented) {
  64. inMap = commented.toObject();
  65. options.sourceMap = inMap;
  66. }
  67. }
  68. return inMap;
  69. }
  70. // copy from https://github.com/evanw/node-source-map-support/blob/master/source-map-support.js#L99
  71. function retrieveSourceMapURL(source) {
  72. // //# sourceMappingURL=foo.js.map /*# sourceMappingURL=foo.js.map */
  73. var re = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/mg;
  74. // Keep executing the search to find the *last* sourceMappingURL to avoid
  75. // picking up sourceMappingURLs from comments, strings, etc.
  76. var lastMatch, match;
  77. while (match = re.exec(source)) {
  78. lastMatch = match;
  79. }
  80. if (!lastMatch) {
  81. return null;
  82. }
  83. return lastMatch[1];
  84. };
  85. function adjustFilepath (filepath, sourceRoot) {
  86. if (!sourceRoot || !isAbsolute(filepath)) {
  87. return filepath;
  88. }
  89. return _path.relative(sourceRoot, filepath);
  90. }
  91. function syntaxErrorLine(lineNumber, maxLineNumberLength, line) {
  92. var content = [' '];
  93. var lineNumberString = String(lineNumber);
  94. for (var i = lineNumberString.length; i < maxLineNumberLength; i++) {
  95. content.push(' ');
  96. }
  97. content.push(lineNumberString, ': ', line);
  98. return content.join('');
  99. }
  100. function showSyntaxErrorDetail(code, error, filepath) {
  101. var i;
  102. var begin = code.lastIndexOf('\n', error.pos);
  103. var end = code.indexOf('\n', error.pos);
  104. if (end === -1) {
  105. end = undefined;
  106. }
  107. var line = code.slice(begin + 1, end);
  108. var beforeLines = [];
  109. for (i = 0; i < 5; i++) {
  110. if (begin === -1) {
  111. break;
  112. }
  113. var lastBegin = begin;
  114. begin = code.lastIndexOf('\n', begin - 1);
  115. beforeLines.unshift(code.slice(begin + 1, lastBegin));
  116. if (begin === 0) {
  117. break;
  118. }
  119. }
  120. var afterLines = [];
  121. for (i = 0; i < 5; i++) {
  122. if (end === undefined) {
  123. break;
  124. }
  125. var lastEnd = end;
  126. end = code.indexOf('\n', end + 1);
  127. if (end === -1) {
  128. end = undefined;
  129. }
  130. afterLines.push(code.slice(lastEnd + 1, end));
  131. }
  132. var lines = [''];
  133. var numberLength = String(error.loc.line + afterLines.length).length;
  134. for (i = 0; i < beforeLines.length; i++) {
  135. lines.push(
  136. syntaxErrorLine(error.loc.line - beforeLines.length + i, numberLength, beforeLines[i])
  137. );
  138. }
  139. lines.push(syntaxErrorLine(error.loc.line, numberLength, line));
  140. var lineContent = [];
  141. for (i = 0; i < 6 + numberLength + error.loc.column - 1; i++) {
  142. lineContent.push(' ');
  143. }
  144. lineContent.push('^');
  145. lines.push(lineContent.join(''));
  146. for (i = 0; i < afterLines.length; i++) {
  147. lines.push(
  148. syntaxErrorLine(error.loc.line + i + 1, numberLength, afterLines[i])
  149. );
  150. }
  151. lines.push('', 'Parse Error: ' + error.message + (filepath ? ' in ' + filepath : ''));
  152. var detail = lines.join('\n');
  153. var err = new SyntaxError(detail);
  154. err.loc = error.loc;
  155. err.pos = error.pos;
  156. err.raisedAt = error.pos;
  157. throw err;
  158. }
  159. function instrument (originalCode, filepath, options) {
  160. var jsAst;
  161. try {
  162. jsAst = acorn.parse(originalCode, {locations: true, ecmaVersion: options.ecmaVersion, sourceType: options.sourceType, plugins: {asyncawait: true}});
  163. } catch (e) {
  164. if (e instanceof SyntaxError && e.pos && e.loc) {
  165. showSyntaxErrorDetail(originalCode, e, filepath);
  166. }
  167. throw e;
  168. }
  169. var modifiedAst = estraverse.replace(jsAst, mergeVisitors([
  170. {
  171. enter: empowerAssert.enter
  172. },
  173. espower.createVisitor(jsAst, options)
  174. ]));
  175. var escodegenOptions = extend({
  176. sourceMap: adjustFilepath(filepath || options.path, options.sourceRoot),
  177. sourceContent: originalCode,
  178. sourceMapWithCode: true
  179. });
  180. if (options.sourceRoot) {
  181. escodegenOptions.sourceMapRoot = options.sourceRoot;
  182. }
  183. return escodegen.generate(modifiedAst, escodegenOptions);
  184. }
  185. function instrumentWithoutSourceMapOutput (originalCode, options) {
  186. var jsAst;
  187. try {
  188. jsAst = acorn.parse(originalCode, {locations: true, ecmaVersion: options.ecmaVersion, sourceType: options.sourceType, plugins: {asyncawait: true}});
  189. } catch (e) {
  190. if (e instanceof SyntaxError && e.pos && e.loc) {
  191. showSyntaxErrorDetail(originalCode, e);
  192. }
  193. throw e;
  194. }
  195. var modifiedAst = estraverse.replace(jsAst, mergeVisitors([
  196. {
  197. enter: empowerAssert.enter
  198. },
  199. espower.createVisitor(jsAst, options)
  200. ]));
  201. return escodegen.generate(modifiedAst);
  202. }
  203. function mergeEspowerOptions (options, filepath) {
  204. return extend(espower.defaultOptions(), {
  205. ecmaVersion: 2018,
  206. sourceType: 'module',
  207. path: filepath
  208. }, options);
  209. }
  210. module.exports = function espowerSource (originalCode, filepath, options) {
  211. if (typeof originalCode === 'undefined' || originalCode === null) {
  212. throw new espower.EspowerError('`originalCode` is not specified', espowerSource);
  213. }
  214. var espowerOptions = mergeEspowerOptions(options, filepath);
  215. var inMap = handleIncomingSourceMap(originalCode, espowerOptions);
  216. if (!(filepath || espowerOptions.path)) {
  217. return instrumentWithoutSourceMapOutput(originalCode, espowerOptions);
  218. }
  219. var instrumented = instrument(originalCode, filepath, espowerOptions);
  220. var outMap = convert.fromJSON(instrumented.map.toString());
  221. if (inMap) {
  222. var reMap = reconnectSourceMap(inMap, outMap);
  223. return instrumented.code + '\n' + reMap.toComment() + '\n';
  224. } else {
  225. return instrumented.code + '\n' + outMap.toComment() + '\n';
  226. }
  227. };