Скидки до 20% + 2-ая профессия бесплатно и подарки на 50 000₽

Главная | Все статьи | Код

Что такое __dirname в JavaScript

Время чтения статьи ~5 минут 148
Что такое __dirname в JavaScript главное изображение

Разработчикам на нативном JS история про различия систем модулей CommonJS и ECMAScript знакома на собственном опыте. Сейчас идёт активное внедрение ECMAScript на уровень языка, а в Node.js новых версий «из коробки» она уже работает нативно. ECMAScript-модули принесли за собой некоторые другие явления:

  1. Необходимость указывать "type": "module" в package.json;
  2. Временный костыль для запуска jest с ключами --experimental-vm-modules и, опционально, --no-warnings;
  3. По умолчанию, отсутствуют глобальные константы __filename и __dirname;

Почему последний пункт важен? Дело в том, что работая с путями, можно построить две рабочих системы: надёжную и ненадёжную. Использование одной из этих констант делает решение по умолчанию надёжнее.

Сначала ломаем

Имеется следующая структура каталогов:

project
├── __fixtures__
│   ├── file1.json
│   ├── file2.json
│   └── expected_file.json
├── __tests__
│   └── index.test.js
└── index.js

Файл index.test.js содержит такой код:

const getFixturePath = (filename) => path.join(__dirname, '..', '__fixtures__', filename);
const readFile = (filename) => fs.readFileSync(getFixturePath(filename), 'utf-8');

// где-то в тестах:
readFile('expected_file.json');

Для начала тесты запускаются в корневом каталоге:

project$ npx -n --experimental-vm-modules jest

  ● read fixtures test

    ReferenceError: __dirname is not defined

    > 25 | const getFixturePath = (filename) => path.join(__dirname, '..', '__fixtures__', filename);
         |                                                ^

Как видно, __dirname отсутствует в глобальной области видимости, самое простое решение — убрать её.

const getFixturePath = (filename) => path.join('..', '__fixtures__', filename);
project$ npx -n --experimental-vm-modules jest

  ● read fixtures test

    ENOENT: no such file or directory, open '../__fixtures__/expected_file.json'

      25 | const getFixturePath = (filename) => path.join('..', '__fixtures__', filename);
    > 26 | const readFile = (filename) => fs.readFileSync(getFixturePath(filename), 'utf-8');
         |                                   ^

Тесты всё ещё падают, но почему? Из текста ошибки так сразу и не скажешь, но теперь поиск файла фикстур происходит на один каталог выше, как будто из корня было набрано cat ../__fixtures__/expected_file.json:

const getFixturePath = (filename) => path.join('__fixtures__', filename);
project$ npx -n --experimental-vm-modules jest

 PASS  __tests__/index.test.js
  ✓ read fixtures test (6 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.326 s, estimated 1 s
Ran all test suites.

Тесты пройдены, но надёжность системы нарушена, потому что этими действиями обеспечено недетерминированное поведение функции построения путей:

project$ cd __tests__
__tests__/project$ npx -n --experimental-vm-modules jest

  ● read fixtures test

    ENOENT: no such file or directory, open '__fixtures__/expected_file.json'

      25 | const getFixturePath = (filename) => path.join('__fixtures__', filename);
    > 26 | const readFile = (filename) => fs.readFileSync(getFixturePath(filename), 'utf-8');
         |                                   ^

Тесты снова упали, потому что теперь path.join собирает путь относительно места запуска.

Так вот, глобальная константа __filename содержит абсолютный путь к файлу, в котором она используется, а __dirname, соответственно, к каталогу. Зная, что необходимый файл лежит относительно текущего всегда на N каталогов выше/ниже, используя данные константы, можно обеспечить детерминированное поведение при запуске кода из любого каталога.

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

Официальная документация Node.js предлагает, может быть, не самое красивое решение, но рабочее:

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Уже зная, как работают эти константы, остаётся вернуть первоначальное решение и опробовать его:

const getFixturePath = (filename) => path.join(__dirname, '..', '__fixtures__', filename);
__tests__/project$ npx -n --experimental-vm-modules jest

 PASS  __tests__/index.test.js
  ✓ read fixtures test (12 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.592 s, estimated 1 s
Ran all test suites.


__tests__/project$ cd ..
project$ npx -n --experimental-vm-modules jest

 PASS  __tests__/index.test.js
  ✓ read fixtures test (29 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.69 s, estimated 1 s
Ran all test suites.

Отличная работа! Теперь пути чётко описывают свой контекст, а код корректно отрабатывает, независимо от места запуска.

Нужно также учесть

Отличие path.join и path.resolve

При выборе между path.join и path.resolve нужно ориентироваться на ожидаемое поведение:

path.join('/a', '/b', 'c') // вернёт /a/b/c сборка происходит слева направо, абсолютный путь строится от первого аргумента с полным путём;

path.resolve('/a', '/b', 'c') // вернёт /b/c абсолютный путь строится от последнего аргумента с абсолютным путём, можно считать что сборка происходит справа налево.

Тесты могут врать

Команда npm test под капотом игнорирует место запуска в рамках проекта и производит запуск от корня. То есть тесты всегда будут проходить вне зависимости от наличия __dirname. С учётом не самого информативного вывода тестов, при ошибке (если оно вообще когда-то вскроется) это позволяет получить гейзенбаг.

Настройки линтера

При использовании eslint с правилами airbnb возникнет две ошибки: Parsing error: Unexpected token import и Unexpected dangling '_' in '__filename'.(no-underscore-dangle). Для их решения в .eslintrc на верхнем уровне достаточно добавить эти настройки:

# Включает поддержку конструкции import.meta.url
parserOptions:
  ecmaVersion: 2020
rules:
  no-underscore-dangle: [2, { "allow": ["__filename", "__dirname"] }] # разрешает подчёркивание в именах только для двух констант

Чтобы столкнуться с данной задачей прямо сейчас, достаточно начать прохождение второго проекта на Хекслете.

Аватар пользователя Sergei Melodyn
Sergei Melodyn 03 июля 2020
148
Похожие статьи
Рекомендуемые программы
профессия
Верстка на HTML5 и CSS3, Программирование на JavaScript в браузере, разработка клиентских приложений используя React
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Тестирование веб-приложений, чек-листы и тест-кейсы, этапы тестирования, DevTools, Postman, SQL, Git, HTTP/HTTPS, API
4 месяца
с нуля
Старт 26 декабря
профессия
Программирование на Java, Разработка веб-приложений и микросервисов используя Spring Boot, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Google таблицы, SQL, Python, Superset, Tableau, Pandas, визуализация данных, Anaconda, Jupyter Notebook, A/B-тесты, ROI
9 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на PHP, Разработка веб-приложений и сервисов используя Laravel, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Ruby, Разработка веб-приложений и сервисов используя Rails, проектирование и реализация REST API
5 месяцев
c опытом
Старт 26 декабря
профессия
Программирование на JavaScript в браузере и на сервере (Node.js), разработка бекендов на Fastify и фронтенда на React
16 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на JavaScript, разработка веб-приложений, bff и сервисов используя Fastify, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Git, JavaScript, Playwright, бэкенд-тесты, юнит-тесты, API-тесты, UI-тесты, Github Actions, HTTP/HTTPS, API, Docker, SQL
8 месяцев
c опытом
Старт 26 декабря