npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@effected/package-json

v0.5.2

Published

package.json parsing, editing, validation and file IO as Effect schemas.

Readme

@effected/package-json

npm License: MIT Node.js %3E%3D24.11.0 TypeScript 7.0

package.json parsing, editing, validation and file IO as Effect schemas. Package is a Schema.Class with the manifest's known fields typed — name is a branded npm name, version is a real SemVer, packageManager decodes into { name, version, integrity } — and a rest catch-all that carries every unknown top-level key through a read, edit and write cycle without losing it. Editing is immutable and dual-signature, validation is a rule set you can replace, and catalog: / workspace: specifiers expand through the @effected/npm resolver contracts as an explicit step you opt into.

Pre-release. This package is part of the @effected/* kit, in pre-1.0.0 development against a single pinned Effect v4 beta. Packages graduate to 1.0.0 once Effect 4.0.0 ships. To hold your own effect versions at exactly the ones the kit is built and tested against, install @effected/pnpm-plugin-effect.

Stability: unstable. This package's API surface is not yet considered complete and may change across 0.x releases. Pin an exact version — even a package marked stable before 1.0.0 can introduce a breaking change by accident, and an exact pin turns that into a type-check error rather than a runtime surprise. Full policy: release strategy.

Why @effected/package-json

Tools that rewrite a package.json usually treat it as a Record<string, unknown>: read it, mutate a key, JSON.stringify it back. That works right up until it does not. Unknown keys survive by accident rather than by design, version is a string you compare with <, and the day someone's manifest has a field your types never modeled is the day you find out whether your write path preserved it. The alternative — a strict schema over the known fields — usually solves the typing by deleting everyone's data.

This package refuses both. Known fields are typed and validated; everything else lands in rest and is flattened back to top-level keys on encode, so the on-disk shape never grows a literal rest key and never loses your customTool block. Serialization applies the canonical sort-package-json key order, alphabetizes dependency maps and strips empty ones, deterministically and locale-independently. PackageJsonFile.write does not silently resolve your workspace: specifiers on the way out, because a write that quietly rewrites your dependency values is not a write, it is a policy — so Package.resolve is a step you compose in deliberately. And every distinct failure has its own tag: a missing file, an unreadable file, invalid JSON and a document that does not satisfy the schema are four different problems with four different recoveries.

Install

npm install @effected/package-json effect @effect/platform-node
pnpm add @effected/package-json effect @effect/platform-node

Requires Node.js >=24.11.0.

All @effected/* packages are ESM-only: the exports maps publish only import conditions, so require() — including tools that resolve in CJS mode — fails with Node's ERR_PACKAGE_PATH_NOT_EXPORTED rather than loading a CJS build that does not exist. Import from an ES module.

effect v4 is the only peer dependency. @effected/semver and @effected/npm come along as ordinary dependencies — they back the version field and the resolver contracts — and spdx-expression-parse is the one external package in the tree, used to validate SPDX license expressions.

Reading and writing files needs a FileSystem and a Path implementation, provided once at the edge, from @effect/platform-node on Node. Everything except PackageJsonFile is pure and needs no platform layer at all.

Quick start

Decode a manifest, edit it, read the computed properties back:

import { Package } from "@effected/package-json";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const pkg = yield* Package.decode({ name: "@acme/widget", version: "1.0.0", private: true });
  const next = yield* Package.setVersion(pkg, "1.1.0");
  return [next.name, next.version.toString(), next.isScoped, next.isPrivate] as const;
});

console.log(Effect.runSync(program));
// => ["@acme/widget", "1.1.0", true, true]

next.version is a SemVer, not a string, so you compare it with SemVer.gt and bump it with version.bump.minor() rather than reaching for a regex.

The Package model

Editing returns a new Package. The mutation statics are dual, so Package.addDependency(pkg, "effect", "^4.0.0") and pkg.pipe(Package.addDependency("effect", "^4.0.0")) are the same call. The ones that can fail — setVersion, setName, setLicense — return an Effect with the corresponding typed error, and the rest are plain functions.

Unknown keys round-trip. toJsonString encodes through the wire codec, flattens rest back to the top level, and applies the canonical key order:

import { Package } from "@effected/package-json";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const pkg = yield* Package.decode({
    name: "widget",
    version: "1.0.0",
    scripts: { build: "tsc" },
    customTool: { flag: true },
  });
  return Package.addDevDependency(pkg, "typescript", "^6.0.0").toJsonString();
});

console.log(Effect.runSync(program));
// {
//   "name": "widget",
//   "version": "1.0.0",
//   "scripts": {
//     "build": "tsc"
//   },
//   "devDependencies": {
//     "typescript": "^6.0.0"
//   },
//   "customTool": {
//     "flag": true
//   }
// }

toJsonString takes PackageFormatOptionsindent, sort, stripEmpty, newline — if you want the raw shape instead of the canonical one.

Alongside Package the leaf concepts are usable on their own. PackageName classifies and validates npm names (isValid, isScoped, scope, unscoped) and brands them as ScopedPackageName or UnscopedPackageName. DependencySpecifier classifies any specifier string into one protocol — range, tag, git, url, npm, file, link, portal, catalog, workspace or unknown — and parses the range case into a Range from @effected/semver. Dependency pairs a name with a specifier and the kind of map it came from, exposing the same protocol predicates as getters.

Reading and writing

PackageJsonFile is the only IO in the package: one service, two methods, over core FileSystem and Path.

import { Package, PackageJsonFile } from "@effected/package-json";
import { NodeFileSystem, NodePath } from "@effect/platform-node";
import { Effect, Layer } from "effect";

const bumpMinor = Effect.gen(function* () {
  const files = yield* PackageJsonFile;
  const pkg = yield* files.read("./package.json");
  const next = pkg.copyWith({ version: pkg.version.bump.minor() });
  yield* files.write("./package.json", next);
});

const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);

Effect.runPromise(bumpMinor.pipe(Effect.provide(PackageJsonFile.layer), Effect.provide(PlatformLive)));

read fails four different ways and says which: PackageJsonNotFoundError when the file is not there, PackageJsonReadError for any other filesystem failure, PackageJsonParseError when the bytes are not JSON, and PackageDecodeError when the JSON is not a package.json. There is no exists pre-check, so a file deleted between the check and the read cannot be misreported as an IO error.

Validation

PackageValidator runs a rule set over a decoded Package and aggregates every failure into one PackageValidationError, rather than stopping at the first.

import { Package, PackageValidator } from "@effected/package-json";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const pkg = yield* Package.decode({ name: "widget", version: "1.0.0" });
  const validator = yield* PackageValidator;
  return yield* validator.validate(pkg);
}).pipe(
  Effect.provide(PackageValidator.layer),
  Effect.catchTag("PackageValidationError", (error) => Effect.succeed(error.failures.map((failure) => failure.rule))),
);

console.log(Effect.runSync(program));
// => ["has-license", "has-description", "has-repository"]

PackageValidator.layer carries the default rules (has-license, has-description, has-repository, not-private). PackageValidator.layerRules({ rules }) takes your own set instead — a ValidationRule is a name plus a check that fails with a RuleFailure. Two extra rules ship for publish gates: noUnresolvedDepsRule fails on any workspace: or catalog: specifier still in the manifest, and noLocalDepsRule fails on file:, link: and portal:.

Resolving catalog: and workspace: specifiers

Package.resolve expands catalog: and workspace: specifiers across all four dependency maps, using the CatalogResolver and WorkspaceResolver contracts from @effected/npm. This package deliberately cannot implement them — it has no view of the workspace — so the implementation arrives from context. @effected/workspaces provides the real ones; a fixed record does fine for a test.

Specifiers the resolvers answer None for are left exactly as they were.

import { CatalogResolver, WorkspaceResolver } from "@effected/npm";
import { Package } from "@effected/package-json";
import { Effect, HashMap, Layer, Option } from "effect";

const Resolvers = Layer.mergeAll(
  Layer.succeed(CatalogResolver, { rangeOf: () => Effect.succeed(Option.some("^4.0.0")) }),
  Layer.succeed(WorkspaceResolver, { versionOf: () => Effect.succeed(Option.some("1.4.0")) }),
);

const program = Effect.gen(function* () {
  const pkg = yield* Package.decode({
    name: "widget",
    version: "1.0.0",
    dependencies: { effect: "catalog:", "@acme/core": "workspace:^" },
  });
  const resolved = yield* Package.resolve(pkg);
  return [
    Option.getOrElse(HashMap.get(resolved.dependencies, "effect"), () => "unresolved"),
    Option.getOrElse(HashMap.get(resolved.dependencies, "@acme/core"), () => "unresolved"),
  ] as const;
}).pipe(Effect.provide(Resolvers));

console.log(Effect.runSync(program));
// => ["^4.0.0", "^1.4.0"]

The workspace: range modifier is honored: workspace:* takes the bare version, workspace:^ and workspace:~ prefix it, and an explicit modifier is used as-is. The projection is @effected/npm's DependencySpecifier statics with full pnpm publish semantics: the alias form workspace:<name>@<range> resolves the target package's version and becomes the npm:<name>@<range> alias pnpm publishes, and a blank catalog name selects the default catalog. A failed catalog assembly surfaces typed as @effected/npm's CatalogAssemblyError, alongside the contracts' DependencyResolutionError.

Errors

Every failure is a Schema.TaggedErrorClass routed with Effect.catchTag. Causes are preserved structurally on a Schema.Defect field — a PackageDecodeError hands you the SchemaError issue tree, not String(error).

| Tag | Means | | --- | ----- | | PackageJsonNotFoundError | No file at the path. Often not an error at all: fall back, or walk up. | | PackageJsonReadError | The file is there and could not be read. Carries path and the structural cause. | | PackageJsonParseError | The bytes are not JSON. Carries path and the SyntaxError. | | PackageDecodeError | The JSON is not a package.json. Carries the SchemaError cause with its issue tree. | | PackageJsonWriteError | The write failed. Narrowed to the filesystem failure only, never an encode error. | | PackageValidationError | One or more validation rules failed. Carries every failure, each with its rule name, message and JSON path. | | InvalidPackageNameError | A string does not satisfy npm's naming rules. Raised by Package.setName. | | InvalidSpdxLicenseError | A string is not a valid SPDX license expression. Raised by Package.setLicense. | | InvalidDependencySpecifierError | A string is not a recognized dependency specifier. Raised by DependencySpecifier.decode. |

Package.setVersion fails with InvalidVersionError from @effected/semver, which is where the version grammar lives.

Features

  • Package — the manifest model: typed known fields, the rest catch-all, computed getters (isPrivate, isScoped, isESM, hasDependency, the four get*Dependencies), dual mutation statics, copyWith, Package.decode and the pure toJsonString serializer.
  • Package.schema / Package.wireFor — the open-JSON ↔ class wire codec, and the factory that builds one for a .extend()ed subclass so its custom fields decode as typed members instead of falling into rest.
  • PackageJsonFile — the IO surface: read and write over core FileSystem / Path, with the platform implementation supplied at the edge.
  • PackageValidator — rule-based validation aggregating every failure, with the default rule set, a parameterized layerRules factory, and the publish-gate rules noUnresolvedDepsRule and noLocalDepsRule.
  • Package.resolvecatalog: and workspace: expansion over the @effected/npm contracts with pnpm's publish-time projection (alias form included), as an explicit step that write never performs for you.
  • PackageName, DependencySpecifier, Dependency, SpdxLicense, PackageManager, Person, DevEngine — the leaf concepts, each owning its own statics, brand and error, usable independently of Package.
  • Field schemas (DependencyMapField, BinField, ExportsField, RepositoryField, PublishConfigField, PeerDependenciesMetaField, StringMapField) exported for subclasses that extend the model.

License

MIT