107 lines
4.0 KiB
TypeScript
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;
|