npm eslint-plugin-import-x 4.18.0
v4.18.0

one hour ago

Minor Changes

  • #520 802441c Thanks @SwastikTripathi! - Add a checkSelfReference option to the extensions rule. When set, a package's import of its own subpath export (e.g. pkg/sub from inside pkg) is treated as a package import, so ignorePackages applies to it.

  • #403 f4d0a4b Thanks @Zamiell! - extensions: when an import has no extension and cannot be resolved as-is, try the extensions configured as always in pattern and use the unique one that resolves for the fix or suggestion. If several resolve, report the missing extension without guessing. When no extension can be determined automatically, explain that in the missing message instead of only reporting a bare missing extension.

  • #572 fc17206 Thanks @SwastikTripathi! - feat(no-duplicates): add a whitelist option listing entry points that share one declaration file but are different modules, such as svelte and svelte/easing

  • #504 f10dcc4 Thanks @morgan-coded! - Add a "top" mode to the order rule's warnOnUnassignedImports option for requiring no-specifier ESM imports before assigned imports.

  • #502 9c85ced Thanks @SukkaW! - Rewrite the module analysis core for performance boosts. The speedup depends on your project, and we expect no performance regression in any setup — if you measure one, please report it.

    Rules like named, default, namespace, no-cycle, no-deprecated, and many more, need to know what the modules you import actually export. Until now, answering that meant running the full ESLint parser over every one of those files — including everything you pull in from node_modules.

    That core has been rewritten:

    • Plain JavaScript is now read with es-module-lexer instead of being parsed. Anything it can't read faithfully — TypeScript, JSX, Flow — still goes through the ESLint parser exactly as before.
    • Analysis is lazy. Much of the work now happens when a rule actually asks for it, so enabling one rule no longer pays for what the others would have needed.
    • Results are cached and shared between rules.

    There are no breaking changes: no configuration changes are needed, and no behavior changes are intended. Every rule should report exactly what it reported before, with one known exception: an imported plain JavaScript file that has a syntax error no longer gets a "Parse errors in imported module" report. ESLint still reports the syntax error when it lints that file itself. The full test suite passes, but there could be some edge cases. If a rule starts reporting something it didn't before, or stops reporting something it should, please open an issue with a minimum reproduction — bug reports on this are very welcome.

  • #456 7b78aae Thanks @43081j! - Add four barrel file detection rules ported from eslint-plugin-barrel-files: avoid-barrel-files, avoid-importing-barrel-files, avoid-namespace-import and avoid-re-export-all.

    avoid-importing-barrel-files checks every runtime way a module can be referenced — static imports, re-exports (export … from, export * from, export * as ns from), dynamic import() and CommonJS require() — with type-only statements exempt.

  • #157 89cfe42 Thanks @Zamiell! - feat(no-rename-default): accept names derived from bare package paths and add a whitelist option for modules that mangle their default export names

