Недавно в Node.js была анонсирована поддержка ECMAScript-модулей, а в ES2020 появилась поддержка динамических импортов. В рамках данной статьи я расскажу о реализации очевидного кейса использования динамических импортов — с неизвестными заранее названиями директорий.
Проблематика
Зачастую наблюдаю в проектах примерно такую структуру каталогов:
$ tree
.
├── modules
│ ├── a
│ │ └── index.ts
│ ├── b
│ │ └── index.ts
│ └── c
│ └── bobule.ts
├── index.ts
└── package.json
и содержимое index.ts
:
import a from './modules/a';
import b from './modules/b';
import c from './modules/c/bobule.ts';
export default {
module: a,
dopule: b,
bobule: c
};
А затем где-то на верхнем уровне есть другой index.ts, который импортирует этот index.ts, который импортирует...
Хотелось бы написать в index.ts
верхнего уровня что-то вроде
import modules from './modules/*/*'
но из коробки это не поддерживается, что пробудило во мне неудержимое желание навернуть свой костыль, велосипед, фреймворк, несомненно, полезный, оригинальный и очень нужный вариант решения этой задачи.
Динамические импорты
Главное преимущество импорта динамического перед статическим — функциональная форма, позволяющая сделать загрузку модулей по условию. Работает это таким образом:
// module.ts
export const a = 'i love hexlet'
const b = { referral: 'hexlet.io/?ref=162475' }
export default b
// index.ts
const module = await import('./module.ts')
module.default // { referral: 'hexlet.io/?ref=162475' }
module.a // 'i love hexlet'
Соответственно, добавив в эту конструкцию немного fs
динамические импорты позволят получить все файлы из вложенных директорий с любого уровня.
Вдохновлённый PHP
Идея автозагрузки не нова и активно используется в PHP, правда, по архитектурно-историческим причинам, но ничего не мешает мне самостоятельно создать себе трудности и героических их преодолевать. Поэтому я попробовал сделать в package.json секцию autoload и создать инструмент, читающий название модуля по ключу, и пути к файлам из значения:
// фрагмент package.json
{
"autoload": {
"modules": ["modules", "*", "index.ts"]
"bobules": ["*", "*", "bobule.ts"],
}
}
В случае использования typescipt, есть досадный момент с тем, что расширения изменяются после сборки приложения и их более двух штук: ts|js|mjs|tsx
поэтому данный момент можно сразу учесть перечислением всех доступных вариантов, а загружать только нужные:
// фрагмент package.json
{
"autoload": {
"modules": ["modules", "*", "index.ts|js"]
"bobules": ["*", "*", "bobule.ts|js"],
}
}
Реализация
Получаются следующие кейсы:
f(projectRoot, ['modules', '*', 'index.js|ts'], moduleName = 'default')
// загрузка модулей по указанному пользователю путиf(projectRoot)
// загрузка модулей из package.json, имена модулей (ключи в секции autoload) в данном случае передаются третьим аргументом уже "под капотом".
Построение путей — задача тривиальная, просто проходимся по массиву и для звёздочек выбираем все подкаталоги, когда массив заканчивается, возвращаем его и загружаем модули в массив. В итоге за пару вечеров я набросал для себя это решение таким образом:
// разруливаются правила package.json / пользовательских путей
const modulesRawPathsParts = await getModulesRawPaths(
projectRoot,
modulePath,
moduleGroupName
);
// обрабатываются расширения файлов и расширяется массив доступых вариантов
const modulesFilesPathsParts = entries(modulesRawPathsParts).reduce(
(acc, [moduleName, moduleRawPath]) => {
const rawFilename = moduleRawPath.pop();
const processedFilenames = processFileExtensions(rawFilename);
const pathsWithFilenames = processedFilenames.map(
filename => moduleRawPath.concat(filename)
);
return { ...acc, [moduleName]: pathsWithFilenames };
},
{}
);
// создаются массивы всех возможных вариантов путей
const modulesFilesPaths = await Promise.all(
entries(modulesFilesPathsParts).map(([moduleName, modulePathParts]) =>
Promise.all(
modulePathParts.map(modulePathPart => buildPaths(projectRoot, modulePathPart))
)
.then(paths => paths.flat().filter(processedPath => processedPath))
.then(existingPaths => ({ [moduleName]: existingPaths })),
),
);
const processedModulesFilesPaths = arrayToObject(modulesFilesPaths);
// выбираются все массивы путей, где директории прошли проверку на существование
const availableModules = entries(processedModulesFilesPaths).reduce(
(acc, [moduleName, modulePaths]) =>
(modulePaths.length === 0 ? acc : { ...acc, [moduleName]: modulePaths }),
{},
);
// загружаются модули
return Promise.all(
entries(availableModules).map(([moduleName, modulePaths]) =>
Promise.all(modulePaths.map(moduleLoadPath =>
// и всё это ради него:
import(moduleLoadPath)
)).then(loadedModule => ({
[moduleName]: loadedModule,
})),
),
).then(arrayToObject);
Зачем это всё?
Мне показалось, что вопрос динамических импортов несправедливо очень слабо освещён и все подобные библиотеки в npm слегка не обновляются (или я плохо искал?), а технология позволяет сделать хорошо без регистрации и смс. Надеюсь, что исходники проекта и мои кейсы его использования вас заинтересуют для применения в своих проектах, немного сократив дублирование кода прикручиванием нового костыля, велосипеда, фреймворка, несомненно, полезного хэлпера.
Ссылки, пруфы, переводы:
- ECMAScript-модулей
- Динамические импорты:
- оригинал: https://v8.dev/features/dynamic-import
- вольный перевод: https://habr.com/ru/post/455200/
Исходники этого безупречного кода лежать тут:
https://github.com/Melodyn/npm-dynamicimport/blob/master/lib/index.js#L93-L120
Получить бесценный пользовательский опыт можно тут:
https://www.npmjs.com/package/@melodyn/dynamicimport
Котик тут:
(^≗ω≗^)