UNPKG

20.5 kBJavaScriptView Raw
1'use strict';
2
3var cb = require('gulp-cache-breaker');
4var autoprefixer = require('gulp-autoprefixer');
5var browserify = require('browserify');
6var uglify = require('gulp-uglify');
7var less = require('gulp-less');
8var del = require('del');
9var gutil = require('gulp-util');
10var gif = require('gulp-if');
11var jshint = require('gulp-jshint');
12var sourcemaps = require('gulp-sourcemaps');
13var source = require('vinyl-source-stream');
14var buffer = require('vinyl-buffer');
15var stylish = require('jshint-stylish');
16var util = require('util');
17var path = require('path');
18var replace = require('gulp-replace');
19var stringify = require('stringify');
20var watchify = require('watchify');
21var merge = require('../merge');
22var plumber = require('gulp-plumber');
23var defaultPaths = require('./default-paths');
24var runSequence = require('run-sequence');
25var babelify = require('babelify');
26var react = require('gulp-react');
27var express = require('express');
28var childProcess = require('child_process');
29
30/**
31 * @private
32 * Returns the top most directory in the specified path, removing any glob style wildcards (*).
33 *
34 * @param {string} p The full path
35 *
36 * @returns {string} The top most directory found. For instance, returns "asdf" if given
37 * "/foo/bar/asdf".
38 */
39function topDirectory(p) {
40 return p.split(path.sep).filter(function(part) {
41 return part.indexOf('*') === -1;
42 }).pop();
43}
44
45/**
46 * @private
47 * Outputs error messages and stops the stream.
48 */
49function logErrorAndKillStream(error) {
50 gutil.log(gutil.colors.red('Error:'), error.toString());
51 this.emit('end');
52}
53
54/**
55 * @private
56 * Returns the time difference between start and now nicely formatted for output.
57 */
58function formattedTimeDiff(start) {
59 var diff = Date.now() - start;
60 if (diff < 1000) {
61 diff = diff + 'ms';
62 } else {
63 diff = diff / 1000;
64 if (diff > 60) {
65 diff = diff / 60 + 'm';
66 } else {
67 diff += 's';
68 }
69 }
70 return gutil.colors.yellow(diff);
71}
72
73/**
74 * @private
75 * Logs a message indicating that the build is complete.
76 */
77function outputBuildCompleteMessage(start) {
78 gutil.log(gutil.colors.green('Build finished successfully in ') + formattedTimeDiff(start));
79}
80
81/**
82 * @private
83 * Helper which converts stdout to gutil.log output
84 */
85function stdoutToLog(gutil, stdout) {
86 var out = stdout.split('\n');
87 out.forEach(function(o) {
88 var trimmed = o.trim();
89 if (trimmed.length > 0) {
90 gutil.log(o.trim());
91 }
92 });
93}
94
95module.exports = {
96 /**
97 * Registers default gulp tasks.
98 *
99 * @param {object} gulp The gulp library.
100 * @param {object} [options] Optional object definining configuration
101 * parameters.
102 * @param {boolean} [options.compressJs=true] If true javascript will be minified.
103 * Defaults to true. This causes the build
104 * to become significantly slower.
105 * @param {boolean} [options.sourceMaps=true] Enables javascript source maps. Defaults
106 * to true.
107 * @param {boolean} [options.compressCss=true] If true styles will be compressed.
108 * Defaults to true.
109 * @param {boolean} [options.detectGlobals=true] Enables browserify global detection and
110 * inclusion. This is necessary for certain
111 * npm packages to work when bundled for
112 * front-end inclusion. Defaults to true.
113 * @param {boolean} [options.insertGlobals=false] Enables automatic insertion of node
114 * globals when preparing a javascript
115 * bundler. Faster alternative to
116 * detectGlobals. Causes an extra ~1000
117 * lines to be added to the bundled
118 * javascript. Defaults to false.
119 * @param {boolean} [options.disableJsHint=false] Disables jshint. Defaults to false.
120 * @param {boolean} [options.handleExceptions=false] If an exception is encountered while
121 * compiling less or bundling javascript,
122 * capture the associated error and output
123 * it cleanly. Defaults to false.
124 * @param {string} [options.jsOut] Overrides the default filename for the
125 * resulting javascript bundle. If not set
126 * the javascript file will be the same name
127 * as the entry point.
128 * @param {boolean} [options.disableBabel=false] Optionally disable babel, the es6 to es6
129 * (and react JSX) transpiler.
130 * See http://babeljs.io for more information.
131 * @param {boolean} [options.enableStringify=false] Optionally enable stringify, a browserify
132 * transform that allows HTML files to be
133 * included via require.
134 * @param {number} [options.port=4000] Optional port for the HTTP server started
135 * via the serve task. Defaults to 4000.
136 * @param {object} [configParameters] Optional map of configuration keys. If
137 * set each key is searched for in the built
138 * HTML and replaced with the corresponding
139 * value.
140 * @param {object} [paths] Optional object defining paths relevant
141 * to the project. Any specified paths are
142 * merged with the defaults where these paths
143 * take precedence.
144 * @param {string} paths.base The base directory of your project where
145 * the gulpfile lives. Defaults to the
146 * current processes working directory.
147 * @param {string} paths.html Path to the project's HTML files.
148 * @param {string} paths.jshint Path to the javascript files which should
149 * be linted using jshint.
150 * @param {string} paths.js Javascript entry point.
151 * @param {string} paths.allLess Path matching all less files which should
152 * be watched for changes.
153 * @param {string} paths.less The less entry-point.
154 * @param {string} paths.assets Path to the project's static assets.
155 * @param {string} paths.build Output directory where the build artifacts
156 * should be placed.
157 *
158 * @returns {undefined}
159 */
160 init: function(gulp, options, configParameters, paths) {
161 // Produce paths by merging any user specified paths with the defaults.
162 paths = merge(defaultPaths, paths);
163
164 if (typeof options !== 'object') {
165 options = {};
166 }
167
168 if (options.silent === true) {
169 gutil.log = gutil.noop;
170 }
171
172 if (!gulp || typeof gulp.task !== 'function') {
173 throw 'Invalid gulp instance';
174 }
175
176 if (!paths || typeof paths !== 'object') {
177 throw 'Invalid paths';
178 }
179
180 /**
181 * @private
182 * Helper method which rebundles the javascript and prints out timing information upon completion.
183 */
184 var rebundleJs = function() {
185 gutil.log(gutil.colors.yellow('Rebundling javascript'));
186 runOrQueue('html-js');
187 };
188
189 var bundlerInstance;
190 var watchJs = false;
191 /**
192 * @private
193 * Helper method which provides a (potentially) shared browserify + possible watchify instance
194 * for js bundling.
195 */
196 var bundler = function() {
197 if (!bundlerInstance) {
198 var b = browserify({
199 debug: options.sourceMaps !== false,
200 detectGlobals: (options.detectGlobals === undefined ? true : options.detectGlobals),
201 insertGlobals: options.insertGlobals,
202 cache: {},
203 packageCache: {},
204 fullPaths: true /* Required for source maps */
205 });
206 if (watchJs) {
207 bundlerInstance = watchify(b, { ignoreWatch: '**/node_modules/**' });
208 bundlerInstance.on('update', function(changedFiles) {
209 // watchify has a bug where it actually emits changes for files in node_modules, even
210 // though by default it's not supposed to. We protect against that bug by checking if
211 // the changes only involve node_modules and if so we don't do anything
212 var nodeModuleChanges = changedFiles.filter(function(f) {
213 return f.indexOf('/node_modules/') !== -1;
214 });
215 if (nodeModuleChanges.length !== changedFiles.length) {
216 // detect if a package.json file was changed and run an npm install if so as to load
217 // the latest dependencies
218 var packageJsonChanges = changedFiles.filter(function(f) {
219 var fileParts = f.split('.');
220 if (fileParts && fileParts.length > 1) {
221 return (
222 path.basename(fileParts[fileParts.length - 2]) === 'package' &&
223 fileParts[fileParts.length - 1] === 'json'
224 );
225 } else {
226 return false;
227 }
228 });
229 if (packageJsonChanges.length > 0) {
230 // we have to release the bundler since there's no way to fully invalidate
231 // cache (other than releasing the bundler itself)
232 bundlerInstance.reset();
233 bundlerInstance.close();
234 bundlerInstance = undefined;
235 gutil.log(gutil.colors.yellow('package.json change detected, running npm prune'));
236 childProcess.exec('npm prune', function(err, stdout) {
237 if (err) {
238 gutil.log(gutil.colors.red('Error running npm prune:'), err.toString());
239 rebundleJs();
240 } else {
241 if (stdout) {
242 stdoutToLog(gutil, stdout);
243 }
244 gutil.log(gutil.colors.yellow('running npm install'));
245 childProcess.exec('npm install', { cwd: paths.base }, function(err, stdout) {
246 if (err) {
247 gutil.log(gutil.colors.red('Error installing dependencies:'), err.toString());
248 rebundleJs();
249 } else {
250 if (stdout) {
251 stdoutToLog(gutil, stdout);
252 }
253 gutil.log(gutil.colors.yellow('Dependencies installed successfully'));
254 rebundleJs();
255 }
256 });
257 }
258 });
259 } else {
260 gutil.log(gutil.colors.yellow('Javascript changed detected'));
261 rebundleJs();
262 }
263 }
264 });
265 } else {
266 bundlerInstance = b;
267 }
268 if (options.enableStringify) {
269 bundlerInstance.transform(stringify({ extensions: ['.html'], minify: true }));
270 }
271 if (!options.disableBabel) {
272 bundlerInstance.transform(babelify);
273 }
274 // Browserify can't handle purely relative paths, so resolve the path for them...
275 bundlerInstance.add(path.resolve(paths.base, paths.js));
276 bundlerInstance.on('error', gutil.log.bind(gutil, 'Browserify Error'));
277 }
278 return bundlerInstance;
279 };
280
281 // Helper method for copying html, see 'html-only' and 'html' tasks.
282 var copyHtml = function() {
283 gutil.log(
284 util.format(
285 'Copying html: %s to %s',
286 gutil.colors.magenta(paths.html),
287 gutil.colors.magenta(paths.build)
288 )
289 );
290 var hasConfig = typeof configParameters === 'object';
291 var configKeys = Object.getOwnPropertyNames(configParameters || {});
292 var reConfigKeys = new RegExp('(?:' + configKeys.join('|') + ')', 'g');
293 var replaceConfigKeys = replace(reConfigKeys, function(key) {
294 return configParameters[key] || '';
295 });
296 return gulp.src(paths.html)
297 .pipe(cb(paths.build))
298 .pipe(gif(hasConfig, replaceConfigKeys))
299 .pipe(gulp.dest(paths.build));
300 };
301
302 /**
303 * Removes all build artifacts.
304 */
305 gulp.task('clean', function(cb) {
306 gutil.log(util.format('Cleaning: %s', gutil.colors.magenta(paths.build)));
307 var targets = [ paths.build ];
308 return del(targets, { force: true }, cb);
309 });
310
311 /**
312 * Compiles less files to css.
313 */
314 gulp.task('less', function() {
315 gutil.log(
316 util.format(
317 'compiling less to css: %s to %s',
318 gutil.colors.magenta(paths.less),
319 gutil.colors.magenta(path.relative(process.cwd(), path.resolve(paths.build, paths.less)))
320 )
321 );
322 return gulp.src(paths.less)
323 .pipe(gif(options.handleExceptions, plumber(logErrorAndKillStream)))
324 .pipe(less({ compress: options.compressCss !== false }))
325 .pipe(cb(paths.build))
326 .pipe(autoprefixer('last 2 versions'))
327 .pipe(gulp.dest(paths.build));
328 });
329
330 /**
331 * Lints javascript
332 */
333 gulp.task('jslint', function() {
334 if (!options.disableJsHint) {
335 gutil.log(util.format('Linting javascript: %s', gutil.colors.magenta(paths.jshint)));
336 return gulp.src(paths.jshint)
337 .pipe(gif(options.handleExceptions, plumber(logErrorAndKillStream)))
338 .pipe(react())
339 .pipe(jshint(path.resolve(__dirname, '../.jshintrc')))
340 .pipe(jshint.reporter(stylish));
341 } else {
342 gutil.log(
343 gutil.colors.gray(
344 'Javascript linting skipped'
345 )
346 );
347 }
348 });
349
350 /**
351 * Bundles, compresses and produces sourcemaps for javascript.
352 */
353 gulp.task('js', function() {
354 var fn = options.jsOut || path.basename(paths.js).replace(/\.jsx$/, '.js');
355 gutil.log(
356 util.format(
357 'Bundling javascript: %s to %s',
358 gutil.colors.magenta(paths.js),
359 gutil.colors.magenta(path.relative(process.cwd(), path.resolve(paths.build, fn)))
360 )
361 );
362 return gif(options.handleExceptions, plumber(logErrorAndKillStream))
363 .pipe(bundler().bundle())
364 .pipe(source(fn))
365 .pipe(buffer())
366 .pipe(gif(options.sourceMaps !== false, sourcemaps.init({ loadMaps: true })))
367 .pipe(gif(options.compressJs !== false, uglify({ compress: { 'drop_debugger': false } })))
368 .pipe(gif(options.sourceMaps !== false, sourcemaps.write('./')))
369 .pipe(gulp.dest(paths.build));
370 });
371
372 /**
373 * Copies fonts and icons into the assets directory.
374 *
375 * This task first copies user-assets, then pipes syrup based assets (currently /fonts
376 * and /icons into the asset directory).
377 */
378 gulp.task('assets', ['user-assets'], function() {
379 var assetDir = topDirectory(paths.assets);
380 var dest = path.relative(process.cwd(), path.resolve(paths.build, assetDir));
381 var iconAndFontBase = path.resolve(__dirname, '..');
382 var iconsAndFontPaths = [
383 path.resolve(iconAndFontBase, 'fonts', '**', '*'),
384 path.resolve(iconAndFontBase, 'icons', '**', '*'),
385 ];
386 return gulp.src(iconsAndFontPaths, { base: iconAndFontBase })
387 .pipe(gulp.dest(dest));
388 });
389
390 /**
391 * Copies user specific assets.
392 */
393 gulp.task('user-assets', function() {
394 var assetDir = topDirectory(paths.assets);
395 var dest = path.relative(process.cwd(), path.resolve(paths.build, assetDir));
396 gutil.log(
397 util.format(
398 'Copying static assets: %s to %s',
399 gutil.colors.magenta(paths.assets),
400 gutil.colors.magenta(dest)
401 )
402 );
403 return gulp.src(paths.assets)
404 .pipe(gulp.dest(dest));
405 });
406
407 /**
408 * The following html gulp tasks are for use with gulp.watch. Each is tied to particular
409 * dependency.
410 */
411 gulp.task('html-only', copyHtml);
412 gulp.task('html-js', [ 'jslint', 'js' ], copyHtml);
413 gulp.task('html-less', [ 'less' ], copyHtml);
414 gulp.task('html-assets', [ 'assets' ], copyHtml);
415
416 /**
417 * Copies all html files to the build directory.
418 */
419 gulp.task('html', [ 'js', 'less', 'assets'], copyHtml);
420
421 /**
422 * Hash of running tasks. The key is task name; value is a boolean - true if the task should be
423 * re-run on completion; false otherwise.
424 */
425 var runningTasks = {};
426
427 /**
428 * Helper function for watch-triggered task calls. This will run the task immediately if there
429 * isn't an instance already running, and will queue the task to run after completion if not.
430 * @param {string} name the name of the task to run.
431 */
432 var runOrQueue = function(name) {
433 if (runningTasks[name] === undefined) {
434 var start = Date.now();
435 runningTasks[name] = false;
436 gulp.start(name, function() {
437 outputBuildCompleteMessage(start);
438 var shouldRunAgain = runningTasks[name];
439 delete runningTasks[name];
440 if (shouldRunAgain) {
441 runOrQueue(name);
442 }
443 });
444 } else {
445 runningTasks[name] = true;
446 }
447 };
448
449 /**
450 * Watches specific files and rebuilds only the changed component(s).
451 */
452 gulp.task('watch', function() {
453 options.handleExceptions = true;
454 watchJs = true;
455 bundler();
456 gulp.start('build', function() {
457 gulp.watch(paths.allLess, function() {
458 gutil.log(gutil.colors.yellow('Less change detected'));
459 runOrQueue('html-less');
460 });
461
462 gulp.watch(paths.assets, function() {
463 gutil.log(gutil.colors.yellow('Asset change detected'));
464 runOrQueue('html-assets');
465 });
466
467 gulp.watch(paths.html, function() {
468 gutil.log(gutil.colors.yellow('HTML change detected'));
469 runOrQueue('html-only');
470 });
471 });
472 });
473
474 /**
475 * Combined build task. This bundles up all required UI resources.
476 */
477 gulp.task('build', function(cb) {
478 var start = Date.now();
479 runSequence(
480 'clean',
481 ['assets', 'jslint', 'js', 'less', 'html'],
482 function() {
483 cb();
484 outputBuildCompleteMessage(start);
485 }
486 );
487 });
488
489 /**
490 * Start a simple http serve which serves the contents of paths.build.
491 */
492 gulp.task('serve', ['build'], function(cb) {
493 var server = express();
494 var port = options.port || gutil.env.port || 4040;
495 server.use(express.static(paths.build));
496 server.listen(port, function() {
497 gutil.log(
498 gutil.colors.yellow('Server listening at ') +
499 gutil.colors.cyan('http://localhost:' + port)
500 );
501 cb();
502 });
503 });
504
505 /**
506 * Alias for watch and serve, start a server with a watcher for dyanmic changes as well.
507 */
508 gulp.task('wserve', ['watch', 'serve']);
509 gulp.task('watch-and-serve', ['wserve']);
510
511 /**
512 * Default task. Gets executed when gulp is called without arguments.
513 */
514 gulp.task('default', ['build']);
515 }
516};