aboutsummaryrefslogtreecommitdiff
path: root/tools/source-maps/sourcemapper.js
blob: 5193b3a82773c0d4c402cba3d34caefce19be7a4 (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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#! /usr/bin/env node

"use strict";

var fs = require('fs');
var path = require('path');

var START_MARKER = '// EMSCRIPTEN_START_FUNCS\n';
var END_MARKER = '// EMSCRIPTEN_END_FUNCS\n';

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

// For a minor optimization, only do win32->unix normalization if we are actually on Windows,
// which avoids redundantly scanning files if not needed.
var isWindows = (process.platform === 'win32');

var unixPathRe = new RegExp('\\\\', 'g');
// Returns the given (possibly Windows) path p normalized to unix path separators '/'.
function toUnixPath(p) {
  if (isWindows) {
    return p.replace(unixPathRe, '/');
  } else {
    return p;
  }
}

var unixLineEndRe = new RegExp('\r\n', 'g');
// Returns the given (possibly Windows) text data t normalized to unix line endings '\n'.
function toUnixLineEnding(t) {
  if (isWindows) {
    return t.replace(unixLineEndRe, '\n');
  } else {
    return t;
  }
}

// If path "p2" is a relative path, joins paths p1 and p2 to form "p1/p2". If p2 is an absolute path, "p2" is returned.
function joinPath(p1, p2) {
  if (p2[0] == '/' || (p2.length >= 3 && p2[1] == ':' && (p2[2] == '/' || p2[2] == '\\'))) // Is p2 an absolute path?
    return p2;
  else
    return toUnixPath(path.join(p1, p2));
}

/*
 * 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(START_MARKER);
  var functionEndIdx = source.lastIndexOf(END_MARKER);
  var lineCount = countLines(source.slice(0, functionStartIdx)) + 2;

  for (var i = functionStartIdx + START_MARKER.length; i < functionEndIdx; i++) {
    var c = source[i];
    var nextC = source[i+1];
    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;
    }

    if (c === '\n') lineCount++;
  }
}

function getMappings(source) {
  // generatedLineNumber -> { originalLineNumber, originalFileName }
  var mappings = {};
  extractComments(source, function(content, generatedLineNumber) {
    var matches = /@line (\d+)(?: "([^"]*)")?/.exec(content);
    if (matches === null) return;
    var originalFileName = matches[2];
    mappings[generatedLineNumber] = {
      originalLineNumber: parseInt(matches[1], 10),
      originalFileName: originalFileName
    }
  });
  return mappings;
}

function generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset) {
  var SourceMapGenerator = require('source-map').SourceMapGenerator;

  var generator = new SourceMapGenerator({ file: mapFileBaseName });
  var seenFiles = Object.create(null);

  for (var generatedLineNumber in mappings) {
    var generatedLineNumber = parseInt(generatedLineNumber, 10);
    var mapping = mappings[generatedLineNumber];
    var originalFileName = mapping.originalFileName;
    generator.addMapping({
      generated: { line: generatedLineNumber + generatedLineOffset, column: 0 },
      original: { line: mapping.originalLineNumber, column: 0 },
      source: originalFileName
    });

    // we could call setSourceContent repeatedly, but readFileSync is slow, so
    // avoid doing it unnecessarily
    if (!(originalFileName in seenFiles)) {
      seenFiles[originalFileName] = true;
      var rootedPath = joinPath(sourceRoot, originalFileName);
      try {
        generator.setSourceContent(originalFileName, fs.readFileSync(rootedPath, 'utf-8'));
      } catch (e) {
        console.warn("sourcemapper: Unable to find original file for " + originalFileName +
          " at " + rootedPath);
      }
    }
  }

  fs.writeFileSync(mapFileBaseName + '.map', generator.toString());
}

function appendMappingURL(fileName, source, mapFileName) {
  var lastLine = source.slice(source.lastIndexOf('\n'));
  if (!/sourceMappingURL/.test(lastLine))
    fs.appendFileSync(fileName, '//# sourceMappingURL=' + path.basename(mapFileName));
}

function parseArgs(args) {
  var rv = { _: [] }; // unflagged args go into `_`; similar to the optimist library
  for (var i = 0; i < args.length; i++) {
    if (/^--/.test(args[i])) rv[args[i].slice(2)] = args[++i];
    else rv._.push(args[i]);
  }
  return rv;
}

if (require.main === module) {
  if (process.argv.length < 3) {
    console.log('Usage: ./sourcemapper.js <original js> <optimized js file ...> \\\n' +
                '\t--sourceRoot <default "."> \\\n' +
                '\t--mapFileBaseName <default `filename`> \\\n' +
                '\t--offset <default 0>');
    process.exit(1);
  } else {
    var opts = parseArgs(process.argv.slice(2));
    var fileName = opts._[0];
    var sourceRoot = opts.sourceRoot ? toUnixPath(opts.sourceRoot) : ".";
    var mapFileBaseName = toUnixPath(opts.mapFileBaseName ? opts.mapFileBaseName : fileName);
    var generatedLineOffset = opts.offset ? parseInt(opts.offset, 10) : 0;

    var generatedSource = toUnixLineEnding(fs.readFileSync(fileName, 'utf-8'));
    var source = generatedSource;
    var mappings = getMappings(generatedSource);
    for (var i = 1, l = opts._.length; i < l; i ++) {
      var optimizedSource = toUnixLineEnding(fs.readFileSync(opts._[i], 'utf-8'))
      var optimizedMappings = getMappings(optimizedSource);
      var newMappings = {};
      // uglify processes the code between EMSCRIPTEN_START_FUNCS and
      // EMSCRIPTEN_END_FUNCS, so its line number maps are relative to those
      // markers. we correct for that here. +2 = 1 for the newline in the marker
      // and 1 to make it a 1-based index.
      var startFuncsLineNumber = countLines(source.slice(0, source.indexOf(START_MARKER))) + 2;
      for (var line in optimizedMappings) {
        var originalLineNumber = optimizedMappings[line].originalLineNumber + startFuncsLineNumber;
        if (originalLineNumber in mappings) {
          newMappings[line] = mappings[originalLineNumber];
        }
      }
      mappings = newMappings;
      source = optimizedSource;
    }

    generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset);
    appendMappingURL(opts._[opts._.length - 1], generatedSource,
                     opts.mapFileBaseName + '.map');
  }
}