import { readFile } from "node:fs/promises"; import type { ClientRuntimeDefinition, PackageAssetDefinition, PluginContributions, } from "@wrnexus/plugin"; import { contentTypeForPath, normalizeClientRuntime, normalizePackageAsset } from "@wrnexus/plugin"; import type { ScriptAsset } from "@wrnexus/ssr"; export interface ServedPluginAsset { id: string; publicPath: string; entry?: string; source?: string | Uint8Array; contentType: string; immutable: boolean; runtime?: boolean; runtimeType?: "module" | "script"; bundle?: boolean; } export interface ClientRuntimeAsset extends ClientRuntimeDefinition { publicPath: string; } function toBody(value: Uint8Array): ArrayBuffer { return Uint8Array.from(value).buffer; } export function pluginAssetsFromContributions( contributions: PluginContributions, ): ServedPluginAsset[] { const assets: ServedPluginAsset[] = []; for (const input of contributions.clientRuntimes) { const runtime = normalizeClientRuntime(input); assets.push({ id: `runtime:${runtime.id}`, publicPath: runtime.publicPath!, entry: runtime.entry, source: runtime.source, contentType: "text/javascript; charset=utf-8", immutable: false, runtime: true, runtimeType: runtime.type, bundle: runtime.bundle, }); } for (const input of contributions.assets) { const asset = normalizePackageAsset(input); assets.push({ id: `asset:${asset.id}`, publicPath: asset.publicPath!, entry: asset.entry, source: asset.source, contentType: asset.contentType ?? contentTypeForPath(asset.entry ?? asset.publicPath!), immutable: asset.immutable ?? false, }); } return assets; } export async function readPluginAsset(asset: ServedPluginAsset): Promise { if (typeof asset.source === "string") return asset.source; if (asset.source instanceof Uint8Array) return toBody(asset.source); if (!asset.entry) throw new Error(`Plugin asset '${asset.id}' has no source.`); const shouldBundle = asset.runtime && (asset.bundle ?? /\.[cm]?tsx?$/.test(asset.entry)); if (!shouldBundle) return toBody(await readFile(asset.entry)); const result = await Bun.build({ entrypoints: [asset.entry], target: "browser", format: asset.runtimeType === "script" ? "iife" : "esm", minify: false, sourcemap: "inline", }); if (!result.success || !result.outputs[0]) { throw new Error( `Plugin runtime '${asset.id}' failed to build:\n${result.logs.map(String).join("\n")}`, ); } return toBody(new Uint8Array(await result.outputs[0].arrayBuffer())); } export async function servePluginAsset( assets: readonly ServedPluginAsset[], pathname: string, mode: "development" | "production", ): Promise { const asset = assets.find((entry) => entry.publicPath === pathname); if (!asset) return null; try { const body = await readPluginAsset(asset); return new Response(body, { headers: { "content-type": asset.contentType, "cache-control": mode === "production" && asset.immutable ? "public, max-age=31536000, immutable" : "no-cache", "x-content-type-options": "nosniff", }, }); } catch (error) { console.error(`[wrnexus] failed to serve plugin asset ${asset.publicPath}`, error); return new Response("Plugin asset unavailable", { status: 503 }); } } export function runtimeScript(input: ClientRuntimeDefinition): ScriptAsset { const runtime = normalizeClientRuntime(input); return { src: runtime.publicPath!, type: runtime.type === "script" ? "classic" : "module", async: runtime.load === "eager" ? false : undefined, defer: runtime.load !== "eager", integrity: runtime.integrity, crossOrigin: runtime.crossOrigin, attributes: { "data-wrnexus-runtime-src": runtime.id, ...(runtime.attributes ?? {}), }, }; } export function runtimeIdsFromMarkup(body: string): Set { const ids = new Set(); for (const match of body.matchAll(/\bdata-wrnexus-runtime\s*=\s*["']([^"']+)["']/gi)) { for (const id of match[1]!.split(/[\s,]+/)) { if (id) ids.add(id); } } return ids; } export function runtimeScriptsForMarkup( body: string, runtimes: readonly ClientRuntimeDefinition[] = [], ): ScriptAsset[] { const ids = runtimeIdsFromMarkup(body); return runtimes.filter((runtime) => ids.has(runtime.id)).map(runtimeScript); } export function mergePluginAssets( runtimes: readonly ClientRuntimeDefinition[], assets: readonly PackageAssetDefinition[], ): ServedPluginAsset[] { return pluginAssetsFromContributions({ componentDirs: [], clientRuntimes: [...runtimes], assets: [...assets], styles: [], routes: [], middleware: [], migrations: [], directives: [], cliCommands: [], virtualModules: [], deploymentAdapters: [], configSchemas: [], documentation: [], typeDefinitions: [], }); }