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

lrs-eslint-plugin

v0.3.1

Published

ESLint rules for Layered React Structure (LRS): enforce React import boundaries and an optional file and folder structure.

Downloads

3,479

Readme

LRS ESLint rules

lrs-eslint-plugin enforces the import boundaries in Layered React Structure, keeps each view feature independent of its siblings, and limits each folder to one .view source file and one .logic.ts file. It ships an ESM bundle and TypeScript declarations built with tsdown. Consumers can use it directly without a build step.

Install it in your React project:

npm install --save-dev lrs-eslint-plugin 'eslint@^10.12.0' typescript-eslint

Compose its flat configuration after your application's parser configuration:

import tseslint from 'typescript-eslint'
import lrs from 'lrs-eslint-plugin'

export default tseslint.config(
  ...tseslint.configs.recommended,
  ...lrs.configs.recommended,
)

The consumer supplies its JavaScript/TypeScript parser and JSX configuration. Paths are relative to the consuming ESLint configuration, so a frontend config in apps/frontend applies the rules to that application's src directory. The plugin does not configure or replace no-restricted-imports; application-specific restrictions can run alongside these rules.

| Rule | Scope | Disallowed behavior | | -------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | lrs/presentation-imports | .ui modules and components/ modules under src/ | services path segments, hooks path segments other than .ui hooks, .view and .logic modules, all @tanstack/* packages | | lrs/service-imports | src/**/services/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts} | React and React DOM, @tanstack/react-*, hooks and components path segments, .view, .ui, and .logic modules | | lrs/hook-rendering | src/**/hooks/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts} | JSX and React.createElement rendering in hooks | | lrs/no-application-io | .ui, components/, and utils/ modules under src/ | fetch, WebSocket/XHR/EventSource, and browser storage access | | lrs/utility-imports | src/**/utils/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts} | components, hooks, services, and views path segments, .view, .ui, and .logic modules, runtime React imports | | lrs/feature-imports | src/**/views/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts} | Imports that resolve into a different views/<feature> folder | | lrs/component-placement | Strict: src/**/components/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts} | Hook declarations and non-rendering modules in component folders | | lrs/one-view-per-folder | src/**/*.view.{ts,tsx,js,jsx,mjs,cjs,mts,cts} | More than one .view source file in the same folder | | lrs/one-logic-per-folder | src/**/*.logic.ts | More than one .logic.ts file in the same folder |

Presentational components may import React, plain types, utilities, constants, other presentation components, and presentation-safe hooks. Services may import framework-independent tools such as @tanstack/query-core, other services, and plain types. Utilities may use type-only React contracts, but not React at runtime, application and presentation layers, network APIs, or browser storage. Type-only imports are subject to the same layer boundaries: move shared types into a types module instead of importing them through a service or component.

lrs.configs.strict is an opt-in addition to lrs.configs.recommended. Spread it after the recommended preset to enable lrs/component-placement; it does not change the recommended preset.

Both presets ignore *.test.*, *.spec.*, *.stories.*, and *-test-support.* files by default, keeping application-layer boundaries focused on production modules.

The import rules inspect static imports, re-exports, import() expressions, unshadowed require() calls, TypeScript import types, and TypeScript import-equals declarations. String literals and template literals without expressions are checked. Component imports are recognized with or without source/output extensions, including .view.tsx and .ui.js. Directory imports such as ../services, Windows separators, and bundler query/fragment suffixes are also recognized.

This is a static specifier check, not an import-graph resolver. Computed module names and indirect exports from otherwise permitted barrels are not traced. Aliases must expose their layer in the module path (for example @/services/chat); an opaque alias such as @chat-client needs an application-specific restriction. Locally declared or parameter-bound functions named require are left alone.

To use different folder conventions, register the plugin and scope its individual rules explicitly:

export default [
  {
    files: ['features/**/*.ui.tsx'],
    plugins: { lrs },
    rules: { 'lrs/presentation-imports': 'error' },
  },
]

From the repository root, run pnpm nx test:lib lrs-eslint-plugin for the TypeScript Vitest suite, including RuleTester, flat-configuration, and packed-package consumer tests. Nx builds the plugin first. Run pnpm nx test:types lrs-eslint-plugin to typecheck the source and tests, and pnpm nx test:eslint lrs-eslint-plugin to lint the package.

The declarations cover the default plugin, configs.recommended, configs.strict, all ten rules, and createLrsConfig. The package also exports the LrsPlugin and LrsConfigOptions types. Strict consumer checks verify the published declarations with NodeNext and Bundler module resolution.

Keep features independent

