msw-mock-gen
Version:
A Vite plugin that automatically generates MSW (Mock Service Worker) handlers from your API calls by watching TypeScript/JavaScript files and parsing URL patterns. Supports Vite 2+ and Node.js 12+ for maximum compatibility.
294 lines (291 loc) • 12.7 kB
JavaScript
/**
* Parses TypeScript/JavaScript files to extract API endpoint URLs
*
* This function analyzes source code to find URL patterns that represent API endpoints.
* It uses regex patterns to match URLs in various contexts like string literals, object properties,
* and array elements. The function also determines if the file is a query or mutation based on
* the filename and attempts to infer the HTTP method from the context.
*
* @param content - The source code content to parse
* @param filename - The name of the file being parsed (used to determine query vs mutation)
* @param excludePatterns - Array of patterns to exclude from URL detection
* @returns Array of parsed URLs with metadata
*
* @example
* ```typescript
* const urls = parseURLsFromFile(
* 'fetch("/api/users")',
* 'useUsersQuery.ts',
* ['navigate({']
* );
* // Returns: [{ path: '/api/users', method: 'get', source: 'useUsersQuery.ts', line: 1, type: 'query' }]
* ```
*/
export function parseURLsFromFile(content, filename, excludePatterns = []) {
const urls = [];
const lines = content.split("\n");
// Enhanced regex to match URL patterns in various contexts
const urlPatterns = [
// String literals with URLs
/['"`]([\/][^'"`]+)['"`]/g,
// Object property assignments
/:\s*['"`]([\/][^'"`]+)['"`]/g,
// Array elements
/['"`]([\/][^'"`]+)['"`],?/g,
];
// Determine if this is a query or mutation file based on filename
const isQuery = filename.includes("Query") || filename.includes("query");
const isMutation = filename.includes("Mutation") || filename.includes("mutation");
lines.forEach((line, index) => {
// Skip comments
if (line.trim().startsWith("//") || line.trim().startsWith("/*")) {
return;
}
// Check if this line should be excluded based on patterns
const shouldExcludeLine = excludePatterns.some((pattern) => line.includes(pattern));
if (shouldExcludeLine) {
return;
}
urlPatterns.forEach((pattern) => {
let match;
while ((match = pattern.exec(line)) !== null) {
const url = match[1];
// Include API-like paths and simple endpoint paths
if (url.startsWith("/api/") ||
url.startsWith("/v1/") ||
url.startsWith("/v2/") ||
(url.startsWith("/") && url.length > 1 && !url.includes("."))) {
// Check if this URL is already added
const exists = urls.some((existing) => existing.path === url);
if (!exists) {
console.log(`DEBUG: Found URL in line ${index + 1}: ${url}`);
// Determine HTTP method based on context
let method = "get"; // Default for queries
if (isMutation) {
// Try to determine method from context
const lowerLine = line.toLowerCase();
if (lowerLine.includes("post") ||
lowerLine.includes("create") ||
lowerLine.includes("add")) {
method = "post";
}
else if (lowerLine.includes("put") ||
lowerLine.includes("update")) {
method = "put";
}
else if (lowerLine.includes("delete") ||
lowerLine.includes("remove")) {
method = "delete";
}
else if (lowerLine.includes("patch")) {
method = "patch";
}
else {
method = "post"; // Default for mutations
}
}
urls.push({
path: url,
method,
source: filename,
line: index + 1,
type: isQuery ? "query" : isMutation ? "mutation" : "unknown",
});
}
}
}
});
});
return urls;
}
/**
* Generates MSW handler code from parsed URLs
*
* This function takes an array of parsed URLs and generates the corresponding MSW handler code.
* It separates URLs by type (query, mutation, unknown) and generates appropriate HTTP handlers
* for each endpoint. The generated code includes proper imports and exports for use with MSW.
*
* @param urls - Array of parsed URLs to generate handlers for
* @returns Object containing generated handler code for queries, mutations, and index file
*
* @example
* ```typescript
* const handlers = generateMSWHandlers([
* { path: '/api/users', method: 'get', source: 'file.ts', type: 'query' },
* { path: '/api/users', method: 'post', source: 'file.ts', type: 'mutation' }
* ]);
*
* // Returns:
* // {
* // queryHandlers: "import { http, HttpResponse } from 'msw';...",
* // mutationHandlers: "import { http, HttpResponse } from 'msw';...",
* // indexFile: "import { queryHandlers } from './queryHandlers.generated';..."
* // }
* ```
*/
export function generateMSWHandlers(urls, mockDataFiles = []) {
if (urls.length === 0) {
return {
queryHandlers: `export const queryHandlers = [];`,
mutationHandlers: `export const mutationHandlers = [];`,
indexFile: `import { queryHandlers } from './queryHandlers.generated';
import { mutationHandlers } from './mutationHandlers.generated';
export const handlers = [
...queryHandlers,
...mutationHandlers
];`,
};
}
// Separate URLs by type
const queryUrls = urls.filter((url) => url.type === "query");
const mutationUrls = urls.filter((url) => url.type === "mutation");
const unknownUrls = urls.filter((url) => url.type === "unknown");
// Deduplicate URLs by path within each category
const uniqueQueryUrls = queryUrls.filter((url, index, self) => index === self.findIndex((u) => u.path === url.path));
const uniqueMutationUrls = mutationUrls.filter((url, index, self) => index === self.findIndex((u) => u.path === url.path));
const uniqueUnknownUrls = unknownUrls.filter((url, index, self) => index === self.findIndex((u) => u.path === url.path));
// Helper function to convert hook name to mock data variable name
const getMockDataVarName = (hookName) => {
const baseName = hookName.replace(/^use/, "mock");
return baseName.charAt(0).toLowerCase() + baseName.slice(1) + "Data";
};
// Helper function to find mock data for a URL
const findMockDataForUrl = (url) => {
// Try to find a mock data file that matches this URL pattern
// This is a simple heuristic - we'll match based on the URL path
return mockDataFiles.find((mockData) => {
// For now, we'll use a simple approach: if the URL path contains keywords
// that might match the hook name, we'll use that mock data
const urlPath = url.path.toLowerCase();
const hookName = mockData.hookName.toLowerCase();
// Check if URL path contains parts of the hook name
const hookParts = hookName
.replace(/use|query|mutation/g, "")
.split(/(?=[A-Z])/);
return hookParts.some((part) => urlPath.includes(part.toLowerCase()));
});
};
// Generate query handlers
const queryHandlers = uniqueQueryUrls.map((url) => {
const method = url.method || "get";
const mockData = findMockDataForUrl(url);
if (mockData) {
// Use the mock data variable name
const mockDataVarName = getMockDataVarName(mockData.hookName);
return ` http.${method}('${url.path}', () => {
return HttpResponse.json(${mockDataVarName});
})`;
}
else {
return ` http.${method}('${url.path}', () => {
return HttpResponse.json({
message: 'Mock response for ${url.path}',
timestamp: new Date().toISOString()
});
})`;
}
});
// Generate mutation handlers
const mutationHandlers = uniqueMutationUrls.map((url) => {
const method = url.method || "post";
const mockData = findMockDataForUrl(url);
if (mockData) {
// Use the mock data variable name
const mockDataVarName = getMockDataVarName(mockData.hookName);
return ` http.${method}('${url.path}', () => {
return HttpResponse.json(${mockDataVarName});
})`;
}
else {
return ` http.${method}('${url.path}', () => {
return HttpResponse.json({
message: 'Mock response for ${url.path}',
timestamp: new Date().toISOString()
});
})`;
}
});
// Generate unknown handlers (treat as queries)
const unknownHandlers = uniqueUnknownUrls.map((url) => {
const method = url.method || "get";
const mockData = findMockDataForUrl(url);
if (mockData) {
// Use the mock data variable name
const mockDataVarName = getMockDataVarName(mockData.hookName);
return ` http.${method}('${url.path}', () => {
return HttpResponse.json(${mockDataVarName});
})`;
}
else {
return ` http.${method}('${url.path}', () => {
return HttpResponse.json({
message: 'Mock response for ${url.path}',
timestamp: new Date().toISOString()
});
})`;
}
});
// Collect all unique mock data imports with their variable names
const mockDataImports = new Map();
// Add imports for all mock data files
mockDataFiles.forEach((mockData) => {
const mockDataVarName = getMockDataVarName(mockData.hookName);
// Calculate the full path from the project root to the mock data file
// Extract the path relative to the project root (everything after the project root)
const projectRootIndex = mockData.mockDataFilePath.indexOf("/src/");
const fullPath = mockData.mockDataFilePath.substring(projectRootIndex + 1); // Remove leading slash
const importPath = fullPath.replace(".ts", "");
mockDataImports.set(mockDataVarName, importPath);
});
// Helper to extract used mock data variable names from handler code
function extractUsedMockVars(handlers) {
const used = new Set();
const regex = /HttpResponse\.json\((\w+)\)/g;
for (const handler of handlers) {
let match;
while ((match = regex.exec(handler))) {
used.add(match[1]);
}
}
return used;
}
const usedQueryVars = extractUsedMockVars([
...queryHandlers,
...unknownHandlers,
]);
const usedMutationVars = extractUsedMockVars(mutationHandlers);
// Generate import statements for mock data, filtered by type
const queryMockDataImports = Array.from(mockDataImports.entries())
.filter(([varName]) => usedQueryVars.has(varName))
.map(([varName, importPath]) => `import { ${varName} } from '${importPath}';`)
.join("\n");
const mutationMockDataImports = Array.from(mockDataImports.entries())
.filter(([varName]) => usedMutationVars.has(varName))
.map(([varName, importPath]) => `import { ${varName} } from '${importPath}';`)
.join("\n");
const queryHandlersCode = queryHandlers.concat(unknownHandlers).length > 0
? `import { http, HttpResponse } from 'msw';
${queryMockDataImports ? `\n${queryMockDataImports}\n` : ""}
export const queryHandlers = [
${queryHandlers.concat(unknownHandlers).join(",\n")}
];`
: `export const queryHandlers = [];`;
const mutationHandlersCode = mutationHandlers.length > 0
? `import { http, HttpResponse } from 'msw';
${mutationMockDataImports ? `\n${mutationMockDataImports}\n` : ""}
export const mutationHandlers = [
${mutationHandlers.join(",\n")}
];`
: `export const mutationHandlers = [];`;
const indexFileCode = `import { queryHandlers } from './queryHandlers.generated';
import { mutationHandlers } from './mutationHandlers.generated';
export const handlers = [
...queryHandlers,
...mutationHandlers
];`;
return {
queryHandlers: queryHandlersCode,
mutationHandlers: mutationHandlersCode,
indexFile: indexFileCode,
};
}