Publishing
Publishing is the finish line. Get the package.json right, verify everything’s clean, ship it. Here’s exactly how.
Before you publish
Section titled “Before you publish”-
TypeScript compiles clean.
Terminal window bun run build:packages # builds every workspace packagebun run typecheck # type-checks all packagesbun run typecheck:root # type-checks the root packageBoth must exit 0. A package with type errors is broken for every TypeScript consumer before they even run it.
-
Tests pass.
Terminal window bun testFix failing tests or delete them — don’t publish knowing something is broken. A red test is a promise you’re breaking.
-
Formatting is clean.
Terminal window bun run format:checkRun
bun run formatto auto-fix, then commit the result. -
JSDoc on every export.
Every function, interface, type, and class in the public API needs a JSDoc comment. If you changed a signature, updated params, or changed behavior, update the JSDoc in the same commit. Stale docs on public exports are a bug — consumers read them.
-
Public API surface is minimal.
Open
src/index.ts. Does every export actually need to be public? Internal helpers, test utilities, and intermediate types belong on internal modules or a/testingsubpath — not the main entry. The less you export, the less you have to support forever.
package.json essentials
Section titled “package.json essentials”A correctly shaped package.json for a Slingshot workspace package:
{ "name": "@lastshotlabs/slingshot-my-plugin", "version": "0.1.0", "type": "module", "exports": { ".": { "bun": "./src/index.ts", "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" }, "./testing": { "bun": "./src/testing.ts", "types": "./dist/src/testing.d.ts", "import": "./dist/src/testing.js" } }, "files": ["dist", "src"], "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", "typecheck": "tsc --noEmit", "prepublishOnly": "bun run build" }, "publishConfig": { "access": "public" }, "dependencies": { "@lastshotlabs/slingshot-core": "^0.1.0" }, "peerDependencies": { "hono": ">=4.12.12 <5", "zod": ">=4.0 <5" }}Key decisions:
"type": "module" — All Slingshot packages are pure ESM. Do not ship CJS.
"files": ["dist", "src"] — Include src alongside dist. The bun condition in the exports map points directly to TypeScript source, so Bun users get the unbuilt source for zero-overhead imports. Never include tests/, node_modules/, or .env files.
"prepublishOnly": "bun run build" — Ensures the dist/ is fresh every time you publish. Never publish a stale build.
"publishConfig": { "access": "public" } — Required for scoped packages (@lastshotlabs/*) on the public npm registry.
The exports map
Section titled “The exports map”The exports field is your package’s public API at the module resolution level — if a path isn’t in there, consumers can’t import it. That’s the feature, not the limitation.
Slingshot packages use three conditions per entry:
bun— Points to the TypeScript source insrc/. Used when the consumer is running under Bun directly. Zero build step, zero overhead.types— The.d.tsfile for the compiled output. Used by TypeScript’s type checker.import— The compiled.jsindist/. Used everywhere else (Node.js, bundlers, Vitest in Node mode).
Main entry point:
".": { "bun": "./src/index.ts", "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js"}Testing subpath — test helpers that consumers must not accidentally import in production code:
"./testing": { "bun": "./src/testing.ts", "types": "./dist/src/testing.d.ts", "import": "./dist/src/testing.js"}Optional feature subpaths — for packages that have opt-in integration code:
"./mongo": { "bun": "./src/entrypoints/mongo.ts", "types": "./dist/src/entrypoints/mongo.d.ts", "import": "./dist/src/entrypoints/mongo.js"}Dependencies vs. peerDependencies
Section titled “Dependencies vs. peerDependencies”Get this wrong and consumers either get duplicate installs or broken type checking.
dependencies — Packages your code imports at runtime that should be installed automatically when a consumer installs your package. Use this for packages consumers do not use directly (internal utilities, your own sub-packages).
"dependencies": { "@lastshotlabs/slingshot-core": "^0.1.0"}peerDependencies — Packages your code imports that the consumer must also have installed. Use this for host-framework packages like hono and zod. A consumer that already has hono installed should not get a second copy from your package.
"peerDependencies": { "hono": ">=4.12.12 <5", "zod": ">=4.0 <5"},"peerDependenciesMeta": { "mongoose": { "optional": true }}Mark optional integrations (database drivers, cloud SDKs) as optional: true in peerDependenciesMeta. The package still works without them — they are only required when the consumer uses that specific feature.
Versioning policy
Section titled “Versioning policy”Pre-production, no external consumers. The rules are practical:
| Change | Version bump |
|---|---|
| Bug fix, documentation, internal refactor (no interface change) | Patch (0.1.x) |
| New exported function, new config option, new subpath (additive) | Minor (0.x.0) |
| Removed export, renamed export, changed required parameter, changed return shape | Major (x.0.0) |
No deprecation cycles. If an API is wrong, rename or remove it and update every call site in the same commit.
Keep a CHANGELOG. Every minor and major bump needs an entry that says what changed and why — not just “updated API”. You’ll want that context six months from now.
Bumping versions in the workspace:
# Bump a single packagecd packages/my-plugin && npm version patch
# Bump all packages at once (root scripts)bun run release:patch # patch across the board + publishbun run release:minor # minor across the board + publishPublishing to npm
Section titled “Publishing to npm”-
Dry run first. Inspect what will actually be included:
Terminal window cd packages/my-pluginnpm publish --dry-runCheck the file list. Verify
dist/is present andsrc/is included. Verifytests/andnode_modules/are absent. -
Publish.
Terminal window # Using npmnpm publish --access public# Using bun publish (Bun 1.1+)bun publish --access publicThe root
package.jsonhas areleasescript that builds everything and publishes all packages in sequence:Terminal window bun run release # build + publish root + all workspace packages -
Verify on npm. Check
https://www.npmjs.com/package/@lastshotlabs/slingshot-my-pluginto confirm the version is live and theexportsmap is correct.
Workspace packages
Section titled “Workspace packages”When one workspace package depends on another, reference it with the workspace:* protocol rather than a version range:
{ "optionalDependencies": { "@lastshotlabs/slingshot-postgres": "workspace:*" }}workspace:* tells Bun to use the local version from the monorepo rather than resolving from npm. This means changes to slingshot-postgres are immediately visible in my-plugin without publishing intermediately.
When you publish, Bun replaces workspace:* with the actual resolved version number automatically.
Building all packages from the root:
bun run build:packages # runs `bun run --filter '*' build`This uses Bun’s workspace filter to run build in every package simultaneously. The --filter '*' wildcard matches all packages in the workspaces array of the root package.json.
Pre-publish checklist
Section titled “Pre-publish checklist”Run through this before npm publish:
-
bun run build:packagesexits 0 — broken types break every consumer immediately -
bun run typecheckexits 0 — catches cross-package type errors the build step misses -
bun testexits 0 — don’t publish a known broken state -
bun run format:checkexits 0 — code is formatted -
package.jsonhas the correctversion - CHANGELOG has an entry for this version
- Every export in
src/index.tshas a JSDoc comment — stale or missing docs on public API is a bug -
exportsmap covers all public entry points (main + subpaths) -
filesincludesdistandsrc, excludestestsand.env -
peerDependencieslistshonoandzod(and any other host-framework packages) - Optional integrations are marked
optional: trueinpeerDependenciesMeta -
npm publish --dry-runshows the expected file list — no secrets, no test artifacts