lrs/feature-imports applies to every source file inside a views/<feature> folder, including its .view, .ui, and .logic files and its nested hooks/, components/, services/, and other layers. It reports any import whose module path resolves into a different feature folder, such as ../people/hooks/use-people or @/views/people/people.ui from src/views/chat/chat.view.tsx. Imports within the same feature and imports of the shared src/ layers are allowed.

When two features need the same hook, component, utility, service, or type, move it to the matching src/-level layer and make it generic enough for both consumers instead of importing across features. For example, a usePeople hook that chat and people both call belongs in src/hooks/use-people.ts, not in src/views/people/hooks/.

The rule resolves relative specifiers against the linted file's path and, like the other import rules, otherwise reads the views/<feature> segments from the specifier itself. It does not check shared layers that import a feature; src/components/person-card.tsx importing ../views/people/people.ui is a separate review item. Nested views/ folders inside a feature are treated as sub-features: they may import from their ancestors and descendants, but not from a sibling sub-feature.

Presentation-safe hooks

Presentational components can't import from hooks/ because application hooks usually read queries, services, or business state. Some hooks only manage display behavior, such as a disclosure toggle or a media query. There are two ways to let .ui files use them.

Name a presentation-only hook with a .ui suffix, such as src/hooks/use-disclosure.ui.ts. lrs/presentation-imports allows .ui modules from hooks/, and the recommended configuration applies the same rule to the hook itself. A .ui hook can use React, utilities, constants, types, and other .ui hooks; importing an application hook, a service, or a TanStack package reports an error. Relative imports that stay inside a hooks/ folder, such as ./use-settings, count as hook imports.

For a hook you can't rename, or one from a package such as @tanstack/react-virtual, list it in the rule's allow option:

export default [
  ...lrs.configs.recommended,
  {
    files: ['src/**/*.ui.{ts,tsx,js,jsx,mjs,cjs,mts,cts}'],
    rules: {
      'lrs/presentation-imports': [
        'error',
        {
          allow: [
            { from: 'src/hooks/use-theme', hooks: ['useTheme'] },
            { hooks: ['useVirtualizer'] },
            { from: 'src/hooks/use-breakpoints' },
          ],
        },
      ],
    },
  },
]

Each allow entry needs from, hooks, or both:

| Option | Type | Behavior | | ------- | ---------- | ----------------------------------------------------------------------------------------------------- | | from | string | The module the entry applies to. When omitted, the entry applies to every otherwise forbidden module. | | hooks | string[] | Named imports allowed from that module. When omitted, every import from the from module is allowed. |

An import passes when each name it takes from a forbidden module is listed by a matching entry, so { from: 'src/hooks/use-theme', hooks: ['useTheme'] } allows import { useTheme } from '../../hooks/use-theme' but still reports useThemeStore from the same module. Entries combine: one entry can allow useTheme and another useThemeColors in the same import. hooks entries only match named imports and export { name } from re-exports. Default and namespace imports, side-effect imports, export *, import(), and require() need an entry with from and no hooks.

from matches the end of an import path on whole segments, ignoring a source extension, so hooks/use-theme doesn't match ../hooks/use-theme-store. Relative imports are resolved against the linted file first: src/hooks/use-theme matches ../../hooks/use-theme from a feature, but not that feature's own ./hooks/use-theme. Aliased imports are matched as written, so use hooks/use-theme to also match @/hooks/use-theme. The rule trusts allowed hooks without checking their imports; prefer a .ui hook when you control the code.

One view and one logic file per folder

lrs/one-view-per-folder keeps each folder's application orchestration in one place. For example, people.view.tsx and edit-person.view.tsx cannot share a folder. Files such as people.view.test.tsx, people.view.spec.tsx, and people.view.stories.tsx don't count. Views in separate folders have independent limits, and a folder may contain no view files.

lrs/one-logic-per-folder applies a separate limit to .logic.ts files. A folder may contain both people.view.tsx and people.logic.ts, but adding edit-person.logic.ts there reports an error. Files such as people.logic.test.ts and people.logic.spec.ts don't count. If people.logic.ts needs further decomposition, extract that behavior into the same folder's hooks/, such as hooks/use-edit-person.ts.

Both rules check the linted file's immediate folder on disk and include the current filename when it hasn't been saved yet. They report every conflicting file, so linting just one file still detects its siblings. ESLint's --cache can reuse a result without running a rule when only a sibling changes; run without --cache after adding, removing, or renaming view or logic files.

When upgrading a project that uses configs.recommended or createLrsConfig, folders with multiple .view files will start reporting errors. Keep one orchestration view per folder and move reusable React behavior into hooks/, I/O into services/, and pure business calculations into utils/. Give an independently composed view its own feature folder and update its imports. Renaming a component to .ui is appropriate only after its business logic has moved into the owning view or application layers.

