import/extensions
Enforce that import paths always include the file extension, or never do
Also called import-x/extensions
import "./user" and import "./user.js" can name the same file. Browsers and Node.js only accept the second in ES modules, while bundlers and Bun accept both. This rule keeps a project on one style, for all files or per extension.
It checks import, export ... from, import() and require(). Built-in modules and bare package names such as "react" or "@babel/core" are never checked.
Configure#
Off by default. To turn it on:
[lint.rules]
"import/extensions" = "error" # or "warn"Every option, set to its default:
[lint.rules]
"import/extensions" = [
"error",
"never", # "always", "ignorePackages", "never" or object
{
pattern = {},
ignorePackages = false,
checkTypeImports = false,
pathGroupOverrides = [], # list of objects
},
]Examples#
Reported:
export function createUser(name) {
return { name };
}"always"1 problemimport { createUser } from "./user"Missing file extension js for ./user;Not reported:
export function createUser(name) {
return { name };
}"always"No problemsimport { createUser } from "./user.js";
import { join } from "node:path";
import { useState } from "react";Options#
[lint.rules]
"import/extensions" = ["error", "never", { json = "always" }]The first option sets the style for every extension:
| Option | What it does |
|---|---|
"never" (default) | Report an extension that the path doesn't need, such as ./user.js where ./user finds the same file. |
"always" | Report a path without an extension. |
"ignorePackages" | Like "always", but paths into packages such as "lodash/fp/map" aren't checked. |
export function createUser(name) {
return { name };
}"never"1 problemimport { createUser } from "./user.js"Unexpected use of file extension js for ./user.js;"always"1 problemimport map from "lodash/fp/map"Missing file extension for lodash/fp/map;"ignorePackages"No problemsimport map from "lodash/fp/map";An object after it, or instead of it, holds exceptions and more settings:
| Option | Default | What it does |
|---|---|---|
an extension, such as js or json | the first option | "always" or "never" for files with that extension. |
pattern | {} | The same extension entries, in their own object. If you set ignorePackages or checkTypeImports, put the entries in here. Entries written next to those two options are ignored. |
ignorePackages | false | Don't require an extension in paths into packages. |
checkTypeImports | false | Also require extensions in import type and export type. |
pathGroupOverrides | [] | A list of { pattern, action }. A path that matches the glob pattern is skipped if action is "ignore". With "enforce" it's checked even if it looks like a package. It's ignored unless you also set pattern, ignorePackages or checkTypeImports. |
"never", { "json": "always" }No problemsimport { createUser } from "./user";
import settings from "./settings.json";"always"No problemsimport type { User } from "./user";"always", { "checkTypeImports": true }1 problemimport type { User } from "./user"Missing file extension for ./user;"ignorePackages"1 problemimport { routes } from "~/generated/routes"Missing file extension for ~/generated/routes;"always", { "ignorePackages": true, "pathGroupOverrides": [{ "pattern": "~/generated/**", "action": "ignore" }] }No problemsimport { routes } from "~/generated/routes";The rule looks up which file ./user stands for, the way Bun and TypeScript do, so it knows that it's ./user.ts:
export function createUser(name: string) {
return { name };
}"ignorePackages", { "ts": "never" }1 problemimport { createUser } from "./user.ts"Unexpected use of file extension ts for ./user.ts;export function createUser(name: string) {
return { name };
}"ignorePackages", { "ts": "never" }No problemsimport { createUser } from "./user";With an ESLint config it only tries .mjs, .js, .json and .node, like Node.js does, unless settings["import/resolver"] says otherwise.