Модуль 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(...)
Просто соединяет части
- Склеивает относительно друг друга
- Обрабатывает
..и. - Не делает путь абсолютным сам
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.
Частые вопросы
Почему нельзя склеивать пути через + и слэш?
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?
import.meta.dirname. Пути к соседним файлам стройте от него, а не от cwd.Относительный путь считается от файла или от запуска?
process.cwd()), а не от файла модуля. Запустили скрипт из другой папки — относительный путь не нашёлся. Поэтому пути к ресурсам строят от import.meta.dirname.