UNPKG

20 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 var start = Date.now();
186 gutil.log(gutil.colors.yellow('Rebundling javascript'));
187 gulp.start('html-js', function() {
188 outputBuildCompleteMessage(start);
189 });
190 };
191
192 var bundlerInstance;
193 var watchJs = false;
194 /**
195 * @private
196 * Helper method which provides a (potentially) shared browserify + possible watchify instance
197 * for js bundling.
198 */
199 var bundler = function() {
200 if (!bundlerInstance) {
201 var b = browserify({
202 debug: options.sourceMaps !== false,
203 detectGlobals: (options.detectGlobals === undefined ? true : options.detectGlobals),
204 insertGlobals: options.insertGlobals,
205 cache: {},
206 packageCache: {},
207 fullPaths: true /* Required for source maps */
208 });
209 if (watchJs) {
210 bundlerInstance = watchify(b, { ignoreWatch: '**/node_modules/**' });
211 bundlerInstance.on('update', function(changedFiles) {
212 // watchify has a bug where it actually emits changes for files in node_modules, even
213 // though by default it's not supposed to. We protect against that bug by checking if
214 // the changes only involve node_modules and if so we don't do anything
215 var nodeModuleChanges = changedFiles.filter(function(f) {
216 return f.indexOf('/node_modules/') !== -1;
217 });
218 if (nodeModuleChanges.length !== changedFiles.length) {
219 // detect if a package.json file was changed and run an npm install if so as to load
220 // the latest dependencies
221 var packageJsonChanges = changedFiles.filter(function(f) {
222 var fileParts = f.split('.');
223 if (fileParts && fileParts.length > 1) {
224 return (
225 path.basename(fileParts[fileParts.length - 2]) === 'package' &&
226 fileParts[fileParts.length - 1] === 'json'
227 );
228 } else {
229 return false;
230 }
231 });
232 if (packageJsonChanges.length > 0) {
233 // we have to release the bundler since there's no way to fully invalidate
234 // cache (other than releasing the bundler itself)
235 bundlerInstance.reset();
236 bundlerInstance.close();
237 bundlerInstance = undefined;
238 gutil.log(gutil.colors.yellow('package.json change detected, running npm prune'));
239 childProcess.exec('npm prune', function(err, stdout) {
240 if (err) {
241 gutil.log(gutil.colors.red('Error running npm prune:'), err.toString());
242 rebundleJs();
243 } else {
244 if (stdout) {
245 stdoutToLog(gutil, stdout);
246 }
247 gutil.log(gutil.colors.yellow('running npm install'));
248 childProcess.exec('npm install', { cwd: paths.base }, function(err, stdout) {
249 if (err) {
250 gutil.log(gutil.colors.red('Error installing dependencies:'), err.toString());
251 rebundleJs();
252 } else {
253 if (stdout) {
254 stdoutToLog(gutil, stdout);
255 }
256 gutil.log(gutil.colors.yellow('Dependencies installed successfully'));
257 rebundleJs();
258 }
259 });
260 }
261 });
262 } else {
263 gutil.log(gutil.colors.yellow('Javascript changed detected'));
264 rebundleJs();
265 }
266 }
267 });
268 } else {
269 bundlerInstance = b;
270 }
271 if (options.enableStringify) {
272 bundlerInstance.transform(stringify({ extensions: ['.html'], minify: true }));
273 }
274 if (!options.disableBabel) {
275 bundlerInstance.transform(babelify);
276 }
277 // Browserify can't handle purely relative paths, so resolve the path for them...
278 bundlerInstance.add(path.resolve(paths.base, paths.js));
279 bundlerInstance.on('error', gutil.log.bind(gutil, 'Browserify Error'));
280 }
281 return bundlerInstance;
282 };
283
284 // Helper method for copying html, see 'html-only' and 'html' tasks.
285 var copyHtml = function() {
286 gutil.log(
287 util.format(
288 'Copying html: %s to %s',
289 gutil.colors.magenta(paths.html),
290 gutil.colors.magenta(paths.build)
291 )
292 );
293 var hasConfig = typeof configParameters === 'object';
294 var configKeys = Object.getOwnPropertyNames(configParameters || {});
295 var reConfigKeys = new RegExp('(?:' + configKeys.join('|') + ')', 'g');
296 var replaceConfigKeys = replace(reConfigKeys, function(key) {
297 return configParameters[key] || '';
298 });
299 return gulp.src(paths.html)
300 .pipe(cb(paths.build))
301 .pipe(gif(hasConfig, replaceConfigKeys))
302 .pipe(gulp.dest(paths.build));
303 };
304
305 /**
306 * Removes all build artifacts.
307 */
308 gulp.task('clean', function(cb) {
309 gutil.log(util.format('Cleaning: %s', gutil.colors.magenta(paths.build)));
310 var targets = [ paths.build ];
311 return del(targets, { force: true }, cb);
312 });
313
314 /**
315 * Compiles less files to css.
316 */
317 gulp.task('less', function() {
318 gutil.log(
319 util.format(
320 'compiling less to css: %s to %s',
321 gutil.colors.magenta(paths.less),
322 gutil.colors.magenta(path.relative(process.cwd(), path.resolve(paths.build, paths.less)))
323 )
324 );
325 return gulp.src(paths.less)
326 .pipe(gif(options.handleExceptions, plumber(logErrorAndKillStream)))
327 .pipe(less({ compress: options.compressCss !== false }))
328 .pipe(cb(paths.build))
329 .pipe(autoprefixer('last 2 versions'))
330 .pipe(gulp.dest(paths.build));
331 });
332
333 /**
334 * Lints javascript
335 */
336 gulp.task('jslint', function() {
337 if (!options.disableJsHint) {
338 gutil.log(util.format('Linting javascript: %s', gutil.colors.magenta(paths.jshint)));
339 return gulp.src(paths.jshint)
340 .pipe(gif(options.handleExceptions, plumber(logErrorAndKillStream)))
341 .pipe(react())
342 .pipe(jshint(path.resolve(__dirname, '../.jshintrc')))
343 .pipe(jshint.reporter(stylish));
344 } else {
345 gutil.log(
346 gutil.colors.gray(
347 'Javascript linting skipped'
348 )
349 );
350 }
351 });
352
353 /**
354 * Bundles, compresses and produces sourcemaps for javascript.
355 */
356 gulp.task('js', function() {
357 var fn = options.jsOut || path.basename(paths.js).replace(/\.jsx$/, '.js');
358 gutil.log(
359 util.format(
360 'Bundling javascript: %s to %s',
361 gutil.colors.magenta(paths.js),
362 gutil.colors.magenta(path.relative(process.cwd(), path.resolve(paths.build, fn)))
363 )
364 );
365 return gif(options.handleExceptions, plumber(logErrorAndKillStream))
366 .pipe(bundler().bundle())
367 .pipe(source(fn))
368 .pipe(buffer())
369 .pipe(gif(options.sourceMaps !== false, sourcemaps.init({ loadMaps: true })))
370 .pipe(gif(options.compressJs !== false, uglify({ compress: { 'drop_debugger': false } })))
371 .pipe(gif(options.sourceMaps !== false, sourcemaps.write('./')))
372 .pipe(gulp.dest(paths.build));
373 });
374
375 /**
376 * Copies fonts and icons into the assets directory.
377 *
378 * This task first copies user-assets, then pipes syrup based assets (currently /fonts
379 * and /icons into the asset directory).
380 */
381 gulp.task('assets', ['user-assets'], function() {
382 var assetDir = topDirectory(paths.assets);
383 var dest = path.relative(process.cwd(), path.resolve(paths.build, assetDir));
384 var iconAndFontBase = path.resolve(__dirname, '..');
385 var iconsAndFontPaths = [
386 path.resolve(iconAndFontBase, 'fonts', '**', '*'),
387 path.resolve(iconAndFontBase, 'icons', '**', '*'),
388 ];
389 return gulp.src(iconsAndFontPaths, { base: iconAndFontBase })
390 .pipe(gulp.dest(dest));
391 });
392
393 /**
394 * Copies user specific assets.
395 */
396 gulp.task('user-assets', function() {
397 var assetDir = topDirectory(paths.assets);
398 var dest = path.relative(process.cwd(), path.resolve(paths.build, assetDir));
399 gutil.log(
400 util.format(
401 'Copying static assets: %s to %s',
402 gutil.colors.magenta(paths.assets),
403 gutil.colors.magenta(dest)
404 )
405 );
406 return gulp.src(paths.assets)
407 .pipe(gulp.dest(dest));
408 });
409
410 /**
411 * The following html gulp tasks are for use with gulp.watch. Each is tied to particular
412 * dependency.
413 */
414 gulp.task('html-only', copyHtml);
415 gulp.task('html-js', [ 'jslint', 'js' ], copyHtml);
416 gulp.task('html-less', [ 'less' ], copyHtml);
417 gulp.task('html-assets', [ 'assets' ], copyHtml);
418
419 /**
420 * Copies all html files to the build directory.
421 */
422 gulp.task('html', [ 'js', 'less', 'assets'], copyHtml);
423
424 /**
425 * Watches specific files and rebuilds only the changed component(s).
426 */
427 gulp.task('watch', function() {
428 options.handleExceptions = true;
429 watchJs = true;
430 bundler();
431 gulp.start('build', function() {
432 gulp.watch(paths.allLess, function() {
433 var start = Date.now();
434 gutil.log(gutil.colors.yellow('Less change detected'));
435 gulp.start('html-less', function() {
436 outputBuildCompleteMessage(start);
437 });
438 });
439
440 gulp.watch(paths.assets, function() {
441 var start = Date.now();
442 gutil.log(gutil.colors.yellow('Asset change detected'));
443 gulp.start('html-assets', function() {
444 outputBuildCompleteMessage(start);
445 });
446 });
447
448 gulp.watch(paths.html, function() {
449 var start = Date.now();
450 gutil.log(gutil.colors.yellow('HTML change detected'));
451 gulp.start('html-only', function() {
452 outputBuildCompleteMessage(start);
453 });
454 });
455 });
456 });
457
458 /**
459 * Combined build task. This bundles up all required UI resources.
460 */
461 gulp.task('build', function(cb) {
462 var start = Date.now();
463 runSequence(
464 'clean',
465 ['assets', 'jslint', 'js', 'less', 'html'],
466 function() {
467 cb();
468 outputBuildCompleteMessage(start);
469 }
470 );
471 });
472
473 /**
474 * Start a simple http serve which serves the contents of paths.build.
475 */
476 gulp.task('serve', ['build'], function(cb) {
477 var server = express();
478 var port = options.port || gutil.env.port || 4040;
479 server.use(express.static(paths.build));
480 server.listen(port, function() {
481 gutil.log(
482 gutil.colors.yellow('Server listening at ') +
483 gutil.colors.cyan('http://localhost:' + port)
484 );
485 cb();
486 });
487 });
488
489 /**
490 * Alias for watch and serve, start a server with a watcher for dyanmic changes as well.
491 */
492 gulp.task('wserve', ['watch', 'serve']);
493 gulp.task('watch-and-serve', ['wserve']);
494
495 /**
496 * Default task. Gets executed when gulp is called without arguments.
497 */
498 gulp.task('default', ['build']);
499 }
500};