Image generated by FLUX.1 [dev] with prompt “clean retro illustration of a NES game controller with a single cable stretching from it to behind a CRT TV screen with an 8-bit version of the poop emoji on the TV screen”
Making the ESLint Import Plugin Work with Yarn PnP
How to resolve incorrect errors from the import/order rule when using Yarn’s Plug'n'Play feature
When collaborating on larger JavaScript or TypeScript projects, the import preambles can become quite long and unmaintainable if conventions are not adopted and enforced. Random import ordering makes code harder to read and update and leads to duplicate imports, as well as opening the door for a lot more git diff noise from differing opinions about import ordering from members of your team. The eslint-plugin-import
’s import/order
rule is a powerful tool to help control import statement entropy, and on projects where I’ve configured it, completely eliminates that class of problems. I wouldn’t want to try to maintain a large codebase without it, in a similar way to how I wouldn’t consider working on a project without Prettier (or equivalent code formatter).
Update February 4, 2025: Since writing this, I started using the excellent Perfectionist plugin in place of the
import/order
rule, as I describe here.
Recently, I upgraded one of my projects to use Yarn’s Plug’n’Play (PnP) feature, which helps eliminate phantom dependencies and makes package management more reliable. However, this broke my setup, with the import/order rule not working anymore, making the lint step fail across the entire codebase.
If you’ve run into this same issue, or you’re considering using Yarn PnP and want to avoid this headache, here’s how to fix it.
The Setup
My project uses ESLint v9 with its new flat config system, which means my configuration lives in eslint.config.js
. Since all of my linted code is TypeScript, I set up the import ordering rules in the TypeScript section of my config:
import jsPlugin from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import importPlugin from 'eslint-plugin-import';
export default [
jsPlugin.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
plugins: {
'@typescript-eslint': tsPlugin,
import: importPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: { projectService: true },
},
settings: {
'import/resolver': { typescript: { alwaysTryTypes: true } },
},
rules: {
...tsPlugin.configs.recommended.rules,
...tsPlugin.configs.stylistic.rules,
...importPlugin.configs.recommended.rules,
...importPlugin.configs.typescript.rules,
'import/order': [
'error',
{
alphabetize: { caseInsensitive: true, order: 'asc' },
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
],
'newlines-between': 'always',
},
],
},
},
];
This configuration ensures that imports are consistently grouped and alphabetized: runtime (node.js/bun/workerd/…) builtins first, followed by external packages, internal project imports, and finally relative imports (parent and sibling files). Each group is separated by a single newline for readability.
The setup also uses eslint-import-resolver-typescript
to handle TypeScript-specific import resolution, including path aliases and type imports. This works when using npm or yarn classic (v1) or even yarn v2+ with the nodeLinker: node-modules
setting in my .yarnrc.yml
which I used initially for better compatibility. Promisingly, the typescript resolver also supports PnP.
Recently, I ran into some issues with my build and realized I had ghost dependencies from some code I copy-pasted from the lexical rich text editor playground. Those transitive dependencies worked in the build, but were causing some headaches with vite dev’s bundle optimization and, more generally, are unstable and best avoided. So I decided to enable Yarn PnP by setting nodeLinker: pnp
in my .yarnrc.yml
.
The Problem
After enabling PnP, running ESLint started producing a flood of errors about import ordering across my codebase. Here’s a representative sample:
/app/routes/users._index.tsx
1:1 error There should be at least one empty line between import groups import/order
2:1 error There should be no empty line within import group import/order
/app/utils/content.tsx
1:1 error There should be at least one empty line between import groups import/order
1:1 error `markdown-to-jsx` import should occur after import of `react` import/order
3:1 error There should be at least one empty line between import groups import/order
11:1 error There should be no empty line within import group import/order
/app/types.ts
5:1 error There should be at least one empty line between import groups import/order
6:1 error `zod` import should occur before type import of `react-router` import/order
These errors appeared regardless of how the imports were actually organized, and confusingly, they didn’t indicate anything about what was actually wrong; they complained about missing newlines between groups when the newlines were there, suggested reordering imports that were already in the correct order, and generally seemed to misinterpret the entire import structure of each file.
Why did this happen? The issue stems from the inability of import plugin to resolve external modules under Yarn PnP. When PnP is enabled, external dependencies are declared in a single file and can no longer be resolved using the default npm-style node_modules-based resolution algorithm. The plugin is unable to differentiate the different types of imports and starts reporting all kinds of erroneous errors.
The Solution
The fix is dead simple. You just need to tell eslint-plugin-import
where to find your external dependencies by adding the .yarn
directory to the import/external-module-folders
setting in your eslint config:
import jsPlugin from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import importPlugin from 'eslint-plugin-import';
export default [
jsPlugin.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
plugins: {
'@typescript-eslint': tsPlugin,
import: importPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: { projectService: true },
},
settings: {
'import/resolver': { typescript: { alwaysTryTypes: true } },
'import/external-module-folders': ['.yarn'], // ← The magic line
},
rules: {
// ... rest of the configuration stays the same
},
},
];
That one additional line tells the plugin where to find external dependencies when running under Yarn PnP. With this change, eslint-plugin-import can correctly distinguish between external dependencies (now in .yarn
) and your internal project modules, allowing the import/order
rule to properly enforce your import ordering conventions.