Folders with multiple .logic.ts files also start reporting errors. Keep one logic file and move further decomposition into that folder's hooks/, then update its imports. The file-count rules enforce these limits; they don't determine whether the code inside each file belongs in its layer.

Optional application structure

createLrsConfig({ root }) combines the recommended import boundaries, feature isolation, and view/logic file limits with a TypeScript React application's file and folder policy through eslint-plugin-project-structure. Use this single preset for the full LRS policy. Applications that use another folder layout can still use the default plugin's configs.recommended array shown above.

import tseslint from 'typescript-eslint'
import { createLrsConfig } from 'lrs-eslint-plugin'

export default tseslint.config(
  ...tseslint.configs.recommended,
  ...createLrsConfig({ root: import.meta.dirname }),
)

root must be the absolute application directory containing src; import.meta.dirname supplies it from the consuming ESLint config. The upstream plugin otherwise derives its root from its installation path, which resolves to the workspace root under pnpm and can silently skip an application's files. The factory scopes all recommended rules and the folder checker to the explicit root, independently of the command's working directory. Configuration files, public assets, end-to-end tests outside src, and sibling applications are outside this policy.

Pass ignorePatterns to exempt framework-owned paths from the folder-structure check without disabling the import rules. Patterns are relative to src, for example createLrsConfig({ root: import.meta.dirname, ignorePatterns: ['routes/**', 'routeTree.gen.ts'] }).

| Location | Accepted contents | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | src | Shared layers, views, app.tsx, main.tsx, their tests, vite-env.d.ts, and styles | | src/views/<feature> | At most one *.view.tsx and one *.logic.ts; *.ui.tsx, tests, stories, styles, index.ts, and feature layers | | components | Components (*.tsx, optionally *.ui.tsx), tests, stories, styles, and barrels | | hooks | use-*.ts, presentation-only use-*.ui.ts, hook tests, and barrels | | services, constants, utils | TypeScript modules, tests, and barrels | | types | TypeScript modules and declarations such as svg.d.ts | | services/schemas | Generated *.d.ts schemas and an optional barrel | | assets | Images, fonts, media, WebAssembly, and JSON |

The same layer directories are available at the shared and feature level. Features, nested layer directories, and filenames use lowercase kebab case. Layer directories may be nested for related responsibilities; arbitrary directories directly under src or a feature are rejected. New features and modules do not require adding their names to the configuration.

Tests may use .spec.ts, .test.ts, .spec.tsx, or .test.tsx, including role suffixes such as results.ui.spec.tsx and results.logic.test.ts. Stories use .stories.tsx. CSS, Sass, and Less can be colocated with components or features, including *.module.css. Production JSX belongs to views or components, with app.tsx and main.tsx as entry points. The utils/testing-utils directory also permits JSX render helpers and provider wrappers for tests.

The factory preserves the application's TypeScript/JavaScript parser and import rules. Its path-only parser applies to other extensions so CSS, SVG, unknown extensions, and extensionless files are checked too. Scope additional code-analysis configurations to their relevant code extensions, particularly rules that require TypeScript parser services.

The upstream plugin deduplicates folder diagnostics through a filesystem cache. This package isolates that cache in a temporary directory for each file and removes it after the check, so linting another file in the same invalid folder still reports an error. No project cache files or additional ignore entries are needed. The package tests exercise separate ESLint instances, non-code filesystem traversal, working-directory independence, and parser/import-rule composition.

Build and release

Use the Node.js version in .nvmrc and the repository's pinned pnpm version. After pnpm install, run pnpm test from the repository root to run lint, formatting, strict typechecks, Vitest, and builds through Nx. pnpm nx build lrs-eslint-plugin produces dist/index.js and dist/index.d.ts; only dist and npm's standard package metadata/documentation are published. ESLint and the folder-structure plugin remain external dependencies.

pnpm --filter lrs-eslint-plugin pack --pack-destination /tmp builds through prepack before creating a tarball. The test suite also unpacks a tarball into an isolated consumer and verifies that its runtime and declarations work without access to this package's source or development dependencies.

Add a Changeset with pnpm changeset for release changes. The GitHub Actions release workflow creates a version pull request; after it is merged, it checks and packs the versioned package before publishing with npm provenance. Plugin metadata takes its name and version from package.json at build time.

An npm maintainer must configure a trusted publisher for crutchcorn/layered-react-structure, workflow release.yml, with permission for direct npm publish. If the package does not exist on npm yet, publish the initial version from an authenticated maintainer environment first. See the Changesets trusted publishing guide and npm trusted publishing documentation.