@tnnquang/eslint

Opinionated ESLint plugin for modern TypeScript projects. Enforces explicit imports, function declarations, path aliases, and consistent type imports across React, Vue, Angular, NestJS, Next.js, and Nuxt.
5,966 1
Install
npm install @tnnquang/eslint
Latest Version:3.1.0
License:MIT
Last Updated:Jul 20, 2026
Links: GitHub  ·  npm
Maintainer: tnnquang

@tnnquang/eslint

Opinionated ESLint plugin for modern TypeScript projects. Enforces explicit imports, function declarations, path aliases, and consistent type imports.

Supports ESLint v7–v10+ (both legacy eslintrc and flat config formats).

Philosophy

This plugin enforces patterns that improve code quality at scale:

  • Explicit over implicit — Named imports make dependencies visible; function declarations are hoisted and produce better stack traces.
  • Consistency over flexibility — Path aliases eliminate fragile relative paths; type imports clarify the value/type boundary.
  • Performance-aware — Named imports enable tree-shaking; avoiding barrel files prevents bundler slowdowns and circular deps.
  • Framework-aware — Rules adapt to your framework's conventions automatically.

Rules

Rule Fixable Description
no-arrow-components ✅ Enforce function declarations for top-level functions and components
no-namespace-import ✅ Disallow import * as X and default-as-namespace usage
enforce-path-alias ✅ Enforce path aliases instead of deep relative imports
no-barrel-imports — Disallow importing from barrel/index files within the same module
consistent-type-imports ✅ Enforce consistent import type usage in TypeScript
arrow-in-body ✅ Enforce arrow functions inside function/component bodies

Installation

npm install --save-dev @tnnquang/eslint
# or
yarn add -D @tnnquang/eslint
# or
pnpm add -D @tnnquang/eslint

Usage

// eslint.config.js
import tnnquang from "@tnnquang/eslint";

export default [
  tnnquang.configs["flat/recommended"],
  // or framework-specific:
  // tnnquang.configs["flat/react"],
  // tnnquang.configs["flat/vue"],
  // tnnquang.configs["flat/typescript"],
];

Available flat configs: flat/recommended, flat/strict, flat/react, flat/vue, flat/angular, flat/nestjs, flat/nextjs, flat/nuxt, flat/typescript.

Legacy Config (eslintrc)

{
  "extends": ["plugin:@tnnquang/eslint/recommended"]
}

Available legacy configs: recommended, strict, react, vue, angular, nestjs, nextjs, nuxt, typescript.

Manual Configuration

// eslint.config.js (flat)
import tnnquang from "@tnnquang/eslint";

export default [
  {
    plugins: { "@tnnquang/eslint": tnnquang },
    rules: {
      "@tnnquang/eslint/no-arrow-components": "warn",
      "@tnnquang/eslint/no-namespace-import": ["warn", {
        allowedLibraries: ["*.module.css", "@/config/*"],
      }],
      "@tnnquang/eslint/enforce-path-alias": ["warn", {
        minRelativeDepth: 2,
      }],
      "@tnnquang/eslint/arrow-in-body": "warn",
      "@tnnquang/eslint/no-barrel-imports": "warn",
      "@tnnquang/eslint/consistent-type-imports": ["warn", {
        prefer: "type-imports",
      }],
    },
  },
];

Rule Details

no-arrow-components

Enforces function declarations for top-level functions and React components. Provides autofix.

Options
{
  allowArrowFunctions?: boolean;        // default: false - allow for non-components
  allowInCallExpressions?: boolean;     // default: true - allow in HOCs (memo, forwardRef, styled)
  allowedWrappers?: string[];           // HOC names that can wrap arrow functions
  allowTypedFunctionExpressions?: boolean; // default: true - allow with type annotation
  allowExportDefault?: boolean;         // default: false - allow `export default () => {}`
  exclude?: string[];                   // regex patterns to exclude
  include?: string[];                   // only check matching patterns
}
Examples
// ❌ Incorrect
const MyComponent = () => <div>Hello</div>;
const fetchData = async (url) => { return fetch(url); };

