UNPKG

7.48 kBJavaScriptView Raw
1/*
2 * grunt-contrib-coffee
3 * http://gruntjs.com/
4 *
5 * Copyright (c) 2014 Eric Woroshow, contributors
6 * Licensed under the MIT license.
7 */
8
9'use strict';
10
11module.exports = function(grunt) {
12 var path = require('path');
13 var chalk = require('chalk');
14 var _ = require('lodash');
15
16 grunt.registerMultiTask('coffee', 'Compile CoffeeScript files into JavaScript', function() {
17 var options = this.options({
18 bare: false,
19 join: false,
20 sourceMap: false,
21 separator: grunt.util.linefeed,
22 });
23
24 options.separator = grunt.util.normalizelf(options.separator);
25
26 this.files.forEach(function(f) {
27 var validFiles = removeInvalidFiles(f);
28
29 if (options.sourceMap === true) {
30 var paths = createOutputPaths(f.dest);
31 // add sourceMapDir to options object
32 var fileOptions = _.extend({ sourceMapDir: paths.destDir }, options);
33 writeFileAndMap(paths, compileWithMaps(validFiles, fileOptions, paths), fileOptions);
34 } else if (options.join === true) {
35 writeFile(f.dest, concatInput(validFiles, options));
36 } else {
37 writeFile(f.dest, concatOutput(validFiles, options));
38 }
39 });
40 });
41
42 var isLiterate = function(ext) {
43 return (ext === ".litcoffee" || ext === ".md");
44 };
45
46 var removeInvalidFiles = function(files) {
47 return files.src.filter(function(filepath) {
48 if (!grunt.file.exists(filepath)) {
49 grunt.log.warn('Source file "' + filepath + '" not found.');
50 return false;
51 } else {
52 return true;
53 }
54 });
55 };
56
57 var createOutputPaths = function(destination) {
58 var fileName = path.basename(destination, path.extname(destination));
59 return {
60 dest: destination,
61 destName: fileName,
62 destDir: appendTrailingSlash(path.dirname(destination)),
63 mapFileName: fileName + '.js.map'
64 };
65 };
66
67 var appendTrailingSlash = function(path) {
68 if (path.length > 0) {
69 return path + '/';
70 } else {
71 return path;
72 }
73 };
74
75 var compileWithMaps = function(files, options, paths) {
76 if (!hasUniformExtensions(files)) {
77 return;
78 }
79
80 var mapOptions, filepath;
81
82 if (files.length > 1) {
83 mapOptions = createOptionsForJoin(files, paths, options.separator);
84 } else {
85 mapOptions = createOptionsForFile(files[0], paths);
86 filepath = files[0];
87 }
88
89 options = _.extend({
90 generatedFile: path.basename(paths.dest),
91 sourceRoot: mapOptions.sourceRoot,
92 sourceFiles: mapOptions.sourceFiles
93 }, options);
94
95 var output = compileCoffee(mapOptions.code, options, filepath);
96 appendFooter(output, paths, options);
97 return output;
98 };
99
100 var hasUniformExtensions = function(files) {
101 // get all extensions for input files
102 var extensions = _.uniq(files.map(path.extname));
103
104 if (extensions.length > 1) {
105 grunt.fail.warn('Join and sourceMap options require input files share the same extension (found ' + extensions.join(', ') + ').');
106 return false;
107 } else {
108 return true;
109 }
110 };
111
112 var createOptionsForJoin = function(files, paths, separator) {
113 var code = concatFiles(files, separator);
114 var targetFileName = paths.destName + '.src.coffee';
115 grunt.file.write(paths.destDir + targetFileName, code);
116
117 return {
118 code: code,
119 sourceFiles: [targetFileName],
120 sourceRoot: ''
121 };
122 };
123
124 var concatFiles = function(files, separator) {
125 return files.map(grunt.file.read).join(separator);
126 };
127
128 var createOptionsForFile = function(file, paths) {
129 return {
130 code: grunt.file.read(file),
131 sourceFiles: [path.basename(file)],
132 sourceRoot: appendTrailingSlash(path.relative(paths.destDir, path.dirname(file)))
133 };
134 };
135
136 var appendFooter = function(output, paths, options) {
137 // we need sourceMappingURL to be relative to the js path
138 var sourceMappingDir = paths.destDir.replace(/[^/]+/g, '..') + options.sourceMapDir;
139 // add sourceMappingURL to file footer
140 output.js = output.js + '\n//# sourceMappingURL=' + sourceMappingDir + paths.mapFileName + '\n';
141 };
142
143 var concatInput = function(files, options) {
144 if (hasUniformExtensions(files)) {
145 var code = concatFiles(files, options.separator);
146 return compileCoffee(code, options);
147 }
148 };
149
150 var concatOutput = function(files, options) {
151 return files.map(function(filepath) {
152 var code = grunt.file.read(filepath);
153 return compileCoffee(code, options, filepath);
154 }).join(options.separator);
155 };
156
157 var compileCoffee = function(code, options, filepath) {
158 var coffeeOptions = _.clone(options);
159 if (filepath) {
160 coffeeOptions.filename = filepath;
161 coffeeOptions.literate = isLiterate(path.extname(filepath));
162 }
163
164 try {
165 return require('coffee-script').compile(code, coffeeOptions);
166 } catch (e) {
167 if (e.location == null ||
168 e.location.first_column == null ||
169 e.location.first_line == null) {
170 grunt.log.error('Got an unexpected exception ' +
171 'from the coffee-script compiler. ' +
172 'The original exception was: ' +
173 e);
174 grunt.log.error('(The coffee-script compiler should not raise *unexpected* exceptions. ' +
175 'You can file this error as an issue of the coffee-script compiler: ' +
176 'https://github.com/jashkenas/coffee-script/issues)');
177 } else {
178 var firstColumn = e.location.first_column;
179 var firstLine = e.location.first_line;
180 var codeLine = code.split('\n')[firstLine];
181 var errorArrows = chalk.red('>>') + ' ';
182 var offendingCharacter;
183
184 if (firstColumn < codeLine.length) {
185 offendingCharacter = chalk.red(codeLine[firstColumn]);
186 } else {
187 offendingCharacter = '';
188 }
189
190 grunt.log.error(e);
191 grunt.log.error('In file: ' + filepath);
192 grunt.log.error('On line: ' + firstLine);
193 // log erroneous line and highlight offending character
194 // grunt.log.error trims whitespace so we have to use grunt.log.writeln
195 grunt.log.writeln(errorArrows + codeLine.substring(0, firstColumn) +
196 offendingCharacter + codeLine.substring(firstColumn + 1));
197 grunt.log.writeln(errorArrows + grunt.util.repeat(firstColumn, ' ') +
198 chalk.red('^'));
199 }
200 grunt.fail.warn('CoffeeScript failed to compile.');
201 }
202 };
203
204 var writeFileAndMap = function(paths, output, options) {
205 if (!output || output.js.length === 0) {
206 warnOnEmptyFile(paths.dest);
207 return;
208 }
209
210 writeCompiledFile(paths.dest, output.js);
211 writeSourceMapFile(options.sourceMapDir + paths.mapFileName, output.v3SourceMap);
212 };
213
214 var warnOnEmptyFile = function(path) {
215 grunt.log.warn('Destination "' + path + '" not written because compiled files were empty.');
216 };
217
218 var writeFile = function(path, output) {
219 if (output.length < 1) {
220 warnOnEmptyFile(path);
221 return false;
222 } else {
223 grunt.file.write(path, output);
224 return true;
225 }
226 };
227
228 var writeCompiledFile = function(path, output) {
229 if (writeFile(path, output)) {
230 grunt.log.writeln('File ' + chalk.cyan(path) + ' created.');
231 }
232 };
233 var writeSourceMapFile = function(path, output) {
234 if (writeFile(path, output)) {
235 grunt.log.writeln('File ' + chalk.cyan(path) + ' created (source map).');
236 }
237 };
238};