feat: add safe project upgrades and production performance fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.14",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -75,6 +75,8 @@ export interface ProdOptions {
|
||||
theme?: ResolvedTheme;
|
||||
/** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
|
||||
uiCssPath?: string;
|
||||
/** Combined production theme + Wire UI stylesheet. */
|
||||
frameworkCssPath?: string;
|
||||
/** Pre-built `window.__wireSchemas = {...}` script for client validation. */
|
||||
schemasJs?: string;
|
||||
/** Resolved i18n bundle (default lang + locale messages). */
|
||||
@@ -216,6 +218,8 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
|
||||
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
|
||||
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")
|
||||
return serveFile(opts.frameworkCssPath, CSS_HEADERS);
|
||||
if (pathname === "/__wrnexus/styles.css") return serveFile(opts.stylesPath, CSS_HEADERS);
|
||||
return servePublicAsset(opts.publicDir, pathname, MODE);
|
||||
},
|
||||
@@ -279,6 +283,7 @@ export function createProductionHandlers(
|
||||
assets,
|
||||
hasStyles: !!opts.stylesPath,
|
||||
hasUi: !!opts.uiCssPath,
|
||||
hasFrameworkStyles: !!opts.frameworkCssPath,
|
||||
theme: opts.theme,
|
||||
i18n: opts.i18n,
|
||||
inlineStyles: opts.inlineStyles,
|
||||
|
||||
@@ -101,6 +101,8 @@ export interface RuntimeDeps {
|
||||
hasStyles?: boolean;
|
||||
/** When true, inject the Wire UI stylesheet link (`/__wrnexus/ui.css`). */
|
||||
hasUi?: boolean;
|
||||
/** Production build combined theme + UI stylesheet. */
|
||||
hasFrameworkStyles?: boolean;
|
||||
/** Resolved theme config: enables `/__wrnexus/theme.css` + `<html data-theme>`. */
|
||||
theme?: ResolvedTheme;
|
||||
/** Resolved i18n bundle: enables `ctx.t`, `<html lang>`, and `{t:key}` markers. */
|
||||
@@ -129,8 +131,14 @@ export interface RuntimeDeps {
|
||||
realtimeBus?: RealtimeBus;
|
||||
}
|
||||
|
||||
const PWA_CLIENT = `if ("serviceWorker" in navigator) {
|
||||
addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {}));
|
||||
export const PWA_CLIENT = `if ("serviceWorker" in navigator) {
|
||||
var swUrl = "/sw.js";
|
||||
if (window.trustedTypes) {
|
||||
try {
|
||||
swUrl = window.trustedTypes.createPolicy("wrnexus-pwa", { createScriptURL: function(value) { return value; } }).createScriptURL(swUrl);
|
||||
} catch (_) {}
|
||||
}
|
||||
addEventListener("load", () => navigator.serviceWorker.register(swUrl).catch(() => {}));
|
||||
}`;
|
||||
|
||||
function renderPwaServiceWorker(pwa: PwaConfig): string {
|
||||
@@ -561,11 +569,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
// Order: theme tokens, then Wire UI, then the app stylesheet — so the app's
|
||||
// own CSS (loaded last) can override both the tokens and the UI classes.
|
||||
const headParts: string[] = [];
|
||||
if (deps.theme) {
|
||||
if (deps.hasFrameworkStyles) {
|
||||
headParts.push(
|
||||
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`,
|
||||
);
|
||||
} else if (deps.theme) {
|
||||
const themeHref = versionAssetUrl(THEME_CSS_HREF, deps.assetVersion);
|
||||
headParts.push(`<link rel="stylesheet" href="${themeHref}" />`);
|
||||
}
|
||||
if (deps.hasUi) {
|
||||
if (deps.hasUi && !deps.hasFrameworkStyles) {
|
||||
headParts.push(
|
||||
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/ui.css", deps.assetVersion)}" />`,
|
||||
);
|
||||
@@ -677,7 +689,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
new Response(PWA_CLIENT, {
|
||||
headers: {
|
||||
"content-type": "text/javascript; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
"cache-control": url.searchParams.has("v")
|
||||
? "public, max-age=31536000, immutable"
|
||||
: "no-cache",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -687,7 +701,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
new Response(MOBILE_CLIENT, {
|
||||
headers: {
|
||||
"content-type": "text/javascript; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
"cache-control": url.searchParams.has("v")
|
||||
? "public, max-age=31536000, immutable"
|
||||
: "no-cache",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1046,14 +1062,17 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
|
||||
// Point 3: only ship the JS this page actually uses.
|
||||
const scripts = collectScripts(body).map((src) => versionAssetUrl(src, deps.assetVersion));
|
||||
if (pwaServiceWorkerEnabled) scripts.push("/__wrnexus/pwa.js");
|
||||
if (deps.mobile?.enabled !== false) scripts.push("/__wrnexus/mobile.js");
|
||||
if (pwaServiceWorkerEnabled)
|
||||
scripts.push(versionAssetUrl("/__wrnexus/pwa.js", deps.assetVersion));
|
||||
if (deps.mobile?.enabled !== false && usesMobileRuntime(body))
|
||||
scripts.push(versionAssetUrl("/__wrnexus/mobile.js", deps.assetVersion));
|
||||
|
||||
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
||||
const attrs: string[] = [];
|
||||
if (deps.theme)
|
||||
attrs.push(`data-theme="${resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme)}"`);
|
||||
if (deps.i18n && ctx.lang) attrs.push(`lang="${ctx.lang}"`);
|
||||
const language = deps.i18n && ctx.lang ? ctx.lang : (meta.lang ?? deps.seo?.lang ?? "en");
|
||||
attrs.push(`lang="${safeLanguageTag(language)}"`);
|
||||
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
|
||||
|
||||
const html = renderDocument({
|
||||
@@ -1412,6 +1431,17 @@ export function collectScripts(body: string): string[] {
|
||||
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";
|
||||
}
|
||||
|
||||
function versionAssetUrl(src: string, version?: string): string {
|
||||
if (!version) return src;
|
||||
return `${src}${src.includes("?") ? "&" : "?"}v=${encodeURIComponent(version)}`;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { PWA_CLIENT, usesMobileRuntime } from "../src/runtime.ts";
|
||||
|
||||
test("PWA registration is valid JavaScript and Trusted Types compatible", () => {
|
||||
expect(() => new Bun.Transpiler({ loader: "js" }).transformSync(PWA_CLIENT)).not.toThrow();
|
||||
expect(PWA_CLIENT).toContain('createPolicy("wrnexus-pwa"');
|
||||
expect(PWA_CLIENT).toContain("createScriptURL(swUrl)");
|
||||
});
|
||||
|
||||
test("mobile runtime is shipped only for pages using mobile or native directives", () => {
|
||||
expect(usesMobileRuntime('<main class="page">Docs</main>')).toBe(false);
|
||||
expect(usesMobileRuntime('<button data-native-mobile="share">Share</button>')).toBe(true);
|
||||
expect(usesMobileRuntime("<div data-mobile-only>App</div>")).toBe(true);
|
||||
expect(usesMobileRuntime('<button data-on-wrnexus-browser-click="open">Open</button>')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user