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

ooxml-validate

v0.0.4

Published

Validate OOXML documents (pptx, xlsx, docx) against the Open XML SDK schema validator, from Node.

Readme

ooxml-validate

Validate OOXML documents — .pptx, .xlsx, .docx and friends — against Microsoft's Open XML SDK schema validator, from Node.

import {validate, validateBuffer} from 'ooxml-validate';

const report = await validate(['deck.pptx', 'book.xlsx']);
for (const result of report.results) {
  if (!result.valid) console.error(result.file, result.diagnostics);
}

The schema validator itself is .NET. This package ships a small self-contained CLI around it and downloads the right prebuilt binary for your platform on first use, so consumers do not need a .NET SDK installed.

  • Package: ooxml-validate on npm (unscoped).
  • Repo: shbernal/ooxml-validate. The name mismatch is deliberate.

Install

pnpm add -D ooxml-validate

Nothing is downloaded at install time. The first call that actually needs the validator fetches a self-contained binary (~42 MB compressed, ~110 MB on disk) into ~/.cache/ooxml-validate/<version>/, verifies its checksum and its GitHub build provenance attestation, and reuses it from then on. Reinstalls and multiple checkouts share one cache entry.

Supported platforms: linux-x64, linux-arm64, osx-x64, osx-arm64, win-x64.

Use

As a library:

| Export | What it does | |---|---| | validate(paths, opts?) | Validate files on disk. | | validateBuffer(bytes, {ext, label?, format?}) | Validate an in-memory package. | | validateBuffers(inputs, opts?) | Batch form of the above. | | validatorAvailable() | Whether the binary can be resolved. Throws under CI. | | probeFormats(paths) | Error counts at every conformance target. | | oracleVersion() | The oracle's version and the Open XML SDK it links. | | FILE_FORMATS / FILE_FORMAT | Conformance targets, and the pinned default. | | isFileFormat(value) | Whether a string names a conformance target, as a type guard. | | resolveValidator() | The binary's path, downloading it once if needed. Throws if it cannot be obtained. | | validatorPath() | Same, but null instead of throwing. | | currentPlatform() / SUPPORTED_PLATFORMS | This host's platform id (null if unsupported), and all of them. | | cacheRoot() | Where downloaded binaries are cached. | | PACKAGE_NAME / PACKAGE_VERSION / RELEASE_TAG | This package's name and version, and the release its binary comes from. |

Validating in-memory packages needs no temp-file bookkeeping from you:

const results = await validateBuffers([
  {bytes: deck, ext: 'pptx', label: 'quarterly-review'},
  {bytes: book, ext: 'xlsx', label: 'figures'},
]);
// results[].file is 'quarterly-review' / 'figures', never a temp path.

The bytes go to temp files (the oracle only reads files), and the temp path is mapped back to your label before you see it. Correlation is by that map alone, never by array position — so results stay attributable however the oracle orders them. Temp files are cleaned up even if a batch crashes. ext must be a bare extension ('pptx' or '.pptx', letters and digits only); anything else throws a TypeError before a byte is written.

Calls made while an invocation is in flight are coalesced into the next batch, which holds the process to one validator child at a time regardless of how many callers there are. That matters: the binary costs ~0.3 s of startup and ~55 MB of RSS, and one child per call multiplies both by your test runner's concurrency.

As a CLI:

pnpm exec ooxml-validate deck.pptx

Exit codes are meaningful and are part of the contract:

| Code | Meaning | |---|---| | 0 | Every input validated clean. | | 1 | Validation errors were found. | | 2 | The tool could not run — bad arguments, unreadable file, crash. |

Diagnostics go to stdout as JSON; tool failures go to stderr as text. A file that could not be opened is a 2, never a 0 — the distinction between "clean" and "never actually checked" is the whole point of the exit codes.

The report

{
  "format": "Microsoft365",   // the conformance target that was applied
  "sdkVersion": "3.5.1",      // the Open XML SDK actually loaded
  "results": [
    {
      "file": "deck.pptx",    // echoed back exactly as given
      "valid": false,
      "truncated": false,     // true when the 1000-per-file cap dropped diagnostics
      "errors": [
        {
          "id": "Sch_UndeclaredAttribute",
          "type": "Schema",   // Schema | Semantic | MarkupCompatibility | Package | Limit
          "description": "The 'bogus' attribute is not declared.",
          "partUri": "/ppt/slides/slide1.xml",  // null when unattributable
          "xpath": "/p:sld[1]"                  // null when unattributable
        }
      ]
    }
  ]
}

Four properties of this output that consumers may rely on:

Every input file appears, each with an explicit valid flag. Clean files are not omitted. Do not write code that infers cleanliness from absence.

file is echoed verbatim — not resolved, not canonicalized, not relabelled. If you pass a relative path you get that relative path back. Callers validating in-memory content therefore own their own temp-path → handle mapping; the CLI has no label or alias channel, so file has exactly one meaning.

Output is deterministic. Results are ordered by path, diagnostics by (partUri, xpath, id, description), all ordinal. The same inputs in a different argument order produce byte-identical stdout.

A package that will not open is a finding, not a crash. It becomes a PackageOpenError diagnostic on that file, the rest of the batch is still validated, and the exit code is 1. A path that names nothing readable is a different thing entirely and exits 2.

