import chalk from 'chalk' import chokidar from 'chokidar' import cookieParser from 'cookie-parser' import { debounce, memoize } from 'es-toolkit' import express from 'express' import fs from 'fs-extra' import { globbySync } from 'globby' import multer from 'multer' import { createRequire } from 'node:module' import { pathToRegexp } from 'path-to-regexp' import { resolveModule } from '@/utils/resolveModule' import paths from '../../paths' const require = createRequire(import.meta.url) let mockCache: Record = {} let isSetup = false const setupMock = debounce(function setupMock() { // clean mockCache = {} const files = globbySync(paths.mock, { expandDirectories: { extensions: ['js', 'ts'] } }) if (isSetup) { console.log(chalk.green('Mock files changed, reloaded')) } else { isSetup = true } // setup for (const file of files) { delete require.cache[require.resolve(file)] try { const mock = resolveModule(require(file)) for (const key in mock) { const [method, path] = key.split(' ') mockCache[key] = { method, path, handler: mock[key] } } } catch (e: any) { console.error(chalk.red(`Mock file ${file} error: ${e.message}`)) } } }, 100) const getPathReAndKeys = memoize((path: string) => { const { regexp, keys } = pathToRegexp(path) return { re: regexp, keys } }) function decodeParam(val: string) { if (typeof val !== 'string' || val.length === 0) { return val } try { return decodeURIComponent(val) } catch (err) { if (err instanceof URIError) { err.message = `Failed to decode param ' ${val} '` // @ts-ignore err.status = 400 // @ts-ignore err.statusCode = 400 } throw err } } /** * @type {import('express').RequestHandler} */ const mockMiddlewave: express.RequestHandler = function mockMiddlewave(req, res, next) { const { method, path } = req for (const key in mockCache) { const mock = mockCache[key] if (mock.method !== method) continue const { keys, re } = getPathReAndKeys(mock.path) const m = re.exec(path) if (m) { console.log(chalk.green(`Mock: ${key}`)) res.setHeader('X-Mock', key) if (typeof mock.handler === 'function') { const params = {} for (let i = 1; i < m.length; i += 1) { const key = keys[i - 1] const prop = key.name const val = decodeParam(m[i]) if (val !== undefined) { // @ts-ignore params[prop] = val } } req.params = params const middelwaves = [ express.urlencoded({ limit: '5mb', extended: true }), express.json({ limit: '5mb', strict: false }), multer().any(), cookieParser(), mock.handler, ] const throwNext = () => { console.log(chalk.red(`Mock: ${key} don't use next()`)) res.sendStatus(500) } const mwNext = () => { const middelwave = middelwaves.shift() middelwave(req, res, middelwaves.length ? mwNext : throwNext) } mwNext() } else { res.json(mock.handler) } return } } next() } type Middlewares = Parameters< NonNullable >[0] function setupMockServer(middelwaves: Middlewares, _devServer: any) { if (!fs.existsSync(paths.mock)) return middelwaves.unshift({ name: 'edu-scripts-mock-middelwave', middleware: mockMiddlewave as any, }) console.log( ` ${chalk.green.bold('[edu-scripts] Mock server created:')} ${chalk.cyan.bold(paths.mock)}` ) chokidar.watch(paths.mock).on('all', setupMock) return middelwaves } export default setupMockServer