complete performance and reliability follow-ups
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "@wrnexus/csr";
|
||||
import {
|
||||
renderStyles,
|
||||
renderActiveThemeCss,
|
||||
renderThemeCss,
|
||||
renderThemeRuntime,
|
||||
type ResolvedTheme,
|
||||
@@ -122,6 +123,18 @@ export function createDevAssetServer(
|
||||
? cssResponse(renderThemeCss(theme))
|
||||
: new Response("Not Found", { status: 404 });
|
||||
}
|
||||
const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname);
|
||||
if (activeThemeMatch && theme) {
|
||||
try {
|
||||
const themeName = decodeURIComponent(activeThemeMatch[1]!);
|
||||
const accentPart = decodeURIComponent(activeThemeMatch[2]!);
|
||||
return cssResponse(
|
||||
renderActiveThemeCss(theme, themeName, accentPart === "_" ? undefined : accentPart),
|
||||
);
|
||||
} catch {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
}
|
||||
if (pathname === "/__wrnexus/theme.js") {
|
||||
return theme
|
||||
? jsResponse(renderThemeRuntime(theme))
|
||||
|
||||
@@ -99,6 +99,8 @@ export interface ProdOptions {
|
||||
clientModulesDir?: string;
|
||||
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
||||
themePath?: string;
|
||||
/** Pre-built active theme/accent stylesheets, loaded on demand. */
|
||||
themeAssetsDir?: string;
|
||||
/** Absolute path to the pre-built theme runtime (`theme.js`). */
|
||||
themeJsPath?: string;
|
||||
/** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
|
||||
@@ -366,6 +368,28 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
|
||||
return new Response(opts.schemasJs ?? "window.__wireSchemas={};", { headers: JS_HEADERS });
|
||||
}
|
||||
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
|
||||
const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname);
|
||||
if (activeThemeMatch && opts.theme && opts.themeAssetsDir) {
|
||||
try {
|
||||
const themeName = decodeURIComponent(activeThemeMatch[1]!);
|
||||
const accentPart = decodeURIComponent(activeThemeMatch[2]!);
|
||||
if (!opts.theme.names.includes(themeName))
|
||||
return new Response("Not Found", { status: 404 });
|
||||
if (accentPart !== "_" && !opts.theme.accentNames.some((name) => name === accentPart)) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
return serveFile(
|
||||
join(
|
||||
opts.themeAssetsDir,
|
||||
encodeURIComponent(themeName),
|
||||
`${accentPart === "_" ? "_" : encodeURIComponent(accentPart)}.css`,
|
||||
),
|
||||
CSS_HEADERS,
|
||||
);
|
||||
} catch {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
}
|
||||
if (pathname === "/__wrnexus/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS);
|
||||
if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
|
||||
if (pathname === "/__wrnexus/framework.css")
|
||||
|
||||
@@ -66,12 +66,12 @@ import type { StoreDefinition } from "@wrnexus/store";
|
||||
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
|
||||
import { CacheCoordinator } from "@wrnexus/cache";
|
||||
import { generateServiceWorker } from "@wrnexus/pwa";
|
||||
import { runtimeScriptsForMarkup } from "./plugin-assets.ts";
|
||||
import { collectScripts, usesMobileRuntime } from "./script-selection.ts";
|
||||
export { collectScripts, usesMobileRuntime } from "./script-selection.ts";
|
||||
import {
|
||||
ACCENT_COOKIE,
|
||||
THEME_COOKIE,
|
||||
THEME_CSS_HREF,
|
||||
THEME_JS_HREF,
|
||||
activeThemeCssHref,
|
||||
resolveAccentName,
|
||||
resolveThemeName,
|
||||
type MobileConfig,
|
||||
@@ -81,7 +81,6 @@ import {
|
||||
type TenancyConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import {
|
||||
I18N_JS_HREF,
|
||||
renderI18nData,
|
||||
makeT,
|
||||
resolveLang,
|
||||
@@ -945,9 +944,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
headParts.push(
|
||||
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`,
|
||||
);
|
||||
} else if (deps.theme && !deps.stylesIncludeFramework) {
|
||||
const themeHref = versionAssetUrl(THEME_CSS_HREF, deps.assetVersion);
|
||||
headParts.push(`<link rel="stylesheet" href="${themeHref}" />`);
|
||||
}
|
||||
if (deps.hasUi && !deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||
headParts.push(
|
||||
@@ -1839,6 +1835,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
|
||||
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
||||
const attrs: string[] = [];
|
||||
let activeThemeHead = "";
|
||||
|
||||
if (deps.theme) {
|
||||
const themeName = resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme);
|
||||
@@ -1850,6 +1847,13 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
if (accentName) {
|
||||
attrs.push(`data-accent="${accentName}"`);
|
||||
}
|
||||
if (!deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||
const themeHref = versionAssetUrl(
|
||||
activeThemeCssHref(themeName, accentName),
|
||||
deps.assetVersion,
|
||||
);
|
||||
activeThemeHead = `<link rel="stylesheet" data-wrnexus-theme href="${themeHref}" />`;
|
||||
}
|
||||
}
|
||||
attrs.push(`lang="${safeLanguageTag(language)}"`);
|
||||
if (deps.i18n) attrs.push(`dir="${deps.i18n.direction[language] ?? "ltr"}"`);
|
||||
@@ -1873,6 +1877,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
pwaEnabled ? `<link rel="manifest" href="/site.webmanifest" />` : "",
|
||||
pwaEnabled ? `<meta name="mobile-web-app-capable" content="yes" />` : "",
|
||||
pwaEnabled ? `<meta name="apple-mobile-web-app-capable" content="yes" />` : "",
|
||||
activeThemeHead,
|
||||
extraHead,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -2443,72 +2448,6 @@ 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
|
||||
* only when the page actually contains a scope or a browser-side API fetch.
|
||||
*/
|
||||
export function collectScripts(
|
||||
body: string,
|
||||
clientRuntimes: readonly ClientRuntimeDefinition[] = [],
|
||||
navigation: { mode?: "auto" | "client" | "document" } = {},
|
||||
): RenderScript[] {
|
||||
const scripts: RenderScript[] = [];
|
||||
if (
|
||||
/\bdata-scope=/.test(body) ||
|
||||
/\bdata-wrnexus-csr=/.test(body) ||
|
||||
/\bdata-wrn-client-template=/.test(body) ||
|
||||
/\bdata-wrn-async=/.test(body)
|
||||
) {
|
||||
scripts.push("/__wrnexus/reactive.js");
|
||||
}
|
||||
if (/\bdata-wrn-action=/.test(body)) scripts.push("/__wrnexus/actions.js");
|
||||
// The theme runtime is only needed when the page can switch themes.
|
||||
if (
|
||||
/\bdata-wire-theme-(toggle|set)\b/.test(body) ||
|
||||
/\bdata-wire-accent-(set|clear)\b/.test(body)
|
||||
) {
|
||||
scripts.push(THEME_JS_HREF);
|
||||
}
|
||||
// Validation: schema descriptors + the generic validator, only for pages with a form.
|
||||
if (/\bdata-schema="[A-Za-z0-9_-]+"/.test(body)) {
|
||||
scripts.push("/__wrnexus/schemas.js", "/__wrnexus/validate.js");
|
||||
}
|
||||
// Language switcher runtime, only when the page has one.
|
||||
if (/\bdata-wire-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wire-lang\b/.test(body)) {
|
||||
scripts.push(I18N_JS_HREF);
|
||||
}
|
||||
// Realtime client, only when the page declares a room.
|
||||
if (/\bdata-room=/.test(body)) {
|
||||
scripts.push("/__wrnexus/realtime.js");
|
||||
}
|
||||
// File-upload runtime (drag-drop + progress), only when a page has an uploader.
|
||||
if (/\bdata-uploader\b/.test(body)) {
|
||||
scripts.push("/__wrnexus/uploader.js");
|
||||
}
|
||||
// Package runtimes are declarative. Components mark the rendered HTML with
|
||||
// `data-wrnexus-runtime="id"`; the corresponding package chunk is loaded
|
||||
// once, without requiring application-authored script tags or public copies.
|
||||
scripts.push(...runtimeScriptsForMarkup(body, clientRuntimes));
|
||||
|
||||
// `auto` is the performance-first default: a fully static page ships no
|
||||
// framework JavaScript and its links use native document navigation. Routes
|
||||
// that already need browser behavior also receive progressive navigation.
|
||||
// `client` preserves the explicit always-on behavior; `document` disables it.
|
||||
const mode = navigation.mode ?? "auto";
|
||||
if (mode === "client" || (mode === "auto" && scripts.length > 0)) {
|
||||
scripts.unshift("/__wrnexus/nav.js");
|
||||
}
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/** Whether rendered markup needs the Capacitor/native browser bridge. */
|
||||
export function usesMobileRuntime(body: string): boolean {
|
||||
return /\b(?:data-mobile-[\w-]+|data-native-(?:browser|mobile|only|requires|unsupported|options)|data-on-wrnexus-(?:browser|mobile)-[\w-]+)\b/.test(
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
function safeLanguageTag(value: string): string {
|
||||
return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) ? value : "en";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { I18N_JS_HREF } from "@wrnexus/i18n";
|
||||
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
|
||||
import { THEME_JS_HREF } from "@wrnexus/styles";
|
||||
import type { RenderScript } from "@wrnexus/ssr";
|
||||
import { runtimeScriptsForMarkup } from "./plugin-assets.ts";
|
||||
|
||||
/** Select browser runtimes from capabilities present in rendered markup. */
|
||||
export function collectScripts(
|
||||
body: string,
|
||||
clientRuntimes: readonly ClientRuntimeDefinition[] = [],
|
||||
navigation: { mode?: "auto" | "client" | "document" } = {},
|
||||
): RenderScript[] {
|
||||
const scripts: RenderScript[] = [];
|
||||
if (
|
||||
/\bdata-scope=/.test(body) ||
|
||||
/\bdata-wrnexus-csr=/.test(body) ||
|
||||
/\bdata-wrn-client-template=/.test(body) ||
|
||||
/\bdata-wrn-async=/.test(body)
|
||||
) {
|
||||
scripts.push("/__wrnexus/reactive.js");
|
||||
}
|
||||
if (/\bdata-wrn-action=/.test(body)) scripts.push("/__wrnexus/actions.js");
|
||||
if (
|
||||
/\bdata-wire-theme-(toggle|set)\b/.test(body) ||
|
||||
/\bdata-wire-accent-(set|clear)\b/.test(body)
|
||||
) {
|
||||
scripts.push(THEME_JS_HREF);
|
||||
}
|
||||
if (/\bdata-schema="[A-Za-z0-9_-]+"/.test(body)) {
|
||||
scripts.push("/__wrnexus/schemas.js", "/__wrnexus/validate.js");
|
||||
}
|
||||
if (/\bdata-wire-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wire-lang\b/.test(body)) {
|
||||
scripts.push(I18N_JS_HREF);
|
||||
}
|
||||
if (/\bdata-room=/.test(body)) scripts.push("/__wrnexus/realtime.js");
|
||||
if (/\bdata-uploader\b/.test(body)) scripts.push("/__wrnexus/uploader.js");
|
||||
scripts.push(...runtimeScriptsForMarkup(body, clientRuntimes));
|
||||
|
||||
const mode = navigation.mode ?? "auto";
|
||||
if (mode === "client" || (mode === "auto" && scripts.length > 0)) {
|
||||
scripts.unshift("/__wrnexus/nav.js");
|
||||
}
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/** Whether rendered markup needs the Capacitor/native browser bridge. */
|
||||
export function usesMobileRuntime(body: string): boolean {
|
||||
return /\b(?:data-mobile-[\w-]+|data-native-(?:browser|mobile|only|requires|unsupported|options)|data-on-wrnexus-(?:browser|mobile)-[\w-]+)\b/.test(
|
||||
body,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user