Files
WRNexusJS/packages/viewer/src/plugin.ts
T

107 lines
4.0 KiB
TypeScript

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 `<Viewer key="…">` 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;