UNPKG

doc-it-up

Version:

Generates automatic documentation for your code. Supports Express, Fastify, Koa, Hono, Elysia, and Hapi.

296 lines (293 loc) 11.7 kB
import { _ as __awaiter } from './chunks/tslib.es6-WQS2tr1v.js'; import fs from 'fs/promises'; import path from 'path'; // --- Configuration & State --- let docsDir = './docs'; const routeSpecs = new Map(); let pkgInfo = { name: 'API', version: '1.0.0', description: '' }; const pathToUserPkg = path.resolve(process.cwd(), 'package.json'); (() => __awaiter(void 0, void 0, void 0, function* () { try { const packageJson = yield fs.readFile(pathToUserPkg, 'utf-8').then(JSON.parse); pkgInfo = { name: packageJson.name, version: packageJson.version, description: packageJson.description || '' }; } catch (e) { } }))(); const initDocsDirectory = (customDocsDir) => __awaiter(void 0, void 0, void 0, function* () { if (customDocsDir) docsDir = customDocsDir; try { yield fs.access(docsDir); } catch (_a) { yield fs.mkdir(docsDir, { recursive: true }); } }); // --- Schema Generation Logic --- const isValidDate = (str) => !isNaN(Date.parse(str)) && str.includes('T'); const isValidEmail = (str) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str); const getTypeSchema = (value) => { if (value === null || value === undefined) return { type: 'string', nullable: true }; const type = typeof value; switch (type) { case 'string': if (isValidDate(value)) return { type: 'string', format: 'date-time' }; if (isValidEmail(value)) return { type: 'string', format: 'email' }; return { type: 'string' }; case 'number': return { type: Number.isInteger(value) ? 'integer' : 'number', example: value }; case 'boolean': return { type: 'boolean', example: value }; case 'object': if (Array.isArray(value)) { return { type: 'array', items: value.length > 0 ? getTypeSchema(value[0]) : { type: 'string' } }; } return generateJsonSchema(value); default: return { type: 'string' }; } }; const generateJsonSchema = (obj) => { if (!obj || typeof obj !== 'object') return getTypeSchema(obj); if (Array.isArray(obj)) return { type: 'array', items: obj.length > 0 ? getTypeSchema(obj[0]) : { type: 'string' } }; const schema = { type: 'object', properties: {}, required: [] }; for (const [key, value] of Object.entries(obj)) { schema.properties[key] = getTypeSchema(value); if (value !== null && value !== undefined) schema.required.push(key); } if (schema.required.length === 0) delete schema.required; return schema; }; const generateSchemaSignature = (obj) => { if (!obj || typeof obj !== 'object') return 'primitive'; if (Array.isArray(obj)) return obj.length === 0 ? 'array:empty' : `array:${generateSchemaSignature(obj[0])}`; const keys = Object.keys(obj).sort(); return keys.map(key => { const value = obj[key]; if (value === null) return `${key}:null`; if (Array.isArray(value)) return `${key}:array:${value.length > 0 ? generateSchemaSignature(value[0]) : 'empty'}`; if (typeof value === 'object') return `${key}:object:${generateSchemaSignature(value)}`; return `${key}:${typeof value}`; }).join('|'); }; // --- Extraction Helpers --- const normalizePath = (path) => { return path .replace(/\/\d+/g, '/{id}') .replace(/\/[a-f0-9]{24}/g, '/{id}') .replace(/\/[a-f0-9-]{36}/g, '/{id}') .replace(/\/[^\/]+\/\d+/g, '/{resource}/{id}') .replace(/\?.*$/, ''); }; const extractParams = (originalPath, normalizedPath) => { const originalSegments = originalPath.split('/'); const normalizedSegments = normalizedPath.split('/'); const params = {}; for (let i = 0; i < normalizedSegments.length; i++) { if (normalizedSegments[i].startsWith('{') && normalizedSegments[i].endsWith('}')) { const paramName = normalizedSegments[i].slice(1, -1); if (originalSegments[i]) params[paramName] = originalSegments[i]; } } return params; }; const extractAuthInfo = (req) => { const auth = {}; const h = req.headers || {}; if (h.authorization) { if (h.authorization.startsWith('Bearer ')) { auth.type = 'bearer'; } else if (h.authorization.startsWith('Basic ')) { auth.type = 'basic'; } else { auth.type = 'custom'; auth.headerName = 'Authorization'; } } const apiKeyHeaders = ['x-api-key', 'api-key', 'x-auth-token', 'x-client-id']; for (const header of apiKeyHeaders) { if (h[header]) { auth.type = 'apiKey'; auth.headerName = header; break; } } return Object.keys(auth).length > 0 ? auth : undefined; }; const analyzeBody = (req) => { if (!req.body) return undefined; const contentType = req.headers['content-type'] || ''; if (contentType.includes('multipart/form-data')) { return { type: 'formData', schema: { type: 'object', properties: {} }, example: '[Multipart Data]', signature: 'multipart' }; } return { type: 'json', schema: generateJsonSchema(req.body), example: req.body, signature: generateSchemaSignature(req.body) }; }; const analyzeResponse = (res) => { const contentType = res.headers['content-type'] || ''; let bodyInfo = {}; if (res.body !== undefined) { if (contentType.includes('application/json') || typeof res.body === 'object') { bodyInfo = { type: 'json', schema: generateJsonSchema(res.body), example: res.body, signature: generateSchemaSignature(res.body) }; } else { bodyInfo = { type: 'text', example: res.body, signature: 'text' }; } } return { statusCode: res.statusCode, statusMessage: res.statusMessage, headers: res.headers, body: bodyInfo }; }; // --- Spec Management --- const shouldUpdateSpec = (existing, newSpec) => { var _a, _b, _c, _d, _e, _f; if (!existing) return true; if (((_a = existing.body) === null || _a === void 0 ? void 0 : _a.signature) !== ((_b = newSpec.body) === null || _b === void 0 ? void 0 : _b.signature)) return true; if (((_d = (_c = existing.response) === null || _c === void 0 ? void 0 : _c.body) === null || _d === void 0 ? void 0 : _d.signature) !== ((_f = (_e = newSpec.response) === null || _e === void 0 ? void 0 : _e.body) === null || _f === void 0 ? void 0 : _f.signature)) return true; return false; }; // --- Core Class --- class DocItUpCore { static recordRequest(req, res) { return __awaiter(this, void 0, void 0, function* () { try { const normalizedPath = normalizePath(req.path); const method = req.method.toLowerCase(); const routeKey = `${method}:${normalizedPath}`; const newSpec = { method: method.toUpperCase(), path: normalizedPath, originalPath: req.path, query: req.query, params: Object.assign(Object.assign({}, extractParams(req.path, normalizedPath)), req.params), body: analyzeBody(req), auth: extractAuthInfo(req), customHeaders: req.headers, response: analyzeResponse(res), timestamp: new Date().toISOString(), lastUpdated: new Date().toISOString() }; const existingSpec = routeSpecs.get(routeKey); if (shouldUpdateSpec(existingSpec, newSpec)) { console.log(`[DocItUp] Updating spec for ${routeKey}`); routeSpecs.set(routeKey, newSpec); const filename = routeKey.replace(/[^a-zA-Z0-9]/g, '_') + '.json'; yield fs.writeFile(path.join(docsDir, filename), JSON.stringify(newSpec, null, 2)); } else if (existingSpec) { existingSpec.lastAccessed = new Date().toISOString(); routeSpecs.set(routeKey, existingSpec); } } catch (err) { console.error('[DocItUp] Error recording request:', err); } }); } static loadSpecs() { return __awaiter(this, void 0, void 0, function* () { try { const files = yield fs.readdir(docsDir); for (const file of files) { if (file.endsWith('.json')) { const content = yield fs.readFile(path.join(docsDir, file), 'utf8'); const spec = JSON.parse(content); routeSpecs.set(`${spec.method.toLowerCase()}:${spec.path}`, spec); } } } catch (e) { } }); } static getSwaggerSpec() { var _a, _b, _c; const spec = { openapi: '3.0.0', info: pkgInfo, paths: {} }; for (const route of routeSpecs.values()) { if (!spec.paths[route.path]) spec.paths[route.path] = {}; const method = route.method.toLowerCase(); spec.paths[route.path][method] = { summary: `${route.method} ${route.path}`, responses: { [((_a = route.response) === null || _a === void 0 ? void 0 : _a.statusCode) || 200]: { description: 'Response', content: ((_c = (_b = route.response) === null || _b === void 0 ? void 0 : _b.body) === null || _c === void 0 ? void 0 : _c.type) === 'json' ? { 'application/json': { schema: route.response.body.schema, example: route.response.body.example } } : {} } } }; if (route.body) { spec.paths[route.path][method].requestBody = { content: { 'application/json': { schema: route.body.schema, example: route.body.example } } }; } } return spec; } static getHtml() { return ` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/png" href="https://cdn.letshost.dpdns.org/image/upload/v1751581420/__cdn/685f0e4b7d1b59a6be2a63db/img/ldyoFzE4kF/101/kjfsymvet1lvkuuyrya5.png" /> <link rel="shortcut icon" href="https://cdn.letshost.dpdns.org/image/upload/v1751581420/__cdn/685f0e4b7d1b59a6be2a63db/img/ldyoFzE4kF/101/kjfsymvet1lvkuuyrya5.png" type="image/png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Auto-Generated API Documentation-doc-it-up</title> <link rel="stylesheet" crossorigin href="https://cdn.letshost.dpdns.org/685f0e4b7d1b59a6be2a63db/css/RGdN_kKAu3/104/index-DLR5SXhN.css"/> <script type="module" crossorigin src="https://cdn.letshost.dpdns.org/685f0e4b7d1b59a6be2a63db/js/-fhDzkflS0/103/index-eEW-mwy-.js"></script> </head> <body> <div id="root"></div> </body> </html>`; } } export { DocItUpCore, initDocsDirectory };