Files
WRNexusJS/packages/plugin/src/manifest.ts
T
2026-07-27 12:42:18 +05:30

145 lines
5.0 KiB
TypeScript

import { extname } from "node:path";
import type {
ClientRuntimeDefinition,
PackageAssetDefinition,
PackageStyleDefinition,
WrnexusPackageManifest,
} from "./types.ts";
const ID_PATTERN = /^[a-z0-9@][a-z0-9@/._-]*$/i;
export function assertContributionId(kind: string, id: string): void {
if (!id || !ID_PATTERN.test(id) || id.includes("..")) {
throw new Error(`WRN-PLUGIN-${kind.toUpperCase()}-ID: invalid id '${id}'.`);
}
}
export function defaultClientRuntimePath(id: string): string {
assertContributionId("runtime", id);
return `/__wrnexus/assets/${encodeURIComponent(id.replace(/^@/, "").replace(/[/.@]+/g, "-"))}.js`;
}
export function defaultPackageAssetPath(asset: PackageAssetDefinition): string {
assertContributionId("asset", asset.id);
const extension = asset.entry ? extname(asset.entry) : "";
const suffix = extension || extensionForContentType(asset.contentType);
const id = encodeURIComponent(asset.id.replace(/^@/, "").replace(/[/.@]+/g, "-"));
return `/__wrnexus/assets/${id}${suffix}`;
}
function extensionForContentType(contentType?: string): string {
if (!contentType) return "";
if (contentType.includes("javascript")) return ".js";
if (contentType.includes("css")) return ".css";
if (contentType.includes("json")) return ".json";
if (contentType.includes("svg")) return ".svg";
if (contentType.includes("wav")) return ".wav";
if (contentType.includes("png")) return ".png";
return "";
}
export function normalizeClientRuntime(
runtime: ClientRuntimeDefinition,
): ClientRuntimeDefinition & {
publicPath: string;
type: "module" | "script";
load: "eager" | "defer" | "idle";
singleton: boolean;
} {
assertContributionId("runtime", runtime.id);
if (!runtime.entry && runtime.source === undefined) {
throw new Error(`WRN-PLUGIN-RUNTIME-SOURCE: runtime '${runtime.id}' needs entry or source.`);
}
const publicPath = runtime.publicPath ?? defaultClientRuntimePath(runtime.id);
if (!publicPath.startsWith("/") || publicPath.includes("..")) {
throw new Error(`WRN-PLUGIN-RUNTIME-PATH: runtime '${runtime.id}' has an unsafe publicPath.`);
}
return {
...runtime,
publicPath,
type: runtime.type ?? "module",
load: runtime.load ?? "defer",
inject: runtime.inject ?? "body-end",
singleton: runtime.singleton ?? true,
};
}
export function normalizePackageAsset(
asset: PackageAssetDefinition,
): PackageAssetDefinition & { publicPath: string } {
assertContributionId("asset", asset.id);
if (!asset.entry && asset.source === undefined) {
throw new Error(`WRN-PLUGIN-ASSET-SOURCE: asset '${asset.id}' needs entry or source.`);
}
const publicPath = asset.publicPath ?? defaultPackageAssetPath(asset);
if (!publicPath.startsWith("/") || publicPath.includes("..")) {
throw new Error(`WRN-PLUGIN-ASSET-PATH: asset '${asset.id}' has an unsafe publicPath.`);
}
return { ...asset, publicPath };
}
export function definePackageManifest<T extends WrnexusPackageManifest>(manifest: T): T {
if (manifest.name) assertContributionId("package", manifest.name);
for (const runtime of manifest.clientRuntimes ?? []) normalizeClientRuntime(runtime);
for (const asset of manifest.assets ?? []) normalizePackageAsset(asset);
validateStyleIds(manifest.styles ?? []);
for (const route of manifest.routes ?? []) {
if (!route.path.startsWith("/") || route.path.includes("..") || !route.entry) {
throw new Error(`WRN-PLUGIN-ROUTE: invalid ${route.kind} route '${route.path}'.`);
}
}
for (const migration of manifest.migrations ?? []) {
assertContributionId("migration", migration.id);
if (!migration.entry && migration.source === undefined) {
throw new Error(
`WRN-PLUGIN-MIGRATION-SOURCE: migration '${migration.id}' needs entry or source.`,
);
}
}
return manifest;
}
export function validateStyleIds(styles: readonly PackageStyleDefinition[]): void {
const seen = new Set<string>();
for (const style of styles) {
assertContributionId("style", style.id);
if (seen.has(style.id)) throw new Error(`WRN-PLUGIN-STYLE-DUPLICATE: ${style.id}`);
seen.add(style.id);
if (!style.entry && !style.source) {
throw new Error(`WRN-PLUGIN-STYLE-SOURCE: style '${style.id}' needs entry or source.`);
}
}
}
export function contentTypeForPath(path: string): string {
const extension = extname(path).toLowerCase();
switch (extension) {
case ".js":
case ".mjs":
return "text/javascript; charset=utf-8";
case ".css":
return "text/css; charset=utf-8";
case ".json":
return "application/json; charset=utf-8";
case ".svg":
return "image/svg+xml; charset=utf-8";
case ".png":
return "image/png";
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".webp":
return "image/webp";
case ".wav":
return "audio/wav";
case ".mp3":
return "audio/mpeg";
case ".woff":
return "font/woff";
case ".woff2":
return "font/woff2";
default:
return "application/octet-stream";
}
}