release: WRNexusJS 0.3.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.79",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
@@ -20,6 +20,7 @@
|
||||
"@wrnexus/i18n": "workspace:*",
|
||||
"@wrnexus/db": "workspace:*",
|
||||
"@wrnexus/pubsub": "workspace:*",
|
||||
"@wrnexus/uploader": "workspace:*"
|
||||
"@wrnexus/uploader": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ export { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
import { resetDevCache } from "./cache.ts";
|
||||
|
||||
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
|
||||
import { createPluginRunner, type PluginInput } from "@wrnexus/plugin";
|
||||
import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles";
|
||||
|
||||
import { createDevToolbarCollector, type DevToolbarCollector } from "@wrnexus/dev-toolbar/server";
|
||||
|
||||
@@ -67,6 +69,9 @@ export interface ServeOptions {
|
||||
mobile?: MobileConfig;
|
||||
pwa?: PwaConfig | false;
|
||||
devToolbar?: boolean | DevToolbarConfig;
|
||||
plugins?: PluginInput;
|
||||
observability?: ObservabilityConfig;
|
||||
tenancy?: TenancyConfig;
|
||||
}
|
||||
|
||||
export interface RunningServer {
|
||||
@@ -153,7 +158,20 @@ function resolveDevToolbarConfig(
|
||||
|
||||
export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
const appDir = resolve(opts.appDir);
|
||||
const appRoot = dirname(appDir);
|
||||
const mode: Mode = opts.mode ?? "development";
|
||||
const pluginRunner = createPluginRunner(opts.plugins, {
|
||||
root: appRoot,
|
||||
mode,
|
||||
command: "dev",
|
||||
metadata: new Map(),
|
||||
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
|
||||
});
|
||||
await pluginRunner.configure(opts as unknown as Record<string, unknown>);
|
||||
await pluginRunner.configResolved(
|
||||
Object.freeze({ ...opts }) as Readonly<Record<string, unknown>>,
|
||||
);
|
||||
|
||||
const hmr = opts.hmr ?? mode === "development";
|
||||
const port = opts.port ?? 3000;
|
||||
const hostname = opts.hostname ?? "::";
|
||||
@@ -161,7 +179,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
|
||||
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
|
||||
const styleEntry = opts.styleEntry ?? null;
|
||||
const appRoot = dirname(appDir);
|
||||
|
||||
const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar);
|
||||
|
||||
@@ -256,6 +273,8 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
mobile: opts.mobile,
|
||||
pwa: opts.pwa,
|
||||
security: opts.security,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
hub,
|
||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||
devToolbar:
|
||||
@@ -278,6 +297,19 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
websocket: handlers.websocket,
|
||||
});
|
||||
|
||||
try {
|
||||
await pluginRunner.hook("configureServer", {
|
||||
server,
|
||||
router,
|
||||
handlers,
|
||||
assets,
|
||||
devToolbarCollector,
|
||||
});
|
||||
} catch (error) {
|
||||
server.stop();
|
||||
throw error;
|
||||
}
|
||||
|
||||
let watcher: ReturnType<typeof startWatcher>;
|
||||
|
||||
// In-process HMR: keep the server and socket alive, invalidate only changed
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
type ResolvedTheme,
|
||||
type MobileConfig,
|
||||
type PwaConfig,
|
||||
type ObservabilityConfig,
|
||||
type TenancyConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import { VALIDATE_RUNTIME } from "@wrnexus/validation";
|
||||
import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n";
|
||||
@@ -116,6 +118,10 @@ export interface ProdOptions {
|
||||
pwa?: PwaConfig | false;
|
||||
/** Framework security headers and CORS policy. */
|
||||
security?: SecurityConfig;
|
||||
/** Built-in request tracing and Server-Timing policy. */
|
||||
observability?: ObservabilityConfig;
|
||||
/** Built-in tenant identity resolution. */
|
||||
tenancy?: TenancyConfig;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
maxBodyBytes?: number;
|
||||
@@ -305,6 +311,8 @@ export function createProductionHandlers(
|
||||
mobile: opts.mobile,
|
||||
pwa: opts.pwa,
|
||||
security: opts.security,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
maxBodyBytes: opts.maxBodyBytes,
|
||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
withContextHeaders,
|
||||
withSecurityHeaders,
|
||||
resolveRequestUrl,
|
||||
tenantMiddleware,
|
||||
tracingMiddleware,
|
||||
type Context,
|
||||
type Middleware,
|
||||
type Mode,
|
||||
@@ -43,6 +45,8 @@ import {
|
||||
type ResolvedTheme,
|
||||
type MobileConfig,
|
||||
type PwaConfig,
|
||||
type ObservabilityConfig,
|
||||
type TenancyConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import {
|
||||
LANG_COOKIE,
|
||||
@@ -129,6 +133,10 @@ export interface RuntimeDeps {
|
||||
pwa?: PwaConfig | false;
|
||||
/** Framework security headers and CORS policy. */
|
||||
security?: SecurityConfig;
|
||||
/** Built-in request tracing and Server-Timing policy. */
|
||||
observability?: ObservabilityConfig;
|
||||
/** Built-in tenant identity resolution. */
|
||||
tenancy?: TenancyConfig;
|
||||
/** Max request body size in bytes (413 above this). Default 10 MB. */
|
||||
maxBodyBytes?: number;
|
||||
/** HMR hub for browser live-update sockets (dev only). */
|
||||
@@ -158,6 +166,65 @@ function shouldEnableDevToolbar(mode: string, deps: RuntimeDeps): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function tenantIdentityFromConfig(
|
||||
config: TenancyConfig,
|
||||
): (ctx: Context) => Promise<{ id: string; slug?: string } | null> {
|
||||
return async (ctx) => {
|
||||
const host = ctx.url.hostname.toLowerCase();
|
||||
|
||||
if (config.mode === "domain") return host ? { id: host, slug: host } : null;
|
||||
|
||||
if (config.mode === "path") {
|
||||
const segments = ctx.url.pathname.split("/").filter(Boolean);
|
||||
const prefix = config.pathPrefix?.replace(/^\/+|\/+$/g, "");
|
||||
const slug = prefix ? (segments[0] === prefix ? segments[1] : undefined) : segments[0];
|
||||
return slug ? { id: slug, slug } : null;
|
||||
}
|
||||
|
||||
if (config.mode === "subdomain" || config.mode === undefined) {
|
||||
const roots = config.rootDomains?.map((domain) => domain.toLowerCase()) ?? [];
|
||||
const root = roots.find((domain) => host === domain || host.endsWith(`.${domain}`));
|
||||
const slug = root ? host.slice(0, -(root.length + 1)) : host.split(".")[0];
|
||||
if (!slug || slug === host || slug === "www" || slug === "localhost") return null;
|
||||
return { id: slug, slug };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
function frameworkMiddleware(deps: RuntimeDeps): Middleware[] {
|
||||
const middleware: Middleware[] = [];
|
||||
|
||||
if (deps.observability && deps.observability.enabled !== false) {
|
||||
middleware.push(
|
||||
tracingMiddleware(undefined, {
|
||||
sampleRate: deps.observability.sampleRate,
|
||||
serverTiming: deps.observability.serverTiming,
|
||||
onComplete:
|
||||
deps.observability.exporter === "console"
|
||||
? (ctx, records) => {
|
||||
const total = records.find((record) => record.name === "http.request")?.durationMs;
|
||||
console.log(
|
||||
`[wrnexus:trace] ${ctx.req.method} ${ctx.url.pathname} ${total?.toFixed(2) ?? "0.00"}ms`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (deps.tenancy && deps.tenancy.mode !== "custom") {
|
||||
middleware.push(
|
||||
tenantMiddleware(tenantIdentityFromConfig(deps.tenancy), {
|
||||
required: deps.tenancy.required,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
function shouldReportNotFound(pathname: string): boolean {
|
||||
return !(
|
||||
pathname.startsWith("/__wrnexus/") ||
|
||||
@@ -597,6 +664,11 @@ export interface Handlers {
|
||||
/** Build the fetch + websocket handlers from a set of dependencies. */
|
||||
export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
const { mode, hmr, router, loadModule, getMiddleware, assets } = deps;
|
||||
const builtInMiddleware = frameworkMiddleware(deps);
|
||||
const resolveMiddleware = async (): Promise<Middleware[]> => [
|
||||
...builtInMiddleware,
|
||||
...(await getMiddleware()),
|
||||
];
|
||||
const maxBodyBytes = deps.maxBodyBytes ?? 10 * 1024 * 1024; // 10 MB default
|
||||
|
||||
// Server-side realtime room manager (shared by every `defineRoom` connection).
|
||||
@@ -832,7 +904,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
);
|
||||
ctx.t = makeT(deps.i18n, ctx.lang);
|
||||
}
|
||||
const mws = await getMiddleware();
|
||||
const mws = await resolveMiddleware();
|
||||
const res = secure(
|
||||
withContextHeaders(ctx, await runMiddleware(mws, ctx, () => dispatch(ctx))),
|
||||
);
|
||||
@@ -1271,7 +1343,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
}
|
||||
const res = withContextHeaders(
|
||||
ctx,
|
||||
await runMiddleware(await getMiddleware(), ctx, () => dispatch(ctx)),
|
||||
await runMiddleware(await resolveMiddleware(), ctx, () => dispatch(ctx)),
|
||||
);
|
||||
const html = await res.text();
|
||||
ws.send(JSON.stringify({ type: "html", html }));
|
||||
|
||||
@@ -43,6 +43,9 @@ const server = await startServer({
|
||||
mobile: config.mobile,
|
||||
pwa: config.pwa,
|
||||
devToolbar: config.devToolbar,
|
||||
plugins: config.plugins,
|
||||
observability: config.observability,
|
||||
tenancy: config.tenancy,
|
||||
});
|
||||
const r = server.router;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user