push
This commit is contained in:
103
build/plugin/mock/client.ts
Normal file
103
build/plugin/mock/client.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable */
|
||||
import type { MockMethod } from './types';
|
||||
|
||||
export async function createProdMockServer(mockList: any[]) {
|
||||
const Mock: any = await import('mockjs');
|
||||
const { pathToRegexp } = await import('path-to-regexp');
|
||||
Mock.XHR.prototype.__send = Mock.XHR.prototype.send;
|
||||
Mock.XHR.prototype.send = function () {
|
||||
if (this.custom.xhr) {
|
||||
this.custom.xhr.withCredentials = this.withCredentials || false;
|
||||
|
||||
if (this.responseType) {
|
||||
this.custom.xhr.responseType = this.responseType;
|
||||
}
|
||||
}
|
||||
if (this.custom.requestHeaders) {
|
||||
const headers: any = {};
|
||||
for (let k in this.custom.requestHeaders) {
|
||||
headers[k.toString().toLowerCase()] = this.custom.requestHeaders[k];
|
||||
}
|
||||
this.custom.options = Object.assign({}, this.custom.options, { headers });
|
||||
}
|
||||
this.__send.apply(this, arguments);
|
||||
};
|
||||
|
||||
Mock.XHR.prototype.proxy_open = Mock.XHR.prototype.open;
|
||||
|
||||
Mock.XHR.prototype.open = function () {
|
||||
let responseType = this.responseType;
|
||||
this.proxy_open(...arguments);
|
||||
if (this.custom.xhr) {
|
||||
if (responseType) {
|
||||
this.custom.xhr.responseType = responseType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const { url, method, response, timeout } of mockList) {
|
||||
__setupMock__(Mock, timeout);
|
||||
Mock.mock(
|
||||
pathToRegexp(url, undefined, { end: false }),
|
||||
method || 'get',
|
||||
__XHR2ExpressReqWrapper__(Mock, response),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function __param2Obj__(url: string) {
|
||||
const search = url.split('?')[1];
|
||||
if (!search) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(
|
||||
'{"' +
|
||||
decodeURIComponent(search)
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/&/g, '","')
|
||||
.replace(/=/g, '":"')
|
||||
.replace(/\+/g, ' ') +
|
||||
'"}',
|
||||
);
|
||||
}
|
||||
|
||||
function __XHR2ExpressReqWrapper__(_Mock: any, handle: (d: any) => any) {
|
||||
return function (options: any) {
|
||||
let result = null;
|
||||
if (typeof handle === 'function') {
|
||||
const { body, type, url, headers } = options;
|
||||
|
||||
let b = body;
|
||||
try {
|
||||
b = JSON.parse(body);
|
||||
} catch {}
|
||||
result = handle({
|
||||
method: type,
|
||||
body: b,
|
||||
query: __param2Obj__(url),
|
||||
headers,
|
||||
});
|
||||
} else {
|
||||
result = handle;
|
||||
}
|
||||
|
||||
return _Mock.mock(result);
|
||||
};
|
||||
}
|
||||
|
||||
function __setupMock__(mock: any, timeout = 0) {
|
||||
timeout &&
|
||||
mock.setup({
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
export function defineMockModule(
|
||||
fn: (config: {
|
||||
env: Record<string, any>;
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
}) => Promise<MockMethod[]> | MockMethod[],
|
||||
) {
|
||||
return fn;
|
||||
}
|
||||
255
build/plugin/mock/createMockServer.ts
Normal file
255
build/plugin/mock/createMockServer.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import type { ViteMockOptions, MockMethod, Recordable, RespThisType } from './types';
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import chokidar from 'chokidar';
|
||||
import colors from 'picocolors';
|
||||
import url from 'url';
|
||||
import fg from 'fast-glob';
|
||||
import Mock from 'mockjs';
|
||||
import { pathToRegexp, match } from 'path-to-regexp';
|
||||
import { isArray, isFunction, sleep, isRegExp, isAbsPath } from './utils';
|
||||
import { IncomingMessage, NextHandleFunction } from 'connect';
|
||||
import { bundleRequire } from 'bundle-require';
|
||||
import type { ResolvedConfig } from 'vite';
|
||||
|
||||
export let mockData: MockMethod[] = [];
|
||||
|
||||
export async function createMockServer(
|
||||
opt: ViteMockOptions = { mockPath: 'mock', configPath: 'vite.mock.config' },
|
||||
config: ResolvedConfig,
|
||||
) {
|
||||
opt = {
|
||||
mockPath: 'mock',
|
||||
watchFiles: true,
|
||||
configPath: 'vite.mock.config.ts',
|
||||
logger: true,
|
||||
...opt,
|
||||
};
|
||||
|
||||
if (mockData.length > 0) return;
|
||||
mockData = await getMockConfig(opt, config);
|
||||
await createWatch(opt, config);
|
||||
}
|
||||
|
||||
// request match
|
||||
export async function requestMiddleware(opt: ViteMockOptions) {
|
||||
const { logger = true } = opt;
|
||||
const middleware: NextHandleFunction = async (req, res, next) => {
|
||||
let queryParams: {
|
||||
query?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
pathname?: string | null;
|
||||
} = {};
|
||||
|
||||
if (req.url) {
|
||||
queryParams = url.parse(req.url, true);
|
||||
}
|
||||
|
||||
const reqUrl = queryParams.pathname;
|
||||
|
||||
const matchRequest = mockData.find((item) => {
|
||||
if (!reqUrl || !item || !item.url) {
|
||||
return false;
|
||||
}
|
||||
if (item.method && item.method.toUpperCase() !== req.method) {
|
||||
return false;
|
||||
}
|
||||
return pathToRegexp(item.url).test(reqUrl);
|
||||
});
|
||||
|
||||
if (matchRequest) {
|
||||
const isGet = req.method && req.method.toUpperCase() === 'GET';
|
||||
const { response, rawResponse, timeout, statusCode, url } = matchRequest;
|
||||
|
||||
if (timeout) {
|
||||
await sleep(timeout);
|
||||
}
|
||||
|
||||
const urlMatch = match(url, { decode: decodeURIComponent });
|
||||
|
||||
let query = queryParams.query as any;
|
||||
if (reqUrl) {
|
||||
if ((isGet && JSON.stringify(query) === '{}') || !isGet) {
|
||||
const params = (urlMatch(reqUrl) as any).params;
|
||||
if (JSON.stringify(params) !== '{}') {
|
||||
query = (urlMatch(reqUrl) as any).params || {};
|
||||
} else {
|
||||
query = queryParams.query || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const self: RespThisType = { req, res, parseJson: parseJson.bind(null, req) };
|
||||
if (isFunction(rawResponse)) {
|
||||
await rawResponse.bind(self)(req, res);
|
||||
} else {
|
||||
const body = await parseJson(req);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.statusCode = statusCode || 200;
|
||||
const mockResponse = isFunction(response)
|
||||
? response.bind(self)({ url: req.url as any, body, query, headers: req.headers })
|
||||
: response;
|
||||
res.end(JSON.stringify(Mock.mock(mockResponse)));
|
||||
}
|
||||
|
||||
logger && loggerOutput('request invoke', req.url!);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
return middleware;
|
||||
}
|
||||
|
||||
// create watch mock
|
||||
function createWatch(opt: ViteMockOptions, config: ResolvedConfig) {
|
||||
const { configPath, logger, watchFiles } = opt;
|
||||
|
||||
if (!watchFiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { absConfigPath, absMockPath } = getPath(opt);
|
||||
|
||||
if (process.env.VITE_DISABLED_WATCH_MOCK === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const watchDir = [];
|
||||
const exitsConfigPath = fs.existsSync(absConfigPath);
|
||||
|
||||
exitsConfigPath && configPath ? watchDir.push(absConfigPath) : watchDir.push(absMockPath);
|
||||
|
||||
const watcher = chokidar.watch(watchDir, {
|
||||
ignored: opt.ignore || /.mjs$/,
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on('all', async (event, file) => {
|
||||
logger && loggerOutput(`mock file ${event}`, file);
|
||||
mockData = await getMockConfig(opt, config);
|
||||
});
|
||||
}
|
||||
|
||||
// clear cache
|
||||
function cleanRequireCache(opt: ViteMockOptions) {
|
||||
if (!require.cache) {
|
||||
return;
|
||||
}
|
||||
const { absConfigPath, absMockPath } = getPath(opt);
|
||||
Object.keys(require.cache).forEach((file) => {
|
||||
if (file === absConfigPath || file.indexOf(absMockPath) > -1) {
|
||||
delete require.cache[file];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson(req: IncomingMessage): Promise<Recordable> {
|
||||
return new Promise((resolve) => {
|
||||
let body = '';
|
||||
let jsonStr = '';
|
||||
req.on('data', function (chunk) {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', function () {
|
||||
try {
|
||||
jsonStr = JSON.parse(body);
|
||||
} catch (err) {
|
||||
jsonStr = '';
|
||||
}
|
||||
resolve(jsonStr as any);
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// load mock .ts files and watch
|
||||
async function getMockConfig(opt: ViteMockOptions, config: ResolvedConfig) {
|
||||
cleanRequireCache(opt);
|
||||
const { absConfigPath, absMockPath } = getPath(opt);
|
||||
const { ignore, configPath, logger } = opt;
|
||||
|
||||
let ret: MockMethod[] = [];
|
||||
|
||||
if (configPath && fs.existsSync(absConfigPath)) {
|
||||
logger && loggerOutput(`load mock data from`, absConfigPath);
|
||||
ret = await resolveModule(absConfigPath, config);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const mockFiles = fg
|
||||
.sync(`**/*.{ts,mjs,js}`, {
|
||||
cwd: absMockPath,
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!ignore) {
|
||||
return true;
|
||||
}
|
||||
if (isFunction(ignore)) {
|
||||
return ignore(item);
|
||||
}
|
||||
if (isRegExp(ignore)) {
|
||||
return !ignore.test(path.basename(item));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
ret = [];
|
||||
const resolveModulePromiseList = [];
|
||||
|
||||
for (let index = 0; index < mockFiles.length; index++) {
|
||||
const mockFile = mockFiles[index];
|
||||
resolveModulePromiseList.push(resolveModule(path.join(absMockPath, mockFile), config));
|
||||
}
|
||||
|
||||
const loadAllResult = await Promise.all(resolveModulePromiseList);
|
||||
for (const resultModule of loadAllResult) {
|
||||
let mod = resultModule;
|
||||
if (!isArray(mod)) {
|
||||
mod = [mod];
|
||||
}
|
||||
ret = [...ret, ...mod];
|
||||
}
|
||||
} catch (error: any) {
|
||||
loggerOutput(`mock reload error`, error);
|
||||
ret = [];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inspired by vite
|
||||
// support mock .ts files
|
||||
async function resolveModule(p: string, config: ResolvedConfig): Promise<any> {
|
||||
const mockData = await bundleRequire({
|
||||
filepath: p,
|
||||
});
|
||||
|
||||
let mod = mockData.mod.default || mockData.mod;
|
||||
if (isFunction(mod)) {
|
||||
mod = await mod({ env: config.env, mode: config.mode, command: config.command });
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
// get custom config file path and mock dir path
|
||||
function getPath(opt: ViteMockOptions) {
|
||||
const { mockPath, configPath } = opt;
|
||||
const cwd = process.cwd();
|
||||
const absMockPath = isAbsPath(mockPath) ? mockPath! : path.join(cwd, mockPath || '');
|
||||
const absConfigPath = path.join(cwd, configPath || '');
|
||||
return {
|
||||
absMockPath,
|
||||
absConfigPath,
|
||||
};
|
||||
}
|
||||
|
||||
function loggerOutput(title: string, msg: string, type: 'info' | 'error' = 'info') {
|
||||
const tag = type === 'info' ? colors.cyan(`[vite:mock]`) : colors.red(`[vite:mock-server]`);
|
||||
return console.log(
|
||||
`${colors.dim(new Date().toLocaleTimeString())} ${tag} ${colors.green(title)} ${colors.dim(
|
||||
msg,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
30
build/plugin/mock/index.ts
Normal file
30
build/plugin/mock/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ViteMockOptions } from './types';
|
||||
import type { Plugin } from 'vite';
|
||||
import { ResolvedConfig } from 'vite';
|
||||
import { createMockServer, requestMiddleware } from './createMockServer';
|
||||
|
||||
export function viteMockServe(opt: ViteMockOptions = {}): Plugin {
|
||||
let isDev = false;
|
||||
let config: ResolvedConfig;
|
||||
|
||||
return {
|
||||
name: 'vite:mock',
|
||||
enforce: 'pre' as const,
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
isDev = config.command === 'serve';
|
||||
isDev && createMockServer(opt, config);
|
||||
},
|
||||
|
||||
configureServer: async ({ middlewares }) => {
|
||||
const { enable = isDev } = opt;
|
||||
if (!enable) {
|
||||
return;
|
||||
}
|
||||
const middleware = await requestMiddleware(opt);
|
||||
middlewares.use(middleware);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export * from './types';
|
||||
9
build/plugin/mock/mock.ts
Normal file
9
build/plugin/mock/mock.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { viteMockServe } from './index';
|
||||
|
||||
export function configMockPlugin({ enable, mockPath }: { enable: boolean; mockPath: string }) {
|
||||
return viteMockServe({
|
||||
// ignore: /.mjs$/,
|
||||
mockPath: mockPath,
|
||||
enable,
|
||||
});
|
||||
}
|
||||
38
build/plugin/mock/types.ts
Normal file
38
build/plugin/mock/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
export interface ViteMockOptions {
|
||||
mockPath?: string;
|
||||
configPath?: string;
|
||||
ignore?: RegExp | ((fileName: string) => boolean);
|
||||
watchFiles?: boolean;
|
||||
enable?: boolean;
|
||||
logger?: boolean;
|
||||
}
|
||||
|
||||
export interface RespThisType {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
parseJson: () => any;
|
||||
}
|
||||
|
||||
export type MethodType = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
|
||||
export type Recordable<T = any> = Record<string, T>;
|
||||
|
||||
export declare interface MockMethod {
|
||||
url: string;
|
||||
method?: MethodType;
|
||||
timeout?: number;
|
||||
statusCode?: number;
|
||||
response?: (
|
||||
this: RespThisType,
|
||||
opt: { url: Recordable; body: Recordable; query: Recordable; headers: Recordable },
|
||||
) => any;
|
||||
rawResponse?: (this: RespThisType, req: IncomingMessage, res: ServerResponse) => void;
|
||||
}
|
||||
|
||||
export interface MockConfig {
|
||||
env: Record<string, any>;
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
}
|
||||
40
build/plugin/mock/utils.ts
Normal file
40
build/plugin/mock/utils.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from 'fs';
|
||||
|
||||
const toString = Object.prototype.toString;
|
||||
|
||||
export function is(val: unknown, type: string) {
|
||||
return toString.call(val) === `[object ${type}]`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function isFunction<T = Function>(val: unknown): val is T {
|
||||
return is(val, 'Function') || is(val, 'AsyncFunction');
|
||||
}
|
||||
|
||||
export function isArray(val: any): val is Array<any> {
|
||||
return val && Array.isArray(val);
|
||||
}
|
||||
|
||||
export function isRegExp(val: unknown): val is RegExp {
|
||||
return is(val, 'RegExp');
|
||||
}
|
||||
|
||||
export function isAbsPath(path: string | undefined) {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
// Windows 路径格式:C:\ 或 \\ 开头,或已含盘符(D:\path\to\file)
|
||||
if (/^([a-zA-Z]:\\|\\\\|(?:\/|\uFF0F){2,})/.test(path)) {
|
||||
return true;
|
||||
}
|
||||
// Unix/Linux 路径格式:/ 开头
|
||||
return /^\/[^/]/.test(path);
|
||||
}
|
||||
|
||||
export function sleep(time: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve('');
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user