zomoc
Version:
A type-safe API mocking tool for frontend development, powered by axios and Zod.
377 lines (371 loc) • 15.4 kB
JavaScript
;
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: "*"
});
}
}
}
};
}