import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import { definePlugin } from "@wrnexus/plugin"; import { configureConversion } from "./convert/config.ts"; import type { ConversionOptions } from "./convert/types.ts"; import { configureUploads } from "./uploads/config.ts"; import type { ViewerUploadsOptions } from "./uploads/types.ts"; export interface ViewerPluginOptions { /** * Server-side conversion for formats no browser can render. Omitted, the * plugin mounts no routes and never runs an external process; the affected * formats keep their download card. */ convert?: ConversionOptions; /** * Lets `` resolve `@wrnexus/uploader` storage keys. Omitted, * no routes are mounted and a `key` on its own produces the download card. */ uploads?: ViewerUploadsOptions; } const here = (path: string) => fileURLToPath( new URL(import.meta.url.endsWith(".js") ? path.replace(/\.ts$/, ".js") : path, import.meta.url), ); const required = createRequire(import.meta.url); function resolves(specifier: string): string | undefined { try { return required.resolve(specifier); } catch { return undefined; } } /** * Tier 2 parsers with an optional peer dependency. Each is served as its own * runtime asset and contributed only when its package resolves here, so an app * that never installs `xlsx` neither builds nor ships it — and the matching * renderer reports itself unavailable because the asset is not there. */ const OPTIONAL = [ { id: "viewer-sheet", package: "xlsx", entry: "./optional/sheet.ts" }, { id: "viewer-docx", package: "mammoth", entry: "./optional/docx.ts" }, { id: "viewer-zip", package: "fflate", entry: "./optional/zip.ts" }, { id: "viewer-office", package: "fflate", entry: "./optional/office.ts" }, { id: "viewer-pdfjs", package: "pdfjs-dist", entry: "./optional/pdf.ts" }, ]; function optionalRuntimes(): { id: string; entry: string; bundle?: boolean }[] { const runtimes: { id: string; entry: string; bundle?: boolean }[] = []; for (const item of OPTIONAL) { if (!resolves(item.package)) continue; runtimes.push({ id: item.id, entry: here(item.entry) }); } // pdf.js is served from its own published build, not bundled: putting its ESM // entry through a bundler breaks the worker handshake. Both files are already // browser-ready ESM, and `.mjs` is served verbatim. const library = resolves("pdfjs-dist/build/pdf.mjs"); const worker = resolves("pdfjs-dist/build/pdf.worker.mjs"); if (library && worker && runtimes.some((runtime) => runtime.id === "viewer-pdfjs")) { runtimes.push({ id: "viewer-pdfjs-lib", entry: library, bundle: false }); runtimes.push({ id: "viewer-pdfjs-worker", entry: worker, bundle: false }); } return runtimes; } const route = (path: string, entry: string) => ({ kind: "api" as const, path: `/api/viewer/${path}`, entry: here(`./routes/${entry}.ts`), }); function routes(options: ViewerPluginOptions) { return [ ...(options.convert ? [ route("capabilities", "capabilities"), route("preview", "preview"), route("thumb", "thumb"), route("text", "text"), ] : []), ...(options.uploads ? [route("source", "source"), route("file", "file")] : []), ]; } export function viewerPlugin(options: ViewerPluginOptions = {}) { configureConversion(options.convert); configureUploads(options.uploads); return definePlugin({ name: "@wrnexus/viewer", componentDirs: [here("../components")], clientRuntimes: [ { id: "viewer-code", entry: here("./code-editor.ts") }, { id: "viewer", entry: here("./runtime.ts") }, ...optionalRuntimes(), ], styleSources: [{ id: "viewer", entry: here("../viewer.css") }], // Each feature mounts only its own routes, so enabling one does not publish // endpoints for the other that could only answer "not enabled". ...(routes(options).length ? { routeEntries: routes(options) } : {}), }); } export default viewerPlugin;