// ✅ Correct
function MyComponent() { return <div>Hello</div>; }
async function fetchData(url) { return fetch(url); }

// ✅ Allowed (HOC wrappers)
const MemoComponent = memo(() => <div/>);
const StyledButton = styled.button`color: red`;

arrow-in-body

Enforces arrow functions for inner function declarations inside function/component bodies to prevent hoisting issues. Provides autofix.

Note: This rule does NOT apply to class methods — they should use standard method syntax.

Options
{
  allowNamedFunctions?: boolean;    // default: false - allow if used for recursion
  onlyInComponents?: boolean;       // default: false - only enforce in PascalCase (components)
  exclude?: string[];               // regex patterns to exclude
  includeObjectMethods?: boolean;   // default: false - also check { key: function() {} }
  checkCallbacks?: boolean;         // default: false - also check callbacks like arr.map(function() {})
}
Examples
function MyComponent() {
  // ❌ Incorrect - function declaration inside body (hoisted)
  function handleClick() { console.log("click"); }

  // ❌ Incorrect - function expression
  const handleHover = function() { console.log("hover"); };

  // ❌ Incorrect (with checkCallbacks: true)
  const doubled = items.map(function(x) { return x * 2; });

  // ✅ Correct - arrow function (not hoisted, clear scope)
  const handleClick = () => { console.log("click"); };
  const doubled = items.map((x) => x * 2);

  return <button onClick={handleClick} />;
}

// With onlyInComponents: true, this is allowed:
function utilityFunction() {
  function helper() { ... } // OK - parent is not a component
}

no-namespace-import

Disallows namespace imports (import * as X) and default imports used as namespaces (import X; X.method()). Provides autofix.

Default behavior: Only checks a curated list of ~100 popular libraries (React, Vue, Angular, NestJS, lodash-es, AWS SDK v3, etc.) where named imports enable better tree-shaking.

Default Target Libraries

