# Open File Viewer: fourteen format plugins, one unclosed array in the quick start

> Open File Viewer is a browser SDK that renders PDF, Office, images, media, archives, email, drawings, 3D and GIS files inside a DOM container you supply, with bindings for four frameworks off one core package. The parts worth reading are the two entry points you must not mix, the iframe script policy for remote PDFs, and the quick start block that ends before its array closes.

**xushanpei/open-file-viewer** — A framework-agnostic embedded file viewer for vanilla JavaScript, React, Vue and Svelte. Put PDF, Office, images, media, archives, email, drawings, 3D, GIS and engineering files inside one stable container.

- Repository: https://github.com/xushanpei/open-file-viewer
- Website: https://open-file-viewer-workspace.void.app/
- Stars: 1,865 · Forks: 197
- Language: TypeScript
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/xushanpei-open-file-viewer

## The quick start block stops before its array closes

The vanilla JavaScript quick start imports fifteen names from the core package, including cadPlugin, model3dPlugin, gisPlugin and fallbackPlugin, and then starts building the plugin array. The block ends there, mid-array, on the line for xmindPlugin, with no closing bracket and no semicolon. Four imported plugins are therefore never instantiated in the example. The paragraph that follows says to use the same plugin list to check support before mounting a viewer, which means the list the reader is meant to pass to is the list the reader never got. Copying the block as printed does not parse. That said, the shape is otherwise correct and instructive: image, video, audio and text first, then the PDF plugin configured with a worker source pulled in as a URL from pdfjs-dist, followed by the format plugins and the terminal fallback.

## Two entry points for one package, and the page says not to mix them

The core package has two documented entries and a hard rule about using them. The lite entry is what Angular CLI 20 applications should import, because that toolchain pre-bundles dependencies with Vite and would otherwise walk the optimizer into the Office conversion dependency graph. It deliberately contains only the viewer runtime and the image, PDF and fallback plugins:

```ts
import {
  createViewer,
  fallbackPlugin,
  imagePlugin,
  pdfPlugin
} from "@open-file-viewer/core/lite";
import "@open-file-viewer/core/style.css";
```

The main entry is for anything needing Office, CAD, archive or other formats, and the instruction not to mix the two entries in the same application is stated plainly. So a project that starts on the lite path and later adds a CAD preview has a migration ahead of it rather than a one-line import change. Installation is the short part: pnpm add @open-file-viewer/core, plus the framework package for React, Vue or Svelte, plus pdfjs-dist for PDF support.

## The remote PDF fallback enables scripts for cross-origin documents by default

When PDF.js cannot parse or load a remote PDF URL, the plugin falls back to the browser's own embedded PDF viewer, which means an iframe. The default policy is called auto, and it is asymmetric on purpose. Cross-origin PDF URLs receive allow-scripts while staying sandboxed. Same-origin URLs keep scripts blocked, because combining script execution with same-origin access is the combination worth avoiding. The reasoning is that different Chromium-based viewers apply different script requirements inside sandboxed iframes, so a single hard-coded policy breaks on some of them. A host can tighten it or loosen it:

```ts
pdfPlugin({ webFallbackScripts: "never" });  // strict sandbox
pdfPlugin({ webFallbackScripts: "always" }); // trusted PDF endpoints only
```

The page is also precise about the limit of the setting: it affects only the remote iframe fallback, does not change the normal PDF.js rendering path, and does not force a referrerpolicy value. So choosing always is a decision about documents served from elsewhere, not about your own.

## EMF and WMF conversion still leans on a Node canvas module marked external

Office document conversion has a browser caveat that the page states rather than hides. When using officePlugin with Vite, the converter has to be excluded from dependency pre-bundling and its optional Node canvas fallback has to be externalized from the browser build, because EMF and WMF conversion in the browser still uses Canvas:

```ts
// vite.config.ts — merge these options into your existing config
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps: { exclude: ["emf-converter"] },
  build: { rollupOptions: { external: ["@napi-rs/canvas"] } }
});
```

Marking a native Node module as external is the kind of instruction that tells you the browser path is the fallback rather than the target. The feature list frames this as progressive enhancement, where formats a browser can preview directly render locally first and complex formats can gradually integrate WASM, dedicated parsers, or server-side conversion. That is an honest description of a format support ladder, and it also means the Office, CAD and 3D paths are the ones most likely to need your own server later.

## Support checking is a pure predicate that shares the plugin array with the viewer

Before mounting anything, you can ask whether a file will be previewed:

```ts
import { isPreviewSupported } from "@open-file-viewer/core";

const supported = await isPreviewSupported(fileOrUrl, plugins, {
  fileName: "contract.pdf",
  mimeType: "application/pdf"
});
```

