next-ts-api
Version:
A powerful TypeScript-first API client generator for Next.js applications with full type safety and automatic api type generation.
121 lines (120 loc) • 5.27 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.findRouteFiles = findRouteFiles;
exports.generateTypeDefinitions = generateTypeDefinitions;
exports.writeNextTsApi = writeNextTsApi;
const tslib_1 = require("tslib");
const fs_1 = tslib_1.__importStar(require("fs"));
const path_1 = tslib_1.__importStar(require("path"));
const logger_1 = require("./logger");
const utils_1 = require("./utils");
function getRelativePath(from, to) {
const relativePath = path_1.default.relative(path_1.default.dirname(from), to)
.replace(/\\/g, '/') // Convert Windows paths to Unix
.replace(/\.ts$/, '');
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
}
function findRouteFiles(dir, routes = [], basePath = []) {
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path_1.default.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip special directories
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
continue;
findRouteFiles(fullPath, routes, [...basePath, entry.name]);
}
else if (entry.name === 'route.ts') {
const fileContent = fs_1.default.readFileSync(fullPath, 'utf-8');
const methods = {};
// Regex to find exported HTTP method handlers (both function and const)
const exportRegex = /export\s+(?:(?:async\s+)?function|const)\s+(GET|POST|PUT|DELETE|PATCH)/g;
let match;
while ((match = exportRegex.exec(fileContent)) !== null) {
const method = match[1];
methods[method] = {
importPath: fullPath,
exportName: method,
};
}
if (Object.keys(methods).length > 0) {
routes.push({
path: basePath.join('/'),
methods,
});
}
}
}
return routes;
}
function generateTypeDefinitions(routes, options) {
let imports = 'import type { ExtractNextBody, ExtractNextQuery, ExtractNextResponse, ExtractNextParams } from \'next-ts-api\';\n';
let importCounter = 1;
const importMap = new Map();
// Collect imports
routes.forEach(route => {
Object.entries(route.methods).forEach(([method, info]) => {
const importPath = getRelativePath((0, path_1.join)(options.outDir, options.outFile), info.importPath);
if (!importMap.has(importPath)) {
importMap.set(importPath, { counter: importCounter++, methods: new Set([method]) });
}
else {
importMap.get(importPath).methods.add(method);
}
});
});
// Generate import statements
importMap.forEach((value, importPath) => {
const methods = Array.from(value.methods)
.map(method => `${method} as ${method}_${value.counter}`)
.join(', ');
imports += `import type { ${methods} } from '${importPath}';\n`;
});
// Generate type definition
let typeDefinition = '\nexport type ApiRoutes = {\n';
routes.forEach(route => {
const routePath = route.path || 'root';
typeDefinition += ` '${routePath}': {\n`;
Object.entries(route.methods).forEach(([method, info]) => {
const importPath = getRelativePath((0, path_1.join)(options.outDir, options.outFile), info.importPath);
const counter = importMap.get(importPath).counter;
typeDefinition += ` ${method}: {\n`;
if (!['GET', 'DELETE'].includes(method)) {
typeDefinition += ` body: ExtractNextBody<typeof ${method}_${counter}>\n`;
}
typeDefinition += ` response: ExtractNextResponse<typeof ${method}_${counter}>\n`;
typeDefinition += ` query: ExtractNextQuery<typeof ${method}_${counter}>\n`;
typeDefinition += ` params: ExtractNextParams<typeof ${method}_${counter}>\n`;
typeDefinition += ` },\n`;
});
typeDefinition += ` };\n`;
});
typeDefinition += '};\n';
return imports + typeDefinition;
}
function writeNextTsApi(options) {
const PROJECT_ROOT = process.cwd();
const API_DIR = path_1.default.join((0, utils_1.getAppDirectory)(), 'api');
const TYPES_DIR = path_1.default.join(PROJECT_ROOT, 'types');
const defaultOptions = {
dir: options?.dir ?? API_DIR,
outDir: options?.outDir ?? TYPES_DIR,
outFile: options?.outFile ?? "next-ts-api.ts",
};
const opts = {
...defaultOptions,
...options,
};
if (opts.outDir && !(0, fs_1.existsSync)(opts.outDir)) {
(0, fs_1.mkdirSync)(opts.outDir, { recursive: true });
}
try {
const routes = findRouteFiles(API_DIR);
const typeDefinitions = generateTypeDefinitions(routes, opts);
const outputFilepath = (0, path_1.join)(opts.outDir, opts.outFile);
fs_1.default.writeFileSync(outputFilepath, typeDefinitions);
}
catch (error) {
logger_1.logger.error('Error generating API type definitions:', error);
}
}