A package too large to validate safely is refused unopened. If its zip directory declares more than 512 MiB uncompressed, it gets one diagnostic, PackageTooLarge of type Limit, and exit 1. No real Office document comes near that; a few megabytes of zip that inflate to gigabytes do. The npm package also runs the oracle under a 3 GiB managed-heap ceiling (DOTNET_GCHeapHardLimit, which you can set yourself to override it), so a package that understates its own size ends as a PackageOpenError on that file instead of an out-of-memory kill of the whole batch.

Errors are capped at 1000 per file, and truncated says whether the cap dropped any. A truncated list is a prefix of the real set, so do not baseline it as if it were complete: an SDK bump that adds one early-sorting diagnostic pushes a different one off the end, and the diff shows a removal nobody fixed.

Batching

Pass --files-from <path> to read newline-delimited paths from a file, or --files-from - to read them from stdin. It composes with explicit path arguments. This is how large corpora are validated without hitting ARG_MAX; duplicate paths collapse to one result. A line is a path, so a path containing a line break cannot be expressed; the Node API rejects one with an error naming it rather than sending it.

Running it through a package script

A bare -- ends the options — everything after it is a path, even if it is spelled like a flag. This matters because package managers forward the separator rather than eating it, so pnpm run validate:ooxml -- book.xlsx reaches the CLI with the -- still attached. Both spellings work.

Conformance target

Validation runs against Microsoft 365 conformance by default, pinned by this package rather than inherited from any CLI or SDK default.

That is the strongest available check, not merely the newest. The SDK's per-version schemas differ in how much markup they model, so an older target skips newer constructs rather than rejecting them — error count is monotonically non-decreasing as the target version rises, and validating lower can only lose coverage. Override with FILE_FORMATS if you specifically need to know how an older Office version sees a document.

Environment

| Variable | Effect | |---|---| | OOXML_VALIDATE_BIN | Use this binary instead of resolving one. For bisecting against another build. | | OOXML_VALIDATE_NO_BATCH | Disable batching, so a failure pins to one input. | | OOXML_VALIDATE_TIMEOUT_MS | Time limit for one oracle invocation (default 120000). A child that exceeds it is killed and the call rejects. | | OOXML_VALIDATE_CACHE_DIR | Override where downloaded binaries are cached. | | OOXML_VALIDATE_NO_DOWNLOAD | Never fetch; fail if the binary is not already cached. | | OOXML_VALIDATE_FROM_SOURCE | Build the oracle from source. Needs a .NET SDK and a checkout of this repo. | | OOXML_VALIDATE_SKIP_ATTESTATION | Accept the checksum alone when provenance cannot be verified. | | CI | Makes an unobtainable binary a hard error instead of a one-line notice. |

validatorAvailable() is the gate to build skipIf on. Under CI an unobtainable binary throws, because a silently-skipped schema suite is green while proving nothing and obtaining the binary is part of the job. Locally it writes one notice to stderr and returns false.

In CI

Nothing to install — but three things are worth setting up once, because both consumer repos hit all three.

Cache the binary, keyed on your lockfile. The fetch is ~42 MB and happens on first use, not on postinstall. The lockfile is the right key because the npm version is the binary version: a bump changes both at once, so a lockfile change is exactly when the old entry stops being the right one.

- uses: actions/cache@v4
  with:
    path: ~/.cache/ooxml-validate
    key: ooxml-oracle-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

Give the fetch a GH_TOKEN. The download verifies build provenance as well as the checksum, and gh attestation verify needs a token to reach GitHub's attestation API. That check fails closed, so without a token the job fails — in the right direction, for the wrong reason. Resolving the binary in a step of its own also puts a download failure where the download's own error message is, rather than inside your test harness:

- name: Warm the oracle
  env:
    GH_TOKEN: ${{ github.token }}
  run: pnpm exec ooxml-validate --version

On pnpm, exclude the package from minimumReleaseAge if you want a fresh release before its cooling-off period elapses. Note the default is invisible: pnpm config get minimumReleaseAge reports undefined while pnpm still enforces 1440 minutes. Exclude by bare name, never name@version — the policy is checked against the lockfile, so a version-pinned exclusion names the version you are leaving and the one that gets rejected is the one you are on.

minimumReleaseAgeExclude:
  - ooxml-validate

Versioning

The npm version and the binary version are the same number, deliberately. The package always fetches the binary release matching its own version; there is no separate binaryVersion field and no resolution matrix. The cost is real — a TypeScript-only fix re-releases five binaries, and a rebuilt binary forces an npm bump — and it is accepted in exchange for never having a package and a binary that disagree about the report contract.

This is exactly the kind of invariant that quietly stops being true. If a binaryVersion split ever happens it will be a deliberate, documented change, not drift.

Project status

Public, supported, issues open and triaged.

What is not promised before 1.0 is a frozen contract: the JSON report shape, the exit codes and the TypeScript types may change. Every such change gets a CHANGELOG entry and a version bump — it will not happen quietly — but pin accordingly if you depend on the report shape.

Verify a checkout

pnpm install
pnpm run verify        # lint + typecheck + tests

The .NET half needs an SDK (see global.json) and has its own gates:

pnpm run oracle:build
pnpm run oracle:test

Prior art

The .NET validator wraps Microsoft's MIT-licensed Open XML SDK, which does the actual schema validation.

Credit to mikeebowen/OOXML-Validator for prior art in this space, and for working out several of the platform-install details a self-contained .NET binary needs. This project is an independent implementation on its own path — no code is shared, the report contract and exit-code behaviour differ on purpose, and it is not a fork, successor or drop-in replacement.

License

MIT