release: WRNexusJS 0.2.75

This commit is contained in:
2026-07-21 12:30:46 +05:30
parent 2ca2d02b22
commit 69b6cd431f
100 changed files with 2313 additions and 116 deletions
@@ -0,0 +1,52 @@
import type { DevToolbarIssue } from "../types.ts";
export type DevToolbarIssueListener = (issues: DevToolbarIssue[]) => void;
export interface DevToolbarCollector {
add(issue: DevToolbarIssue): void;
addMany(issues: DevToolbarIssue[]): void;
clear(scope?: string): void;
getIssues(pathname?: string): DevToolbarIssue[];
subscribe(listener: DevToolbarIssueListener): () => void;
}
export function createDevToolbarCollector(): DevToolbarCollector {
const issues = new Map<string, DevToolbarIssue>();
const listeners = new Set<DevToolbarIssueListener>();
const emit = () => {
const snapshot = [...issues.values()].sort((a, b) => b.createdAt - a.createdAt);
for (const listener of listeners) listener(snapshot);
};
return {
add(issue) {
issues.set(issue.fingerprint, issue);
emit();
},
addMany(next) {
for (const issue of next) issues.set(issue.fingerprint, issue);
emit();
},
clear(scope) {
if (!scope) issues.clear();
else
for (const [key, issue] of issues) {
const pathname =
typeof issue.metadata?.pathname === "string" ? issue.metadata.pathname : undefined;
if (pathname === scope || issue.source?.file === scope || issue.category === scope)
issues.delete(key);
}
emit();
},
getIssues(pathname) {
const values = [...issues.values()];
if (!pathname) return values;
return values.filter(
(issue) => !issue.metadata?.pathname || issue.metadata.pathname === pathname,
);
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
+61
View File
@@ -0,0 +1,61 @@
import { isAbsolute, relative, resolve } from "node:path";
export interface OpenEditorRequest {
file: string;
line?: number;
column?: number;
}
export interface OpenEditorOptions {
root: string;
editor?: string;
spawn?: (command: string[], options?: { cwd?: string }) => unknown;
}
const EDITORS: Record<string, (file: string, line: number, column: number) => string[]> = {
code: (file, line, column) => ["code", "--goto", `${file}:${line}:${column}`],
"code-insiders": (file, line, column) => ["code-insiders", "--goto", `${file}:${line}:${column}`],
cursor: (file, line, column) => ["cursor", "--goto", `${file}:${line}:${column}`],
windsurf: (file, line, column) => ["windsurf", "--goto", `${file}:${line}:${column}`],
zed: (file, line, column) => ["zed", `${file}:${line}:${column}`],
sublime: (file, line, column) => ["subl", `${file}:${line}:${column}`],
webstorm: (file, line) => ["webstorm", "--line", String(line), file],
idea: (file, line) => ["idea", "--line", String(line), file],
};
export function resolveEditorFile(root: string, file: string): string {
if (!file || file.includes("\0") || /^https?:\/\//i.test(file))
throw new Error("Invalid source file path.");
const resolvedRoot = resolve(root);
const resolvedFile = isAbsolute(file) ? resolve(file) : resolve(resolvedRoot, file);
const rel = relative(resolvedRoot, resolvedFile);
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))) return resolvedFile;
throw new Error("Source file is outside the configured project root.");
}
export function buildEditorCommand(
request: OpenEditorRequest,
options: OpenEditorOptions,
): string[] {
const file = resolveEditorFile(options.root, request.file);
const line = Math.max(1, Math.floor(request.line ?? 1));
const column = Math.max(1, Math.floor(request.column ?? 1));
const editor =
options.editor ??
process.env.WRNEXUS_EDITOR ??
process.env.VISUAL ??
process.env.EDITOR ??
"code";
const factory = EDITORS[editor];
if (!factory) throw new Error(`Unsupported editor: ${editor}`);
return factory(file, line, column);
}
export function openInEditor(request: OpenEditorRequest, options: OpenEditorOptions): void {
const command = buildEditorCommand(request, options);
const spawn =
options.spawn ??
((args: string[], spawnOptions?: { cwd?: string }) =>
Bun.spawn(args, { cwd: spawnOptions?.cwd, stdout: "ignore", stderr: "ignore" }));
spawn(command, { cwd: resolve(options.root) });
}
+6
View File
@@ -0,0 +1,6 @@
export * from "./collector.ts";
export * from "./registry.ts";
export * from "./serialize.ts";
export * from "./issues.ts";
export * from "./editor.ts";
export * from "./routes.ts";
+60
View File
@@ -0,0 +1,60 @@
import type {
DevToolbarCategory,
DevToolbarIssue,
DevToolbarSeverity,
DevToolbarSourceLocation,
} from "../types.ts";
export function createServerIssue(input: {
ruleId: string;
category: DevToolbarCategory;
severity: DevToolbarSeverity;
title: string;
message: string;
pathname?: string;
source?: DevToolbarSourceLocation;
stack?: string;
recommendation?: string;
}): DevToolbarIssue {
const sourceKey = input.source?.file
? `${input.source.file}:${input.source.line ?? 0}:${input.source.column ?? 0}`
: "";
return {
id: `wrn-server-${Date.now()}-${Math.random().toString(36).slice(2)}`,
ruleId: input.ruleId,
category: input.category,
severity: input.severity,
title: input.title,
message: input.message,
source: input.source,
recommendation: input.recommendation,
fingerprint: [input.ruleId, input.pathname ?? "", sourceKey, input.message].join("|"),
createdAt: Date.now(),
confidence: "high",
metadata: { pathname: input.pathname, stack: input.stack },
};
}
export function issueFromError(
error: unknown,
input: {
ruleId?: string;
category?: DevToolbarCategory;
title?: string;
pathname?: string;
source?: DevToolbarSourceLocation;
} = {},
): DevToolbarIssue {
const normalized = error instanceof Error ? error : new Error(String(error));
return createServerIssue({
ruleId: input.ruleId ?? "server/unhandled-error",
category: input.category ?? "server",
severity: "error",
title: input.title ?? normalized.name ?? "Server error",
message: normalized.message,
pathname: input.pathname,
source: input.source,
stack: normalized.stack,
recommendation: "Inspect the stack trace and open the referenced source file.",
});
}
@@ -0,0 +1,28 @@
export interface DevToolbarApp {
id: string;
name: string;
icon?: string;
entrypoint?: string;
order?: number;
}
export interface DevToolbarRegistry {
registerApp(app: DevToolbarApp): void;
unregisterApp(id: string): void;
getApps(): DevToolbarApp[];
}
export function createDevToolbarRegistry(): DevToolbarRegistry {
const apps = new Map<string, DevToolbarApp>();
return {
registerApp(app) {
apps.set(app.id, app);
},
unregisterApp(id) {
apps.delete(id);
},
getApps() {
return [...apps.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
},
};
}
+59
View File
@@ -0,0 +1,59 @@
import type { DevToolbarCollector } from "./collector.ts";
import { DEV_TOOLBAR_CSS, DEV_TOOLBAR_RUNTIME } from "../client/index.ts";
import { openInEditor } from "./editor.ts";
import { serializeDevToolbarJson } from "./serialize.ts";
export interface DevToolbarRouteOptions {
mode: string;
root: string;
collector: DevToolbarCollector;
editor?: string;
allowOpenEditor?: boolean;
}
const json = (value: unknown, status = 200) =>
new Response(serializeDevToolbarJson(value), {
status,
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
});
export async function handleDevToolbarRoute(
request: Request,
options: DevToolbarRouteOptions,
): Promise<Response | null> {
const url = new URL(request.url);
if (!url.pathname.startsWith("/__wrnexus/dev-toolbar")) return null;
if (options.mode !== "development") return new Response("Not found", { status: 404 });
if (url.pathname === "/__wrnexus/dev-toolbar.js" && request.method === "GET")
return new Response(DEV_TOOLBAR_RUNTIME, {
headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" },
});
if (url.pathname === "/__wrnexus/dev-toolbar.css" && request.method === "GET")
return new Response(DEV_TOOLBAR_CSS, {
headers: { "content-type": "text/css; charset=utf-8", "cache-control": "no-store" },
});
if (url.pathname === "/__wrnexus/dev-toolbar/issues" && request.method === "GET")
return json({
issues: options.collector.getIssues(url.searchParams.get("pathname") ?? undefined),
});
if (url.pathname === "/__wrnexus/dev-toolbar/open-editor" && request.method === "POST") {
if (options.allowOpenEditor === false)
return json({ error: "Open in editor is disabled." }, 403);
const body = (await request.json().catch(() => null)) as {
file?: string;
line?: number;
column?: number;
} | null;
if (!body?.file) return json({ error: "file is required" }, 400);
try {
openInEditor(
{ file: body.file, line: body.line, column: body.column },
{ root: options.root, editor: options.editor },
);
return json({ ok: true });
} catch (error) {
return json({ error: error instanceof Error ? error.message : String(error) }, 400);
}
}
return new Response("Not found", { status: 404 });
}
@@ -0,0 +1,8 @@
export function serializeDevToolbarJson(value: unknown): string {
return JSON.stringify(value)
.replace(/</g, "\\u003c")
.replace(/>/g, "\\u003e")
.replace(/&/g, "\\u0026")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
}