release: WRNexusJS 0.5.10
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { join, resolve } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { RESTART_EXIT_CODE } from "./restart.ts";
|
||||
|
||||
@@ -150,11 +151,15 @@ export function stripInternalError(res: Response): Response {
|
||||
}
|
||||
|
||||
/** Preserve an intentional verifier redirect while keeping other failures opaque. */
|
||||
export function forwardAuthFailure(res: Response, verifierUrl: string): Response {
|
||||
export function forwardAuthFailure(
|
||||
res: Response,
|
||||
verifierUrl: string,
|
||||
verifierPublicOrigin = verifierUrl,
|
||||
): Response {
|
||||
const location = res.headers.get("location");
|
||||
if (res.status >= 300 && res.status < 400 && location) {
|
||||
try {
|
||||
const redirect = new URL(location, verifierUrl);
|
||||
const redirect = new URL(location, verifierPublicOrigin);
|
||||
if (redirect.protocol === "http:" || redirect.protocol === "https:") {
|
||||
return new Response(null, { status: res.status, headers: { location: redirect.href } });
|
||||
}
|
||||
@@ -197,6 +202,7 @@ async function checkAuth(
|
||||
req: Request,
|
||||
ip: string,
|
||||
internalOrigins: Readonly<Record<string, string>>,
|
||||
publicOrigins: Readonly<Record<string, string>>,
|
||||
publicOrigin?: string,
|
||||
): Promise<Response | null> {
|
||||
if (!auth) return null;
|
||||
@@ -226,6 +232,10 @@ async function checkAuth(
|
||||
|
||||
if (auth.forward) {
|
||||
const verifyUrl = resolveForwardAuthUrl(auth.forward, internalOrigins);
|
||||
const verifierPublicOrigin =
|
||||
typeof auth.forward.app === "string"
|
||||
? publicOrigins[auth.forward.app]
|
||||
: new URL(auth.forward.url).origin;
|
||||
|
||||
try {
|
||||
const res = await fetch(verifyUrl, {
|
||||
@@ -240,7 +250,7 @@ async function checkAuth(
|
||||
`[wrnexus] forward-auth verifier error: ${req.method} ${requestUrl.pathname} via ${verifyUrl} returned ${res.status}${diagnostic ? ` — ${diagnostic}` : ""}`,
|
||||
);
|
||||
}
|
||||
return forwardAuthFailure(res, verifyUrl);
|
||||
return forwardAuthFailure(res, verifyUrl, verifierPublicOrigin);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -351,11 +361,21 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
const hostname = opts.hostname ?? defaultGatewayHostname(mode);
|
||||
const environment = opts.environment ?? (mode === "production" ? "production" : "development");
|
||||
|
||||
const workspaceOrigins = Object.fromEntries(
|
||||
opts.apps.map((app) => [app.name, app.publicOrigin ?? `http://${app.domains[0]}:${port}`]),
|
||||
const workspaceOrigins: Readonly<Record<string, string>> = Object.freeze(
|
||||
Object.fromEntries(
|
||||
opts.apps.map((app) => [app.name, app.publicOrigin ?? `http://${app.domains[0]}:${port}`]),
|
||||
),
|
||||
);
|
||||
const displayHost = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname;
|
||||
const serveEntry = fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry"));
|
||||
// When the CLI is executed directly from a framework checkout, keep child
|
||||
// apps on that same source tree. Resolving the package name from an external
|
||||
// workspace can otherwise select its older installed release on restart.
|
||||
const sourceServeEntry = fileURLToPath(new URL("./serve-entry.ts", import.meta.url));
|
||||
const usesSourceServeEntry = existsSync(sourceServeEntry);
|
||||
const serveEntry = usesSourceServeEntry
|
||||
? sourceServeEntry
|
||||
: fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry"));
|
||||
const frameworkRoot = resolve(dirname(sourceServeEntry), "../../..");
|
||||
|
||||
let stopping = false;
|
||||
const targets: Target[] = opts.apps.map((app, i) => {
|
||||
@@ -397,6 +417,11 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
// Bun can otherwise resolve bare @wrnexus imports against the
|
||||
// external application's node_modules after an HMR restart.
|
||||
// Keep source-checkout children anchored to the same framework
|
||||
// checkout as the gateway command.
|
||||
cwd: usesSourceServeEntry ? frameworkRoot : dir,
|
||||
env: {
|
||||
...process.env,
|
||||
WRNEXUS_ENV: environment,
|
||||
@@ -496,7 +521,14 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
}
|
||||
|
||||
// Per-app access control (basic auth / IP allowlist / forward-auth).
|
||||
const denied = await checkAuth(target.auth, req, ip, internalOrigins, target.publicOrigin);
|
||||
const denied = await checkAuth(
|
||||
target.auth,
|
||||
req,
|
||||
ip,
|
||||
internalOrigins,
|
||||
workspaceOrigins,
|
||||
target.publicOrigin,
|
||||
);
|
||||
if (denied) {
|
||||
if (sec.accessLog)
|
||||
console.log(
|
||||
|
||||
@@ -16,11 +16,19 @@ import {
|
||||
type ThemeConfig,
|
||||
type MobileConfig,
|
||||
type PwaConfig,
|
||||
type NavigationConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import { uiComponentsDir, uiCssPath } from "@wrnexus/ui";
|
||||
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
|
||||
import { loadLocales, resolveI18n, type I18nConfig } from "@wrnexus/i18n";
|
||||
import { applyMigrations, migrate, setDb, registerDb } from "@wrnexus/db";
|
||||
import {
|
||||
applyMigrations,
|
||||
loadMigrations,
|
||||
migrate,
|
||||
setDb,
|
||||
registerDb,
|
||||
registerLazyDb,
|
||||
} from "@wrnexus/db";
|
||||
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
|
||||
import { realtimeBusFromConfig } from "./realtime-bus.ts";
|
||||
@@ -76,6 +84,7 @@ export interface ServeOptions {
|
||||
plugins?: PluginInput;
|
||||
observability?: ObservabilityConfig;
|
||||
tenancy?: TenancyConfig;
|
||||
navigation?: NavigationConfig;
|
||||
}
|
||||
|
||||
export interface RunningServer {
|
||||
@@ -217,6 +226,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
plugins: configuredPlugins,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
navigation: opts.navigation,
|
||||
};
|
||||
|
||||
await pluginRunner.configure(pluginConfig);
|
||||
@@ -304,8 +314,14 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
}
|
||||
};
|
||||
if (opts.db) await connectAndMigrate(null, opts.db);
|
||||
for (const [name, cfg] of Object.entries(opts.databases ?? {}))
|
||||
await connectAndMigrate(name, cfg);
|
||||
for (const [name, cfg] of Object.entries(opts.databases ?? {})) {
|
||||
const migrationDir = join(appDir, "db", name, "migrations");
|
||||
const hasMigrations =
|
||||
loadMigrations(migrationDir).length > 0 ||
|
||||
resolvePackageMigrations(pluginContributions.migrations, name).length > 0;
|
||||
if (hasMigrations) await connectAndMigrate(name, cfg);
|
||||
else registerLazyDb(name, () => connectFromConfig(cfg, appRoot));
|
||||
}
|
||||
|
||||
// File-upload storage: build a driver per configured store (local dir / S3).
|
||||
// Relative local dirs resolve against the app root; served/served-back below.
|
||||
@@ -358,6 +374,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
security: opts.security,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
navigation: opts.navigation,
|
||||
clientRuntimes: pluginContributions.clientRuntimes,
|
||||
hub,
|
||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||
|
||||
@@ -25,10 +25,11 @@ import {
|
||||
type PwaConfig,
|
||||
type ObservabilityConfig,
|
||||
type TenancyConfig,
|
||||
type NavigationConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import { VALIDATE_RUNTIME } from "@wrnexus/validation";
|
||||
import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n";
|
||||
import { setDb, registerDb, getDb, hasDb, migrate } from "@wrnexus/db";
|
||||
import { setDb, registerLazyDb, getDb, hasDb, migrate } from "@wrnexus/db";
|
||||
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||
import {
|
||||
configureStorage,
|
||||
@@ -133,6 +134,8 @@ export interface ProdOptions {
|
||||
observability?: ObservabilityConfig;
|
||||
/** Built-in tenant identity resolution. */
|
||||
tenancy?: TenancyConfig;
|
||||
/** Page navigation strategy. */
|
||||
navigation?: NavigationConfig;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
maxBodyBytes?: number;
|
||||
@@ -287,14 +290,7 @@ export function createProductionHandlers(
|
||||
}
|
||||
}
|
||||
for (const [name, cfg] of Object.entries(opts.databases ?? {})) {
|
||||
try {
|
||||
registerDb(name, connectFromConfig(cfg));
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[wrnexus] database '${name}' setup failed:`,
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
registerLazyDb(name, () => connectFromConfig(cfg));
|
||||
}
|
||||
|
||||
// File-upload storage. Relative local dirs resolve against the deployment cwd
|
||||
@@ -334,6 +330,7 @@ export function createProductionHandlers(
|
||||
security: opts.security,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
navigation: opts.navigation,
|
||||
maxBodyBytes: opts.maxBodyBytes,
|
||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||
});
|
||||
|
||||
@@ -133,6 +133,8 @@ export interface RuntimeDeps {
|
||||
assetVersion?: string;
|
||||
/** Package browser runtimes resolved by the plugin system. */
|
||||
clientRuntimes?: ClientRuntimeDefinition[];
|
||||
/** Page navigation strategy. `document` disables same-origin link interception. */
|
||||
navigation?: { mode?: "client" | "document" };
|
||||
/** Raw HTML appended to every page head (e.g. CDN framework links). */
|
||||
head?: string;
|
||||
/** Global SEO defaults. */
|
||||
@@ -1140,9 +1142,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
selfClose === "/" ? { inner: "", end: openEnd } : readElementBody(body, tag!, openEnd);
|
||||
i = end;
|
||||
|
||||
const normalizedName = name.toLowerCase();
|
||||
const normalizedName = normalizeComponentName(name);
|
||||
const component = router.components.find(
|
||||
(candidate) => candidate.name.toLowerCase() === normalizedName,
|
||||
(candidate) => normalizeComponentName(candidate.name) === normalizedName,
|
||||
);
|
||||
if (!component) {
|
||||
console.warn(`[wrnexus] no component registered for '${name}'`);
|
||||
@@ -1312,7 +1314,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
if (deps.i18n) body = translateHtml(body, ctx.t);
|
||||
|
||||
// Point 3: only ship the JS this page actually uses.
|
||||
const scripts = collectScripts(body, deps.clientRuntimes).map((script) =>
|
||||
const scripts = collectScripts(body, deps.clientRuntimes, deps.navigation).map((script) =>
|
||||
versionRenderScript(script, deps.assetVersion),
|
||||
);
|
||||
if (pwaServiceWorkerEnabled)
|
||||
@@ -1669,6 +1671,17 @@ export function resolveTProps(
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize component identifiers for registry lookup.
|
||||
*
|
||||
* WRN declarations use PascalCase (`PublicHeader`) while explicit
|
||||
* `data-component` mounts commonly use kebab-case (`public-header`) or
|
||||
* snake_case (`public_header`). All three forms identify the same component.
|
||||
*/
|
||||
export function normalizeComponentName(name: string): string {
|
||||
return name.toLowerCase().replace(/[-_]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide which framework scripts a rendered page needs. Components are already
|
||||
* server-rendered into the HTML; the only script is the reactive runtime, and
|
||||
@@ -1677,10 +1690,11 @@ export function resolveTProps(
|
||||
export function collectScripts(
|
||||
body: string,
|
||||
clientRuntimes: readonly ClientRuntimeDefinition[] = [],
|
||||
navigation: { mode?: "client" | "document" } = {},
|
||||
): RenderScript[] {
|
||||
// Client-side navigation is an app-wide progressive enhancement: it must load
|
||||
// on every page (you navigate *from* any page), and degrades to full loads.
|
||||
const scripts: RenderScript[] = ["/__wrnexus/nav.js"];
|
||||
// Client navigation is optional. In document mode, links retain native browser
|
||||
// behavior and each route receives a fresh server-rendered HTML document.
|
||||
const scripts: RenderScript[] = navigation.mode === "document" ? [] : ["/__wrnexus/nav.js"];
|
||||
if (/\bdata-scope=/.test(body) || /\bdata-wrnexus-csr=/.test(body)) {
|
||||
scripts.push("/__wrnexus/reactive.js");
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ const server = await startServer({
|
||||
plugins: config.plugins,
|
||||
observability: config.observability,
|
||||
tenancy: config.tenancy,
|
||||
navigation: config.navigation,
|
||||
});
|
||||
|
||||
const router = server.router;
|
||||
|
||||
Reference in New Issue
Block a user