lint-suite
v2.2.0
Published
Reusable ESLint, Stylelint, and Prettier configurations
Maintainers
Readme
Lint Suite — ESLint, Stylelint, and Prettier configs for TypeScript and Angular
lint-suite provides composable ESLint flat configs for JavaScript, TypeScript, Angular, RxJS, Vitest, and Playwright, plus standalone Stylelint and Prettier presets. Use the framework-agnostic recommended config as a baseline, then add the framework and testing configs your project needs.
The suite includes custom rules for TypeScript code structure, unused exports, unused Angular instance fields, and mismatches between Angular template classes and CSS/SCSS selectors. Each custom rule is also available as an independent @lint-suite package when you do not need the full preset.
Quick navigation
- Installation and ESLint flat config setup
- Available configurations
- Valid and invalid TypeScript examples
- Standalone custom rule packages
- Angular rule customization
- Stylelint and Prettier presets
Features
- TypeScript Linting: Strict typing rules, v8 type-safety replacements, consistent imports, and code organization
- Angular Support: Component best practices (including Signals), Angular 21+ template rules, and modern control flow
- RxJS Guidelines: Observable patterns, Finnish notation, subject encapsulation, and operator safety
- Code Style: Formatting rules, line limits, and structural consistency
- Accessibility: ARIA validation, keyboard events, and semantic HTML
- Testing: Vitest and Playwright configurations with best-practice rules
- Prettier: Automatic disabling of formatting rules that conflict with Prettier (
eslint-config-prettier) - Prettier config: Standalone formatting preset (subpath
lint-suite/prettier) with the suite's house defaults and Angular/HTML overrides - Stylelint: Standalone SCSS/CSS preset (subpath
lint-suite/stylelint) with standard + recess-order + BEM selector enforcement andlint-suite/no-unused-classes - Architecture: Module boundary enforcement with
eslint-plugin-boundaries - Additional Support: JSON (with comment support for tsconfig), Storybook CSF enforcement
Installation
pnpm add -D lint-suiteStandalone custom rules
All 21 custom rules also have independently versioned scoped packages. Installing one does not install this umbrella or its unrelated presets. For example:
pnpm add -D @lint-suite/eslint-plugin-arrow-body-fits-line eslint typescript typescript-eslintimport arrow from '@lint-suite/eslint-plugin-arrow-body-fits-line';
import tseslint from 'typescript-eslint';
export default [
{
files: ['**/*.ts'],
languageOptions: { parser: tseslint.parser },
plugins: { arrow },
rules: { 'arrow/arrow-body-fits-line': 'error' }
}
];The package catalog
lists every package and its rule key. The Angular instance-field package is
@lint-suite/eslint-plugin-no-unused-angular-instance-fields; its rule key
remains no-unused-instance-fields. The Stylelint package is
@lint-suite/stylelint-no-unused-classes, with rule ID
lint-suite/no-unused-classes.
No migration is needed for existing umbrella users: its three entrypoints, presets, rule IDs, severities, and options are unchanged. Avoid enabling a standalone rule alongside the corresponding umbrella rule.
Dependencies
pnpm add -D eslint typescript typescript-eslint eslint-config-prettierIf you use the Prettier preset (lint-suite/prettier), also install its peer dependency:
pnpm add -D prettierUsage
Create an eslint.config.mjs file in your project root:
import { recommended } from 'lint-suite/eslint';
export default [...recommended];The packages ship as ESM. Use Node.js 24 with the current ESLint, TypeScript, and Angular toolchain.
Or selectively include configurations:
import { base, javascript, typescript, prettier } from 'lint-suite/eslint';
export default [
...base,
...javascript,
...typescript,
...prettier // Must be last to disable conflicting formatting rules
];Composing framework configs on top
recommended is intentionally framework-agnostic — it ships only the language + architecture + format baseline (base, javascript, typescript, json, boundaries, prettier). Add the framework/tooling configs your project actually uses:
import {
recommended,
angular,
angularTemplate,
rxjs,
vitest
} from 'lint-suite/eslint';
export default [
...recommended,
...angular,
...angularTemplate,
...rxjs,
...vitest
];recommended already ends with prettier. The composable configs above are rule-only, so appending them after recommended is safe — but if a config you add re-enables a formatting rule, append ...prettier again at the very end.
Available Configurations
| Configuration | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| base | Core JavaScript rules, formatting, and complexity limits |
| javascript | JavaScript-specific rules via @nx/eslint-plugin |
| typescript | TypeScript strict typing, imports, and naming conventions |
| angular | Angular component best practices with Signal support |
| angularTemplate | HTML template rules with accessibility, performance, and lint-suite-angular-template/no-unstyled-classes |
| rxjs | Observable patterns, operator safety, and subscriptions |
| vitest | Vitest testing rules and matcher improvements |
| playwright | Playwright e2e locator and matcher best practices |
| json | JSON linting with comment support for tsconfig/vscode |
| storybook | Storybook CSF enforcement |
| boundaries | Module boundary rules (feature, data-access, ui, etc.) |
| prettier | Disables rules that conflict with Prettier (use last) |
| recommended | Baseline only: base + javascript + typescript + json + boundaries + prettier — compose the rest on top |
Angular project analysis: The angular preset enables projectService:
true and project analysis for lint-suite-angular/no-unused-instance-fields.
It counts exact reads in the configured TypeScript/Angular Program, including
external parent templates/TypeScript, subclasses, and Angular interface
implementations; code outside that Program is unknowable. Project mode also
reports unused public/protected directive members.
Direct rule usage also defaults to project analysis; select analysis:
'local' explicitly for local analysis. allowEffectFields is opt-in. After
cross-file or template changes, do not use ESLint --cache for correctness
gates; run a full non-cached lint such as eslint --no-cache.
Disk cache
no-unstyled-classes and no-unused-classes cache parsed component metadata,
stylesheets, and templates in memory and mirror those entries to
node_modules/.cache/lint-suite/ under the current working directory at
process exit. Each cache is isolated by its owning scoped plugin's package
name, version, and cache format. Upgrading either plugin invalidates its own
entries without reusing or overwriting the other plugin's cache.
Entries are also invalidated by file mtime and size. Malformed disk entries
are discarded and rebuilt. Set LINT_SUITE_CACHE_DIR to choose a directory,
or LINT_SUITE_CACHE=0 to keep caching in memory only.
The Angular instance-field rule maintains a separate in-memory project usage index; it does not use this disk cache.
Customization
You can override any rules by adding a rules section to your ESLint config:
import { typescript, prettier } from 'lint-suite/eslint';
export default [
...typescript,
{
rules: {
'@typescript-eslint/explicit-function-return-type': 'off'
}
},
...prettier
];Unused Angular instance fields
The angular config enables lint-suite-angular/no-unused-instance-fields.
Project analysis is the default and recognizes reads from other TypeScript
files and Angular templates. It requires parser services with type information:
{
files: ['**/*.ts'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
},
rules: {
'lint-suite-angular/no-unused-instance-fields': [
'error',
{
analysis: 'project',
allowEffectFields: true
}
]
}
}- In an editor session the index is updated incrementally: a save re-indexes
only the saved file and the files whose resolutions depended on it. The
saved file's own templates, and the templates of every component in its
folder, are re-read on every lint; edits to templates in other folders are
picked up on a throttled schedule (at most every 100× the duration of the
last check), so a cross-folder template edit can take a moment to show up
in another file's diagnostics. A
templateUrlfile that does not exist yet is remembered as missing and read on the same schedule once it does. analysisdefaults to'project'. Setanalysis: 'local'to check only the class, component template, and host expressions without type information. Project mode excludes spec-file reads. A file it cannot index exactly (a template that does not parse, metadata it cannot evaluate, a read it cannot type) falls back to name matching for that file only: every member whose name that file mentions counts as read. SetLINT_SUITE_DEBUG=1to print which files fell back and why, for exampleLINT_SUITE_DEBUG=1 eslint --no-cache src/app/some.component.ts.- A file with an import that resolves to no module (its file does not exist yet, or is still empty) reads through untyped values, so every candidate member it mentions counts as read, and it is re-indexed on every Program until the import resolves. The project service of typescript-eslint never retries a resolution that failed, so in an editor this lasts until the importing file is edited or ESLint restarts.
allowEffectFieldsdefaults tofalse. When enabled, fields holding auto-cleaned Angulareffect()calls are allowed; effects configured withmanualCleanup: truemust still be read.allowRxjsInteropFieldsdefaults tofalse. When enabled, unread fields holding@angular/core/rxjs-interopcalls (toSignal,toObservable,outputToObservable,rxResource) are allowed.- Inputs, outputs, and queries (
input(),model(),output(),viewChild(),viewChildren(),contentChild(),contentChildren(), and the matching@Input(),@Output(),@ViewChild(),@ViewChildren(),@ContentChild(), and@ContentChildren()fields) are reported when the class, its template, and its host metadata never read them. Parent bindings do not count as reads.outputFromObservable()fields,@Output()fields not initialized withnew EventEmitter(), and inputs of classes that declarengOnChangesare always treated as framework-managed. - Fields typed with
ComponentRefimported from@angular/coreare excluded from unused-field reports. - In local mode, non-private members of
abstractcomponents and directives are exempt: subclasses that read them live in other files. Project mode resolves those subclass reads and reports the members normally. - Project analysis is incremental. The index is kept per tsconfig; when an
editor hands the rule a changed Program after a save, only the saved file and
the files whose reads depended on it are re-indexed, so feedback stays fast
in large workspaces. Template references (
#ref,#ref="exportAs") resolve within a standalone component'simports; when that scope cannot be determined statically (NgModule declarations,hostDirectives, spreads), every matching component or directive in the Program is a candidate. Extra candidates can only add reads. Metadata strings may be constants.
No unstyled classes
The angularTemplate config enables
lint-suite-angular-template/no-unstyled-classes, which reports a class name
used in an Angular HTML template that no stylesheet of that component selects.
It reads three sources in the template: the static class="a b" attribute
(each token reported at its own column), [class.name] bindings, and the
literal class names inside [class]="..." expressions and class="a {{ b }}"
interpolations. String literals, object-literal keys, array elements, and both
branches of a ternary contribute names; identifiers, calls, pipes, and +
concatenations contribute nothing, so a class the rule cannot see is never
reported. [ngClass] is deliberately not analysed. routerLinkActive,
animate.enter, and animate.leave are also read as class lists, static or
bound.
Stylesheets come from the component beside the template: styleUrl,
styleUrls, and inline styles read as string or template literals from the
@Component metadata, falling back to a sibling .scss or .css file when
the metadata declares none. A <link rel="stylesheet" href="..."> in the
template itself is read too, resolved against the template's directory, so a
plain HTML page without a component is judged against the stylesheets it
links; root-relative (/x.css) and absolute (https://...) links are
skipped. Each stylesheet is parsed with postcss-scss, so
&__element, &--modifier, &.other, & > .child, .wrapper &, and rules
nested inside @media all resolve against their parent selector, and a
selector list such as .a, .b { &__x {} } yields both .a__x and .b__x.
@use, @import, and @forward are followed to their partials
(_name.scss, name/index.scss, name/_index.scss). A selector built with
interpolation (.icon-#{$size}) becomes a pattern, so icon-lg counts as
styled and bare icon does not.
{
files: ['**/*.html'],
rules: {
'lint-suite-angular-template/no-unstyled-classes': [
'error',
{
ignoreClassPatterns: ['^(js|qa|mat|cdk|mdc)-', '^u-'],
globalStyles: ['src/styles.scss']
}
]
}
}ignoreClassPatternsdefaults to['^(js|qa|mat|cdk|mdc)-']. Each entry is compiled withnew RegExp(pattern, 'u'), and a class matching any of them is never reported. A configured list replaces the default one instead of extending it.globalStylesdefaults to[]. Paths are resolved against the ESLint working directory and merged into the known classes of every template. A path that does not exist is ignored.- Elements whose tag name contains a dash are skipped: the classes on a child
component,
ng-container, orng-templatemay be styled by that component's own:host(.x), which this rule cannot see. - A template with no stylesheet reports nothing. The same holds when the only stylesheet found fails to parse: with nothing to compare against, the rule has no opinion.
No unused classes
The stylelint preset enables lint-suite/no-unused-classes, the dual of the
rule above: it reports a class selector in a component stylesheet that no
template of that component uses.
Templates are found from the stylesheet. Every .ts file beside it is read for
@Component metadata whose styleUrl or styleUrls resolves to the linted
file; each matching component contributes its templateUrl file or its inline
template literal, and their classes are merged, so a stylesheet shared by two
components is judged against both templates. When no component declares the
stylesheet, a sibling template of the same name (card.component.scss →
card.component.html) and every .html file in the same directory whose
<link rel="stylesheet" href="..."> resolves to it are used instead, plus
every template anywhere under the working directory whose link resolves to
it (node_modules, dist, coverage, and dot-folders are skipped), so a
plain styles.css beside an index.html and a shared stylesheet linked from
other folders are both checked. A partial (_tokens.scss) or a global
styles.scss that nothing links has no template, and the rule stays silent.
Selectors resolve through the same parser as the ESLint rule, so &__element,
&--modifier, &.other, & > .child, .wrapper &, @media blocks, and
selector lists all report the resolved name on the rule that declares it: in
.panel { .inner {} } only inner is checked on the inner rule, never panel
twice. A rule that only wraps nested rules, like .dialog in
.dialog { &__name {} }, emits no selector of its own and is never reported;
.shell in .shell { .inner {} } still is, because .shell .inner reaches
the output. Arguments of :host(.dark) and :host-context(.rtl) are skipped, and
everything after ::ng-deep, /deep/, or >>> is skipped too, because those
classes live in other templates. A selector built with interpolation
(.icon-#{$size}) is never reported, and @extend .base counts base as
used.
The template side reads the same sources as no-unstyled-classes (including
routerLinkActive, animate.enter, and animate.leave, static or bound) plus
[ngClass], and it does not skip custom elements: a class on
<app-child class="foo"> is written by this template, so .foo counts as
used. When any template of the stylesheet holds a class source the rule cannot
read — [class]="classes()", [ngClass]="map", a whole token that is
{{ expr }}, an unparseable template — the rule reports nothing for that
stylesheet rather than guessing.
// stylelint.config.mjs
import { stylelint } from 'lint-suite/stylelint';
export default {
...stylelint,
overrides: [
...stylelint.overrides,
{
files: ['**/*.scss', '**/*.css'],
rules: {
'lint-suite/no-unused-classes': [
true,
{ ignoreClassPatterns: ['^(js|qa|mat|cdk|mdc)-', '^u-'] }
]
}
}
]
};ignoreClassPatternsdefaults to['^(js|qa|mat|cdk|mdc)-']. Each entry is compiled withnew RegExp(pattern, 'u'), and a class matching any of them is never reported. A configured list replaces the default one instead of extending it.
Explicit accessibility
The typescript preset enables local/explicit-accessibility, which reports
class members (fields, methods, accessors, abstract members, and constructor
parameter properties) without an explicit public, private, or protected
modifier. #private members are ignored: TypeScript forbids a modifier there.
{
rules: {
'local/explicit-accessibility': [
'error',
{ defaultAccessibility: 'private' }
]
}
}defaultAccessibilitydefaults topublicand driveseslint --fix; the IDE offers the other two levels as suggestions.defaultAccessibility: 'none'reports without an auto-fix and offers all three levels as suggestions.- Constructors are always fixed to
public. A private constructor breaksnewand dependency injection. - The fix does not default to
privatebecause members implementing an interface or read by an Angular template must stay non-private, and the rule cannot see either.
Readonly type properties
The typescript preset enables local/readonly-type-properties, which
reports primitive-typed properties in type aliases, interfaces, and
inline object types that are not marked readonly, and auto-fixes them
with eslint --fix. A property is primitive-typed when its annotation is
string, number, boolean, bigint, symbol, null, undefined, a
literal or template-literal type, or a union/intersection of those.
// Before
type User = { name: string; roles: string[]; profile: Profile };
// After --fix
type User = { readonly name: string; roles: string[]; profile: Profile };- Arrays, object types, type references (including string-union aliases
like
Status), functions, and tuples are left untouched because the rule is syntactic and does not resolve types. - The same rule reports and fixes
readonly T[]andReadonlyArray<T>toT[]: the property reference is readonly, the array contents stay mutable. - Index signatures, mapped types, and method signatures are out of scope.
- Use
// eslint-disable-next-line local/readonly-type-propertieswhen a property genuinely needs to stay mutable.
No inline object types
The typescript preset enables local/no-inline-object-types, which
reports every object type literal that is not the direct body of a
type NAME = ... alias: nested properties, array element types, union and
intersection members, generic arguments such as Readonly<{...}>, function
parameter and return types, satisfies targets, interface and class
members, and members of a declare module block. It is not auto-fixable:
extracting an inline object type requires choosing a name.
// Before
const describe = (field: { readonly name: string }): { readonly label: string } => ...
// After
type Field = { readonly name: string };
type FieldSummary = { readonly label: string };
const describe = (field: Field): FieldSummary => ...- The direct body of a
type X = {...}alias is the only allowed position. - Mapped types (
{ [K in Keys]: T }) are a different node and stay valid.
One-line guard
The typescript preset enables local/one-line-guard (with
maxLineLength set to the preset print width, 135), which reports an
if statement whose braced body is a lone return, throw, continue,
or break when the whole statement would fit on one line. It is
auto-fixable: the fix drops the braces and joins the guard onto the if
line.
// Before
if (!user) {
return null;
}
// After
if (!user) return null;- Only a block body containing exactly one
return,throw,continue, orbreakstatement is considered a guard; any other body is left alone. - The rule bails out (no report, no fix) when the
ifhas anelse, the block holds a comment, the condition spans multiple lines, the guard statement spans multiple lines, or the collapsed line would exceedmaxLineLength. - Pass a different width with the rule's options:
'local/one-line-guard': ['error', { maxLineLength: 80 }]. - Complements
curly: multi-line: that rule tolerates a brace-less single-line guard once it exists, whilelocal/one-line-guardis what collapses a braced guard down to one line in the first place.
Statement shape rules
The typescript preset enables a family of small syntactic rules that make
control flow and data shape visible by reading the code's outline. None of
them reads type information or the filesystem; each listens to one node
type and reports in microseconds per file. Where a fix needs a name the rule
offers an IDE suggestion with a placeholder name instead of an auto-fix.
| Rule | Reports | Fix |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| local/no-call-in-condition | A function call inside an if condition, or inside a boolean const that an if tests. Zero-argument this.x() calls (Angular signal reads) and type-predicate calls are exempt; predicates are found through scope in the same file, or through the type checker when a program is available. Option allowPredicates (regex sources, default ['^(is\|has)[A-Z]']) applies when there is no program. | Suggestion: hoist to a const |
| local/max-condition-operands | An if condition with more than max (default 3) operands joined by && / \|\|. | none |
| local/no-grouped-condition | A parenthesised group with a different operator inside an if condition or a boolean const (a && (b \|\| c)). | Suggestion: hoist the group |
| local/ternary-branch-shape | A ternary branch that is not a name, literal, template literal, or plain member access. | none |
| local/chain-receiver-is-name | A member chain starting on an inline expression: (a ?? b).x, {...}.x, [...].x, (await p).x. | Suggestion: name the receiver |
| local/chain-fits-line | A chain of two or more calls whose .method( parts sit on different lines. A multi-line callback argument does not count. | none |
| local/arrow-body-fits-line | An expression-bodied arrow whose body wraps onto more lines. | Fix: block body with return |
| local/no-nested-object-value | A property value that is a non-empty object literal, an array holding object literals, a ternary, or a call chain. Decorator arguments (@Component({...})) and files matching configFiles (default **/*.config.*, **/eslint.config.*, **/*.schema.ts) are exempt. | Suggestion: hoist to a const |
| local/no-spread-expression | ...(expr) where the argument is not a name or member access. | Suggestion: hoist to a const |
| local/no-inline-return-object | return {...} and => ({...}). | Suggestion: const result = {...}; return result; |
Project layout rules
Also in the typescript preset. These read only the file's own path and
return no listeners for files they do not cover, so they cost one regex
per file.
| Rule | Reports | Options |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| local/type-placement | An exported type outside a common/*.type.ts or test/common/*.type.ts file; a value exported from a *.type.ts file; a non-const export from a *.const.ts file; an import type from a relative or internal path that is neither a *.type.ts / *.schema.ts file nor a common barrel (../common, ./common/index.ts, @shared/common). .spec.ts, .stub.ts, .schema.ts (an inferred type lives beside its schema), .d.ts, state.type.ts (any prefix), and fixtures are exempt; a .spec.util.ts file is not, so an exported type in one is reported. Never resolves imports. | internalPatterns: regex sources for alias prefixes that must resolve to a *.type.ts file. Default empty: a workspace alias (@shared/common) resolves to a library entry point, and module boundaries forbid deep imports, so alias type imports pass. |
| local/util-purity | Inside utils/*.util.ts (excluding paths under test/ or testing/): imports of node:fs, child_process, os, process, http, net, worker_threads; a module-level let; a module-level new Map/Set/WeakMap/WeakSet; process.*, globalThis, window, document, localStorage, console; Date.now, Math.random, performance.now, crypto.randomUUID; setTimeout, setInterval, fetch, inject, require. | bannedModules |
| local/test-file-shape | A file named *.spec-support.ts, *.spec-helper.ts, *.test-utils.ts, *-fixture.ts, or under __mocks__/ / helpers/ / common/stubs/; a .stub.ts outside test/stubs/, a .mock.ts outside test/mocks/, a .spec.util.ts outside test/utils/, or any fixtures/ directory outside test/fixtures/ (a dedicated testing/ library is exempt from all of these); a test/stubs/*.stub.ts export not named UPPER_SNAKE_STUB or without a type annotation; a test/mocks/*.mock.ts export that is not a camelCase ...Mock function with an explicit return type. | none |
No unused exports
The typescript preset enables local/no-unused-exports, which reports an
export (values and types) that no other file in the TypeScript program
imports, and a module that exports names but is never imported at all.
- Source of truth is the program typescript-eslint already built for the type-aware rules: no extra parse, no file enumeration. The disk is only asked about a specifier the checker cannot resolve, because the project service of typescript-eslint never retries a failed resolution; such a file is re-read on every Program until the specifier resolves.
- Per file the rule walks top-level statements, plus every node of a file
whose text contains
import(, and caches the result on thets.SourceFileobject; the project-wide usage map is cached perts.Program, and an edit that only touches a few files patches that map in place instead of rebuilding it. The patch is scoped per project (tsconfig): programs from different projects never share an aggregate, even when ESLint alternates between them in the same process. Editing one file re-walks that file and rebuilds the map once. At 10k files the warm cost is map lookups. - Re-exports are followed:
export { x } from,export * from, andexport * as ns fromcount usage at the file that declaresx, so a barrel does not hide a dead export.import * as ns, default imports, and dynamicimport('./x')anywhere in the file (a lazy route'sloadComponent: () => import('./x')included) count every export of the target as used. - Files matching
entryPointsare never reported (default**/main.ts,**/main.*.ts,**/public-api.ts,**/index.ts,**/*.config.ts,**/*.config.mts,**/*.config.cts,**/*.spec.ts,**/*.spec.util.ts,**/*.stub.ts,**/*.mock.ts,**/*.d.ts,**/*.stories.ts,**/environment*.ts). Files withexport =or adeclare moduleblock are skipped. - Names an entry point re-exports (
export { x } from,export * from) are public API and never reported: an Nx library'sindex.tsprotects the exports other projects consume, even though those projects sit outside the library's own TypeScript program. - The project is the linted file's tsconfig program. An export used only
from a file outside that program (for example a spec excluded by
tsconfig.lib.json) reports as unused; that is the same boundarytscdraws.
Examples
These five valid and five invalid TypeScript examples illustrate individual custom rules enabled by the typescript preset. Each example is judged by the named rule only; it is not a complete file guaranteed to pass every rule in the suite. Values such as items, name, and enabled represent existing application variables.
Valid examples
1. Name an object type before using it
local/no-inline-object-types allows a named type alias used in a parameter annotation.
type User = { name: string };
function greet(user: User): string {
return user.name;
}2. Name a returned object
local/no-inline-return-object allows returning an identifier instead of an inline object literal.
function createUser() {
const user = { name: 'Ada' };
return user;
}3. Extract a nested object value
local/no-nested-object-value allows an identifier as an object property value.
const address = { city: 'London' };
const user = { name: 'Ada', address };4. Name a spread operand
local/no-spread-expression allows spreading a named array.
const copy = [...items];5. Mark a primitive type property readonly
local/readonly-type-properties accepts readonly primitive properties in type aliases.
type Settings = { readonly enabled: boolean };Invalid examples
1. Put an object type directly in a parameter annotation
local/no-inline-object-types reports the inline type. Extract the User type shown above.
function greet(user: { name: string }): string {
return user.name;
}2. Return an object literal directly
local/no-inline-return-object reports the returned literal. Assign it to a named constant first.
function createUser() {
return { name: 'Ada' };
}3. Nest an object literal inside another object
local/no-nested-object-value reports the inline address value. Declare it separately.
const user = { name: 'Ada', address: { city: 'London' } };4. Spread the result of a call directly
local/no-spread-expression reports the inline call used as a spread operand. Assign the filtered array to a named constant before spreading it.
const copy = [...items.filter(isVisible)];5. Leave a primitive type property mutable
local/readonly-type-properties reports the missing readonly modifier.
type Settings = { enabled: boolean };For more valid and invalid code examples, configuration options, and rule-specific exceptions, see the standalone package catalog.
Stylelint and Prettier presets
These are standalone configs exported as subpaths — they are not part of the recommended ESLint array.
// stylelint.config.mjs
import { stylelint } from 'lint-suite/stylelint';
export default stylelint;// prettier.config.mjs
import { prettier } from 'lint-suite/prettier';
export default prettier;The Prettier preset is published with prettier as a peer dependency. The Stylelint preset requires stylelint and the referenced shared configs/plugins, which ship as dependencies of this package.
Available Rules (you can add more as you prefer)
nx/eslint-plugin
@nx/enforce-module-boundaries: Enforces module boundary restrictions@nx/dependency-checks: Validates dependencies in workspace projects- ...
@vitest/eslint-plugin
vitest/max-nested-describe: Limits describe nesting depthvitest/prefer-to-be: EnforcestoBematcher usagevitest/no-conditional-in-test: Disallows conditionals in tests- ...
eslint-plugin-playwright
playwright/prefer-locator: Enforces modern locator APIplaywright/prefer-native-locators: Prefers native locator methodsplaywright/prefer-to-be: EnforcestoBematcher usage- ...
eslint-plugin-json
json/json: Validates JSON syntaxjson/sort-package-json: Enforces consistent ordering in package.json- ...
@smarttools/eslint-plugin-rxjs
@rxjs/finnish: Enforces Finnish notation for observables@rxjs/no-exposed-subjects: Enforces subject encapsulation@rxjs/no-cyclic-action: Prevents infinite loops in NgRx effects@rxjs/no-unsafe-takeuntil: Ensures proper usage of takeUntil operator- ...
eslint-plugin-storybook
storybook/csf-component: Enforces component property in storiesstorybook/no-stories-of: Prevents deprecatedstoriesOfAPI- ...
eslint-plugin-import-x
import-x/no-cycle: Detects circular dependenciesimport-x/no-self-import: Prevents modules importing themselvesimport-x/order: Enforces a consistent order of import statementsimport-x/consistent-type-specifier-style: Consistent type import style- ...
@stylistic/eslint-plugin
@stylistic/max-len: Enforces maximum line length@stylistic/indent: Enforces consistent indentation@stylistic/quotes: Enforces consistent quote style- ...
eslint-config-prettier
- Automatically disables all ESLint rules that conflict with Prettier
- Must be the last configuration in the array
stylelint
- Scoped to
**/*.scssand**/*.cssvia anoverridesentry (SCSS is a CSS superset; SCSS-only rules simply don't fire on.css) - Extends
stylelint-config-standard,stylelint-config-standard-scss, andstylelint-config-recess-order selector-class-pattern: BEM-aware class names with ITCSS-style namespace prefixes (o-,c-,u-,is-,has-,js-,qa-, etc.)plugin/selector-bem-pattern: enforces BEM selectors, treats*.component.scss/*.component.cssas implicit components, ignores--mdc/--syscustom propertieslint-suite/no-unused-classes: reports a class selector no template of the component uses (see No unused classes)no-descending-specificity: disabled
prettier (format config)
singleQuote: true,semi: true,tabWidth: 2,printWidth: 135trailingComma: 'none',bracketSpacing: true,bracketSameLine: true,arrowParens: 'always',endOfLine: 'lf'- Overrides:
*.html→htmlparser,*.component.html→angularparser
Contributing
See CONTRIBUTING.md for contribution guidelines. See CHANGELOG.md for version history. See RELEASE_NOTES.md for detailed release notes.
License
MIT
