How to Load ESLint Rules in TSSLint: Using importESLintRules with Plugin Prefix Resolution
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 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/master/packages/config/lib/eslint.ts#L41-L86), it accepts a configuration object mapping rule names to severity levels or options arrays.
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/nameformat (e.g.,@typescript-eslint/no-unused-vars) — resolves to@org/eslint-plugin - Shorthand plugins:
plugin/nameformat (e.g.,react/jsx-uses-react) — automatically prependseslint-plugin-prefix
The resolveRuleKey Implementation
The resolveRuleKey generator yields one or two possible (pluginName?, ruleName) tuples, handling nested plugin paths:
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 function iterates over candidates from resolveRuleKey and returns the first successfully loaded module:
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 function handles the actual import using a cached loader promise:
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:
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/master/packages/compat-eslint/index.ts#L7-L93), this wrapper:
- Provides an ESLint-compatible
RuleContextobject - 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:
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
importESLintRulesinpackages/config/lib/eslint.tsis the primary API for loading ESLint rules into TSSLintresolveRuleKeyautomatically handles three plugin prefix formats: core rules, scoped plugins (@org/), and shorthand pluginsloadRuleuses a caching mechanism for plugins and filesystem traversal for core rules@tsslint/compat-eslintprovides theconvertRulewrapper that bridges ESLint's API to TSSLint's TypeScript-native diagnostics- Nested plugin paths like
plugin/sub/nameare 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →