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 sourcemaps = require('gulp-sourcemaps');
|
12 | var source = require('vinyl-source-stream');
|
13 | var buffer = require('vinyl-buffer');
|
14 | var stylish = require('jshint-stylish');
|
15 | var util = require('util');
|
16 | var path = require('path');
|
17 | var replace = require('gulp-replace');
|
18 | var stringify = require('stringify');
|
19 | var watchify = require('watchify');
|
20 | var merge = require('../merge');
|
21 | var plumber = require('gulp-plumber');
|
22 | var defaultPaths = require('./default-paths');
|
23 | var runSequence = require('run-sequence');
|
24 | var babelify = require('babelify');
|
25 | var express = require('express');
|
26 | var childProcess = require('child_process');
|
27 | var 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 | */
|
38 | function 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 | */
|
48 | function 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 | */
|
57 | function 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 | */
|
76 | function 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 | */
|
84 | function 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 |
|
94 | module.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 | };
|