npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

to-ooxml

v0.0.6

Published

Isolated browser conversion of legacy binary Office files to OOXML bytes.

Readme

to-ooxml

An isolated, browser-first TypeScript library for converting legacy binary Microsoft Office bytes to OOXML bytes. It renders nothing, uploads nothing, and runs native parsers in disposable Web Workers.

This repository contains working conversion paths for all three legacy families. BIFF8 .xls/.xlt uses pinned DocSharp.Binary.Xls 0.21.0 on the shared .NET browser-WASM runtime; BIFF5/7 and safely undetectable workbooks fall back to the pinned rxls 0.1.3 Rust engine. Word 97–2003 .doc/.dot uses the matching patched DocSharp.Binary.Doc engine. PowerPoint 97–2003 .ppt/.pot/.pps uses DocSharp.Binary.Ppt on the same isolated runtime.

Usage

Use both the filename and MIME type when they are available. The CFB directory inside the bytes remains authoritative, and contradictory hints are rejected.

import { LegacyOfficeConverter } from "to-ooxml";

const bytes = await file.arrayBuffer();
const converter = LegacyOfficeConverter.forInput(bytes, {
  fileName: file.name,
  mimeType: file.type,
});

if (!converter) {
  throw new Error("Not a supported legacy Office file");
}

const result = await converter.convert(bytes, {
  signal: abortController.signal,
  timeoutMs: 60_000,
});

// Feed the OOXML payload to @silurus/ooxml.
consumeOoxml(result.bytes, result.mimeType);

console.table(result.diagnostics);

MIME-only selection is also available for dispatch code:

const converter = LegacyOfficeConverter.forMimeType(
  "application/vnd.ms-excel",
);
const xlsx = await converter?.convertBytes(bytes);

The metadata-rich convert() result is preferred. It returns the actual output extension and MIME type, engine identity, source detection, engine report, and fidelity diagnostics. Macro-enabled and template outputs are returned with their actual extension and MIME type rather than being mislabeled as plain OOXML.

Vite and SvelteKit

Add the asset plugin to the Vite configuration. In a SvelteKit project it goes in the same plugins array as sveltekit().

import { defineConfig } from "vite";
import { toOoxmlAssets } from "to-ooxml/vite";

export default defineConfig({
  plugins: [toOoxmlAssets()],
});

to-ooxml refers to its two engine entry files with new URL("./runtime/...", import.meta.url). Vite treats those as static assets: it emits dotnet.js and rxls_wasm_bg.wasm into build.assetsDir under content-hashed names and rewrites both URLs for the final base, including an absolute kit.paths.assets CDN origin. The plugin copies the remaining .NET runtime files (assemblies, native module, ICU data) beside the emitted dotnet.js, which is where the loader resolves them. The .NET SDK inlines the boot configuration into dotnet.js, so there is no separate manifest request, and every other runtime file carries an SDK content fingerprint. The emitted tree is therefore fully content-addressed and safe under SvelteKit's immutable asset caching across package upgrades.

In development the plugin excludes to-ooxml from dependency pre-bundling so the package-relative URLs keep pointing into node_modules/to-ooxml/dist/runtime, which the Vite dev server serves directly. Nothing is copied. An SSR build receives no runtime files.

Both worker programs are bundled into the browser entry and started from a page-owned blob: URL. No worker script is fetched from the application or a CDN.

CORS and CSP

When assets live on another origin, that origin needs only a plain Access-Control-Allow-Origin for the application (or *). The loader and its runtime modules are imported as anonymous ES modules, and assemblies, ICU data, and WASM are fetched without credentials, so no Access-Control-Allow-Credentials header and no cookies are involved. Bucket-backed CDNs such as Google Cloud Storage, which cannot send that header, work unchanged.

The asset origin must be allowed in script-src (module imports of dotnet.js and its native and runtime modules) and in connect-src (assembly, ICU, and WASM fetches). 'wasm-unsafe-eval' permits WebAssembly compilation. worker-src needs only 'self' blob:. A strict policy can use:

Content-Security-Policy: default-src 'none'; script-src 'self' https://assets.example.com 'wasm-unsafe-eval'; connect-src 'self' https://assets.example.com; worker-src 'self' blob:

Add the directives the rest of the application needs. The package's browser integration test runs exactly this policy against a CDN origin that reflects Access-Control-Allow-Origin and sends no credentials header.

Hosting the runtime yourself

Deployments that do not use the plugin can copy node_modules/to-ooxml/dist/runtime to a static directory and pass its URL. The layout below runtimeBaseUrl must match dist/runtime: docsharp/dotnet.js with its sibling files and rxls/rxls_wasm_bg.wasm. Relative values resolve against the page URL.

await converter.convert(bytes, { runtimeBaseUrl: "/to-ooxml/" });

