gulpfile.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. 'use strict';
  2. // dependencies
  3. var path = require('path');
  4. var fs = require('fs');
  5. var fse = require('fs-extra');
  6. var child_process = require('child_process');
  7. var gulp = require('gulp');
  8. var gutil = require('gulp-util');
  9. var less = require('gulp-less');
  10. var sass = require('gulp-sass');
  11. var replace = require('gulp-replace');
  12. var header = require('gulp-header');
  13. var footer = require('gulp-footer');
  14. var rename = require('gulp-rename');
  15. var browsersync = require('browser-sync');
  16. var vstream = require('vinyl-source-stream');
  17. var buffer = require('vinyl-buffer');
  18. var browserify = require('browserify');
  19. var babelify = require('babelify');
  20. var uglify = require('gulp-uglify');
  21. var envify = require('envify');
  22. var htmllint = require('gulp-htmllint');
  23. var crawler = require('simplecrawler');
  24. var ncp = require('ncp');
  25. var nextversion = require('./tools/bin/nextversion');
  26. var util = require('./tools/bin/util');
  27. // constants
  28. var ROOT_DIR = '.';
  29. var CONFIG_DIR = 'conf';
  30. var SOURCE_DIR = path.join(ROOT_DIR, 'www');
  31. var DEV_DIR = path.join(ROOT_DIR, 'build-dev');
  32. var PROD_DIR = path.join(ROOT_DIR, 'build-prod');
  33. var DATA_DIR = path.join(SOURCE_DIR, '_data');
  34. var TOC_DIR = path.join(DATA_DIR, 'toc');
  35. var DOCS_DIR = path.join(SOURCE_DIR, 'docs');
  36. var FETCH_DIR = path.join(DOCS_DIR, 'en', 'dev', 'reference');
  37. var CSS_SRC_DIR = path.join(SOURCE_DIR, 'static', 'css-src');
  38. var CSS_OUT_DIR = path.join(SOURCE_DIR, 'static', 'css');
  39. var PLUGINS_SRC_DIR = path.join(SOURCE_DIR, 'static', 'plugins');
  40. var JS_DIR = path.join(SOURCE_DIR, 'static', 'js');
  41. var BIN_DIR = path.join(ROOT_DIR, 'tools', 'bin');
  42. var CONFIG_FILE = path.join(CONFIG_DIR, '_config.yml');
  43. var DEFAULTS_CONFIG_FILE = path.join(CONFIG_DIR, '_defaults.yml');
  44. var VERSION_CONFIG_FILE = path.join(CONFIG_DIR, '_version.yml');
  45. var PROD_CONFIG_FILE = path.join(CONFIG_DIR, '_prod.yml');
  46. var DEV_CONFIG_FILE = path.join(CONFIG_DIR, '_dev.yml');
  47. var NODOCS_CONFIG_FILE = path.join(CONFIG_DIR, '_nodocs.yml');
  48. var VERSION_FILE = 'VERSION';
  49. var DOCS_VERSION_FILE = path.join(DATA_DIR, 'docs-versions.yml');
  50. var ALL_PAGES_FILE = path.join(DATA_DIR, 'all-pages.yml');
  51. var FETCH_CONFIG = path.join(DATA_DIR, 'fetched-files.yml');
  52. var REDIRECTS_FILE = path.join(DATA_DIR, 'redirects.yml');
  53. var PLUGINS_FILE_NAME = 'plugins.js';
  54. var PLUGINS_FILE = path.join(JS_DIR, PLUGINS_FILE_NAME);
  55. var PLUGINS_SRC_FILE = path.join(PLUGINS_SRC_DIR, 'app.js');
  56. var BASE_CONFIGS = [CONFIG_FILE, DEFAULTS_CONFIG_FILE, VERSION_CONFIG_FILE];
  57. var DEV_CONFIGS = [DEV_CONFIG_FILE];
  58. var PROD_CONFIGS = [PROD_CONFIG_FILE];
  59. var DEV_FLAGS = ['--trace'];
  60. var PROD_FLAGS = [];
  61. var BASE_URL = '';
  62. var YAML_FRONT_MATTER = '---\n---\n';
  63. var WATCH_INTERVAL = 1000; // in milliseconds
  64. var VERSION_VAR_NAME = 'latest_docs_version';
  65. var LATEST_DOCS_VERSION = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
  66. var NEXT_DOCS_VERSION = nextversion.getNextVersion(LATEST_DOCS_VERSION);
  67. var LANGUAGES = util.listdirsSync(DOCS_DIR);
  68. var PROD_BY_DEFAULT = false;
  69. // compute/get/set/adjust passed options
  70. gutil.env.prod = gutil.env.prod || PROD_BY_DEFAULT;
  71. gutil.env.dev = !gutil.env.prod;
  72. gutil.env.outDir = gutil.env.prod ? PROD_DIR : DEV_DIR;
  73. // check for errors
  74. if (gutil.env.prod && gutil.env.nodocs) {
  75. fatal("can't ignore docs when doing a production build");
  76. }
  77. // helpers
  78. function fatal (message) {
  79. gutil.log(gutil.colors.red('ERROR') + ': ' + message);
  80. process.exit(1);
  81. }
  82. function execPiped (command, args, fileName) {
  83. console.log(command + ' ' + args.join(' '));
  84. var task = child_process.spawn(command, args);
  85. return task.stdout.pipe(vstream(fileName)).pipe(buffer());
  86. }
  87. function exec (command, args, cb) {
  88. console.log(command + ' ' + args.join(' '));
  89. var task = child_process.spawn(command, args, { stdio: 'inherit' });
  90. task.on('exit', cb);
  91. }
  92. function bin (name) {
  93. return path.join(BIN_DIR, name);
  94. }
  95. function remove (path) {
  96. console.log('removing ' + path);
  97. fse.removeSync(path);
  98. }
  99. function getBundleExecutable () {
  100. if (process.platform === 'win32') {
  101. return 'bundle.bat';
  102. } else {
  103. return 'bundle';
  104. }
  105. }
  106. function getJekyllConfigs () {
  107. var configs = BASE_CONFIGS;
  108. // add build-specific config files
  109. if (gutil.env.prod) {
  110. configs = configs.concat(PROD_CONFIGS);
  111. } else {
  112. configs = configs.concat(DEV_CONFIGS);
  113. }
  114. // add a special exclude file if "nodocs" was specified
  115. if (gutil.env.nodocs) {
  116. configs = configs.concat(NODOCS_CONFIG_FILE);
  117. }
  118. return configs;
  119. }
  120. function jekyllBuild (done) {
  121. var bundle = getBundleExecutable();
  122. var configs = getJekyllConfigs();
  123. var flags = gutil.env.prod ? PROD_FLAGS : DEV_FLAGS;
  124. flags = flags.concat(['--config', configs.join(',')]);
  125. exec(bundle, ['exec', 'jekyll', 'build'].concat(flags), done);
  126. }
  127. function copyDocsVersion (oldVersion, newVersion, cb) {
  128. // copying a folder and a ToC file for each language
  129. var numCopyOperations = LANGUAGES.length * 2;
  130. // pseudo-CV (condition variable)
  131. var numCopied = 0;
  132. function doneCopying (error) {
  133. if (error) {
  134. cb(error);
  135. return;
  136. }
  137. // call callback if all folders have finished copying
  138. numCopied += 1;
  139. if (numCopied === numCopyOperations) {
  140. cb();
  141. }
  142. }
  143. // create a new version for each language
  144. LANGUAGES.forEach(function (languageName) {
  145. // get files to copy
  146. var oldVersionDocs = path.join(DOCS_DIR, languageName, oldVersion);
  147. var oldVersionToc = path.join(TOC_DIR, util.srcTocfileName(languageName, oldVersion));
  148. var newVersionDocs = path.join(DOCS_DIR, languageName, newVersion);
  149. var newVersionToc = path.join(TOC_DIR, util.srcTocfileName(languageName, newVersion));
  150. var copyOptions = {
  151. stopOnErr: true
  152. };
  153. // copy docs
  154. console.log(oldVersionDocs + ' -> ' + newVersionDocs);
  155. ncp.ncp(oldVersionDocs, newVersionDocs, copyOptions, doneCopying);
  156. // copy ToC
  157. console.log(oldVersionToc + ' -> ' + newVersionToc);
  158. ncp.ncp(oldVersionToc, newVersionToc, copyOptions, doneCopying);
  159. });
  160. }
  161. // tasks
  162. gulp.task('default', ['help']);
  163. gulp.task('help', function () {
  164. gutil.log('');
  165. gutil.log('Tasks:');
  166. gutil.log('');
  167. gutil.log(' build same as configs + data + styles + plugins + jekyll');
  168. gutil.log(' jekyll build with jekyll');
  169. gutil.log(' regen same as jekyll + reload');
  170. gutil.log(' serve build the site and open it in a browser');
  171. gutil.log(' reload refresh the browser');
  172. gutil.log('');
  173. gutil.log(' newversion create ' + NEXT_DOCS_VERSION + ' docs from dev docs');
  174. gutil.log(' snapshot copy dev docs to ' + LATEST_DOCS_VERSION + ' docs');
  175. gutil.log('');
  176. gutil.log(' plugins build ' + PLUGINS_FILE);
  177. gutil.log('');
  178. gutil.log(' configs run all the below tasks');
  179. gutil.log(' defaults create ' + DEFAULTS_CONFIG_FILE);
  180. gutil.log(' version create ' + VERSION_CONFIG_FILE);
  181. gutil.log('');
  182. gutil.log(' data run all the below tasks');
  183. gutil.log(' docs-versions create ' + DOCS_VERSION_FILE);
  184. gutil.log(' pages-dict create ' + ALL_PAGES_FILE);
  185. gutil.log(' toc create all generated ToC files in ' + TOC_DIR);
  186. gutil.log(' fetch download docs specified in ' + FETCH_CONFIG);
  187. gutil.log('');
  188. gutil.log(' styles run all the below tasks');
  189. gutil.log(' less compile all .less files');
  190. gutil.log(' sass compile all .scss files');
  191. gutil.log(' css copy over all .css files');
  192. gutil.log('');
  193. gutil.log(' watch serve + then watch all source files and regenerate as necessary');
  194. gutil.log(' link-bugs replace CB-XXXX references with nice links');
  195. gutil.log('');
  196. gutil.log(' help show this text');
  197. gutil.log(' clean remove all generated files and folders');
  198. gutil.log('');
  199. gutil.log('Arguments:');
  200. gutil.log(" --nodocs don't generate docs");
  201. gutil.log(' --prod build for production; without it, will build dev instead');
  202. gutil.log('');
  203. });
  204. gulp.task('data', ['toc', 'docs-versions', 'pages-dict']);
  205. gulp.task('configs', ['defaults', 'version']);
  206. gulp.task('styles', ['less', 'css', 'sass']);
  207. gulp.task('watch', ['serve'], function () {
  208. gulp.watch(
  209. [
  210. path.join(CSS_SRC_DIR, '**', '*')
  211. ],
  212. { interval: WATCH_INTERVAL },
  213. ['styles']
  214. );
  215. gulp.watch(
  216. [
  217. path.join(PLUGINS_SRC_DIR, '**', '*.js'),
  218. path.join(PLUGINS_SRC_DIR, '**', '*.jsx'),
  219. path.join(PLUGINS_SRC_DIR, '**', '*.json')
  220. ],
  221. { interval: WATCH_INTERVAL },
  222. ['plugins']
  223. );
  224. gulp.watch(
  225. [
  226. path.join(ROOT_DIR, '**', '*.yml'),
  227. path.join(JS_DIR, '**', '*.js'),
  228. path.join(CSS_OUT_DIR, '**', '*.css'),
  229. // NOTE:
  230. // watch all non-docs HTML, and only docs/en/dev HTML because
  231. // versions other than dev usually don't change much; this is
  232. // an optimization
  233. path.join(SOURCE_DIR, '_layouts', '*.html'),
  234. path.join(SOURCE_DIR, '_includes', '*.html'),
  235. path.join(SOURCE_DIR, '**', '*.html') + '!' + path.join(DOCS_DIR, '**'),
  236. path.join(SOURCE_DIR, '**', '*.md') + '!' + path.join(DOCS_DIR, '**'),
  237. path.join(DOCS_DIR, 'en', 'dev', '**', '*.md'),
  238. path.join(DOCS_DIR, 'en', 'dev', '**', '*.html')
  239. ],
  240. { interval: WATCH_INTERVAL },
  241. ['regen']
  242. );
  243. });
  244. gulp.task('serve', ['build'], function () {
  245. var route = {};
  246. // set site root for browsersync
  247. if (gutil.env.prod) {
  248. route[BASE_URL] = gutil.env.outDir;
  249. }
  250. browsersync({
  251. notify: true,
  252. server: {
  253. baseDir: gutil.env.outDir,
  254. routes: route
  255. }
  256. });
  257. });
  258. gulp.task('build', ['configs', 'data', 'styles', 'plugins'], function (done) {
  259. jekyllBuild(done);
  260. });
  261. gulp.task('jekyll', function (done) {
  262. jekyllBuild(done);
  263. });
  264. gulp.task('regen', ['jekyll'], function () {
  265. browsersync.reload();
  266. });
  267. gulp.task('fetch', function (done) {
  268. // skip fetching if --nofetch was passed
  269. if (gutil.env.nofetch) {
  270. gutil.log(gutil.colors.yellow(
  271. 'Skipping fetching external docs.'));
  272. done();
  273. return;
  274. }
  275. exec('node', [bin('fetch_docs.js'), '--config', FETCH_CONFIG, '--docsRoot', DOCS_DIR], done);
  276. });
  277. gulp.task('reload', function () {
  278. browsersync.reload();
  279. });
  280. gulp.task('docs-versions', function () {
  281. return execPiped('node', [bin('gen_versions.js'), DOCS_DIR], DOCS_VERSION_FILE)
  282. .pipe(gulp.dest(ROOT_DIR));
  283. });
  284. gulp.task('pages-dict', function () {
  285. var args = [
  286. bin('gen_pages_dict.js'),
  287. '--siteRoot', SOURCE_DIR,
  288. '--redirectsFile', REDIRECTS_FILE,
  289. '--latestVersion', LATEST_DOCS_VERSION,
  290. '--languages', LANGUAGES.join(',')
  291. ];
  292. return execPiped('node', args, ALL_PAGES_FILE).pipe(gulp.dest(ROOT_DIR));
  293. });
  294. gulp.task('version', function () {
  295. // this code is stupid; it's basically the line:
  296. // cat VERSION | sed -e 's/^/VERSION_VAR_NAME: /' > _version.yml
  297. // however we're in Gulp, and we need to support Windows...
  298. // so we contort it into a monster
  299. return gulp
  300. .src(VERSION_FILE)
  301. .pipe(header(VERSION_VAR_NAME + ': '))
  302. .pipe(footer('\n'))
  303. .pipe(rename(VERSION_CONFIG_FILE))
  304. .pipe(gulp.dest('.'));
  305. });
  306. gulp.task('defaults', function () {
  307. return execPiped('node', [bin('gen_defaults.js'), DOCS_DIR, LATEST_DOCS_VERSION], DEFAULTS_CONFIG_FILE)
  308. .pipe(gulp.dest(ROOT_DIR));
  309. });
  310. gulp.task('toc', ['fetch'], function (done) {
  311. exec('node', [bin('toc.js'), DOCS_DIR, TOC_DIR], done);
  312. });
  313. gulp.task('less', function () {
  314. return gulp
  315. .src(path.join(CSS_SRC_DIR, '**', '*.less'))
  316. .pipe(less())
  317. .pipe(header(YAML_FRONT_MATTER))
  318. .pipe(gulp.dest(CSS_OUT_DIR))
  319. .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
  320. .pipe(browsersync.reload({ stream: true }));
  321. });
  322. gulp.task('css', function () {
  323. return gulp
  324. .src(path.join(CSS_SRC_DIR, '**', '*.css'))
  325. .pipe(header(YAML_FRONT_MATTER))
  326. .pipe(gulp.dest(CSS_OUT_DIR))
  327. .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
  328. .pipe(browsersync.reload({ stream: true }));
  329. });
  330. gulp.task('sass', function () {
  331. return gulp
  332. .src(path.join(CSS_SRC_DIR, '**', '*.scss'))
  333. .pipe(sass().on('error', sass.logError))
  334. .pipe(header(YAML_FRONT_MATTER))
  335. .pipe(gulp.dest(CSS_OUT_DIR))
  336. .pipe(gulp.dest(CSS_OUT_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
  337. .pipe(browsersync.reload({ stream: true }));
  338. });
  339. gulp.task('plugins', function () {
  340. if (gutil.env.prod) {
  341. process.env.NODE_ENV = 'production';
  342. }
  343. var stream = browserify(PLUGINS_SRC_FILE, { debug: !gutil.env.prod })
  344. .transform(babelify, {
  345. presets: ['react'],
  346. plugins: [
  347. ['transform-react-jsx', { 'pragma': 'h' }]
  348. ]
  349. })
  350. .transform(envify)
  351. .bundle()
  352. .on('error', gutil.log)
  353. .pipe(vstream(PLUGINS_FILE_NAME))
  354. .pipe(buffer());
  355. if (gutil.env.prod) {
  356. stream = stream
  357. .pipe(uglify())
  358. .on('error', gutil.log);
  359. }
  360. return stream
  361. .pipe(gulp.dest(JS_DIR.replace(SOURCE_DIR, gutil.env.outDir)))
  362. .pipe(browsersync.reload({ stream: true }))
  363. // NOTE:
  364. // adding YAML front matter after doing everything
  365. // else so that uglify doesn't screw it up
  366. .pipe(header(YAML_FRONT_MATTER))
  367. // WORKAROUND:
  368. // minified JS has some things that look like
  369. // Liquid tags, so we replace them manually
  370. .pipe(replace('){{', '){ {'))
  371. .pipe(gulp.dest(JS_DIR));
  372. });
  373. // convenience tasks
  374. gulp.task('link-bugs', function (done) {
  375. exec(bin('linkify-bugs.sh'), [path.join(SOURCE_DIR, '_posts')], done);
  376. });
  377. gulp.task('lint', function () {
  378. return gulp.src(path.join('./', '**', '*.html'))
  379. .pipe(htmllint());
  380. });
  381. gulp.task('newversion', ['fetch'], function (done) {
  382. copyDocsVersion('dev', NEXT_DOCS_VERSION, function (error) {
  383. if (error) {
  384. console.error(error);
  385. done();
  386. return;
  387. }
  388. // finally update the version file with the new version
  389. fs.writeFile(VERSION_FILE, NEXT_DOCS_VERSION + '\n', done);
  390. });
  391. });
  392. gulp.task('snapshot', ['fetch'], function (done) {
  393. // remove current version first
  394. LANGUAGES.forEach(function (languageName) {
  395. var languageLatestDocs = path.join(DOCS_DIR, languageName, LATEST_DOCS_VERSION);
  396. remove(languageLatestDocs);
  397. });
  398. copyDocsVersion('dev', LATEST_DOCS_VERSION, done);
  399. });
  400. gulp.task('checklinks', function (done) {
  401. crawler
  402. .crawl('http://localhost:3000/')
  403. .on('fetch404', function (queueItem, response) {
  404. gutil.log(
  405. 'Resource not found linked from ' +
  406. queueItem.referrer + ' to', queueItem.url
  407. );
  408. gutil.log('Status code: ' + response.statusCode);
  409. })
  410. .on('complete', function (queueItem) {
  411. done();
  412. });
  413. });
  414. gulp.task('clean', function () {
  415. remove(DEV_DIR);
  416. remove(PROD_DIR);
  417. remove(FETCH_DIR);
  418. remove(path.join(DATA_DIR, 'toc', '*-gen.yml'));
  419. remove(CSS_OUT_DIR);
  420. remove(PLUGINS_FILE);
  421. remove(DOCS_VERSION_FILE);
  422. remove(ALL_PAGES_FILE);
  423. remove(DEFAULTS_CONFIG_FILE);
  424. remove(VERSION_CONFIG_FILE);
  425. });