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
JavaScript
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 };