Patch Changes

  • #562 7cbe683 Thanks @JounQin! - perf(avoid-importing-barrel-files): skip the surface parse for a dependency that cannot be a barrel

    The rule sizes the runtime module graph behind every imported target, and a
    target only becomes a candidate once its export count passes
    amountOfExportsToConsiderModuleAsBarrel. A lexer-analyzed module already
    reports its export entries, so a count at or below the threshold settles the
    question without producing the exact export/declaration surface — one parse
    saved per small plain-JS dependency.

    Anything es-module-lexer does not read faithfully still goes through the parser,
    and a module whose source declares a default export is never ruled out: the
    properties of export default { … } are exports no entry count can see (a
    default that esModuleInterop synthesizes does not count).

    A declarator that binds no name — export const {} = obj, export const { a: {} } = obj — is no longer counted as an export by the barrel surface: it
    exports nothing, and counting it made the surface disagree with the lexer's
    entry count.

  • #568 225a5aa Thanks @SwastikTripathi! - Fix no-extraneous-dependencies telling you to add a package that is already in devDependencies or optionalDependencies when its import resolves through @types/*

  • #579 db16385 Thanks @thekhegay! - fix(core): keep the size of settings out of module and resolver cache keys

    Every key held the full stableHash text of settings, parserOptions and the parser. One large value in shared settings, like a custom alphabet from eslint-plugin-perfectionist, was copied into each key on every resolve, so no-cycle and any other rule that resolves imports could run out of memory. Keys now hold a short id per distinct hash.

  • #309 4b9ea02 Thanks @remcohaszing! - Allow packages to import themselves in import-x/no-extraneous-dependencies when the exports field is defined in package.json; importing a package's own name without exports is reported by the new selfImport message.

  • #565 3d0f5a9 Thanks @SwastikTripathi! - fix(namespace, no-deprecated): don't report members of a parameter that shadows a namespace import when the namespace is also used in the same scope, e.g. in a type annotation

  • #539 bb7b18b Thanks @SwastikTripathi! - chore(deps): upgrade es-module-lexer from v2 to v3. The v3 API is a complete redesign using discriminated union types (StaticImport | DynamicImport | ImportMetaRef for imports, DirectExport | Reexport | ReexportAll for exports) replacing numeric d fields and short property names. The lexer route now resolves export { x } where x comes from a non-namespace import as a reexport rather than an own export, which is more semantically accurate.

  • #566 4c66c87 Thanks @SwastikTripathi! - fix(export): don't report a namespace that only declares types as a duplicate of a value with the same name

  • #536 db674b5 Thanks @SukkaW! - fix(export): check types and values separately

    export type * from only re-exports types, so it no longer conflicts with a value export of the same name, and a type reached through export * no longer conflicts with a value. Duplicate types among re-exports are now reported, as are a type-only export and a value export of the same name, and ambient function overloads that clash with another export. String-literal export names such as "type:Foo" are no longer mistaken for type exports. Fixes #414.

  • #594 0c82c7e Thanks @i1stG! - fix(extensions): resolve directory imports to their index file in autofixes and suggestions, and only offer rewrites of resolved imports that point back to the same file.

  • #571 2bc321d Thanks @TonyTech404! - Fix the extensions rule treating an extension alias as a missing extension. An import such as ./foo.js that the resolver maps to foo.ts now keeps its own extension instead of being reported as missing ts and autofixed to ./foo.js.ts. The pattern extension inference also recognizes an extension alias, so an extensionless ./foo that resolves that way is fixed to ./foo.js. The inference only accepts a candidate that resolves to the same file, and an ambiguous result is reported without a fix. Type-only imports and exports (including inline type specifiers) are ignored before the extension branches, so an aliased .js specifier is preserved when checkTypeImports is off.

  • #512 42dba04 Thanks @LeulTew! - fix(extensions): honor pathGroupOverrides when no pattern, ignorePackages, or checkTypeImports option is provided.

  • #575 27112d6 Thanks @SwastikTripathi! - fix: keep trailing comments and line-scoped directives with their targets. first inserts moved imports right after the directive/statement, before any trailing comment, and withholds the autofix for the whole group when a moved import has comments. newline-after-import inserts the blank before a single-line trailing comment (or after a multi-line one), so third-party directives such as prettier-ignore keep their target. Both rules share the getTrailingComments util.

  • #555 27876f2 Thanks @JounQin! - perf(core): memoize getExportKind per module and name

    Rules ask for an export kind once per imported specifier and once per
    namespace member access. Without a memo, every lookup re-walks all export *
    paths and re-resolves each target; on a 100-star barrel, 2,000 namespace
    member accesses took ~5.7 s instead of ~0.1 s. Answers are cached per
    (module, name, typeOnly), so the type-only and value answers stay
    independent.

  • #519 f6a66a0 Thanks @Cherry! - perf: cache legacy resolver lookups per directory instead of re-resolving the resolver package on every import resolution.

  • #570 7254048 Thanks @devon-scythe! - fix: support minimatch v9/v10/v11 and drop makeRe

    minimatch v11 is the only release line with the fix for GHSA-g3hv-75wg-m249.
    It removes makeRe(), so no-internal-modules now matches its allow and
    forbid globs with Minimatch#match.

  • #535 66632ec Thanks @SwastikTripathi! - fix(core): send a .js file to the AST route when es-module-lexer cannot read one of its exports. On the lexer fast path, an export the lexer does not read was dropped from the module's export set as soon as the file had any export it did read, so hasExport/getExportNames — and named, namespace, default, no-named-as-default with them — reported the name as missing. The lexer route now keeps only the declaration heads whose exported names are read faithfully by es-module-lexer v3 (default, {, *, function, class, const, let, var) and bails on every unrecognised head as a conservative fallback, including module, using, await using, @dec class, abstract class, declare, enum, const enum, namespace, and interface (v3 reads those, but they are not yet in the whitelist).

  • #534 595f2d7 Thanks @SwastikTripathi! - fix(module-visitor): treat import x = require('y') as a module reference, so every rule built on moduleVisitor (no-unresolved, no-cycle, extensions, no-extraneous-dependencies, no-restricted-paths, no-absolute-path, …) sees the specifier. export import x = require('y') was missed too, because its ExportNamedDeclaration has a null source. no-cycle and avoid-importing-barrel-files keep ignoring the type-only form, import type x = require('y')

  • #556 19ff6c5 Thanks @SwastikTripathi! - fix(namespace): report member access in decorator expressions by walking up to the enclosing module scope.

  • #567 b2afa70 Thanks @SwastikTripathi! - fix(namespace): don't report a heritage clause that names a type from a module resolved to JavaScript, which has no types to check against.

  • #526 8f4f6b9 Thanks @JounQin! - fix: count export * as ns from '…' as a runtime dependency in the module analysis core

    export * as ns from 'x' loads x at runtime, but both ModuleInfo construction paths omitted the edge, so graph traversal — and avoid-importing-barrel-files in particular — could undercount the modules an import pulls in. The namespace export metadata is unchanged, and export type * as ns from stays type-only. Fixes #525.

  • #541 bbd0026 Thanks @MFA-G! - fix(namespace): report members exported only as types

    A name exported only as a type (export type Foo, export interface Foo, export type { Foo }, export type * from) is erased at runtime, so accessing it as a member of an imported namespace (ns.Foo, const { Foo } = ns, <ns.Foo />) is now reported. Classes, enums and names that are both a type and a value are still accepted, and unresolvable names still fail open. Fixes #540.

  • #543 35fc9bf Thanks @thekhegay! - fix(newline-after-import): ignore comments between two imports when considerComments is enabled, matching eslint-plugin-import.

  • #577 5169813 Thanks @SwastikTripathi! - fix(newline-after-import): treat a comment on the import's own line as part of the import

    With considerComments, a trailing comment such as import a from 'a' // why
    was taken as the comment after the import, so the rule reported even with an
    empty line before the next statement. The autofix also inserted the new lines
    between the import and its trailing comment, moving the comment onto its own
    line. Both now skip comments that start on the statement's last line.

    The fix is withheld when any trailing comment is a line-scoped directive
    (eslint-disable-next-line, @ts-ignore, @ts-expect-error), so the directive
    keeps its target, and comments inside the following statement no longer satisfy
    the blank-line check.

  • #591 65ea15c Thanks @SwastikTripathi! - fix(no-absolute-path): prefix ./ when the fixed path starts with a dot directory such as .config/, which would otherwise turn into a package import

  • #507 aa1071d Thanks @BridgeAR! - Reduce no-cycle CPU usage for converging dependency graphs.

  • #518 c45cfb9 Thanks @wimvanschandevijl! - fix(no-cycle): allowUnsafeDynamicCyclicDependency no longer hides static-only cycles that happen to sit in the same module as an unrelated dynamic import

    Previously, no-cycle's traversal bailed out of a module's entire remaining import list as soon as it found one path where any declaration was dynamic - return rather than continue inside the loop over m.imports. That meant:

    • a purely static cycle declared after an unrelated dynamic import earlier in the same file went unreported, and
    • a path imported both statically and dynamically (the static declaration on its own closing a real cycle) was skipped entirely, because "any declaration to this path is dynamic" discarded the static declaration too.

    Both are now handled per-declaration: only the dynamic declarations for a path are dropped when allowUnsafeDynamicCyclicDependency is set, and only that one path is skipped when nothing traversable remains - the module's other, unrelated import paths (and any static declaration to the very same path) are still checked. See #515 for the repro that found this.

  • #538 b6a674b Thanks @b84330808! - fix(no-cycle): report cycles through side-effect imports such as import './foo' and import {} from './foo'. They have no specifiers, so they were skipped as if they were type-only imports. The unambiguous-module prefilter also recognizes them now, so a file whose only module syntax is a side-effect import is analyzed on the AST route too, as it already was on the lexer route

  • #585 8777312 Thanks @thekhegay! - fix(no-duplicates): do not treat commas inside comments as specifier separators in the autofix

  • #581 76e97db Thanks @thekhegay! - fix(no-duplicates): imports of one path with different attributes are not duplicates, and the autofix no longer drops an attribute, mistakes the braces of with {...} for the braces of the specifiers, or takes a resolution-mode import out of import type

  • #584 85c1b88 Thanks @thekhegay! - fix(no-duplicates): remove extra whitespace before the closing brace when merging import specifiers

  • #522 56b1c48 Thanks @giaBaoJS! - fix(no-duplicates): the autofix no longer drops x when merging import { x } into an import of x as y, and prefer-inline no longer turns the from keyword or an alias into type ... when the first import is import type { ... }

  • #546 1eb95b0 Thanks @thekhegay! - fix(no-duplicates): report a module imported twice with import type * as, as it already does for import * as and import type x

  • #548 d565b2f Thanks @thekhegay! - fix(no-duplicates): without prefer-inline, the autofix moves inline type specifiers into the import type { ... } of the same module and leaves the value specifiers in their own import, instead of merging both into one import that is invalid or changes which specifiers are type-only.

  • #574 2fd859d Thanks @SwastikTripathi! - fix(no-empty-named-blocks): use the reported import's tokens in the side-effect suggestion

    The suggestion looked up import and from in the whole file, so an earlier import with a from clause, like import a from 'a', supplied the tokens and the suggestion left the code unchanged. Earlier side-effect imports have no from and weren't affected.

  • #547 3598430 Thanks @thekhegay! - fix(no-extraneous-dependencies): report the runtime package instead of its @types/* package when an import resolves to type definitions, matching eslint-plugin-import. A @types/* package in dependencies no longer counts as a declaration of its runtime package, and whitelist accepts either name. A type-only import checked with includeTypes still needs only the @types/* package.

  • #571 2bc321d Thanks @TonyTech404! - fix(no-extraneous-dependencies): ignore inline type-only export specifiers when verifyTypeImports is off, as inline type imports already are

  • #580 059ca92 Thanks @thekhegay! - fix: ignore type-only named exports for no-named-as-default and no-named-as-default-member

  • #573 a3553f3 Thanks @SwastikTripathi! - fix(no-namespace): don't autofix computed member access on the namespace

    foo[key] was fixed as if it were foo.key, importing an export named key and dropping the lookup, list[foo] was rewritten the same way, and foo[1] produced import { 1 }, which doesn't parse. All of them keep the namespace import now, and the rule still reports them.

  • #561 981ada8 Thanks @wleeaf! - fix(no-namespace): check all enclosing scopes for name conflicts in the autofix

    The autofix only looked at the innermost scope of each ns.name reference and its parent for names that would shadow the new import, so a variable declared further out (or an undeclared global used elsewhere in the module) changed what the rewritten reference pointed to. It also turned ns.default and ns['some-key'] into named imports that aren't valid syntax; those are now reported without a fix.

  • #582 47c5419 Thanks @thekhegay! - fix(no-namespace): don't crash or write broken code on members named like Object.prototype ones, e.g. _.toString

  • #583 2538093 Thanks @thekhegay! - fix: don't crash with The "path" argument must be of type string when there is no package.json above the linted file. The directory ESLint runs in stands in for the root of the package, and no-extraneous-dependencies without packageDir lints nothing, as its documentation says

  • #560 4afef66 Thanks @wleeaf! - fix(no-relative-packages): keep the imported path when suggesting the package import

    The suggested package import was built from the directory of the resolved file plus the last segment of the import path. An extensionless import of a file named like its package directory (../bar/src/bar) became bar/src, which resolves to another module, and a directory import (../bar/src) became bar/src/src. The suffix is now taken from the resolved file below the package root: explicit import extensions are preserved, extensionless imports omit the resolved extension, directory index files collapse back to the directory, and .d.ts targets drop the .d marker. If the package name itself resolves to the original module, it is preferred (for example ../node_modules/nested-main becomes nested-main). The autofix is only offered when the suggested package path resolves back to the original module; otherwise the rule reports without suggesting that path. Packages reached through symlinks (pnpm, workspaces) therefore keep their package name and path.

  • #524 2384a48 Thanks @giaBaoJS! - fix(no-unused-modules): missingExports no longer reports "No exports found" for a file whose only exports are export * from declarations, and unusedExports now checks export * as ns from as the export ns and counts its source module as namespace imported

  • #533 6ffa22e Thanks @giaBaoJS! - fix(no-unused-modules): exports named ImportDefaultSpecifier, ImportNamespaceSpecifier or ExportAllDeclaration no longer collide with the rule's internal bookkeeping keys, and import { default as x } now counts as a use of the default export

  • #593 9144c94 Thanks @thekhegay! - fix(order): move all out of order imports of a block in one fix, so one --fix is enough for a block with more than 10 of them

  • #544 1a2bf7a Thanks @thekhegay! - fix(order): compare statement indexes as numbers, so the autofix no longer moves an import across a non-import statement when the indexes sort differently as strings, such as 2 and 10.

  • #517 d6808bd Thanks @Cherry! - fix(order): make the alphabetize comparator transitive, so parent imports now consistently sort before sibling imports in the same group (matching eslint-plugin-import) and results no longer depend on the Node version

  • #578 57729ae Thanks @SwastikTripathi! - fix(prefer-namespace-import): keep a namespace specifier when splitting the default import

    The autofix looked for { and } to find the other specifiers, so
    import Foo, * as Bar from 'foo' was rewritten into code that does not parse.
    It now takes the text between the comma after the default specifier and from,
    which also covers * as Bar.

  • #564 ba0b9f6 Thanks @SwastikTripathi! - fix(core): declare namespace foo.bar.baz {} behind export = foo now only adds bar to the module's exports. baz and the block's members were previously reported as direct exports, so named accepted import { baz } and import { deep }, and a qualified declare module foo.qux {} exposed its members too.

  • #482 53a88f3 Thanks @B4nan! - Merge dependencies from the closest package.json in addition to packageDir in no-extraneous-dependencies, so workspace packages see the dependencies declared in their own manifest; bundledDependencies from every merged manifest are unioned instead of overwritten.

  • #492 9669829 Thanks @andersk! - Ignore TypeScript namespace members for no-named-as-default, no-named-as-default-member rules

  • #590 8c11eb7 Thanks @SwastikTripathi! - fix(consistent-type-specifier-style): keep the quotes of a string specifier name, like type "a-b" as AB, when the prefer-top-level fix moves it into a new import type

New Contributors

Full Changelog: v4.17.1...v4.18.0

Don't miss a new eslint-plugin-import-x release

NewReleases is sending notifications on new releases.