UNPKG

smoke

Version:

Simple yet powerful file-based mock server with recording abilities

176 lines (147 loc) 5.06 kB
import process from 'node:process'; import path from 'node:path'; import fs from 'node:fs/promises'; import {globby} from 'globby'; import {pathToRegexp} from 'path-to-regexp'; const methodRegExp = /^([a-z+]+?)_/i; const paramsRegExp = /[$?]([^.\s]+$)/; const setRegExp = /__([\w-]+?)$/; const defaultType = 'application/octet-stream'; const importFresh = async (moduleName) => { // Jest monkey patched import function behaves differently if (process.env.JEST_WORKER_ID !== undefined) { return import(`${moduleName}?${Date.now()}`); } return import(`${moduleName.replaceAll('#', '%23')}?${Date.now()}`); }; export class MockContentError extends Error { constructor(message, error) { super(message); this.innerError = error; } toString() { return `${this.message}: ${this.innerError.message}`; } } export function getMock(basePath, file, data = undefined) { let ext = path.extname(file); const isTemplate = ext.endsWith('_'); let basename = path.basename(file, ext); ext = ext ? ext.slice(1, ext.length - (isTemplate ? 1 : 0)).toLowerCase() : ext; if (data !== undefined) { ext = typeof data === 'function' ? 'js' : !ext && typeof data === 'object' ? 'json' : ext; } let set = null; const matchSet = basename.match(setRegExp); if (matchSet) { set = matchSet[1]; basename = path.basename(basename, matchSet[0]); } let params = null; const matchParams = basename.match(paramsRegExp); if (matchParams) { params = matchParams[1] .split('&') .map((value) => decodeURIComponent(value)) .reduce((acc, value) => { const param = value.split('='); acc[param[0]] = param[1]; return acc; }, {}); basename = basename.slice(0, Math.max(0, basename.length - matchParams[1].length - 1)); } const dirComponents = path .dirname(file) .split(path.delimiter) .filter((c) => c && c !== '.'); const fileComponents = basename.split('#').filter(Boolean); let methods = null; const matchMethods = fileComponents[0].match(methodRegExp); if (matchMethods) { methods = matchMethods[1]; fileComponents[0] = fileComponents[0].slice(Math.max(0, methods.length + 1)); methods &&= methods .split('+') .filter(Boolean) .map((m) => m.toLowerCase()); } const reqPath = [...dirComponents, ...fileComponents].filter(Boolean).join('/').replaceAll('@', ':'); const {regexp, keys} = pathToRegexp(reqPath); return { file: path.join(basePath, file), ext, type: ext || defaultType, set, isTemplate, methods, reqPath, regexp, keys, params, data, }; } export async function getMocks(basePath, ignoreGlobs, globs = ['**/*']) { let mockFiles = await getMockFiles(basePath, ignoreGlobs, globs); const mockCollectionFiles = []; mockFiles = mockFiles.filter((mock) => { if (mock.endsWith('.mocks.js') || mock.endsWith('.mocks.cjs')) { mockCollectionFiles.push(mock); return false; } return true; }); return mockFiles .map((file) => getMock(basePath, file)) .concat(await getMocksFromCollections(basePath, mockCollectionFiles)); } export function getMockFiles(basePath, ignoreGlobs, globs) { if (!basePath) { globs.push('!node_modules'); } // Ensure relative paths for ignore globs ignoreGlobs = ignoreGlobs.map((glob) => `!${path.isAbsolute(glob) ? path.relative(basePath, glob) : glob}`); return globby(globs.concat(ignoreGlobs), {cwd: basePath}); } export async function getMocksFromCollections(basePath, mockCollectionFiles) { let mocks = []; basePath = path.isAbsolute(basePath) ? basePath : path.join(process.cwd(), basePath); for (const file of mockCollectionFiles) { try { // eslint-disable-next-line no-await-in-loop const collection = (await importFresh(path.join(basePath, file))).default; const newMocks = Object.entries(collection).map(([route, data]) => getMock(basePath, route, data)); mocks = mocks.concat(newMocks); } catch (error) { console.error(`Error while loading collection "${file}"`, error); } } return mocks; } export async function getMockContent(mock) { let content; if (mock.data !== undefined) { content = mock.data; } else if (mock.isTemplate || mock.ext === 'json') { try { content = await fs.readFile(mock.file, 'utf8'); } catch (error) { throw new MockContentError(`Error while reading mock file "${mock.file}"`, error); } } else if (mock.ext === 'js' || mock.ext === 'cjs') { try { const filePath = path.isAbsolute(mock.file) ? mock.file : path.join(process.cwd(), mock.file); content = (await importFresh(filePath)).default; } catch { throw new MockContentError(`Error while evaluating JS for mock "${mock.file}"`); } } else { try { // Read file as buffer content = await fs.readFile(mock.file); } catch { throw new MockContentError(`Error while reading mock file "${mock.file}"`); } } return content; }