test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. require('./source-map-support').install({
  2. emptyCacheBetweenOperations: true // Needed to be able to test for failure
  3. });
  4. var SourceMapGenerator = require('source-map').SourceMapGenerator;
  5. var child_process = require('child_process');
  6. var assert = require('assert');
  7. var fs = require('fs');
  8. function compareLines(actual, expected) {
  9. assert(actual.length >= expected.length, 'got ' + actual.length + ' lines but expected at least ' + expected.length + ' lines');
  10. for (var i = 0; i < expected.length; i++) {
  11. // Some tests are regular expressions because the output format changed slightly between node v0.9.2 and v0.9.3
  12. if (expected[i] instanceof RegExp) {
  13. assert(expected[i].test(actual[i]), JSON.stringify(actual[i]) + ' does not match ' + expected[i]);
  14. } else {
  15. assert.equal(actual[i], expected[i]);
  16. }
  17. }
  18. }
  19. function createEmptySourceMap() {
  20. return new SourceMapGenerator({
  21. file: '.generated.js',
  22. sourceRoot: '.'
  23. });
  24. }
  25. function createSourceMapWithGap() {
  26. var sourceMap = createEmptySourceMap();
  27. sourceMap.addMapping({
  28. generated: { line: 100, column: 0 },
  29. original: { line: 100, column: 0 },
  30. source: '.original.js'
  31. });
  32. return sourceMap;
  33. }
  34. function createSingleLineSourceMap() {
  35. var sourceMap = createEmptySourceMap();
  36. sourceMap.addMapping({
  37. generated: { line: 1, column: 0 },
  38. original: { line: 1, column: 0 },
  39. source: '.original.js'
  40. });
  41. return sourceMap;
  42. }
  43. function createSecondLineSourceMap() {
  44. var sourceMap = createEmptySourceMap();
  45. sourceMap.addMapping({
  46. generated: { line: 2, column: 0 },
  47. original: { line: 1, column: 0 },
  48. source: '.original.js'
  49. });
  50. return sourceMap;
  51. }
  52. function createMultiLineSourceMap() {
  53. var sourceMap = createEmptySourceMap();
  54. for (var i = 1; i <= 100; i++) {
  55. sourceMap.addMapping({
  56. generated: { line: i, column: 0 },
  57. original: { line: 1000 + i, column: 99 + i },
  58. source: 'line' + i + '.js'
  59. });
  60. }
  61. return sourceMap;
  62. }
  63. function createMultiLineSourceMapWithSourcesContent() {
  64. var sourceMap = createEmptySourceMap();
  65. var original = new Array(1001).join('\n');
  66. for (var i = 1; i <= 100; i++) {
  67. sourceMap.addMapping({
  68. generated: { line: i, column: 0 },
  69. original: { line: 1000 + i, column: 4 },
  70. source: 'original.js'
  71. });
  72. original += ' line ' + i + '\n';
  73. }
  74. sourceMap.setSourceContent('original.js', original);
  75. return sourceMap;
  76. }
  77. function compareStackTrace(sourceMap, source, expected) {
  78. // Check once with a separate source map
  79. fs.writeFileSync('.generated.js.map', sourceMap);
  80. fs.writeFileSync('.generated.js', 'exports.test = function() {' +
  81. source.join('\n') + '};//@ sourceMappingURL=.generated.js.map');
  82. try {
  83. delete require.cache[require.resolve('./.generated')];
  84. require('./.generated').test();
  85. } catch (e) {
  86. compareLines(e.stack.split(/\r\n|\n/), expected);
  87. }
  88. fs.unlinkSync('.generated.js');
  89. fs.unlinkSync('.generated.js.map');
  90. // Check again with an inline source map (in a data URL)
  91. fs.writeFileSync('.generated.js', 'exports.test = function() {' +
  92. source.join('\n') + '};//@ sourceMappingURL=data:application/json;base64,' +
  93. new Buffer(sourceMap.toString()).toString('base64'));
  94. try {
  95. delete require.cache[require.resolve('./.generated')];
  96. require('./.generated').test();
  97. } catch (e) {
  98. compareLines(e.stack.split(/\r\n|\n/), expected);
  99. }
  100. fs.unlinkSync('.generated.js');
  101. }
  102. function compareStdout(done, sourceMap, source, expected) {
  103. fs.writeFileSync('.original.js', 'this is the original code');
  104. fs.writeFileSync('.generated.js.map', sourceMap);
  105. fs.writeFileSync('.generated.js', source.join('\n') +
  106. '//@ sourceMappingURL=.generated.js.map');
  107. child_process.exec('node ./.generated', function(error, stdout, stderr) {
  108. try {
  109. compareLines(
  110. (stdout + stderr)
  111. .trim()
  112. .split(/\r\n|\n/)
  113. .filter(function (line) { return line !== '' }), // Empty lines are not relevant.
  114. expected
  115. );
  116. } catch (e) {
  117. return done(e);
  118. }
  119. fs.unlinkSync('.generated.js');
  120. fs.unlinkSync('.generated.js.map');
  121. fs.unlinkSync('.original.js');
  122. done();
  123. });
  124. }
  125. it('normal throw', function() {
  126. compareStackTrace(createMultiLineSourceMap(), [
  127. 'throw new Error("test");'
  128. ], [
  129. 'Error: test',
  130. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  131. ]);
  132. });
  133. it('throw inside function', function() {
  134. compareStackTrace(createMultiLineSourceMap(), [
  135. 'function foo() {',
  136. ' throw new Error("test");',
  137. '}',
  138. 'foo();'
  139. ], [
  140. 'Error: test',
  141. /^ at foo \((?:.*[/\\])?line2\.js:1002:102\)$/,
  142. /^ at Object\.exports\.test \((?:.*[/\\])?line4\.js:1004:104\)$/
  143. ]);
  144. });
  145. it('throw inside function inside function', function() {
  146. compareStackTrace(createMultiLineSourceMap(), [
  147. 'function foo() {',
  148. ' function bar() {',
  149. ' throw new Error("test");',
  150. ' }',
  151. ' bar();',
  152. '}',
  153. 'foo();'
  154. ], [
  155. 'Error: test',
  156. /^ at bar \((?:.*[/\\])?line3\.js:1003:103\)$/,
  157. /^ at foo \((?:.*[/\\])?line5\.js:1005:105\)$/,
  158. /^ at Object\.exports\.test \((?:.*[/\\])?line7\.js:1007:107\)$/
  159. ]);
  160. });
  161. it('eval', function() {
  162. compareStackTrace(createMultiLineSourceMap(), [
  163. 'eval("throw new Error(\'test\')");'
  164. ], [
  165. 'Error: test',
  166. // Before Node 4, `Object.eval`, after just `eval`.
  167. /^ at (?:Object\.)?eval \(eval at (<anonymous>|exports.test) \((?:.*[/\\])?line1\.js:1001:101\)/,
  168. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  169. ]);
  170. });
  171. it('eval inside eval', function() {
  172. compareStackTrace(createMultiLineSourceMap(), [
  173. 'eval("eval(\'throw new Error(\\"test\\")\')");'
  174. ], [
  175. 'Error: test',
  176. /^ at (?:Object\.)?eval \(eval at (<anonymous>|exports.test) \(eval at (<anonymous>|exports.test) \((?:.*[/\\])?line1\.js:1001:101\)/,
  177. /^ at (?:Object\.)?eval \(eval at (<anonymous>|exports.test) \((?:.*[/\\])?line1\.js:1001:101\)/,
  178. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  179. ]);
  180. });
  181. it('eval inside function', function() {
  182. compareStackTrace(createMultiLineSourceMap(), [
  183. 'function foo() {',
  184. ' eval("throw new Error(\'test\')");',
  185. '}',
  186. 'foo();'
  187. ], [
  188. 'Error: test',
  189. /^ at eval \(eval at foo \((?:.*[/\\])?line2\.js:1002:102\)/,
  190. /^ at foo \((?:.*[/\\])?line2\.js:1002:102\)/,
  191. /^ at Object\.exports\.test \((?:.*[/\\])?line4\.js:1004:104\)$/
  192. ]);
  193. });
  194. it('eval with sourceURL', function() {
  195. compareStackTrace(createMultiLineSourceMap(), [
  196. 'eval("throw new Error(\'test\')//@ sourceURL=sourceURL.js");'
  197. ], [
  198. 'Error: test',
  199. /^ at (?:Object\.)?eval \(sourceURL\.js:1:7\)$/,
  200. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  201. ]);
  202. });
  203. it('eval with sourceURL inside eval', function() {
  204. compareStackTrace(createMultiLineSourceMap(), [
  205. 'eval("eval(\'throw new Error(\\"test\\")//@ sourceURL=sourceURL.js\')");'
  206. ], [
  207. 'Error: test',
  208. /^ at (?:Object\.)?eval \(sourceURL\.js:1:7\)$/,
  209. /^ at (?:Object\.)?eval \(eval at (<anonymous>|exports.test) \((?:.*[/\\])?line1\.js:1001:101\)/,
  210. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  211. ]);
  212. });
  213. it('native function', function() {
  214. compareStackTrace(createSingleLineSourceMap(), [
  215. '[1].map(function(x) { throw new Error(x); });'
  216. ], [
  217. 'Error: 1',
  218. /[/\\].original\.js/,
  219. /at Array\.map \(native\)/
  220. ]);
  221. });
  222. it('function constructor', function() {
  223. compareStackTrace(createMultiLineSourceMap(), [
  224. 'throw new Function(")");'
  225. ], [
  226. 'SyntaxError: Unexpected token )',
  227. ]);
  228. });
  229. it('throw with empty source map', function() {
  230. compareStackTrace(createEmptySourceMap(), [
  231. 'throw new Error("test");'
  232. ], [
  233. 'Error: test',
  234. /^ at Object\.exports\.test \((?:.*[/\\])?.generated.js:1:34\)$/
  235. ]);
  236. });
  237. it('throw in Timeout with empty source map', function(done) {
  238. compareStdout(done, createEmptySourceMap(), [
  239. 'require("./source-map-support").install();',
  240. 'setTimeout(function () {',
  241. ' throw new Error("this is the error")',
  242. '})'
  243. ], [
  244. /[/\\].generated.js:3$/,
  245. ' throw new Error("this is the error")',
  246. /^ \^$/,
  247. 'Error: this is the error',
  248. /^ at ((null)|(Timeout))\._onTimeout \((?:.*[/\\])?.generated.js:3:11\)$/
  249. ]);
  250. });
  251. it('throw with source map with gap', function() {
  252. compareStackTrace(createSourceMapWithGap(), [
  253. 'throw new Error("test");'
  254. ], [
  255. 'Error: test',
  256. /^ at Object\.exports\.test \((?:.*[/\\])?.generated.js:1:34\)$/
  257. ]);
  258. });
  259. it('sourcesContent with data URL', function() {
  260. compareStackTrace(createMultiLineSourceMapWithSourcesContent(), [
  261. 'throw new Error("test");'
  262. ], [
  263. 'Error: test',
  264. /^ at Object\.exports\.test \((?:.*[/\\])?original.js:1001:5\)$/
  265. ]);
  266. });
  267. it('finds the last sourceMappingURL', function() {
  268. compareStackTrace(createMultiLineSourceMapWithSourcesContent(), [
  269. '//# sourceMappingURL=missing.map.js', // NB: compareStackTrace adds another source mapping.
  270. 'throw new Error("test");'
  271. ], [
  272. 'Error: test',
  273. /^ at Object\.exports\.test \((?:.*[/\\])?original.js:1002:5\)$/
  274. ]);
  275. });
  276. it('default options', function(done) {
  277. compareStdout(done, createSecondLineSourceMap(), [
  278. '',
  279. 'function foo() { throw new Error("this is the error"); }',
  280. 'require("./source-map-support").install();',
  281. 'process.nextTick(foo);',
  282. 'process.nextTick(function() { process.exit(1); });'
  283. ], [
  284. /[/\\].original\.js:1$/,
  285. 'this is the original code',
  286. '^',
  287. 'Error: this is the error',
  288. /^ at foo \((?:.*[/\\])?.original\.js:1:1\)$/
  289. ]);
  290. });
  291. it('handleUncaughtExceptions is true', function(done) {
  292. compareStdout(done, createSecondLineSourceMap(), [
  293. '',
  294. 'function foo() { throw new Error("this is the error"); }',
  295. 'require("./source-map-support").install({ handleUncaughtExceptions: true });',
  296. 'process.nextTick(foo);'
  297. ], [
  298. /[/\\].original\.js:1$/,
  299. 'this is the original code',
  300. '^',
  301. 'Error: this is the error',
  302. /^ at foo \((?:.*[/\\])?.original\.js:1:1\)$/
  303. ]);
  304. });
  305. it('handleUncaughtExceptions is false', function(done) {
  306. compareStdout(done, createSecondLineSourceMap(), [
  307. '',
  308. 'function foo() { throw new Error("this is the error"); }',
  309. 'require("./source-map-support").install({ handleUncaughtExceptions: false });',
  310. 'process.nextTick(foo);'
  311. ], [
  312. /[/\\].generated.js:2$/,
  313. 'function foo() { throw new Error("this is the error"); }',
  314. // Before Node 4, the arrow points on the `new`, after on the
  315. // `throw`.
  316. /^ (?: )?\^$/,
  317. 'Error: this is the error',
  318. /^ at foo \((?:.*[/\\])?.original\.js:1:1\)$/
  319. ]);
  320. });
  321. it('default options with empty source map', function(done) {
  322. compareStdout(done, createEmptySourceMap(), [
  323. '',
  324. 'function foo() { throw new Error("this is the error"); }',
  325. 'require("./source-map-support").install();',
  326. 'process.nextTick(foo);'
  327. ], [
  328. /[/\\].generated.js:2$/,
  329. 'function foo() { throw new Error("this is the error"); }',
  330. /^ (?: )?\^$/,
  331. 'Error: this is the error',
  332. /^ at foo \((?:.*[/\\])?.generated.js:2:24\)$/
  333. ]);
  334. });
  335. it('default options with source map with gap', function(done) {
  336. compareStdout(done, createSourceMapWithGap(), [
  337. '',
  338. 'function foo() { throw new Error("this is the error"); }',
  339. 'require("./source-map-support").install();',
  340. 'process.nextTick(foo);'
  341. ], [
  342. /[/\\].generated.js:2$/,
  343. 'function foo() { throw new Error("this is the error"); }',
  344. /^ (?: )?\^$/,
  345. 'Error: this is the error',
  346. /^ at foo \((?:.*[/\\])?.generated.js:2:24\)$/
  347. ]);
  348. });
  349. it('specifically requested error source', function(done) {
  350. compareStdout(done, createSecondLineSourceMap(), [
  351. '',
  352. 'function foo() { throw new Error("this is the error"); }',
  353. 'var sms = require("./source-map-support");',
  354. 'sms.install({ handleUncaughtExceptions: false });',
  355. 'process.on("uncaughtException", function (e) { console.log("SRC:" + sms.getErrorSource(e)); });',
  356. 'process.nextTick(foo);'
  357. ], [
  358. /^SRC:.*[/\\].original.js:1$/,
  359. 'this is the original code',
  360. '^'
  361. ]);
  362. });
  363. it('sourcesContent', function(done) {
  364. compareStdout(done, createMultiLineSourceMapWithSourcesContent(), [
  365. '',
  366. 'function foo() { throw new Error("this is the error"); }',
  367. 'require("./source-map-support").install();',
  368. 'process.nextTick(foo);',
  369. 'process.nextTick(function() { process.exit(1); });'
  370. ], [
  371. /[/\\]original\.js:1002$/,
  372. ' line 2',
  373. ' ^',
  374. 'Error: this is the error',
  375. /^ at foo \((?:.*[/\\])?original\.js:1002:5\)$/
  376. ]);
  377. });
  378. it('missing source maps should also be cached', function(done) {
  379. compareStdout(done, createSingleLineSourceMap(), [
  380. '',
  381. 'var count = 0;',
  382. 'function foo() {',
  383. ' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
  384. '}',
  385. 'require("./source-map-support").install({',
  386. ' overrideRetrieveSourceMap: true,',
  387. ' retrieveSourceMap: function(name) {',
  388. ' if (/\\.generated.js$/.test(name)) count++;',
  389. ' return null;',
  390. ' }',
  391. '});',
  392. 'process.nextTick(foo);',
  393. 'process.nextTick(foo);',
  394. 'process.nextTick(function() { console.log(count); });',
  395. ], [
  396. 'Error: this is the error',
  397. /^ at foo \((?:.*[/\\])?.generated.js:4:15\)$/,
  398. 'Error: this is the error',
  399. /^ at foo \((?:.*[/\\])?.generated.js:4:15\)$/,
  400. '1', // The retrieval should only be attempted once
  401. ]);
  402. });
  403. it('should consult all retrieve source map providers', function(done) {
  404. compareStdout(done, createSingleLineSourceMap(), [
  405. '',
  406. 'var count = 0;',
  407. 'function foo() {',
  408. ' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
  409. '}',
  410. 'require("./source-map-support").install({',
  411. ' retrieveSourceMap: function(name) {',
  412. ' if (/\\.generated.js$/.test(name)) count++;',
  413. ' return undefined;',
  414. ' }',
  415. '});',
  416. 'require("./source-map-support").install({',
  417. ' retrieveSourceMap: function(name) {',
  418. ' if (/\\.generated.js$/.test(name)) {',
  419. ' count++;',
  420. ' return ' + JSON.stringify({url: '.original.js', map: createMultiLineSourceMapWithSourcesContent().toJSON()}) + ';',
  421. ' }',
  422. ' }',
  423. '});',
  424. 'process.nextTick(foo);',
  425. 'process.nextTick(foo);',
  426. 'process.nextTick(function() { console.log(count); });',
  427. ], [
  428. 'Error: this is the error',
  429. /^ at foo \((?:.*[/\\])?original.js:1004:5\)$/,
  430. 'Error: this is the error',
  431. /^ at foo \((?:.*[/\\])?original.js:1004:5\)$/,
  432. '1', // The retrieval should only be attempted once
  433. ]);
  434. });
  435. it('should allow for runtime inline source maps', function(done) {
  436. var sourceMap = createMultiLineSourceMapWithSourcesContent();
  437. fs.writeFileSync('.generated.jss', 'foo');
  438. compareStdout(function(err) {
  439. fs.unlinkSync('.generated.jss');
  440. done(err);
  441. }, createSingleLineSourceMap(), [
  442. 'require("./source-map-support").install({',
  443. ' hookRequire: true',
  444. '});',
  445. 'require.extensions[".jss"] = function(module, filename) {',
  446. ' module._compile(',
  447. JSON.stringify([
  448. '',
  449. 'var count = 0;',
  450. 'function foo() {',
  451. ' console.log(new Error("this is the error").stack.split("\\n").slice(0, 2).join("\\n"));',
  452. '}',
  453. 'process.nextTick(foo);',
  454. 'process.nextTick(foo);',
  455. 'process.nextTick(function() { console.log(count); });',
  456. '//@ sourceMappingURL=data:application/json;charset=utf8;base64,' + new Buffer(sourceMap.toString()).toString('base64')
  457. ].join('\n')),
  458. ', filename);',
  459. '};',
  460. 'require("./.generated.jss");',
  461. ], [
  462. 'Error: this is the error',
  463. /^ at foo \(.*[/\\]original.js:1004:5\)$/,
  464. 'Error: this is the error',
  465. /^ at foo \(.*[/\\]original.js:1004:5\)$/,
  466. '0', // The retrieval should only be attempted once
  467. ]);
  468. });
  469. /* The following test duplicates some of the code in
  470. * `compareStackTrace` but appends a charset to the
  471. * source mapping url.
  472. */
  473. it('finds source maps with charset specified', function() {
  474. var sourceMap = createMultiLineSourceMap()
  475. var source = [ 'throw new Error("test");' ];
  476. var expected = [
  477. 'Error: test',
  478. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  479. ];
  480. fs.writeFileSync('.generated.js', 'exports.test = function() {' +
  481. source.join('\n') + '};//@ sourceMappingURL=data:application/json;charset=utf8;base64,' +
  482. new Buffer(sourceMap.toString()).toString('base64'));
  483. try {
  484. delete require.cache[require.resolve('./.generated')];
  485. require('./.generated').test();
  486. } catch (e) {
  487. compareLines(e.stack.split(/\r\n|\n/), expected);
  488. }
  489. fs.unlinkSync('.generated.js');
  490. });
  491. /* The following test duplicates some of the code in
  492. * `compareStackTrace` but appends some code and a
  493. * comment to the source mapping url.
  494. */
  495. it('allows code/comments after sourceMappingURL', function() {
  496. var sourceMap = createMultiLineSourceMap()
  497. var source = [ 'throw new Error("test");' ];
  498. var expected = [
  499. 'Error: test',
  500. /^ at Object\.exports\.test \((?:.*[/\\])?line1\.js:1001:101\)$/
  501. ];
  502. fs.writeFileSync('.generated.js', 'exports.test = function() {' +
  503. source.join('\n') + '};//# sourceMappingURL=data:application/json;base64,' +
  504. new Buffer(sourceMap.toString()).toString('base64') +
  505. '\n// Some comment below the sourceMappingURL\nvar foo = 0;');
  506. try {
  507. delete require.cache[require.resolve('./.generated')];
  508. require('./.generated').test();
  509. } catch (e) {
  510. compareLines(e.stack.split(/\r\n|\n/), expected);
  511. }
  512. fs.unlinkSync('.generated.js');
  513. });
  514. it('handleUncaughtExceptions is true with existing listener', function(done) {
  515. var source = [
  516. 'process.on("uncaughtException", function() { /* Silent */ });',
  517. 'function foo() { throw new Error("this is the error"); }',
  518. 'require("./source-map-support").install();',
  519. 'process.nextTick(foo);',
  520. '//@ sourceMappingURL=.generated.js.map'
  521. ];
  522. fs.writeFileSync('.original.js', 'this is the original code');
  523. fs.writeFileSync('.generated.js.map', createSingleLineSourceMap());
  524. fs.writeFileSync('.generated.js', source.join('\n'));
  525. child_process.exec('node ./.generated', function(error, stdout, stderr) {
  526. fs.unlinkSync('.generated.js');
  527. fs.unlinkSync('.generated.js.map');
  528. fs.unlinkSync('.original.js');
  529. assert.equal((stdout + stderr).trim(), '');
  530. done();
  531. });
  532. });