var fs = Npm.require('fs'); var path = Npm.require('path'); var coffee = Npm.require('coffee-script'); var _ = Npm.require('underscore'); 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.6.3. // - 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. var foundVarLine = false; lines = _.map(lines, function (line) { if (foundVarLine) return line; var match = /^var (.+)([,;])$/.exec(line); if (!match) return line; foundVarLine = true; // 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) return line; var vars = match[1].split(', '); vars = _.difference(vars, exports); if (!_.isEmpty(vars)) return "var " + vars.join(', ') + match[2]; // 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 '. return match[2] === ';' ? '' : 'var'; }); return lines.join('\n'); }; var addSharedHeader = function (source) { // 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 in case the first line is a linker @comment, which // should be at the beginning of a line. 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. return source.replace(/^(?:(['"])use strict\1;\n)?/, function (match) { return match + header; }); }; var handler = function (compileStep) { var source = compileStep.read().toString('utf8'); var options = { bare: true, filename: compileStep.inputPath, literate: path.extname(compileStep.inputPath) === '.litcoffee' }; try { var output = coffee.compile(source, options); } catch (e) { // XXX better error handling, once the Plugin interface support it throw new Error( compileStep.inputPath + ':' + (e.location ? (e.location.first_line + ': ') : ' ') + e.message ); } compileStep.addJavaScript({ path: compileStep.inputPath + ".js", sourcePath: compileStep.inputPath, data: output, lineForLine: false, linkerUnitTransform: function (source, exports) { return addSharedHeader(stripExportedVars(source, exports)); } }); }; Plugin.registerSourceHandler("coffee", handler); Plugin.registerSourceHandler("litcoffee", handler);