Why Import Order Matters
In a monorepo with multiple path aliases (@/ for app internals,
@danieljoffe.com/* for library packages), imports can land in any order, and
over time that creates a subtle problem: you can't glance at the top of a file
and immediately tell which dependency layer each import belongs to.
Compare these two versions of the same imports:
// Before: no ordering — mental effort to parse
import { analytics } from '@/lib/analytics';
import Button from '@/components/Button';
import { useState } from 'react';
import { Badge } from '@danieljoffe/shared-ui';
import type { Metadata } from 'next';// After: layered from external → internal → local
import type { Metadata } from 'next';
import { useState } from 'react';
import { Badge } from '@danieljoffe/shared-ui';
import { analytics } from '@/lib/analytics';
import Button from '@/components/Button';The second version reads in layers: framework deps first, then library deps, then app internals, so you know at a glance where each import comes from.
The Configuration
I use eslint-plugin-import's import/order rule with custom pathGroups
to handle the monorepo aliases:
'import/order': [
'error',
{
groups: [
'builtin', // node:fs, path
'external', // react, next, lucide-react
'internal', // @/ aliases
'parent', // ../
'sibling', // ./
'index', // .
],
pathGroups: [
{
pattern: '@danieljoffe.com/**',
group: 'external',
position: 'after',
},
{
pattern: '@/**',
group: 'internal',
position: 'before',
},
],
pathGroupsExcludedImportTypes: ['builtin'],
'newlines-between': 'never',
},
],The pathGroups config places @danieljoffe.com/* imports after the
other external packages but before @/ app-internal imports, which gives me
a clear hierarchy: node → npm packages → monorepo libraries → app code
→ local files.
I leave out alphabetize on purpose, since forcing alphabetical order within
groups adds friction without buying much readability.
The ESLint 10 Compatibility Problem
Running the rule immediately crashed:
TypeError: sourceCode.getTokenOrCommentBefore is not a function
Rule: "import/order"
eslint-plugin-import 2.32.0 uses getTokenOrCommentBefore, an API that
ESLint 10 removed. The Next.js ESLint config registers the plugin without the
compatibility shim, so it worked for some rules but blew up on import/order.
The fix is to strip the import plugin out of Next.js's config and re-register it
with fixupPluginRules, the same pattern already used for eslint-plugin-react:
const nextConfigs = nextCoreWebVitals.map(cfg => {
if (!cfg.plugins) return cfg;
const { import: _import, react: _react, ...keep } = cfg.plugins;
return {
...cfg,
plugins: {
...keep,
react: fixupPluginRules(reactPlugin),
import: fixupPluginRules(importPlugin),
},
};
});Fixing 77 Violations
Once the config was stable, the first lint run turned up 77 errors, and 66 of
them were auto-fixable with --fix since the rule reorders import statements for you.
The remaining 11 were all in test files, with the same pattern:
import { render } from '@testing-library/react';
// jest.mock sits here, creating a blank line
jest.mock('@/lib/analytics');
import CalendlyButton from './CalendlyButton';
import { analytics } from '@/lib/analytics';The fix was the same each time: move all the imports to the top, grouped
together, and put the jest.mock() calls after them. Jest hoists mock calls
regardless of where they sit, so this is purely a readability win:
import { render } from '@testing-library/react';
import { analytics } from '@/lib/analytics';
import CalendlyButton from './CalendlyButton';
jest.mock('@/lib/analytics');Circular Dependency Prevention
Alongside import/order, I turned on import/no-cycle as an error, so
circular imports get caught at lint time instead of at runtime, where they
surface as mysterious undefined values.
The shared-ui library already had this rule, so extending it to the app means the whole monorepo is covered.
Results
- 77 violations fixed in a single commit (66 auto-fixed, 11 manual)
- Zero false positives after stabilizing the ESLint 10 compat
- Import order is now enforced on every commit via pre-commit hooks
- Circular dependencies are caught at lint time across the full monorepo
