iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

package.json

package.json is your project’s manifest: name, version, scripts, dependencies, entry points, engine requirements. Knowing the fields well prevents wasted hours debugging exports, peer dependencies, and lockfile drift.

Scripts, deps, exports, engines, types

EXAMPLE
{
    "name": "my-app",
    "version": "1.2.3",
    "description": "My amazing app",
    "type": "module",
    "private": true,
    "engines": { "node": ">=20" },

    "main":   "dist/index.js",
    "types":  "dist/index.d.ts",
    "exports": {
        ".":         { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
        "./utils":   { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" },
        "./package.json": "./package.json"
    },
    "files": ["dist", "README.md", "LICENSE"],

    "scripts": {
        "dev":     "vite",
        "build":   "tsc && vite build",
        "start":   "node dist/server.js",
        "test":    "vitest",
        "lint":    "eslint . --ext .ts,.tsx",
        "format":  "prettier --write \".\"",
        "prepare": "husky install"
    },

    "dependencies":     { "express": "^4.21.0", "zod": "^3.23.0" },
    "devDependencies":  { "vitest": "^2.0.0", "typescript": "^5.5.0" },
    "peerDependencies": { "react": ">=18.0.0" },
    "optionalDependencies": { "fsevents": "^2.3.0" },

    "keywords": ["web", "api"],
    "author":   "Mara <mara@example.com>",
    "license":  "MIT",
    "repository": { "type": "git", "url": "https://github.com/me/my-app.git" },
    "bugs":     { "url": "https://github.com/me/my-app/issues" },
    "homepage": "https://github.com/me/my-app#readme"
}

// 1) name + version — semver matters
// • Scoped packages start with @ (e.g. '@my-org/utils')
// • semver: MAJOR.MINOR.PATCH; pre-release tags: 1.2.3-alpha.1

// 2) type field
// • 'module' — .js files are ESM
// • 'commonjs' (or omitted) — .js files are CJS
// • .mjs always ESM; .cjs always CJS regardless of type

// 3) main / module / browser / types
// • main — legacy entry; what require() picks up
// • module — deprecated bundler hint; replaced by exports
// • browser — bundler picks for browser builds
// • types — TypeScript types entry (or per-condition types in exports)

// 4) exports — modern entrypoint declaration (preferred)
// • Restricts what consumers can import (subpath encapsulation)
// • Conditions: import, require, node, browser, types, default
// • Anything NOT listed is unreachable — deliberate API surface

// 5) files — what's published to the registry
// • Defaults include README, LICENSE, package.json + main + everything tracked by npm
// • Use 'files' allowlist; or .npmignore (rare); 'files' is recommended

// 6) scripts
// • npm run <name> runs the script in the package directory
// • npm test / npm start / npm restart / npm stop are special (no 'run')
// • Lifecycle hooks: prepare, prepublish, preversion, postinstall (avoid for security)
// • Cross-platform: use cross-env, rimraf for portable scripts

// 7) dependencies tiers
// • dependencies      — runtime requirements (ship in install)
// • devDependencies   — build / test / tooling (NOT installed for consumers)
// • peerDependencies  — your package expects the host to provide them (libraries)
// • optionalDependencies — install if possible; failures are ignored
// • bundledDependencies — copy into the published tarball (rare)

// 8) workspaces (monorepo)
{
    "workspaces": ["packages/*", "apps/*"],
    "scripts":    { "build": "npm run build --workspaces --if-present" }
}
// Sub-packages share node_modules; one install across the whole repo.

// 9) engines + engineStrict (deprecated)
{
    "engines": { "node": ">=20", "npm": ">=10" }
}
// npm warns when the host doesn't satisfy engines; pnpm and Yarn refuse.
// Use 'volta' or '.nvmrc' to PIN node version per project for the team.

// 10) sideEffects — tree-shaking hint
{
    "sideEffects": false
}
// or list specific files: ["./src/polyfill.js", "*.css"]
// Bundlers drop unused exports more aggressively.

// 11) bin — CLI scripts
{
    "bin": { "my-cli": "./bin/my-cli.js" }
}
// After install, my-cli is on PATH. Make the file executable: chmod +x bin/my-cli.js
// Add a shebang: #!/usr/bin/env node

// 12) Build the manifest in CI — npm view / npm publish --dry-run
npm publish --dry-run                    // see what would be published
npm pack --dry-run                       // same; lists contents
npm publish --provenance                 // sigstore-signed (recommended)

// 13) Migration tips
// • Switching to 'type: module' may break some CJS-only dependencies
// • Adding 'exports' is BREAKING — consumers using internal paths get errors
// • Renaming 'main' to point at a new location — bump major version
// • Removing a published version is destructive — use 'deprecate' instead

// 14) Common bugs
// • Setting type: module without converting require → ERR_REQUIRE_ESM
// • exports map missing './package.json' — tooling that reads it breaks
// • files allowlist misses dist/ — npm publish ships nothing
// • peerDependencies in devDependencies → consumers don't get the hint
// • Pinning every dep at exact (no ^/~) → security patches require manual bumps
// • Forgetting to commit package-lock.json — every install resolves a fresh tree
// • 'prepare' script running heavy build at install time — slow for downstream users

Why it matters

Treat package.json as your project’s public contract. Use exports to lock down your API surface, allowlist published files, pin Node via engines + .nvmrc, and split deps correctly — runtime in dependencies, tooling in devDependencies, host requirements in peerDependencies.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
{
    "name": "my-app",
    "type": "module",
    "scripts": { "start": "node src/index.js" }
}
Try it Yourself »

Discussion

Loading…