JavaScript
Node.JS
TypeScript
6 декабря 2019

Автозагрузка модулей с использованием динамического импорта

Недавно в Node.js была анонсирована поддержка ECMAScript-модулей, а в ES2020 появилась поддержка динамических импортов. В рамках данной статьи я расскажу о реализации очевидного кейса использования динамических импортов — с неизвестными заранее названиями директорий.


cover


Проблематика


Зачастую наблюдаю в проектах примерно такую структуру каталогов:


$ 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"],
  }
}

Реализация


Получаются следующие кейсы:


  1. f(projectRoot, ['modules', '*', 'index.js|ts'], moduleName = 'default')// загрузка модулей по указанному пользователю пути
  2. 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);

осталось только typescript прихуярить


Зачем это всё?


Мне показалось, что вопрос динамических импортов несправедливо очень слабо освещён и все подобные библиотеки в npm слегка не обновляются (или я плохо искал?), а технология позволяет сделать хорошо без регистрации и смс. Надеюсь, что исходники проекта и мои кейсы его использования вас заинтересуют для применения в своих проектах, немного сократив дублирование кода прикручиванием нового костыля, велосипеда, фреймворка, несомненно, полезного хэлпера.




Ссылки, пруфы, переводы:



Исходники этого безупречного кода лежать тут:
https://github.com/Melodyn/npm-dynamicimport/blob/master/lib/index.js#L93-L120
Получить бесценный пользовательский опыт можно тут:
https://www.npmjs.com/package/@melodyn/dynamicimport
Котик тут:
(^≗ω≗^)


+4
1,5k 33
Поддержать автора
Комментарии 4

Рекомендуем