165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
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<string | ArrayBuffer> {
|
|
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<Response | null> {
|
|
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<string> {
|
|
const ids = new Set<string>();
|
|
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: [],
|
|
});
|
|
}
|