import/extensions

Enforce that import paths always include the file extension, or never do

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:

bunfig.toml
[lint.rules]
"import/extensions" = "error" # or "warn"

Every option, set to its default:

bunfig.toml
[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 };
}
import { createUser } from "./user";

Not reported:

export function createUser(name) {
  return { name };
}
import { createUser } from "./user.js";
import { join } from "node:path";
import { useState } from "react";

Options#

bunfig.toml
[lint.rules]
"import/extensions" = ["error", "never", { json = "always" }]

The first option sets the style for every extension:

OptionWhat 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 };
}
import { createUser } from "./user.js";
import map from "lodash/fp/map";
import map from "lodash/fp/map";

An object after it, or instead of it, holds exceptions and more settings:

OptionDefaultWhat it does
an extension, such as js or jsonthe 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.
ignorePackagesfalseDon't require an extension in paths into packages.
checkTypeImportsfalseAlso 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.
import { createUser } from "./user";
import settings from "./settings.json";
import type { User } from "./user";
import type { User } from "./user";
import { routes } from "~/generated/routes";
import { 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 };
}
import { createUser } from "./user.ts";
export function createUser(name: string) {
  return { name };
}
import { 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.