release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+78 -12
View File
@@ -10,7 +10,7 @@
* app/pages/*.wrn api -> embedded /api/* routes
* app/pages/*.wrn realtime -> embedded /realtime/* routes
* app/middleware/*.ts -> global middleware (alphabetical)
* app/components/*.wrn -> server-rendered components (by basename),
* app/components/*.wrn -> server-rendered components (by declaration),
* mounted in a page via data-component="<name>"
*/
@@ -62,12 +62,23 @@ export interface Router {
matchRealtime(pathname: string): RouteMatch | null;
}
export interface ExternalRouteDefinition {
kind: "page" | "api" | "realtime";
path: string;
entry: string;
name?: string;
}
export interface RouterOptions {
/**
* Extra directories to scan for `.wrn` components (e.g. `@wrnexus/ui`).
* Scanned before `app/components`, so an app component of the same name wins.
*/
componentDirs?: string[];
/** Package-owned routes registered by the plugin contribution system. */
externalRoutes?: ExternalRouteDefinition[];
/** Package-owned middleware executed before app/middleware. */
middlewareFiles?: string[];
}
/**
@@ -139,39 +150,92 @@ function warnRouteConflicts(kind: string, routes: Route[]): void {
}
}
/**
* Merge package routes with application routes. Exact application paths always
* win so a package can safely provide defaults that an app may override.
*/
function mergeExternalRoutes(
kind: string,
applicationRoutes: Route[],
externalRoutes: Route[],
): Route[] {
const applicationPaths = new Set(applicationRoutes.map((route) => route.raw));
const retainedExternal = externalRoutes.filter((route) => {
if (!applicationPaths.has(route.raw)) return true;
console.warn(
`[wrnexus] WRN-ROUTE-OVERRIDE: application ${kind} route '${route.raw}' overrides package route from ${route.file}`,
);
return false;
});
return sortRoutes([...applicationRoutes, ...retainedExternal]);
}
/** Scan an app directory and build all route tables. */
export function buildRouter(appDir: string, opts: RouterOptions = {}): Router {
const pageFiles = scanDir(join(appDir, "pages"));
const embedded = embeddedWireRoutes(pageFiles);
const pages = buildRoutes(pageFiles, "");
const api = sortRoutes([...buildRoutes(scanDir(join(appDir, "api")), "/api"), ...embedded.api]);
const realtime = sortRoutes([
const external = opts.externalRoutes ?? [];
const externalPages = external
.filter((route) => route.kind === "page")
.map((route) => routeFromRaw(route.path, route.entry));
const externalApi = external
.filter((route) => route.kind === "api")
.map((route) => routeFromRaw(route.path, route.entry));
const externalRealtime = external
.filter((route) => route.kind === "realtime")
.map((route) => routeFromRaw(route.path, route.entry));
const applicationPages = buildRoutes(pageFiles, "");
const applicationApi = sortRoutes([
...buildRoutes(scanDir(join(appDir, "api")), "/api"),
...embedded.api,
]);
const applicationRealtime = sortRoutes([
...buildRoutes(scanDir(join(appDir, "realtime")), "/realtime"),
...embedded.realtime,
]);
const pages = mergeExternalRoutes("page", applicationPages, externalPages);
const api = mergeExternalRoutes("API", applicationApi, externalApi);
const realtime = mergeExternalRoutes("realtime", applicationRealtime, externalRealtime);
warnRouteConflicts("page", pages);
warnRouteConflicts("API", api);
warnRouteConflicts("realtime", realtime);
// Middleware runs in deterministic (alphabetical) order.
const middlewareFiles = scanDir(join(appDir, "middleware"))
.map((f) => f.file)
.sort();
const middlewareFiles = [
...new Set([
...(opts.middlewareFiles ?? []),
...scanDir(join(appDir, "middleware"))
.map((f) => f.file)
.sort(),
]),
];
// Components: `.wrn` files, keyed by name so later scans override earlier
// ones. Library dirs (e.g. @wrnexus/ui) are scanned first; app/components last,
// so an app component of the same name shadows the library's.
// Components are keyed case-insensitively by their declared name so later
// scans override earlier ones without creating duplicate aliases. Library
// dirs (e.g. @wrnexus/ui) are scanned first; app/components last.
const componentMap = new Map<string, ComponentRef>();
const scanComponents = (dir: string): void => {
for (const f of scanDir(dir)) {
if (!f.file.endsWith(".wrn")) continue;
const name = basename(f.file).replace(/\.wrn$/, "");
const fileName = basename(f.file).replace(/\.wrn$/, "");
let name = fileName;
try {
const source = readFileSync(f.file, "utf8");
const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(source);
if (declaration) name = declaration[1]!;
} catch (error) {
console.warn(`[wrnexus] unable to inspect component declaration in ${f.file}`, error);
}
if (!isSafeIslandName(name)) {
console.warn(`[wrnexus] skipping component with unsafe name: ${name}`);
continue;
}
componentMap.set(name, { name, file: f.file });
componentMap.set(name.toLowerCase(), { name, file: f.file });
}
};
for (const dir of opts.componentDirs ?? []) scanComponents(dir);
@@ -220,3 +284,5 @@ export function buildRouter(appDir: string, opts: RouterOptions = {}): Router {
/** Re-export for callers that load middleware modules themselves. */
export type { Middleware };
export { routeName, nameRoutes, createRouteManifest, routeUrl, findNamedRoute } from "./named.ts";
export type { NamedRoute, RouteManifestEntry } from "./named.ts";