release: WRNexusJS 0.4.0
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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: [],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user