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

eslint-plugin-code-policy

v0.2.2

Published

Architectural linting for TypeScript · enforce atomic files, explicit public APIs, clean runtime boundaries, and view/logic separation.

Readme

npm version License: MIT ESLint Flat Config TypeScript


Installation

Requirements

  • ESLint >=9.0.0 (flat config, including v10)
  • TypeScript ^5.4.0
  • Node.js >=20.19
# npm
npm install --save-dev eslint-plugin-code-policy

# pnpm
pnpm add --save-dev eslint-plugin-code-policy

# yarn
yarn add --dev eslint-plugin-code-policy

Usage

Flat config (eslint.config.mjs)

import codePolicy from 'eslint-plugin-code-policy'

export default [codePolicy.configs.recommended]

Choosing a preset

| Preset | Import path | Best for | | ------------- | -------------------------------- | ---------------------- | | recommended | codePolicy.configs.recommended | Any TypeScript project | | strict | codePolicy.configs.strict | Maximum enforcement | | react | codePolicy.configs.react | React (Vite, CRA, …) | | next | codePolicy.configs.next | Next.js App Router |

// Next.js example
import codePolicy from 'eslint-plugin-code-policy'

export default [codePolicy.configs.next]

Manual rule configuration

If you prefer to cherry-pick rules:

import codePolicy from 'eslint-plugin-code-policy'

export default [
  {
    plugins: { 'code-policy': codePolicy },
    rules: {
      'code-policy/atomic-file': 'error',
      'code-policy/no-inline-types': 'error',
      'code-policy/public-api-imports': 'error',
      'code-policy/no-cross-module-deep-imports': 'error',
      'code-policy/view-logic-separation': 'error',
    },
  },
]

Rules

code-policy/atomic-file

Enforce exactly one top-level declaration per file.

Each file must export exactly one top-level unit — a function, class, constant, or type. This is the foundation of hyper-modular architecture: if a file has two things, one of them belongs in a new file.

❌ Incorrect

// ❌ src/UserUtils.ts — two exports in one file
export function formatUserName(user: User) {
  return `${user.firstName} ${user.lastName}`
}

export function getUserAge(user: User) {
  return new Date().getFullYear() - user.birthYear
}

✅ Correct

// ✅ src/formatUserName.ts
export function formatUserName(user: User) {
  return `${user.firstName} ${user.lastName}`
}
// ✅ src/getUserAge.ts
export function getUserAge(user: User) {
  return new Date().getFullYear() - user.birthYear
}

Exemptions (automatically skipped)

  • *.config.ts / *.config.js / *.config.mjs
  • index.ts / index.tsx / index.js (barrel files)
  • *.d.ts (ambient declaration files)
  • Next.js special files: page.tsx, layout.tsx, route.ts, etc. — reserved exports like GET, POST, metadata are not counted.

code-policy/no-inline-types

Enforce that type aliases and interfaces live in their own files.

Type declarations that appear alongside implementation code create hidden coupling and violate the single responsibility principle. Every type or interface must be in a dedicated file, ideally inside a types/ directory.

❌ Incorrect

// ❌ Mixed type + implementation in one file
type UserRole = 'admin' | 'member' | 'guest'

export function canEdit(role: UserRole) {
  return role === 'admin'
}

✅ Correct

// ✅ src/types/UserRole.ts
export type UserRole = 'admin' | 'member' | 'guest'
// ✅ src/canEdit.ts
import type { UserRole } from '@/types/UserRole'

export function canEdit(role: UserRole) {
  return role === 'admin'
}

