AgentSkillsCN

npm-package-develop

遵循现代最佳实践,使用 Node.js 与 TypeScript 开发 npm 包。 当您:(1) 从零开始创建一个新的 npm 包;(2) 为双重 ESM/CJS 配置 package.json 的 exports;(3) 为库的开发配置 TypeScript;(4) 构建并发布 npm 包;(5) 使用 bin 字段创建 CLI 工具;(6) 通过 vitest 配置测试;(7) 为 npm 发布配置 CI/CD;(8) 排查 ESM/CJS 互操作性问题;(9) 当用户提及“npm 包”、“发布到 npm”、“库开发”或“创建 npm 模块”时,可使用此技能。

SKILL.md
--- frontmatter
name: npm-package-develop
description: |
  Develop npm packages with Node.js and TypeScript following modern best practices.
  Use when: (1) Creating a new npm package from scratch, (2) Setting up package.json exports
  for dual ESM/CJS, (3) Configuring TypeScript for library authoring, (4) Building and
  publishing npm packages, (5) Creating CLI tools with bin field, (6) Setting up testing
  with vitest, (7) Configuring CI/CD for npm publishing, (8) Troubleshooting ESM/CJS
  interop issues, (9) User mentions 'npm package', 'publish to npm', 'library development',
  or 'create npm module'.

npm Package Development

Quick Start: Recommended Stack

  • Build: tsup (esbuild-powered, zero-config, dual CJS/ESM)
  • Test: vitest (native ESM/TS, Jest-compatible API)
  • Lint: Biome (all-in-one linter+formatter) or ESLint flat config + Prettier
  • Types: TypeScript with moduleResolution: "Bundler"
  • Dev: tsx for running TS, tsup --watch for rebuilding
  • Publish validation: publint + @arethetypeswrong/cli (attw)

Determine Package Type

  1. Library package -> Follow "Library Setup" below
  2. CLI tool package -> Follow "CLI Setup" below
  3. Both -> Combine both patterns

Library Setup

Minimal package.json

json
{
  "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"
  }
}

tsup.config.ts

ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["cjs", "esm"],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
});

tsconfig.json

json
{
  "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"]
}

File Structure

code
my-library/
  src/
    index.ts
    index.test.ts
  package.json
  tsconfig.json
  tsup.config.ts
  vitest.config.ts
  biome.json
  .gitignore
  LICENSE
  README.md

CLI Setup

package.json (CLI-specific fields)

json
{
  "bin": {
    "my-cli": "./dist/cli.js"
  },
  "files": ["dist"]
}

Entry file (src/cli.ts)

ts
#!/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).

Key Rules

exports Field

  • Always place types before default within each condition block
  • import condition for ESM, require condition for CJS
  • main/module/types at top level exist for backward compatibility with older tools

files Field

Always use files as a whitelist (not .npmignore). Set to ["dist"] to publish only build output. Verify with npm pack --dry-run.

prepublishOnly

Always include a prepublishOnly script to build before publishing:

json
{ "prepublishOnly": "npm run build" }

sideEffects

Set "sideEffects": false for pure utility libraries to enable tree-shaking. If some files have side effects, list them: "sideEffects": ["*.css"].

Tree-Shaking

Use named exports (not default export of objects). Avoid classes when individual functions suffice.

Pre-Publish Checklist

bash
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

Detailed References

Read these when you need specifics:

  • Build tools, tsconfig, testing, linting, monorepo: references/tooling.md - tsup/tsdown/unbuild comparison, TypeScript config, vitest setup, Biome vs ESLint, pnpm workspaces + Turborepo, dev workflow
  • Publishing, versioning, CI/CD, security: references/publishing.md - semver, Changesets/semantic-release, GitHub Actions OIDC trusted publishing, npm provenance, publint/attw, size-limit, supply chain security
  • Architecture, ESM/CJS, exports, CLI, dependencies: references/patterns.md - dual publishing patterns, conditional exports, subpath exports, dependency types (peer/optional/bundled), CLI bin setup, argument parsing, tree-shaking optimization