-
Notifications
You must be signed in to change notification settings - Fork 2k
/
coffeescript.js
437 lines (410 loc) · 17.7 KB
/
coffeescript.js
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
// Generated by CoffeeScript 2.0.2
(function() {
// CoffeeScript can be used both on the server, as a command-line compiler based
// on Node.js/V8, or to run CoffeeScript directly in the browser. This module
// contains the main entry functions for tokenizing, parsing, and compiling
// source CoffeeScript into JavaScript.
var FILE_EXTENSIONS, Lexer, SourceMap, base64encode, checkShebangLine, compile, formatSourcePosition, getSourceMap, helpers, lexer, packageJson, parser, sourceMaps, sources, withPrettyErrors,
indexOf = [].indexOf;
({Lexer} = require('./lexer'));
({parser} = require('./parser'));
helpers = require('./helpers');
SourceMap = require('./sourcemap');
// Require `package.json`, which is two levels above this file, as this file is
// evaluated from `lib/coffeescript`.
packageJson = require('../../package.json');
// The current CoffeeScript version number.
exports.VERSION = packageJson.version;
exports.FILE_EXTENSIONS = FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md'];
// Expose helpers for testing.
exports.helpers = helpers;
// Function that allows for btoa in both nodejs and the browser.
base64encode = function(src) {
switch (false) {
case typeof Buffer !== 'function':
return Buffer.from(src).toString('base64');
case typeof btoa !== 'function':
// The contents of a `<script>` block are encoded via UTF-16, so if any extended
// characters are used in the block, btoa will fail as it maxes out at UTF-8.
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
// for the gory details, and for the solution implemented here.
return btoa(encodeURIComponent(src).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
default:
throw new Error('Unable to base64 encode inline sourcemap.');
}
};
// Function wrapper to add source file information to SyntaxErrors thrown by the
// lexer/parser/compiler.
withPrettyErrors = function(fn) {
return function(code, options = {}) {
var err;
try {
return fn.call(this, code, options);
} catch (error) {
err = error;
if (typeof code !== 'string') { // Support `CoffeeScript.nodes(tokens)`.
throw err;
}
throw helpers.updateSyntaxError(err, code, options.filename);
}
};
};
// For each compiled file, save its source in memory in case we need to
// recompile it later. We might need to recompile if the first compilation
// didn’t create a source map (faster) but something went wrong and we need
// a stack trace. Assuming that most of the time, code isn’t throwing
// exceptions, it’s probably more efficient to compile twice only when we
// need a stack trace, rather than always generating a source map even when
// it’s not likely to be used. Save in form of `filename`: [`(source)`]
sources = {};
// Also save source maps if generated, in form of `(source)`: [`(source map)`].
sourceMaps = {};
// Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
// If `options.sourceMap` is specified, then `options.filename` must also be
// specified. All options that can be passed to `SourceMap#generate` may also
// be passed here.
// This returns a javascript string, unless `options.sourceMap` is passed,
// in which case this returns a `{js, v3SourceMap, sourceMap}`
// object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for
// doing programmatic lookups.
exports.compile = compile = withPrettyErrors(function(code, options = {}) {
var currentColumn, currentLine, encoded, filename, fragment, fragments, generateSourceMap, header, i, j, js, len, len1, map, newLines, ref, ref1, sourceMapDataURI, sourceURL, token, tokens, transpiler, transpilerOutput, v3SourceMap;
// Clone `options`, to avoid mutating the `options` object passed in.
options = Object.assign({}, options);
// Always generate a source map if no filename is passed in, since without a
// a filename we have no way to retrieve this source later in the event that
// we need to recompile it to get a source map for `prepareStackTrace`.
generateSourceMap = options.sourceMap || options.inlineMap || (options.filename == null);
filename = options.filename || '<anonymous>';
checkShebangLine(filename, code);
if (sources[filename] == null) {
sources[filename] = [];
}
sources[filename].push(code);
if (generateSourceMap) {
map = new SourceMap;
}
tokens = lexer.tokenize(code, options);
// Pass a list of referenced variables, so that generated variables won’t get
// the same name.
options.referencedVars = (function() {
var i, len, results;
results = [];
for (i = 0, len = tokens.length; i < len; i++) {
token = tokens[i];
if (token[0] === 'IDENTIFIER') {
results.push(token[1]);
}
}
return results;
})();
// Check for import or export; if found, force bare mode.
if (!((options.bare != null) && options.bare === true)) {
for (i = 0, len = tokens.length; i < len; i++) {
token = tokens[i];
if ((ref = token[0]) === 'IMPORT' || ref === 'EXPORT') {
options.bare = true;
break;
}
}
}
fragments = parser.parse(tokens).compileToFragments(options);
currentLine = 0;
if (options.header) {
currentLine += 1;
}
if (options.shiftLine) {
currentLine += 1;
}
currentColumn = 0;
js = "";
for (j = 0, len1 = fragments.length; j < len1; j++) {
fragment = fragments[j];
// Update the sourcemap with data from each fragment.
if (generateSourceMap) {
// Do not include empty, whitespace, or semicolon-only fragments.
if (fragment.locationData && !/^[;\s]*$/.test(fragment.code)) {
map.add([fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn], {
noReplace: true
});
}
newLines = helpers.count(fragment.code, "\n");
currentLine += newLines;
if (newLines) {
currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1);
} else {
currentColumn += fragment.code.length;
}
}
// Copy the code from each fragment into the final JavaScript.
js += fragment.code;
}
if (options.header) {
header = `Generated by CoffeeScript ${this.VERSION}`;
js = `// ${header}\n${js}`;
}
if (generateSourceMap) {
v3SourceMap = map.generate(options, code);
if (sourceMaps[filename] == null) {
sourceMaps[filename] = [];
}
sourceMaps[filename].push(map);
}
if (options.transpile) {
if (typeof options.transpile !== 'object') {
// This only happens if run via the Node API and `transpile` is set to
// something other than an object.
throw new Error('The transpile option must be given an object with options to pass to Babel');
}
// Get the reference to Babel that we have been passed if this compiler
// is run via the CLI or Node API.
transpiler = options.transpile.transpile;
delete options.transpile.transpile;
// See https://github.com/babel/babel/issues/827#issuecomment-77573107:
// Babel can take a v3 source map object as input in `inputSourceMap`
// and it will return an *updated* v3 source map object in its output.
if (v3SourceMap && (options.transpile.inputSourceMap == null)) {
options.transpile.inputSourceMap = v3SourceMap;
}
transpilerOutput = transpiler(js, options.transpile);
js = transpilerOutput.code;
if (v3SourceMap && transpilerOutput.map) {
v3SourceMap = transpilerOutput.map;
}
}
if (options.inlineMap) {
encoded = base64encode(JSON.stringify(v3SourceMap));
sourceMapDataURI = `//# sourceMappingURL=data:application/json;base64,${encoded}`;
sourceURL = `//# sourceURL=${(ref1 = options.filename) != null ? ref1 : 'coffeescript'}`;
js = `${js}\n${sourceMapDataURI}\n${sourceURL}`;
}
if (options.sourceMap) {
return {
js,
sourceMap: map,
v3SourceMap: JSON.stringify(v3SourceMap, null, 2)
};
} else {
return js;
}
});
// Tokenize a string of CoffeeScript code, and return the array of tokens.
exports.tokens = withPrettyErrors(function(code, options) {
return lexer.tokenize(code, options);
});
// Parse a string of CoffeeScript code or an array of lexed tokens, and
// return the AST. You can then compile it by calling `.compile()` on the root,
// or traverse it by using `.traverseChildren()` with a callback.
exports.nodes = withPrettyErrors(function(source, options) {
if (typeof source === 'string') {
return parser.parse(lexer.tokenize(source, options));
} else {
return parser.parse(source);
}
});
// This file used to export these methods; leave stubs that throw warnings
// instead. These methods have been moved into `index.coffee` to provide
// separate entrypoints for Node and non-Node environments, so that static
// analysis tools don’t choke on Node packages when compiling for a non-Node
// environment.
exports.run = exports.eval = exports.register = function() {
throw new Error('require index.coffee, not this file');
};
// Instantiate a Lexer for our use here.
lexer = new Lexer;
// The real Lexer produces a generic stream of tokens. This object provides a
// thin wrapper around it, compatible with the Jison API. We can then pass it
// directly as a "Jison lexer".
parser.lexer = {
lex: function() {
var tag, token;
token = parser.tokens[this.pos++];
if (token) {
[tag, this.yytext, this.yylloc] = token;
parser.errorToken = token.origin || token;
this.yylineno = this.yylloc.first_line;
} else {
tag = '';
}
return tag;
},
setInput: function(tokens) {
parser.tokens = tokens;
return this.pos = 0;
},
upcomingInput: function() {
return '';
}
};
// Make all the AST nodes visible to the parser.
parser.yy = require('./nodes');
// Override Jison's default error handling function.
parser.yy.parseError = function(message, {token}) {
var errorLoc, errorTag, errorText, errorToken, tokens;
// Disregard Jison's message, it contains redundant line number information.
// Disregard the token, we take its value directly from the lexer in case
// the error is caused by a generated token which might refer to its origin.
({errorToken, tokens} = parser);
[errorTag, errorText, errorLoc] = errorToken;
errorText = (function() {
switch (false) {
case errorToken !== tokens[tokens.length - 1]:
return 'end of input';
case errorTag !== 'INDENT' && errorTag !== 'OUTDENT':
return 'indentation';
case errorTag !== 'IDENTIFIER' && errorTag !== 'NUMBER' && errorTag !== 'INFINITY' && errorTag !== 'STRING' && errorTag !== 'STRING_START' && errorTag !== 'REGEX' && errorTag !== 'REGEX_START':
return errorTag.replace(/_START$/, '').toLowerCase();
default:
return helpers.nameWhitespaceCharacter(errorText);
}
})();
// The second argument has a `loc` property, which should have the location
// data for this token. Unfortunately, Jison seems to send an outdated `loc`
// (from the previous token), so we take the location information directly
// from the lexer.
return helpers.throwSyntaxError(`unexpected ${errorText}`, errorLoc);
};
// Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js
// Modified to handle sourceMap
formatSourcePosition = function(frame, getSourceMapping) {
var as, column, fileLocation, filename, functionName, isConstructor, isMethodCall, line, methodName, source, tp, typeName;
filename = void 0;
fileLocation = '';
if (frame.isNative()) {
fileLocation = "native";
} else {
if (frame.isEval()) {
filename = frame.getScriptNameOrSourceURL();
if (!filename) {
fileLocation = `${frame.getEvalOrigin()}, `;
}
} else {
filename = frame.getFileName();
}
filename || (filename = "<anonymous>");
line = frame.getLineNumber();
column = frame.getColumnNumber();
// Check for a sourceMap position
source = getSourceMapping(filename, line, column);
fileLocation = source ? `${filename}:${source[0]}:${source[1]}` : `${filename}:${line}:${column}`;
}
functionName = frame.getFunctionName();
isConstructor = frame.isConstructor();
isMethodCall = !(frame.isToplevel() || isConstructor);
if (isMethodCall) {
methodName = frame.getMethodName();
typeName = frame.getTypeName();
if (functionName) {
tp = as = '';
if (typeName && functionName.indexOf(typeName)) {
tp = `${typeName}.`;
}
if (methodName && functionName.indexOf(`.${methodName}`) !== functionName.length - methodName.length - 1) {
as = ` [as ${methodName}]`;
}
return `${tp}${functionName}${as} (${fileLocation})`;
} else {
return `${typeName}.${methodName || '<anonymous>'} (${fileLocation})`;
}
} else if (isConstructor) {
return `new ${functionName || '<anonymous>'} (${fileLocation})`;
} else if (functionName) {
return `${functionName} (${fileLocation})`;
} else {
return fileLocation;
}
};
getSourceMap = function(filename, line, column) {
var answer, i, map, ref, ref1, sourceLocation;
if (!(filename === '<anonymous>' || (ref = filename.slice(filename.lastIndexOf('.')), indexOf.call(FILE_EXTENSIONS, ref) >= 0))) {
// Skip files that we didn’t compile, like Node system files that appear in
// the stack trace, as they never have source maps.
return null;
}
if (filename !== '<anonymous>' && (sourceMaps[filename] != null)) {
return sourceMaps[filename][sourceMaps[filename].length - 1];
// CoffeeScript compiled in a browser or via `CoffeeScript.compile` or `.run`
// may get compiled with `options.filename` that’s missing, which becomes
// `<anonymous>`; but the runtime might request the stack trace with the
// filename of the script file. See if we have a source map cached under
// `<anonymous>` that matches the error.
} else if (sourceMaps['<anonymous>'] != null) {
ref1 = sourceMaps['<anonymous>'];
// Work backwards from the most recent anonymous source maps, until we find
// one that works. This isn’t foolproof; there is a chance that multiple
// source maps will have line/column pairs that match. But we have no other
// way to match them. `frame.getFunction().toString()` doesn’t always work,
// and it’s not foolproof either.
for (i = ref1.length - 1; i >= 0; i += -1) {
map = ref1[i];
sourceLocation = map.sourceLocation([line - 1, column - 1]);
if (((sourceLocation != null ? sourceLocation[0] : void 0) != null) && (sourceLocation[1] != null)) {
return map;
}
}
}
// If all else fails, recompile this source to get a source map. We need the
// previous section (for `<anonymous>`) despite this option, because after it
// gets compiled we will still need to look it up from
// `sourceMaps['<anonymous>']` in order to find and return it. That’s why we
// start searching from the end in the previous block, because most of the
// time the source map we want is the last one.
if (sources[filename] != null) {
answer = compile(sources[filename][sources[filename].length - 1], {
filename: filename,
sourceMap: true,
literate: helpers.isLiterate(filename)
});
return answer.sourceMap;
} else {
return null;
}
};
// Based on [michaelficarra/CoffeeScriptRedux](http://goo.gl/ZTx1p)
// NodeJS / V8 have no support for transforming positions in stack traces using
// sourceMap, so we must monkey-patch Error to display CoffeeScript source
// positions.
Error.prepareStackTrace = function(err, stack) {
var frame, frames, getSourceMapping;
getSourceMapping = function(filename, line, column) {
var answer, sourceMap;
sourceMap = getSourceMap(filename, line, column);
if (sourceMap != null) {
answer = sourceMap.sourceLocation([line - 1, column - 1]);
}
if (answer != null) {
return [answer[0] + 1, answer[1] + 1];
} else {
return null;
}
};
frames = (function() {
var i, len, results;
results = [];
for (i = 0, len = stack.length; i < len; i++) {
frame = stack[i];
if (frame.getFunction() === exports.run) {
break;
}
results.push(` at ${formatSourcePosition(frame, getSourceMapping)}`);
}
return results;
})();
return `${err.toString()}\n${frames.join('\n')}\n`;
};
checkShebangLine = function(file, input) {
var args, firstLine, ref, rest;
firstLine = input.split(/$/m)[0];
rest = firstLine != null ? firstLine.match(/^#!\s*([^\s]+\s*)(.*)/) : void 0;
args = rest != null ? (ref = rest[2]) != null ? ref.split(/\s/).filter(function(s) {
return s !== '';
}) : void 0 : void 0;
if ((args != null ? args.length : void 0) > 1) {
console.error('The script to be run begins with a shebang line with more than one\nargument. This script will fail on platforms such as Linux which only\nallow a single argument.');
console.error(`The shebang line was: '${firstLine}' in file '${file}'`);
return console.error(`The arguments were: ${JSON.stringify(args)}`);
}
};
}).call(this);