Exemptions

  • Files inside types/ or types/** directories
  • *.d.ts files
  • "Pure type files" — files whose entire body consists only of import + type/interface declarations

code-policy/public-api-imports

Prevent importing directly from internal module subpaths.

When consuming a package or module, you must import from its public API (the root / index), not from a deep internal path. Deep imports couple you to internal implementation details.

❌ Incorrect

// ❌ Bypassing the public API
import { Button } from '@myorg/ui/src/components/Button'
import { formatDate } from '@myorg/utils/src/date/formatDate'

✅ Correct

// ✅ Always import through the public surface
import { Button } from '@myorg/ui'
import { formatDate } from '@myorg/utils'

Options

{
  'code-policy/public-api-imports': ['error', {
    bannedSubpaths: ['/src/'] // default
  }]
}

| Option | Type | Default | Description | | ---------------- | ---------- | ----------- | ------------------------------------------- | | bannedSubpaths | string[] | ['/src/'] | Segments that signal a deep internal import |


code-policy/no-cross-module-deep-imports

Prevent relative imports that bypass another module's public API within a monorepo.

In a monorepo, relative paths like ../../core/src/utils/helper skip the core module's public API entirely. This rule detects that pattern by counting ../ traversal depth and checking for internal directory names in the descent.

❌ Incorrect

// ❌ packages/ui/src/Button.tsx
import { helper } from '../../core/src/utils/helper'

✅ Correct

// ✅ Import through the published public API
import { helper } from '@myorg/core'

Options

{
  'code-policy/no-cross-module-deep-imports': ['error', {
    minParentTraversals: 2,   // how many `../` levels before checking
    internalDirs: ['src']     // dirs that signal internal code
  }]
}

| Option | Type | Default | Description | | --------------------- | ---------- | --------- | ------------------------------------------------ | | minParentTraversals | number | 2 | Minimum ../ segments before the rule activates | | internalDirs | string[] | ['src'] | Directory names that indicate internal code |


code-policy/view-logic-separation

Prevent state, effects, and inline handlers inside React view components.

React view components (.tsx files) are responsible for rendering only. State management, side effects, and event handler logic must live in a dedicated custom hook. This enforces a clean view/controller split.

❌ Incorrect

// ❌ src/UserCard.tsx — logic inside a view
export function UserCard({ userId }: UserCardProps) {
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    fetchUser(userId).then(setUser)
  }, [userId])

  const handleDelete = () => {
    deleteUser(userId)
  }

  return <div onClick={handleDelete}>{user?.name}</div>
}

✅ Correct

// ✅ src/useUserCard.ts
export function useUserCard(userId: string) {
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    fetchUser(userId).then(setUser)
  }, [userId])

  const handleDelete = () => deleteUser(userId)

  return { user, handleDelete }
}
// ✅ src/UserCard.tsx — pure view
import { useUserCard } from './useUserCard'

export function UserCard({ userId }: UserCardProps) {
  const { user, handleDelete } = useUserCard(userId)
  return <div onClick={handleDelete}>{user?.name}</div>
}

What triggers this rule (inside .tsx files)

  • Calling React hooks: useState, useEffect, useReducer, useCallback, useMemo, useRef, and more
  • Declaring inline functions/handlers directly inside a view component body

Shareable Configs Reference

recommended

Enables all five rules as errors. Best starting point for any TypeScript project.

// Rules enabled:
'code-policy/atomic-file': 'error'
'code-policy/no-inline-types': 'error'
'code-policy/view-logic-separation': 'error'
'code-policy/public-api-imports': 'error'
'code-policy/no-cross-module-deep-imports': 'error'

strict

Extends recommended. Intended for projects that want zero tolerance for architectural deviation. Reserved for additional strictness overrides in future versions.

react

Extends recommended with React-specific adjustments.

next

Extends recommended. Correctly handles Next.js App Router special files (page.tsx, layout.tsx, route.ts, etc.) and reserved exports (metadata, GET, POST, …), preventing false positives.


Migrating from Legacy Config

This plugin only supports the ESLint flat config format (ESLint v9+). If you're still on the legacy .eslintrc format, migrate using the official ESLint migration guide before installing this plugin.


FAQ

Q: Why do I get errors on my index.ts barrel files?

index.ts files are automatically exempted from the atomic-file rule because barrel files by design re-export multiple things.

Q: How do I exempt a specific file from a rule?

Use ESLint's standard inline disable comment:

// eslint-disable-next-line code-policy/atomic-file

Or add file overrides in your eslint.config.mjs:

{
  files: ['src/legacy/**'],
  rules: {
    'code-policy/atomic-file': 'off',
  },
}

Q: Does this work with JavaScript (non-TypeScript) projects?

The rules are language-agnostic at the ESLint AST level. TypeScript-specific nodes are handled gracefully. You can use the plugin on .js files, though some rules (like no-inline-types) are most meaningful in TypeScript codebases.


License

MIT