UNPKG

vite-plugin-mock-dev-server

Version:
718 lines (704 loc) 23.4 kB
import { isArray, isBoolean, promiseParallel, toArray, uniq } from "./dist-CAA1v47s.js"; import { createDefineMock, createSSEStream, defineMock, defineMockData } from "./helper-DHb-Bj_j.js"; import { baseMiddleware, createLogger, debug, doesProxyContextMatchUrl, ensureProxies, logLevels, lookupFile, mockWebSocket, normalizePath, recoverRequest, sortByValidator, transformMockData, transformRawData, urlParse } from "./server-C-u7jwot.js"; import pc from "picocolors"; import fs, { promises } from "node:fs"; import fsp from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { createFilter } from "@rollup/pluginutils"; import fg from "fast-glob"; import isCore from "is-core-module"; import { pathToFileURL } from "node:url"; import { build } from "esbuild"; import JSON5 from "json5"; import { pathToRegexp } from "path-to-regexp"; import cors from "cors"; import EventEmitter from "node:events"; import chokidar from "chokidar"; //#region src/core/compiler.ts const externalizeDeps = { name: "externalize-deps", setup(build$1) { build$1.onResolve({ filter: /.*/ }, ({ path: id }) => { if (id[0] !== "." && !path.isAbsolute(id)) return { external: true }; }); } }; const json5Loader = { name: "json5-loader", setup(build$1) { build$1.onLoad({ filter: /\.json5$/ }, async ({ path: path$1 }) => { const content = await promises.readFile(path$1, "utf-8"); return { contents: `export default ${JSON.stringify(JSON5.parse(content))}`, loader: "js" }; }); } }; const jsonLoader = { name: "json-loader", setup(build$1) { build$1.onLoad({ filter: /\.json$/ }, async ({ path: path$1 }) => { const content = await promises.readFile(path$1, "utf-8"); return { contents: `export default ${content}`, loader: "js" }; }); } }; const renamePlugin = { name: "rename-plugin", setup(build$1) { build$1.onResolve({ filter: /.*/ }, ({ path: id }) => { if (id === "vite-plugin-mock-dev-server") return { path: "vite-plugin-mock-dev-server/helper", external: true }; return null; }); } }; function aliasPlugin(alias) { return { name: "alias-plugin", setup(build$1) { build$1.onResolve({ filter: /.*/ }, async ({ path: id }) => { const matchedEntry = alias.find(({ find: find$1 }) => aliasMatches(find$1, id)); if (!matchedEntry) return null; const { find, replacement } = matchedEntry; const result = await build$1.resolve(id.replace(find, replacement), { kind: "import-statement", resolveDir: replacement, namespace: "file" }); return { path: result.path, external: false }; }); } }; } function aliasMatches(pattern, importee) { if (pattern instanceof RegExp) return pattern.test(importee); if (importee.length < pattern.length) return false; if (importee === pattern) return true; return importee.startsWith(`${pattern}/`); } async function transformWithEsbuild(entryPoint, options) { const { isESM = true, define, alias, cwd = process.cwd() } = options; const filepath = path.resolve(cwd, entryPoint); const filename = path.basename(entryPoint); const dirname = path.dirname(filepath); try { const result = await build({ entryPoints: [entryPoint], outfile: "out.js", write: false, target: ["node18"], platform: "node", bundle: true, metafile: true, format: isESM ? "esm" : "cjs", define: { ...define, __dirname: JSON.stringify(dirname), __filename: JSON.stringify(filename), ...isESM ? {} : { "import.meta.url": JSON.stringify(pathToFileURL(filepath)) } }, plugins: [ aliasPlugin(alias), renamePlugin, externalizeDeps, jsonLoader, json5Loader ], absWorkingDir: cwd }); return { code: result.outputFiles[0].text, deps: result.metafile?.inputs || {} }; } catch (e) { console.error(e); } return { code: "", deps: {} }; } async function loadFromCode({ filepath, code, isESM, cwd }) { filepath = path.resolve(cwd, filepath); const ext = isESM ? ".mjs" : ".cjs"; const filepathTmp = `${filepath}.timestamp-${Date.now()}${ext}`; const file = pathToFileURL(filepathTmp).toString(); await promises.writeFile(filepathTmp, code, "utf8"); try { const mod = await import(file); return mod.default || mod; } finally { try { fs.unlinkSync(filepathTmp); } catch {} } } //#endregion //#region src/core/build.ts async function generateMockServer(ctx, options) { const include = toArray(options.include); const exclude = toArray(options.exclude); const cwd = options.cwd || process.cwd(); let pkg = {}; try { const pkgStr = lookupFile(options.context, ["package.json"]); if (pkgStr) pkg = JSON.parse(pkgStr); } catch {} const outputDir = options.build.dist; const content = await generateMockEntryCode(cwd, include, exclude); const mockEntry = path.join(cwd, `mock-data-${Date.now()}.js`); await fsp.writeFile(mockEntry, content, "utf-8"); const { code, deps } = await transformWithEsbuild(mockEntry, options); const mockDeps = getMockDependencies(deps, options.alias); await fsp.unlink(mockEntry); const outputList = [ { filename: path.join(outputDir, "mock-data.js"), source: code }, { filename: path.join(outputDir, "index.js"), source: generatorServerEntryCode(options) }, { filename: path.join(outputDir, "package.json"), source: generatePackageJson(pkg, mockDeps) } ]; try { if (path.isAbsolute(outputDir)) { for (const { filename } of outputList) if (fs.existsSync(filename)) await fsp.rm(filename); options.logger.info(`${pc.green("✓")} generate mock server in ${pc.cyan(outputDir)}`); for (const { filename, source } of outputList) { fs.mkdirSync(path.dirname(filename), { recursive: true }); await fsp.writeFile(filename, source, "utf-8"); const sourceSize = (source.length / 1024).toFixed(2); const name = path.relative(outputDir, filename); const space = name.length < 30 ? " ".repeat(30 - name.length) : ""; options.logger.info(` ${pc.green(name)}${space}${pc.bold(pc.dim(`${sourceSize} kB`))}`); } } else for (const { filename, source } of outputList) ctx.emitFile({ type: "asset", fileName: filename, source }); } catch (e) { console.error(e); } } function getMockDependencies(deps, alias) { const list = /* @__PURE__ */ new Set(); const excludeDeps = [ "vite-plugin-mock-dev-server", "connect", "cors" ]; const isAlias = (p) => alias.find(({ find }) => aliasMatches(find, p)); Object.keys(deps).forEach((mPath) => { const imports = deps[mPath].imports.filter((_) => _.external && !_.path.startsWith("<define:") && !isAlias(_.path)).map((_) => _.path); imports.forEach((dep) => { const name = normalizePackageName(dep); if (!excludeDeps.includes(name) && !isCore(name)) list.add(name); }); }); return Array.from(list); } function normalizePackageName(dep) { const [scope, name] = dep.split("/"); if (scope[0] === "@") return `${scope}/${name}`; return scope; } function generatePackageJson(pkg, mockDeps) { const { dependencies = {}, devDependencies = {} } = pkg; const dependents = { ...dependencies, ...devDependencies }; const mockPkg = { name: "mock-server", type: "module", scripts: { start: "node index.js" }, dependencies: { connect: "^3.7.0", ["vite-plugin-mock-dev-server"]: `^1.9.2`, cors: "^2.8.5" }, pnpm: { peerDependencyRules: { ignoreMissing: ["vite"] } } }; mockDeps.forEach((dep) => { mockPkg.dependencies[dep] = dependents[dep] || "latest"; }); return JSON.stringify(mockPkg, null, 2); } function generatorServerEntryCode({ proxies, wsProxies, cookiesOptions, bodyParserOptions, priority, build: build$1 }) { const { serverPort, log } = build$1; return `import { createServer } from 'node:http'; import connect from 'connect'; import corsMiddleware from 'cors'; import { baseMiddleware, createLogger, mockWebSocket } from 'vite-plugin-mock-dev-server/server'; import mockData from './mock-data.js'; const app = connect(); const server = createServer(app); const logger = createLogger('mock-server', '${log}'); const proxies = ${JSON.stringify(proxies)}; const wsProxies = ${JSON.stringify(wsProxies)}; const cookiesOptions = ${JSON.stringify(cookiesOptions)}; const bodyParserOptions = ${JSON.stringify(bodyParserOptions)}; const priority = ${JSON.stringify(priority)}; const compiler = { mockData } mockWebSocket(compiler, server, { wsProxies, cookiesOptions, logger }); app.use(corsMiddleware()); app.use(baseMiddleware(compiler, { formidableOptions: { multiples: true }, proxies, priority, cookiesOptions, bodyParserOptions, logger, })); server.listen(${serverPort}); console.log('listen: http://localhost:${serverPort}'); `; } async function generateMockEntryCode(cwd, include, exclude) { const includePaths = await fg(include, { cwd }); const includeFilter = createFilter(include, exclude, { resolve: false }); const mockFiles = includePaths.filter(includeFilter); let importers = ""; const exporters = []; mockFiles.forEach((filepath, index) => { const file = normalizePath(path.join(cwd, filepath)); importers += `import * as m${index} from '${file}';\n`; exporters.push(`[m${index}, '${filepath}']`); }); return `import { transformMockData, transformRawData } from 'vite-plugin-mock-dev-server/server'; ${importers} const exporters = [\n ${exporters.join(",\n ")}\n]; const mockList = exporters.map(([mod, filepath]) => { const raw = mod.default || mod; return transformRawData(raw, filepath); }); export default transformMockData(mockList);`; } //#endregion //#region src/core/mockCompiler.ts function createMockCompiler(options) { return new MockCompiler(options); } /** * mock配置加载器 */ var MockCompiler = class extends EventEmitter { moduleCache = /* @__PURE__ */ new Map(); moduleDeps = /* @__PURE__ */ new Map(); cwd; mockWatcher; depsWatcher; moduleType = "cjs"; _mockData = {}; constructor(options) { super(); this.options = options; this.cwd = options.cwd || process.cwd(); try { const pkg = lookupFile(this.cwd, ["package.json"]); this.moduleType = !!pkg && JSON.parse(pkg).type === "module" ? "esm" : "cjs"; } catch {} } get mockData() { return this._mockData; } run(watch) { const { include, exclude } = this.options; /** * 使用 rollup 提供的 include/exclude 规则, * 过滤包含文件 */ const includeFilter = createFilter(include, exclude, { resolve: false }); fg(include, { cwd: this.cwd }).then((files) => files.filter(includeFilter).map((file) => () => this.loadMock(file))).then((loadList) => promiseParallel(loadList, 10)).then(() => this.updateMockList()); if (!watch) return; this.watchMockEntry(); this.watchDeps(); let timer = null; this.on("mock:update", async (filepath) => { if (!includeFilter(filepath)) return; await this.loadMock(filepath); if (timer) clearImmediate(timer); timer = setImmediate(() => { this.updateMockList(); this.emit("mock:update-end", filepath); timer = null; }); }); this.on("mock:unlink", async (filepath) => { if (!includeFilter(filepath)) return; this.moduleCache.delete(filepath); this.updateMockList(); this.emit("mock:update-end", filepath); }); } watchMockEntry() { const { include } = this.options; const [firstGlob, ...otherGlob] = toArray(include); const watcher = this.mockWatcher = chokidar.watch(firstGlob, { ignoreInitial: true, cwd: this.cwd }); if (otherGlob.length > 0) otherGlob.forEach((glob) => watcher.add(glob)); watcher.on("add", async (filepath) => { filepath = normalizePath(filepath); this.emit("mock:update", filepath); debug("watcher:add", filepath); }); watcher.on("change", async (filepath) => { filepath = normalizePath(filepath); this.emit("mock:update", filepath); debug("watcher:change", filepath); }); watcher.on("unlink", async (filepath) => { filepath = normalizePath(filepath); this.emit("mock:unlink", filepath); debug("watcher:unlink", filepath); }); } /** * 监听 mock文件依赖的本地文件变动, * mock依赖文件更新,mock文件也一并更新 */ watchDeps() { const oldDeps = []; this.depsWatcher = chokidar.watch([], { ignoreInitial: true, cwd: this.cwd }); this.depsWatcher.on("change", (filepath) => { filepath = normalizePath(filepath); const mockFiles = this.moduleDeps.get(filepath); mockFiles?.forEach((file) => { this.emit("mock:update", file); }); }); this.depsWatcher.on("unlink", (filepath) => { filepath = normalizePath(filepath); this.moduleDeps.delete(filepath); }); this.on("update:deps", () => { const deps = []; for (const [dep] of this.moduleDeps.entries()) deps.push(dep); const exactDeps = deps.filter((dep) => !oldDeps.includes(dep)); if (exactDeps.length > 0) this.depsWatcher.add(exactDeps); }); } close() { this.mockWatcher?.close(); this.depsWatcher?.close(); } updateMockList() { this._mockData = transformMockData(this.moduleCache); } updateModuleDeps(filepath, deps) { Object.keys(deps).forEach((mPath) => { const imports = deps[mPath].imports.map((_) => _.path); imports.forEach((dep) => { if (!this.moduleDeps.has(dep)) this.moduleDeps.set(dep, /* @__PURE__ */ new Set()); const cur = this.moduleDeps.get(dep); cur.add(filepath); }); }); this.emit("update:deps"); } async loadMock(filepath) { if (!filepath) return; let isESM = false; if (/\.m[jt]s$/.test(filepath)) isESM = true; else if (/\.c[jt]s$/.test(filepath)) isESM = false; else isESM = this.moduleType === "esm"; const { define, alias } = this.options; const { code, deps } = await transformWithEsbuild(filepath, { isESM, define, alias, cwd: this.cwd }); try { const raw = await loadFromCode({ filepath, code, isESM, cwd: this.cwd }) || {}; this.moduleCache.set(filepath, transformRawData(raw, filepath)); this.updateModuleDeps(filepath, deps); } catch (e) { console.error(e); } } }; //#endregion //#region src/core/mockMiddleware.ts function mockServerMiddleware(options, server, ws) { /** * 加载 mock 文件, 包括监听 mock 文件的依赖文件变化, * 并注入 vite `define` / `alias` */ const compiler = createMockCompiler(options); compiler.run(!!server); /** * 监听 mock 文件是否发生变更,如何配置了 reload 为 true, * 当发生变更时,通知当前页面进行重新加载 */ compiler.on("mock:update-end", () => { if (options.reload) ws?.send({ type: "full-reload" }); }); server?.on("close", () => compiler.close()); /** * 虽然 config.server.proxy 中有关于 ws 的代理配置, * 但是由于 vite 内部在启动时,直接对 ws相关的请求,通过 upgrade 事件,发送给 http-proxy * 的 ws 代理方法。如果插件直接使用 config.server.proxy 中的 ws 配置, * 就会导致两次 upgrade 事件 对 wss 实例的冲突。 * 由于 vite 内部并没有提供其他的方式跳过 内部 upgrade 的方式,(个人认为也没有必要提供此类方式) * 所以插件选择了通过插件的配置项 `wsPrefix` 来做 判断的首要条件。 * 当前插件默认会将已配置在 wsPrefix 的值,从 config.server.proxy 的删除,避免发生冲突问题。 */ mockWebSocket(compiler, server, options); const middlewares = []; middlewares.push( /** * 在 vite 的开发服务中,由于插件 的 enforce 为 `pre`, * mock 中间件的执行顺序 早于 vite 内部的 cors 中间件执行, * 这导致了 vite 默认开启的 cors 对 mock 请求不生效。 * 在一些比如 微前端项目、或者联合项目中,会由于端口不一致而导致跨域问题。 * 所以在这里,使用 cors 中间件 来解决这个问题。 * * 同时为了使 插件内的 cors 和 vite 的 cors 不产生冲突,并拥有一致的默认行为, * 也会使用 viteConfig.server.cors 配置,并支持 用户可以对 mock 中的 cors 中间件进行配置。 * 而用户的配置也仅对 mock 的接口生效。 */ corsMiddleware(compiler, options), baseMiddleware(compiler, options) ); return middlewares.filter(Boolean); } function corsMiddleware(compiler, { proxies, cors: corsOptions }) { return !corsOptions ? void 0 : function(req, res, next) { const { pathname } = urlParse(req.url); if (!pathname || proxies.length === 0 || !proxies.some((context) => doesProxyContextMatchUrl(context, req.url))) return next(); const mockData = compiler.mockData; const mockUrl = Object.keys(mockData).find((key) => pathToRegexp(key).test(pathname)); if (!mockUrl) return next(); cors(corsOptions)(req, res, next); }; } //#endregion //#region src/core/define.ts function viteDefine(config) { const processNodeEnv = {}; const nodeEnv = process.env.NODE_ENV || config.mode; Object.assign(processNodeEnv, { "process.env.NODE_ENV": JSON.stringify(nodeEnv), "global.process.env.NODE_ENV": JSON.stringify(nodeEnv), "globalThis.process.env.NODE_ENV": JSON.stringify(nodeEnv) }); const userDefine = {}; const userDefineEnv = {}; for (const key in config.define) { const val = config.define[key]; const isMetaEnv = key.startsWith("import.meta.env."); if (typeof val === "string") { if (canJsonParse(val)) { userDefine[key] = val; if (isMetaEnv) userDefineEnv[key.slice(16)] = val; } } else { userDefine[key] = handleDefineValue(val); if (isMetaEnv) userDefineEnv[key.slice(16)] = val; } } const importMetaKeys = {}; const importMetaEnvKeys = {}; const importMetaFallbackKeys = {}; importMetaKeys["import.meta.hot"] = `undefined`; for (const key in config.env) { const val = JSON.stringify(config.env[key]); importMetaKeys[`import.meta.env.${key}`] = val; importMetaEnvKeys[key] = val; } importMetaFallbackKeys["import.meta.env"] = `undefined`; const define = { ...processNodeEnv, ...importMetaKeys, ...userDefine, ...importMetaFallbackKeys }; if ("import.meta.env" in define) define["import.meta.env"] = serializeDefine({ ...importMetaEnvKeys, ...userDefineEnv }); return define; } /** * Like `JSON.stringify` but keeps raw string values as a literal * in the generated code. For example: `"window"` would refer to * the global `window` object directly. */ function serializeDefine(define) { let res = `{`; const keys = Object.keys(define); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const val = define[key]; res += `${JSON.stringify(key)}: ${handleDefineValue(val)}`; if (i !== keys.length - 1) res += `, `; } return `${res}}`; } function handleDefineValue(value) { if (typeof value === "undefined") return "undefined"; if (typeof value === "string") return value; return JSON.stringify(value); } function canJsonParse(value) { try { JSON.parse(value); return true; } catch { return false; } } //#endregion //#region src/core/resolvePluginOptions.ts function resolvePluginOptions({ prefix = [], wsPrefix = [], cwd, include = ["mock/**/*.mock.{js,ts,cjs,mjs,json,json5}"], exclude = [ "**/node_modules/**", "**/.vscode/**", "**/.git/**" ], reload = false, log = "info", cors: cors$1 = true, formidableOptions = {}, build: build$1 = false, cookiesOptions = {}, bodyParserOptions = {}, priority = {} }, config) { const logger = createLogger("vite:mock", isBoolean(log) ? log ? "info" : "error" : log); const { httpProxies } = ensureProxies(config.server.proxy || {}); const proxies = uniq([...toArray(prefix), ...httpProxies]); const wsProxies = toArray(wsPrefix); if (!proxies.length && !wsProxies.length) logger.warn(`No proxy was configured, mock server will not work. See ${pc.cyan("https://vite-plugin-mock-dev-server.netlify.app/guide/usage")}`); const enabled = cors$1 === false ? false : config.server.cors !== false; let corsOptions = {}; if (enabled && config.server.cors !== false) corsOptions = { ...corsOptions, ...typeof config.server.cors === "boolean" ? {} : config.server.cors }; if (enabled && cors$1 !== false) corsOptions = { ...corsOptions, ...typeof cors$1 === "boolean" ? {} : cors$1 }; const alias = []; const aliasConfig = config.resolve.alias || []; if (isArray(aliasConfig)) alias.push(...aliasConfig); else Object.entries(aliasConfig).forEach(([find, replacement]) => { alias.push({ find, replacement }); }); return { cwd: cwd || process.cwd(), include, exclude, context: config.root, reload, cors: enabled ? corsOptions : false, cookiesOptions, log, formidableOptions: { multiples: true, ...formidableOptions }, bodyParserOptions, priority, build: build$1 ? Object.assign({ serverPort: 8080, dist: "mockServer", log: "error" }, typeof build$1 === "object" ? build$1 : {}) : false, proxies, wsProxies, logger, alias, define: viteDefine(config) }; } //#endregion //#region src/plugin.ts function mockDevServerPlugin(options = {}) { const plugins = [serverPlugin(options)]; if (options.build) plugins.push(buildPlugin(options)); return plugins; } function buildPlugin(options) { let viteConfig = {}; let resolvedOptions; return { name: "vite-plugin-mock-dev-server-generator", enforce: "post", apply: "build", configResolved(config) { viteConfig = config; resolvedOptions = resolvePluginOptions(options, config); config.logger.warn(""); }, async buildEnd(error) { if (error || viteConfig.command !== "build") return; await generateMockServer(this, resolvedOptions); } }; } function serverPlugin(options) { let resolvedOptions; return { name: "vite-plugin-mock-dev-server", enforce: "pre", apply: "serve", config(config) { const wsPrefix = toArray(options.wsPrefix); if (wsPrefix.length && config.server?.proxy) { const proxy = {}; Object.keys(config.server.proxy).forEach((key) => { if (!wsPrefix.includes(key)) proxy[key] = config.server.proxy[key]; }); config.server.proxy = proxy; } recoverRequest(config); }, configResolved(config) { resolvedOptions = resolvePluginOptions(options, config); config.logger.warn(""); }, configureServer({ middlewares, httpServer, ws }) { const middlewareList = mockServerMiddleware(resolvedOptions, httpServer, ws); middlewareList.forEach((middleware) => middlewares.use(middleware)); }, configurePreviewServer({ middlewares, httpServer }) { const middlewareList = mockServerMiddleware(resolvedOptions, httpServer); middlewareList.forEach((middleware) => middlewares.use(middleware)); } }; } //#endregion //#region src/index.ts /** * @deprecated use named export instead */ function mockDevServerPluginWithDefaultExportWasDeprecated(options = {}) { console.warn(`${pc.yellow("[vite-plugin-mock-dev-server]")} ${pc.yellow(pc.bold("WARNING:"))} The plugin default export is ${pc.bold("deprecated")}, it will be removed in next major version, use ${pc.bold("named export")} instead:\n\n ${pc.green("import { mockDevServerPlugin } from \"vite-plugin-mock-dev-server\"")}\n`); return mockDevServerPlugin(options); } //#endregion export { baseMiddleware, createDefineMock, createLogger, createSSEStream, mockDevServerPluginWithDefaultExportWasDeprecated as default, defineMock, defineMockData, logLevels, mockDevServerPlugin, mockWebSocket, sortByValidator, transformMockData, transformRawData };