aboutsummaryrefslogtreecommitdiff
path: root/tools/sourcemapper.js
blob: 3d8dbe99c8611dec7eb33f02cc06e16c69e5dae9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#! /usr/bin/env node

"use strict";

function countLines(s) {
  var count = 0;
  for (var i = 0, l = s.length; i < l; i ++) {
    if (s[i] === '\n') count++;
  }
  return count;
}

/*
 * Extracts the line (not block) comments from the generated function code and
 * invokes commentHandler with (comment content, line number of comment). This
 * function implements a simplistic lexer with the following assumptions:
 *     1. "// EMSCRIPTEN_START_FUNCS" and "// EMSCRIPTEN_END_FUNCS" are unique
 *     markers for separating library code from generated function code. Things
 *     will break if they appear as part of a string in library code (but OK if
 *     they occur in generated code).
 *     2. Between these two markers, no regexes are used.
 */
function extractComments(source, commentHandler) {
  var state = 'code';
  var commentContent = '';
  var functionStartIdx = source.indexOf('// EMSCRIPTEN_START_FUNCS');
  var functionEndIdx = source.lastIndexOf('// EMSCRIPTEN_END_FUNCS');
  var lineCount = countLines(source.slice(0, functionStartIdx));

  for (var i = functionStartIdx; i < functionEndIdx; i++) {
    var c = source[i];
    var nextC = source[i+1];
    if (c === '\n') lineCount++;
    switch (state) {
      case 'code':
          if (c === '/') {
            if (nextC === '/') { state = 'lineComment'; i++; }
            else if (nextC === '*') { state = 'blockComment'; i++; }
          }
          else if (c === '"') state = 'doubleQuotedString';
          else if (c === '\'') state = 'singleQuotedString';
          break;
      case 'lineComment':
          if (c === '\n') {
            state = 'code';
            commentHandler(commentContent, lineCount);
            commentContent = "";
          } else {
            commentContent += c;
          }
          break;
      case 'blockComment':
          if (c === '*' && nextC === '/') state = 'code';
      case 'singleQuotedString':
          if (c === '\\') i++;
          else if (c === '\'') state = 'code';
          break;
      case 'doubleQuotedString':
          if (c === '\\') i++;
          else if (c === '"') state = 'code';
          break;
      }
  }
}

function generateMap(fileName, sourceRoot, mapFileBaseName) {
  var fs = require('fs');
  var path = require('path');
  var SourceMapGenerator = require('source-map').SourceMapGenerator;

  var generator = new SourceMapGenerator({ file: fileName });
  var generatedSource = fs.readFileSync(fileName, 'utf-8');
  var seenFiles = Object.create(null);

  extractComments(generatedSource, function(content, generatedLineNumber) {
    var matches = /@line (\d+) "([^"]*)"/.exec(content);
    if (matches === null) return;
    var originalLineNumber = parseInt(matches[1], 10);
    var originalFileName = matches[2];

    if (!(originalFileName in seenFiles)) {
      seenFiles[originalFileName] = true;
      try {
        generator.setSourceContent(originalFileName,
                                   fs.readFileSync(sourceRoot + "/" + originalFileName));
      } catch (e) {
        console.warn("Unable to find original file for " + originalFileName);
      }
    }

    generator.addMapping({
      generated: { line: generatedLineNumber, column: 0 },
      original: { line: originalLineNumber, column: 0 },
      source: originalFileName
    });
  });

  var mapFileName = mapFileBaseName + '.map';
  fs.writeFileSync(mapFileName, generator.toString());

  var lastLine = generatedSource.slice(generatedSource.lastIndexOf('\n'));
  if (!/sourceMappingURL/.test(lastLine))
    fs.appendFileSync(fileName, '//@ sourceMappingURL=' + path.basename(mapFileName));
}

if (require.main === module) {
  if (process.argv.length < 3) {
    console.log('Usage: ./sourcemapper.js <filename> <source root (default: .)> ' +
                '<map file basename (default: filename)>');
    process.exit(1);
  } else {
    var sourceRoot = process.argv.length > 3 ? process.argv[3] : ".";
    var mapFileBaseName = process.argv.length > 4 ? process.argv[4] : process.argv[2];
    generateMap(process.argv[2], sourceRoot, mapFileBaseName);
  }
}