mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Also combine the two coffee registerCompiler calls into one it's a bad on-disk cache that should be improved to not write out all the unchanged files every time too. but the tests do show that it skips the unnecessary recompile less doesn't use an on-disk cache yet
260 lines
9.4 KiB
JavaScript
260 lines
9.4 KiB
JavaScript
var fs = Npm.require('fs');
|
|
var path = Npm.require('path');
|
|
var _ = Npm.require('underscore');
|
|
var sourcemap = Npm.require('source-map');
|
|
var LRU = Npm.require('lru-cache');
|
|
|
|
// The coffee-script compiler overrides Error.prepareStackTrace, mostly for the
|
|
// use of coffee.run which we don't use. This conflicts with the tool's use of
|
|
// Error.prepareStackTrace to properly show error messages in linked code. Save
|
|
// the tool's one and restore it after coffee-script clobbers it.
|
|
var prepareStackTrace = Error.prepareStackTrace;
|
|
var coffee = Npm.require('coffee-script');
|
|
Error.prepareStackTrace = prepareStackTrace;
|
|
|
|
var CACHE_SIZE = process.env.METEOR_COFFEESCRIPT_CACHE_SIZE || 1024*1024*10;
|
|
var CACHE_DEBUG = !! process.env.METEOR_TEST_PRINT_CACHE_DEBUG;
|
|
|
|
var stripExportedVars = function (source, exports) {
|
|
if (!exports || _.isEmpty(exports))
|
|
return source;
|
|
var lines = source.split("\n");
|
|
|
|
// We make the following assumptions, based on the output of CoffeeScript
|
|
// 1.7.1.
|
|
// - The var declaration in question is not indented and is the first such
|
|
// var declaration. (CoffeeScript only produces one var line at each
|
|
// scope and there's only one top-level scope.) All relevant variables
|
|
// are actually on this line.
|
|
// - The user hasn't used a ###-comment containing a line that looks like
|
|
// a var line, to produce something like
|
|
// /* bla
|
|
// var foo;
|
|
// */
|
|
// before an actual var line. (ie, we do NOT attempt to figure out if
|
|
// we're inside a /**/ comment, which is produced by ### comments.)
|
|
// - The var in question is not assigned to in the declaration, nor are any
|
|
// other vars on this line. (CoffeeScript does produce some assignments
|
|
// but only for internal helpers generated by CoffeeScript, and they end
|
|
// up on subsequent lines.)
|
|
// XXX relax these assumptions by doing actual JS parsing (eg with jsparse).
|
|
// I'd do this now, but there's no easy way to "unparse" a jsparse AST.
|
|
// Or alternatively, hack the compiler to allow us to specify unbound
|
|
// symbols directly.
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i];
|
|
var match = /^var (.+)([,;])$/.exec(line);
|
|
if (!match)
|
|
continue;
|
|
|
|
// If there's an assignment on this line, we assume that there are ONLY
|
|
// assignments and that the var we are looking for is not declared. (Part
|
|
// of our strong assumption about the layout of this code.)
|
|
if (match[1].indexOf('=') !== -1)
|
|
continue;
|
|
|
|
// We want to replace the line with something no shorter, so that all
|
|
// records in the source map continue to point at valid
|
|
// characters.
|
|
var replaceLine = function (x) {
|
|
if (x.length >= lines[i].length) {
|
|
lines[i] = x;
|
|
} else {
|
|
lines[i] = x + new Array(1 + (lines[i].length - x.length)).join(' ');
|
|
}
|
|
};
|
|
|
|
var vars = match[1].split(', ');
|
|
vars = _.difference(vars, exports);
|
|
if (!_.isEmpty(vars)) {
|
|
replaceLine("var " + vars.join(', ') + match[2]);
|
|
} else {
|
|
// We got rid of all the vars on this line. Drop the whole line if this
|
|
// didn't continue to the next line, otherwise keep just the 'var '.
|
|
if (match[2] === ';')
|
|
replaceLine('');
|
|
else
|
|
replaceLine('var');
|
|
}
|
|
break;
|
|
}
|
|
|
|
return lines.join('\n');
|
|
};
|
|
|
|
var addSharedHeader = function (source, sourceMap) {
|
|
var sourceMapJSON = JSON.parse(sourceMap);
|
|
|
|
// We want the symbol "share" to be visible to all CoffeeScript files in the
|
|
// package (and shared between them), but not visible to JavaScript
|
|
// files. (That's because we don't want to introduce two competing ways to
|
|
// make package-local variables into JS ("share" vs assigning to non-var
|
|
// variables).) The following hack accomplishes that: "__coffeescriptShare"
|
|
// will be visible at the package level and "share" at the file level. This
|
|
// should work both in "package" mode where __coffeescriptShare will be added
|
|
// as a var in the package closure, and in "app" mode where it will end up as
|
|
// a global.
|
|
//
|
|
// This ends in a newline to make the source map easier to adjust.
|
|
var header = ("__coffeescriptShare = typeof __coffeescriptShare === 'object' " +
|
|
"? __coffeescriptShare : {}; " +
|
|
"var share = __coffeescriptShare;\n");
|
|
|
|
// If the file begins with "use strict", we need to keep that as the first
|
|
// statement.
|
|
source = source.replace(/^(?:((['"])use strict\2;)\n)?/, function (match, useStrict) {
|
|
if (match) {
|
|
// There's a "use strict"; we keep this as the first statement and insert
|
|
// our header at the end of the line that it's on. This doesn't change
|
|
// line numbers or the part of the line that previous may have been
|
|
// annotated, so we don't need to update the source map.
|
|
return useStrict + " " + header;
|
|
} else {
|
|
// There's no use strict, so we can just add the header at the very
|
|
// beginning. This adds a line to the file, so we update the source map to
|
|
// add a single un-annotated line to the beginning.
|
|
sourceMapJSON.mappings = ";" + sourceMapJSON.mappings;
|
|
return header;
|
|
}
|
|
});
|
|
return {
|
|
source: source,
|
|
sourceMap: JSON.stringify(sourceMapJSON)
|
|
};
|
|
};
|
|
|
|
var CoffeeCompiler = function () {
|
|
var self = this;
|
|
// Maps from a cache key (encoding the source hash, exports, and the other
|
|
// options passed to coffee.compile) to a {source,sourceMap} object (both
|
|
// strings). Note that this is the result of both coffee.compile and the
|
|
// post-processing that we do.
|
|
self._cache = new LRU({
|
|
max: CACHE_SIZE,
|
|
// Cache is measured in bytes.
|
|
length: function (value) {
|
|
return value.source.length + value.sourceMap.length;
|
|
}
|
|
});
|
|
self._diskCache = null;
|
|
// For testing.
|
|
self._callCount = 0;
|
|
};
|
|
|
|
_.extend(CoffeeCompiler.prototype, {
|
|
processFilesForTarget: function (inputFiles) {
|
|
var self = this;
|
|
var cacheMisses = [];
|
|
|
|
inputFiles.forEach(function (inputFile) {
|
|
var source = inputFile.getContentsAsString();
|
|
var outputFilePath = inputFile.getPathInPackage() + ".js";
|
|
var extension = inputFile.getExtension();
|
|
var literate = extension !== 'coffee';
|
|
|
|
var options = {
|
|
bare: true,
|
|
filename: inputFile.getPathInPackage(),
|
|
literate: literate,
|
|
// Return a source map.
|
|
sourceMap: true,
|
|
// Include the original source in the source map (sourcesContent field).
|
|
inline: true,
|
|
// This becomes the "file" field of the source map.
|
|
generatedFile: "/" + outputFilePath,
|
|
// This becomes the "sources" field of the source map.
|
|
sourceFiles: [inputFile.getDisplayPath()]
|
|
};
|
|
|
|
var cacheKey = JSON.stringify([inputFile.getSourceHash(),
|
|
inputFile.getDeclaredExports(),
|
|
options]);
|
|
var sourceWithMap = self._cache.get(cacheKey);
|
|
if (! sourceWithMap) {
|
|
cacheMisses.push(inputFile.getDisplayPath());
|
|
try {
|
|
var output = coffee.compile(source, options);
|
|
} catch (e) {
|
|
inputFile.error({
|
|
message: e.message,
|
|
line: e.location && (e.location.first_line + 1),
|
|
column: e.location && (e.location.first_column + 1)
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
var stripped = stripExportedVars(
|
|
output.js,
|
|
_.pluck(inputFile.getDeclaredExports(), 'name'));
|
|
sourceWithMap = addSharedHeader(stripped, output.v3SourceMap);
|
|
self._cache.set(cacheKey, sourceWithMap);
|
|
}
|
|
|
|
inputFile.addJavaScript({
|
|
path: outputFilePath,
|
|
sourcePath: inputFile.getPathInPackage(),
|
|
data: sourceWithMap.source,
|
|
sourceMap: sourceWithMap.sourceMap,
|
|
bare: inputFile.getFileOptions().bare
|
|
});
|
|
});
|
|
|
|
// Rewrite the cache to disk.
|
|
// XXX BBP we should just write individual entries separately.
|
|
self._writeCache();
|
|
|
|
if (CACHE_DEBUG) {
|
|
cacheMisses.sort();
|
|
console.log("Ran coffee.compile (#%s) on: %s",
|
|
++self._callCount, JSON.stringify(cacheMisses));
|
|
}
|
|
},
|
|
setDiskCacheDirectory: function (diskCache) {
|
|
var self = this;
|
|
if (self._diskCache)
|
|
throw Error("setDiskCacheDirectory called twice?");
|
|
self._diskCache = diskCache;
|
|
self._readCache();
|
|
},
|
|
// XXX BBP this is an inefficiently designed cache that will cause quadratic
|
|
// behavior due to writing the whole cache on each write, and has no error
|
|
// handling, and uses sync, and has an exists/read race condition, and might
|
|
// not work on Windows
|
|
_cacheFile: function () {
|
|
var self = this;
|
|
return path.join(self._diskCache, 'cache.json');
|
|
},
|
|
_readCache: function () {
|
|
var self = this;
|
|
var cacheFile = self._cacheFile();
|
|
if (! fs.existsSync(cacheFile))
|
|
return;
|
|
var cacheJSON = JSON.parse(fs.readFileSync(cacheFile));
|
|
_.each(cacheJSON, function (value, cacheKey) {
|
|
self._cache.set(cacheKey, value);
|
|
});
|
|
if (CACHE_DEBUG) {
|
|
console.log("Loaded coffeescript cache");
|
|
}
|
|
},
|
|
_writeCache: function () {
|
|
var self = this;
|
|
if (! self._diskCache)
|
|
return;
|
|
var cacheJSON = {};
|
|
self._cache.forEach(function (value, cacheKey) {
|
|
cacheJSON[cacheKey] = value;
|
|
});
|
|
fs.writeFileSync(self._cacheFile(), JSON.stringify(cacheJSON));
|
|
}
|
|
});
|
|
|
|
Plugin.registerCompiler({
|
|
extensions: ['coffee', 'litcoffee', 'coffee.md']
|
|
}, function () {
|
|
return new CoffeeCompiler();
|
|
});
|
|
|