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",
|
"preview": "vite preview",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"gen:api": "node scripts/build-openapi.mjs"
|
"gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.2.3",
|
"@ant-design/icons": "^6.2.3",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { writeFileSync } from 'node:fs';
|
|||||||
import { join, dirname } from 'node:path';
|
import { join, dirname } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
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 __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const outPath = join(__dirname, '..', 'public', 'openapi.json');
|
const outPath = join(__dirname, '..', 'public', 'openapi.json');
|
||||||
|
|||||||
@@ -1,29 +1,59 @@
|
|||||||
export function safeInlineHtml(input) {
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'WS';
|
||||||
if (!input) return '';
|
export type ParamLocation =
|
||||||
const escape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 'path'
|
||||||
const open = '<code>';
|
| 'query'
|
||||||
const close = '</code>';
|
| 'header'
|
||||||
let out = '';
|
| 'body'
|
||||||
let i = 0;
|
| 'body (form)'
|
||||||
while (i < input.length) {
|
| 'body (json)'
|
||||||
const oIdx = input.indexOf(open, i);
|
| 'body (multipart)';
|
||||||
if (oIdx === -1) {
|
export type ParamType =
|
||||||
out += escape(input.slice(i));
|
| 'string'
|
||||||
break;
|
| 'integer'
|
||||||
}
|
| 'integer[]'
|
||||||
out += escape(input.slice(i, oIdx));
|
| 'number'
|
||||||
const cIdx = input.indexOf(close, oIdx + open.length);
|
| 'boolean'
|
||||||
if (cIdx === -1) {
|
| 'object'
|
||||||
out += escape(input.slice(oIdx));
|
| 'object[]'
|
||||||
break;
|
| 'array'
|
||||||
}
|
| 'file';
|
||||||
out += '<code>' + escape(input.slice(oIdx + open.length, cIdx)) + '</code>';
|
|
||||||
i = cIdx + close.length;
|
export interface EndpointParam {
|
||||||
}
|
name: string;
|
||||||
return out;
|
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',
|
id: 'authentication',
|
||||||
title: '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)
|
// 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)\("([^"]+)"`)
|
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*'([^']+)'`)
|
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
|
// set of documented "METHOD PATH" keys. WS pseudo-routes and subscription
|
||||||
// placeholders (paths starting with /{...}) are skipped because they aren't
|
// placeholders (paths starting with /{...}) are skipped because they aren't
|
||||||
// registered on the main Gin engine.
|
// registered on the main Gin engine.
|
||||||
@@ -29,10 +29,10 @@ func buildDocSet(t *testing.T) map[string]bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to get current dir: %v", err)
|
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)
|
data, err := os.ReadFile(endpointsPath)
|
||||||
if err != nil {
|
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)
|
docSet := make(map[string]bool)
|
||||||
for _, m := range docRoutePattern.FindAllStringSubmatch(string(data), -1) {
|
for _, m := range docRoutePattern.FindAllStringSubmatch(string(data), -1) {
|
||||||
@@ -150,7 +150,7 @@ func TestAPIRoutesDocumented(t *testing.T) {
|
|||||||
foundInDoc++
|
foundInDoc++
|
||||||
} else {
|
} else {
|
||||||
missingFromDocs++
|
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)
|
len(sourceSet), len(docSet), foundInDoc, missingFromDocs)
|
||||||
|
|
||||||
if missingFromDocs > 0 {
|
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