Npm package
Universal AI development toolkit. 74 production-ready skills for every coding agent. Works with Claude Code, Cursor, Codex.
npx -y skills add medy-gribkov/arcana --skill npm-packageAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
npm and pnpm package authoring expertise. Covers tsup and unbuild bundling, dual ESM/CJS exports, package.json fields (main, module, exports, types, bin, files), semver discipline, changesets for versioning, monorepo setup with pnpm workspaces and turborepo, npm publish workflow, scoped packages, peer dependencies, tree-shaking optimization, package provenance, and .npmrc configuration. Use when creating npm packages, configuring exports, setting up monorepos, or publishing to npm.
SKILL.md
12.0 KB, as published. Nobody here has run it
You are a senior JavaScript package author who ships library code that works everywhere with zero configuration headaches for consumers.
Use this skill when
- Creating a new npm package from scratch
- Configuring package.json exports for dual ESM/CJS
- Setting up tsup or unbuild for library bundling
- Managing a monorepo with pnpm workspaces
- Publishing packages to npm (public or private)
- Debugging "Cannot find module" or "ERR_REQUIRE_ESM" in consumers
- Setting up changesets for automated versioning
Package.json: The Complete Field Reference
{
"name": "@scope/my-lib",
"version": "1.0.0",
"description": "One line, searchable on npm",
"license": "MIT",
"author": "Name <email>",
"repository": { "type": "git", "url": "https://github.com/user/repo" },
"type": "module", // ESM by default. Omit for CJS-first.
"main": "./dist/index.cjs", // CJS entry (Node <12, bundlers fallback)
"module": "./dist/index.js", // ESM entry (bundlers: webpack, rollup)
"types": "./dist/index.d.ts", // TypeScript declarations
"exports": { // Modern entry point resolution (Node 12+)
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
},
"./utils": {
"import": { "types": "./dist/utils.d.ts", "default": "./dist/utils.js" },
"require": { "types": "./dist/utils.d.cts", "default": "./dist/utils.cjs" }
}
},
"files": ["dist", "README.md"], // Whitelist what gets published. Nothing else ships.
"bin": { "my-cli": "./dist/cli.js" },
"sideEffects": false, // Enables tree-shaking in bundlers
"engines": { "node": ">=18" },
"keywords": ["relevant", "search", "terms"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"prepublishOnly": "pnpm build",
"release": "changeset publish"
}
}
Critical ordering in exports: types MUST come first in each condition block. Node resolves top-down and TypeScript needs to find declarations before the runtime file.
Bundling with tsup
tsup is the gold standard for library bundling. Zero-config, built on esbuild.
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts", "src/utils.ts"],
format: ["esm", "cjs"],
dts: true, // Generate .d.ts files
splitting: true, // Code-split shared chunks (ESM only)
clean: true, // rm dist/ before build
treeshake: true, // Dead code elimination
sourcemap: true,
minify: false, // Don't minify libraries. Let consumers decide.
target: "node18",
outDir: "dist",
external: ["react", "react-dom"], // Never bundle peer deps
});
unbuild Alternative
Use unbuild when you want passive (stub) builds during development:
// build.config.ts
import { defineBuildConfig } from "unbuild";
export default defineBuildConfig({
entries: ["src/index"],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
inlineDependencies: false,
},
});
Run unbuild --stub for development -- it creates a proxy that imports directly from source. No rebuild loop needed.
Dual ESM/CJS: Getting It Right
The most common source of package bugs. Follow these rules exactly:
- Set
"type": "module"in package.json. Your source is ESM. - tsup outputs
.js(ESM) and.cjs(CJS) when format is["esm", "cjs"]. - Map exports explicitly. Never rely on auto-resolution.
- Test both paths:
# Test ESM
node --input-type=module -e "import { foo } from './dist/index.js'; console.log(foo)"
# Test CJS
node -e "const { foo } = require('./dist/index.cjs'); console.log(foo)"
Common Pitfalls
- Missing
.cjsextension: Iftype: "module", CJS files MUST use.cjs. Node will try to parse.jsas ESM. - Default export interop: CJS
module.exports = xbecomesimport x from "pkg"in ESM, butimport { default as x }in some bundlers. Prefer named exports for libraries. - Conditional exports mismatch: If
exportsfield exists,mainandmoduleare IGNORED by Node. They only serve as fallbacks for old bundlers.
Semver Discipline
- MAJOR (1.0.0 -> 2.0.0): Any breaking change. Removed exports, changed function signatures, dropped Node version support.
- MINOR (1.0.0 -> 1.1.0): New features, new exports. Everything existing still works.
- PATCH (1.0.0 -> 1.0.1): Bug fixes only. No new API surface.
Pre-1.0 (0.x.y): minor = breaking, patch = features. Get to 1.0 as fast as possible.
What counts as breaking:
- Removing or renaming an exported function/type
- Changing parameter order or types
- Narrowing accepted input or widening output types
- Dropping a Node.js version from
engines - Changing default behavior
Changesets for Versioning
pnpm add -D @changesets/cli
pnpm changeset init
Workflow:
- Developer runs
pnpm changesetand selects packages + bump type + writes summary. - This creates
.changeset/<random>.md-- committed with the PR. - On merge to main, CI runs
changeset version(consumes changesets, bumps versions, updates CHANGELOG). - Then
changeset publishpushes to npm.
# .github/workflows/release.yml
- uses: changesets/action@v1
with:
publish: pnpm changeset publish
version: pnpm changeset version
commit: "chore: release"
title: "chore: release"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Monorepo with pnpm Workspaces
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
// packages/core/package.json
{
"name": "@scope/core",
"version": "1.0.0",
"dependencies": {}
}
// packages/utils/package.json
{
"name": "@scope/utils",
"version": "1.0.0",
"dependencies": {
"@scope/core": "workspace:*" // Resolved to local package
}
}
workspace:* becomes the actual version on publish. pnpm handles the rewriting.
Turborepo for Task Orchestration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // Build deps first
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"cache": false // Don't cache test results
},
"lint": {} // No deps, runs in parallel
}
}
Run turbo build -- it builds in dependency order, caches outputs, skips unchanged packages.
Peer Dependencies
Use peer deps when your package wraps or extends another library:
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": { "optional": false }
},
"devDependencies": {
"react": "^19.0.0" // Install for development/testing
}
}
Rules:
- NEVER bundle peer deps. Add them to
externalin your bundler config. - Keep peer dep ranges as wide as possible.
^18.0.0 || ^19.0.0not^19.1.3. - Always install peers as devDependencies for your own tests.
Tree-Shaking Optimization
For consumers to tree-shake your library:
- Ship ESM (
import/export, notrequire/module.exports). - Set
"sideEffects": falsein package.json. - Use named exports, not default exports with namespace objects.
- Avoid top-level side effects (module-scoped
console.log,addEventListener, mutation).
// Bad: barrel file re-exports everything, defeats tree-shaking
export * from "./button";
export * from "./modal";
export * from "./table";
// Good: separate entry points in exports map
// "exports": { "./button": "...", "./modal": "...", "./table": "..." }
Publishing Checklist
# 1. Verify package contents
pnpm pack --dry-run # Shows exactly what ships. No secrets, no tests, no src.
# 2. Check exports resolve
npx publint # Catches exports/types mismatches
npx arethetypeswrong # Tests if types resolve for all consumers
# 3. Publish with provenance
npm publish --provenance # Links package to GitHub commit (npm provenance)
.npmrc for Publishing
# .npmrc (project root)
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
access=public
provenance=true
Scoped Packages
@scope/namerequires"access": "public"for free npm accounts.- First publish:
npm publish --access public. After that, access is remembered. - Private packages:
npm publish(default access is restricted for scoped).
Files Field vs .npmignore
Always use "files" whitelist. Never use .npmignore.
"files": ["dist", "README.md", "LICENSE"]
This is a whitelist. Only these paths end up in the tarball. package.json is always included automatically. node_modules is always excluded automatically. The whitelist approach is safer than blacklisting with .npmignore because new files you add (like .env.local) never accidentally ship.
bin Field for CLIs
{
"bin": { "my-tool": "./dist/cli.js" }
}
The CLI entry file needs a shebang:
#!/usr/bin/env node
// dist/cli.js
import { run } from "./index.js";
run(process.argv.slice(2));
Ensure the built file has the shebang. tsup preserves it if your source has it. Add banner: { js: "#!/usr/bin/env node" } in tsup config if needed.
Wrong Exports Edge Case: ESM/CJS Mismatch Debugging
SYMPTOM: "Cannot find module" or "ERR_REQUIRE_ESM" in consumers
Diagnosis Checklist
# 1. Verify package contents
npx publint # Catches exports issues
npx arethetypeswrong # Tests TypeScript types across formats
# 2. Check what Node.js resolves
node -p "require.resolve('@scope/my-lib')" # CJS resolution
node --input-type=module -e "import('@scope/my-lib')" # ESM resolution
# 3. Inspect actual exports in published package
npm pack --dry-run
tar -tzf *.tgz | grep dist/
Common Mismatches
// ❌ BAD: Exports mismatch with actual files
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.js", // File exists
"require": "./dist/index.cjs" // ❌ File missing or wrong extension
}
}
}
// ✅ GOOD: Match tsup output exactly
// tsup outputs: index.js (ESM), index.cjs (CJS)
{
"type": "module",
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
}
}
Fix: ESM-only package causing CJS errors
If your package is ESM-only and consumers using require() fail:
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.js"
// No "require" field = CJS consumers get clear error
}
}
}
Document in README: "This is an ESM-only package. Use import not require."
Fix: Dual package hazard
// ❌ HAZARD: Same code loaded twice if not careful
// app.mjs: import { state } from 'my-lib'
// app.cjs: const { state } = require('my-lib')
// Result: Two separate instances of `state`
Solution: Document that mixing ESM/CJS in the same app can cause state duplication. Recommend consumers stick to one format project-wide.
Testing Both Formats Locally
# Create test CJS consumer
mkdir test-cjs && cd test-cjs
npm init -y
npm install ../my-package
node -e "const x = require('@scope/my-lib'); console.log(x)"
# Create test ESM consumer
mkdir test-esm && cd test-esm
npm init -y && echo '{"type":"module"}' > package.json
npm install ../my-package
node -e "import('@scope/my-lib').then(x => console.log(x))"