Three properties make this worth using rather than writing yourself. It normalizes the source exactly the way createViewer does, so the answer matches what the viewer would do with the same input. It evaluates each plugin's match in order without mounting DOM or calling render, so it is safe to run over a long queue before anything is displayed. And a matching fallbackPlugin does not count as native preview support, which means the answer distinguishes real rendering from the download fallback. The constraint that comes with it is an ordering rule: keep the plugin list identical to the viewer configuration, because a matching fallbackPlugin is terminal and must not be placed before the native plugins.

## The check script builds four example apps and a documentation site

The workspace manifest is private and named for the workspace rather than for a publishable package, and its check script is a single chain: pnpm test, then pnpm typecheck, then pnpm build, then pnpm build:examples, then pnpm build:doc, then pnpm pack:check. That last step runs a custom script at scripts/check-package-exports.mjs, which exists because the package publishes more than one entry point plus a stylesheet, and the export map has to stay honest as those entries move. The cost of the chain is that the unit tests are the first step of a sequence that also builds every package, all four example applications and the documentation site, so the fast loop and the full check are not the same command. Tests run under Vitest with jsdom, and the root devDependencies carry testing libraries for three frameworks along with react-dom and svelte themselves, in a package that publishes nothing. The documentation site is deployed from doc/dist as a single-page app.

## A stray test page at the root, and two releases published in one morning

Two things sit oddly at the top of the repository. One is a file called test-user-management.html, sitting at the root beside the manifests, when everything else that belongs to an example or a fixture lives in a directory. The other is the release cadence. The three most recent tags are v1.0.0-rc.1 published on 2026-09-28 at 09:31, v0.1.49 published the same morning at 05:52, and v0.1.48 from 2026-09-21, so the project moved from a 0.1.x series to a release candidate in a single day rather than announcing the candidate alongside a long 1.0.0 preview. Unlike most projects in this shape, the manifest version does match the newest tag, both reading 1.0.0-rc.1, so what you install and what is in the tree agree. All three release names are identical to their tags, with no descriptive titles to say what changed. The project has 1,845 stars, 196 forks and 2 open issues, and six readme languages are committed to the root.

## Conclusion

Open File Viewer suits a product team that needs attachment preview inside an existing page and has decided a new window is not acceptable, because container-first rendering is enforced rather than suggested, and the plugin split means formats can be dropped without touching the core. Two of its edges deserve a look before adoption. The heavy formats are not purely client-side: the progressive enhancement line leaves server-side conversion open, and browser EMF and WMF handling still leans on a Canvas path whose Node fallback has to be marked external in the browser build. And the remote PDF fallback ships with scripts enabled for cross-origin documents, which is a deliberate default with a documented way to turn it off. Three things to check. Which entry you import, since the lite and main builds must not be mixed and the difference is invisible until a format is missing. Whether you can serve the pdfjs-dist wasm directory at a matching version, because JPEG2000 and JBIG2 decode from it. And whether your own container sizing survives the CSS units the viewer listens to, since everything it does is relative to the box you hand it.

## FAQ

### What file formats can Open File Viewer preview?

The page groups them as PDFs, Office documents, images, audio and video, archives, emails, drawings, 3D files, GIS data and source code. Each format is handled by an independent plugin, so what you can render depends on which plugin functions you instantiate, and the terminal fallback plugin handles anything else as a download.

### Does Open File Viewer need pdfjs-dist?

Yes, PDF preview requires pdfjs-dist when you use the PDF plugin. PDF.js 5 and newer also decode formats such as JPEG2000 and JBIG2 from files under the pdfjs-dist wasm directory, which you serve from your own application and pass to the plugin as a public URL including the trailing slash.

### What is the lite entry for in Open File Viewer?

It is a lighter build of the core package containing only the viewer runtime and the image, PDF and fallback plugins. It exists for toolchains that pre-bundle dependencies, such as Angular CLI 20 with Vite, so the optimizer never enters the Office conversion dependency graph. The page says not to mix the lite and main entries in one application.

### How does Open File Viewer decide if a file can be shown?

The isPreviewSupported function takes the file or URL, the same plugin array used to mount a viewer, and the file name and MIME type. It normalizes the source the way createViewer does and evaluates each plugin's match in order, without mounting DOM or calling render, and a matching fallback plugin does not count as native preview support.

### Does Open File Viewer open previews in a new window?

No. Everything renders inside the DOM container you provide, so it does not open a new window or interrupt the host page. Sizing follows CSS units including px, %, vh, vw, rem and calc(), and the viewer responds automatically when the container changes.

## Sources

- [License: MIT](https://github.com/xushanpei/open-file-viewer/blob/main/LICENSE)
- [Project website](https://open-file-viewer-workspace.void.app/)
- [README](https://github.com/xushanpei/open-file-viewer/blob/main/README.md)
- [Releases](https://github.com/xushanpei/open-file-viewer/releases)
- [xushanpei/open-file-viewer on GitHub](https://github.com/xushanpei/open-file-viewer)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/xushanpei-open-file-viewer
