Модуль path в Node.js: пути без ошибок на всех ОС

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов с бейджем. Код дан по документации Node.js. Говорим прямо, а не ставим бейдж «проверено».

path — маленький модуль, который решает одну надоедливую проблему: пути к файлам на разных операционных системах устроены по-разному, и наивный код ломается при переносе.

Главное правило: не склеивайте строками

// ❌ сломается на Windows
const configPath = baseDir + '/config/app.json';

// ✅ работает везде
import { join } from 'node:path';
const configPath = join(baseDir, 'config', 'app.json');

Причина простая: на Unix разделитель пути — прямой слэш /, на Windows — обратный \. Строка с прямым слэшем на Windows иногда работает, а иногда нет, и баг всплывает у того пользователя, чью систему вы не тестировали. path.join подставляет правильный для текущей ОС разделитель и заодно убирает двойные слэши.

Это то же правило, что мы упоминали в разборе модуля fs: пути — только через path.

join против resolve: разница, которую путают

Оба соединяют части пути, но по-разному, и это источник багов.

import { join, resolve } from 'node:path';

join('/app', 'src', 'index.js');      // "/app/src/index.js"
join('/app', '../lib');               // "/lib" — учёл ..

resolve('/app', 'src');               // "/app/src" — абсолютный
resolve('/app', '/etc');              // "/etc" — АБСОЛЮТНЫЙ сегмент отбросил левое!
path.join против path.resolve

path.join(...)

Просто соединяет части

  • Склеивает относительно друг друга
  • Обрабатывает .. и .
  • Не делает путь абсолютным сам
  • join('a', 'b')a/b

path.resolve(...)

Строит абсолютный путь

  • Идёт с конца к началу
  • Абсолютный сегмент отбрасывает всё левее
  • Достраивает до абсолютного через cwd()
  • resolve('/a', '/b')/b

Практическое правило: join — когда собираете относительный путь из кусков. resolve — когда нужен гарантированно абсолютный путь, например для сравнения или проверки, что файл внутри разрешённого каталога.

Кстати, resolve с проверкой — простая защита от path traversal: если после resolve путь вышел за пределы вашего каталога, значит в нём был ../ от пользователя.

Разбор пути на части

import { basename, dirname, extname, parse } from 'node:path';

const p = '/app/src/index.js';

basename(p);         // "index.js"
basename(p, '.js');  // "index" — убрали расширение
dirname(p);          // "/app/src"
extname(p);          // ".js" — с точкой!

parse(p);
// { root: '/', dir: '/app/src', base: 'index.js', ext: '.js', name: 'index' }

parse удобен, когда нужно сразу несколько частей: он разбирает путь целиком в объект. Обратная операция — path.format(obj) — собирает путь из такого объекта.

Обратите внимание: extname возвращает расширение с точкой (.js, а не js). Забыть про точку при сравнении — классическая мелкая ошибка.

import.meta.dirname: где я нахожусь

Частая задача — прочитать файл, лежащий рядом с кодом. И тут важно понимать, откуда считаются относительные пути.

// ❌ относительный путь считается от cwd(), а не от файла
await readFile('./template.html');   // найдётся, только если запустили из этой папки

// ✅ строим от каталога модуля
import { join } from 'node:path';
await readFile(join(import.meta.dirname, 'template.html'));

Относительный путь в Node считается от рабочего каталога процесса (process.cwd()), а не от файла, где написан. Запустили скрипт из другой папки — и ./template.html не нашёлся. Это классическое «работает у меня, падает в проде».

import.meta.dirname (доступен с Node 21.2) даёт каталог текущего модуля — от него пути к ресурсам стройте всегда. В CommonJS ту же роль играл __dirname, которого в ESM нет.

Windows и POSIX явно

Иногда нужно работать с путями конкретной ОС независимо от текущей — например, разбирать Windows-путь на Linux-сервере:

import { win32, posix } from 'node:path';

win32.sep;                    // "\\"
posix.join('a', 'b');         // "a/b" всегда, даже на Windows

path.sep — разделитель текущей ОС, path.win32 и path.posix — принудительно та или иная схема. Нужно редко, но когда парсите чужие пути — незаменимо.


Смежные темы: модуль fs — куда эти пути идут; объект process — про cwd() и окружение; работа с каталогами. Полный список — в справочнике по Node.js.

Частые вопросы

Почему нельзя склеивать пути через + и слэш?
Потому что разделитель зависит от ОС: на Windows это обратный слэш. Строка dir + '/' + file сломается на Windows, а path.join(dir, file) подставит правильный разделитель. Это причина «работает у меня, падает в проде».
Чем path.join отличается от path.resolve?
join просто соединяет части относительно друг друга. resolve строит абсолютный путь: он идёт с конца, и если встречает абсолютный сегмент, отбрасывает всё левее. resolve('/a', '/b') вернёт /b, а join('/a', '/b')/a/b.
Как получить имя файла и расширение из пути?
path.basename(p) — имя с расширением, path.extname(p) — расширение с точкой, path.dirname(p) — каталог. Ещё есть path.parse(p), который разбирает путь на все части сразу.
Что такое __dirname и почему его нет в ESM?
Это каталог текущего модуля. В CommonJS он был всегда, в ESM его убрали, но с Node 21.2 вернули как import.meta.dirname. Пути к соседним файлам стройте от него, а не от cwd.
Относительный путь считается от файла или от запуска?
От рабочего каталога процесса (process.cwd()), а не от файла модуля. Запустили скрипт из другой папки — относительный путь не нашёлся. Поэтому пути к ресурсам строят от import.meta.dirname.