doc-it-up
Version:
Generates automatic documentation for your code. So you don't have to.
1,025 lines (1,013 loc) • 41.9 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
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 };