require('require-yaml'); const gulp = require('gulp'); const PluginError = require('plugin-error'); const argv = require('minimist')(process.argv.slice(2)); const log = require('fancy-log'); const request = require('request'); const e2eConfig = require('./e2e/helpers/config.js'); const Docker = require('./db/docker.js'); // 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/**', '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 commands = ['node --tls-min-v1.0 --inspect ./node_modules/gulp/bin/gulp.js']; if (!isWindows) commands.unshift('sleep 1'); nodemon({ exec: commands.join(' && '), ext: 'js html css json', args: ['backOnly'], watch: backSources, done: done }); } backWatch.description = `Starts backend in watcher 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 - Private method async function launchBackTest(done) { let err; let dataSources = require('./loopback/server/datasources.json'); const container = new Docker(); await container.run(argv.ci); dataSources = JSON.parse(JSON.stringify(dataSources)); Object.assign(dataSources.vn, { host: container.dbConf.host, port: container.dbConf.port }); let bootOptions = {dataSources}; let app = require(`./loopback/server/server`); try { app.boot(bootOptions); await new Promise((resolve, reject) => { const jasmine = require('gulp-jasmine'); let options = { errorOnFail: false, config: {} }; if (argv.ci) { const reporters = require('jasmine-reporters'); options.reporter = new reporters.JUnitXmlReporter(); } let backSpecFiles = [ 'back/**/*.spec.js', 'loopback/**/*.spec.js', 'modules/*/back/**/*.spec.js' ]; gulp.src(backSpecFiles) .pipe(jasmine(options)) .on('end', resolve) .on('error', reject) .resume(); }); } catch (e) { err = e; } await app.disconnect(); await container.rm(); done(); if (err) throw err; } launchBackTest.description = `Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`; // Backend tests function backTest(done) { const nodemon = require('gulp-nodemon'); nodemon({ exec: ['node --tls-min-v1.0 ./node_modules/gulp/bin/gulp.js'], args: ['launchBackTest'], watch: backSources, done: done }); } backTest.description = `Watches for changes in modules to execute backTest task`; // End to end tests function e2eSingleRun() { require('@babel/register')({presets: ['@babel/preset-env']}); require('@babel/polyfill'); const jasmine = require('gulp-jasmine'); const SpecReporter = require('jasmine-spec-reporter').SpecReporter; if (argv.show || argv.s) process.env.E2E_SHOW = true; const specFiles = [ `${__dirname}/e2e/paths/01*/*[sS]pec.js`, `${__dirname}/e2e/paths/02*/*[sS]pec.js`, `${__dirname}/e2e/paths/03*/*[sS]pec.js`, `${__dirname}/e2e/paths/04*/*[sS]pec.js`, `${__dirname}/e2e/paths/05*/*[sS]pec.js`, `${__dirname}/e2e/paths/06*/*[sS]pec.js`, `${__dirname}/e2e/paths/07*/*[sS]pec.js`, `${__dirname}/e2e/paths/08*/*[sS]pec.js`, `${__dirname}/e2e/paths/09*/*[sS]pec.js`, `${__dirname}/e2e/paths/10*/*[sS]pec.js`, `${__dirname}/e2e/paths/11*/*[sS]pec.js`, `${__dirname}/e2e/paths/12*/*[sS]pec.js`, `${__dirname}/e2e/paths/13*/*[sS]pec.js`, `${__dirname}/e2e/paths/**/*[sS]pec.js` ]; return gulp.src(specFiles).pipe(jasmine({ errorOnFail: false, timeout: 30000, config: { random: false, // TODO: Waiting for this option to be implemented // https://github.com/jasmine/jasmine/issues/1533 stopSpecOnExpectationFailure: false }, reporter: [ new SpecReporter({ spec: { displayStacktrace: 'none', displaySuccessful: true, displayFailedSpec: true, displaySpecDuration: true, }, summary: { displayStacktrace: 'pretty', displayPending: false } }) ] })); } e2e = gulp.series(docker, async function isBackendReady() { const attempts = await backendStatus(); log(`Backend ready after ${attempts} attempt(s)`); return attempts; }, e2eSingleRun); e2e.description = `Restarts database and runs the e2e tests`; async function backendStatus() { const milliseconds = 250; return new Promise(resolve => { let timer; let attempts = 1; timer = setInterval(() => { const url = `${e2eConfig.url}/api/Applications/status`; request.get(url, (err, res) => { if (err || attempts > 100) // 250ms * 100 => 25s timeout throw new Error('Could not connect to backend'); else if (res && res.body == 'true') { clearInterval(timer); resolve(attempts); } else attempts++; }); }, milliseconds); }); } backendStatus.description = `Performs a simple requests to check the backend status`; function install() { const install = require('gulp-install'); const print = require('gulp-print'); let npmArgs = []; if (argv.ci) npmArgs = ['--no-audit', '--prefer-offline']; let packageFiles = ['front/package.json', 'print/package.json']; return gulp.src(packageFiles) .pipe(print(filepath => { return `Installing packages in ${filepath}`; })) .pipe(install({npm: npmArgs})); } 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); // XXX: 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 gulpFile = require('gulp-file'); 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}`))); } } for (let mod in localePaths) { for (let lang of langs) { let file = `${buildDir}/locale/${mod}/${lang}.json`; if (fs.existsSync(file)) continue; streams.push(gulpFile('en.json', '{}', {src: true}) .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 async function dockerStart() { const container = new Docker('salix-db'); await container.start(); } dockerStart.description = `Starts the salix-db container`; async function docker() { const container = new Docker('salix-db'); await container.run(); } docker.description = `Runs the salix-db container`; module.exports = { default: defaultTask, front, back, backOnly, backWatch, backTest, launchBackTest, e2e, i, install, build, clean, webpack, webpackDevServer, routes, locales, localesRoutes, watch, docker, backendStatus, };