UNPKG

zomoc

Version:

A type-safe API mocking tool for frontend development, powered by axios and Zod.

377 lines (371 loc) 15.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/vite.ts var vite_exports = {}; __export(vite_exports, { default: () => zomoc }); module.exports = __toCommonJS(vite_exports); // src/core.ts var import_glob = require("glob"); var import_ts_to_zod = require("ts-to-zod"); var import_promises = __toESM(require("fs/promises"), 1); var import_camelcase = __toESM(require("camelcase"), 1); async function createInterfaceIndex(projectRoot, interfacePaths) { const interfaceLocationMap = /* @__PURE__ */ new Map(); const files = await (0, import_glob.glob)(interfacePaths, { cwd: projectRoot, absolute: true, ignore: "node_modules/**" }); const interfaceRegex = /export\s+(?:interface|type)\s+([A-Za-z0-9_]+)/g; for (const file of files) { try { const content = await import_promises.default.readFile(file, "utf-8"); let match; while ((match = interfaceRegex.exec(content)) !== null) { if (interfaceLocationMap.has(match[1])) { console.warn( `[Zomoc] Warning: Duplicate interface name "${match[1]}" found in ${file}. The one in ${interfaceLocationMap.get( match[1] )} will be used.` ); } else { interfaceLocationMap.set(match[1], file); } } } catch (e) { console.warn(`[Zomoc] Failed to read or parse ${file}`, e); } } return interfaceLocationMap; } async function generateSurvivedSchemas(interfaceLocationMap) { const survivedSchemaDeclarations = /* @__PURE__ */ new Map(); const uniqueFilePaths = [...new Set(interfaceLocationMap.values())]; for (const filePath of uniqueFilePaths) { try { const sourceText = await import_promises.default.readFile(filePath, "utf-8"); let sanitizedSourceText = sourceText; const enumRegex = /export (?:const\s+)?enum\s+(\w+)\s*{([\s\S]+?)}/g; sanitizedSourceText = sanitizedSourceText.replace( enumRegex, (_match, enumName, enumBody) => { const enumMemberRegex = /['"]([^'"]+)['"]/g; const enumMembers = []; let memberMatch; while ((memberMatch = enumMemberRegex.exec(enumBody)) !== null) { enumMembers.push(`'${memberMatch[1]}'`); } if (enumMembers.length === 0) { return `/* Zomoc: Non-string enum '${enumName}' is not supported and has been commented out. */`; } return `export type ${enumName} = ${enumMembers.join(" | ")};`; } ); const genericTypeRegex = /export\s+(interface|type)\s+\w+<.+?>\s*=.+?;|export\s+(interface|type)\s+\w+<.+?>\s*{[\s\S]*?}/gs; sanitizedSourceText = sanitizedSourceText.replace( genericTypeRegex, (match) => `/* Zomoc: Generic type commented out ${match} */` ); const zodSchemaFileContent = (0, import_ts_to_zod.generate)({ sourceText: sanitizedSourceText }).getZodSchemasFile(""); const sanitizedContent = zodSchemaFileContent.replace( /z\.literal\(React\.\w+\)/g, "z.any()" ); const declarations = sanitizedContent.split(/\n(?=export const)/g); for (const declaration of declarations) { const match = declaration.match(/export const (\w+Schema) =/); if (match && match[1]) { const schemaName = match[1]; const trimmedDeclaration = declaration.trim(); if (trimmedDeclaration && !survivedSchemaDeclarations.has(schemaName)) { survivedSchemaDeclarations.set(schemaName, trimmedDeclaration); } } } } catch (error) { console.warn( `[Zomoc] Warning: Skipped processing file '${filePath}' due to an error.`, error.message ); } } return survivedSchemaDeclarations; } async function buildInterfaceToSchemaNameMap(filePaths) { const map = /* @__PURE__ */ new Map(); const interfaceRegex = /\bexport\s+(?:interface|type)\s+([A-Z]\w*)/g; for (const filePath of filePaths) { try { const content = await import_promises.default.readFile(filePath, "utf-8"); let match; while ((match = interfaceRegex.exec(content)) !== null) { const interfaceName = match[1]; const schemaName = (0, import_camelcase.default)(interfaceName, { preserveConsecutiveUppercase: true }) + "Schema"; if (!map.has(interfaceName)) { map.set(interfaceName, schemaName); } } } catch (e) { } } return map; } async function generateRegistryString(projectRoot, options) { const interfacePaths = options?.interfacePaths || []; const interfaceLocationMap = await createInterfaceIndex( projectRoot, interfacePaths ); const survivedSchemaDeclarations = await generateSurvivedSchemas(interfaceLocationMap); const interfaceToSchemaNameMap = await buildInterfaceToSchemaNameMap([ ...interfaceLocationMap.values() ]); const schemaNameToInterfaceNameMap = /* @__PURE__ */ new Map(); for (const [iName, sName] of interfaceToSchemaNameMap.entries()) { schemaNameToInterfaceNameMap.set(sName, iName); } const allInterfaceNames = new Set(interfaceLocationMap.keys()); const allGeneratedSchemaNames = new Set(survivedSchemaDeclarations.keys()); const allExpectedSchemaNames = new Set(interfaceToSchemaNameMap.values()); const failedInterfaceNames = []; for (const interfaceName of allInterfaceNames) { const schemaName = `${(0, import_camelcase.default)(interfaceName, { preserveConsecutiveUppercase: true })}Schema`; if (!allGeneratedSchemaNames.has(schemaName)) { failedInterfaceNames.push(interfaceName); } } if (failedInterfaceNames.length > 0) { console.warn( `\x1B[33m[Zomoc] The following types were skipped (often due to unsupported generics):\x1B[0m` ); console.warn(failedInterfaceNames.map((name) => ` - ${name}`).join("\n")); } const typeMapEntries = []; for (const schemaName of survivedSchemaDeclarations.keys()) { const interfaceName = schemaNameToInterfaceNameMap.get(schemaName); if (interfaceName) { typeMapEntries.push(`'${interfaceName}': ${schemaName},`); } } const urlMapEntries = []; const mockPaths = options?.mockPaths || []; const mockFiles = await (0, import_glob.glob)(mockPaths, { cwd: projectRoot, absolute: true, ignore: "node_modules/**" }); if (mockFiles.length > 0) { for (const mockFile of mockFiles) { try { const mockContent = await import_promises.default.readFile(mockFile, "utf-8"); const mockMap = JSON.parse(mockContent); for (const [key, value] of Object.entries(mockMap)) { const mockConfig = value; if (typeof mockConfig === "object" && mockConfig !== null && "responses" in mockConfig) { const responseMap = mockConfig; const activeStatusKey = String(responseMap.status); let activeResponse = responseMap.responses[activeStatusKey]; let finalStatus = responseMap.status; if (!activeResponse) { const fallbackStatusKey = Object.keys(responseMap.responses)[0]; if (fallbackStatusKey) { activeResponse = responseMap.responses[fallbackStatusKey]; finalStatus = Number(fallbackStatusKey); console.warn( `[Zomoc] Warning: status ${activeStatusKey} not found in responses for '${key}'. Falling back to the first available status: ${finalStatus}.` ); } } if (!activeResponse) continue; const schemaName = activeResponse.responseType ? `${(0, import_camelcase.default)(activeResponse.responseType, { preserveConsecutiveUppercase: true })}Schema` : void 0; if (schemaName && !survivedSchemaDeclarations.has(schemaName)) { continue; } const registryValueObject = `{ schema: ${schemaName || "undefined"}, responseBody: ${activeResponse.responseBody ? JSON.stringify(activeResponse.responseBody) : "undefined"}, status: ${finalStatus}, pagination: ${activeResponse.pagination ? JSON.stringify(activeResponse.pagination) : "undefined"}, strategy: '${activeResponse.mockingStrategy || "random"}', repeatCount: ${activeResponse.repeatCount ?? "undefined"} }`; const urlEntry = `'${key}': ${registryValueObject},`; urlMapEntries.push(urlEntry); } else { let responseDef = {}; if (typeof mockConfig === "string") { responseDef = { responseType: mockConfig, status: 200 }; } else if (typeof mockConfig === "object" && mockConfig !== null) { const { responseBody, ...rest } = mockConfig; if (responseBody) { console.warn( `[Zomoc] Warning: 'responseBody' is not supported in simple mode for '${key}'. Please use the 'responses' map structure.` ); } responseDef = { ...rest, status: rest.status || 200 }; } else { continue; } const schemaName = responseDef.responseType ? `${(0, import_camelcase.default)(responseDef.responseType, { preserveConsecutiveUppercase: true })}Schema` : void 0; if (schemaName && !survivedSchemaDeclarations.has(schemaName)) { continue; } const registryValueObject = `{ schema: ${schemaName || "undefined"}, status: ${responseDef.status}, pagination: ${responseDef.pagination ? JSON.stringify(responseDef.pagination) : "undefined"}, strategy: '${responseDef.mockingStrategy || "random"}', repeatCount: ${responseDef.repeatCount ?? "undefined"} }`; const urlEntry = `'${key}': ${registryValueObject},`; urlMapEntries.push(urlEntry); } } } catch (e) { console.error(`[Zomoc] Error processing mock file ${mockFile}:`, e); } } } let finalRegistryString = `// Zomoc: Auto-generated mock registry. Do not edit. import { z } from 'zod'; `; finalRegistryString += [...survivedSchemaDeclarations.values()].join("\n"); const urlSchemaEntries = urlMapEntries.join("\n"); const typeSchemaEntries = typeMapEntries.join("\n"); finalRegistryString += ` export const finalSchemaUrlMap = { ${urlSchemaEntries} } as const; `; finalRegistryString += ` export const finalSchemaTypeMap = { ${typeSchemaEntries} } as const; `; return finalRegistryString; } async function generateViteVirtualModule(projectRoot, options) { const registryString = await generateRegistryString(projectRoot, options); return registryString.replace(/ as const/g, ""); } // src/vite.ts var import_micromatch = __toESM(require("micromatch"), 1); var VIRTUAL_MODULE_ID = "virtual:zomoc"; var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`; function zomoc(options = {}) { const { mockPaths = ["**/mock.json"], interfacePaths = ["**/interface.ts", "**/type.ts"], ...restOptions } = options; const finalOptions = { mockPaths, interfacePaths, ...restOptions }; return { name: "zomoc-vite-plugin", /** * Vite hook to handle resolution of the virtual module ID. * When Vite encounters `import ... from 'virtual:zomoc'`, this hook intercepts it * and returns a resolved ID, signaling that this plugin will handle loading it. * @description 가상 모듈 ID의 resolve를 처리하는 Vite 훅입니다. * Vite가 `import ... from 'virtual:zomoc'` 구문을 만나면, 이 훅이 해당 요청을 가로채 * resolve된 ID를 반환함으로써, 이 플러그인이 모듈 로딩을 처리할 것임을 알립니다. */ resolveId(id) { if (id === VIRTUAL_MODULE_ID) { return RESOLVED_VIRTUAL_MODULE_ID; } return null; }, /** * Vite hook to provide the content of the virtual module. * When the resolved virtual module ID is requested, this hook runs the Zomoc core engine * (`generateViteVirtualModule`) to generate the registry code on-the-fly. * @description 가상 모듈의 내용을 제공하는 Vite 훅입니다. * resolve된 가상 모듈 ID가 요청되면, 이 훅은 Zomoc 코어 엔진(`generateViteVirtualModule`)을 실행하여 * 레지스트리 코드를 동적으로 생성합니다. */ async load(id) { if (id === RESOLVED_VIRTUAL_MODULE_ID) { const projectRoot = process.cwd(); const registryString = await generateViteVirtualModule(projectRoot, { mockPaths: finalOptions.mockPaths, interfacePaths: finalOptions.interfacePaths }); return registryString; } return null; }, /** * Vite hook to handle Hot Module Replacement (HMR). * It watches for changes in any of the user-specified mock or interface files. * If a change is detected, it invalidates the virtual module, forcing Vite * to re-request it, and triggers a full page reload. * @description HMR(Hot Module Replacement)을 처리하는 Vite 훅입니다. * 사용자가 지정한 mock 또는 인터페이스 파일의 변경을 감시합니다. * 변경이 감지되면, 가상 모듈을 무효화하여 Vite가 모듈을 다시 요청하게 만들고, * 전체 페이지를 새로고침하도록 합니다. */ async handleHotUpdate({ file, server }) { const isMockFile = import_micromatch.default.isMatch(file, finalOptions.mockPaths); const isInterfaceFile = import_micromatch.default.isMatch( file, finalOptions.interfacePaths ); if (isMockFile || isInterfaceFile) { const module2 = server.moduleGraph.getModuleById( RESOLVED_VIRTUAL_MODULE_ID ); if (module2) { server.moduleGraph.invalidateModule(module2); server.ws.send({ type: "full-reload", path: "*" }); } } } }; }