#!/usr/bin/env node 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const Discover = require('node-discover'); const yargs = require('yargs'); const path = require('path'); const promises = require('fs/promises'); const helpers = require('yargs/helpers'); const ansiColors = require('ansi-colors'); const h3 = require('h3'); const listhen = require('listhen'); const mergician = require('mergician'); const httpProxyMiddleware = require('http-proxy-middleware'); const serveStatic$1 = require('serve-static'); const morgan$1 = require('morgan'); const fs = require('fs'); const replacer = require('@jota-one/replacer'); const rrdir = require('rrdir'); const uuid = require('uuid'); const module$1 = require('module'); const Loki = require('lokijs'); const _vorpal = require('@moleculer/vorpal'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; } const Discover__default = /*#__PURE__*/_interopDefaultLegacy(Discover); const yargs__default = /*#__PURE__*/_interopDefaultLegacy(yargs); const ansiColors__default = /*#__PURE__*/_interopDefaultLegacy(ansiColors); const mergician__default = /*#__PURE__*/_interopDefaultLegacy(mergician); const serveStatic__default = /*#__PURE__*/_interopDefaultLegacy(serveStatic$1); const morgan__default = /*#__PURE__*/_interopDefaultLegacy(morgan$1); const replacer__default = /*#__PURE__*/_interopDefaultLegacy(replacer); const Loki__default = /*#__PURE__*/_interopDefaultLegacy(Loki); const _vorpal__default = /*#__PURE__*/_interopDefaultLegacy(_vorpal); function cleanIndexInpath(pathPart) { return pathPart.startsWith("[") ? pathPart.replace(/\D/g, "") : pathPart; } function getObjectPaths(paths) { return [].concat(paths).map( (path) => path.split(".").map(cleanIndexInpath).join(".") ); } function curry(fn) { return function(...args) { return fn.bind(null, ...args); }; } function isEmpty(obj) { if (!obj) { return true; } return !Object.keys(obj).length; } function get(obj, path, defaultValue) { const result = path.split(".").reduce((r, p) => { if (typeof r === "object" && r !== null) { p = cleanIndexInpath(p); return r[p]; } return void 0; }, obj); return result === void 0 ? defaultValue : result; } function omit(obj, paths) { if (obj === null) { return null; } if (!obj) { return obj; } if (typeof obj !== "object") { return obj; } const buildObj = (obj2, paths2, _path = "", _result) => { const result = !_result ? Array.isArray(obj2) ? [] : {} : _result; for (const [key, value] of Object.entries(obj2)) { if (paths2.includes(key)) { continue; } if (typeof value === "object" && value !== null) { result[key] = buildObj( value, paths2, `${_path}${key}.`, Array.isArray(value) ? [] : {} ); } else { result[key] = value; } } return result; }; return buildObj(obj, getObjectPaths(paths)); } function pick(obj, paths) { const result = {}; for (const path of getObjectPaths(paths)) { const value = get(obj, path); if (value) { set(result, path, value); } } return result; } function set(obj, path, val) { path.split && (path = path.split(".")); let i = 0; const l = path.length; let t = obj; let x; let k; while (i < l) { k = path[i++]; if (k === "__proto__" || k === "constructor" || k === "prototype") break; t = t[k] = i === l ? val : typeof (x = t[k]) === typeof path && t[k] !== null ? x : path[i] * 0 !== 0 || !!~("" + path[i]).indexOf(".") ? {} : []; } } function cloneDeep(obj) { return mergician__default({}, obj); } function merge(obj1, obj2) { return mergician__default(obj1, obj2); } const RESTART_DISABLED_IN_ESM_MODE = "Restart is not supported in esm mode."; const config = { db: { reservedFields: ["DROSSE", "meta", "$loki"] }, state: { assetsPath: "assets", baseUrl: "", basePath: "", collectionsPath: "collections", database: "mocks.json", dbAdapter: "LokiFsAdapter", name: "Drosse mock server", port: 8e3, reservedRoutes: { ui: "/UI", cmd: "/CMD" }, routesFile: "routes", scrapedPath: "scraped", scraperServicesPath: "scrapers", servicesPath: "services", shallowCollections: [], staticPath: "static", uuid: "" }, cli: null, commands: {}, errorHandler: null, extendServer: null, middlewares: ["morgan"], templates: {}, onHttpUpgrade: null }; function Logger() { this.debug = function(...args) { log("white", args); }; this.success = function(...args) { log("green", args); }; this.info = function(...args) { log("cyan", args); }; this.warn = function(...args) { log("yellow", args); }; this.error = function(...args) { log("red", args); }; } function getTime() { return new Date().toLocaleTimeString(); } function log(color, args) { args = args.map((arg) => { if (typeof arg === "object") { return JSON.stringify(arg); } return arg; }); console.log(ansiColors__default.gray(getTime()), ansiColors__default[color](args.join(" "))); } const logger = new Logger(); const rewriteLinks$1 = (links) => Object.entries(links).reduce((links2, [key, link]) => { let { href } = link; if (href.indexOf("http") === 0) { href = `/${href.split("/").slice(3).join("/")}`; } links2[key] = { ...link, href }; return links2; }, {}); const walkObj$1 = (root = {}, prefix = "") => { if (typeof root !== "object") { return root; } const obj = (prefix ? get(root, prefix) : root) || {}; Object.entries(obj).forEach(([key, value]) => { const path = `${prefix && prefix + "."}${key}`; if (key === "_links") { set(root, path, rewriteLinks$1(value)); } walkObj$1(root, path); }); }; function halLinks(body) { walkObj$1(body); return body; } const rewriteLinks = (links) => links.map((link) => { let { href } = link; if (href.indexOf("http") === 0) { href = `/${href.split("/").slice(3).join("/")}`; } return { ...link, href }; }); const walkObj = (root = {}, prefix = "") => { if (typeof root !== "object") { return root; } const obj = (prefix ? get(root, prefix) : root) || {}; Object.entries(obj).forEach(([key, value]) => { const path = `${prefix && prefix + "."}${key}`; if (key === "links") { const linkKeys = Object.keys(value[0] || []); if (linkKeys.includes("href") && linkKeys.includes("rel")) { set(root, path, rewriteLinks(value)); } else { walkObj(root, path); } } walkObj(root, path); }); }; function hateoasLinks(body) { walkObj(body); return body; } let state$9 = JSON.parse(JSON.stringify(config.state)); function useState() { return { set(key, value) { state$9[key] = value; }, get(key) { if (key) { return state$9[key]; } return state$9; }, merge(conf) { const keysWhitelist = Object.keys(state$9); state$9 = merge(state$9, pick(conf, keysWhitelist)); } }; } const state$8 = useState(); morgan$1.token("time", function getTime() { return ansiColors__default.gray(new Date().toLocaleTimeString()); }); morgan$1.token("status", function(req, res) { const col = color(res.statusCode); return ansiColors__default[col](res.statusCode); }); morgan$1.token("method", function(req, res) { const verb = req.method.padEnd(7); const col = color(res.statusCode); return ansiColors__default[col](verb); }); morgan$1.token("url", function(req, res) { const url = req.originalUrl || req.url; return h3.getResponseHeader(res, "x-proxied") ? ansiColors__default.cyan(url) : ansiColors__default[color(res.statusCode)](url); }); morgan$1.token("proxied", function(req, res) { return h3.getResponseHeader(res, "x-proxied") ? ansiColors__default.cyanBright("\u{1F500} proxied") : ""; }); const color = (status) => { if (status >= 400) { return "red"; } if (status >= 300) { return "yellow"; } return "green"; }; const format = function(tokens, req, res) { return [ tokens.time(req, res), tokens.method(req, res), tokens.status(req, res), "-", tokens["response-time"](req, res, 0) ? tokens["response-time"](req, res, 0).concat("ms").padEnd(7) : "\u{1F6AB}", tokens.url(req, res), tokens.proxied(req, res) ].join(" "); }; const morgan = morgan__default(format, { skip: (req) => Object.values(state$8.get("reservedRoutes")).includes(req.url) }); function openCors(req, res) { h3.setResponseHeaders(res, { "Access-Control-Allow-Credentials": true, ...h3.getRequestHeader(req, "origin") ? { "Access-Control-Allow-Origin": h3.getRequestHeader(req, "origin") } : {}, "Access-Control-Allow-Methods": "GET, PUT, POST, PATCH, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Length, X-Requested-With" }); if (req.method === "OPTIONS") { res.statusCode = 200; } } const internalMiddlewares = { "hal-links": halLinks, "hateoas-links": hateoasLinks, morgan, "open-cors": openCors }; let cjsRequire; const esmMode = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)).endsWith(".mjs") || process.argv[1].endsWith(".mjs"); const getFullPath = (path$1) => { const state = useState(); const root = state.get("root") || __dirname; return path.resolve(root, path$1); }; const load$1 = async function(path) { let module; const fullPath = getFullPath(path); if (esmMode) { module = (await import(fullPath)).default; } else { if (!cjsRequire) { cjsRequire = module$1.createRequire((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))); } delete require.cache[fullPath]; module = require(fullPath); } return module; }; const isEsmMode$1 = function() { return esmMode; }; function useIO$1() { return { isEsmMode: isEsmMode$1, load: load$1 }; } const state$7 = useState(); const { load } = useIO$1(); const fileExists = async (path) => { let exists; try { await fs.promises.access(path); exists = true; } catch { exists = false; } return exists; }; const checkRoutesFile$1 = async () => { const filePath = path.join( state$7.get("root"), `${state$7.get("routesFile")}.json` ); if (await fileExists(filePath)) { state$7.set("_routesFile", filePath); return true; } return false; }; const getUserConfig$1 = async (root) => { const rcFilePath = path.join(root || state$7.get("root") || "", ".drosserc.js"); try { await fs.promises.stat(rcFilePath); return load(rcFilePath); } catch (e) { console.error("Could not load any user config."); console.error(e); return {}; } }; const loadService$1 = async (routePath, verb) => { const serviceFile = path.join( state$7.get("root"), state$7.get("servicesPath"), routePath.filter((el) => el[0] !== ":").join(".") ) + `.${verb}.js`; if (!await fileExists(serviceFile)) { return function() { logger.error(`service [${serviceFile}] not found`); }; } const service = await load(serviceFile); return { serviceFile, service }; }; const loadScraperService$1 = async (routePath) => { const serviceFile = path.join( state$7.get("root"), state$7.get("scraperServicesPath"), routePath.filter((el) => el[0] !== ":").join(".") ) + ".js"; if (!await fileExists(serviceFile)) { return function() { logger.error(`scraper service [${serviceFile}] not found`); }; } return load(serviceFile); }; const writeScrapedFile = async (filename, content) => { const root = path.join(state$7.get("root"), state$7.get("scrapedPath")); await fs.promises.writeFile( path.join(root, filename), JSON.stringify(content), "utf-8" ); return true; }; const loadStatic$2 = async ({ routePath, params = {}, query = {}, verb = null, skipVerb = false, extensions = ["json"] }) => { const root = path.join(state$7.get("root"), state$7.get("staticPath")); const files = await rrdir.async(root); return findStatic({ root, files, routePath, params, verb, skipVerb, query, extensions }); }; const loadScraped$2 = async ({ routePath, params = {}, query = {}, verb = null, skipVerb = false, extensions = ["json"] }) => { const root = path.join(state$7.get("root"), state$7.get("scrapedPath")); const files = await rrdir.async(root); return findStatic({ root, files, extensions, routePath, params, verb, skipVerb, query }); }; const loadUuid$1 = async () => { const uuidFile = path.join(state$7.get("root"), ".uuid"); if (!await fileExists(uuidFile)) { await fs.promises.writeFile(uuidFile, uuid.v4(), "utf8"); } const uuid$1 = await fs.promises.readFile(uuidFile, "utf8"); state$7.merge({ uuid: uuid$1 }); }; const getRoutesDef$1 = async () => { const content = await fs.promises.readFile(state$7.get("_routesFile"), "utf8"); return JSON.parse(content); }; const getStaticFileName = (routePath, extension, params = {}, verb = null, query = {}) => { const queryPart = Object.entries(query).sort(([name1], [name2]) => { return name1 > name2 ? 1 : -1; }).reduce((acc, [name, value]) => { return acc.concat(`${name}=${value}`); }, []).join("&"); let filename = replacer__default.replace( routePath.join(".").concat(verb ? `.${verb.toLowerCase()}` : "").replace(/:([^\\/\\.]+)/gim, "{$1}").concat(queryPart ? `&&${queryPart}` : ""), params ); const extensionLength = extension.length; return filename.concat(filename.slice(-(extensionLength + 1)) === `.${extension}` ? "" : `.${extension}`); }; const findStatic = async ({ root, files, routePath, extensions, params = {}, verb = null, skipVerb = false, query = {}, initial = null, extensionIndex = 0 }) => { const normalizedPath = (filePath) => filePath.replace(root, "").substring(1); if (initial === null) { initial = cloneDeep({ params, query, verb, skipVerb }); } const filename = getStaticFileName( routePath, extensions[extensionIndex], params, !skipVerb && verb, query ); let staticFile = path.join(root, filename); const foundFiles = files.filter( (file) => normalizedPath(file.path).replace(/\//gim, ".") === normalizedPath(staticFile) ); if (foundFiles.length > 1) { const error = `findStatic: more than 1 file found for: [${staticFile}]: ${foundFiles.map((f) => f.path).join("\n")}`; throw new Error(error); } if (foundFiles.length === 0) { if (!isEmpty(query)) { logger.error(`findStatic: tried with [${staticFile}]. File not found.`); return findStatic({ root, files, routePath, params, verb, skipVerb, extensions, initial, extensionIndex }); } if (verb && !skipVerb) { logger.error(`findStatic: tried with [${staticFile}]. File not found.`); return findStatic({ root, files, routePath, params, verb, skipVerb: true, query, extensions, initial, extensionIndex }); } if (!isEmpty(params)) { logger.error(`findStatic: tried with [${staticFile}]. File not found.`); const newParams = Object.keys(params).slice(1).reduce((acc, name) => ({ ...acc, [name]: params[name] }), {}); return findStatic({ root, files, routePath, params: newParams, verb, extensions, initial, extensionIndex }); } if (extensionIndex === extensions.length - 1) { logger.error(`findStatic: I think I've tried everything. No match...`); return [false, false]; } else { logger.warn(`findStatic: Okay, tried everything with ${extensions[extensionIndex]} extension. Let's try with the next one.`); return findStatic({ root, files, routePath, params: cloneDeep(initial.params), query: cloneDeep(initial.query), verb: initial.verb, skipVerb: initial.skipVerb, extensions, initial, extensionIndex: extensionIndex + 1 }); } } else { const foundExtension = extensions[extensionIndex]; staticFile = foundFiles[0].path; logger.info(`findStatic: file used: ${staticFile}`); if (foundExtension === "json") { const fileContent = await fs.promises.readFile(staticFile, "utf-8"); const result = replacer__default.replace(fileContent, initial.params); return [JSON.parse(result), foundExtension]; } return [staticFile, foundExtension]; } }; function useIO() { return { checkRoutesFile: checkRoutesFile$1, getRoutesDef: getRoutesDef$1, getStaticFileName, getUserConfig: getUserConfig$1, loadService: loadService$1, loadScraperService: loadScraperService$1, loadStatic: loadStatic$2, loadScraped: loadScraped$2, loadUuid: loadUuid$1, writeScrapedFile }; } let db$2; function useDB() { const state = useState(); const collectionsPath = () => path.join(state.get("root"), state.get("collectionsPath")); const normalizedPath = (filePath) => filePath.replace(collectionsPath(), "").substr(1); const loadAllMockFiles = async () => { const res = await rrdir.async(collectionsPath()); return res.filter((entry) => !entry.directory && entry.path.endsWith("json")).map((entry) => { entry.path = normalizedPath(entry.path); return entry; }); }; const handleCollection = (name, alreadyHandled, newCollections) => { const shallowCollections = state.get("shallowCollections"); const coll = db$2.getCollection(name); if (coll) { if (newCollections.includes(name)) { return coll; } if (!shallowCollections.includes(name)) { if (alreadyHandled.includes(name)) { return false; } logger.warn( "\u{1F4E6} collection", name, "already exists and won't be overriden." ); alreadyHandled.push(name); return false; } if (alreadyHandled.includes(name)) { return coll; } logger.warn( "\u{1F30A} collection", name, "already exists and will be overriden." ); db$2.removeCollection(name); alreadyHandled.push(name); } newCollections.push(name); return db$2.addCollection(name); }; const loadContents = async () => { const files = await loadAllMockFiles(); const handled = []; const newCollections = []; for (const entry of files) { const filename = entry.path.split(path.sep).pop(); const fileContent = await fs.promises.readFile( path.join(collectionsPath(), entry.path), "utf-8" ); const content = JSON.parse(fileContent); const collectionName = Array.isArray(content) ? entry.path.slice(0, -5).split(path.sep).join(".") : entry.path.split(path.sep).slice(0, -1).join("."); const coll = handleCollection(collectionName, handled, newCollections); if (coll) { coll.insert(content); logger.success(`loaded ${filename} into collection ${collectionName}`); } } }; const clean = (...fields) => (result) => omit(result, config.db.reservedFields.concat(fields || [])); const service = { loadDb() { return new Promise((resolve, reject) => { try { const AdapterName = state.get("dbAdapter"); const adapter = Loki__default[AdapterName] ? new Loki__default[AdapterName]() : new AdapterName(); db$2 = new Loki__default(path.join(state.get("root"), state.get("database")), { adapter, autosave: true, autosaveInterval: 4e3, autoload: true, autoloadCallback: () => { loadContents().then(() => resolve(db$2)); } }); } catch (e) { reject(e); } }); }, loki: function() { return db$2; }, collection: function(name) { let coll = db$2.getCollection(name); if (!coll) { coll = db$2.addCollection(name); } return coll; }, list: { all(collection, cleanFields = []) { const coll = service.collection(collection); return coll.data.map(clean(...cleanFields)); }, byId(collection, id, cleanFields = []) { const coll = service.collection(collection); return coll.find({ "DROSSE.ids": { $contains: id } }).map(clean(...cleanFields)); }, byField(collection, field, value, cleanFields = []) { return this.byFields(collection, [field], value, cleanFields); }, byFields(collection, fields, value, cleanFields = []) { return this.find( collection, { $or: fields.map((field) => ({ [field]: { $contains: value } })) }, cleanFields ); }, find(collection, query, cleanFields = []) { const coll = service.collection(collection); return coll.chain().find(query).data().map(clean(...cleanFields)); }, where(collection, searchFn, cleanFields = []) { const coll = service.collection(collection); return coll.chain().where(searchFn).data().map(clean(...cleanFields)); } }, get: { byId(collection, id, cleanFields = []) { const coll = service.collection(collection); return clean(...cleanFields)( coll.findOne({ "DROSSE.ids": { $contains: id } }) ); }, byRef(refObj, dynamicId, cleanFields = []) { const { collection, id: refId } = refObj; const id = dynamicId || refId; return { ...this.byId(collection, id, cleanFields), ...omit(refObj, ["collection", "id"]) }; }, byField(collection, field, value, cleanFields = []) { return this.byFields(collection, [field], value, cleanFields); }, byFields(collection, fields, value, cleanFields = []) { return this.find( collection, { $or: fields.map((field) => ({ [field]: { $contains: value } })) }, cleanFields ); }, find(collection, query, cleanFields = []) { const coll = service.collection(collection); return clean(...cleanFields)(coll.findOne(query)); }, where(collection, searchFn, cleanFields = []) { const result = service.list.where(collection, searchFn, cleanFields); if (result.length > 0) { return result[0]; } return null; } }, query: { getIdMap(collection, fieldname, firstOnly = false) { const coll = service.collection(collection); return coll.data.reduce( (acc, item) => ({ ...acc, [item[fieldname]]: firstOnly ? item.DROSSE.ids[0] : item.DROSSE.ids }), {} ); }, chain(collection) { return service.collection(collection).chain(); }, clean }, insert(collection, ids, payload) { const coll = service.collection(collection); return coll.insert(cloneDeep({ ...payload, DROSSE: { ids } })); }, update: { byId(collection, id, newValue) { const coll = service.collection(collection); coll.findAndUpdate({ "DROSSE.ids": { $contains: id } }, (doc) => { Object.entries(newValue).forEach(([key, value]) => { set(doc, key, value); }); }); }, subItem: { append(collection, id, subPath, payload) { const coll = service.collection(collection); coll.findAndUpdate({ "DROSSE.ids": { $contains: id } }, (doc) => { if (!get(doc, subPath)) { set(doc, subPath, []); } get(doc, subPath).push(payload); }); }, prepend(collection, id, subPath, payload) { const coll = service.collection(collection); coll.findAndUpdate({ "DROSSE.ids": { $contains: id } }, (doc) => { if (!get(doc, subPath)) { set(doc, subPath, []); } get(doc, subPath).unshift(payload); }); } } }, remove: { byId(collection, id) { const coll = service.collection(collection); const toDelete = coll.findOne({ "DROSSE.ids": { $contains: id } }); return toDelete && coll.remove(toDelete); } } }; return service; } const db$1 = useDB(); const state$6 = useState(); const { loadStatic: loadStatic$1, loadScraped: loadScraped$1 } = useIO(); function useAPI(req, res) { return { req, res, db: db$1, logger, io: { loadStatic: loadStatic$1, loadScraped: loadScraped$1 }, config: state$6.get() }; } const state$5 = useState(); const parse$1 = async ({ routes, root = [], hierarchy = [], onRouteDef }) => { let inherited = []; const localHierarchy = [].concat(hierarchy); if (routes.DROSSE) { localHierarchy.push({ ...routes.DROSSE, path: root }); } const orderedRoutes = Object.entries(routes).filter(([path]) => path !== "DROSSE"); for (const orderedRoute of orderedRoutes) { const [path, content] = orderedRoute; const fullPath = `/${root.join("/")}`; if (Object.values(state$5.get("reservedRoutes")).includes(fullPath)) { throw new Error(`Route "${fullPath}" is reserved`); } const parsed = await parse$1({ routes: content, root: root.concat(path), hierarchy: localHierarchy, onRouteDef }); inherited = inherited.concat(parsed); } if (routes.DROSSE) { const routeDef = await onRouteDef(routes.DROSSE, root, localHierarchy); inherited = inherited.concat(routeDef); } return inherited; }; function useParser() { return { parse: parse$1 }; } function useScraper() { const { getStaticFileName, writeScrapedFile } = useIO(); const staticService = async (json, api) => { const { req } = api; const filename = getStaticFileName( req.baseUrl.split("/").slice(1), "json", req.params, req.method, req.query ); await writeScrapedFile(filename, json); return true; }; return { staticService }; } let state$4 = config.templates; function useTemplates() { return { merge(tpls) { state$4 = { ...state$4, ...tpls }; }, set(tpl) { state$4 = tpl; }, list() { return state$4; } }; } const { loadService, loadScraperService, loadStatic, loadScraped } = useIO(); const { parse } = useParser(); const state$3 = useState(); const templates = useTemplates(); const getThrottle = function(min, max) { return Math.floor(Math.random() * (max - min)) + min; }; const getThrottleMiddleware = (def) => async () => new Promise((resolve) => { const delay = getThrottle( def.throttle.min || 0, def.throttle.max || def.throttle.min ); setTimeout(resolve, delay); }); const getProxy = function(def) { return typeof def.proxy === "string" ? { target: def.proxy } : def.proxy; }; const createRoutes = async (app, router, routes) => { const context = { app, router, proxies: [], assets: [] }; const inherited = await parse({ routes, onRouteDef: await createRoute.bind(context) }); const result = inherited.reduce((acc, item) => { if (!acc[item.path]) { acc[item.path] = {}; } if (!acc[item.path][item.verb]) { acc[item.path][item.verb] = { template: false, throttle: false, proxy: false }; } acc[item.path][item.verb][item.type] = true; return acc; }, {}); createAssets(context); createProxies(context); return result; }; const createRoute = async function(def, root, defHierarchy) { const { router, app, proxies, assets } = this; const inheritance = []; const verbs = ["get", "post", "put", "delete"].filter((verb) => def[verb]); for (const verb of verbs) { const originalThrottle = !isEmpty(def[verb].throttle); def[verb].throttle = def[verb].throttle || defHierarchy.reduce((acc, item) => item.throttle || acc, {}); if (!originalThrottle && !isEmpty(def[verb].throttle)) { inheritance.push({ path: root.join("/"), type: "throttle", verb }); } const originalTemplate = def[verb].template === null || Boolean(def[verb].template); def[verb].template = originalTemplate ? def[verb].template : defHierarchy.reduce((acc, item) => item.template || acc, {}); if (!originalTemplate && Object.keys(def[verb].template).length) { inheritance.push({ path: root.join("/"), type: "template", verb }); } const inheritsProxy = Boolean(defHierarchy.find( (item) => root.join("/").includes(item.path.join("/")) )?.proxy); await setRoute(app, router, def[verb], verb, root, inheritsProxy); } if (def.assets) { const routePath = [""].concat(root); const assetsSubPath = def.assets === true ? routePath : typeof def.assets === "string" ? def.assets.split("/") : def.assets; assets.push({ path: routePath.join("/"), context: { target: path.join(state$3.get("assetsPath"), ...assetsSubPath) } }); } if (def.proxy || def.scraper) { const proxyResHooks = []; const path = [""].concat(root); const onProxyReq = async function(proxyReq, req, res) { return new Promise((resolve, reject) => { try { if (!isEmpty(req.body)) { const bodyData = JSON.stringify(req.body); proxyReq.setHeader("Content-Type", "application/json"); proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData)); proxyReq.write(bodyData); } resolve(); } catch (e) { reject(e); } }); }; const applyProxyRes = function(hooks, def2) { if (hooks.length === 0) { return; } return httpProxyMiddleware.responseInterceptor(async (responseBuffer, proxyRes, req, res) => { const response = responseBuffer.toString("utf8"); try { const json = JSON.parse(response); hooks.forEach((hook) => hook(json, req, res)); return JSON.stringify(json); } catch (e) { console.log("Response error: could not encode string to JSON"); console.log(response); console.log(e); console.log( "Will try to fallback on the static mock or at least return a vaild JSON string." ); return JSON.stringify(def2.body) || "{}"; } }); }; if (def.scraper) { let tmpProxy = def.proxy; if (!tmpProxy) { tmpProxy = defHierarchy.reduce((acc, item) => { if (item.proxy) { return { proxy: getProxy(item), path: item.path }; } else { if (!acc) { return acc; } const subpath = item.path.filter((path2) => !acc.path.includes(path2)); const proxy = getProxy(acc); proxy.target = proxy.target.split("/").concat(subpath).join("/"); return { proxy, path: item.path }; } }, null); def.proxy = tmpProxy && tmpProxy.proxy; } let scraperService; if (def.scraper.service) { scraperService = await loadScraperService(root); } else if (def.scraper.static) { scraperService = useScraper().staticService; } proxyResHooks.push((json, req, res) => { const api = useAPI(req, res); scraperService(json, api); }); } if (def.proxy) { if (def.proxy.responseRewriters) { def.proxy.selfHandleResponse = true; def.proxy.responseRewriters.forEach((rewriterName) => { proxyResHooks.push(internalMiddlewares[rewriterName]); }); } proxies.push({ path: path.join("/"), context: { ...getProxy(def), changeOrigin: true, selfHandleResponse: Boolean(proxyResHooks.length), pathRewrite: { [path.join("/")]: "/" }, onProxyReq, onProxyRes: applyProxyRes(proxyResHooks, def) }, def }); } } return inheritance; }; const setRoute = async (app, router, def, verb, root, inheritsProxy) => { const path = `${state$3.get("basePath")}/${root.join("/")}`; if (Object.keys(def.throttle).length) { app.use(path, getThrottleMiddleware(def)); } const handler = async (req, res, next) => { let response; let applyTemplate = true; let staticExtension = "json"; if (def.service) { const api = useAPI(req, res); const { serviceFile, service } = await loadService(root, verb); try { response = await service(api); } catch (e) { console.log("Error in service", serviceFile, e); return next(e); } } if (def.static) { try { const params = h3.getRouterParams(req); const query = h3.getQuery(req); const { extensions } = def; const [result, extension] = await loadStatic({ routePath: root, params, verb, query, extensions }); response = result; staticExtension = extension; if (!response) { const [result2, extension2] = await loadScraped({ routePath: root, params, verb, query, extensions }); applyTemplate = false; response = result2; staticExtension = extension2; if (!response) { applyTemplate = true; response = { drosse: `loadStatic: file not found with routePath = ${root.join( "/" )}` }; } } } catch (e) { response = { drosse: e.message }; } } if (def.body) { response = def.body; applyTemplate = true; } if (applyTemplate && def.responseType !== "file" && staticExtension === "json" && def.template && Object.keys(def.template).length) { response = templates.list()[def.template](response); } if (def.responseType === "file" || staticExtension && staticExtension !== "json") { return res.sendFile(response, function(err) { if (err) { logger.error(err.stack); } else { logger.success("File downloaded successfully"); } }); } return response; }; if (inheritsProxy) { app.use(path, handler, { match: (url) => url === "/" }); } else { router[verb](path, handler); } logger.success( `-> ${verb.toUpperCase().padEnd(7)} ${state$3.get("basePath")}/${root.join( "/" )}` ); }; const createAssets = ({ app, assets }) => { if (!assets) { return; } const _assets = []; assets.forEach(({ path: routePath, context }) => { const fsPath = path.join(state$3.get("root"), context.target); let mwPath = routePath; if (routePath.includes("*")) { mwPath = mwPath.substring(0, mwPath.indexOf("*")); mwPath = mwPath.substring(0, mwPath.lastIndexOf("/")); const re = new RegExp(routePath.replaceAll("*", "[^/]*")); app.use(mwPath, (req, res) => { if (re.test(`${mwPath}${req.url}`)) { h3.setResponseHeader( res, "x-wildcard-asset-target", context.target ); } }); _assets.push({ mwPath, fsPath, wildcardPath: routePath }); } else { _assets.push({ mwPath, fsPath }); } }); _assets.forEach(({ mwPath, fsPath, wildcardPath }) => { app.use(mwPath, (req, res, next) => { const wildcardAssetTarget = h3.getResponseHeader( res, "x-wildcard-asset-target" ); if (wildcardAssetTarget) { req.url = wildcardAssetTarget.replace(path.join(state$3.get("assetsPath"), mwPath), ""); } return serveStatic__default(fsPath, { redirect: false })(req, res, next); }); logger.info( `-> STATIC ASSETS ${wildcardPath || mwPath || "/"} => ${fsPath}` ); }); }; const createProxies = ({ app, router, proxies }) => { if (!proxies) { return; } proxies.forEach(({ path, context, def }) => { const proxyMw = httpProxyMiddleware.createProxyMiddleware({ ...context, logLevel: "warn" }); if (Object.keys(def.throttle || {}).length) { app.use(path || "/", getThrottleMiddleware(def)); } app.use( path || "/", async (req, res) => { return new Promise((resolve, reject) => { const next = (err) => { if (err) { reject(err); } else { resolve(true); } }; h3.setResponseHeader(res, "x-proxied", true); return proxyMw(req, res, next); }); } ); logger.info(`-> PROXY ${path || "/"} => ${context.target}`); }); }; let state$2 = config.commands; function useCommand() { return { merge(commands) { state$2 = { ...state$2, ...commands }; }, executeCommand(command) { return get(state$2, command.name)(command.params); } }; } let state$1 = []; function useMiddlewares() { return { append(mw) { state$1 = [...state$1, ...mw]; }, set(mw) { state$1 = [...mw]; }, list() { return state$1; } }; } const api = useAPI(); const { executeCommand } = useCommand(); const { loadDb } = useDB(); const { checkRoutesFile, loadUuid, getUserConfig, getRoutesDef } = useIO(); const { isEsmMode } = useIO$1(); const middlewares = useMiddlewares(); const state = useState(); let app, emit$1, root, listener, userConfig, version; const init = async (_root, _emit, _version) => { version = _version; root = path.resolve(_root); emit$1 = _emit; state.set("root", root); userConfig = await getUserConfig(); state.merge(userConfig); if (!await checkRoutesFile()) { logger.error( `Please create a "${state.get( "routesFile" )}.json" file in this directory: ${state.get("root")}, and restart.` ); process.exit(); } await loadUuid(); }; const initServer = async () => { app = h3.createApp({ debug: true }); await loadDb(); middlewares.set(config.middlewares); if (userConfig.middlewares) { middlewares.append(userConfig.middlewares); } if (userConfig.templates) { useTemplates().merge(userConfig.templates); } if (userConfig.commands) { useCommand().merge(userConfig.commands(api)); } logger.info("-> Middlewares:"); console.info(middlewares.list()); for (let mw of middlewares.list()) { if (typeof mw !== "function") { mw = internalMiddlewares[mw]; } if (mw.length === 4) { mw = curry(mw)(api); } app.use(mw); } const routesDef = await getRoutesDef(); const router = h3.createRouter(); await createRoutes(app, router, routesDef); if (userConfig.errorHandler) { app.use(userConfig.errorHandler); } app.use((req) => { if (!Object.values(state.get("reservedRoutes")).includes(req.url)) { emit$1("request", { url: req.url, method: req.method }); } }); app.use(state.get("reservedRoutes").ui, internalMiddlewares["open-cors"]); router.get(state.get("reservedRoutes").ui, () => { return { routes: routesDef }; }); app.use(state.get("reservedRoutes").cmd, internalMiddlewares["open-cors"]); router.post(state.get("reservedRoutes").cmd, async (req) => { const body = await h3.readBody(req); if (body.cmd === "restart") { emit$1("restart"); if (isEsmMode()) { return { restarted: false, comment: RESTART_DISABLED_IN_ESM_MODE }; } else { return { restarted: true }; } } else { const result = await executeCommand({ name: body.cmd, params: body }); return result; } }); app.use(router); }; const getDescription = () => ({ isDrosse: true, version, uuid: state.get("uuid"), name: state.get("name"), proto: "http", port: state.get("port"), root: state.get("root"), routesFile: state.get("routesFile"), collectionsPath: state.get("collectionsPath") }); const start = async () => { await initServer(); const description = getDescription(); console.log(); logger.debug( `App ${description.name ? ansiColors__default.magenta(description.name) + " " : ""}(version ${ansiColors__default.magenta(version)}) running at:` ); listener = await listhen.listen(app, { port: description.port }); if (typeof userConfig.extendServer === "function") { userConfig.extendServer({ server: listener.server, app, db: api.db }); } if (typeof userConfig.onHttpUpgrade === "function") { listener.server.on("upgrade", userConfig.onHttpUpgrade); } console.log(); logger.debug(`Mocks root: ${ansiColors__default.magenta(description.root)}`); console.log(); emit$1("start", state.get()); return listener; }; const stop = async () => { await listener.close(); logger.warn("Server stopped"); emit$1("stop"); }; const restart = async () => { if (isEsmMode()) { console.warn(RESTART_DISABLED_IN_ESM_MODE); console.info("Please use ctrl+c to restart drosse."); } else { await stop(); await start(); } }; const describe = () => { return getDescription(); }; function serveStatic(root, port, proxy) { const app = h3.createApp({ debug: true }); app.use(internalMiddlewares.morgan); const staticMw = serveStatic__default(root, { fallthrough: false, redirect: false }); if (proxy) { const proxyMw = httpProxyMiddleware.createProxyMiddleware({ target: proxy, changeOriging: true }); app.use(async (req, res) => { let fileExists; try { await fs.promises.access(path.join(root, req.url)); fileExists = true; } catch { fileExists = false; } return new Promise((resolve, reject) => { const next = (err) => { if (err) { reject(err); } else { resolve(true); } }; return fileExists ? staticMw(req, res, next) : proxyMw(req, res, next); }); }); } else { app.use("/", staticMw); } listhen.listen(app, { port }); } function db(vorpal, { config, restart }) { const dropDatabase = async () => { const dbFile = path.join(config.root, config.database); await fs.promises.rm(dbFile); return restart(); }; vorpal.command("db drop", "Delete the database file.").action(dropDatabase); } function cli(vorpal, params) { vorpal.command("rs", "Restart the server.").action(() => { return params.restart(); }); db(vorpal, params); } function useCLI(config, restart) { const vorpal = _vorpal__default(); const { executeCommand } = useCommand(); const runCommand = async (name, params) => executeCommand({ name, params }); return { extend(callback) { callback(vorpal, { config, runCommand, restart }); }, start() { this.extend(cli); vorpal.delimiter("\u{1F3A4}").show(); } }; } const defineDrosseServer = (userConfig) => userConfig; const defineDrosseService = (handler) => handler; process.title = `node drosse ${process.argv[1]}`; let _version, discover, description, noRepl; const getVersion = async () => { if (!_version) { try { const importPath = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href)).replace("file://", "/"); const packageFile = path.join(importPath, "..", "..", "package.json"); const content = await promises.readFile(packageFile, "utf8"); _version = JSON.parse(content).version; } catch (e) { console.error("Failed to get Drosse version", e); } } return _version; }; const emit = async (event, data) => { switch (event) { case "start": description = describe(); if (!Boolean(discover)) { discover = new Discover__default(); discover.advertise(description); } try { setTimeout(() => { discover?.send(event, { uuid: description.uuid }); }, 10); } catch (e) { console.error(e); } if (Boolean(noRepl)) { return; } const io = useIO(); const cli = useCLI(data, restart); const userConfig = await io.getUserConfig(data.root); if (userConfig.cli) { cli.extend(userConfig.cli); } cli.start(); break; case "restart": restart(); break; case "stop": discover?.send(event, { uuid: description.uuid }); break; case "request": discover?.send(event, { uuid: description.uuid, ...data }); break; } }; function getMatchablePath(path) { let stop = false; return path.replace(/^(.*\/\/)?\/(.*)$/g, "/$2").split("/").reduce((matchablePath, dir) => { if (["node_modules", "dist", "src"].includes(dir)) { stop = true; } if (!stop) { matchablePath.push(dir); } return matchablePath; }, []).join("/"); } if (getMatchablePath((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))) === getMatchablePath(process.argv[1])) { yargs__default(helpers.hideBin(process.argv)).usage("Usage: $0 [args]").command({ command: "describe ", desc: "Describe the mock server", handler: async (argv) => { const version = await getVersion(); await init(argv.rootPath, emit, version); console.log(describe()); process.exit(); } }).command({ command: "serve ", desc: "Run the mock server", builder: { norepl: { default: false, describe: "Disable repl mode", type: "boolean" } }, handler: async (argv) => { noRepl = argv.norepl; const version = await getVersion(); await init(argv.rootPath, emit, version); return start(); } }).command({ command: "static ", desc: "Run a static file server", builder: { port: { alias: "p", describe: "HTTP port", type: "number" }, proxy: { alias: "P", describe: "Proxy requests to another host", type: "string" } }, handler: async (argv) => { return serveStatic(argv.rootPath, argv.port, argv.proxy); } }).demandCommand(1).strict().parse(); } exports.defineDrosseServer = defineDrosseServer; exports.defineDrosseService = defineDrosseService;