A document's CSP nonce is fixed at load, so an inline script delivered by a later response can never carry a nonce that document accepts. The HMR client is now served at /__wrnexus/hmr-client.js, which script-src 'self' already covers and which needs no nonce at all. This removes one of the two inline scripts CSP was blocking in development. The i18n data script is still blocked and needs the same treatment; it is shared with the CSR navigation and HMR parsers, so moving it spans @wrnexus/i18n, csr, and dev-server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
/**
|
|
* Dev-mode asset server for `/__wrnexus/*`:
|
|
* /__wrnexus/reactive.js the reactive runtime
|
|
* /__wrnexus/theme.css design-token themes (per resolved theme config)
|
|
* /__wrnexus/theme.js client theme switcher
|
|
* /__wrnexus/styles.css bundled global stylesheet (cached)
|
|
*
|
|
* Components are `.wrn` files rendered on the server (see runtime.ts), so there
|
|
* are no per-component browser chunks to build or serve here. The CSS cache is
|
|
* invalidated in-process by the file watcher so edits show without a restart.
|
|
*/
|
|
|
|
import {
|
|
getActionRuntime,
|
|
getComponentControllerRuntime,
|
|
getReactiveRuntime,
|
|
getNavRuntime,
|
|
getRealtimeRuntime,
|
|
} from "@wrnexus/csr";
|
|
import {
|
|
renderStyles,
|
|
renderActiveThemeCss,
|
|
renderThemeCss,
|
|
renderThemeRuntime,
|
|
type ResolvedTheme,
|
|
type StylesConfig,
|
|
} from "@wrnexus/styles";
|
|
import { getIslandRuntime } from "@wrnexus/react/runtime";
|
|
import { VALIDATE_RUNTIME } from "@wrnexus/validation";
|
|
import { I18N_RUNTIME } from "@wrnexus/i18n";
|
|
import { UPLOAD_RUNTIME, UPLOAD_JS_HREF, UPLOADS_PREFIX, serveStoredFile } from "@wrnexus/uploader";
|
|
import type { Mode } from "@wrnexus/core";
|
|
import type { AssetServer } from "./runtime.ts";
|
|
import { servePublicAsset } from "./public.ts";
|
|
import { servePluginAsset, type ServedPluginAsset } from "./plugin-assets.ts";
|
|
import { serveIslandArtifact, serveWrnBrowserArtifact } from "./pipeline.ts";
|
|
import { HMR_CLIENT_HREF, HMR_CLIENT_JS } from "./runtime.ts";
|
|
|
|
/** Style inputs the dev asset server needs to build `/__wrnexus/styles.css`. */
|
|
export interface DevStyles {
|
|
entry: string | null;
|
|
config?: StylesConfig;
|
|
appRoot: string;
|
|
publicDir?: string;
|
|
sources?: string[];
|
|
entries?: string[];
|
|
}
|
|
|
|
/** A dev asset server also supports invalidating its caches in-process. */
|
|
export interface DevAssetServer extends AssetServer {
|
|
invalidateCss(): void;
|
|
updateSchemas(code: string): void;
|
|
}
|
|
|
|
function jsResponse(code: string): Response {
|
|
return new Response(code, {
|
|
headers: {
|
|
"content-type": "text/javascript; charset=utf-8",
|
|
"cache-control": "no-store, max-age=0",
|
|
pragma: "no-cache",
|
|
expires: "0",
|
|
},
|
|
});
|
|
}
|
|
|
|
function cssResponse(code: string): Response {
|
|
return new Response(code, {
|
|
headers: {
|
|
"content-type": "text/css; charset=utf-8",
|
|
"cache-control": "no-cache",
|
|
},
|
|
});
|
|
}
|
|
|
|
export function createDevAssetServer(
|
|
appDir: string,
|
|
mode: Mode,
|
|
styles?: DevStyles,
|
|
theme?: ResolvedTheme,
|
|
uiCss?: string | (() => string),
|
|
schemasJs?: string,
|
|
pluginAssets: readonly ServedPluginAsset[] = [],
|
|
): DevAssetServer {
|
|
let cssCache: string | null = null;
|
|
let schemasCode = schemasJs ?? "window.__wrnSchemas={};";
|
|
|
|
return {
|
|
invalidateCss() {
|
|
cssCache = null;
|
|
},
|
|
|
|
updateSchemas(code) {
|
|
schemasCode = code;
|
|
},
|
|
|
|
async serve(pathname: string): Promise<Response | null> {
|
|
if (pathname.startsWith("/__wrnexus/client/")) {
|
|
return serveWrnBrowserArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
|
}
|
|
if (pathname.startsWith("/__wrnexus/island/")) {
|
|
return serveIslandArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
|
}
|
|
if (pathname === HMR_CLIENT_HREF) return jsResponse(HMR_CLIENT_JS);
|
|
if (pathname === "/__wrnexus/islands.js") return jsResponse(getIslandRuntime(true));
|
|
if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime(true));
|
|
if (pathname === "/__wrnexus/controllers.js")
|
|
return jsResponse(getComponentControllerRuntime(true));
|
|
if (pathname === "/__wrnexus/nav.js") return jsResponse(getNavRuntime());
|
|
if (pathname === "/__wrnexus/realtime.js") return jsResponse(getRealtimeRuntime());
|
|
if (pathname === "/__wrnexus/actions.js") return jsResponse(getActionRuntime());
|
|
if (pathname === "/__wrnexus/validate.js") return jsResponse(VALIDATE_RUNTIME);
|
|
if (pathname === "/__wrnexus/i18n.js") return jsResponse(I18N_RUNTIME);
|
|
if (pathname === UPLOAD_JS_HREF) return jsResponse(UPLOAD_RUNTIME);
|
|
|
|
// Public local uploads served at /__wrnexus/uploads/<store>/<key>.
|
|
if (pathname.startsWith(UPLOADS_PREFIX)) {
|
|
return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 });
|
|
}
|
|
if (pathname === "/__wrnexus/schemas.js") return jsResponse(schemasCode);
|
|
|
|
if (pathname === "/__wrnexus/ui.css") {
|
|
const currentUiCss = typeof uiCss === "function" ? uiCss() : uiCss;
|
|
return currentUiCss
|
|
? cssResponse(currentUiCss)
|
|
: new Response("Not Found", { status: 404 });
|
|
}
|
|
|
|
if (pathname === "/__wrnexus/theme.css") {
|
|
return theme
|
|
? 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))
|
|
: new Response("Not Found", { status: 404 });
|
|
}
|
|
|
|
if (pathname === "/__wrnexus/styles.css") {
|
|
if (!styles?.entry && !styles?.entries?.length) {
|
|
return new Response("Not Found", { status: 404 });
|
|
}
|
|
if (cssCache === null) {
|
|
cssCache = await renderStyles(
|
|
{
|
|
entryPath: styles.entry,
|
|
appDir,
|
|
appRoot: styles.appRoot,
|
|
mode,
|
|
sources: styles.sources,
|
|
entries: styles.entries,
|
|
},
|
|
styles.config,
|
|
);
|
|
}
|
|
return cssResponse(cssCache);
|
|
}
|
|
|
|
const pluginAsset = await servePluginAsset(pluginAssets, pathname, mode);
|
|
if (pluginAsset) return pluginAsset;
|
|
|
|
return servePublicAsset(styles?.publicDir, pathname, mode);
|
|
},
|
|
};
|
|
}
|