1 | ;
|
2 |
|
3 | var cb = require('gulp-cache-breaker');
|
4 | var autoprefixer = require('gulp-autoprefixer');
|
5 | var browserify = require('browserify');
|
6 | var uglify = require('gulp-uglify');
|
7 | var less = require('gulp-less');
|
8 | var del = require('del');
|
9 | var gutil = require('gulp-util');
|
10 | var gif = require('gulp-if');
|
11 | var jshint = require('gulp-jshint');
|
12 | var sourcemaps = require('gulp-sourcemaps');
|
13 | var source = require('vinyl-source-stream');
|
14 | var buffer = require('vinyl-buffer');
|
15 | var stylish = require('jshint-stylish');
|
16 | var util = require('util');
|
17 | var path = require('path');
|
18 | var replace = require('gulp-replace');
|
19 | var stringify = require('stringify');
|
20 | var watchify = require('watchify');
|
21 | var merge = require('../merge');
|
22 | var plumber = require('gulp-plumber');
|
23 | var defaultPaths = require('./default-paths');
|
24 | var runSequence = require('run-sequence');
|
25 | var babelify = require('babelify');
|
26 | var react = require('gulp-react');
|
27 | var express = require('express');
|
28 | var 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 | */
|
39 | function 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 | */
|
49 | function 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 | */
|
58 | function 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 | */
|
77 | function 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 | */
|
85 | function 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 |
|
95 | module.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 | };
|