release: WRNexusJS 0.2.75
This commit is contained in:
@@ -31,6 +31,17 @@ export class HmrHub {
|
||||
this.sockets.delete(ws);
|
||||
}
|
||||
|
||||
broadcastJson(message: unknown): void {
|
||||
const payload = JSON.stringify(message);
|
||||
for (const ws of this.sockets) {
|
||||
try {
|
||||
ws.send(payload);
|
||||
} catch {
|
||||
this.sockets.delete(ws);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcast(message: HmrMessage): void {
|
||||
const payload = JSON.stringify(message);
|
||||
for (const ws of this.sockets) {
|
||||
|
||||
@@ -31,6 +31,10 @@ import { startWatcher } from "./watch.ts";
|
||||
export { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
import { resetDevCache } from "./cache.ts";
|
||||
|
||||
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
|
||||
|
||||
import { createDevToolbarCollector, type DevToolbarCollector } from "@wrnexus/dev-toolbar/server";
|
||||
|
||||
export interface ServeOptions {
|
||||
appDir: string;
|
||||
port?: number;
|
||||
@@ -62,6 +66,7 @@ export interface ServeOptions {
|
||||
storage?: StorageConfig;
|
||||
mobile?: MobileConfig;
|
||||
pwa?: PwaConfig | false;
|
||||
devToolbar?: boolean | DevToolbarConfig;
|
||||
}
|
||||
|
||||
export interface RunningServer {
|
||||
@@ -112,6 +117,40 @@ async function schemaRuntime(router: Router): Promise<string> {
|
||||
return renderSchemasScript(descriptors);
|
||||
}
|
||||
|
||||
function resolveDevToolbarConfig(
|
||||
mode: string,
|
||||
value: ServeOptions["devToolbar"],
|
||||
): DevToolbarConfig | null {
|
||||
if (mode !== "development" || value === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value === true || value === undefined) {
|
||||
return {
|
||||
enabled: true,
|
||||
position: "bottom-center",
|
||||
defaultOpen: false,
|
||||
scanOnNavigation: true,
|
||||
scanOnHmr: true,
|
||||
openEditor: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (value.enabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
position: "bottom-center",
|
||||
defaultOpen: false,
|
||||
scanOnNavigation: true,
|
||||
scanOnHmr: true,
|
||||
openEditor: true,
|
||||
...value,
|
||||
};
|
||||
}
|
||||
|
||||
export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
const appDir = resolve(opts.appDir);
|
||||
const mode: Mode = opts.mode ?? "development";
|
||||
@@ -124,6 +163,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
const styleEntry = opts.styleEntry ?? null;
|
||||
const appRoot = dirname(appDir);
|
||||
|
||||
const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar);
|
||||
|
||||
const devToolbarCollector: DevToolbarCollector | undefined = devToolbarConfig
|
||||
? createDevToolbarCollector()
|
||||
: undefined;
|
||||
|
||||
resetDevCache({
|
||||
rootDir: appRoot,
|
||||
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
|
||||
@@ -187,6 +232,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
);
|
||||
|
||||
const hub = hmr ? new HmrHub() : undefined;
|
||||
const unsubscribeDevToolbar =
|
||||
devToolbarCollector && hub
|
||||
? devToolbarCollector.subscribe((issues) => {
|
||||
hub.broadcastJson({ channel: "toolbar", type: "toolbar:issues", issues });
|
||||
})
|
||||
: undefined;
|
||||
const middleware = middlewareLoader(router);
|
||||
|
||||
const runtimeDeps = {
|
||||
@@ -207,6 +258,14 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
security: opts.security,
|
||||
hub,
|
||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||
devToolbar:
|
||||
devToolbarConfig && devToolbarCollector
|
||||
? {
|
||||
config: devToolbarConfig,
|
||||
collector: devToolbarCollector,
|
||||
root: appRoot,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
const handlers = createHandlers(runtimeDeps);
|
||||
|
||||
@@ -252,8 +311,20 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
|
||||
console.log(`[wrnexus] hot update — ${files.join(", ")}`);
|
||||
hub.reload();
|
||||
hub.broadcastJson({
|
||||
channel: "toolbar",
|
||||
type: "toolbar:scan",
|
||||
reason: "source-change",
|
||||
files,
|
||||
});
|
||||
};
|
||||
watcher = startWatcher({ appDir, hub, assets, onHotChange: hotUpdate });
|
||||
watcher = startWatcher({
|
||||
appDir,
|
||||
hub,
|
||||
assets,
|
||||
devToolbarCollector,
|
||||
onHotChange: hotUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
const boundPort = server.port ?? port;
|
||||
@@ -264,6 +335,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
router,
|
||||
stop: () => {
|
||||
watcher?.close();
|
||||
unsubscribeDevToolbar?.();
|
||||
server.stop();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -54,6 +54,14 @@ import {
|
||||
} from "@wrnexus/i18n";
|
||||
import { runMiddleware } from "./pipeline.ts";
|
||||
import type { HmrHub } from "./hmr.ts";
|
||||
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
|
||||
|
||||
import {
|
||||
createServerIssue,
|
||||
handleDevToolbarRoute,
|
||||
issueFromError,
|
||||
type DevToolbarCollector,
|
||||
} from "@wrnexus/dev-toolbar/server";
|
||||
|
||||
/** HTTP methods we recognize as API handler exports. */
|
||||
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const;
|
||||
@@ -131,6 +139,32 @@ export interface RuntimeDeps {
|
||||
* bus (use the Redis pub/sub driver). Enables realtime across multiple apps.
|
||||
*/
|
||||
realtimeBus?: RealtimeBus;
|
||||
|
||||
devToolbar?: {
|
||||
config: DevToolbarConfig;
|
||||
collector: DevToolbarCollector;
|
||||
root: string;
|
||||
};
|
||||
}
|
||||
|
||||
const DEV_TOOLBAR_SCRIPT =
|
||||
'<script type="module" src="/__wrnexus/dev-toolbar.js" data-wrnexus-dev-toolbar></script>';
|
||||
|
||||
function shouldEnableDevToolbar(mode: string, deps: RuntimeDeps): boolean {
|
||||
return (
|
||||
mode === "development" &&
|
||||
deps.devToolbar !== undefined &&
|
||||
deps.devToolbar.config.enabled !== false
|
||||
);
|
||||
}
|
||||
|
||||
function shouldReportNotFound(pathname: string): boolean {
|
||||
return !(
|
||||
pathname.startsWith("/__wrnexus/") ||
|
||||
pathname.startsWith("/.well-known/") ||
|
||||
pathname === "/favicon.ico" ||
|
||||
pathname.endsWith(".map")
|
||||
);
|
||||
}
|
||||
|
||||
export const PWA_CLIENT = `if ("serviceWorker" in navigator) {
|
||||
@@ -429,8 +463,12 @@ export const HMR_CLIENT_JS = `
|
||||
};
|
||||
ws.onmessage = function (e) {
|
||||
var msg; try { msg = JSON.parse(e.data); } catch (_) { return; }
|
||||
if (msg.type === "css") swapCss(msg.version);
|
||||
else if (msg.type === "reload") requestSync();
|
||||
if (msg.channel === "toolbar") {
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:toolbar-message", { detail: msg }));
|
||||
} else if (msg.type === "css") {
|
||||
swapCss(msg.version);
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:hmr", { detail: msg }));
|
||||
} else if (msg.type === "reload") requestSync();
|
||||
else if (msg.type === "html") applyHtml(msg.html);
|
||||
else if (msg.type === "error") console.error("[wrnexus] HMR update failed:", msg.message);
|
||||
};
|
||||
@@ -479,6 +517,7 @@ export const HMR_CLIENT_JS = `
|
||||
// their reactive scopes (and any browser-side API fetches) in place.
|
||||
if (window.__wrnexusHydrateScopes) window.__wrnexusHydrateScopes(document);
|
||||
if (window.__wrnexusHydrateCsrFetches) window.__wrnexusHydrateCsrFetches(document);
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:hmr", { detail: { type: "html" } }));
|
||||
}
|
||||
|
||||
// Minimal index-based DOM morph: preserve matching nodes (keeps state/focus),
|
||||
@@ -717,6 +756,17 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
return secure(new Response("Payload Too Large", { status: 413 }));
|
||||
}
|
||||
|
||||
if (deps.devToolbar) {
|
||||
const toolbarResponse = await handleDevToolbarRoute(req, {
|
||||
mode,
|
||||
root: deps.devToolbar.root,
|
||||
collector: deps.devToolbar.collector,
|
||||
editor: deps.devToolbar.config.editor,
|
||||
allowOpenEditor: deps.devToolbar.config.openEditor !== false,
|
||||
});
|
||||
if (toolbarResponse) return secure(toolbarResponse);
|
||||
}
|
||||
|
||||
// --- HMR socket (dev only): upgrade before anything else. ---
|
||||
if (hmr && url.pathname === "/__wrnexus/hmr") {
|
||||
if (!isWebSocketOriginAllowed(req, deps.security)) return secure(forbiddenOrigin());
|
||||
@@ -795,6 +845,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
console.error(
|
||||
`[wrnexus] unhandled request error (${app}) ${req.method} ${url.pathname}\n${detail}`,
|
||||
);
|
||||
deps.devToolbar?.collector.add(
|
||||
issueFromError(err, {
|
||||
ruleId: "server/request-error",
|
||||
category: "server",
|
||||
title: "Request processing failed",
|
||||
pathname: url.pathname,
|
||||
}),
|
||||
);
|
||||
const response = secure(renderError(err, mode));
|
||||
// Gateway-managed production apps bind to loopback. Carry a bounded,
|
||||
// encoded diagnostic to the parent gateway so centralized log collectors
|
||||
@@ -1004,6 +1062,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
result += await renderComponents(rendered, translate, depth + 1);
|
||||
} catch (err) {
|
||||
console.error(`[wrnexus] component '${name}' failed to render`, err);
|
||||
deps.devToolbar?.collector.add(
|
||||
issueFromError(err, {
|
||||
ruleId: "server/component-render-error",
|
||||
category: "server",
|
||||
title: `Component '${name}' failed to render`,
|
||||
source: { file: component.file, component: name },
|
||||
}),
|
||||
);
|
||||
result += body.slice(tagStart, end);
|
||||
}
|
||||
}
|
||||
@@ -1018,6 +1084,20 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
);
|
||||
const matched = router.matchPage(ctx.url.pathname);
|
||||
if (!matched) {
|
||||
if (deps.devToolbar && shouldReportNotFound(ctx.url.pathname)) {
|
||||
deps.devToolbar.collector.add(
|
||||
createServerIssue({
|
||||
ruleId: "routing/not-found",
|
||||
category: "routing",
|
||||
severity: "warning",
|
||||
title: "Route not found",
|
||||
message: `No page route matched ${ctx.url.pathname}.`,
|
||||
pathname: ctx.url.pathname,
|
||||
recommendation:
|
||||
"Verify the URL, page filename, dynamic route parameters, and route casing.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
const response = renderNotFound();
|
||||
if (!isMobileRequest) return response;
|
||||
const headers = new Headers(response.headers);
|
||||
@@ -1065,6 +1145,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[wrnexus] layout '${layoutName}' failed to render`, err);
|
||||
deps.devToolbar?.collector.add(
|
||||
issueFromError(err, {
|
||||
ruleId: "server/layout-render-error",
|
||||
category: "server",
|
||||
title: `Layout '${layoutName}' failed to render`,
|
||||
pathname: ctx.url.pathname,
|
||||
source: { file: layout.file },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,7 +1191,13 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n "),
|
||||
extraBody: hmr ? hmrClientTag((ctx.locals.cspNonce as string) ?? "") : undefined,
|
||||
extraBody:
|
||||
[
|
||||
hmr ? hmrClientTag((ctx.locals.cspNonce as string) ?? "") : "",
|
||||
shouldEnableDevToolbar(mode, deps) ? DEV_TOOLBAR_SCRIPT : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n") || undefined,
|
||||
htmlAttrs,
|
||||
});
|
||||
// Conditional GET: hash the page CONTENT (`body`), not the assembled shell —
|
||||
@@ -1121,6 +1216,12 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
etag: tag,
|
||||
"cache-control": "private, no-cache",
|
||||
...(shouldEnableDevToolbar(mode, deps)
|
||||
? {
|
||||
"x-wrnexus-dev-toolbar": "enabled",
|
||||
"x-wrnexus-route": matched.route.raw,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ const server = await startServer({
|
||||
storage: config.storage,
|
||||
mobile: config.mobile,
|
||||
pwa: config.pwa,
|
||||
devToolbar: config.devToolbar,
|
||||
});
|
||||
const r = server.router;
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
import { watch, type FSWatcher } from "node:fs";
|
||||
import type { HmrHub } from "./hmr.ts";
|
||||
import type { DevAssetServer } from "./assets.ts";
|
||||
import type { DevToolbarCollector } from "@wrnexus/dev-toolbar/server";
|
||||
|
||||
export interface WatchOptions {
|
||||
appDir: string;
|
||||
hub: HmrHub;
|
||||
assets: DevAssetServer;
|
||||
devToolbarCollector?: DevToolbarCollector;
|
||||
/** Called with changed app-relative files that need an in-process hot update. */
|
||||
onHotChange: (files: string[]) => void | Promise<void>;
|
||||
}
|
||||
@@ -50,6 +52,7 @@ export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
||||
pending.clear();
|
||||
const files = [...pendingFiles];
|
||||
pendingFiles.clear();
|
||||
for (const file of files) opts.devToolbarCollector?.clear(file);
|
||||
void Promise.resolve(opts.onHotChange(files)).catch((error) => {
|
||||
console.error("[wrnexus] hot update failed", error);
|
||||
});
|
||||
@@ -58,6 +61,12 @@ export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
||||
if (pending.has("css")) {
|
||||
assets.invalidateCss();
|
||||
hub.css();
|
||||
hub.broadcastJson({
|
||||
channel: "toolbar",
|
||||
type: "toolbar:scan",
|
||||
reason: "styles-change",
|
||||
files: [...pendingFiles],
|
||||
});
|
||||
}
|
||||
pending.clear();
|
||||
pendingFiles.clear();
|
||||
|
||||
Reference in New Issue
Block a user