Пошук уроків, статей та іншого контенту
З’ясуємо, як Node.js шукає локальні файли, пакети та вбудовані модулі під час імпорту.
Module resolution — це алгоритм, за яким Node.js визначає, який саме файл або пакет потрібно завантажити для вказаного шляху імпорту.
Наприклад:
const logger = require("./utils/logger");Node.js має знайти файл, який відповідає ./utils/logger, завантажити його та повернути значення module.exports.
Шлях імпорту називають
відносним: ./utils/logger, ../config;
абсолютним: /app/config.js;
назвою пакета: express, lodash;
вбудованим модулем Node.js: fs, node:path.
Алгоритм відрізняється залежно від того, використовується CommonJS або ECMAScript Modules. Найчастіше класичний алгоритм демонструють через require().
Відносний шлях починається з ./ або ../:
const config = require("./config");
const logger = require("../shared/logger");Такий шлях обчислюється відносно файлу, у якому виконується require(), а не відносно поточної робочої директорії процесу.
Якщо файл має таку структуру:
project/
├── src/
│ ├── app.js
│ └── config.js
└── config.jsУ файлі src/app.js:
const config = require("./config");буде завантажено src/config.js, а не project/config.js.
Абсолютний шлях починається з кореня файлової системи:
const config = require("/app/config.js");На практиці абсолютні шляхи часто будують за допомогою __dirname:
const path = require("node:path");
const config = require(path.join(__dirname, "config.js"));__dirname у CommonJS містить абсолютний шлях до директорії поточного файлу.
Якщо specifier не починається з ./, ../ або /, Node.js розглядає його як назву пакета або вбудованого модуля:
const express = require("express");
const path = require("node:path");Для пакета Node.js починає шукати директорії node_modules.
Для такого імпорту:
const logger = require("./utils/logger");Node.js у CommonJS перевіряє варіанти приблизно в такому порядку:
файл із точною назвою;
файл із розширенням .js;
файл із розширенням .json;
файл із розширенням .node;
директорію з такою назвою.
Наприклад, для require("./utils/logger") можливі такі результати:
utils/logger
utils/logger.js
utils/logger.json
utils/logger.node
utils/logger/package.json
utils/logger/index.js
utils/logger/index.json
utils/logger/index.nodeУ сучасних проєктах найкраще явно вказувати розширення у випадках, де це покращує зрозумілість. Водночас CommonJS підтримує скорочений варіант без .js.
Якщо існує файл config.js, обидва варіанти працюють у CommonJS:
const configA = require("./config");
const configB = require("./config.js");Якщо одночасно існують config.js і config.json, Node.js спочатку вибере config.js.
Якщо specifier вказує на директорію, Node.js спочатку перевіряє її package.json.
Наприклад:
utils/
├── package.json
└── logger.js{
"main": "logger.js"
}Тоді цей імпорт завантажить utils/logger.js:
const logger = require("./utils");Якщо package.json відсутній або поле main не вказує на доступний файл, CommonJS може перейти до пошуку:
utils/index.js
utils/index.json
utils/index.nodeФайл index.js — це резервний варіант для директорії, але для нових пакетів краще явно описувати точку входу через package.json.
node_modulesДля імпорту:
const formatter = require("formatter");Node.js не шукає файл formatter.js у випадкових місцях. Він шукає пакет у node_modules.
Припустімо, структура проєкту така:
project/
├── node_modules/
│ └── formatter/
│ ├── package.json
│ └── index.js
├── src/
│ └── app.js
└── package.jsonПід час виконання src/app.js Node.js перевіряє:
project/src/node_modules/formatter
project/node_modules/formatter
node_modules/formatterПошук рухається від директорії поточного файлу вгору до кореня файлової системи.
Тому пакет, встановлений у кореневому project/node_modules, доступний файлам у project/src. Але пакет, встановлений у project/src/node_modules, не буде автоматично доступний файлам, розташованим у project/.
Пакет може мати власні залежності:
project/
├── node_modules/
│ ├── app/
│ │ └── node_modules/
│ │ └── helper/
│ └── helper/Якщо код усередині app імпортує helper, Node.js спочатку перевірить:
app/node_modules/helperі лише потім директорії вище.
Це дозволяє різним пакетам використовувати різні версії однієї залежності.
main і exportsmainПоле main у package.json традиційно визначає головний файл пакета:
{
"name": "formatter",
"main": "src/index.js"
}Тоді:
const formatter = require("formatter");завантажить formatter/src/index.js.
Якщо main не вказано, для CommonJS історично використовується index.js у корені пакета.
exportsСучасні пакети можуть використовувати поле exports:
{
"name": "formatter",
"exports": {
".": "./src/index.js",
"./format": "./src/format.js"
}
}Тоді дозволені такі імпорти:
const formatter = require("formatter");
const format = require("formatter/format");А прямий імпорт внутрішнього файлу, якого немає в exports, може завершитися помилкою:
require("formatter/src/index.js");Типова помилка в такому випадку — ERR_PACKAGE_PATH_NOT_EXPORTED.
exports не лише визначає точку входу, а й приховує внутрішню структуру пакета. Споживачі повинні імпортувати тільки публічні шляхи, оголошені в цьому полі.
Node.js має вбудовані модулі, для яких не потрібно встановлювати пакет:
const fs = require("node:fs");
const path = require("node:path");
const events = require("node:events");Префікс node: явно показує, що імпортується вбудований модуль Node.js.
Також працюють історичні форми без префікса:
const fs = require("fs");
const path = require("path");Однак форма з node: є однозначнішою:
const path = require("node:path");Вбудований модуль має пріоритет над пошуком пакета з такою самою назвою. Тобто require("fs") не означає пошук node_modules/fs.
Створімо такий проєкт:
module-demo/
├── package.json
├── src/
│ ├── app.js
│ └── utils/
│ ├── logger.js
│ └── package.json
└── node_modules/
└── formatter/
├── package.json
└── index.jssrc/utils/package.json:
{
"main": "logger.js"
}src/utils/logger.js:
module.exports = {
info(message) {
console.log(`[INFO] ${message}`);
}
};node_modules/formatter/package.json:
{
"name": "formatter",
"main": "index.js"
}node_modules/formatter/index.js:
module.exports = function format(value) {
return String(value).toUpperCase();
};src/app.js:
const path = require("node:path");
const logger = require("./utils");
const format = require("formatter");
logger.info(format(`Поточна директорія: ${path.basename(__dirname)}`));Запуск із кореня проєкту:
node src/app.jsРезультат:
[INFO] ПОТОЧНА ДИРЕКТОРІЯ: SRCУ цьому прикладі:
node:path знайдено серед вбудованих модулів Node.js;
./utils перетворено на ./utils/package.json, а потім на logger.js;
formatter знайдено в module-demo/node_modules/formatter;
головний файл пакета визначено через поле main.
require.resolverequire.resolve() виконує пошук модуля, але не завантажує його. Він повертає фактичний шлях до знайденого файлу:
console.log(require.resolve("./utils"));
console.log(require.resolve("formatter"));
console.log(require.resolve("node:path"));Приблизний результат:
/.../module-demo/src/utils/logger.js
/.../module-demo/node_modules/formatter/index.js
node:pathЦе корисний інструмент для діагностики:
перевірки, яку версію пакета знайдено;
виявлення неочікуваного node_modules;
перевірки, який файл вибрано замість іншого.
Якщо модуль не знайдено, require.resolve() викине помилку так само, як і require().
Node.js має два основні механізми модулів:
CommonJS: require() і module.exports;
ECMAScript Modules: import і export.
Алгоритми пошуку в них не повністю однакові.
importУ ESM відносний імпорт зазвичай повинен містити розширення:
import logger from "./utils/logger.js";Такий запис може не спрацювати:
import logger from "./utils/logger";На відміну від CommonJS, ESM не покладається на автоматичне додавання .js, .json або .node для відносних шляхів. Також імпорт директорії через import "./utils" не є заміною конкретному файлу index.js.
Поведінка .js залежить від поля "type" у найближчому package.json:
{
"type": "module"
}У такому пакеті файли .js трактуються як ESM. Без "type": "module" файли .js зазвичай трактуються як CommonJS.
Не слід змішувати правила require() та import: скорочений шлях, який працює в CommonJS, може бути некоректним у ESM.
./Ці два записи мають різне значення:
require("./logger");
require("logger");./logger — локальний файл або директорія;
logger — пакет, який Node.js шукатиме в node_modules.
Якщо потрібно імпортувати власний файл, не можна пропускати ./ або ../.
Шлях рахується від поточного файлу:
// src/features/report.js
const config = require("./config");Це означає src/features/config.js, а не src/config.js.
Для файлу в батьківській директорії потрібен ../:
const config = require("../config");Помилка:
Error: Cannot find module 'formatter'може означати, що:
пакет не встановлено;
він встановлений в іншому проєкті;
команда запускається не з того проєкту;
назва пакета написана неправильно;
пакет недоступний із поточної директорії через структуру node_modules.
Якщо пакет має exports, не можна припускати, що будь-який його файл доступний напряму:
require("formatter/src/internal");Потрібно використовувати публічний шлях, оголошений у exports.
index.jsФайл index.js часто використовується як резервна точка входу, але не кожен пакет підтримує такий спосіб імпорту. Поле exports може заборонити імпорт директорії, а ESM має суворіші правила для шляхів.
Перевіряйте package.json пакета і використовуйте його задокументовані точки входу.
Node.js розрізняє локальні шляхи, назви пакетів і вбудовані модулі.
./ та ../ означають пошук відносно поточного файлу.
Для CommonJS Node.js може автоматично перевіряти .js, .json, .node і index.js.
Пакети шукаються в node_modules, починаючи з поточної директорії та рухаючись вгору.
Поля main і exports визначають точки входу пакета.
Вбудовані модулі можна імпортувати з префіксом node:.
require.resolve() допомагає побачити, який саме шлях буде використано.
Правила ESM і CommonJS відрізняються, особливо щодо розширень файлів і директорій.
Найчастіша помилка — використати назву локального файлу без ./.