# How to Load ESLint Rules in TSSLint: Using importESLintRules with Plugin Prefix Resolution

> Easily load ESLint rules in TSSLint using importESLintRules. Automatically resolve plugin prefixes like react/ with this powerful tool. Optimize your TSSLint configuration today.

- Repository: [Johnson Chu/tsslint](https://github.com/johnsoncodehk/tsslint)
- Tags: how-to-guide
- Published: 2026-03-04

---

**Use `importESLintRules()` from `@tsslint/config` to convert any ESLint ruleset—core rules or plugins—into native TSSLint rules, with automatic resolution of plugin prefixes like `react/` or `@typescript-eslint/`.**

TSSLint provides a compatibility layer that lets you reuse existing ESLint configurations without rewriting rules. The `importESLintRules` function in [`packages/config/lib/eslint.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/config/lib/eslint.ts) handles the entire pipeline: resolving plugin names, loading rule implementations from npm packages or core ESLint distributions, and wrapping them for the TSSLint engine using `@tsslint/compat-eslint`.

## The importESLintRules Entry Point

The **`importESLintRules`** function serves as the primary API for importing ESLint configurations into TSSLint. Located at [[`packages/config/lib/eslint.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/config/lib/eslint.ts)](https://github.com/johnsoncodehk/tsslint/blob/master/packages/config/lib/eslint.ts#L41-L86), it accepts a configuration object mapping rule names to severity levels or options arrays.

```typescript
export async function importESLintRules(
    config: { [K in keyof ESLintRulesConfig]: RuleSeverity | [RuleSeverity, ...ESLintRulesConfig[K]] },
    context: Partial<ESLint.Rule.RuleContext> = {},
    getConvertRule = async () => {
        try {
            return (await import('@tsslint/compat-eslint')).convertRule;
        } catch {
            throw new Error('Please install @tsslint/compat-eslint to use importESLintRules().');
        }
    },
) {
    const convertRule = await getConvertRule();
    const rules: TSSLint.Rules = {};
    
    for (const [rule, severityOrOptions] of Object.entries(config)) {
        // Severity normalization occurs here
        const ruleModule = await loadRuleByKey(rule);
        if (!ruleModule) throw new Error(`Failed to resolve rule "${rule}".`);
        
        rules[rule] = convertRule(
            ruleModule,
            options,
            { id: rule, ...context },
            severity === 'error' ? 1 : severity === 'warn' ? 0 : 3,
        );
    }
    return rules;
}

```

The function requires **`@tsslint/compat-eslint`** as a peer dependency. It normalizes rule severity, calls **`loadRuleByKey`** to resolve the rule implementation, and wraps each rule with the compatibility converter.

## How Plugin Prefix Resolution Works

TSSLint automatically handles ESLint's plugin prefix syntax through the **`resolveRuleKey`** generator function. This logic parses rule identifiers to determine whether they reference core rules, scoped plugins, or shorthand plugins.

### The Three Rule Identifier Formats

ESLint rules can be referenced in three distinct forms:

- **Core rules**: No prefix (e.g., `no-console`) — loaded directly from ESLint's built-in rule directory
- **Scoped plugins**: `@org/plugin/name` format (e.g., `@typescript-eslint/no-unused-vars`) — resolves to `@org/eslint-plugin`
- **Shorthand plugins**: `plugin/name` format (e.g., `react/jsx-uses-react`) — automatically prepends `eslint-plugin-` prefix

### The resolveRuleKey Implementation

The [`resolveRuleKey`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/config/lib/eslint.ts#L88-L107) generator yields one or two possible `(pluginName?, ruleName)` tuples, handling nested plugin paths:

```typescript
function* resolveRuleKey(rule: string): Generator<[pluginName: string | undefined, ruleName: string]> {
    const slashIndex = rule.indexOf('/');
    
    if (slashIndex !== -1) {
        // Has a slash → treat as plugin/rule
        let pluginName = rule.startsWith('@')
            ? `${rule.slice(0, slashIndex)}/eslint-plugin`
            : `eslint-plugin-${rule.slice(0, slashIndex)}`;
        let ruleName = rule.slice(slashIndex + 1);
        yield [pluginName, ruleName];

        // Support nested plugin names like "plugin/sub/rule"
        if (ruleName.indexOf('/') >= 0) {
            pluginName += `-${ruleName.slice(0, ruleName.indexOf('/'))}`;
            ruleName = ruleName.slice(ruleName.indexOf('/') + 1);
            yield [pluginName, ruleName];
        }
    } else {
        // No slash → core rule
        yield [undefined, rule];
    }
}

```

For a rule like `react/jsx-uses-react`, this yields `['eslint-plugin-react', 'jsx-uses-react']`. For nested paths like `plugin/sub/name`, it yields multiple candidates to attempt resolution.

## Loading Rule Implementations

Once the rule key is resolved, TSSLint loads the actual rule module through a two-stage process involving **`loadRuleByKey`** and **`loadRule`**.

### Resolving Plugin Packages

The [`loadRuleByKey`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/config/lib/eslint.ts#L12-L19) function iterates over candidates from `resolveRuleKey` and returns the first successfully loaded module:

```typescript
async function loadRuleByKey(rule: string): Promise<ESLint.Rule.RuleModule | undefined> {
    for (const resolved of resolveRuleKey(rule)) {
        const ruleModule = await loadRule(...resolved);
        if (ruleModule) return ruleModule;
    }
}

```

The [`loadRule`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/config/lib/eslint.ts#L21-L38) function handles the actual import using a cached loader promise:

```typescript
async function loadRule(pluginName: string | undefined, ruleName: string): Promise<ESLint.Rule.RuleModule | undefined> {
    if (pluginName) {
        plugins[pluginName] ??= loader(pluginName);
        const plugin = await plugins[pluginName];
        return plugin?.rules[ruleName];
    }
    // Core rule handling...
}

```

Plugins are cached in the `plugins` object to avoid redundant imports across multiple rule resolutions.

### Locating Core ESLint Rules

For core rules (where `pluginName` is `undefined`), TSSLint walks up the filesystem from `__dirname` searching for `node_modules/eslint/lib/rules/{ruleName}.js`:

```typescript
async function loadRule(pluginName: string | undefined, ruleName: string): Promise<ESLint.Rule.RuleModule | undefined> {
    // Plugin case handled above...
    
    // Core rule – search upward for eslint's built-in rule file
    let dir = __dirname;
    while (true) {
        const rulePath = path.join(dir, 'node_modules', 'eslint', 'lib', 'rules', `${ruleName}.js`);
        if (fs.existsSync(rulePath)) return loader(rulePath);
        const parentDir = path.resolve(dir, '..');
        if (parentDir === dir) break;
        dir = parentDir;
    }
}

```

This mirrors ESLint's own resolution logic, ensuring core rules are found even when `eslint` is installed as a transitive dependency.

## The Compatibility Conversion Layer

Once the raw ESLint rule module is obtained, TSSLint transforms it into a native rule using **`convertRule`** from `@tsslint/compat-eslint`. Located at [[`packages/compat-eslint/index.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/compat-eslint/index.ts)](https://github.com/johnsoncodehk/tsslint/blob/master/packages/compat-eslint/index.ts#L7-L93), this wrapper:

- Provides an ESLint-compatible `RuleContext` object
- Translates ESLint's `report()` calls into TSSLint diagnostics
- Handles automatic fixes and suggestions using the ESTree parser
- Manages source code traversal for rules that use ESLint's AST utilities

The converter preserves the original rule's severity and options while adapting the execution environment to TSSLint's TypeScript-aware engine.

## Practical Implementation Example

To load ESLint rules in your TSSLint configuration:

```typescript
import { importESLintRules } from '@tsslint/config';
import type { TSSLint } from '@tsslint/types';

// Define standard ESLint ruleset
const eslintConfig = {
    'no-console': 'error',
    'react/jsx-uses-react': ['warn'],
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
};

// Convert to TSSLint rules
async function getTSSLintRules(): Promise<TSSLint.Rules> {
    return importESLintRules(eslintConfig);
}

// Use in TSSLint program
(async () => {
    const tsslintRules = await getTSSLintRules();
    
    const program = new TSSLint.Program({
        // Program configuration (tsconfig, files, etc.)
        rules: tsslintRules,
    });
    
    const diagnostics = program.lint();
    console.log(diagnostics);
})();

```

Under the hood, `'react/jsx-uses-react'` resolves to `eslint-plugin-react`, while `'@typescript-eslint/no-unused-vars'` resolves to `@typescript-eslint/eslint-plugin`. Core rules like `'no-console'` resolve directly to ESLint's built-in rule files.

## Summary

- **`importESLintRules`** in [`packages/config/lib/eslint.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/config/lib/eslint.ts) is the primary API for loading ESLint rules into TSSLint
- **`resolveRuleKey`** automatically handles three plugin prefix formats: core rules, scoped plugins (`@org/`), and shorthand plugins
- **`loadRule`** uses a caching mechanism for plugins and filesystem traversal for core rules
- **`@tsslint/compat-eslint`** provides the `convertRule` wrapper that bridges ESLint's API to TSSLint's TypeScript-native diagnostics
- Nested plugin paths like `plugin/sub/name` are supported through multiple resolution candidates

## Frequently Asked Questions

### How does TSSLint resolve scoped plugin names like `@typescript-eslint/no-unused-vars`?

TSSLint detects the `@` prefix in the rule name and constructs the package name as `@typescript-eslint/eslint-plugin` before loading the rule from that package's `rules` export. This follows the standard ESLint plugin naming convention for scoped packages.

### What is the difference between how TSSLint loads core ESLint rules versus plugin rules?

Core rules (those without a slash in the name) are loaded by traversing up the directory tree from `__dirname` to locate `node_modules/eslint/lib/rules/{ruleName}.js`. Plugin rules are loaded via `require()` or `import()` of the resolved npm package, with results cached to prevent duplicate loads.

### Why do I need to install `@tsslint/compat-eslint` separately?

The `importESLintRules` function dynamically imports `@tsslint/compat-eslint` to access the `convertRule` function. If this package is not installed, the function throws an explicit error requesting installation. This keeps the core configuration package lightweight while allowing optional ESLint compatibility.

### Can `importESLintRules` handle nested plugin rule paths like `plugin/sub/name`?

Yes. The `resolveRuleKey` generator yields multiple resolution candidates for nested paths. For `plugin/sub/name`, it first tries `eslint-plugin-plugin/sub/name`, then falls back to `eslint-plugin-plugin-sub/name`, ensuring compatibility with plugins that use nested rule organization.