doc-it-up
Version:
Generates automatic documentation for your code. Supports Express, Fastify, Koa, Hono, Elysia, and Hapi.
994 lines (988 loc) • 40.2 kB
JavaScript
import { _ as __awaiter } from './chunks/tslib.es6-WQS2tr1v.js';
import fs from 'fs/promises';
import path from 'path';
const pathToUserPkg = path.resolve(process.cwd(), 'package.json');
let name, version, description;
(() => __awaiter(void 0, void 0, void 0, function* () {
const packageJson = yield fs.readFile(pathToUserPkg, 'utf-8').then(JSON.parse);
name = packageJson.name;
version = packageJson.version;
description = packageJson.description;
}))();
// Global state
const routeSpecs = new Map();
let docsDir = './docs';
// Initialize docs directory
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 });
}
});
// Normalize dynamic routes
const normalizePath = (path) => {
return path
.replace(/\/\d+/g, '/{id}')
.replace(/\/[a-f0-9]{24}/g, '/{id}') // MongoDB ObjectId
.replace(/\/[a-f0-9-]{36}/g, '/{id}') // UUID
.replace(/\/[^\/]+\/\d+/g, '/{resource}/{id}')
.replace(/\?.*$/, ''); // Remove query string
};
// Extract parameters from URL
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);
params[paramName] = originalSegments[i];
}
}
return params;
};
// Generate schema signature based on keys only (for comparison)
const generateSchemaSignature = (obj) => {
if (!obj || typeof obj !== 'object')
return 'primitive';
if (Array.isArray(obj)) {
if (obj.length === 0)
return 'array:empty';
return `array:${generateSchemaSignature(obj[0])}`;
}
const keys = Object.keys(obj).sort();
const keySignatures = 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}`;
});
return keySignatures.join('|');
};
// Extract authentication info
const extractAuthInfo = (req) => {
const auth = {};
// Check for Authorization header
if (req.headers.authorization) {
const authHeader = req.headers.authorization;
if (authHeader.startsWith('Bearer ')) {
auth.type = 'bearer';
auth.extractedData = { token: authHeader.substring(7) };
}
else if (authHeader.startsWith('Basic ')) {
auth.type = 'basic';
auth.extractedData = { credentials: authHeader.substring(6) };
}
else {
auth.type = 'custom';
auth.headerName = 'Authorization';
auth.customScheme = authHeader.split(' ')[0];
auth.extractedData = { fullHeader: authHeader };
}
}
// Check for API key in headers (extended list)
const apiKeyHeaders = [
'x-api-key', 'api-key', 'x-auth-token', 'x-access-token',
'x-client-id', 'x-app-key', 'x-secret-key'
];
for (const header of apiKeyHeaders) {
if (req.headers[header]) {
auth.type = 'apiKey';
auth.headerName = header;
auth.extractedData = {
[header]: req.headers[header],
keyType: header
};
break;
}
}
// Check for custom auth headers (any header starting with 'x-auth-' or 'x-token-')
const customAuthHeaders = Object.keys(req.headers).filter(header => header.startsWith('x-auth-') || header.startsWith('x-token-') || header.startsWith('x-jwt-'));
if (customAuthHeaders.length > 0 && !auth.type) {
auth.type = 'custom';
auth.headerName = customAuthHeaders[0];
auth.extractedData = customAuthHeaders.reduce((acc, header) => {
acc[header] = req.headers[header];
return acc;
}, {});
}
// Check for cookies
if (req.headers.cookie) {
auth.cookies = true;
if (!auth.extractedData)
auth.extractedData = {};
auth.extractedData.cookies = req.headers.cookie;
}
// Check for custom auth data in extended request properties
if ('auth' in req && req.auth) {
if (!auth.extractedData)
auth.extractedData = {};
auth.extractedData.customAuth = req.auth;
}
// Check for user data in extended request properties
if ('user' in req && req.user) {
if (!auth.extractedData)
auth.extractedData = {};
auth.extractedData.user = req.user;
}
return Object.keys(auth).length > 0 ? auth : undefined;
};
// Extract custom headers (exclude standard ones)
// custom header extraction with configurable exclusions
const extractCustomHeaders = (headers, additionalStandardHeaders = []) => {
const standardHeaders = [
'host', 'user-agent', 'accept', 'accept-encoding', 'accept-language',
'cache-control', 'connection', 'content-length', 'content-type',
'cookie', 'origin', 'referer', 'sec-fetch-dest', 'sec-fetch-mode',
'sec-fetch-site', 'upgrade-insecure-requests', 'postman-token',
'if-none-match', 'if-modified-since', 'pragma', 'expires',
'last-modified', 'etag', 'server', 'date', 'vary',
...additionalStandardHeaders // Allow custom exclusions
];
const customHeaders = {};
for (const [key, value] of Object.entries(headers)) {
const lowerKey = key.toLowerCase();
// Skip standard headers, sec- prefixed headers, and undefined values
if (!standardHeaders.includes(lowerKey) &&
!lowerKey.startsWith('sec-') &&
!lowerKey.startsWith('cf-') && // Cloudflare headers
!lowerKey.startsWith('x-forwarded-') && // Proxy headers
!lowerKey.startsWith('x-real-ip') && // Real IP headers
value !== undefined) {
customHeaders[key] = value;
}
}
return Object.keys(customHeaders).length > 0 ? customHeaders : undefined;
};
// Extract response info with schema signature
// response extraction with support for custom response data
const extractResponseInfo = (res, body) => {
const responseInfo = {
statusCode: res.statusCode,
statusMessage: res.statusMessage || '',
headers: {},
cookies: [],
redirects: [],
customData: {}
};
// Extract response headers (excluding standard ones)
const headers = res.getHeaders();
const standardResponseHeaders = [
'content-length', 'date', 'connection', 'keep-alive',
'transfer-encoding', 'server', 'x-powered-by'
];
for (const [key, value] of Object.entries(headers)) {
if (!standardResponseHeaders.includes(key.toLowerCase())) {
responseInfo.headers[key] = value;
}
}
// Extract set-cookie headers
const setCookies = res.getHeader('set-cookie');
if (setCookies) {
responseInfo.cookies = Array.isArray(setCookies) ? setCookies : [setCookies];
}
// Check for redirects
if (res.statusCode >= 300 && res.statusCode < 400) {
const location = res.getHeader('location');
if (location) {
responseInfo.redirects.push(location);
}
}
// Extract custom response data from extended response properties
const customResponseKeys = ['customData', 'metadata', 'context', 'extras'];
for (const key of customResponseKeys) {
if (key in res && res[key]) {
responseInfo.customData[key] = res[key];
}
}
// body parsing with custom type detection
if (body !== undefined && body !== null) {
const contentType = res.getHeader('content-type') || '';
if (contentType.includes('application/json')) {
try {
const parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
responseInfo.body = {
type: 'json',
schema: generateJsonSchema(parsedBody),
example: parsedBody,
signature: generateSchemaSignature(parsedBody)
};
}
catch (_a) {
responseInfo.body = {
type: 'text',
example: body,
signature: 'text'
};
}
}
else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
responseInfo.body = {
type: 'custom',
customType: 'xml',
example: body,
signature: 'xml'
};
}
else if (contentType.includes('application/octet-stream') || contentType.includes('application/pdf')) {
responseInfo.body = {
type: 'binary',
customType: contentType.split('/')[1],
example: '[Binary Data]',
signature: 'binary'
};
}
else if (contentType.includes('text/')) {
responseInfo.body = {
type: 'text',
example: body,
signature: 'text'
};
}
else {
// Handle custom content types
responseInfo.body = {
type: 'custom',
customType: contentType,
example: body,
signature: contentType || 'unknown'
};
}
}
return responseInfo;
};
// Check if route spec should be updated based on schema changes
const shouldUpdateSpec = (existingSpec, newSpec) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
// Always update if no existing spec
if (!existingSpec)
return true;
// Compare request body schemas
if (((_a = existingSpec.body) === null || _a === void 0 ? void 0 : _a.signature) !== ((_b = newSpec.body) === null || _b === void 0 ? void 0 : _b.signature)) {
return true;
}
// Compare response body schemas
if (((_d = (_c = existingSpec.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;
}
// Compare query parameters (keys only)
const existingQueryKeys = existingSpec.query ? Object.keys(existingSpec.query).sort() : [];
const newQueryKeys = newSpec.query ? Object.keys(newSpec.query).sort() : [];
if (JSON.stringify(existingQueryKeys) !== JSON.stringify(newQueryKeys)) {
return true;
}
// Compare custom headers (keys only)
const existingHeaderKeys = existingSpec.customHeaders ? Object.keys(existingSpec.customHeaders).sort() : [];
const newHeaderKeys = newSpec.customHeaders ? Object.keys(newSpec.customHeaders).sort() : [];
if (JSON.stringify(existingHeaderKeys) !== JSON.stringify(newHeaderKeys)) {
return true;
}
// Compare auth type
if (((_g = existingSpec.auth) === null || _g === void 0 ? void 0 : _g.type) !== ((_h = newSpec.auth) === null || _h === void 0 ? void 0 : _h.type)) {
return true;
}
return false;
};
// Save individual route spec to JSON file
const saveRouteSpec = (routeKey, spec) => __awaiter(void 0, void 0, void 0, function* () {
const filename = routeKey.replace(/[^a-zA-Z0-9]/g, '_') + '.json';
const filepath = path.join(docsDir, filename);
try {
yield fs.writeFile(filepath, JSON.stringify(spec, null, 2));
}
catch (error) {
console.error('Error saving route spec:', error);
}
});
// Load existing specs from files
const loadExistingSpecs = () => __awaiter(void 0, void 0, void 0, function* () {
try {
const files = yield fs.readdir(docsDir);
const jsonFiles = files.filter(file => file.endsWith('.json'));
for (const file of jsonFiles) {
try {
const filepath = path.join(docsDir, file);
const content = yield fs.readFile(filepath, 'utf8');
const spec = JSON.parse(content);
const routeKey = `${spec.method.toLowerCase()}:${spec.path}`;
routeSpecs.set(routeKey, spec);
}
catch (error) {
console.error(`Error loading spec from ${file}:`, error);
}
}
}
catch (error) {
console.error('Error loading existing specs:', error);
}
});
// Helper function to get tag from path
const getTagFromPath = (path) => {
const segments = path.split('/').filter(s => s && !s.startsWith('{'));
return segments[0] || 'default';
};
// Helper function to get status description
const getStatusDescription = (statusCode) => {
const descriptions = {
'200': 'Success',
'201': 'Created',
'204': 'No Content',
'400': 'Bad Request',
'401': 'Unauthorized',
'403': 'Forbidden',
'404': 'Not Found',
'422': 'Validation Error',
'500': 'Internal Server Error'
};
return descriptions[statusCode] || 'Response';
};
// Helper functions for format detection
const isValidDate = (str) => {
return !isNaN(Date.parse(str)) && str.includes('T');
};
const isValidEmail = (str) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
};
// getTypeSchema function
const getTypeSchema = (value) => {
if (value === null || value === undefined) {
return { type: 'string', nullable: true };
}
const type = typeof value;
switch (type) {
case 'string':
// Check if it's a date string
if (isValidDate(value)) {
return { type: 'string', format: 'date-time' };
}
// Check if it's an email
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' };
}
};
// generateJsonSchema function
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)) {
if (schema.properties) {
schema.properties[key] = getTypeSchema(value);
}
if (value !== null && value !== undefined && schema.required) {
schema.required.push(key);
}
}
if (schema.required && schema.required.length === 0) {
delete schema.required;
}
return schema;
};
// form data field extraction
const ExtractFormDataFields = (body, files) => {
const fields = {};
// Handle regular form fields
if (body) {
for (const [key, value] of Object.entries(body)) {
fields[key] = {
type: Array.isArray(value) ? 'array' : typeof value === 'string' ? 'string' : 'object',
required: true,
description: `Form field: ${key}`,
example: Array.isArray(value) ? value : [value]
};
}
}
// file handling with proper Swagger UI support
if (files) {
for (const [key, file] of Object.entries(files)) {
if (Array.isArray(file)) {
// Multiple files with same field name
fields[key] = {
type: 'array',
items: {
type: 'string',
format: 'binary'
},
required: true,
description: `Multiple file upload field: ${key}`,
'x-swagger-ui-file-upload': true,
maxItems: file.length
};
}
else {
// Single file - this is the key fix for Swagger UI file upload
fields[key] = Object.assign(Object.assign(Object.assign({ type: 'string', format: 'binary', required: true, description: `File upload field: ${key}`, 'x-swagger-ui-file-upload': true }, (file.mimetype && {
contentMediaType: file.mimetype,
'x-content-type': file.mimetype
})), (file.size && {
'x-max-size': file.size
})), (file.originalname && {
'x-original-name': file.originalname
}));
}
}
}
return fields;
};
// Fixed multipart form data schema generation for Swagger UI
const generateMultipartSchema = (fields) => {
var _a;
const schema = {
type: 'object',
properties: {},
required: []
};
if (fields && schema.properties && schema.required) {
for (const [fieldName, fieldSpec] of Object.entries(fields)) {
// File upload fields
if (fieldSpec.format === 'binary') {
schema.properties[fieldName] = Object.assign({ type: 'string', format: 'binary', description: fieldSpec.description || `File upload: ${fieldName}` }, (fieldSpec.contentMediaType && { contentMediaType: fieldSpec.contentMediaType }));
}
// Multiple file upload fields
else if (fieldSpec.type === 'array' && ((_a = fieldSpec.items) === null || _a === void 0 ? void 0 : _a.format) === 'binary') {
schema.properties[fieldName] = Object.assign({ type: 'array', items: {
type: 'string',
format: 'binary'
}, description: fieldSpec.description || `Multiple file upload: ${fieldName}` }, (fieldSpec.maxItems && { maxItems: fieldSpec.maxItems }));
}
// Regular form fields
else {
schema.properties[fieldName] = Object.assign({ type: fieldSpec.type || 'string', description: fieldSpec.description || `Form field: ${fieldName}` }, (fieldSpec.example && { example: fieldSpec.example }));
}
if (fieldSpec.required) {
schema.required.push(fieldName);
}
}
}
// Remove empty required array
if (schema.required && schema.required.length === 0) {
delete schema.required;
}
return schema;
};
const ParseRequestBody = (req) => {
const contentType = req.headers['content-type'] || '';
// Handle standard content types
if (contentType.includes('application/json')) {
return {
type: 'json',
schema: generateJsonSchema(req.body),
example: req.body,
signature: generateSchemaSignature(req.body)
};
}
if (contentType.includes('multipart/form-data')) {
// Type guard to check if files is an object with fieldname keys
const isFilesObject = (files) => {
return files && typeof files === 'object' && !Array.isArray(files);
};
// Convert files to a consistent format for processing
const filesForProcessing = req.files
? isFilesObject(req.files)
? req.files
: {} // If it's an array, we'll handle it differently
: {};
const fields = ExtractFormDataFields(req.body, filesForProcessing);
// Create example object with file information
let filesExample = {};
if (req.files) {
if (Array.isArray(req.files)) {
// Handle array of files
filesExample = {
files: req.files.map(f => `[File: ${f.originalname || 'uploaded_file'}]`)
};
}
else if (isFilesObject(req.files)) {
// Handle object with fieldname keys - use the type-guarded variable
const filesObj = req.files;
filesExample = Object.keys(filesObj).reduce((acc, key) => {
const fileArray = filesObj[key];
acc[key] = fileArray.map(f => `[File: ${f.originalname || 'uploaded_file'}]`);
return acc;
}, {});
}
}
return {
type: 'formData',
fields: fields,
schema: generateMultipartSchema(fields),
example: Object.assign(Object.assign({}, req.body), filesExample),
signature: generateSchemaSignature(Object.assign(Object.assign({}, req.body), { files: req.files && isFilesObject(req.files) ? Object.keys(req.files) : [] }))
};
}
if (contentType.includes('application/x-www-form-urlencoded')) {
return {
type: 'urlencoded',
schema: generateJsonSchema(req.body),
example: req.body,
signature: generateSchemaSignature(req.body)
};
}
// Handle custom content types
if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
return {
type: 'custom',
customType: 'xml',
schema: { type: 'string', format: 'xml' },
example: req.body,
signature: 'xml',
rawData: req.body
};
}
if (contentType.includes('text/plain')) {
return {
type: 'custom',
customType: 'text',
schema: { type: 'string' },
example: req.body,
signature: 'text',
rawData: req.body
};
}
if (contentType.includes('application/octet-stream')) {
return {
type: 'custom',
customType: 'binary',
schema: { type: 'string', format: 'binary' },
example: '[Binary Data]',
signature: 'binary',
rawData: req.body
};
}
// Handle any other custom content types
if (contentType && req.body !== undefined) {
return {
type: 'custom',
customType: contentType,
schema: { type: 'string' },
example: req.body,
signature: contentType.replace(/[^a-zA-Z0-9]/g, '_'),
rawData: req.body
};
}
// Check for custom body parsers in extended request properties
const customBodyKeys = ['parsedBody', 'rawBody', 'customBody'];
for (const key of customBodyKeys) {
if (key in req && req[key]) {
return {
type: 'custom',
customType: key,
schema: generateJsonSchema(req[key]),
example: req[key],
signature: generateSchemaSignature(req[key]),
rawData: req[key]
};
}
}
return undefined;
};
// Generate encoding for multipart form data
const generateFormDataEncoding = (fields) => {
var _a;
const encoding = {};
if (fields) {
for (const [fieldName, fieldSpec] of Object.entries(fields)) {
if (fieldSpec.format === 'binary' ||
(fieldSpec.type === 'array' && ((_a = fieldSpec.items) === null || _a === void 0 ? void 0 : _a.format) === 'binary')) {
encoding[fieldName] = {
contentType: fieldSpec.contentMediaType || 'application/octet-stream'
};
}
}
}
return Object.keys(encoding).length > 0 ? encoding : undefined;
};
// Swagger/OpenAPI spec generation
const generateSwaggerSpec = () => {
var _a;
const spec = {
openapi: '3.0.0',
info: {
title: `${name}`,
version: `${version}`,
description: `Automatically generated API documentation based on actual API usage\n ${description}`,
},
servers: [
{
url: '/',
description: 'Current server'
}
],
paths: {},
components: {
securitySchemes: {},
schemas: {}
}
};
// If no routes are captured yet, return a basic spec
if (routeSpecs.size === 0) {
spec.paths['/'] = {
get: {
summary: 'No routes documented yet',
description: 'Make API calls to auto-generate documentation',
parameters: [],
responses: {
'200': {
description: 'Success'
}
},
tags: ['default']
}
};
return spec;
}
// Group specs by path
const pathGroups = new Map();
for (const [routeKey, routeSpec] of routeSpecs.entries()) {
const path = routeSpec.path;
if (!pathGroups.has(path)) {
pathGroups.set(path, new Map());
}
pathGroups.get(path).set(routeSpec.method.toLowerCase(), routeSpec);
}
// Convert to Swagger format
for (const [path, methods] of pathGroups.entries()) {
// Ensure path starts with /
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
spec.paths[normalizedPath] = {};
for (const [method, routeSpec] of methods.entries()) {
const operation = {
summary: `${method.toUpperCase()} ${normalizedPath}`,
description: `Auto-generated documentation for ${method.toUpperCase()} ${normalizedPath}`,
parameters: [],
responses: {
'200': {
description: 'Default response'
}
},
tags: [getTagFromPath(normalizedPath)]
};
// Add path parameters
if (routeSpec.params && Object.keys(routeSpec.params).length > 0) {
for (const [paramName, paramValue] of Object.entries(routeSpec.params)) {
operation.parameters.push({
name: paramName,
in: 'path',
required: true,
schema: { type: 'string' },
description: `Path parameter: ${paramName}`,
example: paramValue
});
}
}
// Add query parameters
if (routeSpec.query && Object.keys(routeSpec.query).length > 0) {
for (const [queryName, queryValue] of Object.entries(routeSpec.query)) {
operation.parameters.push({
name: queryName,
in: 'query',
required: false,
schema: getTypeSchema(queryValue),
description: `Query parameter: ${queryName}`,
example: queryValue
});
}
}
// Add custom headers
if (routeSpec.customHeaders) {
for (const [headerName, headerValue] of Object.entries(routeSpec.customHeaders)) {
operation.parameters.push({
name: headerName,
in: 'header',
required: false,
schema: { type: 'string' },
description: `Custom header: ${headerName}`,
example: headerValue
});
}
}
// request body handling
if (['post', 'put', 'patch'].includes(method) && routeSpec.body) {
operation.requestBody = {
required: true,
content: {}
};
if (routeSpec.body.type === 'json') {
operation.requestBody.content['application/json'] = {
schema: routeSpec.body.schema || { type: 'object' },
example: routeSpec.body.example
};
}
else if (routeSpec.body.type === 'formData') {
operation.requestBody.content['multipart/form-data'] = {
schema: routeSpec.body.schema || { type: 'object' },
encoding: generateFormDataEncoding(routeSpec.body.fields || {})
};
}
else if (routeSpec.body.type === 'urlencoded') {
operation.requestBody.content['application/x-www-form-urlencoded'] = {
schema: routeSpec.body.schema || { type: 'object' },
example: routeSpec.body.example
};
}
}
// Add authentication
if (routeSpec.auth) {
operation.security = [];
if (routeSpec.auth.type === 'bearer') {
operation.security.push({ BearerAuth: [] });
spec.components.securitySchemes.BearerAuth = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT Bearer token authentication'
};
}
else if (routeSpec.auth.type === 'basic') {
operation.security.push({ BasicAuth: [] });
spec.components.securitySchemes.BasicAuth = {
type: 'http',
scheme: 'basic',
description: 'Basic HTTP authentication'
};
}
else if (routeSpec.auth.type === 'apiKey' && routeSpec.auth.headerName) {
const schemeName = `ApiKey_${routeSpec.auth.headerName.replace(/[^a-zA-Z0-9]/g, '_')}`;
operation.security.push({ [schemeName]: [] });
spec.components.securitySchemes[schemeName] = {
type: 'apiKey',
in: 'header',
name: routeSpec.auth.headerName,
description: `API Key authentication via ${routeSpec.auth.headerName} header`
};
}
}
// response handling
if (routeSpec.response) {
const statusCode = ((_a = routeSpec.response.statusCode) === null || _a === void 0 ? void 0 : _a.toString()) || '200';
const responseObj = {
description: routeSpec.response.statusMessage || getStatusDescription(statusCode)
};
// Add response headers
if (routeSpec.response.headers && Object.keys(routeSpec.response.headers).length > 0) {
responseObj.headers = {};
for (const [headerName, headerValue] of Object.entries(routeSpec.response.headers)) {
responseObj.headers[headerName] = {
schema: { type: 'string' },
description: `Response header: ${headerName}`,
example: Array.isArray(headerValue) ? headerValue[0] : headerValue
};
}
}
// Add response body
if (routeSpec.response.body) {
responseObj.content = {};
if (routeSpec.response.body.type === 'json') {
responseObj.content['application/json'] = {
schema: routeSpec.response.body.schema || { type: 'object' },
example: routeSpec.response.body.example
};
}
else {
responseObj.content['text/plain'] = {
schema: { type: 'string' },
example: routeSpec.response.body.example
};
}
}
operation.responses[statusCode] = responseObj;
}
spec.paths[normalizedPath][method] = operation;
}
}
return spec;
};
// Main middleware function
// Generic middleware function that works with extended request/response objects
const autoDocMiddleware = (options = {}) => {
// Initialize with options
if (options.docsDir) {
initDocsDirectory(options.docsDir);
}
else {
initDocsDirectory();
}
return (req, res, next) => {
// Skip documentation route
if (req.path === '/docs' || req.path.startsWith('/docs/')) {
return next();
}
const originalPath = req.path;
const normalizedPath = normalizePath(originalPath);
const method = req.method.toLowerCase();
const routeKey = `${method}:${normalizedPath}`;
// request data capture with custom data extraction
const requestData = {
method: method.toUpperCase(),
path: normalizedPath,
originalPath,
query: req.query,
params: extractParams(originalPath, normalizedPath),
body: ParseRequestBody(req),
auth: extractAuthInfo(req),
customHeaders: extractCustomHeaders(req.headers),
timestamp: new Date().toISOString(),
customData: {},
extensions: {}
};
// Extract custom data from extended request properties
const customRequestKeys = ['context', 'metadata', 'extras', 'customData'];
for (const key of customRequestKeys) {
if (key in req && req[key]) {
requestData.customData[key] = req[key];
}
}
// Extract framework-specific extensions
const frameworkKeys = ['app', 'route', 'baseUrl', 'originalUrl', 'session'];
for (const key of frameworkKeys) {
if (key in req && req[key] && key !== 'app') { // Skip app to avoid circular references
requestData.extensions[key] = req[key];
}
}
// response capturing with support for custom response methods
const originalSend = res.send;
const originalJson = res.json;
const originalEnd = res.end;
let responseBody = '';
// Override send method
res.send = function (data) {
responseBody = data;
return originalSend.call(this, data);
};
// Override json method
res.json = function (data) {
responseBody = JSON.stringify(data);
return originalJson.call(this, data);
};
// Override end method to catch responses that bypass send/json
res.end = function (chunk, encoding) {
if (chunk && !responseBody) {
responseBody = chunk;
}
return originalEnd.call(this, chunk, encoding);
};
// Process after response with error handling
res.on('finish', () => __awaiter(void 0, void 0, void 0, function* () {
try {
// Document both successful and error responses
if (res.statusCode >= 200 && res.statusCode < 300) { // Capture all status codes
const responseInfo = extractResponseInfo(res, responseBody);
const newApiSpec = Object.assign(Object.assign({}, requestData), { response: responseInfo, lastUpdated: new Date().toISOString() });
// Check if we should update the spec
const existingSpec = routeSpecs.get(routeKey);
if (shouldUpdateSpec(existingSpec, newApiSpec)) {
console.log(`Updating API spec for ${routeKey} - Schema change detected`);
// Store in memory map
routeSpecs.set(routeKey, newApiSpec);
// Save to file
yield saveRouteSpec(routeKey, newApiSpec);
}
else {
console.log(`Skipping update for ${routeKey} - Same schema structure`);
// Update only the timestamp in existing spec
if (existingSpec) {
existingSpec.lastAccessed = new Date().toISOString();
routeSpecs.set(routeKey, existingSpec);
yield saveRouteSpec(routeKey, existingSpec);
}
}
}
}
catch (error) {
console.error('Error processing route documentation:', error);
}
}));
next();
};
};
// Generic docs handler that works with extended request/response objects
const docsHandler = () => {
return (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
// Load existing specs from files
yield loadExistingSpecs();
// Handle JSON endpoint
if (req.path === '/docs/swagger.json' || req.path.endsWith('/swagger.json')) {
const swaggerSpec = generateSwaggerSpec();
// Validate the spec before sending
if (!swaggerSpec.openapi || !swaggerSpec.info || !swaggerSpec.paths) {
res.status(500).json({
error: 'Invalid OpenAPI specification generated'
});
return;
}
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.json(swaggerSpec);
return;
}
const html = `
<!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>
`;
res.setHeader('Content-Type', 'text/html');
res.send(html);
}
catch (error) {
console.error('Error serving docs:', error);
res.status(500).json({
error: 'Failed to generate documentation',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
};
export { autoDocMiddleware, docsHandler, initDocsDirectory };