Все статьи | Разработка

Что такое __dirname в JavaScript

Что такое __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, возникнет две ошибки:

1 Parsing error: Unexpected token import

На строке с import.meta.url. Решается добавлением в .eslintrc указания использовать последнюю версию ecma в парсере:

# Включает поддержку конструкции import.meta.url
parserOptions:
  ecmaVersion: 2020

2 Unexpected dangling '_' in '__filename'.(no-underscore-dangle)

На константах __filename и __dirname. Решается отключением данного правила для каждой строки или для всего файла: /* eslint-disable no-underscore-dangle */


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