mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-26 07:08:01 +00:00
refactor(frontend): port api-docs/endpoints to TypeScript
endpoints.js was the only remaining JS file under src/. It's a pure data
file describing every panel API surface for the in-panel Swagger docs;
scripts/build-openapi.mjs reads it at build time to emit
public/openapi.json.
Convert it to endpoints.ts with explicit interfaces:
HttpMethod, ParamLocation, ParamType,
EndpointParam, Endpoint, SubscriptionHeader, Section
Type-checking surfaced shapes the .js had silently accepted:
- 'in' values beyond plain 'body' — 'body (form)', 'body (json)',
'body (multipart)' for non-JSON request bodies
- 'type' arrays — 'integer[]', 'object[]'
- Subscription section's subHeader documenting response headers
All four are now part of the union types so the existing data type-checks.
Dead exports removed:
- safeInlineHtml — unused since the docs page switched to Swagger UI
- methodColors — unused
Build pipeline:
- scripts/build-openapi.mjs imports endpoints.ts directly
- gen:api runs via Node 22's native --experimental-strip-types; no
tsx/ts-node dependency added
- --disable-warning=ExperimentalWarning silences just the strip-types
notice while keeping deprecation warnings intact
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"gen:api": "node scripts/build-openapi.mjs"
|
||||
"gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.3",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import { sections } from '../src/pages/api-docs/endpoints.js';
|
||||
import { sections } from '../src/pages/api-docs/endpoints.ts';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const outPath = join(__dirname, '..', 'public', 'openapi.json');
|
||||
|
||||
@@ -1,29 +1,59 @@
|
||||
export function safeInlineHtml(input) {
|
||||
if (!input) return '';
|
||||
const escape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const open = '<code>';
|
||||
const close = '</code>';
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < input.length) {
|
||||
const oIdx = input.indexOf(open, i);
|
||||
if (oIdx === -1) {
|
||||
out += escape(input.slice(i));
|
||||
break;
|
||||
}
|
||||
out += escape(input.slice(i, oIdx));
|
||||
const cIdx = input.indexOf(close, oIdx + open.length);
|
||||
if (cIdx === -1) {
|
||||
out += escape(input.slice(oIdx));
|
||||
break;
|
||||
}
|
||||
out += '<code>' + escape(input.slice(oIdx + open.length, cIdx)) + '</code>';
|
||||
i = cIdx + close.length;
|
||||
}
|
||||
return out;
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'WS';
|
||||
export type ParamLocation =
|
||||
| 'path'
|
||||
| 'query'
|
||||
| 'header'
|
||||
| 'body'
|
||||
| 'body (form)'
|
||||
| 'body (json)'
|
||||
| 'body (multipart)';
|
||||
export type ParamType =
|
||||
| 'string'
|
||||
| 'integer'
|
||||
| 'integer[]'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'object'
|
||||
| 'object[]'
|
||||
| 'array'
|
||||
| 'file';
|
||||
|
||||
export interface EndpointParam {
|
||||
name: string;
|
||||
in: ParamLocation;
|
||||
type: ParamType;
|
||||
desc?: string;
|
||||
optional?: boolean;
|
||||
defaultValue?: string | number | boolean;
|
||||
}
|
||||
|
||||
export const sections = [
|
||||
export interface Endpoint {
|
||||
method: HttpMethod;
|
||||
path: string;
|
||||
summary: string;
|
||||
description?: string;
|
||||
deprecated?: boolean;
|
||||
params?: EndpointParam[];
|
||||
body?: string;
|
||||
response?: string;
|
||||
errorResponse?: string;
|
||||
errorStatus?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptionHeader {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
subHeader?: SubscriptionHeader[];
|
||||
endpoints: Endpoint[];
|
||||
}
|
||||
|
||||
export const sections: readonly Section[] = [
|
||||
{
|
||||
id: 'authentication',
|
||||
title: 'Authentication',
|
||||
@@ -975,12 +1005,3 @@ export const sections = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const methodColors = {
|
||||
GET: 'blue',
|
||||
POST: 'green',
|
||||
PUT: 'orange',
|
||||
PATCH: 'orange',
|
||||
DELETE: 'red',
|
||||
WS: 'purple',
|
||||
};
|
||||
@@ -16,10 +16,10 @@ type routeDef struct {
|
||||
// routePattern matches route registrations like g.GET("/path", handler) or api.GET("/path", handler)
|
||||
var routePattern = regexp.MustCompile(`\b(g|api)\.(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\("([^"]+)"`)
|
||||
|
||||
// docRoutePattern matches { method: 'X', path: 'Y' ... } entries in endpoints.js.
|
||||
// docRoutePattern matches { method: 'X', path: 'Y' ... } entries in endpoints.ts.
|
||||
var docRoutePattern = regexp.MustCompile(`method:\s*'([A-Z]+)'\s*,\s*path:\s*'([^']+)'`)
|
||||
|
||||
// buildDocSet parses frontend/src/pages/api-docs/endpoints.js and returns the
|
||||
// buildDocSet parses frontend/src/pages/api-docs/endpoints.ts and returns the
|
||||
// set of documented "METHOD PATH" keys. WS pseudo-routes and subscription
|
||||
// placeholders (paths starting with /{...}) are skipped because they aren't
|
||||
// registered on the main Gin engine.
|
||||
@@ -29,10 +29,10 @@ func buildDocSet(t *testing.T) map[string]bool {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get current dir: %v", err)
|
||||
}
|
||||
endpointsPath := filepath.Join(controllerDir, "..", "..", "frontend", "src", "pages", "api-docs", "endpoints.js")
|
||||
endpointsPath := filepath.Join(controllerDir, "..", "..", "frontend", "src", "pages", "api-docs", "endpoints.ts")
|
||||
data, err := os.ReadFile(endpointsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read endpoints.js at %s: %v", endpointsPath, err)
|
||||
t.Fatalf("failed to read endpoints.ts at %s: %v", endpointsPath, err)
|
||||
}
|
||||
docSet := make(map[string]bool)
|
||||
for _, m := range docRoutePattern.FindAllStringSubmatch(string(data), -1) {
|
||||
@@ -150,7 +150,7 @@ func TestAPIRoutesDocumented(t *testing.T) {
|
||||
foundInDoc++
|
||||
} else {
|
||||
missingFromDocs++
|
||||
t.Errorf("Route not documented in endpoints.js: %s %s", r.Method, r.Path)
|
||||
t.Errorf("Route not documented in endpoints.ts: %s %s", r.Method, r.Path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,6 @@ func TestAPIRoutesDocumented(t *testing.T) {
|
||||
len(sourceSet), len(docSet), foundInDoc, missingFromDocs)
|
||||
|
||||
if missingFromDocs > 0 {
|
||||
t.Errorf("Found %d undocumented route(s). Update endpoints.js to match.", missingFromDocs)
|
||||
t.Errorf("Found %d undocumented route(s). Update endpoints.ts to match.", missingFromDocs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user