All skills
jwynia avatar

/npm-package

@626387a
by J Wyniajwynia/agent-skills160 stars
20

Build and publish npm packages using Bun as the primary toolchain with npm-compatible output. Use when the user wants to create a new npm library, set up a TypeScript package for publishing, configure build/test/lint tooling for a package, fix CJS/ESM interop issues, or publish to npm. Covers scaffolding, strict TypeScript, Biome + ESLint linting, Vitest testing, Bunup bundling, and publishing workflows. Keywords: npm, package, library, publish, bun, bunup, esm, cjs, exports, typescript, biome, vitest, changesets.

Use this Skill: https://skilld.dev/gh/jwynia/agent-skills/npm-package

This session only. Nothing lands on disk.

referenceesm-cjs-guide.md

≈1.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

ESM/CJS Interoperability Guide

The Current State (2026)

ESM-only is now the correct default for new packages. require(esm) is stable and unflagged in all supported Node.js LTS versions (v20.19.0+, v22.12.0+). Node.js 18 reached EOL in April 2025.

Default to ESM-only ("type": "module") unless the package has a specific, documented need to support Node.js < 20.19.0.

Package.json exports Map

ESM-Only (Preferred)

{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist"]
}

Dual CJS/ESM (When Required)

Use the module-sync condition (Node.js 22.10+, backported to 20.19.0) to serve ESM to both import and require() consumers, eliminating the dual package hazard:

{
  "type": "module",
  "exports": {
    ".": {
      "module-sync": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./package.json": "./package.json"
  }
}

Critical Rules

types Must Be First

Within each condition block, types must appear before default or any other condition. TypeScript silently fails to resolve types otherwise.

// CORRECT
{ "types": "./dist/index.d.ts", "default": "./dist/index.js" }

// BROKEN — TypeScript won't find types
{ "default": "./dist/index.js", "types": "./dist/index.d.ts" }

exports Encapsulates Completely

Once you add an exports field, all non-exported paths become inaccessible to consumers. Always explicitly export "./package.json": "./package.json" — many tools need it.

Condition Order Matters

default must always be last. Custom conditions go before import/require. Wrong order silently serves the wrong file to some consumers.

Prefer Named Exports

Default exports behave differently between CJS and ESM. A CJS consumer doing const pkg = require('your-pkg') gets the module namespace, not the default export, unless they use pkg.default. Named exports avoid this entirely.

// PREFER
export function doThing(): void { /* ... */ }
export const CONFIG = { /* ... */ };

// AVOID as primary API surface
export default class MyThing { /* ... */ }

Changes to exports Are Semver-Major

Adding, removing, or restructuring export paths will break some consumer in some environment. Treat exports map changes as breaking.

The Dual Package Hazard

When Node.js loads both a CJS and ESM copy of the same package (e.g., one dependency imports it and another require()s it), the package initializes twice. This causes:

  • Duplicate state (singletons aren't singletons)
  • instanceof checks fail across module boundaries
  • Side effects run twice

Solutions (in order of preference):

  1. Ship ESM-only — no hazard possible
  2. Use module-sync condition — both import and require resolve to the same ESM file
  3. Use a stateless API design where duplicate instantiation doesn't matter

Subpath Exports

For packages that expose multiple entry points:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "default": "./dist/utils.js"
    },
    "./package.json": "./package.json"
  }
}

Each subpath needs its own types + default pair. The types condition must be first in every subpath.

Common Mistakes

  1. Using main instead of exports: main is legacy. Use exports for all new packages. Only add main as a fallback for very old tooling.

  2. Forgetting "type": "module": Without this, .js files are treated as CJS by Node.js, even if they contain ESM syntax.

  3. Using .mjs/.cjs extensions when unnecessary: With "type": "module", .js is ESM. Only use .cjs for the rare CJS file in an ESM package. Avoid .mjs in new packages — it causes issues with some tooling.

  4. Path aliases in published code: tsc does not rewrite path aliases ("@lib/utils") in emitted JS or .d.ts files. Consumers can't resolve them. Use Node.js subpath imports ("#imports" in package.json) instead, or keep directory structures flat.

Source: SKILL.md on GitHub

1 warning14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    The skill provides a comprehensive toolkit for npm package development using the Bun toolchain, promoting modern security best practices like npm provenance and restricted file whitelisting. It includes templates and a local scaffolding script for project setup without any detected malicious patterns.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer7mo

    14/14 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 626387a. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago

README badge

README badge for jwynia/agent-skills/npm-package