
Package Publishing
- 77 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
package-publishing is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- package-publishing
- AI & Agent Building
- AI-coding skill
Package Publishing by the numbers
- 77 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,386 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill package-publishingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 77 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Package Publishing
Overview
Covers modern npm package authoring: package.json configuration with the exports field, dual ESM/CJS builds, TypeScript type declarations, and secure publishing workflows with provenance.
When to use: Configuring package entry points, setting up conditional exports, building dual-format packages, publishing scoped packages, or troubleshooting module resolution.
When NOT to use: Application-level bundling (Vite/webpack app configs), monorepo workspace orchestration (Turborepo/Nx), private registry setup (Verdaccio/Artifactory).
Quick Reference
| Pattern | Field / Command | Key Points |
|---|---|---|
| Entry point | exports in package.json | Replaces main/module; encapsulates internals |
| CJS fallback | main | Legacy consumers without exports support |
| ESM entry | module | Bundler convention; not used by Node.js |
| Type declarations | types condition in exports | Must be listed first in each condition block |
| Subpath exports | "./utils": { ... } | Clean public API; blocks deep imports |
| Conditional exports | import/require conditions | Toggle ESM vs CJS per consumer |
| Package type | "type": "module" | Makes .js files ESM; use .cjs for CommonJS |
| Side effects | "sideEffects": false | Enables tree-shaking in bundlers |
| Peer deps | peerDependencies | Shared runtime deps (React, Vue, etc.) |
| Engine constraints | "engines": { "node": ">=18" } | Document minimum Node.js version |
| Files allowlist | "files": ["dist"] | Controls what gets published to npm |
| Prepublish check | "prepublishOnly": "npm run build" | Ensure build runs before publish |
| Dry run | npm pack --dry-run | Preview package contents before publishing |
| Provenance | --provenance flag or trusted publishers | Cryptographic build attestation |
| Scoped publish | --access public | Required for first publish of scoped packages |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Putting types after default in exports | types must be the first condition in every export block |
Missing "./package.json" in exports | Include "./package.json": "./package.json" for tooling compatibility |
Different APIs for import vs require | Same API surface; write ESM source, transpile to CJS |
Using main without exports for new packages | Use exports as the primary entry point definition |
Forgetting "type": "module" with .js ESM output | Set "type": "module" or use .mjs extension explicitly |
Publishing src/ or node_modules/ | Use "files" allowlist to include only dist/ |
No prepublishOnly script | Add build step to prevent publishing stale artifacts |
Using default export for libraries | Prefer named exports for consistent cross-tooling behavior |
Not testing with npm pack before publish | Always dry-run to verify package contents and size |
Omitting peerDependencies for framework plugins | Declare shared runtime dependencies as peers |
| Publishing without provenance | Enable provenance for supply-chain transparency |
Using .d.ts for CJS when package is "type": "module" | Use .d.cts for CJS type declarations, .d.ts or .d.mts for ESM |
Delegation
- Build tooling setup: Use
Exploreagent to examine tsup/unbuild/rollup configs - Type resolution debugging: Use
Taskagent with "Are the Types Wrong?" (attw) - Publish pipeline review: Delegate to
code-revieweragent
References
- Package.json configuration: exports, main, types, files, engines, peerDependencies
- Build scripts, prepublishOnly, npm pack, provenance, scoped packages
- Dual ESM/CJS builds, conditional exports, type declarations
Build and Publish
Lifecycle Scripts
npm runs scripts in a specific order during npm publish:
prepublishOnly -> prepare -> prepack -> postpack -> publish -> postpublishprepublishOnly
Runs only on npm publish, not on npm install. Use it to ensure the build is fresh:
{
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"prepublishOnly": "npm run build"
}
}prepare
Runs on both npm install (for git dependencies) and npm publish. Useful for packages installed directly from git:
{
"scripts": {
"prepare": "npm run build"
}
}Verifying Package Contents
npm pack --dry-run
Preview exactly what will be published without creating the tarball:
npm pack --dry-runOutput shows every file, its size, and the total package size. Check for:
- No
src/,node_modules/, test files, or config files leaking in - All
dist/files present (JS, type declarations, source maps if desired) - Package size is reasonable
npm pack
Create the actual tarball for local inspection:
npm packThis creates a .tgz file. Extract and inspect it, or install it in a test project:
mkdir /tmp/test-install && cd /tmp/test-install
npm init -y
npm install /path/to/my-lib-1.0.0.tgzpublint
Use publint to catch common package.json configuration issues:
npx publintattw (Are the Types Wrong?)
Verify type declarations resolve correctly across all TypeScript module resolution modes:
npx @arethetypeswrong/cli --pack .This checks node10, node16 (CJS), node16 (ESM), and bundler resolution modes.
Provenance
Provenance provides cryptographic proof of where and how a package was built, linking the published package to its source repository and CI build.
GitHub Actions with Trusted Publishers
The recommended approach uses npm trusted publishing (no long-lived tokens):
name: Publish
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}Key requirements:
id-token: writepermission is mandatory for provenance- Trusted publishers eliminate the need for
NPM_TOKEN(configure on npmjs.com under package settings) - Provenance is not supported for private repositories publishing public packages
- Only works on cloud-hosted CI runners
Manual Publishing with Provenance
For local publishing (not recommended for production):
npm publish --provenanceThis requires the npm CLI to be able to generate an OIDC token, which only works in supported CI environments.
Scoped Packages
First Publish
Scoped packages (@org/package) are private by default. To publish publicly:
npm publish --access publicOr set it permanently in package.json:
{
"name": "@my-org/my-lib",
"publishConfig": {
"access": "public"
}
}publishConfig
Override registry or access settings for publishing:
{
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
}
}This is useful for monorepos where the root .npmrc might point to a different registry.
Version Management
Manual Versioning
npm version patch -m "release: %s"
npm version minor -m "release: %s"
npm version major -m "release: %s"This updates package.json, creates a git commit, and tags it.
Changesets (Monorepo-Friendly)
For multi-package repos, changesets manages versioning and changelogs:
npx changeset
npx changeset version
npx changeset publishPre-Publish Checklist
1. Version bumped in package.json 2. Build passes with latest source 3. `npm pack --dry-run` shows correct files and reasonable size 4. `npx publint` reports no issues 5. `npx @arethetypeswrong/cli --pack .` shows no type resolution problems 6. Tests pass against the built output, not source 7. `README.md` is current (displayed on npmjs.com) 8. `LICENSE` file present 9. No secrets in published files (check .env, credentials) 10. Provenance enabled in CI pipeline
Dual Format
Why Dual Format
Not all consumers support ESM yet. Legacy Node.js applications, older bundlers, and tools like Jest (without ESM transform) may require CJS. Dual-format packages serve both audiences from a single codebase.
Node.js v22+ supports require() of ESM modules natively, which reduces the need for dual publishing over time. For new packages targeting current Node.js versions, ESM-only may be sufficient.
Conditional Exports
The exports field supports per-condition resolution. Node.js and bundlers evaluate conditions top-to-bottom and use the first match:
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
}
}Condition Evaluation Order
Conditions are matched in the order they appear in the object. Place more specific conditions before general ones:
{
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"default": "./dist/index.js"
}
}
}The types condition must always be first within each nested block. The default condition at the end acts as a fallback.
Available Conditions
| Condition | Matched When |
|---|---|
types | TypeScript resolving type declarations |
import | Loaded via import or import() |
require | Loaded via require() |
node | Running in Node.js |
browser | Bundler targeting browser |
development | Development mode (bundler-specific) |
production | Production mode (bundler-specific) |
default | Fallback; always matches |
File Extensions
With "type": "module" in package.json:
| Extension | Interpreted As | Type Declaration |
|---|---|---|
.js | ESM | .d.ts |
.mjs | ESM (always) | .d.mts |
.cjs | CJS (always) | .d.cts |
Explicit extensions (.mjs/.cjs) are unambiguous regardless of the type field. For dual-format packages with "type": "module", use .js for ESM and .cjs for CJS output.
Type Declarations for Dual Format
TypeScript must resolve the correct type declarations for each format. Mismatched types cause resolution failures.
Colocated Type Declarations (Recommended)
Place .d.ts next to .js and .d.cts next to .cjs:
dist/
index.js # ESM
index.d.ts # Types for ESM
index.cjs # CJS
index.d.cts # Types for CJSWith "type": "module", TypeScript automatically associates .d.ts with .js (ESM) and .d.cts with .cjs (CJS).
Explicit Type Conditions
Always specify types as the first condition:
{
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
}
}If types is not first, TypeScript may skip it and fail to find declarations.
Build Tool Configuration
tsup
Generates ESM, CJS, and type declarations in one command:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
outDir: 'dist',
});npx tsupOutput structure:
dist/
index.js # ESM
index.cjs # CJS
index.d.ts # ESM types
index.d.cts # CJS typesunbuild
Configuration-light build tool used by Nuxt:
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
});Rollup
For fine-grained control:
import typescript from '@rollup/plugin-typescript';
export default [
{
input: 'src/index.ts',
output: [
{ file: 'dist/index.js', format: 'esm' },
{ file: 'dist/index.cjs', format: 'cjs' },
],
plugins: [typescript()],
},
];The Dual Package Hazard
When a package provides both ESM and CJS entry points, a consumer might load both versions in the same process. This creates two separate module instances with separate state, breaking singletons, instanceof checks, and identity comparisons.
Mitigation Strategies
Stateless packages are safe. If your library is purely functional with no module-level state, the hazard does not apply.
Isolate state by extracting shared state into an internal CJS module that both ESM and CJS entry points import:
{
"exports": {
".": {
"import": "./dist/wrapper.js",
"require": "./dist/index.cjs"
},
"./state": "./dist/state.cjs"
}
}The ESM wrapper imports from the CJS state module, ensuring a single instance.
Document the hazard. If your library maintains state, note in your README that consumers should not mix import styles.
Complete Dual-Format package.json
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"sideEffects": false,
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
"prepublishOnly": "npm run build"
}
}Validation
Verify your dual-format setup works across all resolution modes:
npx @arethetypeswrong/cli --pack .Expected output shows green checkmarks for all resolution modes: node10, node16-cjs, node16-esm, and bundler.
Test the package as consumers would:
node -e "import('my-lib').then(m => console.log(Object.keys(m)))"
node -e "console.log(Object.keys(require('my-lib')))"Package.json Configuration
The exports Field
The exports field is the modern standard for defining package entry points. It replaces main and module, provides encapsulation (consumers cannot import internal files), and supports conditional resolution per environment.
Basic Single Entry Point
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}Subpath Exports
Expose specific modules without leaking internals:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./utils": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
},
"./hooks": {
"types": "./dist/hooks.d.ts",
"default": "./dist/hooks.js"
},
"./package.json": "./package.json"
}
}Consumers can now import { debounce } from 'my-lib/utils' but cannot import { internal } from 'my-lib/dist/internal'.
Subpath Patterns
Use wildcards for packages with many entry points:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
}
}Always Include ./package.json
Many tools (bundlers, linters, framework CLIs) read package.json directly. Without this entry, the exports encapsulation blocks access:
{
"exports": {
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"./package.json": "./package.json"
}
}Legacy Fields: main, module, types
Keep these for backward compatibility with older tools and bundlers:
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}| Field | Used By | Notes |
|---|---|---|
main | Node.js (pre-exports), older bundlers | CJS entry; fallback when exports is absent |
module | Bundlers (webpack, Rollup, esbuild) | ESM entry; not recognized by Node.js |
types | TypeScript (pre-exports support) | Top-level type declaration entry |
exports | Node.js 12.7+, modern bundlers, TypeScript 4.7+ | Preferred; takes precedence over all above |
files Allowlist
Controls which files are included in the published package. Without it, everything not in .gitignore or .npmignore is published:
{
"files": ["dist", "LICENSE"]
}Certain files are always included regardless: package.json, README.md, LICENSE/LICENCE, and the main entry file. Certain files are always excluded: node_modules, .git, .npmrc.
Verify contents before publishing:
npm pack --dry-runsideEffects
Tells bundlers whether modules have side effects (code that runs on import). Setting this enables aggressive tree-shaking:
{
"sideEffects": false
}If specific files do have side effects (CSS imports, polyfills):
{
"sideEffects": ["./dist/polyfill.js", "**/*.css"]
}engines
Document the minimum runtime version required:
{
"engines": {
"node": ">=18"
}
}This is advisory by default. Consumers can enforce it with engine-strict=true in their .npmrc. Use this to communicate which Node.js APIs your package depends on.
peerDependencies
Declare dependencies that must be provided by the consuming project:
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
}Guidelines:
- Framework packages (React, Vue, Angular) should always be peers
- npm 7+ auto-installs peer dependencies
- Use
peerDependenciesMetato mark optional peers (e.g.,react-domfor a lib that also works in React Native) - Specify broad version ranges to avoid conflicts in consumer projects
type Field
Controls how Node.js interprets .js files:
{
"type": "module"
}type value | .js interpreted as | CJS files use | ESM files use |
|---|---|---|---|
"module" | ESM | .cjs | .js or .mjs |
"commonjs" (default) | CJS | .js or .cjs | .mjs |
For dual-format packages, "type": "module" is the recommended default. Use .cjs extension for any CommonJS output files.