require('require-yaml'); const gulp = require('gulp'); const fs = require('fs-extra'); 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 env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; let langs = ['es', 'en']; let srcDir = './front'; let modulesDir = './modules'; let servicesDir = './services'; let modules = require('./modules.yml'); let wpConfig = require('./webpack.config.yml'); let buildDir = wpConfig.buildDir; let devServerPort = wpConfig.devServerPort; let nginxDir = `${servicesDir}/nginx`; let proxyConf = require(`${nginxDir}/config.yml`); let proxyEnvFile = `${nginxDir}/config.${env}.yml`; if (fs.existsSync(proxyEnvFile)) Object.assign(proxyConf, require(proxyEnvFile)); let defaultService = proxyConf.main; let defaultPort = proxyConf.defaultPort; // Development const nginx = gulp.series(nginxStart); nginx.description = `Starts/restarts the nginx process`; const localesRoutes = gulp.parallel(locales, routes); localesRoutes.description = `Builds locales and routes`; const front = gulp.series(buildClean, gulp.parallel(localesRoutes, watch, webpackDevServer)); front.description = `Starts frontend service`; const back = gulp.series(dockerStart, backOnly, nginx); back.description = `Starts backend and database service`; const defaultTask = gulp.parallel(front, back); defaultTask.description = `Starts all application services`; function backOnly(done) { let app = require(`./loopback/server/server`); app.start(defaultPort); app.on('started', done); } backOnly.description = `Starts backend service`; function backTestsOnly() { serviceRoot = 'vn-loopback'; let app = require(`./loopback/server/server`); let specFiles = [ `./back/**/*.spec.js`, `./loopback/**/*.spec.js` ]; for (let mod of modules) specFiles.push(`./modules/${mod}/back/**/*.spec.js`); const jasmine = require('gulp-jasmine'); return gulp.src(specFiles) .pipe(jasmine({errorOnFail: false})) .on('jasmineDone', function() { app.disconnect(); }); } backTestsOnly.description = `Runs the backend tests only`; const backTests = gulp.series(docker, backTestsOnly); backTests.description = `Restarts database and runs the backend 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`; const clean = gulp.parallel(buildClean, nginxClean); clean.description = 'Cleans all generated project files'; function install() { const install = require('gulp-install'); const print = require('gulp-print'); let packageFiles = ['front/package.json']; let services = fs.readdirSync(servicesDir); services.forEach(service => { let packageJson = `${servicesDir}/${service}/package.json`; if (fs.existsSync(packageJson)) packageFiles.push(packageJson); }); 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, nginxConf)); build.description = `Generates binaries and configuration files`; function buildClean() { const del = require('del'); const files = [ `${buildDir}/*` ]; return del(files, {force: true}); } buildClean.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]; let wdsAssets = [`webpack-dev-server/client?http://127.0.0.1:${devServer.port}/`]; if (Array.isArray(entry)) wdsAssets = wdsAssets.concat(entry); else wdsAssets.push(entry); wpConfig.entry[entryName] = wdsAssets; } 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'); let streams = []; let localePaths = []; for (let mod of modules) localePaths[mod] = `${modulesDir}/${mod}`; let baseMods = ['core', 'auth', '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'; 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 = 30 * 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`; // Nginx let nginxConfFile = 'temp/nginx.conf'; let nginxTemp = `${nginxDir}/temp`; async function nginxStart() { await nginxConf(); let nginxBin = await nginxGetBin(); if (isWindows) nginxBin = `start /B ${nginxBin}`; log(`Application available at http://${proxyConf.host}:${proxyConf.port}/`); await execP(`${nginxBin} -c "${nginxConfFile}" -p "${nginxDir}"`); } nginxStart.description = `Starts the nginx process`; async function nginxStop() { try { let nginxBin = await nginxGetBin(); await fs.stat(`${nginxTemp}/nginx.pid`); await execP(`${nginxBin} -c "${nginxConfFile}" -p "${nginxDir}" -s stop`); } catch (e) {} } nginxStop.description = `Stops the nginx process`; /** * Generates the nginx configuration file. If NODE_ENV is defined and the * 'nginx.[environment].mst' file exists, it is used as a template, otherwise, * the 'nginx.mst' template file is used. */ async function nginxConf() { await nginxStop(); const mustache = require('mustache'); if (!await fs.exists(nginxTemp)) await fs.mkdir(nginxTemp); let params = { services: modules, defaultService: defaultService, defaultPort: defaultPort, devServerPort: devServerPort, port: proxyConf.port, host: proxyConf.host }; let confFile = `${nginxDir}/nginx.${env}.mst`; if (!await fs.exists(confFile)) confFile = `${nginxDir}/nginx.mst`; let template = await fs.readFile(confFile, 'utf8'); let nginxConfData = mustache.render(template, params); await fs.writeFile(`${nginxTemp}/nginx.conf`, nginxConfData); } nginxConf.description = `Generates the nginx configuration file`; async function nginxClean() { await nginxStop(); const del = require('del'); return del([`${nginxTemp}/*`], {force: true}); } nginxClean.description = `Cleans all files generated by nginx`; async function nginxGetBin() { if (isWindows) return 'nginx'; try { let nginxBin = '/usr/sbin/nginx'; await fs.stat(nginxBin); return nginxBin; } catch (e) { return 'nginx'; } } // 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, backTestsOnly, backTests, e2eOnly, e2e, smokesOnly, smokes, clean, install, i, build, buildClean, nginxStart, nginx, nginxStop, nginxConf, nginxClean, webpack, webpackDevServer, locales, routes, localesRoutes, watch, docker, dockerStart, dockerWait };