release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+237
View File
@@ -0,0 +1,237 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, dirname, join, relative, resolve } from "node:path";
import { buildRouter } from "@wrnexus/router";
export type SdkLanguage = "typescript" | "javascript" | "java" | "go" | "python";
export interface ApiOperation {
id: string;
method: string;
path: string;
source: string;
summary?: string;
description?: string;
webhook?: { event: string; payloadSchema?: string; signatureHeader?: string };
}
export interface ApiArtifacts {
operations: ApiOperation[];
files: string[];
}
function methods(file: string): string[] {
const source = readFileSync(file, "utf8");
const values = [
...source.matchAll(
/export\s+(?:async\s+)?(?:const|function)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g,
),
].map((match) => match[1]!);
if (file.endsWith(".wrn"))
for (const match of source.matchAll(/\bapi\s+(GET|POST|PUT|PATCH|DELETE)\s+/g))
values.push(match[1]!);
return [...new Set(values.length ? values : ["GET"])];
}
function openapiPath(path: string): string {
return path
.replace(/\[\.\.\.([A-Za-z_$][\w$]*)\]/g, "{$1}")
.replace(/\[([A-Za-z_$][\w$]*)\]/g, "{$1}")
.replace(/:([A-Za-z_$][\w$]*)/g, "{$1}")
.replace(/\*([A-Za-z_$][\w$]*)/g, "{$1}");
}
function operationId(method: string, path: string): string {
const words =
`${method.toLowerCase()}-${path.replace(/^\/api\/?/, "").replace(/[^A-Za-z0-9]+/g, "-") || "root"}`
.split("-")
.filter(Boolean);
return (
words[0]! +
words
.slice(1)
.map((word) => word[0]!.toUpperCase() + word.slice(1))
.join("")
);
}
export function inspectApi(appRoot: string): ApiOperation[] {
const root = resolve(appRoot);
const router = buildRouter(join(root, "app"));
return router.api.flatMap((route) => {
const sourceText = readFileSync(route.file, "utf8");
const field = (name: string) =>
new RegExp(`${name}\\s*:\\s*["']([^"']+)["']`).exec(sourceText)?.[1];
const webhook = /\b(?:defineWebhook\s*\(|webhook\s*=)\s*\{/.test(sourceText)
? {
event: field("event") ?? operationId("event", route.raw),
payloadSchema: field("payloadSchema"),
signatureHeader: field("signatureHeader"),
}
: undefined;
return methods(route.file).map((method) => ({
id: operationId(method, route.raw),
method,
path: openapiPath(route.raw),
source: relative(root, route.file).replace(/\\/g, "/"),
summary: field("summary"),
description: field("description"),
webhook,
}));
});
}
export function createOpenApi(operations: ApiOperation[], title = "WRNexus API") {
const paths: Record<string, Record<string, unknown>> = {};
for (const operation of operations) {
const parameters = [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => ({
name: match[1],
in: "path",
required: true,
schema: { type: "string" },
}));
(paths[operation.path] ??= {})[operation.method.toLowerCase()] = {
operationId: operation.id,
summary: operation.summary ?? `${operation.method} ${operation.path}`,
...(operation.description ? { description: operation.description } : {}),
tags: ["API"],
parameters,
responses: {
"200": {
description: "Successful response",
content: { "application/json": { schema: {} } },
},
"400": { description: "Invalid request" },
"500": { description: "Internal error" },
},
"x-wrnexus-source": operation.source,
};
}
const webhooks = Object.fromEntries(
operations
.filter((operation) => operation.webhook)
.map((operation) => [
operation.webhook!.event,
{
post: {
summary: operation.summary ?? operation.webhook!.event,
description: operation.description,
parameters: operation.webhook!.signatureHeader
? [
{
name: operation.webhook!.signatureHeader,
in: "header",
required: true,
schema: { type: "string" },
},
]
: [],
requestBody: {
required: true,
content: {
"application/json": {
schema: operation.webhook!.payloadSchema
? { $ref: operation.webhook!.payloadSchema }
: {},
},
},
},
responses: { "200": { description: "Webhook accepted" } },
"x-wrnexus-source": operation.source,
},
},
]),
);
return {
openapi: "3.1.0",
info: { title, version: "0.8.0" },
paths,
...(Object.keys(webhooks).length ? { webhooks } : {}),
};
}
function sdk(language: SdkLanguage, operations: ApiOperation[]): string {
const route = (operation: ApiOperation) =>
operation.path.replace(/\{([^}]+)\}/g, "${encodeURIComponent(params.$1)}");
if (language === "typescript" || language === "javascript")
return `${language === "typescript" ? "export type RequestOptions = { baseUrl?: string; headers?: HeadersInit };\ntype ApiEnvelope = { data?: unknown; error?: { message?: string } };\n" : ""}const request = async (${language === "typescript" ? "method: string, path: string, body: unknown, options: RequestOptions = {}" : "method, path, body, options = {}"}) => { const response = await globalThis.fetch((options.baseUrl || "") + path, { method, headers: { "content-type": "application/json", ...(options.headers || {}) }, body: body === undefined ? undefined : JSON.stringify(body) }); const value${language === "typescript" ? ": ApiEnvelope" : ""} = await response.json(); if (!response.ok) throw Object.assign(new Error(value?.error?.message || "API request failed"), { status: response.status, body: value }); return value.data ?? value; };\n${operations.map((operation) => `export const ${operation.id} = (params${language === "typescript" ? ": Record<string, string> = {}" : " = {}"}, body${language === "typescript" ? ": unknown" : ""}, options${language === "typescript" ? ": RequestOptions = {}" : " = {}"}) => { ${operation.path.includes("{") ? "" : "void params; "}return request("${operation.method}", \`${route(operation)}\`, body, options); };`).join("\n")}\n`;
if (language === "python")
return `import json, urllib.request\n\nclass WrnexusApi:\n def __init__(self, base_url): self.base_url = base_url.rstrip('/')\n def request(self, method, path, body=None):\n data = None if body is None else json.dumps(body).encode()\n request = urllib.request.Request(self.base_url + path, data=data, method=method, headers={'content-type':'application/json'})\n with urllib.request.urlopen(request) as response: return json.load(response)\n${operations.map((operation) => ` def ${operation.id}(self, path, body=None): return self.request('${operation.method}', path, body)`).join("\n")}\n`;
if (language === "go")
return `package wrnexussdk\n\nimport ("bytes"; "encoding/json"; "fmt"; "net/http")\ntype Client struct { BaseURL string; HTTP *http.Client }\nfunc (c *Client) Request(method, path string, body any) (map[string]any, error) { data,_:=json.Marshal(body); req,_:=http.NewRequest(method,c.BaseURL+path,bytes.NewReader(data)); req.Header.Set("content-type","application/json"); client:=c.HTTP;if client==nil{client=http.DefaultClient};res,err:=client.Do(req);if err!=nil{return nil,err};defer res.Body.Close();if res.StatusCode>=400{return nil,fmt.Errorf("API status %d",res.StatusCode)};var out map[string]any;err=json.NewDecoder(res.Body).Decode(&out);return out,err }\n`;
return `package dev.wrnexus.sdk;\nimport java.net.URI; import java.net.http.*;\npublic final class WrnexusApi { private final String baseUrl; private final HttpClient http = HttpClient.newHttpClient(); public WrnexusApi(String baseUrl){this.baseUrl=baseUrl;} public String request(String method,String path,String json)throws Exception{var request=HttpRequest.newBuilder(URI.create(baseUrl+path)).header("content-type","application/json").method(method,HttpRequest.BodyPublishers.ofString(json==null?"":json)).build();var response=http.send(request,HttpResponse.BodyHandlers.ofString());if(response.statusCode()>=400)throw new IllegalStateException("API status "+response.statusCode());return response.body();} }\n`;
}
function write(path: string, value: string, files: string[]): void {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, value);
files.push(path);
}
const escapeHtml = (value: unknown) =>
String(value ?? "").replace(
/[&<>"']/g,
(character) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
);
export function generateApiArtifacts(
appRoot: string,
languages: SdkLanguage[] = ["typescript"],
): ApiArtifacts {
const root = resolve(appRoot);
const output = join(root, "generated", "api");
const operations = inspectApi(root);
const files: string[] = [];
const spec = createOpenApi(operations, `${basename(root)} API`);
write(join(output, "openapi.json"), JSON.stringify(spec, null, 2) + "\n", files);
write(
join(output, "postman.json"),
JSON.stringify(
{
info: {
name: spec.info.title,
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
},
item: operations.map((operation) => ({
name: operation.id,
request: { method: operation.method, url: `{{baseUrl}}${operation.path}` },
})),
variable: [{ key: "baseUrl", value: "http://localhost:3000" }],
},
null,
2,
) + "\n",
files,
);
write(
join(output, "index.html"),
`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(spec.info.title)}</title><style>body{font:16px system-ui;max-width:960px;margin:auto;padding:2rem}code,pre{background:#f4f4f5;padding:.2rem .4rem}article{border-bottom:1px solid #ddd;padding:1rem 0}</style><h1>${escapeHtml(spec.info.title)}</h1><p>OpenAPI 3.1 · ${operations.length} operations · <a href="openapi.json">specification</a></p>${operations.map((operation) => `<article><h2><code>${escapeHtml(operation.method)}</code> ${escapeHtml(operation.path)}</h2><p>${escapeHtml(operation.summary ?? operation.id)}</p>${operation.description ? `<p>${escapeHtml(operation.description)}</p>` : ""}${operation.webhook ? `<p>Webhook event: <code>${escapeHtml(operation.webhook.event)}</code>${operation.webhook.signatureHeader ? ` · signature: <code>${escapeHtml(operation.webhook.signatureHeader)}</code>` : ""}</p>` : ""}<small>${escapeHtml(operation.source)}</small></article>`).join("")}`,
files,
);
write(
join(output, "examples.md"),
`# API examples\n\n${operations.map((operation) => `## ${operation.id}\n\n\`\`\`bash\ncurl -X ${operation.method} "http://localhost:3000${operation.path}"\n\`\`\`\n`).join("\n")}`,
files,
);
const extensions = {
typescript: "ts",
javascript: "js",
java: "java",
go: "go",
python: "py",
} as const;
for (const language of languages)
write(
join(output, "sdk", language, `wrnexus-api.${extensions[language]}`),
sdk(language, operations),
files,
);
return { operations, files };
}
export function runApiCommand(appRoot: string, kind: "api" | "sdk", args: string[]): ApiArtifacts {
const supported: SdkLanguage[] = ["typescript", "javascript", "java", "go", "python"];
const language = args.find((value) => supported.includes(value as SdkLanguage)) as
SdkLanguage | undefined;
if (kind === "sdk" && !language)
throw new Error("WRN-SDK-LANGUAGE: choose typescript, javascript, java, go, or python.");
const result = generateApiArtifacts(appRoot, kind === "sdk" ? [language!] : supported);
console.log(
`✓ Generated ${result.operations.length} API operations and ${result.files.length} artifacts in generated/api`,
);
return result;
}