blob: 55dc4bf5c74d6c2b13432fd2cce7ddad1c551336 [file] [log] [blame]
'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);
});