Releasing packages
Seven packages are published to npm by the release workflow: @christophervr/xlsx-core and one self-contained editor package per framework (@christophervr/xlsx-react-viewer, and the unscoped xlsx-vue-viewer, xlsx-angular-viewer, xlsx-svelte-viewer, xlsx-solid-viewer and xlsx-vanilla-viewer). Each has its own version line, bumped only when it actually changes, and its own git tag <npm-name>@<version> (for example @christophervr/xlsx-core@0.2.0 or xlsx-vue-viewer@0.2.0). The flow is the one used by docx-viewer and pptx-viewer, adapted to this repository. Only user-facing packages are published: the shared internals are private and bundled into each framework package.
| Package | Dir | Depends on (published) | Internal packages bundled in |
|---|---|---|---|
@christophervr/xlsx-core | packages/core | ooxml-core | none |
@christophervr/xlsx-react-viewer | packages/react | @christophervr/xlsx-core, ooxml-core, ooxml-ui, react | both (below) |
xlsx-vue-viewer | packages/vue | @christophervr/xlsx-core, ooxml-core, ooxml-ui, vue | both |
xlsx-angular-viewer | packages/angular | @christophervr/xlsx-core, ooxml-core, ooxml-ui, @angular/core | both |
xlsx-svelte-viewer | packages/svelte | @christophervr/xlsx-core, ooxml-core, ooxml-ui, svelte | both |
xlsx-solid-viewer | packages/solid | @christophervr/xlsx-core, ooxml-core, ooxml-ui, solid-js | both |
xlsx-vanilla-viewer | packages/vanilla | @christophervr/xlsx-core, ooxml-core, ooxml-ui | both |
The internal packages are private: true and are never published: web-component (xlsx-web-component) and bindings (xlsx-bindings). scripts/build-packages.mjs bundles them into every framework package, so a published tarball imports only @christophervr/xlsx-core, ooxml-core, ooxml-ui and its framework peer. ole2 is inlined inside ooxml-core (ooxml-core/xlsx/load), so it is not a dependency of anything published and does not have to be released first. ooxml-ui (the shared office-ui-* controls) is a real registry dependency of every framework package, never bundled; it is not a workspace package, so the planner treats it as external: a change to its range in a manifest releases that package only, and the packaging checks (check:published, pack:smoke) allow exactly @christophervr/xlsx-core, ooxml-ui and ooxml-core as project dependencies. @christophervr/xlsx-core stays a dependency (not bundled) so Workbook is the same module on both sides of the application's own imports.
Dependencies between published packages are read from the manifests, never from a hand-kept list; the directories that are bundled in are the triggers of each package in scripts/release-plan.mjs. All Excel logic lives in ooxml-core (a separate repository with its own releases); @christophervr/ole2 is released from its own repository.
Status
The seven package names were claimed with 0.0.1 placeholders that contain only a README. The first real version, 0.1.0, needs the ooxml-core release that contains the xlsx area (the manifests ask for ooxml-core@^0.9.0; on 0.x a caret range pins the minor, so bump it with each core minor release).
How a release is decided
Everything is computed by scripts/release-plan.mjs from git history; run it locally at any time (it writes the git-ignored release-plan.json and changes nothing else):
bun run release:plan # or: node scripts/release-plan.mjs --no-npm (offline)For each package it finds the newest <npm-name>@x.y.z tag that is an ancestor of HEAD (the baseline) and looks at the files changed since:
- Own files. Any non-test file under
packages/<name>/releases that package. Test files,__tests__,CHANGELOG.mdand manifest edits that only touchversion,scripts,devDependenciesor ranges on sibling packages never do (so a release commit cannot retrigger a release). - Bundled internal packages (triggers). A non-test change under
packages/web-componentorbindingsreleases every framework package, because each one ships that code. A bump of theooxml-corerange changes each framework manifest and releases them too. - Published dependencies. A package re-releases whenever a package it depends on is released, because its dependency range changes. Releasing
@christophervr/xlsx-coretherefore releases all six framework packages; releasing one framework package releases nothing else. - Shared build pipeline.
scripts/build-packages.mjsandtsconfig.release.jsonchange every artifact and release everything. - Never published. A package with no tag that is not on npm releases at its manifest version.
On the first run the plan is seven packages, all 0.1.0 with bump initial, and no internal package appears in it:
core 0.1.0 -> 0.1.0 (initial; no previous tag) tag @christophervr/xlsx-core@0.1.0
react 0.1.0 -> 0.1.0 (initial; no previous tag) tag @christophervr/xlsx-react-viewer@0.1.0
vue ... angular ... svelte ... solid ... vanilla (same shape)The bump level is the highest Conventional Commit level among commits since the baseline that touch published files in the package's scope (its own directory, its bundled internal directories and its dependencies'): ! / BREAKING CHANGE: is major, feat is minor, anything else is patch. A test-only feat does not raise the level. The new version is that bump applied to the highest of the package's tags, the npm latest and its manifest version.
--write (used by the workflow) stamps the new versions and repoints every sibling dependency range at the version being released, keeping an existing ^ or ~ prefix. The framework packages depend on @christophervr/xlsx-core with a ^ range (^0.1.0 today); --write rewrites it to the exact version being released, so the tarball a consumer installs resolves an @christophervr/xlsx-core released in the same run. The private packages use workspace:* and are never rewritten.
Commit conventions
The bump level comes from the commit type, so conforming commits are enforced. Rules and examples are in CONTRIBUTING.md. The PR hygiene / Conventional Commits workflow validates the PR title and every commit subject (scripts/check-conventional-commits.mjs): a missing or unknown type fails; header length, casing and a trailing period only warn. Make that check required in the ruleset.
The release workflow
.github/workflows/release.yml has two jobs and two ways to start.
Scheduled or manual run without input (release job, then publish job):
- Check out
mainwith full history usingRELEASE_TOKEN, run the planner with--write. No changed package means the run ends here; that is what a quiet hour looks like. - Fail early if the
NPM_PUBLISHrepository variable is nottrue, so nothing is tagged that cannot be published. bun install,bun run build:packages,bun run check:published(no tarball imports an internal package orole2),bun run pack:smoke(installs the seven tarballs into a clean consumer and loads an.xlsxand a legacy.xlsworkbook through every framework package). Any failure stops the run before anything is committed or tagged; the next run retries from the same baseline.- Prepend the new section to each released package's
CHANGELOG.mdwith git-cliff (cliff.toml, scoped to the package's paths and tag pattern) and a dated section to the rootCHANGELOG.md(cliff-root.toml) listing the tags released. Changelogs are prepend-only: old tags are pruned, so history is never regenerated. - Commit
chore(release): bump versions and update changelogs [skip ci]straight tomain(version bumps, changelogs,bun.lock), retrying with a rebase ifmainmoved.[skip ci]keeps it from starting CI. - Tag each released package at that commit through a GitHub release (
scripts/release-notes.mjswrites the body), upload the plan, and prune superseded releases (scripts/prune-releases.mjs --keep 1; tags are kept). publishjob (environmentnpm): check out the release commit,bun install --frozen-lockfile,bun run build:packages, thennode scripts/publish-released.mjs --plan release-plan.json.
Manual dispatch with tag re-publishes one existing tag (skips the release job), for when a publish failed or was skipped:
gh workflow run release.yml -f tag=@christophervr/xlsx-core@0.1.0scripts/publish-released.mjs publishes in dependency order with npm publish --provenance --access public. For each package it first checks that the manifest on disk is the version being published, that no dependency uses workspace: or file:, that no dependency names an internal package or @christophervr/ole2, that sibling ranges match the siblings' versions, and that the version is not already on npm (it is skipped if so, making re-runs safe). A version older than the registry's latest is published under the old dist-tag. --dry-run prints the commands without publishing.
Authentication: trusted publishing, no secrets
There is no npm token anywhere. The publish job requests an OIDC token (id-token: write), npm >= 11.5.1 exchanges it for a short-lived publish credential, and --provenance attaches the attestation. Nothing else in the repository can publish.
Why the release job is not gated on a second test run
Like pptx-viewer, the release job does not repeat typecheck, unit and browser tests; it only builds and smoke-tests the packages it will publish. main is protected by the ci-success check (which ci.yml produces by funnelling every required job), so what is on main has passed CI. Do not push untested code straight to main.
One-time setup
- Rulesets / branch protection on
main: require theci-successcheck and theConventional Commitscheck; add a bypass for repository admins (the release commit is pushed straight tomain). RELEASE_TOKENsecret: a fine-grained personal access token withContents: Read and writeon this repository, owned by a repository admin. The defaultGITHUB_TOKENcannot be granted ruleset bypass, so without it the release commit cannot be pushed.NPM_PUBLISHrepository variable set totrue.npmenvironment (Settings, Environments), optionally with required reviewers.- npm trusted publisher for each of the seven packages (
@christophervr/xlsx-core,@christophervr/xlsx-react-viewer,xlsx-vue-viewer,xlsx-angular-viewer,xlsx-svelte-viewer,xlsx-solid-viewer,xlsx-vanilla-viewer; none of the internal packages, they are never published) (package settings on npmjs.com, "Trusted Publisher", GitHub Actions): organization/userChristopherVR, repositoryxlsx-viewer, workflowrelease.yml, environmentnpm. Every package already exists on npm (the0.0.1placeholders), so the trusted publisher can be configured before the first workflow run. - Run the workflow once by hand before relying on the hourly schedule.
Before the first release
- Make sure an
ooxml-corewith thexlsxarea (^0.9.0) andooxml-ui@^0.1.8are on npm, and that no manifest still has a localfile:range;@christophervr/xlsx-coredepends onooxml-core.@christophervr/ole2is not a dependency here: its codecs are inlined inooxml-core/xlsx/load. - Check the plan:
bun run release:planshould list seven packages at0.1.0with bumpinitialand no internal package. - Dispatch the workflow by hand. The seven packages are tagged
<npm-name>@0.1.0and published with@christophervr/xlsx-corefirst.
Local checks before dispatching:
bun install --frozen-lockfile
bun run typecheck && bun run test && bun run test:scripts
bun run build:packages && bun run check:published && bun run pack:smoke
bun run release:plan
node scripts/publish-released.mjs --plan release-plan.json --dry-runHousekeeping
prune-releases.ymlruns daily and keeps only the newest GitHub release per package (tags and npm versions are untouched). Run it manually withdry_runto preview.scripts/check-changelog-sections.mjs(CI) fails if a changelog contains a stub section such as_Releases: _, because changelogs are prepend-only and cannot be regenerated.- Scripts that are candidates to move into a shared package:
release-plan.mjs(only the package table differs),check-conventional-commits.mjs,check-changelog-sections.mjs,release-notes.mjs,prune-releases.mjs,publish-released.mjs. They are copied between this repository, docx-viewer,ooxml-coreandpptx-viewer.