TL;DR
pkg-types is the UnJS package for reading package.json — TypeScript types for package.json fields, finds the nearest package.json, resolves workspaces, and exports typed utilities. read-pkg reads and normalizes a single package.json — parses, validates, normalizes fields (like man, bin). read-package-up (formerly read-pkg-up) walks up directories to find the nearest package.json — useful for tools that need to find the project root. In 2026: pkg-types for typed package.json access, read-package-up for traversal, read-pkg for normalized parsing.
Key Takeaways
- pkg-types: ~10M weekly downloads — UnJS, TypeScript types for package.json, workspace resolution
- read-pkg: ~15M weekly downloads — reads + normalizes package.json, Sindre Sorhus
- read-package-up: ~15M weekly downloads — finds nearest package.json by walking up directories
- pkg-types provides full TypeScript types for every package.json field
- read-pkg normalizes package.json (e.g., converts
binstring to object format) - All three are commonly used by CLI tools, bundlers, and linters
pkg-types
pkg-types — typed package.json utilities:
Read package.json
import { readPackageJSON, findWorkspaceDir, resolvePackageJSON } from "pkg-types"
// Read the nearest package.json:
const pkg = await readPackageJSON()
// → { name: "my-app", version: "1.0.0", dependencies: {...}, ... }
// Read from a specific path:
const pkg2 = await readPackageJSON("/path/to/project")
// Find the nearest package.json path:
const pkgPath = await resolvePackageJSON()
// → "/Users/royce/project/package.json"
// Find workspace root:
const workspaceRoot = await findWorkspaceDir()
// → "/Users/royce/monorepo" (root with workspaces config)
TypeScript types
import type { PackageJson } from "pkg-types"
// Full TypeScript types for package.json fields:
const pkg: PackageJson = {
name: "pkgpulse",
version: "1.0.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" },
},
},
dependencies: { "react": "^19.0.0" },
devDependencies: { "typescript": "^5.5.0" },
peerDependencies: { "react": ">=18" },
scripts: { build: "tsup", test: "vitest" },
}
// Type-safe access:
pkg.name // string | undefined
pkg.version // string | undefined
pkg.exports // Exports | undefined (full conditional exports type)
pkg.type // "module" | "commonjs" | undefined
TSConfig utilities
import { readTSConfig, resolveTSConfig } from "pkg-types"
// Read tsconfig.json:
const tsconfig = await readTSConfig()
// → { compilerOptions: { target: "ES2022", ... }, include: [...] }
// Find the nearest tsconfig.json:
const tsconfigPath = await resolveTSConfig()
// → "/Users/royce/project/tsconfig.json"
read-pkg
read-pkg — read and normalize package.json:
Basic usage
import { readPackage } from "read-pkg"
// Read from current directory:
const pkg = await readPackage()
// → { name: "my-app", version: "1.0.0", ... }
// Read from specific directory:
const pkg2 = await readPackage({ cwd: "/path/to/project" })
// Normalize (converts shorthand fields to full format):
const pkg3 = await readPackage({ normalize: true })
Normalization
import { readPackage } from "read-pkg"
// Before normalization:
// package.json: { "bin": "./cli.js", "man": "./man/doc.1" }
const pkg = await readPackage({ normalize: true })
// After normalization:
// pkg.bin → { "my-app": "./cli.js" } (string → object)
// pkg.man → ["./man/doc.1"] (string → array)
// pkg.repository → { type: "git", url: "..." } (string → object)
Sync version
import { readPackageSync } from "read-pkg"
// Synchronous read:
const pkg = readPackageSync()
const name = pkg.name
const version = pkg.version
read-package-up
read-package-up — find nearest package.json:
Basic usage
import { readPackageUp } from "read-package-up"
// Walks up from cwd to find nearest package.json:
const result = await readPackageUp()
if (result) {
console.log(result.packageJson.name) // Package name
console.log(result.path) // Full path to package.json
// → "/Users/royce/project/package.json"
}
// From a specific directory:
const result2 = await readPackageUp({
cwd: "/Users/royce/project/src/utils/deep/nested",
})
// Walks up: nested → deep → utils → src → project (found!)
Use case: Finding project root
import { readPackageUp } from "read-package-up"
import path from "node:path"
async function findProjectRoot(startDir?: string) {
const result = await readPackageUp({ cwd: startDir })
if (!result) throw new Error("No package.json found")
return path.dirname(result.path)
}
// Usage in a CLI tool:
const projectRoot = await findProjectRoot()
console.log(`Project root: ${projectRoot}`)
Use case: Getting package info from anywhere
import { readPackageUp } from "read-package-up"
// In a deeply nested file, find the package it belongs to:
async function getPackageInfo() {
const result = await readPackageUp()
if (!result) return null
return {
name: result.packageJson.name,
version: result.packageJson.version,
root: path.dirname(result.path),
}
}
// Useful for:
// - CLI tools that need the project name
// - Bundlers that need package.json metadata
// - Linters that need to know project boundaries
Common Patterns
Version detection
import { readPackageJSON } from "pkg-types"
// Check if a dependency is installed and get its version:
async function getDependencyVersion(name: string): Promise<string | null> {
try {
const pkg = await readPackageJSON(`node_modules/${name}`)
return pkg.version ?? null
} catch {
return null
}
}
const reactVersion = await getDependencyVersion("react")
// → "19.0.0" or null
Workspace detection
import { readPackageJSON, findWorkspaceDir } from "pkg-types"
async function getWorkspacePackages() {
const rootDir = await findWorkspaceDir()
if (!rootDir) return []
const rootPkg = await readPackageJSON(rootDir)
const workspaces = rootPkg.workspaces
if (!workspaces) return []
// workspaces could be string[] or { packages: string[] }
const patterns = Array.isArray(workspaces)
? workspaces
: workspaces.packages ?? []
return patterns
}
Feature Comparison
| Feature | pkg-types | read-pkg | read-package-up |
|---|---|---|---|
| Read package.json | ✅ | ✅ | ✅ (with traversal) |
| Walk up directories | ✅ (resolvePackageJSON) | ❌ | ✅ |
| TypeScript types | ✅ (full) | ✅ (basic) | ✅ (via read-pkg) |
| Normalization | ❌ | ✅ | ✅ |
| TSConfig reading | ✅ | ❌ | ❌ |
| Workspace detection | ✅ | ❌ | ❌ |
| Sync API | ❌ | ✅ | ✅ |
| Dependencies | 0 | Few | read-pkg |
| UnJS ecosystem | ✅ | ❌ | ❌ |
| Weekly downloads | ~10M | ~15M | ~15M |
When to Use Each
Use pkg-types if:
- Need full TypeScript types for package.json fields
- Want workspace and tsconfig.json utilities
- In the UnJS ecosystem
- Building tools that need typed package metadata
Use read-pkg if:
- Need normalized package.json (shorthand fields expanded)
- Want a synchronous API option
- Building tools that parse and validate package.json
Use read-package-up if:
- Need to find the nearest package.json from any directory
- Building a CLI tool that runs from anywhere in the project
- Need both the parsed content and the file path
Real-World Usage: Who Uses What in 2026
All three packages appear as transitive dependencies in the Node.js ecosystem, but for different reasons:
pkg-types is a dependency of unbuild, Nitro, and the wider UnJS ecosystem. When you install Nuxt 3, you get pkg-types via the Nuxt build pipeline. Tools that need to read package.json in a cross-platform, type-safe way — particularly Nuxt's module system and Nitro's server config — use pkg-types because it provides TypeScript types for every field without requiring @types/ packages or manual type declarations.
read-pkg is pulled in by ESLint, Prettier, and other Sindre Sorhus packages. Sindre's ecosystem consistently uses read-pkg because it normalizes edge cases (like the bin field being a string vs an object) that matter for CLI tools published to npm. If you use ESLint or prettier in a project, read-pkg is almost certainly in your node_modules.
read-package-up is a dependency of tools that need to "find the project root" — linters, code generators, and migration scripts. ESLint uses it to find which package's configuration applies. Prettier uses it to determine the project root when searching for configuration files. It is the standard choice for any tool that runs from within a project directory and needs to locate the enclosing package.
Bundle Size and Performance
| Package | Min+gzip | Find + read time | Sync API |
|---|---|---|---|
| pkg-types | ~3 KB | ~2-5ms | ❌ |
| read-pkg | ~4 KB | ~3-6ms | ✅ |
| read-package-up | ~5 KB | ~5-15ms (traversal) | ✅ |
read-package-up is slower because it traverses the directory tree — starting from the current working directory and walking up until it finds a package.json. For deeply nested projects, this means multiple filesystem stat calls. In practice this is not a bottleneck (milliseconds), but for tools that call read-package-up in a hot loop (like linting every file), caching the result is worth doing.
Migration Guide
From read-pkg to pkg-types:
// Before:
import { readPackage } from "read-pkg"
const pkg = await readPackage({ cwd: "/path/to/project" })
console.log(pkg.name, pkg.version)
// After:
import { readPackageJSON } from "pkg-types"
const pkg = await readPackageJSON("/path/to/project")
console.log(pkg.name, pkg.version)
// Key differences:
// 1. read-pkg normalizes fields (bin: string → object), pkg-types does not
// 2. pkg-types returns raw package.json structure with full TypeScript types
// 3. pkg-types has no sync equivalent — everything is async
From manual JSON.parse to any of these:
// Before (common pattern in many older tools):
import { readFileSync } from "node:fs"
import { join } from "node:path"
const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8"))
// After with pkg-types (typed, finds nearest automatically):
import { readPackageJSON } from "pkg-types"
const pkg = await readPackageJSON()
// pkg.name is string | undefined — TypeScript knows the type
// After with read-package-up (traversal, includes path):
import { readPackageUp } from "read-package-up"
const result = await readPackageUp()
if (result) {
const { packageJson, path } = result
// Know both the content and where it came from
}
Decision Guide
Choosing between these three depends on what your tool needs to do with package.json:
| Need | Best Choice |
|---|---|
| Read current project's package.json with full TypeScript types | pkg-types |
| Read any package.json and normalize shorthand fields | read-pkg |
| Find the nearest package.json by walking up directories | read-package-up |
| Read tsconfig.json or find workspace root | pkg-types |
| Need a synchronous API | read-pkg or read-package-up |
| In the UnJS ecosystem | pkg-types |
| Building a CLI that runs from any project directory | read-package-up |
| Get both the parsed content and the file path | read-package-up |
In practice, most build tools and CLIs want read-package-up — the combination of directory traversal plus the file path (which gives you the project root) covers the broadest set of tool-building scenarios. pkg-types is the better choice when you want rich TypeScript types and workspace utilities without the traversal overhead.
Security Considerations When Reading package.json
Reading package.json files from arbitrary paths introduces a class of security concern that is easy to overlook in tool development: path traversal. A CLI tool that reads package.json based on user-supplied paths should validate that the resolved path stays within the project boundaries before passing it to any of these libraries. read-package-up's traversal mechanism stops at the filesystem root, so it will never traverse outside the repository, but readPackageJSON(userSuppliedPath) from pkg-types does not apply any boundary checking — a malicious --cwd argument of ../../../../etc/ could cause unintended behavior if the calling code doesn't validate the path first.
The scripts field in package.json is a particular concern for tools that read and display or execute package metadata. A compromised or malicious package.json could contain scripts with shell injection payloads that become dangerous if your tool executes them. pkg-types, read-pkg, and read-package-up all return the raw scripts object without sanitization — tools that display script content to users should treat it as untrusted user-provided strings and escape output appropriately. In CI environments where these tools process package.json files from pull request branches (which may contain attacker-controlled content), the tool's behavior with adversarial scripts fields is worth auditing.
Ecosystem Compatibility and ESM Migration
All three packages are ESM-only in their current major versions (pkg-types v1.x, read-pkg v9.x, read-package-up v11.x), following Sindre Sorhus's ESM-first migration across his package ecosystem. This means they cannot be require()'d in CommonJS modules without dynamic import(). For tools that must support both ESM and CJS consumers, the typical approach is to use dynamic import() with a top-level await in an async initialization function, or to use the jiti package to enable ESM imports in CJS contexts without a build step.
The UnJS ecosystem (which pkg-types is part of) takes an ESM-first approach but publishes CJS builds as well through unbuild's dual-format output. pkg-types specifically ships both ESM and CJS builds, making it compatible with CommonJS tools that use dynamic import(). For CLI tools that need to work immediately without a build step, jiti combined with pkg-types is a common pattern — jiti handles the ESM-CJS boundary at runtime while pkg-types provides the typed package.json access. Teams building tools that must run in both contexts should test their require() and import() codepaths explicitly, as dual-format packages occasionally have subtle behavioral differences.
Handling Non-Standard package.json Extensions
Many package.json files in the wild contain non-standard extensions — fields like ava, jest, prettier, eslint configuration embedded directly in package.json rather than in separate config files. pkg-types's PackageJson type includes an index signature that permits arbitrary additional fields with type unknown, so accessing these non-standard fields requires type assertions. read-pkg normalizes the standard fields but passes through non-standard fields unchanged. The practical implication for tool authors is that reading configuration from package.json — common for lint rules, test configuration, or bundler settings — works transparently with all three libraries, but the TypeScript type for the non-standard fields requires either a type assertion or a custom type that extends PackageJson with the additional fields your tool reads. This is a minor ergonomic consideration but worth planning for when building tools that read tool-specific configuration from package.json.
Performance and Caching in Monorepos
In large monorepos with dozens of packages, reading package.json files frequently can become a noticeable overhead — especially in linters and bundlers that process every file. All three packages perform synchronous or async filesystem reads, which means repeated calls in hot paths benefit significantly from caching.
import { readPackageJSON } from "pkg-types"
// Cache the result to avoid repeated disk reads:
const packageCache = new Map<string, Awaited<ReturnType<typeof readPackageJSON>>>()
async function getCachedPackage(dir: string) {
if (!packageCache.has(dir)) {
packageCache.set(dir, await readPackageJSON(dir))
}
return packageCache.get(dir)!
}
For read-package-up, the traversal cost compounds in deep directory trees. In a monorepo with packages/ui/src/components/Button/ as the working directory, read-package-up must stat 5+ directories before finding packages/ui/package.json. Tools like ESLint cache this result per-file rather than re-traversing on every lint run.
// ESLint-style caching for read-package-up:
import { readPackageUp } from "read-package-up"
import path from "node:path"
const rootCache = new Map<string, string>()
async function findPackageRoot(cwd: string): Promise<string> {
// Cache by directory, not by file path
const dir = path.dirname(cwd)
if (!rootCache.has(dir)) {
const result = await readPackageUp({ cwd: dir })
if (!result) throw new Error(`No package.json found from ${dir}`)
rootCache.set(dir, path.dirname(result.path))
}
return rootCache.get(dir)!
}
pkg-types' findWorkspaceDir() is specifically optimized for monorepos — it stops traversal when it finds a workspace root (a package.json with a workspaces field or a pnpm-workspace.yaml), making it faster than read-package-up in workspace-aware tooling.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on pkg-types v1.x, read-pkg v9.x, and read-package-up v11.x.
Compare package utilities and developer tooling on PkgPulse →
See also: patch-package vs pnpm patch vs yarn patch and taze vs npm-check-updates vs npm-check, archiver vs adm-zip vs JSZip (2026).