UNPKG

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