Skip to content

eslint-plugin-next: add ESM named exports via conditional exports field#94132

Open
12122J wants to merge 2 commits into
vercel:canaryfrom
12122J:fix/eslint-plugin-next-esm-named-exports
Open

eslint-plugin-next: add ESM named exports via conditional exports field#94132
12122J wants to merge 2 commits into
vercel:canaryfrom
12122J:fix/eslint-plugin-next-esm-named-exports

Conversation

@12122J
Copy link
Copy Markdown

@12122J 12122J commented May 26, 2026

What?

Add a dual CJS/ESM export setup to @next/eslint-plugin-next so that named ESM imports like import { configs } from '@next/eslint-plugin-next' work correctly.

Fixes #86504.

Why?

SWC compiles TypeScript named exports to CJS using Object.defineProperty() with getter functions:

Object.defineProperty(exports, "configs", { enumerable: true, get: () => configs });

Node.js only creates synthetic ESM named exports from own data properties on module.exports — not accessor (getter) properties. So importing the package via ESM named imports throws:

SyntaxError: Named export 'configs' not found. The requested module
'@next/eslint-plugin-next' is a CommonJS module, which may not support
all module.exports as named exports.

How?

Added a dual-export setup via the package.json "exports" field:

  • "require" condition → existing dist/index.js (CJS, no change)
  • "import" condition → new dist/index.mjs, a thin ESM wrapper that imports the CJS default and explicitly re-exports the named bindings as data properties:
import cjs from './index.js'
export const { rules, configs } = cjs
export default cjs

dist/index.mjs is generated by scripts/generate-esm.cjs which runs as part of the existing build script after SWC compilation.

After this change:

// ✅ works — ESM named import
import { configs } from '@next/eslint-plugin-next'

// ✅ works — CJS require (unchanged)
const { configs } = require('@next/eslint-plugin-next')

// ✅ works — default import (unchanged)
import plugin from '@next/eslint-plugin-next'

Comment thread packages/eslint-plugin-next/package.json Outdated
@12122J 12122J marked this pull request as ready for review May 26, 2026 18:00
12122J and others added 2 commits May 26, 2026 20:07
SWC compiles TypeScript named exports to CJS getter-based properties using
Object.defineProperty(). Node.js only creates synthetic ESM named exports
from own *data* properties on module.exports, ignoring getters. This means
`import { configs } from '@next/eslint-plugin-next'` throws:

  SyntaxError: Named export 'configs' not found.

Fix: add a dual-export setup using the package.json "exports" field.
- "require" (CJS callers): existing dist/index.js, unchanged.
- "import" (ESM callers): new dist/index.mjs, a thin wrapper that imports
  the CJS default and re-exports the named bindings as data properties.

The dist/index.mjs file is generated by scripts/generate-esm.cjs which runs
as part of the build step after SWC compilation.
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
@12122J 12122J force-pushed the fix/eslint-plugin-next-esm-named-exports branch from 0e94f72 to e17f8f2 Compare May 26, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@next/eslint-plugin-next incorrect named exports

1 participant