UNPKG

syrup

Version:

A collection of common UI utilities and libraries leveraged by AI2.

520 lines (490 loc) 20.9 kB
'use strict'; var cb = require('gulp-cache-breaker'); var autoprefixer = require('gulp-autoprefixer'); var browserify = require('browserify'); var uglify = require('gulp-uglify'); var less = require('gulp-less'); var del = require('del'); var gutil = require('gulp-util'); var gif = require('gulp-if'); var sourcemaps = require('gulp-sourcemaps'); var source = require('vinyl-source-stream'); var buffer = require('vinyl-buffer'); var stylish = require('jshint-stylish'); var util = require('util'); var path = require('path'); var replace = require('gulp-replace'); var stringify = require('stringify'); var watchify = require('watchify'); var merge = require('../merge'); var plumber = require('gulp-plumber'); var defaultPaths = require('./default-paths'); var runSequence = require('run-sequence'); var babelify = require('babelify'); var express = require('express'); var childProcess = require('child_process'); var eslint = require('gulp-eslint'); /** * @private * Returns the top most directory in the specified path, removing any glob style wildcards (*). * * @param {string} p The full path * * @returns {string} The top most directory found. For instance, returns "asdf" if given * "/foo/bar/asdf". */ function topDirectory(p) { return p.split(path.sep).filter(function(part) { return part.indexOf('*') === -1; }).pop(); } /** * @private * Outputs error messages and stops the stream. */ function logErrorAndKillStream(error) { gutil.log(gutil.colors.red('Error:'), error.toString()); this.emit('end'); } /** * @private * Returns the time difference between start and now nicely formatted for output. */ function formattedTimeDiff(start) { var diff = Date.now() - start; if (diff < 1000) { diff = diff + 'ms'; } else { diff = diff / 1000; if (diff > 60) { diff = diff / 60 + 'm'; } else { diff += 's'; } } return gutil.colors.yellow(diff); } /** * @private * Logs a message indicating that the build is complete. */ function outputBuildCompleteMessage(start) { gutil.log(gutil.colors.green('Build finished successfully in ') + formattedTimeDiff(start)); } /** * @private * Helper which converts stdout to gutil.log output */ function stdoutToLog(gutil, stdout) { var out = stdout.split('\n'); out.forEach(function(o) { var trimmed = o.trim(); if (trimmed.length > 0) { gutil.log(o.trim()); } }); } module.exports = { /** * Registers default gulp tasks. * * @param {object} gulp The gulp library. * @param {object} [options] Optional object definining configuration * parameters. * @param {boolean} [options.compressJs=true] If true javascript will be minified. * Defaults to true. This causes the build * to become significantly slower. * @param {boolean} [options.sourceMaps=true] Enables javascript source maps. Defaults * to true. * @param {boolean} [options.compressCss=true] If true styles will be compressed. * Defaults to true. * @param {boolean} [options.detectGlobals=true] Enables browserify global detection and * inclusion. This is necessary for certain * npm packages to work when bundled for * front-end inclusion. Defaults to true. * @param {boolean} [options.insertGlobals=false] Enables automatic insertion of node * globals when preparing a javascript * bundler. Faster alternative to * detectGlobals. Causes an extra ~1000 * lines to be added to the bundled * javascript. Defaults to false. * @param {boolean} [options.disableJsLint=false] Disables linting of javascript. Defaults to false. * @param {boolean} [options.handleExceptions=false] If an exception is encountered while * compiling less or bundling javascript, * capture the associated error and output * it cleanly. Defaults to false. * @param {string} [options.jsOut] Overrides the default filename for the * resulting javascript bundle. If not set * the javascript file will be the same name * as the entry point. * @param {boolean} [options.disableBabel=false] Optionally disable babel, the es6 to es6 * (and react JSX) transpiler. * See http://babeljs.io for more information. * @param {boolean} [options.enableStringify=false] Optionally enable stringify, a browserify * transform that allows HTML files to be * included via require. * @param {number} [options.port=4000] Optional port for the HTTP server started * via the serve task. Defaults to 4000. * @param {objectd} [options.eslintOptions] Eslint configuration overrides. See * https://github.com/adametry/gulp-eslint for * a full list of options. * @param {object} [configParameters] Optional map of configuration keys. If * set each key is searched for in the built * HTML and replaced with the corresponding * value. * @param {object} [paths] Optional object defining paths relevant * to the project. Any specified paths are * merged with the defaults where these paths * take precedence. * @param {string} paths.base The base directory of your project where * the gulpfile lives. Defaults to the * current processes working directory. * @param {string} paths.html Path to the project's HTML files. * @param {string} paths.jsLint Path to the javascript files which should * be linted using eslint. * @param {string} paths.js Javascript entry point. * @param {string} paths.allLess Path matching all less files which should * be watched for changes. * @param {string} paths.less The less entry-point. * @param {string} paths.assets Path to the project's static assets. * @param {string} paths.build Output directory where the build artifacts * should be placed. * * @returns {undefined} */ init: function(gulp, options, configParameters, paths) { // Produce paths by merging any user specified paths with the defaults. paths = merge(defaultPaths, paths); if (typeof options !== 'object') { options = {}; } if (options.silent === true) { gutil.log = gutil.noop; } if (!gulp || typeof gulp.task !== 'function') { throw 'Invalid gulp instance'; } if (!paths || typeof paths !== 'object') { throw 'Invalid paths'; } /** * @private * Helper method which rebundles the javascript and prints out timing information upon completion. */ var rebundleJs = function() { gutil.log(gutil.colors.yellow('Rebundling javascript')); runOrQueue('html-js'); }; var bundlerInstance; var watchJs = false; /** * @private * Helper method which provides a (potentially) shared browserify + possible watchify instance * for js bundling. */ var bundler = function() { if (!bundlerInstance) { var b = browserify({ debug: options.sourceMaps !== false, detectGlobals: (options.detectGlobals === undefined ? true : options.detectGlobals), insertGlobals: options.insertGlobals, cache: {}, packageCache: {}, fullPaths: true /* Required for source maps */ }); if (watchJs) { bundlerInstance = watchify(b, { ignoreWatch: '**/node_modules/**' }); bundlerInstance.on('update', function(changedFiles) { // watchify has a bug where it actually emits changes for files in node_modules, even // though by default it's not supposed to. We protect against that bug by checking if // the changes only involve node_modules and if so we don't do anything var nodeModuleChanges = changedFiles.filter(function(f) { return f.indexOf('/node_modules/') !== -1; }); if (nodeModuleChanges.length !== changedFiles.length) { // detect if a package.json file was changed and run an npm install if so as to load // the latest dependencies var packageJsonChanges = changedFiles.filter(function(f) { var fileParts = f.split('.'); if (fileParts && fileParts.length > 1) { return ( path.basename(fileParts[fileParts.length - 2]) === 'package' && fileParts[fileParts.length - 1] === 'json' ); } else { return false; } }); if (packageJsonChanges.length > 0) { // we have to release the bundler since there's no way to fully invalidate // cache (other than releasing the bundler itself) bundlerInstance.reset(); bundlerInstance.close(); bundlerInstance = undefined; gutil.log(gutil.colors.yellow('package.json change detected, running npm prune')); childProcess.exec('npm prune', function(err, stdout) { if (err) { gutil.log(gutil.colors.red('Error running npm prune:'), err.toString()); rebundleJs(); } else { if (stdout) { stdoutToLog(gutil, stdout); } gutil.log(gutil.colors.yellow('running npm install')); childProcess.exec('npm install', { cwd: paths.base }, function(err, stdout) { if (err) { gutil.log(gutil.colors.red('Error installing dependencies:'), err.toString()); rebundleJs(); } else { if (stdout) { stdoutToLog(gutil, stdout); } gutil.log(gutil.colors.yellow('Dependencies installed successfully')); rebundleJs(); } }); } }); } else { gutil.log(gutil.colors.yellow('Javascript changed detected')); rebundleJs(); } } }); } else { bundlerInstance = b; } if (options.enableStringify) { bundlerInstance.transform(stringify({ extensions: ['.html'], minify: true })); } if (!options.disableBabel) { bundlerInstance.transform(babelify); } // Browserify can't handle purely relative paths, so resolve the path for them... bundlerInstance.add(path.resolve(paths.base, paths.js)); bundlerInstance.on('error', gutil.log.bind(gutil, 'Browserify Error')); } return bundlerInstance; }; // Helper method for copying html, see 'html-only' and 'html' tasks. var copyHtml = function() { gutil.log( util.format( 'Copying html: %s to %s', gutil.colors.magenta(paths.html), gutil.colors.magenta(paths.build) ) ); var hasConfig = typeof configParameters === 'object'; var configKeys = Object.getOwnPropertyNames(configParameters || {}); var reConfigKeys = new RegExp('(?:' + configKeys.join('|') + ')', 'g'); var replaceConfigKeys = replace(reConfigKeys, function(key) { return configParameters[key] || ''; }); return gulp.src(paths.html) .pipe(cb(paths.build)) .pipe(gif(hasConfig, replaceConfigKeys)) .pipe(gulp.dest(paths.build)); }; /** * Removes all build artifacts. */ gulp.task('clean', function(cb) { var target = paths.build + '/**/*'; gutil.log(util.format('Cleaning: %s', gutil.colors.magenta(target))); return del([target], { force: true }); }); /** * Compiles less files to css. */ gulp.task('less', function() { gutil.log( util.format( 'compiling less to css: %s to %s', gutil.colors.magenta(paths.less), gutil.colors.magenta(path.relative(process.cwd(), path.resolve(paths.build, paths.less))) ) ); return gulp.src(paths.less) .pipe(gif(options.handleExceptions, plumber(logErrorAndKillStream))) .pipe(less({ compress: options.compressCss !== false })) .pipe(cb(paths.build)) .pipe(autoprefixer('last 2 versions')) .pipe(gulp.dest(paths.build)); }); /** * Lints javascript */ gulp.task('jslint', function() { var eslintOptions = merge({ configFile: path.resolve(__dirname, 'eslint-config.json') }, options.eslintOptions); if (!options.disableJsLint) { gutil.log(util.format('Linting javascript: %s', gutil.colors.magenta(paths.jsLint))); return gulp.src(paths.jsLint) .pipe(gif(options.handleExceptions, plumber(logErrorAndKillStream))) .pipe(eslint(eslintOptions)) .pipe(eslint.format()) .pipe(gif(!options.handleExceptions, eslint.failOnError())); } else { gutil.log( gutil.colors.gray( 'Javascript linting skipped' ) ); } }); /** * Bundles, compresses and produces sourcemaps for javascript. */ gulp.task('js', function() { var fn = options.jsOut || path.basename(paths.js).replace(/\.jsx$/, '.js'); gutil.log( util.format( 'Bundling javascript: %s to %s', gutil.colors.magenta(paths.js), gutil.colors.magenta(path.relative(process.cwd(), path.resolve(paths.build, fn))) ) ); return gif(options.handleExceptions, plumber(logErrorAndKillStream)) .pipe(bundler().bundle()) .pipe(source(fn)) .pipe(buffer()) .pipe(gif(options.sourceMaps !== false, sourcemaps.init({ loadMaps: true }))) .pipe(gif(options.compressJs !== false, uglify({ compress: { 'drop_debugger': false } }))) .pipe(gif(options.sourceMaps !== false, sourcemaps.write('./'))) .pipe(gulp.dest(paths.build)); }); /** * Copies fonts and icons into the assets directory. * * This task first copies user-assets, then pipes syrup based assets (currently /fonts * and /icons into the asset directory). */ gulp.task('assets', ['user-assets'], function() { var assetDir = topDirectory(paths.assets); var dest = path.relative(process.cwd(), path.resolve(paths.build, assetDir)); var iconAndFontBase = path.resolve(__dirname, '..'); var iconsAndFontPaths = [ path.resolve(iconAndFontBase, 'fonts', '**', '*'), path.resolve(iconAndFontBase, 'icons', '**', '*'), ]; return gulp.src(iconsAndFontPaths, { base: iconAndFontBase }) .pipe(gulp.dest(dest)); }); /** * Copies user specific assets. */ gulp.task('user-assets', function() { var assetDir = topDirectory(paths.assets); var dest = path.relative(process.cwd(), path.resolve(paths.build, assetDir)); gutil.log( util.format( 'Copying static assets: %s to %s', gutil.colors.magenta(paths.assets), gutil.colors.magenta(dest) ) ); return gulp.src(paths.assets) .pipe(gulp.dest(dest)); }); /** * The following html gulp tasks are for use with gulp.watch. Each is tied to particular * dependency. */ gulp.task('html-only', copyHtml); gulp.task('html-js', [ 'jslint', 'js' ], copyHtml); gulp.task('html-less', [ 'less' ], copyHtml); gulp.task('html-assets', [ 'assets' ], copyHtml); /** * Copies all html files to the build directory. */ gulp.task('html', [ 'js', 'less', 'assets'], copyHtml); /** * Hash of running tasks. The key is task name; value is a boolean - true if the task should be * re-run on completion; false otherwise. */ var runningTasks = {}; /** * Helper function for watch-triggered task calls. This will run the task immediately if there * isn't an instance already running, and will queue the task to run after completion if not. * @param {string} name the name of the task to run. */ var runOrQueue = function(name) { if (runningTasks[name] === undefined) { var start = Date.now(); runningTasks[name] = false; gulp.start(name, function() { outputBuildCompleteMessage(start); var shouldRunAgain = runningTasks[name]; delete runningTasks[name]; if (shouldRunAgain) { runOrQueue(name); } }); } else { runningTasks[name] = true; } }; /** * Watches specific files and rebuilds only the changed component(s). */ gulp.task('watch', function() { options.handleExceptions = true; watchJs = true; bundler(); gulp.start('build', function() { gulp.watch(paths.allLess, function() { gutil.log(gutil.colors.yellow('Less change detected')); runOrQueue('html-less'); }); gulp.watch(paths.assets, function() { gutil.log(gutil.colors.yellow('Asset change detected')); runOrQueue('html-assets'); }); gulp.watch(paths.html, function() { gutil.log(gutil.colors.yellow('HTML change detected')); runOrQueue('html-only'); }); }); }); /** * Combined build task. This bundles up all required UI resources. */ gulp.task('build', function(cb) { var start = Date.now(); runSequence( 'clean', ['assets', 'jslint', 'js', 'less', 'html'], function() { cb(); outputBuildCompleteMessage(start); } ); }); /** * Start a simple http serve which serves the contents of paths.build. */ gulp.task('serve', ['build'], function(cb) { var server = express(); var port = options.port || gutil.env.port || 4040; server.use(express.static(paths.build)); server.listen(port, function() { gutil.log( gutil.colors.yellow('Server listening at ') + gutil.colors.cyan('http://localhost:' + port) ); cb(); }); }); /** * Alias for watch and serve, start a server with a watcher for dyanmic changes as well. */ gulp.task('wserve', ['watch', 'serve']); gulp.task('watch-and-serve', ['wserve']); /** * Default task. Gets executed when gulp is called without arguments. */ gulp.task('default', ['build']); } };