Расширения файлов: extensions

Механизм разрешения модулей в Webpack позволяет импортировать файлы без указания полного имени файла. За это отвечает свойство resolve.extensions.

Без настройки:

import Button from './Button'

Webpack не сможет определить, какой именно файл требуется загрузить:

Button.js
Button.jsx
Button.ts
Button.vue
Button.json

Список расширений из resolve.extensions задаёт порядок, в котором Webpack будет искать подходящие файлы.

Базовая конфигурация:

module.exports = {
  resolve: {
    extensions: ['.js']
  }
}

Теперь импорт:

import sum from './math/sum'

будет автоматически преобразован в:

./math/sum.js

Как работает механизм поиска

При импорте:

import App from './App'

Webpack начинает последовательно проверять расширения:

extensions: ['.js', '.json']

Алгоритм:

./App.js
./App.json

Если файл найден — поиск прекращается.

Если файл отсутствует — Webpack переходит к следующему расширению.


Расширения по умолчанию

Webpack автоматически использует несколько встроенных расширений:

['.js', '.json', '.wasm']

Это означает, что даже без настройки можно импортировать:

import data from './data'

при наличии файла:

data.json

или:

data.js

Полная настройка списка расширений

Чаще всего конфигурация выглядит так:

module.exports = {
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx']
  }
}

Теперь Webpack поддерживает импорты:

import App from './App'

для файлов:

App.js
App.jsx
App.ts
App.tsx

Важность порядка расширений

Порядок расширений критически важен.

Пример:

extensions: ['.ts', '.js']

При импорте:

import api from './api'

Webpack сначала ищет:

api.ts

и только потом:

api.js

Если существуют оба файла:

api.ts
api.js

будет выбран именно api.ts.

Изменение порядка полностью меняет результат.


Проблемы неправильного порядка

Неверный порядок расширений может приводить к:

  • подключению не той версии модуля;
  • конфликтам между TypeScript и JavaScript;
  • случайной загрузке тестовых файлов;
  • неоднозначному поведению сборки.

Опасный пример:

extensions: ['.test.js', '.js']

Импорт:

import utils from './utils'

может неожиданно подключить:

utils.test.js

вместо:

utils.js

Использование ...

Webpack 5 поддерживает специальный оператор:

'...'

Он позволяет сохранить стандартные расширения Webpack.

Пример:

module.exports = {
  resolve: {
    extensions: ['.ts', '.tsx', '...']
  }
}

Webpack объединит:

['.ts', '.tsx', '.js', '.json', '.wasm']

Без ... стандартные расширения полностью заменяются.


Полная замена встроенных расширений

Конфигурация:

extensions: ['.ts']

отключает:

.js
.json
.wasm

Теперь импорт:

import data from './data'

не найдёт:

data.json

Поддержка TypeScript

Для TypeScript обычно используется:

resolve: {
  extensions: ['.ts', '.tsx', '.js']
}

или:

resolve: {
  extensions: ['.ts', '.tsx', '...']
}

Причины:

  • TypeScript-файлы должны иметь приоритет;
  • JavaScript остаётся совместимым;
  • сторонние библиотеки продолжают работать.

React и JSX

Для React-проектов:

resolve: {
  extensions: ['.js', '.jsx']
}

или:

resolve: {
  extensions: ['.tsx', '.ts', '.jsx', '.js']
}

Это позволяет писать:

import Header from './components/Header'

вместо:

import Header from './components/Header.jsx'

Vue и расширения .vue

Vue-компоненты обычно подключаются так:

resolve: {
  extensions: ['.js', '.vue']
}

Импорт:

import App from './App'

найдёт:

App.vue

Angular и расширения .ts

Angular-проекты почти всегда используют:

resolve: {
  extensions: ['.ts', '.js']
}

Поскольку большая часть кода написана на TypeScript, .ts располагается первым.


Svelte

Для Svelte:

resolve: {
  extensions: ['.mjs', '.js', '.svelte']
}

Поддержка ECMAScript Modules

Современные проекты часто используют:

resolve: {
  extensions: ['.mjs', '.js']
}

Файл .mjs обозначает полноценный ESM-модуль.


Работа с JSON

Webpack умеет импортировать JSON напрямую:

import config from './config'

Если присутствует:

config.json

Webpack автоматически загрузит JSON-файл.

Это работает благодаря расширению .json.


Импорт без расширения и читаемость

Импорт без расширения:

import api from './api'

выглядит компактнее.

Но при большом количестве одинаковых имён может возникать неоднозначность:

api.js
api.ts
api.mock.js
api.test.js

В крупных проектах иногда предпочитают явные расширения:

import api from './api.ts'

Производительность и количество расширений

Каждое расширение увеличивает количество проверок файловой системы.

Конфигурация:

extensions: [
  '.ts',
  '.tsx',
  '.js',
  '.jsx',
  '.json',
  '.vue',
  '.mjs'
]

заставляет Webpack выполнять множество проверок.

При импорте:

import App from './App'

Webpack может последовательно проверять:

App.ts
App.tsx
App.js
App.jsx
App.json
App.vue
App.mjs

На больших проектах это влияет на скорость сборки.


Оптимизация списка расширений

