release: WRNexusJS 0.5.0
This commit is contained in:
@@ -65,7 +65,7 @@ export function createDevAssetServer(
|
||||
mode: Mode,
|
||||
styles?: DevStyles,
|
||||
theme?: ResolvedTheme,
|
||||
uiCss?: string,
|
||||
uiCss?: string | (() => string),
|
||||
schemasJs?: string,
|
||||
pluginAssets: readonly ServedPluginAsset[] = [],
|
||||
): DevAssetServer {
|
||||
@@ -96,7 +96,10 @@ export function createDevAssetServer(
|
||||
if (pathname === "/__wrnexus/schemas.js") return jsResponse(schemasCode);
|
||||
|
||||
if (pathname === "/__wrnexus/ui.css") {
|
||||
return uiCss ? cssResponse(uiCss) : new Response("Not Found", { status: 404 });
|
||||
const currentUiCss = typeof uiCss === "function" ? uiCss() : uiCss;
|
||||
return currentUiCss
|
||||
? cssResponse(currentUiCss)
|
||||
: new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
if (pathname === "/__wrnexus/theme.css") {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* the running process while the HMR socket morphs fresh HTML into the browser.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve, dirname, isAbsolute, join } from "node:path";
|
||||
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
|
||||
import { buildRouter, type Router } from "@wrnexus/router";
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
type MobileConfig,
|
||||
type PwaConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import { uiComponentsDir, uiCss } from "@wrnexus/ui";
|
||||
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";
|
||||
@@ -46,6 +47,7 @@ export interface ServeOptions {
|
||||
mode?: Mode;
|
||||
/** Inject the live-reload client (defaults to true in development). */
|
||||
hmr?: boolean;
|
||||
appConfig?: Record<string, unknown>;
|
||||
/** Resolved absolute path to the global CSS entry, or null. */
|
||||
styleEntry?: string | null;
|
||||
/** Custom styles config (e.g. a Tailwind/PostCSS processor). */
|
||||
@@ -162,11 +164,14 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
const appDir = resolve(opts.appDir);
|
||||
const appRoot = dirname(appDir);
|
||||
const mode: Mode = opts.mode ?? "development";
|
||||
const discoveredPlugins = await discoverPlugins(appRoot, opts.plugins, {
|
||||
const configuredPlugins = opts.plugins ?? (opts.appConfig?.plugins as PluginInput | undefined);
|
||||
|
||||
const discoveredPlugins = await discoverPlugins(appRoot, configuredPlugins, {
|
||||
includeDevDependencies: true,
|
||||
strict: true,
|
||||
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
|
||||
});
|
||||
|
||||
const pluginRunner = createPluginRunner(discoveredPlugins, {
|
||||
root: appRoot,
|
||||
mode,
|
||||
@@ -174,11 +179,64 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
metadata: new Map(),
|
||||
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
|
||||
});
|
||||
await pluginRunner.configure(opts as unknown as Record<string, unknown>);
|
||||
|
||||
/**
|
||||
* Plugins must receive the complete
|
||||
* wrnexus.config.ts object.
|
||||
*
|
||||
* ServeOptions only contains framework-known
|
||||
* properties. Package-specific configuration,
|
||||
* such as `auth`, is stored in appConfig.
|
||||
*/
|
||||
const pluginConfig: Record<string, unknown> = {
|
||||
...(opts.appConfig ?? {}),
|
||||
|
||||
// Resolved runtime values take priority.
|
||||
appDir,
|
||||
port: opts.port,
|
||||
hostname: opts.hostname,
|
||||
mode,
|
||||
hmr: opts.hmr,
|
||||
|
||||
styleEntry: opts.styleEntry,
|
||||
|
||||
styles: opts.stylesConfig ?? (opts.appConfig?.styles as StylesConfig | undefined),
|
||||
|
||||
head: opts.head,
|
||||
seo: opts.seo,
|
||||
security: opts.security,
|
||||
theme: opts.theme,
|
||||
i18n: opts.i18n,
|
||||
db: opts.db,
|
||||
databases: opts.databases,
|
||||
realtime: opts.realtime,
|
||||
storage: opts.storage,
|
||||
mobile: opts.mobile,
|
||||
pwa: opts.pwa,
|
||||
devToolbar: opts.devToolbar,
|
||||
plugins: configuredPlugins,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
};
|
||||
|
||||
await pluginRunner.configure(pluginConfig);
|
||||
|
||||
await pluginRunner.configResolved(
|
||||
Object.freeze({ ...opts }) as Readonly<Record<string, unknown>>,
|
||||
Object.freeze({
|
||||
...pluginConfig,
|
||||
}),
|
||||
);
|
||||
|
||||
const pluginContributions = await pluginRunner.contributions();
|
||||
|
||||
console.log(
|
||||
`[wrnexus:plugin] discovered: ${
|
||||
pluginRunner.plugins.map((plugin) => plugin.name).join(", ") || "none"
|
||||
}`,
|
||||
);
|
||||
|
||||
console.log(`[wrnexus:plugin] contributed routes: ${pluginContributions.routes.length}`);
|
||||
|
||||
const pluginToolbarPanels = await pluginRunner.devToolbarPanels();
|
||||
const componentDirs = [uiComponentsDir(), ...pluginContributions.componentDirs];
|
||||
|
||||
@@ -209,7 +267,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
|
||||
setCompileCacheDir(join(appRoot, ".wrnexus"));
|
||||
const theme = resolveThemeConfig(opts.theme);
|
||||
const uiStyles = uiCss();
|
||||
const uiStylesPath = uiCssPath();
|
||||
|
||||
const schemasJs = await schemaRuntime(router);
|
||||
|
||||
@@ -268,7 +326,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])),
|
||||
},
|
||||
theme,
|
||||
uiStyles,
|
||||
() => readFileSync(uiStylesPath, "utf8"),
|
||||
schemasJs,
|
||||
pluginAssetsFromContributions(pluginContributions),
|
||||
);
|
||||
@@ -407,6 +465,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
});
|
||||
};
|
||||
const packageWatchDirs = [
|
||||
dirname(uiStylesPath),
|
||||
...componentDirs,
|
||||
...pluginContributions.clientRuntimes.flatMap((runtime) =>
|
||||
runtime.entry ? [dirname(runtime.entry)] : [],
|
||||
|
||||
@@ -87,6 +87,7 @@ export function loadModule(file: string): Promise<Record<string, unknown>> {
|
||||
* When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
|
||||
*/
|
||||
let compileCacheDir: string | null = null;
|
||||
const WRN_COMPILE_CACHE_VERSION = "v2";
|
||||
|
||||
/**
|
||||
* Point all `.wrn` compilation at ONE cache dir (typically `<appRoot>/.wrnexus`)
|
||||
@@ -124,7 +125,10 @@ function compileWireToTs(file: string, version = 0): string {
|
||||
// checkouts, archive extraction, and linked dependencies can all replace a
|
||||
// file while preserving (or moving backwards) its mtime. An mtime-only cache
|
||||
// then serves an older compiled component even across a clean build.
|
||||
const out = join(cacheDir, `${name}-${hashPath(file)}-${hashPath(source)}${suffix}.wrn.ts`);
|
||||
const out = join(
|
||||
cacheDir,
|
||||
`${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}${suffix}.wrn.ts`,
|
||||
);
|
||||
|
||||
// The content hash makes this safe even when source timestamps are preserved.
|
||||
try {
|
||||
|
||||
@@ -599,10 +599,20 @@ export const HMR_CLIENT_JS = `
|
||||
|
||||
// Minimal index-based DOM morph: preserve matching nodes (keeps state/focus),
|
||||
// patch text and attributes, clone genuinely new nodes, drop removed ones.
|
||||
// Hydrated subtrees (reactive scopes) are CLIENT-OWNED and left untouched,
|
||||
// so live state (e.g. a counter at 5) is never reset to the SSR 0.
|
||||
// Preserve a hydrated subtree only while its server hydration signature and
|
||||
// behavior are unchanged. Component edits must replace and re-hydrate the
|
||||
// old subtree or HMR will keep stale markup indefinitely.
|
||||
function morph(from, to) {
|
||||
if (from.__wrnexusHydrated) return;
|
||||
if (from.__wrnexusHydrated) {
|
||||
var sameHydration =
|
||||
from.getAttribute("data-wrn-hydration") === to.getAttribute("data-wrn-hydration") &&
|
||||
from.getAttribute("data-wrn-behavior") === to.getAttribute("data-wrn-behavior") &&
|
||||
from.getAttribute("data-scope") === to.getAttribute("data-scope");
|
||||
if (sameHydration) return;
|
||||
if (window.__wrnexusDisposeBehaviors) window.__wrnexusDisposeBehaviors(from);
|
||||
from.replaceWith(to.cloneNode(true));
|
||||
return;
|
||||
}
|
||||
syncAttrs(from, to);
|
||||
var fc = from.childNodes, tc = to.childNodes, i;
|
||||
for (i = 0; i < tc.length; i++) {
|
||||
@@ -1070,6 +1080,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
const matched = router.matchApi(ctx.url.pathname);
|
||||
if (!matched) return Response.json({ error: "Not Found" }, { status: 404 });
|
||||
|
||||
// Expose the canonical matched route to package dispatchers. A package may
|
||||
// contribute several URL paths from one module, and request URLs can be
|
||||
// rewritten by gateways or internal framework calls. The router match is
|
||||
// the authoritative route identity.
|
||||
ctx.params = matched.params;
|
||||
ctx.locals.__wrnexusRoute = matched.route.raw;
|
||||
ctx.locals.__wrnexusRouteKind = "api";
|
||||
|
||||
const mod = await loadModule(matched.route.file);
|
||||
const method = ctx.req.method.toUpperCase();
|
||||
const embeddedApi = mod.__wrnexusApi as ApiRegistry | undefined;
|
||||
@@ -1087,7 +1105,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
});
|
||||
}
|
||||
|
||||
ctx.params = matched.params;
|
||||
return (await handler(ctx)) as Response;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,29 +9,43 @@
|
||||
*/
|
||||
|
||||
import { dirname } from "node:path";
|
||||
import { startServer } from "./index.ts";
|
||||
import { loadAppConfig, headToString, findStyleEntry, renderFontHead } from "@wrnexus/styles";
|
||||
import type { Mode } from "@wrnexus/core";
|
||||
import { findStyleEntry, headToString, loadAppConfig, renderFontHead } from "@wrnexus/styles";
|
||||
import { startServer } from "./index.ts";
|
||||
|
||||
const [appDir, portStr, modeStr, hostname, hmrStr] = process.argv.slice(2);
|
||||
|
||||
const mode = (modeStr as Mode) || "development";
|
||||
|
||||
const port = Number(portStr) || 3000;
|
||||
|
||||
// Load optional wrnexus.config.ts (sits next to the app/ dir) + resolve styles.
|
||||
const appRoot = dirname(appDir!);
|
||||
if (!appDir) {
|
||||
throw new Error("WRN-DEV-APP-DIR: app directory argument is required.");
|
||||
}
|
||||
|
||||
// Load optional wrnexus.config.ts next to the app directory.
|
||||
const appRoot = dirname(appDir);
|
||||
|
||||
const config = await loadAppConfig(appRoot);
|
||||
const styleEntry = findStyleEntry(appDir!, appRoot, config.styles?.entry);
|
||||
|
||||
const styleEntry = findStyleEntry(appDir, appRoot, config.styles?.entry);
|
||||
|
||||
const server = await startServer({
|
||||
appDir: appDir!,
|
||||
appConfig: {
|
||||
...config,
|
||||
},
|
||||
|
||||
port,
|
||||
hostname,
|
||||
mode,
|
||||
hmr: hmrStr === undefined ? undefined : hmrStr === "true",
|
||||
|
||||
styleEntry,
|
||||
stylesConfig: config.styles,
|
||||
|
||||
head: [renderFontHead(config.fonts), headToString(config.head)].filter(Boolean).join("\n "),
|
||||
|
||||
seo: config.seo,
|
||||
security: config.security,
|
||||
theme: config.theme,
|
||||
@@ -47,20 +61,44 @@ const server = await startServer({
|
||||
observability: config.observability,
|
||||
tenancy: config.tenancy,
|
||||
});
|
||||
const r = server.router;
|
||||
|
||||
const group = (label: string, items: { raw: string }[]) => {
|
||||
if (!items.length) return;
|
||||
console.log(` ${label}`);
|
||||
for (const it of items) console.log(` ${it.raw}`);
|
||||
};
|
||||
const router = server.router;
|
||||
|
||||
interface PrintableRoute {
|
||||
raw: string;
|
||||
}
|
||||
|
||||
function printRouteGroup(label: string, items: readonly PrintableRoute[]): void {
|
||||
const routes = Array.from(new Set(items.map((item) => item.raw.trim()).filter(Boolean))).sort(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
|
||||
console.log(` ${label}: ${routes.length}`);
|
||||
|
||||
if (routes.length === 0) {
|
||||
console.warn(` ⚠ No ${label.toLowerCase()} registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
console.log(` ${route}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n ⚡ WrNexus — ${server.url}\n`);
|
||||
group("Pages", r.pages);
|
||||
group("API", r.api);
|
||||
group("Realtime", r.realtime);
|
||||
if (r.components.length) {
|
||||
console.log(" Components");
|
||||
for (const c of r.components) console.log(` ${c.name}`);
|
||||
}
|
||||
|
||||
printRouteGroup("Pages", router.pages);
|
||||
|
||||
console.log("");
|
||||
|
||||
printRouteGroup("API Routes", router.api);
|
||||
|
||||
if (router.realtime.length > 0) {
|
||||
console.log("");
|
||||
|
||||
printRouteGroup("Realtime Routes", router.realtime);
|
||||
}
|
||||
|
||||
console.log(`\n Components: ${router.components.length}`);
|
||||
|
||||
console.log("");
|
||||
|
||||
Reference in New Issue
Block a user