require('require-yaml'); const gulp = require('gulp'); const exec = require('child_process').exec; const PluginError = require('plugin-error'); const argv = require('minimist')(process.argv.slice(2)); const log = require('fancy-log'); // Configuration let isWindows = /^win/.test(process.platform); if (argv.NODE_ENV) process.env.NODE_ENV = argv.NODE_ENV; let langs = ['es', 'en']; let srcDir = './front'; let modulesDir = './modules'; let buildDir = 'dist'; let backSources = [ '!node_modules', 'loopback', 'modules/*/back/**', 'back', 'print' ]; // Development const localesRoutes = gulp.parallel(locales, routes); localesRoutes.description = `Builds locales and routes`; const front = gulp.series(clean, gulp.parallel(localesRoutes, watch, webpackDevServer)); front.description = `Starts frontend service`; function backOnly(done) { let app = require(`./loopback/server/server`); app.start(); app.on('started', done); } backOnly.description = `Starts backend service`; function backWatch(done) { const nodemon = require('gulp-nodemon'); // XXX: Workaround to avoid nodemon bug // https://github.com/remy/nodemon/issues/1346 let sleepBin = isWindows ? '' : 'sleep 1 &&'; nodemon({ exec: `${sleepBin} node --inspect ./node_modules/gulp/bin/gulp.js`, args: ['backOnly'], watch: backSources, done: done }); } backWatch.description = `Starts backend in waching mode`; const back = gulp.series(dockerStart, backWatch); back.description = `Starts backend and database service`; const defaultTask = gulp.parallel(front, back); defaultTask.description = `Starts all application services`; // Backend tests function backendUnitTest() { let app = require(`./loopback/server/server`); let specFiles = [ 'back/**/*.spec.js', 'loopback/**/*.spec.js', 'modules/*/back/**/*.spec.js' ]; const jasmine = require('gulp-jasmine'); let options = {errorOnFail: false}; if (argv.junit || argv.j) { const reporters = require('jasmine-reporters'); options.reporter = new reporters.JUnitXmlReporter(); } return gulp.src(specFiles) .pipe(jasmine(options)) .on('jasmineDone', function() { app.disconnect(); }); } backendUnitTest.description = `Runs the backend tests only, can receive args --junit or -j to save reports on a xml file`; const dockerAndBackTest = gulp.series(docker, backendUnitTest); dockerAndBackTest.description = `Restarts database and runs the backend tests`; function backTest(done) { const nodemon = require('gulp-nodemon'); let gulpBin = isWindows ? 'node_modules/.bin/gulp.cmd' : 'node_modules/.bin/gulp'; nodemon({ exec: gulpBin, args: ['dockerAndBackTest'], watch: backSources, done: done }); } backTest.description = `Watches for changes in modules to execute backTest task`; // End to end tests function e2eOnly() { const jasmine = require('gulp-jasmine'); if (argv.show || argv.s) process.env.E2E_SHOW = true; return gulp.src('./e2e/tests.js') .pipe(jasmine({reporter: 'none'})); } e2eOnly.description = `Runs the e2e tests only`; e2e = gulp.series(docker, e2eOnly); e2e.description = `Restarts database and runs the e2e tests`; function smokesOnly() { const jasmine = require('gulp-jasmine'); return gulp.src('./e2e/smokes-tests.js') .pipe(jasmine({reporter: 'none'})); } smokesOnly.description = `Runs the smokes tests only`; smokes = gulp.series(docker, smokesOnly); smokes.description = `Restarts database and runs the smokes tests`; function install() { const install = require('gulp-install'); const print = require('gulp-print'); let packageFiles = ['front/package.json', 'print/package.json']; return gulp.src(packageFiles) .pipe(print(filepath => { return `Installing packages in ${filepath}`; })) .pipe(install({ npm: ['--no-package-lock'] })); } install.description = `Installs node dependencies in all directories`; const i = gulp.series(install); i.description = `Alias for the 'install' task`; // Deployment const build = gulp.series(clean, gulp.parallel(localesRoutes, webpack)); build.description = `Generates binaries and configuration files`; function clean() { const del = require('del'); const files = [ `${buildDir}/*` ]; return del(files, {force: true}); } clean.description = `Cleans all files generated by the 'build' task`; // Webpack function webpack(done) { const webpackCompile = require('webpack'); const merge = require('webpack-merge'); let wpConfig = require('./webpack.config.js'); wpConfig = merge(wpConfig, {}); let compiler = webpackCompile(wpConfig); compiler.run(function(err, stats) { if (err) throw new PluginError('webpack', err); log('[webpack]', stats.toString(wpConfig.stats)); done(); }); } webpack.description = `Transpiles application into files`; function webpackDevServer(done) { const webpack = require('webpack'); const merge = require('webpack-merge'); const WebpackDevServer = require('webpack-dev-server'); let wpConfig = require('./webpack.config.js'); wpConfig = merge(wpConfig, {}); let devServer = wpConfig.devServer; for (let entryName in wpConfig.entry) { let entry = wpConfig.entry[entryName]; if (!Array.isArray(entry)) entry = [entry]; let wdsAssets = [ `webpack-dev-server/client?http://localhost:${devServer.port}/`, `webpack/hot/dev-server` ]; wpConfig.entry[entryName] = wdsAssets.concat(entry); } let compiler = webpack(wpConfig); new WebpackDevServer(compiler, wpConfig.devServer) .listen(devServer.port, devServer.host, function(err) { if (err) throw new PluginError('webpack-dev-server', err); // TODO: Keep the server alive or continue? done(); }); } webpackDevServer.description = `Transpiles application into memory`; // Locale let localeFiles = [ `${srcDir}/**/locale/*.yml`, `${modulesDir}/*/front/**/locale/*.yml` ]; /** * Mixes all locale files into one JSON file per module and language. It looks * recursively in all project directories for locale folders with per language * yaml translation files. * * @return {Stream} The merged gulp streams */ function locales() { const mergeJson = require('gulp-merge-json'); const yaml = require('gulp-yaml'); const merge = require('merge-stream'); const fs = require('fs-extra'); let streams = []; let localePaths = []; let modules = fs.readdirSync(modulesDir); for (let mod of modules) localePaths[mod] = `${modulesDir}/${mod}`; let baseMods = ['core', 'salix']; for (let mod of baseMods) localePaths[mod] = `${srcDir}/${mod}`; for (let mod in localePaths) { let path = localePaths[mod]; for (let lang of langs) { let localeFiles = `${path}/**/locale/${lang}.yml`; streams.push(gulp.src(localeFiles) .pipe(yaml()) .pipe(mergeJson({fileName: `${lang}.json`})) .pipe(gulp.dest(`${buildDir}/locale/${mod}`))); } } return merge(streams); } locales.description = `Generates client locale files`; // Routes let routeFiles = `${modulesDir}/*/front/routes.json`; function routes() { const concat = require('gulp-concat'); const wrap = require('gulp-wrap'); return gulp.src(routeFiles) .pipe(concat('routes.js', {newLine: ','})) .pipe(wrap('var routes = [<%=contents%>\n];')) .pipe(gulp.dest(buildDir)); } routes.description = 'Merges all module routes file into one file'; // Watch function watch(done) { gulp.watch(routeFiles, gulp.series(routes)); gulp.watch(localeFiles, gulp.series(locales)); done(); } watch.description = `Watches for changes in routes and locale files`; // Docker /** * Builds the database image and runs a container. It only rebuilds the * image when fixtures have been modified or when the day on which the * image was built is different to today. Some workarounds have been used * to avoid a bug with OverlayFS driver on MacOS. */ async function docker() { try { await execP('docker rm -fv salix-db'); } catch (e) {} let d = new Date(); let pad = v => v < 10 ? '0' + v : v; let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; await execP(`docker build --build-arg STAMP=${stamp} -t salix-db ./services/db`); let runChown = process.platform != 'linux'; if (argv['run-chown']) runChown = true; await execP(`docker run --env RUN_CHOWN=${runChown} -d --name salix-db -p 3306:3306 salix-db`); if (runChown) await dockerWait(); } docker.description = `Builds the database image and runs a container`; /** * Does the minium effort to start the database container, if it doesn't exists * calls the 'docker' task, if it is started does nothing. Keep in mind that when * you do not rebuild the docker you may be using an outdated version of it. * See the 'docker' task for more info. */ async function dockerStart() { let state; try { let result = await execP('docker container inspect -f "{{json .State}}" salix-db'); state = JSON.parse(result.stdout); } catch (err) { return await docker(); } switch (state.Status) { case 'running': return; case 'exited': await execP('docker start salix-db'); await dockerWait(); return; default: throw new Error(`Unknown docker status: ${state.Status}`); } } dockerStart.description = `Starts the database container`; function dockerWait() { return new Promise((resolve, reject) => { const mysql = require('mysql2'); let interval = 100; let elapsedTime = 0; let maxInterval = 5 * 60 * 1000; log('Waiting for MySQL init process...'); checker(); async function checker() { elapsedTime += interval; let state; try { let result = await execP('docker container inspect -f "{{json .State}}" salix-db'); state = JSON.parse(result.stdout); } catch (err) { return reject(new Error(err.message)); } if (state.Status === 'exited') return reject(new Error('Docker exited, please see the docker logs for more info')); let conn = mysql.createConnection({ host: 'localhost', user: 'root', password: 'root' }); conn.on('error', () => {}); conn.connect(err => { conn.destroy(); if (!err) return resolve(); if (elapsedTime >= maxInterval) reject(new Error(`MySQL not initialized whithin ${elapsedTime} secs`)); else setTimeout(checker, interval); }); } }); } dockerWait.description = `Waits until database service is ready`; // Helpers /** * Promisified version of exec(). * * @param {String} command The exec command * @return {Promise} The promise */ function execP(command) { return new Promise((resolve, reject) => { exec(command, (err, stdout, stderr) => { if (err) reject(err); else { resolve({ stdout: stdout, stderr: stderr }); } }); }); } module.exports = { default: defaultTask, front, back, backOnly, backWatch, backendUnitTest, dockerAndBackTest, backTest, e2eOnly, e2e, smokesOnly, smokes, install, i, build, clean, webpack, webpackDevServer, locales, routes, localesRoutes, watch, docker, dockerStart, dockerWait };