eslint-plugin-code-policy
v0.7.4
Published
Architectural linting for TypeScript · enforce atomic files, explicit public APIs, clean runtime boundaries, and view/logic separation.
Maintainers
Readme
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-policyFull stack baseline (recommended for VibraComet projects)
This plugin ships rules and small presets only. For the full shared ESLint
baseline (TypeScript ESLint, imports, security, framework layers, and optional
code-policy wiring), use the published config package:
@vibracomet/eslint-config— published from theengineering-baselinerepository (packages/eslint-config,PUBLIC_API.md, anddocs/adoption/for migration steps).
Install it alongside this plugin when you want parity with the monorepo templates.
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/one-primary-unit': 'error',
'code-policy/no-hidden-top-level-declarations': 'error',
'code-policy/no-inline-types-in-runtime-files': 'error',
'code-policy/file-kind-placement': 'error',
'code-policy/public-api-imports': 'error',
'code-policy/no-cross-module-deep-imports': 'error',
'code-policy/view-logic-separation': 'error',
},
},
]Rules
code-policy/one-primary-unit
Enforce exactly one top-level exported declaration per file.
Each file must export exactly one top-level unit — a function, class, constant, or type. This focuses the identity of every module.
Exemptions (automatically skipped)
*.config.ts/*.config.js/*.config.mjsindex.ts/index.tsx/index.js(barrel files)- Next.js special files:
page.tsx,layout.tsx,route.ts, etc. — reserved exports likeGET,POST,metadataare not counted.
code-policy/no-hidden-top-level-declarations
Forbid internal module-scoped logic and helpers.
Files cannot contain private/unexported top-level declarations that hide complexity. If a helper is needed, it should be extracted to its own file and imported explicitly.
code-policy/no-inline-types-in-runtime-files
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, isolating runtime from type
declarations.
Exemptions
- Files inside
types/ortypes/**directories *.d.tsfiles- "Pure type files" — files whose entire body consists only of
import+type/interfacedeclarations
code-policy/file-kind-placement
Ensure files strictly follow kind-based naming and folder placement.
Ensures types are placed in types/, contexts in contexts/, and hooks start
with use. Strict architectural adherence is enforced at a structural level.
Options
allowGenericFolders(defaultfalse): permitutils/andhelpers/at any depth as homes for pure helpers; files under such a folder are exempt from placement checks.allowColocation(defaultfalse): exempt a placement-checked unit (hook, mapper, formatter, validator, selector) when it is colocated with its consumer instead of orphaned in a purely technical folder. A folder counts as a colocation home when it holds an anchor next to the unit: a public-API barrel (index.*), a component (PascalCase.tsx/.jsx), or any neighbouring code file that is not itself a placement-checked kind (the consumer). This matches modern feature-folder guidance — a single-consumer hook or util lives beside its consumer; only shared units graduate to a dedicatedhooks//mappers/folder. An orphaned unit with no anchor still flags.
'code-policy/file-kind-placement': ['error', { allowGenericFolders: true, allowColocation: true }]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/one-primary-unit': 'error'
'code-policy/no-hidden-top-level-declarations': 'error'
'code-policy/no-inline-types-in-runtime-files': 'error'
'code-policy/file-kind-placement': '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 primary unit rules 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-fileOr 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.
