const fs = require('fs'); const chokidar = require('chokidar'); const express = require('express'); const removeRoute = require('remove-route-runtime'); const portfinder = require('portfinder'); const program = require('commander'); require('colors'); program.version('1.0.0'); program.option('-p, --port ', 'change the port used', 8000); program.option('-v, --verbose', 'displays more information', false); program.parse(process.argv); const app = express(); app.use(express.json()); const wrapperPath = './wrapper.json'; function isdir(path) { return fs.statSync(path).isDirectory(); } function scandir(dirname, exclude = []) { dirname += (dirname[dirname.length - 1] === '/') ? '' : '/'; const result = []; const items = fs.readdirSync(dirname) items.forEach(elt => { if (exclude.indexOf(elt) !== -1) { return; } const file = dirname + elt; if (isdir(file)) { const res = scandir(file, exclude); res.forEach(e => { result.push(e); }) } else { result.push(file); } }); return result; } function parseFilename(filename) { let data = null; if (fs.existsSync('./' + filename)) { data = fs.readFileSync('./' + filename).toString(); } const split = filename.split('/'); split.shift(); const method = split.shift(); const path = '/' + split.join('/').replace('.json', '').replace(/param-/g, ':').replace(/-optional/g, '?').replace(/\./g, '/'); return {data: data, method: method, path: path}; } function verbose(req, res, next) { if (program.verbose) { const body = Object.keys(req.body); const param = Object.keys(req.params); let nbParam = 0; for (let prop in req.params) { if (req.params[prop] !== undefined) { nbParam++; } } const total = body.length + nbParam console.info('*'.green, '>'.cyan, `Call ${req.method} ${req.originalUrl} with ${total} parameter(s) (${nbParam} params, ${body.length} body)`.bold); if (nbParam > 0) { for (let prop in req.params) { if (req.params[prop] !== undefined) { console.info('\t-'.cyan, `${prop}: ${req.params[prop]} (Param)`); } } } if (body.length > 0) { for (let prop in req.body) { console.info('\t-'.cyan, `${prop}: ${req.body[prop]} (Body)`); } } } next(); } function extractParam(req) { const res = {}; const body = Object.keys(req.body); const param = Object.keys(req.params); if (param.length > 0) { for (let prop in req.params) { if (req.params[prop] !== undefined) { res[prop] = req.params[prop]; } } } if (body.length > 0) { for (let prop in req.body) { res[prop] = req.body[prop]; } } return res; } function applyParam(data, param) { let result = data; const key = Object.keys(param); key.forEach(elt => { result = result.replace(new RegExp('\\$\\{' + elt + '\\}', 'g'), param[elt]); }); return result; } function answer(req, res, data, wrapper = null) { const param = extractParam(req); let json = applyParam(data, param); try { if (json.trim() === '') { if (wrapper !== null) { json = wrapper.replace(new RegExp('\\$\\{data\\}', 'g'), 'null'); } else { json = '{}'; } } else if (wrapper !== null) { json = wrapper.replace(new RegExp('\\$\\{data\\}', 'g'), json); } return res.json(JSON.parse(json)); } catch (error) { console.error('Unable to answer'.red); return res.json({error: 'Unable to answer'}); } } function addRoute(app, method, route, data, wrapper = null) { switch (method) { case "GET": app.get(route, [verbose, (req, res) => { return answer(req, res, data, wrapper); }]); break; case "POST": app.post(route, [verbose, (req, res) => { return answer(req, res, data, wrapper); }]);; break; case "PUT": app.put(route, [verbose, (req, res) => { return answer(req, res, data, wrapper); }]); break; case "DELETE": app.delete(route, [verbose, (req, res) => { return answer(req, res, data, wrapper); }]); break; default: app.all(route, [verbose, (req, res) => { return answer(req, res, data, wrapper); }]);; } } // Recherche des fichiers console.info('*'.bold.green, 'Read mock files'.bold); const files = scandir('server', ['.gitkeep']); if (files.length === 0) { console.info('*'.bold.green, 'No mock files found'.bold.yellow); } // Création du serveur console.info('*'.bold.green, 'Creating mock server'.bold); // Chargement encapsulation let wrapper = null; if (fs.existsSync(wrapperPath)) { if (program.verbose) { console.info('*'.green, '>'.yellow, 'Load wrapper'.bold); } wrapper = fs.readFileSync(wrapperPath).toString(); } // Ajout des routes pour chaque fichier files.forEach(elt => { const parse = parseFilename(elt); if (program.verbose) { console.info('*'.green, '>'.yellow, `Load URL: ${parse.method} ${parse.path}`.bold) } addRoute(app, parse.method, parse.path, parse.data, wrapper); }); // Ajout page de test erreur if (fs.existsSync('./error.json')) { if (program.verbose) { console.info('*'.green, '>'.yellow, `Load URL: ANY /error`.bold) } addRoute(app, 'ANY', '/error', fs.readFileSync('./error.json').toString()); } // Ajout watcher console.info('*'.green, 'Watching changes...'.bold); const watcher = chokidar.watch(['./server'], { ignored: /(^|[\/\\])\../, persistent: true }); watcher.on('add', path => { // Remplace les \ du chemin Windows par des / path = path.replace(/\\/g, '/'); // Skip la 1er detection const index = files.indexOf(path); if (index !== -1) { files.splice(index, 1); return; } // Ajoute les nouveaux chemins const parse = parseFilename(path); if (program.verbose) { console.info('*'.green, '>'.yellow, `Load URL: ${parse.method} ${parse.path}`.bold) } addRoute(app, parse.method, parse.path, parse.data, wrapper); }); watcher.on('change', path => { const method = ['GET', 'POST', 'PUT', 'DELETE']; const parse = parseFilename(path.replace(/\\/g, '/')); // Supprime l'ancienne route avec les anciennes données if (method.indexOf(parse.method) === -1) { removeRoute(app, parse.path); } else { removeRoute(app, parse.path, parse.method.toLowerCase()); } // Remet la route avec les nouvelles données if (program.verbose) { console.info('*'.green, '>'.yellow, `Reload URL: ${parse.method} ${parse.path}`.bold) } addRoute(app, parse.method, parse.path, parse.data, wrapper); }); watcher.on('unlink', path => { const method = ['GET', 'POST', 'PUT', 'DELETE']; const parse = parseFilename(path.replace(/\\/g, '/')); if (program.verbose) { console.info('*'.green, '>'.yellow, `Unload URL: ${parse.method} ${parse.path}`.bold) } if (method.indexOf(parse.method) === -1) { removeRoute(app, parse.path); } else { removeRoute(app, parse.path, parse.method.toLowerCase()); } }); // Lancement du serveur portfinder.getPort({port: program.port}, (err, freePort) => { if (err) { console.error('*'.bold.green, 'Unable to find a free port'.bold.red); process.exit(-1); } if (program.port != freePort) { console.info('*'.bold.green, `Port ${program.port} is unavailable`.bold.yellow); } app.listen(freePort, () => { console.info('*'.bold.green, 'Mock server stating on'.bold, `http://localhost:${freePort}`.bold.blue); }); });