From claude-resources
Develops npm packages with Node.js and TypeScript: library setup, CLI tools, dual ESM/CJS exports, tsup/tsc builds, vitest testing, and versioning/dist-tag strategies.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-resources:dev-npm-packageThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- **Build**: tsup (esbuild-powered, zero-config, dual CJS/ESM) or tsc alone (for ESM-only packages)
moduleResolution: "Bundler" (with tsup) or "Node16" (with tsc alone){
"name": "my-library",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": ["dist"],
"sideEffects": false,
"engines": { "node": ">=18" },
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest",
"test:run": "vitest run",
"lint": "biome check .",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@biomejs/biome": "^2.3",
"tsup": "^8.4",
"typescript": "^5.7",
"vitest": "^3.0"
}
}
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
});
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"noUncheckedIndexedAccess": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
my-library/
src/
index.ts
index.test.ts
package.json
tsconfig.json
tsup.config.ts
vitest.config.ts
biome.json
.gitignore
LICENSE
README.md
For packages targeting modern Node.js (>=18) without CJS compatibility needs. Simpler than dual publishing.
{
"name": "@myorg/my-library",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"engines": { "node": ">=18" },
"scripts": {
"build": "tsc",
"test": "vitest run",
"prepublishOnly": "tsc && vitest run"
},
"devDependencies": {
"typescript": "^5.7",
"vitest": "^3.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Important: With Node16 resolution, all relative imports must include the .js extension (even for .ts source files): import { foo } from './utils.js'.
require() ESM natively)tsc only (no bundler){
"bin": {
"my-cli": "dist/cli.js"
},
"files": ["dist"]
}
Note: npm recommends bin paths without a ./ prefix ("dist/cli.js" not "./dist/cli.js"). Modern npm normalizes this automatically, but omitting ./ avoids warnings in older npm versions. Run npm pkg fix to check for issues.
#!/usr/bin/env node
import { program } from "commander";
program
.name("my-cli")
.version("1.0.0")
.description("Description here");
program
.command("init")
.option("-t, --template <name>", "template to use", "default")
.action((options) => {
console.log(`Template: ${options.template}`);
});
program.parse();
CLI argument parsing libraries: commander (most popular, subcommands), yargs (validation, middleware), citty (lightweight ESM-first).
The default install is the latest dist-tag: a tagless npm install <pkg> (or pnpm add / pnpm dlx) dereferences latest directly — it is NOT a semver range match, so whatever latest points at is exactly what new consumers get, prerelease or not. Keeping latest on the newest shippable build is the whole game; never strand it on an old version.
Pre-1.0 (0.x) — ship clean 0.MINOR.PATCH straight to latest. Do not put a -next/-beta suffix on the everyday dev mainline. 0.x (major-zero) is itself SemVer's "anything may change" signal, so a breaking change rides a minor bump (0.2 → 0.3) and everything else a patch bump. Every release is then a clean, monotonically-increasing version that npm routes to latest automatically — a tagless install always gets the newest build, with no machinery to get stuck (esbuild, pre-1.0 Vite, Bun, Biome all do this).
Prereleases are an opt-in side channel, not the mainline. Reserve -alpha/-beta/-rc/-next plus the next (or canary) dist-tag for genuine previews — a 1.0.0-beta run-up, or a bleeding-edge line published ahead of latest. next conventionally means "ahead of/distinct from latest" — never mirror it onto latest.
In CI, derive --tag from the version string and always pass it explicitly: hyphen → --tag next, clean X.Y.Z → --tag latest. npm ≥ 11 hard-errors when you publish a prerelease without --tag; npm ≤ 10 silently routed prereleases onto latest (a silent-downgrade footgun). Never rely on the implicit default for a prerelease. At 1.0.0 the normal stable/preview split resumes automatically under this same rule — no special-casing.
Detailed mechanics, the dual-tag "advance-latest" anti-pattern that strands latest, and ^0.x range gotchas: references/publishing.md.
types before default within each condition blockimport condition for ESM, require condition for CJSmain/module/types at top level exist for backward compatibility with older toolsAlways use files as a whitelist (not .npmignore). Set to ["dist"] to publish only build output. Verify with npm pack --dry-run.
Always include a prepublishOnly script to build (and ideally test) before publishing:
{ "prepublishOnly": "npm run build && npm test" }
For tsc-only projects, you can call commands directly: "prepublishOnly": "tsc && vitest run".
For scoped packages (@myorg/pkg), configure public access via .npmrc in the project root:
access=public
Alternatively, use publishConfig in package.json:
{ "publishConfig": { "access": "public" } }
Set "sideEffects": false for pure utility libraries to enable tree-shaking. If some files have side effects, list them: "sideEffects": ["*.css"].
Use named exports (not default export of objects). Avoid classes when individual functions suffice.
npm run build # Build the package
npx publint # Validate package.json/exports
npx attw --pack . # Validate TypeScript types
npm pack --dry-run # Inspect package contents
npm publish --dry-run # Simulate publish
Read these when you need specifics:
latest/next, the pre-1.0 0.x clean-mainline ruling, the dual-tag stale-latest anti-pattern, ^0.x range mechanics), Changesets/semantic-release, GitHub Actions OIDC trusted publishing, npm provenance, publint/attw, size-limit, supply chain securitynpx claudepluginhub takazudo/claude-resources --plugin claude-resourcesGuides TypeScript library authoring: project setup, dual CJS/ESM package exports, tsdown/unbuild config, type-safe API design, advanced type patterns, vitest testing, and npm release workflows.
Manages NPM packages, configures Node.js projects, handles dependencies, and troubleshoots issues using npm, yarn, or pnpm.
Provides opinionated Vite builds, ESLint configs, semantic versioning publishing, and TypeScript setup for JS/TS packages in pnpm/Nx monorepos.