Files served this way are not content-hashed by a bundler; version the directory or set cache headers accordingly when upgrading. This option replaces the earlier resolveWorkerUrl hook.

Current support

| Input | Output | Status | Engine | | --- | --- | --- | --- | | .xls (BIFF8) | .xlsx or .xlsm | Implemented; output follows binary macro metadata | DocSharp .NET/WASM | | .xlt (BIFF8) | .xltx or .xltm | Implemented; output follows binary template/macro metadata | DocSharp .NET/WASM | | .xls (BIFF5/7) | .xlsx | Implemented compatibility path | rxls Rust/WASM | | .xlt (BIFF5/7) | .xlsx | Implemented with explicit template-loss diagnostic | rxls Rust/WASM | | .doc | .docx or .docm | Implemented; output follows binary macro metadata | DocSharp .NET/WASM | | .dot | .dotx or .dotm | Implemented; output follows binary template/macro metadata | DocSharp .NET/WASM | | .ppt | .pptx or .pptm | Implemented; output follows binary macro metadata | DocSharp .NET/WASM | | .pot | .potx or .potm | Implemented; template subtype follows filename, macros follow bytes | DocSharp .NET/WASM | | .pps | .ppsx or .ppsm | Implemented; show subtype follows filename, macros follow bytes | DocSharp .NET/WASM |

“Lossless” is an acceptance target, not a blanket promise. DocSharp's direct BIFF8-to-SpreadsheetML mapper has a much broader preservation surface than the compact fallback, including workbook styles, drawings, charts, external links, and VBA where its model supports them. Older BIFF workbooks use rxls, which retains only the workbook model it understands; macros, embedded objects, and other unsupported records may not survive. DocSharp's Word path preserves sections, headers/footers, numbering, notes, comments, fields, drawings, embedded OLE, and VBA where modeled. Its PowerPoint path preserves slide structure, masters, images, embedded OLE, and VBA where modeled. All three still have unsupported binary edge cases and need a much larger fidelity corpus. Every conversion therefore includes an engine-boundary warning.

Isolation and limits

  • MIME and extensions are hints; CFB stream names identify the actual family.
  • Each conversion gets a new Worker. Success, failure, cancellation, and timeout all terminate it, reclaiming its WebAssembly memory.
  • Caller-owned buffers are copied before transfer, so conversion never detaches the source buffer.
  • XLS, Word, and PowerPoint input are capped at 32 MiB; both runtimes cap output at 128 MiB.
  • Workers start from a page-owned blob: URL and load only the engine assets the bundler emitted (or the caller-provided runtimeBaseUrl). No document bytes are sent over the network.
  • Encrypted files fail with a typed encrypted error; password handling is not implemented.
  • Detected BIFF2–4 files fail with typed unsupported-format; the implemented spreadsheet range begins at BIFF5/7.
  • VBA is packaged as data and never executed. Macro-bearing BIFF8, Word, and PowerPoint input returns a macro-enabled OOXML subtype instead of silently pretending to be macro-free.
  • PPT bytes do not reliably encode whether the source was a presentation, template, or show. .pot/.pps identity therefore comes from the supplied filename and is disclosed as subtype-selected-from-filename.

The minified public entry, including both inlined worker programs, is 29.6 KB raw (8.9 KB gzip). The older-BIFF fallback worker and its 1.19 MB raw WASM asset cost about 463 KB Brotli including glue. BIFF8 uses the larger .NET host shared with Word and PowerPoint: the complete published framework is 10.9 MB raw, while an EFIGS session is about 2.63 MB Brotli. The rooted XLS assembly itself is 212 KB raw/70 KB Brotli. The current shared host fetches all rooted DocSharp format assemblies on its first conversion; neither runtime is downloaded merely by importing the dispatcher.

Development

git submodule update --init --recursive
pnpm install
pnpm check

pnpm build compiles the pinned Rust crate with wasm-pack, publishes the pinned DocSharp host with the .NET 10 browser-WASM SDK, bundles both worker programs into strings, and then uses tsdown to emit the browser ESM entry, its declarations, the Vite plugin, and the dist/runtime engine assets. The DocSharp assemblies are deliberately rooted during trimming because upstream's record and shape registries still use reflection.

DocSharp is a submodule pinned to the to-ooxml-0.21.0.1 tag in the kvist-no/DocSharp fork. Clone with --recurse-submodules, or run the initialization command above before building.

The reproducible toolchain is Rust 1.97.1 with the wasm32-unknown-unknown target, wasm-pack 0.15.0, and .NET SDK 10.0.107. After building, tests/browser/smoke.html can be served from the repository root with a same-origin ?fixture=/path/to/file.xls&fileName=file.xls to exercise the actual Worker, runtime assets, byte interop, and OOXML ZIP structure in a browser.

The full engine audit and staged architecture are in docs/architecture/legacy-office-engine-research.md.