@hansenexus/hud 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/next.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { n as HudMountProps } from "./define-hud-BHQR8Lbv.js";
2
+ import { ComponentType } from "react";
3
+ //#region src/next/env.d.ts
4
+ /**
5
+ * Where the HUD believes it runs. An explicit `NEXT_PUBLIC_HUD_ENV` wins over
6
+ * heuristics because k3s deploys have no `VERCEL_ENV`.
7
+ */
8
+ type HudEnv = "development" | "preview" | "staging" | "production" | "disabled";
9
+ /**
10
+ * Env ladder: kill switch → `NODE_ENV` (inlined by Next, so production
11
+ * bundles fold the development branch) → `NEXT_PUBLIC_HUD_ENV` (k8s overlays)
12
+ * → `VERCEL_ENV` → production.
13
+ *
14
+ * Each read is a literal `process.env.X` so Next can inline it in client code.
15
+ */
16
+ export declare function resolveHudEnv(): HudEnv;
17
+ export declare function hudEnvLabel(env?: HudEnv): string;
18
+ //#endregion
19
+ //#region src/next/loader.d.ts
20
+ type HudModuleLoader = () => Promise<{
21
+ default: ComponentType<HudMountProps>;
22
+ }>;
23
+ /**
24
+ * Mounts the app's `defineHud` module, lazily and client side only.
25
+ *
26
+ * The build gate belongs in the consumer's file, as a literal expression
27
+ * around the `import()`:
28
+ *
29
+ * ```tsx
30
+ * "use client";
31
+ * import { createHudLoader } from "@hansenexus/hud/next";
32
+ *
33
+ * export const HudLoader = createHudLoader(
34
+ * process.env.NODE_ENV === "development" || process.env.NEXT_PUBLIC_HUD_ENABLED === "true"
35
+ * ? () => import("./hud")
36
+ * : null
37
+ * );
38
+ * ```
39
+ *
40
+ * Next inlines both env reads, so a production build folds the ternary to
41
+ * `null` and emits no HUD chunk at all. A check inside this function could not
42
+ * do that: the `import()` would still sit in the consumer's module and the
43
+ * chunk would ship unreachable but readable (hn-monorepo#1283). Pass `null`
44
+ * and the loader renders nothing.
45
+ */
46
+ export declare function createHudLoader(load: HudModuleLoader | null): ComponentType<HudMountProps>;
47
+ //#endregion
48
+ export type { HudEnv, HudModuleLoader, HudMountProps };
package/dist/next.js ADDED
@@ -0,0 +1,77 @@
1
+ import { Suspense, lazy, useEffect, useState } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/next/env.ts
4
+ /**
5
+ * Env ladder: kill switch → `NODE_ENV` (inlined by Next, so production
6
+ * bundles fold the development branch) → `NEXT_PUBLIC_HUD_ENV` (k8s overlays)
7
+ * → `VERCEL_ENV` → production.
8
+ *
9
+ * Each read is a literal `process.env.X` so Next can inline it in client code.
10
+ */
11
+ function resolveHudEnv() {
12
+ if (process.env.NEXT_PUBLIC_HUD_DISABLED === "true") return "disabled";
13
+ if (process.env.NODE_ENV === "development") return "development";
14
+ const explicit = process.env.NEXT_PUBLIC_HUD_ENV;
15
+ if (explicit === "staging" || explicit === "preview" || explicit === "production") return explicit;
16
+ if (process.env.VERCEL_ENV === "preview") return "preview";
17
+ return "production";
18
+ }
19
+ const LABELS = {
20
+ development: "DEV",
21
+ preview: "PREVIEW",
22
+ staging: "STAGING",
23
+ production: "PROD",
24
+ disabled: "OFF"
25
+ };
26
+ function hudEnvLabel(env = resolveHudEnv()) {
27
+ return LABELS[env];
28
+ }
29
+ //#endregion
30
+ //#region src/next/loader.tsx
31
+ /**
32
+ * Mounts the app's `defineHud` module, lazily and client side only.
33
+ *
34
+ * The build gate belongs in the consumer's file, as a literal expression
35
+ * around the `import()`:
36
+ *
37
+ * ```tsx
38
+ * "use client";
39
+ * import { createHudLoader } from "@hansenexus/hud/next";
40
+ *
41
+ * export const HudLoader = createHudLoader(
42
+ * process.env.NODE_ENV === "development" || process.env.NEXT_PUBLIC_HUD_ENABLED === "true"
43
+ * ? () => import("./hud")
44
+ * : null
45
+ * );
46
+ * ```
47
+ *
48
+ * Next inlines both env reads, so a production build folds the ternary to
49
+ * `null` and emits no HUD chunk at all. A check inside this function could not
50
+ * do that: the `import()` would still sit in the consumer's module and the
51
+ * chunk would ship unreachable but readable (hn-monorepo#1283). Pass `null`
52
+ * and the loader renders nothing.
53
+ */
54
+ function createHudLoader(load) {
55
+ if (!load) return NoHud;
56
+ const LazyHud = lazy(load);
57
+ function HudLoader(props) {
58
+ const [mounted, setMounted] = useState(false);
59
+ useEffect(() => setMounted(true), []);
60
+ if (!mounted) return null;
61
+ const env = resolveHudEnv();
62
+ if (env === "disabled") return null;
63
+ return /* @__PURE__ */ jsx(Suspense, {
64
+ fallback: null,
65
+ children: /* @__PURE__ */ jsx(LazyHud, {
66
+ envLabel: hudEnvLabel(env),
67
+ ...props
68
+ })
69
+ });
70
+ }
71
+ return HudLoader;
72
+ }
73
+ function NoHud(_props) {
74
+ return null;
75
+ }
76
+ //#endregion
77
+ export { createHudLoader, hudEnvLabel, resolveHudEnv };
@@ -0,0 +1,25 @@
1
+ import { Metric } from "web-vitals";
2
+ //#region src/plugins/vitals/vitals-panel.d.ts
3
+ declare function VitalsPanel(): import("react").JSX.Element;
4
+ //#endregion
5
+ //#region src/plugins/vitals/store.d.ts
6
+ type VitalName = Metric["name"];
7
+ type VitalRating = Metric["rating"];
8
+ interface Vital {
9
+ name: VitalName;
10
+ value: number;
11
+ rating: VitalRating;
12
+ }
13
+ type VitalsSnapshot = Partial<Record<VitalName, Vital>>;
14
+ //#endregion
15
+ //#region src/plugins/vitals/index.d.ts
16
+ declare function VitalsPill(): import("react").JSX.Element;
17
+ /** Core Web Vitals: LCP in the pill, all five metrics in the panel. Client only. */
18
+ export declare function vitals(): {
19
+ readonly id: "vitals";
20
+ readonly title: "Vitals";
21
+ readonly pill: typeof VitalsPill;
22
+ readonly panel: typeof VitalsPanel;
23
+ };
24
+ //#endregion
25
+ export type { Vital, VitalName, VitalRating, VitalsSnapshot };
@@ -0,0 +1,109 @@
1
+ import { i as definePlugin } from "../plugins-CzUiTn_r.js";
2
+ import { useEffect, useSyncExternalStore } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
5
+ //#region src/plugins/vitals/store.ts
6
+ /** Display order: the three Core Web Vitals first, then the diagnostics. */
7
+ const VITAL_ORDER = [
8
+ "LCP",
9
+ "INP",
10
+ "CLS",
11
+ "FCP",
12
+ "TTFB"
13
+ ];
14
+ let snapshot = {};
15
+ const listeners = /* @__PURE__ */ new Set();
16
+ let started = false;
17
+ function record(metric) {
18
+ snapshot = {
19
+ ...snapshot,
20
+ [metric.name]: {
21
+ name: metric.name,
22
+ value: metric.value,
23
+ rating: metric.rating
24
+ }
25
+ };
26
+ for (const listener of listeners) listener();
27
+ }
28
+ /**
29
+ * Starts collection once per page. Called when the HUD mounts, not when the
30
+ * panel opens: LCP and FCP are only reported for the initial load, so a lazy
31
+ * start would miss them.
32
+ */
33
+ function startVitals() {
34
+ if (started) return;
35
+ started = true;
36
+ const opts = { reportAllChanges: true };
37
+ try {
38
+ onLCP(record, opts);
39
+ onINP(record, opts);
40
+ onCLS(record, opts);
41
+ onFCP(record, opts);
42
+ onTTFB(record, opts);
43
+ } catch {}
44
+ }
45
+ function getVitals() {
46
+ return snapshot;
47
+ }
48
+ function subscribeVitals(listener) {
49
+ listeners.add(listener);
50
+ return () => listeners.delete(listener);
51
+ }
52
+ function useVitals() {
53
+ return useSyncExternalStore(subscribeVitals, getVitals, getVitals);
54
+ }
55
+ function formatVital(name, value) {
56
+ if (name === "CLS") return value.toFixed(2);
57
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)} s`;
58
+ return `${Math.round(value)} ms`;
59
+ }
60
+ //#endregion
61
+ //#region src/plugins/vitals/vitals-panel.tsx
62
+ function VitalsPanel() {
63
+ const vitals = useVitals();
64
+ const rows = VITAL_ORDER.flatMap((name) => {
65
+ const vital = vitals[name];
66
+ return vital ? [vital] : [];
67
+ });
68
+ if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
69
+ className: "empty",
70
+ children: "No metrics yet. LCP reports after the first interaction or tab switch."
71
+ });
72
+ return /* @__PURE__ */ jsx("table", {
73
+ className: "vitals",
74
+ children: /* @__PURE__ */ jsx("tbody", { children: rows.map((vital) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
75
+ className: "label",
76
+ children: vital.name
77
+ }), /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("span", {
78
+ className: "dot",
79
+ "data-rating": vital.rating,
80
+ style: {
81
+ display: "inline-block",
82
+ marginRight: 6
83
+ }
84
+ }), formatVital(vital.name, vital.value)] })] }, vital.name)) })
85
+ });
86
+ }
87
+ //#endregion
88
+ //#region src/plugins/vitals/index.tsx
89
+ function VitalsPill() {
90
+ useEffect(() => {
91
+ startVitals();
92
+ }, []);
93
+ const lcp = useVitals().LCP;
94
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
95
+ className: "dot",
96
+ "data-rating": lcp?.rating
97
+ }), lcp ? `LCP ${formatVital("LCP", lcp.value)}` : "LCP –"] });
98
+ }
99
+ /** Core Web Vitals: LCP in the pill, all five metrics in the panel. Client only. */
100
+ function vitals() {
101
+ return definePlugin({
102
+ id: "vitals",
103
+ title: "Vitals",
104
+ pill: VitalsPill,
105
+ panel: VitalsPanel
106
+ });
107
+ }
108
+ //#endregion
109
+ export { vitals };
@@ -0,0 +1,76 @@
1
+ import { ComponentType } from "react";
2
+ //#region src/core/plugins.d.ts
3
+ /**
4
+ * A capability a plugin needs, such as `"cicd.read"`. Core assumes development
5
+ * and grants everything; the `access` plugin brings a matrix for deployed tiers.
6
+ */
7
+ type HudCapability = string;
8
+ type PinEdge = "left" | "right" | "bottom";
9
+ /** What a plugin's panel and pill receive from the shell. */
10
+ interface HudPluginContext {
11
+ /** App slug from `defineHud({ app })`. */
12
+ app: string;
13
+ /** Whether the current tier grants `capability`. */
14
+ can: (capability: HudCapability) => boolean;
15
+ /**
16
+ * Fetches JSON from this plugin's server routes: `api("/runs")` requests
17
+ * `<apiBase>/<plugin id>/runs`. Rejects with `HudApiError` on a non-2xx status.
18
+ */
19
+ api: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
20
+ /** Pins this plugin's panel to an edge. Below 768 px the pin is kept but not applied. */
21
+ pin: (edge: PinEdge) => void;
22
+ /** Closes this plugin's panel. */
23
+ close: () => void;
24
+ }
25
+ interface HudPluginProps {
26
+ ctx: HudPluginContext;
27
+ }
28
+ interface HudCommand {
29
+ /** Unique within the plugin. */
30
+ id: string;
31
+ title: string;
32
+ /** Shortcut in `mod+shift+k` form; `mod` is ⌘ on macOS and Ctrl elsewhere. */
33
+ shortcut?: string;
34
+ run: (ctx: HudPluginContext) => void;
35
+ }
36
+ type HudHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
37
+ interface HudRouteContext {
38
+ /** Values of `:name` segments in the matched path. */
39
+ params: Record<string, string>;
40
+ }
41
+ type HudRouteHandler = (request: Request, ctx: HudRouteContext) => Response | Promise<Response>;
42
+ /**
43
+ * Server routes keyed by path below the plugin's prefix, then by method:
44
+ * `{ "/runs/:id": { GET } }` serves `GET <apiBase>/<plugin id>/runs/42`.
45
+ */
46
+ type HudRoutes = Record<string, Partial<Record<HudHttpMethod, HudRouteHandler>>>;
47
+ /**
48
+ * The plugin contract. A plugin ships as two subpath exports that share this
49
+ * type: `@hansenexus/hud/plugins/<id>` carries the client half (`panel`,
50
+ * `pill`, `commands`) and `@hansenexus/hud/plugins/<id>/server` the server half
51
+ * (`routes`), so route code never reaches a client bundle and React never
52
+ * reaches the route handler. Both halves carry the same `id` and `capability`.
53
+ */
54
+ interface HudPlugin {
55
+ /** Unique per HUD; also the URL segment of the plugin's routes. */
56
+ id: string;
57
+ /** Panel title and pill label when the plugin has no `pill`. Defaults to `id`. */
58
+ title?: string;
59
+ /** Without it the plugin is dropped, client and server side. */
60
+ capability?: HudCapability;
61
+ /** Floating panel content, opened from the plugin's pill item. */
62
+ panel?: ComponentType<HudPluginProps>;
63
+ /** Live content inside the plugin's pill item. Keep it inline and non-interactive. */
64
+ pill?: ComponentType<HudPluginProps>;
65
+ commands?: readonly HudCommand[];
66
+ routes?: HudRoutes;
67
+ }
68
+ /** Identity helper that types a plugin object without widening it. */
69
+ declare function definePlugin<const P extends HudPlugin>(plugin: P): P;
70
+ declare class HudApiError extends Error {
71
+ readonly status: number;
72
+ readonly body: unknown;
73
+ constructor(status: number, body: unknown, url: string);
74
+ }
75
+ //#endregion
76
+ export { HudPlugin as a, HudRouteContext as c, PinEdge as d, definePlugin as f, HudHttpMethod as i, HudRouteHandler as l, HudCapability as n, HudPluginContext as o, HudCommand as r, HudPluginProps as s, HudApiError as t, HudRoutes as u };
@@ -0,0 +1,68 @@
1
+ //#region src/core/plugins.ts
2
+ const HUD_HTTP_METHODS = [
3
+ "GET",
4
+ "POST",
5
+ "PUT",
6
+ "PATCH",
7
+ "DELETE"
8
+ ];
9
+ /** Identity helper that types a plugin object without widening it. */
10
+ function definePlugin(plugin) {
11
+ return plugin;
12
+ }
13
+ const PLUGIN_ID = /^[a-z0-9][a-z0-9-]*$/;
14
+ /**
15
+ * Validates plugin ids and drops plugins whose capability `can` denies.
16
+ * Ids must be unique and URL-safe because they become route segments.
17
+ */
18
+ function resolvePlugins(plugins, can) {
19
+ const seen = /* @__PURE__ */ new Set();
20
+ for (const plugin of plugins) {
21
+ if (!PLUGIN_ID.test(plugin.id)) throw new Error(`hud: plugin id "${plugin.id}" must match ${PLUGIN_ID}`);
22
+ if (seen.has(plugin.id)) throw new Error(`hud: duplicate plugin id "${plugin.id}"`);
23
+ seen.add(plugin.id);
24
+ }
25
+ return plugins.filter((plugin) => plugin.capability === void 0 || can(plugin.capability));
26
+ }
27
+ var HudApiError = class extends Error {
28
+ status;
29
+ body;
30
+ constructor(status, body, url) {
31
+ super(`hud: ${url} answered ${status}`);
32
+ this.status = status;
33
+ this.body = body;
34
+ this.name = "HudApiError";
35
+ }
36
+ };
37
+ function createPluginContext(plugin, { app, apiBase, can, pin, close }) {
38
+ const prefix = `${apiBase.replace(/\/+$/, "")}/${plugin.id}`;
39
+ return {
40
+ app,
41
+ can,
42
+ pin,
43
+ close,
44
+ api: async (path, init) => {
45
+ const url = `${prefix}/${path.replace(/^\/+/, "")}`;
46
+ const headers = new Headers(init?.headers);
47
+ if (!headers.has("accept")) headers.set("accept", "application/json");
48
+ const res = await fetch(url, {
49
+ ...init,
50
+ headers
51
+ });
52
+ const body = parseBody(await res.text());
53
+ if (!res.ok) throw new HudApiError(res.status, body, url);
54
+ return body;
55
+ }
56
+ };
57
+ }
58
+ /** JSON when it parses, the raw text otherwise (an HTML error page, say). */
59
+ function parseBody(text) {
60
+ if (!text) return void 0;
61
+ try {
62
+ return JSON.parse(text);
63
+ } catch {
64
+ return text;
65
+ }
66
+ }
67
+ //#endregion
68
+ export { resolvePlugins as a, definePlugin as i, HudApiError as n, createPluginContext as r, HUD_HTTP_METHODS as t };
@@ -0,0 +1,39 @@
1
+ import { a as HudPlugin, c as HudRouteContext, f as definePlugin, i as HudHttpMethod, l as HudRouteHandler, n as HudCapability, u as HudRoutes } from "./plugins-1QmTmZyL.js";
2
+ //#region src/server/handler.d.ts
3
+ /**
4
+ * Decides whether a request may reach a plugin route. Return a `Response` to
5
+ * refuse it (404, 401, 403 ...) or `null` to let it through. The `access`
6
+ * plugin supplies one for deployed tiers.
7
+ */
8
+ type HudGuard = (request: Request, capability: HudCapability | undefined) => Response | null | Promise<Response | null>;
9
+ interface HudHandlerOptions {
10
+ /** Path the catch-all route is mounted at. Default `/api/hud`. */
11
+ basePath?: string;
12
+ /** Default: `developmentOnly`, which answers 404 unless `NODE_ENV` is `development`. */
13
+ guard?: HudGuard;
14
+ /** Drops plugins server side, like `defineHud({ can })` does client side. Default: grant all. */
15
+ can?: (capability: HudCapability) => boolean;
16
+ }
17
+ type HudRequestHandler = (request: Request) => Promise<Response>;
18
+ /** One function per method, so a Next route can `export const { GET, POST } = createHudHandler(...)`. */
19
+ type HudHandler = {
20
+ handle: HudRequestHandler;
21
+ } & Record<HudHttpMethod, HudRequestHandler>;
22
+ /**
23
+ * The default guard. Core assumes development, so a deployed build that
24
+ * mounts the route without an `access` guard serves nothing and reveals
25
+ * nothing about which plugins exist.
26
+ */
27
+ export declare const developmentOnly: HudGuard;
28
+ /**
29
+ * Serves every plugin's routes from one catch-all route
30
+ * (`app/api/hud/[...hud]/route.ts` in Next). A request to
31
+ * `<basePath>/<plugin id>/<path>` reaches that plugin's `routes[path][method]`;
32
+ * unknown plugin or path answers 404, a known path with another method 405.
33
+ *
34
+ * Pass the server halves (`@hansenexus/hud/plugins/<id>/server`): they carry
35
+ * `routes` and no React.
36
+ */
37
+ export declare function createHudHandler(plugins: readonly HudPlugin[], options?: HudHandlerOptions): HudHandler;
38
+ //#endregion
39
+ export { type HudGuard, type HudHandler, type HudHandlerOptions, type HudHttpMethod, type HudPlugin, type HudRequestHandler, type HudRouteContext, type HudRouteHandler, type HudRoutes, definePlugin };
package/dist/server.js ADDED
@@ -0,0 +1,106 @@
1
+ import { a as resolvePlugins, i as definePlugin, t as HUD_HTTP_METHODS } from "./plugins-CzUiTn_r.js";
2
+ //#region src/server/handler.ts
3
+ function json(status, body, headers) {
4
+ const res = Response.json(body, { status });
5
+ if (headers) for (const [key, value] of new Headers(headers)) res.headers.set(key, value);
6
+ return res;
7
+ }
8
+ function notFound() {
9
+ return json(404, {
10
+ ok: false,
11
+ error: "not found"
12
+ });
13
+ }
14
+ /**
15
+ * The default guard. Core assumes development, so a deployed build that
16
+ * mounts the route without an `access` guard serves nothing and reveals
17
+ * nothing about which plugins exist.
18
+ */
19
+ const developmentOnly = () => {
20
+ return (typeof process === "undefined" ? void 0 : process.env.NODE_ENV) === "development" ? null : notFound();
21
+ };
22
+ function splitPath(path) {
23
+ return path.split("/").filter(Boolean);
24
+ }
25
+ function compile(plugin) {
26
+ return Object.entries(plugin.routes ?? {}).map(([path, methods]) => ({
27
+ segments: splitPath(path),
28
+ methods
29
+ }));
30
+ }
31
+ function match(route, segments) {
32
+ if (route.segments.length !== segments.length) return null;
33
+ const params = {};
34
+ for (const [i, pattern] of route.segments.entries()) {
35
+ const segment = segments[i] ?? "";
36
+ if (!pattern.startsWith(":")) {
37
+ if (pattern !== segment) return null;
38
+ continue;
39
+ }
40
+ try {
41
+ params[pattern.slice(1)] = decodeURIComponent(segment);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ return params;
47
+ }
48
+ function isMethod(method) {
49
+ return HUD_HTTP_METHODS.includes(method);
50
+ }
51
+ /**
52
+ * Serves every plugin's routes from one catch-all route
53
+ * (`app/api/hud/[...hud]/route.ts` in Next). A request to
54
+ * `<basePath>/<plugin id>/<path>` reaches that plugin's `routes[path][method]`;
55
+ * unknown plugin or path answers 404, a known path with another method 405.
56
+ *
57
+ * Pass the server halves (`@hansenexus/hud/plugins/<id>/server`): they carry
58
+ * `routes` and no React.
59
+ */
60
+ function createHudHandler(plugins, options = {}) {
61
+ const basePath = splitPath(options.basePath ?? "/api/hud");
62
+ const guard = options.guard ?? developmentOnly;
63
+ const table = new Map(resolvePlugins(plugins, options.can ?? (() => true)).map((plugin) => [plugin.id, {
64
+ plugin,
65
+ routes: compile(plugin)
66
+ }]));
67
+ const handle = async (request) => {
68
+ const segments = splitPath(new URL(request.url).pathname);
69
+ if (!basePath.every((segment, i) => segments[i] === segment)) return notFound();
70
+ const [pluginId, ...rest] = segments.slice(basePath.length);
71
+ const entry = pluginId === void 0 ? void 0 : table.get(pluginId);
72
+ const refusal = await guard(request, entry?.plugin.capability);
73
+ if (refusal) return refusal;
74
+ if (!entry) return notFound();
75
+ for (const route of entry.routes) {
76
+ const params = match(route, rest);
77
+ if (!params) continue;
78
+ const method = request.method === "HEAD" ? "GET" : request.method;
79
+ const fn = isMethod(method) ? route.methods[method] : void 0;
80
+ if (!fn) return json(405, {
81
+ ok: false,
82
+ error: "method not allowed"
83
+ }, { allow: HUD_HTTP_METHODS.filter((m) => route.methods[m]).join(", ") });
84
+ try {
85
+ return await fn(request, { params });
86
+ } catch (error) {
87
+ console.error(`hud: ${entry.plugin.id} ${request.method} /${rest.join("/")} failed`, error);
88
+ return json(500, {
89
+ ok: false,
90
+ error: "plugin route failed"
91
+ });
92
+ }
93
+ }
94
+ return notFound();
95
+ };
96
+ return {
97
+ handle,
98
+ GET: handle,
99
+ POST: handle,
100
+ PUT: handle,
101
+ PATCH: handle,
102
+ DELETE: handle
103
+ };
104
+ }
105
+ //#endregion
106
+ export { createHudHandler, definePlugin, developmentOnly };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hansenexus/hud",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Modular dev HUD for React apps: status pill, floating panels, element grabber, plugin API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,6 +21,18 @@
21
21
  ".": {
22
22
  "types": "./dist/index.d.ts",
23
23
  "import": "./dist/index.js"
24
+ },
25
+ "./server": {
26
+ "types": "./dist/server.d.ts",
27
+ "import": "./dist/server.js"
28
+ },
29
+ "./next": {
30
+ "types": "./dist/next.d.ts",
31
+ "import": "./dist/next.js"
32
+ },
33
+ "./plugins/*": {
34
+ "types": "./dist/plugins/*.d.ts",
35
+ "import": "./dist/plugins/*.js"
24
36
  }
25
37
  },
26
38
  "sideEffects": false,
@@ -52,6 +64,7 @@
52
64
  "@types/react": "^19.3.0",
53
65
  "@types/react-dom": "^19.3.0",
54
66
  "jsdom": "^30.1.2",
67
+ "rolldown": "^1.2.12",
55
68
  "tsdown": "^0.23.0",
56
69
  "typescript": "^7.0.2",
57
70
  "vitest": "^5.0.3"