const { winPath, createDebug, glob, parseRequireDeps, } = require('@umijs/utils'); const path = require('path'); const fs = require('fs'); const assert = require('assert'); const bodyParser = require('body-parser'); const pathToRegexp = require('path-to-regexp'); const multer = require('multer'); const VALID_METHODS = ['get', 'post', 'put', 'patch', 'delete']; const BODY_PARSED_METHODS = ['post', 'put', 'patch', 'delete']; const debug = createDebug('preset-build-in:mock:utils'); const registerBabel = paths => { // support // clear require cache and set babel register const requireDeps = paths.reduce((memo, file) => { memo = memo.concat(parseRequireDeps(file)); return memo; }, []); requireDeps.forEach(f => { if (require.cache[f]) { delete require.cache[f]; } }); }; const getMockData = ({ cwd, ignore = [], registerBabel = () => {} }) => { const mockPaths = [ ...(glob.sync('mock/**/*.[jt]s', { cwd, ignore, }) || []), ...(glob.sync('**/_mock.[jt]s', { cwd, ignore, }) || []), ] .map((p) => path.join(cwd, p)) .filter((p) => p && fs.existsSync(p)) .map((p) => winPath(p)); debug(`load mock data including files ${JSON.stringify(mockPaths)}`); registerBabel(mockPaths); // get mock data const mockData = normalizeConfig(getMockConfig(mockPaths)); const mockWatcherPaths = [...(mockPaths || []), path.join(cwd, 'mock')] .filter((p) => p && fs.existsSync(p)) .map((p) => winPath(p)); return { mockData, mockPaths, mockWatcherPaths, }; }; function parseKey(key) { let method = 'get'; // eslint-disable-next-line no-shadow let path = key; if (/\s+/.test(key)) { const splited = key.split(/\s+/); method = splited[0].toLowerCase(); // eslint-disable-next-line prefer-destructuring path = splited[1]; } assert( VALID_METHODS.includes(method), `Invalid method ${method} for path ${path}, please check your mock files.`, ); return { method, path, }; } function getMockConfig(files) { return files.reduce((memo, mockFile) => { try { const m = require(mockFile); // eslint-disable-line memo = { ...memo, ...(m.default || m), }; return memo; } catch (e) { throw new Error(e.stack); } }, {}); } function normalizeConfig(config) { return Object.keys(config).reduce((memo, key) => { const handler = config[key]; const type = typeof handler; assert( type === 'function' || type === 'object', `mock value of ${key} should be function or object, but got ${type}`, ); // eslint-disable-next-line no-shadow const { method, path } = parseKey(key); const keys = []; const re = pathToRegexp(path, keys); memo.push({ method, path, re, handler: createHandler(method, path, handler), }); return memo; }, []); } // eslint-disable-next-line no-shadow function createHandler(method, path, handler) { return function(req, res, next) { if (BODY_PARSED_METHODS.includes(method)) { bodyParser.json({ limit: '5mb', strict: false })(req, res, () => { bodyParser.urlencoded({ limit: '5mb', extended: true })( req, res, () => { sendData(); }, ); }); } else { sendData(); } function sendData() { if (typeof handler === 'function') { multer().any()(req, res, () => { handler(req, res, next); }); } else { res.json(handler); } } }; } function decodeParam(val) { 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; } } function cleanRequireCache(paths) { Object.keys(require.cache).forEach(file => { if (paths.some(p => winPath(file).indexOf(p) > -1)) { delete require.cache[file]; } }); } function matchMock(req, mockData) { const { method } = req; const targetMethod = method.toLowerCase(); // eslint-disable-next-line no-underscore-dangle const resolvepath = req._parsedUrl.path; for (const mock of mockData) { // eslint-disable-next-line no-shadow const { method, re, keys } = mock; if (method === targetMethod) { // eslint-disable-next-line no-underscore-dangle const match = re.exec(resolvepath); if (match) { const params = {}; for (let i = 1; i < match.length; i += 1) { const key = keys[i - 1]; const prop = key.name; const val = decodeParam(match[i]); // @ts-ignore // eslint-disable-next-line no-undef if (val !== undefined || !hasOwnProdperty.call(params, prop)) { params[prop] = val; } } req.params = params; return mock; } } } } function getFlatRoutes(routes) { return routes.reduce((memo, route) => { // eslint-disable-next-line no-shadow const { routes, path } = route; if (path && !path.includes('?')) { memo.push(route); } if (routes) { memo = memo.concat( getFlatRoutes({ routes, }), ); } return memo; }, []); } function getConflictPaths({ mockData, routes }) { const conflictPaths = []; getFlatRoutes({ routes }).forEach(route => { const { path, redirect } = route; if (path && !path.startsWith(':') && !redirect) { const req = { path: !path.startsWith('/') ? `/${path}` : path, method: 'get', }; const matched = matchMock(req, mockData); if (matched) { conflictPaths.push({ path: matched.path }); } } }); return conflictPaths; } module.exports = { getMockData, matchMock, cleanRequireCache, };