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

nested-boundaries

v0.2.0

Published

Enforce the Recursive Deep Modules convention from paths alone: nested module boundaries for ESLint, oxlint, and Biome, plus cycle detection and structural checks.

Downloads

120

Readme

nested-boundaries

Enforce the Recursive Deep Modules convention from paths alone: modules nest fractally under modules/ folders, every module exposes only its root-level files as entry points, visibility follows the directory tree, tests are black-box, and nothing imports upward — at unbounded nesting depth, with zero per-module configuration. Works with ESLint, oxlint, or Biome.

The convention itself — vocabulary, rules, and fix guidance — lives in MODULE-BOUNDARIES.md, which ships inside the package. Every error message points at node_modules/nested-boundaries/MODULE-BOUNDARIES.md, so the doc is always the version you installed. Tip: reference that path from your CLAUDE.md / AGENTS.md so coding agents onboard to the rules too.

What you get

One package. Four things need enforcing, and no single tool can see all of them — wire the ones that match your toolchain:

| What gets enforced | How | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Entry-point boundaries, visibility, upward imports, black-box test imports | the nested-boundaries/imports rule (ESLint or oxlint), or the boundaries.grit plugin (Biome) — with editor feedback either way | | No file or Home-Module dependency cycles (type-only cycles exempt) | the nested-boundaries check-structure CLI, using dependency-cruiser's resolved project graph | | The filesystem grammar (misplaced modules//tests/ folders, stray test files, reserved names) | that same check-structure command — one project-wide check for every linter choice | | The humans and LLMs who hit the errors | MODULE-BOUNDARIES.md, shipped in the package; every diagnostic points at it |

All convention settings and policies are baked into the plugin — adoption is wiring, not configuration.

Setup

  1. Install (dependency-cruiser supplies the resolved graph used by the project-wide structure check):

    npm install -D nested-boundaries dependency-cruiser
  2. Wire your linter — whichever you run, or several:

    ESLint (eslint.config.mjs):

    import nestedBoundaries from "nested-boundaries";
    
    export default [
      {
        files: ["src/**/*.ts"],
        plugins: { "nested-boundaries": nestedBoundaries },
        rules: { "nested-boundaries/imports": "error" },
      },
      // ...the rest of your config
    ];

    oxlint (.oxlintrc.json):

    {
      "jsPlugins": [{ "name": "nested-boundaries", "specifier": "nested-boundaries" }],
      "rules": { "nested-boundaries/imports": "error" },
    }

    Biome (biome.json) — Biome references plugins by file path:

    {
      "plugins": [
        {
          "path": "./node_modules/nested-boundaries/boundaries.grit",
          "includes": ["**/src/**"],
        },
      ],
    }

    If that node_modules path doesn't exist in your layout (npm/yarn hoisted workspaces, Yarn PnP), commit a copy instead: npx nested-boundaries emit-biome writes boundaries.grit next to your config, and check-structure verifies the copy against the installed package on every run so it can never silently go stale. In a Biome monorepo, put the plugin in the package-level config ({ "root": false, "extends": "//" }) so it applies to that package only.

  3. Wire the project-wide structure check into your scripts/CI. It checks both the filesystem grammar and file/Home-Module cycles:

    // package.json
    "scripts": {
      "lint:structure": "nested-boundaries check-structure"  // defaults to src
    }

Then organize code under src/modules/… per the convention. That's the whole contract: boundaries come from where files sit.

Monorepos

Each workspace package is an independent enforcement universe: its lowercase src/ has its own Root Scope and Modules, and the tsconfig.json beside that src/ owns alias resolution. The ESLint/oxlint rule derives this package root from each linted file's last exact lowercase src path segment, so the same process can enforce many packages without treating the monorepo working directory as a shared source root. Earlier ancestor directories also named src are harmless; an illegal src nested inside a real source tree is still rejected by check-structure.

With centralized configuration, put the snippets above at the monorepo root and widen their file scopes to the workspace trees (for example, files: ["packages/*/src/**/*.ts"] in ESLint and includes: ["**/src/**"] in Biome). Both styles of invocation are supported:

# From the monorepo root
eslint packages && oxlint packages && biome lint packages

# From one workspace package; the tools discover the ancestor config
cd packages/catalog
eslint src && oxlint src && biome lint src

With package-local configuration, put the snippets in each configured package (keep ESLint's files: ["src/**/*.ts"]; for a nested Biome config use { "root": false, "extends": "//" }). You can still lint all configured packages from the monorepo root or run the same src commands inside one package. Run the project-wide check once per source tree, either inside the package with no argument or from the root with its path:

nested-boundaries check-structure packages/catalog/src

Import another workspace package by its bare package name. That import uses the package's declared public interface; nested-boundaries does not reinterpret the other workspace's internal Modules as relationships in the importing package.

Adopting in an existing codebase

You don't need a green field. With no modules/ folder yet, the lint rule has nothing to say — enforcement grows exactly as fast as you carve modules out, and the untouched remainder can consume each new module immediately. ADOPTION.md (shipped in the package) is the incremental migration playbook: triage, a bottom-up carving loop, and the anti-patterns that technically comply but defeat the point. It is written to be handed to an AI agent working on your behalf.

Assumptions and requirements

  • TypeScript codebase with a tsconfig.json beside each enforced src/ tree; path aliases are resolved through that source tree's own config automatically, regardless of where the linter command starts. A root alias is optional but recommended (imports read the same from any depth); the package does not create one — declare it in your tsconfig:

    // tsconfig.json
    "compilerOptions": {
      "paths": { "@/*": ["./src/*"] }
    }
  • Source root named exactly lowercase src/. Public source-root configurability is not part of this release; the internal inference seam keeps it additive later.

  • ESLint ≥ 10 (flat config), oxlint ≥ 1.73, and/or Biome ≥ 2.5. Note oxlint's JS-plugin support is alpha — behavior differences between the two ESLint-API linters are oxlint bugs. The Biome plugin is a separate GritQL implementation of the same policies (Biome cannot load ESLint plugins); this package's own violation suite runs every check under all three linters so they cannot drift. One Biome caveat: because GritQL cannot touch the filesystem, a directory import of an internals folder (@/modules/checkout/lib resolving to lib/index.ts) is misread as an entry point named lib — ESLint/oxlint catch that case.

  • Node ≥ 20. check-structure uses dependency-cruiser's JavaScript interface rather than its CLI; an even-numbered Node release remains the tested setup.

How it works (short version)

For every linted file, the rule infers an absolute workspace-package root from the last lowercase src segment and hands that root—and the adjacent tsconfig.json—to @boundaries/eslint-plugin. It then supplies two path-suffix element descriptors (modules/*, and the source root itself) plus a policy list over the computed relationship between importer and target: allow internal, child, sibling, and uncle targets — entry points only for the last three — forbid parent and ancestor, deny everything else, with black-box-test overrides keyed off tests/ paths. Suffix matching is what makes two descriptors cover unbounded depth; the structure CLI keeps the grammar unambiguous so path matching stays sound.

check-structure asks dependency-cruiser for the resolved local file graph, checks file cycles, then groups every file by its innermost Home Module and checks that collapsed graph too. This catches cycles distributed across unrelated files that a file-only cycle rule cannot see. Test-file dependencies do not contribute to the Home-Module graph, and cycles whose every edge is type-only remain legal.