'use strict'; // dependencies var path = require('path'); var fs = require('fs'); var fse = require('fs-extra'); var child_process = require('child_process'); var gulp = require('gulp'); var gutil = require('gulp-util'); var less = require('gulp-less'); var sass = require('gulp-sass'); var replace = require('gulp-replace'); var header = require('gulp-header'); var footer = require('gulp-footer'); var rename = require('gulp-rename'); var browsersync = require('browser-sync'); var vstream = require('vinyl-source-stream'); var buffer = require('vinyl-buffer'); var browserify = require('browserify'); var babelify = require('babelify'); var uglify = require('gulp-uglify'); var envify = require('envify'); var htmllint = require('gulp-htmllint'); var crawler = require('simplecrawler'); var ncp = require('ncp'); var nextversion = require('./tools/bin/nextversion'); var util = require('./tools/bin/util'); // constants var ROOT_DIR = '.'; var CONFIG_DIR = 'conf'; var SOURCE_DIR = path.join(ROOT_DIR, 'www'); var DEV_DIR = path.join(ROOT_DIR, 'build-dev'); var PROD_DIR = path.join(ROOT_DIR, 'build-prod'); var DATA_DIR = path.join(SOURCE_DIR, '_data'); var TOC_DIR = path.join(DATA_DIR, 'toc'); var DOCS_DIR = path.join(SOURCE_DIR, 'docs'); var FETCH_DIR = path.join(DOCS_DIR, 'en', 'dev', 'reference'); var CSS_SRC_DIR = path.join(SOURCE_DIR, 'static', 'css-src'); var CSS_OUT_DIR = path.join(SOURCE_DIR, 'static', 'css'); var PLUGINS_SRC_DIR = path.join(SOURCE_DIR, 'static', 'plugins'); var JS_DIR = path.join(SOURCE_DIR, 'static', 'js'); var BIN_DIR = path.join(ROOT_DIR, 'tools', 'bin'); var CONFIG_FILE = path.join(CONFIG_DIR, '_config.yml'); var DEFAULTS_CONFIG_FILE = path.join(CONFIG_DIR, '_defaults.yml'); var VERSION_CONFIG_FILE = path.join(CONFIG_DIR, '_version.yml'); var PROD_CONFIG_FILE = path.join(CONFIG_DIR, '_prod.yml'); var DEV_CONFIG_FILE = path.join(CONFIG_DIR, '_dev.yml'); var NODOCS_CONFIG_FILE = path.join(CONFIG_DIR, '_nodocs.yml'); var VERSION_FILE = 'VERSION'; var DOCS_VERSION_FILE = path.join(DATA_DIR, 'docs-versions.yml'); var ALL_PAGES_FILE = path.join(DATA_DIR, 'all-pages.yml'); var FETCH_CONFIG = path.join(DATA_DIR, 'fetched-files.yml'); var REDIRECTS_FILE = path.join(DATA_DIR, 'redirects.yml'); var PLUGINS_FILE_NAME = 'plugins.js'; var PLUGINS_FILE = path.join(JS_DIR, PLUGINS_FILE_NAME); var PLUGINS_SRC_FILE = path.join(PLUGINS_SRC_DIR, 'app.js'); var BASE_CONFIGS = [CONFIG_FILE, DEFAULTS_CONFIG_FILE, VERSION_CONFIG_FILE]; var DEV_CONFIGS = [DEV_CONFIG_FILE]; var PROD_CONFIGS = [PROD_CONFIG_FILE]; var DEV_FLAGS = ['--trace']; var PROD_FLAGS = []; var BASE_URL = ''; var YAML_FRONT_MATTER = '---\n---\n'; var WATCH_INTERVAL = 1000; // in milliseconds var VERSION_VAR_NAME = 'latest_docs_version'; var LATEST_DOCS_VERSION = fs.readFileSync(VERSION_FILE, 'utf-8').trim(); var NEXT_DOCS_VERSION = nextversion.getNextVersion(LATEST_DOCS_VERSION); var LANGUAGES = util.listdirsSync(DOCS_DIR); var PROD_BY_DEFAULT = false; // compute/get/set/adjust passed options gutil.env.prod = gutil.env.prod || PROD_BY_DEFAULT; gutil.env.dev = !gutil.env.prod; gutil.env.outDir = gutil.env.prod ? PROD_DIR : DEV_DIR; // check for errors if (gutil.env.prod && gutil.env.nodocs) { fatal("can't ignore docs when doing a production build"); } // helpers function fatal (message) { gutil.log(gutil.colors.red('ERROR') + ': ' + message); process.exit(1); } function execPiped (command, args, fileName) { console.log(command + ' ' + args.join(' ')); var task = child_process.spawn(command, args); return task.stdout.pipe(vstream(fileName)).pipe(buffer()); } function exec (command, args, cb) { console.log(command + ' ' + args.join(' ')); var task = child_process.spawn(command, args, { stdio: 'inherit' }); task.on('exit', cb); } function bin (name) { return path.join(BIN_DIR, name); } function remove (path) { console.log('removing ' + path); fse.removeSync(path); } function getBundleExecutable () { if (process.platform === 'win32') { return 'bundle.bat'; } else { return 'bundle'; } } function getJekyllConfigs () { var configs = BASE_CONFIGS; // add build-specific config files if (gutil.env.prod) { configs = configs.concat(PROD_CONFIGS); } else { configs = configs.concat(DEV_CONFIGS); } // add a special exclude file if "nodocs" was specified if (gutil.env.nodocs) { configs = configs.concat(NODOCS_CONFIG_FILE); } return configs; } function jekyllBuild (done) { var bundle = getBundleExecutable(); var configs = getJekyllConfigs(); var flags = gutil.env.prod ? PROD_FLAGS : DEV_FLAGS; flags = flags.concat(['--config', configs.join(',')]); exec(bundle, ['exec', 'jekyll', 'build'].concat(flags), done); } function copyDocsVersion (oldVersion, newVersion, cb) { // copying a folder and a ToC file for each language var numCopyOperations = LANGUAGES.length * 2; // pseudo-CV (condition variable) var numCopied = 0; function doneCopying (error) { if (error) { cb(error); return; } // call callback if all folders have finished copying numCopied += 1; if (numCopied === numCopyOperations) { cb(); } } // create a new version for each language LANGUAGES.forEach(function (languageName) { // get files to copy var oldVersionDocs = path.join(DOCS_DIR, languageName, oldVersion); var oldVersionToc = path.join(TOC_DIR, util.srcTocfileName(languageName, oldVersion)); var newVersionDocs = path.join(DOCS_DIR, languageName, newVersion); var newVersionToc = path.join(TOC_DIR, util.srcTocfileName(languageName, newVersion)); var copyOptions = { stopOnErr: true }; // copy docs console.log(oldVersionDocs + ' -> ' + newVersionDocs); ncp.ncp(oldVersionDocs, newVersionDocs, copyOptions, doneCopying); // copy ToC console.log(oldVersionToc + ' -> ' + newVersionToc); ncp.ncp(oldVersionToc, newVersionToc, copyOptions, doneCopying); }); } // tasks gulp.task('default', ['help']); gulp.task('help', function () { gutil.log(''); gutil.log('Tasks:'); gutil.log(''); gutil.log(' build same as configs + data + styles + plugins + jekyll'); gutil.log(' jekyll build with jekyll'); gutil.log(' regen same as jekyll + reload'); gutil.log(' serve build the site and open it in a browser'); gutil.log(' reload refresh the browser'); gutil.log(''); gutil.log(' newversion create ' + NEXT_DOCS_VERSION + ' docs from dev docs'); gutil.log(' snapshot copy dev docs to ' + LATEST_DOCS_VERSION + ' docs'); gutil.log(''); gutil.log(' plugins build ' + PLUGINS_FILE); gutil.log(''); gutil.log(' configs run all the below tasks'); gutil.log(' defaults create ' + DEFAULTS_CONFIG_FILE); gutil.log(' version create ' + VERSION_CONFIG_FILE); gutil.log(''); gutil.log(' data run all the below tasks'); gutil.log(' docs-versions create ' + DOCS_VERSION_FILE); gutil.log(' pages-dict create ' + ALL_PAGES_FILE); gutil.log(' toc create all generated ToC files in ' + TOC_DIR); gutil.log(' fetch download docs specified in ' + FETCH_CONFIG); gutil.log(''); gutil.log(' styles run all the below tasks'); gutil.log(' less compile all .less files'); gutil.log(' sass compile all .scss files'); gutil.log(' css copy over all .css files'); gutil.log(''); gutil.log(' watch serve + then watch all source files and regenerate as necessary'); gutil.log(' link-bugs replace CB-XXXX references with nice links'); gutil.log(''); gutil.log(' help show this text'); gutil.log(' clean remove all generated files and folders'); gutil.log(''); gutil.log('Arguments:'); gutil.log(" --nodocs don't generate docs"); gutil.log(' --prod build for production; without it, will build dev instead'); gutil.log(''); }); gulp.task('data', ['toc', 'docs-versions', 'pages-dict']); gulp.task('configs', ['defaults', 'version']); gulp.task('styles', ['less', 'css', 'sass']); gulp.task('watch', ['serve'], function () { gulp.watch( [ path.join(CSS_SRC_DIR, '**', '*') ], { interval: WATCH_INTERVAL }, ['styles'] ); gulp.watch( [ path.join(PLUGINS_SRC_DIR, '**', '*.js'), path.join(PLUGINS_SRC_DIR, '**', '*.jsx'), path.join(PLUGINS_SRC_DIR, '**', '*.json') ], { interval: WATCH_INTERVAL }, ['plugins'] ); gulp.watch( [ path.join(ROOT_DIR, '**', '*.yml'), path.join(JS_DIR, '**', '*.js'), path.join(CSS_OUT_DIR, '**', '*.css'), // NOTE: // watch all non-docs HTML, and only docs/en/dev HTML because // versions other than dev usually don't change much; this is // an optimization path.join(SOURCE_DIR, '_layouts', '*.html'), path.join(SOURCE_DIR, '_includes', '*.html'), path.join(SOURCE_DIR, '**', '*.html') + '!' + path.join(DOCS_DIR, '**'), path.join(SOURCE_DIR, '**', '*.md') + '!' + path.join(DOCS_DIR, '**'), path.join(DOCS_DIR, 'en', 'dev', '**', '*.md'), path.join(DOCS_DIR, 'en', 'dev', '**', '*.html') ], { interval: WATCH_INTERVAL }, ['regen'] ); }); gulp.task('serve', ['build'], function () { var route = {}; // set site root for browsersync if (gutil.env.prod) { route[BASE_URL] = gutil.env.outDir; } browsersync({ notify: true, server: { baseDir: gutil.env.outDir, routes: route } }); }); gulp.task('build', ['configs', 'data', 'styles', 'plugins'], function (done) { jekyllBuild(done); }); gulp.task('jekyll', function (done) { jekyllBuild(done); }); gulp.task('regen', ['jekyll'], function () { browsersync.reload(); }); gulp.task('fetch', function (done) { // skip fetching if --nofetch was passed if (gutil.env.nofetch) { gutil.log(gutil.colors.yellow( 'Skipping fetching external docs.')); done(); return; } exec('node', [bin('fetch_docs.js'), '--config', FETCH_CONFIG, '--docsRoot', DOCS_DIR], done); }); gulp.task('reload', function () { browsersync.reload(); }); gulp.task('docs-versions', function () { return execPiped('node', [bin('gen_versions.js'), DOCS_DIR], DOCS_VERSION_FILE) .pipe(gulp.dest(ROOT_DIR)); }); gulp.task('pages-dict', function () { var args = [ bin('gen_pages_dict.js'), '--siteRoot', SOURCE_DIR, '--redirectsFile', REDIRECTS_FILE, '--latestVersion', LATEST_DOCS_VERSION, '--languages', LANGUAGES.join(',') ]; return execPiped('node', args, ALL_PAGES_FILE).pipe(gulp.dest(ROOT_DIR)); }); gulp.task('version', function () { // this code is stupid; it's basically the line: // cat VERSION | sed -e 's/^/VERSION_VAR_NAME: /' > _version.yml // however we're in Gulp, and we need to support Windows... // so we contort it into a monster return gulp .src(VERSION_FILE) .pipe(header(VERSION_VAR_NAME + ': ')) .pipe(footer('\n')) .pipe(rename(VERSION_CONFIG_FILE)) .pipe(gulp.dest('.')); }); gulp.task('defaults', function () { return execPiped('node', [bin('gen_defaults.js'), DOCS_DIR, LATEST_DOCS_VERSION], DEFAULTS_CONFIG_FILE) .pipe(gulp.dest(ROOT_DIR)); }); gulp.task('toc', ['fetch'], function (done) { exec('node', [bin('toc.js'), DOCS_DIR, TOC_DIR], done); }); gulp.task('less', function () { return gulp .src(path.join(CSS_SRC_DIR, '**', '*.less')) .pipe(less()) .pipe(header(YAML_FRONT_MATTER)) .pipe(gulp.dest(CSS_OUT_DIR)) .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir))) .pipe(browsersync.reload({ stream: true })); }); gulp.task('css', function () { return gulp .src(path.join(CSS_SRC_DIR, '**', '*.css')) .pipe(header(YAML_FRONT_MATTER)) .pipe(gulp.dest(CSS_OUT_DIR)) .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir))) .pipe(browsersync.reload({ stream: true })); }); gulp.task('sass', function () { return gulp .src(path.join(CSS_SRC_DIR, '**', '*.scss')) .pipe(sass().on('error', sass.logError)) .pipe(header(YAML_FRONT_MATTER)) .pipe(gulp.dest(CSS_OUT_DIR)) .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir))) .pipe(browsersync.reload({ stream: true })); }); gulp.task('plugins', function () { if (gutil.env.prod) { process.env.NODE_ENV = 'production'; } var stream = browserify(PLUGINS_SRC_FILE, { debug: !gutil.env.prod }) .transform(babelify, { presets: ['react'], plugins: [ ['transform-react-jsx', { 'pragma': 'h' }] ] }) .transform(envify) .bundle() .on('error', gutil.log) .pipe(vstream(PLUGINS_FILE_NAME)) .pipe(buffer()); if (gutil.env.prod) { stream = stream .pipe(uglify()) .on('error', gutil.log); } return stream .pipe(gulp.dest(JS_DIR.replace(SOURCE_DIR, gutil.env.outDir))) .pipe(browsersync.reload({ stream: true })) // NOTE: // adding YAML front matter after doing everything // else so that uglify doesn't screw it up .pipe(header(YAML_FRONT_MATTER)) // WORKAROUND: // minified JS has some things that look like // Liquid tags, so we replace them manually .pipe(replace('){{', '){ {')) .pipe(gulp.dest(JS_DIR)); }); // convenience tasks gulp.task('link-bugs', function (done) { exec(bin('linkify-bugs.sh'), [path.join(SOURCE_DIR, '_posts')], done); }); gulp.task('lint', function () { return gulp.src(path.join('./', '**', '*.html')) .pipe(htmllint()); }); gulp.task('newversion', ['fetch'], function (done) { copyDocsVersion('dev', NEXT_DOCS_VERSION, function (error) { if (error) { console.error(error); done(); return; } // finally update the version file with the new version fs.writeFile(VERSION_FILE, NEXT_DOCS_VERSION + '\n', done); }); }); gulp.task('snapshot', ['fetch'], function (done) { // remove current version first LANGUAGES.forEach(function (languageName) { var languageLatestDocs = path.join(DOCS_DIR, languageName, LATEST_DOCS_VERSION); remove(languageLatestDocs); }); copyDocsVersion('dev', LATEST_DOCS_VERSION, done); }); gulp.task('checklinks', function (done) { crawler .crawl('http://localhost:3000/') .on('fetch404', function (queueItem, response) { gutil.log( 'Resource not found linked from ' + queueItem.referrer + ' to', queueItem.url ); gutil.log('Status code: ' + response.statusCode); }) .on('complete', function (queueItem) { done(); }); }); gulp.task('clean', function () { remove(DEV_DIR); remove(PROD_DIR); remove(FETCH_DIR); remove(path.join(DATA_DIR, 'toc', '*-gen.yml')); remove(CSS_OUT_DIR); remove(PLUGINS_FILE); remove(DOCS_VERSION_FILE); remove(ALL_PAGES_FILE); remove(DEFAULTS_CONFIG_FILE); remove(VERSION_CONFIG_FILE); });