updating lint, format, and CI

This commit is contained in:
Luke Hagar
2025-03-19 23:18:04 -05:00
parent 32de48691f
commit 8cb6672e3b
9 changed files with 185 additions and 117 deletions

64
.eslintrc.json Normal file
View File

@@ -0,0 +1,64 @@
{
"env": {
"es2022": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/require-await": "warn",
"no-console": ["warn", {
"allow": ["warn", "error", "info", "log"]
}],
"eqeqeq": ["error", "always"],
"no-unused-vars": "off",
"prefer-const": "error",
"no-var": "error",
"curly": ["error", "all"]
},
"overrides": [
{
"files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
"env": {
"jest": true
},
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/unbound-method": "off"
}
}
],
"ignorePatterns": ["dist", "node_modules", "coverage", "*.config.mjs"],
"root": true
}

View File

@@ -26,7 +26,8 @@ jobs:
run: npm run format:check
- name: Run linter
run: npm run lint
run: npm run lint -- --max-warnings 0
continue-on-error: true
test:
name: Tests

View File

@@ -19,8 +19,8 @@
"test:ci": "vitest run",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\""
"format": "prettier --write \"src/**/*.ts\" \"integration/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"integration/**/*.ts\""
},
"keywords": [
"api",

View File

@@ -1,25 +1,53 @@
import { Context } from 'hono';
import { openApiStore } from '../store/openApiStore.js';
import type { Context, Next } from 'hono';
import type { OpenAPIStore } from '../store/openApiStore.js';
export const apiDocGenerator = async (c: Context, next: () => Promise<void>) => {
export function apiDocGenerator(store: OpenAPIStore): (c: Context, next: Next) => Promise<void> {
return async (c: Context, next: Next): Promise<void> => {
const startTime = Date.now();
try {
await next();
} catch (error) {
console.error('Error in apiDocGenerator middleware:', error);
throw error;
}
// Record the API call in OpenAPI format
openApiStore.recordEndpoint(
const endTime = Date.now();
const responseTime = endTime - startTime;
// Get request details
const url = new URL(c.req.url);
const queryParams: Record<string, string> = {};
for (const [key, value] of url.searchParams.entries()) {
queryParams[key] = value;
}
// Get request headers
const requestHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(c.req.header())) {
if (typeof value === 'string') {
requestHeaders[key] = value;
}
}
// Record the endpoint in OpenAPI format
try {
store.recordEndpoint(
c.req.path,
c.req.method.toLowerCase(),
{
query: Object.fromEntries(new URL(c.req.url).searchParams),
body: await c.req.json().catch(() => null),
query: queryParams,
headers: requestHeaders,
contentType: c.req.header('content-type') || 'application/json',
},
{
status: c.res.status,
body: await c.res
.clone()
.json()
.catch(() => null),
contentType: c.res.headers.get('content-type') || 'application/json',
headers: Object.fromEntries(c.res.headers.entries()),
}
);
} catch (error) {
console.error('Error recording endpoint:', error);
}
};
}

View File

@@ -1,5 +1,5 @@
import { Context, Next } from 'hono';
import { openApiStore } from '../store/openApiStore.js';
import type { Context, Next } from 'hono';
import type { OpenAPIStore } from '../store/openApiStore.js';
import { SecurityInfo } from '../store/openApiStore.js';
interface HAREntry {
@@ -29,92 +29,33 @@ interface HAREntry {
};
}
export async function harRecorder(c: Context, next: Next) {
export function harRecorder(store: OpenAPIStore): (c: Context, next: Next) => Promise<void> {
return async (c: Context, next: Next): Promise<void> => {
const startTime = Date.now();
// Get request body if present
let requestBody: any;
const contentType = c.req.header('content-type') || 'application/json';
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
try {
const body = await c.req.text();
if (contentType.includes('application/json')) {
requestBody = JSON.parse(body);
} else {
requestBody = body;
}
} catch (e) {
// Body might not be valid JSON or might be empty
requestBody = undefined;
}
}
// Get query parameters from URL
const url = new URL(c.req.url);
const queryParams: Record<string, string> = {};
for (const [key, value] of url.searchParams.entries()) {
queryParams[key] = value;
}
// Get all request headers
const requestHeaders: Record<string, string> = {};
Object.entries(c.req.header()).forEach(([key, value]) => {
if (typeof value === 'string') {
requestHeaders[key] = value;
}
});
// Check for security schemes
const security: SecurityInfo[] = [];
const apiKey = c.req.header('x-api-key');
if (apiKey) {
security.push({
type: 'apiKey',
name: 'x-api-key',
in: 'header',
});
}
// Call next middleware
await next();
} catch (error) {
console.error('Error in harRecorder middleware:', error);
throw error;
}
// Calculate response time
const responseTime = Date.now() - startTime;
const endTime = Date.now();
const responseTime = endTime - startTime;
// Get response body
let responseBody: any;
const responseContentType = c.res.headers.get('content-type') || 'application/json';
// Record the request/response in HAR format
try {
const body = await c.res.clone().text();
if (responseContentType.includes('application/json')) {
responseBody = JSON.parse(body);
} else {
responseBody = body;
}
} catch (e) {
// Response body might not be valid JSON or might be empty
responseBody = undefined;
}
// Record the request/response in OpenAPI format
openApiStore.recordEndpoint(
c.req.path,
c.req.method.toLowerCase(),
{
query: queryParams,
body: requestBody,
contentType,
headers: requestHeaders,
security,
store.recordHAREntry({
request: c.req.raw,
response: c.res,
timing: {
wait: responseTime,
receive: 0,
send: 0,
},
{
status: c.res.status,
body: responseBody,
contentType: responseContentType,
headers: Object.fromEntries(c.res.headers.entries()),
});
} catch (error) {
console.error('Error recording HAR entry:', error);
}
);
// Set HAR data in context
c.set('har', openApiStore.generateHAR());
};
}

View File

@@ -9,6 +9,9 @@ import { openApiStore } from './store/openApiStore.js';
import { IncomingMessage, ServerResponse, createServer, Server } from 'node:http';
import { Agent } from 'node:https';
import chalk from 'chalk';
import { harRecorder } from './middleware/harRecorder.js';
import { apiDocGenerator } from './middleware/apiDocGenerator.js';
import type { ServerConfig } from './types.js';
export interface ServerOptions {
target: string;
@@ -61,6 +64,16 @@ export async function startServers(
docsApp.use('*', cors());
docsApp.use('*', prettyJSON());
// Configure proxy server middleware
proxyApp.use('*', async (c, next) => {
await harRecorder(openApiStore)(c);
await next();
});
proxyApp.use('*', async (c, next) => {
await apiDocGenerator(openApiStore)(c);
await next();
});
// Documentation endpoints
docsApp.get('/docs', async (c: Context) => {
const spec = openApiStore.getOpenAPISpec();
@@ -381,3 +394,10 @@ export async function startServers(
return { proxyServer, docsServer };
}
function createServerConfig(app: Hono, port: number): ServerConfig {
return {
fetch: app.fetch,
port,
};
}

View File

@@ -5,9 +5,9 @@ import type { OpenAPI, OpenAPIV3_1 } from 'openapi-types';
export interface SecurityInfo {
type: 'apiKey' | 'oauth2' | 'http' | 'openIdConnect';
scheme?: 'bearer' | 'basic' | 'digest';
name?: string;
in?: 'header' | 'query' | 'cookie';
scheme?: string;
flows?: {
implicit?: {
authorizationUrl: string;
@@ -91,7 +91,7 @@ type PathsObject = {
[path: string]: PathItemObject;
};
class OpenAPIStore {
export class OpenAPIStore {
private endpoints: Map<string, EndpointInfo>;
private harEntries: HAREntry[];
private targetUrl: string;

6
src/types.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { Hono } from 'hono';
export interface ServerConfig {
fetch: Hono['fetch'];
port: number;
}

View File

@@ -27,7 +27,7 @@
/* Modules */
"module": "NodeNext",
"rootDir": "./src",
"rootDir": ".",
"moduleResolution": "NodeNext",
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
@@ -59,7 +59,7 @@
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist",
"outDir": "dist",
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
@@ -110,6 +110,14 @@
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"include": [
"src/**/*.ts",
"integration/**/*.ts",
"*.ts",
"*.mjs"
],
"exclude": [
"node_modules",
"dist"
]
}