The rule automatically checks these library categories:

  • React ecosystem: react, react-dom, react-router, redux, zustand, jotai, recoil
  • Next.js: next, next/router, next/navigation, next/link, next/image
  • Vue ecosystem: vue, vue-router, pinia, vuex
  • Angular: @angular/core, @angular/common, @angular/router, @angular/forms
  • NestJS: @nestjs/common, @nestjs/core, @nestjs/config, @nestjs/jwt, @nestjs/typeorm, etc.
  • Express/Fastify: express, fastify, @fastify/*
  • Database/ORM: typeorm, @prisma/client, mongoose, sequelize, drizzle-orm
  • Utilities: lodash-es, ramda, date-fns, rxjs, zod, yup
  • UI libraries: @mui/material, antd, @chakra-ui/react, @radix-ui/*
  • AWS SDK v3: @aws-sdk/client-, @aws-sdk/lib-
  • Firebase: firebase/*, firebase-admin
  • Testing: vitest, jest, @testing-library/*
Options
{
  // Exclude libraries from checking (supports glob patterns)
  allowedLibraries?: string[];  // e.g., ["*.module.css", "@/config/*", "axios"]

  // Add more libraries to check (in addition to defaults)
  targetLibraries?: string[];   // e.g., ["my-internal-lib"]

  // Set to ['*'] to check ALL libraries
  targetLibraries?: ["*"];

  // Disable default target list, only use your custom targetLibraries
  useDefaultTargets?: boolean;  // default: true

  // Specific controls for star imports vs default-as-namespace
  allowStarImports?: boolean;          // default: false
  allowDefaultAsNamespace?: boolean;   // default: false
  allowedStarImports?: string[];       // e.g., ["@sentry/*"]
  allowedDefaultAsNamespace?: string[];

  // TypeScript options
  allowTypeNamespaces?: boolean;       // default: false
}
Examples
// ❌ Incorrect (react is in default targets)
import React from "react";
React.useState(0);

import * as Sentry from "@sentry/browser"; // unless in allowedStarImports

// ✅ Correct
import { useState } from "react";

// With config: allowedStarImports: ["@sentry/*"]
import * as Sentry from "@sentry/browser"; // now allowed
Configuration Examples
// Check only default libraries, exclude some
"@tnnquang/eslint/no-namespace-import": ["warn", {
  allowedLibraries: ["axios", "mongoose", "*.module.css"],
}]

// Check ALL libraries (like v2 behavior)
"@tnnquang/eslint/no-namespace-import": ["warn", {
  targetLibraries: ["*"],
}]

// Disable all checks
"@tnnquang/eslint/no-namespace-import": ["warn", {
  useDefaultTargets: false,
}]

// Only check custom libraries
"@tnnquang/eslint/no-namespace-import": ["warn", {
  useDefaultTargets: false,
  targetLibraries: ["my-internal-lib", "company-utils"],
}]

// Add to defaults + allow some star imports
"@tnnquang/eslint/no-namespace-import": ["warn", {
  targetLibraries: ["my-internal-lib"],
  allowedStarImports: ["@sentry/*", "protobufjs"],
}]

enforce-path-alias

Enforces path aliases for imports instead of deep relative paths. Auto-detects from tsconfig.json, vite.config.*. Provides autofix.

Options
{
  mode?: "all" | "direct-children";     // default: "direct-children"
  minRelativeDepth?: number;            // default: 1 - minimum ../ count to trigger
  configFile?: string;                  // default: "tsconfig.json"
  paths?: Record<string, string[]>;     // manual aliases (overrides auto-detect)
  baseUrl?: string;                     // auto-detected if omitted
  exclude?: string[];                   // folder patterns to exclude
  excludeFiles?: string[];              // file patterns to skip
  allowRelativeInSameFolder?: boolean;  // default: true
  includeDynamicImports?: boolean;      // default: true
}
Examples
// tsconfig.json: { "paths": { "@/*": ["./src/*"] } }

// ❌ Incorrect (in src/components/Button.tsx)
import { validate } from "../../utils/validation";

// ✅ Correct
import { validate } from "@/utils/validation";

// ✅ Allowed (same folder)
import { ButtonProps } from "./Button.types";

no-barrel-imports

Disallows importing from barrel/index files within the same module to prevent circular dependencies. Not auto-fixable (target file is ambiguous).

Options
{
  scope?: "same-module" | "same-parent" | "all-relative"; // default: "same-parent"
  barrelFiles?: string[];       // default: ["index"]
  allowedPatterns?: string[];   // patterns to allow
  excludeFolders?: string[];    // default: ["node_modules"]
}

consistent-type-imports

Enforces consistent import type usage in TypeScript. Only active for .ts/.tsx files. Provides autofix.

Options
{
  prefer?: "type-imports" | "inline-type-imports" | "no-type-imports"; // default: "type-imports"
  allowedLibraries?: string[];  // libraries to exclude
}
Examples
// prefer: "type-imports" (default)

// ❌ Incorrect
import { type User, type Post } from "./types";

// ✅ Correct
import type { User, Post } from "./types";

Configuration Recipes

React + TypeScript (strict)

import tnnquang from "@tnnquang/eslint";

export default [
  tnnquang.configs["flat/strict"],
  {
    rules: {
      "@tnnquang/eslint/no-namespace-import": ["error", {
        allowedStarImports: ["@testing-library/*"],
        allowedLibraries: ["*.module.css", "*.module.scss"],
      }],
      "@tnnquang/eslint/no-arrow-components": ["error", {
        exclude: ["^use[A-Z]"], // allow hooks as arrow functions
      }],
    },
  },
];

Vue/Nuxt

import tnnquang from "@tnnquang/eslint";

export default [
  tnnquang.configs["flat/vue"],
  {
    rules: {
      "@tnnquang/eslint/enforce-path-alias": ["warn", {
        exclude: ["composables", "stores"],
      }],
    },
  },
];

NestJS Backend

import tnnquang from "@tnnquang/eslint";

export default [
  tnnquang.configs["flat/nestjs"],
  {
    rules: {
      "@tnnquang/eslint/no-namespace-import": ["error", {
        allowedLibraries: ["class-validator", "class-transformer"],
      }],
      "@tnnquang/eslint/consistent-type-imports": ["warn", {
        prefer: "inline-type-imports",
      }],
    },
  },
];

Migration from v2

Breaking Changes in v3

  1. no-namespace-import now only checks default target libraries — previously checked ALL imports
    • Add targetLibraries: ["*"] to restore v2 behavior
  2. New rules added: no-barrel-imports, consistent-type-imports, arrow-in-body
  3. no-arrow-components now has autofix
  4. Node.js 18+ required
  5. Flat config support — use flat/* config names for ESLint v9+

Migration Steps

// If you want v2 behavior (check ALL libraries):
"@tnnquang/eslint/no-namespace-import": ["error", {
+  targetLibraries: ["*"],
}]

// If you have libraries that need namespace imports:
"@tnnquang/eslint/no-namespace-import": ["error", {
+  allowedStarImports: ["@sentry/*", "protobufjs"],
+  allowedLibraries: ["*.module.css"],
}]

License

MIT

Author

Tran Ngoc Nhat Quang

Contributing

Issues and pull requests are welcome at github.com/tnnquang/eslint-plugin.


Changelog

v3.1.0

  • no-namespace-import now has smart defaults — only checks ~100 popular libraries by default instead of all imports
  • Added useDefaultTargets option to control default library list
  • Added targetLibraries: ["*"] to check all libraries (v2 behavior)
  • Expanded default targets to include backend libraries (NestJS, Express, Fastify, TypeORM, Prisma, AWS SDK v3, Firebase)
  • Use allowedLibraries to exclude specific libraries from checking

Improvements:

  • enforce-path-alias: Windows path separator fix, Vite array-style alias support, recursive nested extends in tsconfig with cycle detection, added ExportNamedDeclaration and ExportAllDeclaration support
  • no-barrel-imports: Added TTL cache (30s), added ExportNamedDeclaration and ExportAllDeclaration support
  • consistent-type-imports: Handle import React, { type FC } (default + inline type mixed), added mixedDefaultAndType message
  • no-namespace-import: Handle template literal property access, destructuring from namespace, JSX spread attributes
  • arrow-in-body: Handle unary IIFE patterns (!function(){}(), void function(){}())

v3.0.0

  • New rule: arrow-in-body — enforce arrow functions inside function bodies
  • New rule: no-barrel-imports — prevent circular dependencies from barrel files
  • New rule: consistent-type-imports — enforce import type in TypeScript
  • no-arrow-components now has autofix
  • no-namespace-import covers import * as — add exceptions with allowedStarImports
  • Flat config support — use flat/* config names for ESLint v9+
  • ESLint v10 compatible — uses new context API
  • Node.js 18+ required

Related Packages

@vfourny/node-toolkit

Shared ESLint, TypeScript, Prettier, Vitest and Commitlint configurations for No...

10,173 1
@eslint-sets/eslint-config

Modern ESLint config with flat config support for Vue, React, Svelte, TypeScript...

6,835 7
@batuhan-bas/configs

Shared ESLint (flat config) and Prettier configs for TypeScript, React, Vue and...

592 2
@vasekzdvihal/eslint-config

Shared ESLint flat config for Vasek's Vue/Nuxt/TS projects. Wraps @antfu/eslint-...

954 0
@coderefine/cli

CodeRefine — Multi-framework tech debt scanner CLI for React, Vue, Svelte, and A...

2,313

Version History

Version Released License
3.1.0 MIT
3.0.1 MIT
3.0.0 MIT
2.0.4 MIT
2.0.3 MIT