Хорошая практика — оставлять только реально используемые расширения.

Плохо:

extensions: [
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.vue',
  '.json',
  '.mjs',
  '.coffee'
]

если половина из них не используется.

Лучше:

extensions: ['.ts', '.tsx', '.js']

Расширения и resolve.alias

extensions работает совместно с алиасами.

Конфигурация:

resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src')
  },
  extensions: ['.js', '.ts']
}

Импорт:

import Button from '@/components/Button'

будет проверять:

src/components/Button.js
src/components/Button.ts

Расширения и mainFiles

Webpack умеет автоматически искать индексные файлы.

Конфигурация:

resolve: {
  mainFiles: ['index'],
  extensions: ['.js', '.ts']
}

Импорт:

import utils from './utils'

может привести к поиску:

utils/index.js
utils/index.ts

Расширения и fullySpecified

В ESM-режиме Webpack может требовать полные пути:

import utils from './utils.js'

Опция:

resolve: {
  fullySpecified: false
}

разрешает использовать:

import utils from './utils'

вместе с extensions.


Использование с Babel

При работе Babel и Webpack важно синхронизировать расширения.

Webpack:

resolve: {
  extensions: ['.js', '.jsx']
}

Babel:

test: /\.(js|jsx)$/

Если Webpack поддерживает .jsx, а Babel — нет, файл будет найден, но не обработан транспилятором.


Использование с ESLint

ESLint также должен понимать используемые расширения.

Пример настройки:

settings: {
  'import/resolver': {
    node: {
      extensions: ['.js', '.jsx', '.ts', '.tsx']
    }
  }
}

Иначе линтер может ошибочно считать импорт несуществующим.


Использование с Jest

Jest имеет собственный механизм резолвинга модулей.

Обычно настройки синхронизируют:

moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx']

Несовпадение между Jest и Webpack приводит к ошибкам тестирования.


Использование с tsconfig.json

TypeScript также хранит список расширений.

Webpack:

extensions: ['.ts', '.tsx', '.js']

TypeScript:

{
  "compilerOptions": {
    "allowJs": true
  }
}

Важно поддерживать согласованность всей инфраструктуры проекта.


Типичные ошибки

Отсутствие точки

Неправильно:

extensions: ['js']

Правильно:

extensions: ['.js']

Потеря стандартных расширений

Неправильно:

extensions: ['.ts']

если проект использует JSON.

Правильно:

extensions: ['.ts', '...']

Слишком длинный список

Избыточные расширения:

extensions: [
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.json',
  '.vue',
  '.mjs',
  '.cjs',
  '.coffee'
]

ухудшают производительность.


Неоднозначные файлы

Опасная структура:

Button.js
Button.ts
Button.jsx

Импорт:

import Button from './Button'

становится зависимым от порядка extensions.


Практическая конфигурация для разных стеков

JavaScript

resolve: {
  extensions: ['.js', '...']
}

React

resolve: {
  extensions: ['.jsx', '.js', '...']
}

React + TypeScript

resolve: {
  extensions: ['.tsx', '.ts', '.jsx', '.js', '...']
}

Vue

resolve: {
  extensions: ['.vue', '.js', '...']
}

Node.js + ESM

resolve: {
  extensions: ['.mjs', '.js', '.json']
}

Внутренний алгоритм Webpack

При импорте:

import module from './module'

Webpack:

  1. Определяет каталог.
  2. Проверяет наличие точного файла.
  3. Добавляет расширения из extensions.
  4. Проверяет директории.
  5. Анализирует mainFiles.
  6. Использует кэш разрешения.
  7. Возвращает найденный путь.

Из-за большого количества файлов механизм резолвинга считается одной из наиболее нагруженных частей сборки.


Кэширование разрешения

Webpack кэширует результаты поиска модулей.

Если файл уже был найден как:

Button.tsx

Webpack повторно использует результат без повторного перебора расширений.

Это особенно важно в больших monorepo-проектах.


Связь с enhanced-resolve

Webpack использует библиотеку enhanced-resolve.

Именно она:

  • перебирает расширения;
  • анализирует каталоги;
  • работает с alias;
  • обрабатывает symlink;
  • реализует Node.js-подобный алгоритм поиска модулей.

Свойство extensions является частью этого механизма.


Расширения и Node.js

Node.js и Webpack работают по-разному.

Node.js ESM:

import './utils.js'

обычно требует полного расширения.

Webpack способен автоматически дополнять расширения через resolve.extensions.

Это упрощает разработку, но создаёт различия между средами выполнения.


Когда лучше указывать расширение явно

Явные расширения полезны:

  • в ESM-проектах;
  • при библиотечной разработке;
  • при смешении CommonJS и ESM;
  • для совместимости с Node.js;
  • при наличии одинаковых имён файлов.

Пример:

import Button from './Button.tsx'

Когда расширения особенно полезны

resolve.extensions максимально полезен:

  • в React;
  • в TypeScript;
  • в Vue;
  • в monorepo;
  • при использовании alias;
  • в больших SPA;
  • при активном code splitting.

Без него импорты становятся значительно длиннее:

import Header from './components/Header/Header.jsx'

вместо:

import Header from './components/Header/Header'