Skip to content

feat!: rewrite as ECMAScript modules - #308

Merged
alexander-akait merged 1 commit into
mainfrom
refactor/esm
Sep 7, 2026
Merged

feat!: rewrite as ECMAScript modules#308
alexander-akait merged 1 commit into
mainfrom
refactor/esm

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

src/ becomes ECMAScript modules, the package declares "type": "module" and an exports field, and babel emits an ESM build beside a CommonJS one — so import LintPlugin from "lint-webpack-plugin" and require("lint-webpack-plugin") both keep working. The layout follows less-loader: dist/esm and dist/cjs, each stamped with its own package.json, main pointing at the CommonJS entry for old resolvers.

"exports": {
  ".": {
    "types": "./types/index.d.ts",
    "import": "./dist/esm/index.js",
    "require": "./dist/cjs/index.js",
    "default": "./dist/esm/index.js"
  },
  "./package.json": "./package.json"
}

Four things the rewrite forced, each worth a reviewer's attention because none of them is obvious from the diff:

  • "type": "module" decides how every .js in the repository is parsed, not just src/. The CommonJS test fixtures and mocks stopped being CommonJS, so webpack built no dependency for their require(...) and the plugin silently linted only entry files. Those directories now carry a package.json naming themselves commonjs.
  • The stylelint worker entry stays CommonJS, as stylelint-worker.cjs. jest declines to require an ES module below node 24.9 — which is what jest-worker does when it loads a worker in band — and a worker entry has no reason to be anything else. Node itself loads the ESM version fine; this is jest's limit, verified by driving jest-worker against it outside jest.
  • eslintPath and stylelintPath may name a directory or a CommonJS entry, neither of which ESM resolution finds. importFrom resolves those through CommonJS first, and imports a bare package name as it is so a test can still mock it.
  • The schemas are read with createRequire rather than an import ... with { type: "json" } attribute, which eslint-plugin-import cannot yet parse.

What kind of change does this PR introduce?

feat (breaking).

Did you add tests for your changes?

No new tests — the existing suite is the check, and it drove the whole migration. 124 passing, 2 skipped (the ESLint 10 eslintrc suites, as on main). Three tests changed shape rather than intent: error and eslint-lint mocked the eslint module through jest.mock, which ESM does not support, and now use the plugin's own eslintPath with a mock — the same pattern the stylelint tests already use. utils keeps module mocking through jest.unstable_mockModule.

Beyond the suite I verified both published entry points load (import from dist/esm, require from dist/cjs) and that the built worker keeps its .cjs extension in both builds.

Does this PR introduce a breaking change?

Yes. The package is "type": "module" with an exports field, so deep imports into it are no longer reachable — the plugin and ./package.json are what it exports. Consumers importing or requiring the package itself are unaffected.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

The README's setup example now leads with import and notes that a CommonJS configuration works too. A changeset is included marking this a major.

Use of AI

Written with Claude Code, driven interactively. I asked for the rewrite; Claude did the conversion and worked the test suite back to green, reporting the four constraints above as it hit them. I reviewed the result.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy


Generated by Claude Code

`src/` is ECMAScript modules, the package declares `"type": "module"` and an
`exports` field, and babel emits an ESM build beside a CommonJS one, so
`import LintPlugin from "lint-webpack-plugin"` and `require("lint-webpack-plugin")`
both keep working.

Four things the rewrite forced, each the smallest fix that holds:

`"type": "module"` decides how every `.js` in the repository is parsed, so the
CommonJS test fixtures and mocks stopped being CommonJS and webpack quietly
built no dependency for their `require`. Each of those directories now carries
a `package.json` naming itself `commonjs`.

The stylelint worker entry stays CommonJS as `stylelint-worker.cjs`. jest
declines to `require` an ES module below node 24.9, which is what jest-worker
does when it loads a worker in band, and a worker entry has no reason to be
anything else.

`eslintPath` and `stylelintPath` may name a directory or a CommonJS entry, which
ESM resolution does not find. `importFrom` resolves those through CommonJS
first, and imports a bare package name as it is so that it can still be mocked.

The schemas are read with `createRequire` rather than an import attribute, which
`eslint-plugin-import` cannot yet parse.

BREAKING CHANGE: the package is now `"type": "module"` with an `exports` field.
Deep imports into the package are no longer reachable; the plugin and
`./package.json` are what it exports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedbabel-plugin-transform-import-meta@​2.3.31001009880100

View full report

@alexander-akait
alexander-akait merged commit 70aba50 into main Sep 7, 2026
25 checks passed
@alexander-akait
alexander-akait deleted the refactor/esm branch September 7, 2026 13:00
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.

1 participant