Инспекция графа модулей через API

Граф модулей представляет собой внутреннюю структуру зависимостей, которую Rollup строит во время анализа проекта. Каждый файл рассматривается как отдельный узел графа, а операторы import и export образуют связи между этими узлами.

Например, структура проекта:

src/
├── main.js
├── utils.js
├── api.js
└── config.js

Содержимое файлов:

// main.js
import { fetchData } from './api.js';
import { format } from './utils.js';
// api.js
import config from './config.js';
// utils.js
import config from './config.js';

После анализа Rollup сформирует граф:

main.js
 ├── api.js
 │    └── config.js
 └── utils.js
      └── config.js

Такой граф используется для:

  • разрешения зависимостей;
  • удаления неиспользуемого кода;
  • разделения кода на чанки;
  • поиска циклических зависимостей;
  • анализа производительности сборки;
  • построения пользовательских инструментов визуализации.

Когда требуется инспекция графа

Доступ к графу модулей особенно полезен при создании плагинов.

Наиболее распространённые задачи:

  • построение отчётов о зависимостях;
  • генерация диаграмм архитектуры;
  • выявление дублирующихся модулей;
  • анализ размера импортируемых цепочек;
  • поиск «мертвых» файлов;
  • контроль правил архитектуры проекта;
  • аудит сторонних библиотек.

Rollup предоставляет API, позволяющий получать сведения о каждом модуле ещё до завершения сборки.


Объект ModuleInfo

Основным инструментом инспекции графа является объект ModuleInfo.

Получить его можно через методы контекста плагина:

this.getModuleInfo(id);

или

this.getModuleIds();

Каждый модуль содержит подробную информацию о своём состоянии внутри графа.

Пример:

export default function inspectPlugin() {
    return {
        name: 'inspect-plugin',

        buildEnd() {
            const info = this.getModuleInfo('src/main.js');

            console.log(info);
        }
    };
}

Перебор всех модулей графа

Метод getModuleIds() возвращает итератор всех зарегистрированных модулей.

Пример обхода:

export default function inspectPlugin() {
    return {
        name: 'inspect-plugin',

        buildEnd() {
            for (const id of this.getModuleIds()) {
                console.log(id);
            }
        }
    };
}

Результат:

src/main.js
src/api.js
src/utils.js
src/config.js

Такой обход часто становится отправной точкой для собственного анализа структуры проекта.


Получение информации о модуле

После получения идентификатора можно запросить подробное описание.

const info = this.getModuleInfo(id);

Пример:

for (const id of this.getModuleIds()) {
    const info = this.getModuleInfo(id);

    console.log(info.id);
}

Свойство:

info.id

содержит абсолютный путь к файлу.

Например:

C:\project\src\main.js

или

/home/project/src/main.js

в зависимости от операционной системы.


Импортируемые модули

Поле importedIds содержит прямые зависимости текущего модуля.

Файл:

import './a.js';
import './b.js';

Инспекция:

const info = this.getModuleInfo(id);

console.log(info.importedIds);

Результат:

[
    '/src/a.js',
    '/src/b.js'
]

Это позволяет строить карту связей между файлами.


Модули, импортирующие текущий файл

Для обратного анализа используется свойство:

info.importers

Предположим:

// main.js
import './utils.js';
// admin.js
import './utils.js';

Информация о модуле:

const info = this.getModuleInfo('/src/utils.js');

console.log(info.importers);

Результат:

[
    '/src/main.js',
    '/src/admin.js'
]

Такой анализ помогает определить популярность отдельных модулей внутри проекта.


Динамические импорты

Rollup отдельно хранит сведения о динамических импортируемых файлах.

Пример:

const page = await import('./page.js');

Инспекция:

console.log(info.dynamicallyImportedIds);

Результат:

[
    '/src/page.js'
]

Для обратной связи используется:

info.dynamicImporters

Пример:

[
    '/src/main.js'
]

Это особенно важно при анализе code splitting.


Проверка входных модулей

Свойство:

info.isEntry

показывает, является ли файл входной точкой сборки.

Конфигурация:

export default {
    input: {
        main: 'src/main.js',
        admin: 'src/admin.js'
    }
};

Проверка:

if (info.isEntry) {
    console.log('Entry:', info.id);
}

Результат:

Entry: src/main.js
Entry: src/admin.js

Проверка внешних модулей

Если зависимость помечена как внешняя:

export default {
    external: ['lodash']
};

Rollup не включает её в бандл.

Информация:

console.log(info.isExternal);

Позволяет отличать локальные зависимости от внешних пакетов.


Анализ экспортов

Rollup предоставляет список экспортируемых сущностей.

Модуль:

export const api = {};
export const config = {};
export default {};

Проверка:

console.log(info.exports);

Результат:

[
    'api',
    'config',
    'default'
]

Такой механизм используется для статического анализа API проекта.


Анализ использования экспортов

Одной из сильных сторон Rollup является отслеживание использования экспортируемых значений.

Информация доступна через:

info.hasModuleSideEffects

и внутренние механизмы tree shaking.

Плагин может использовать эти данные для выявления файлов, которые подключаются исключительно из-за побочных эффектов.

Пример:

if (info.hasModuleSideEffects === false) {
    console.log('Pure module');
}

Построение карты зависимостей

Простейший вариант визуального представления:

export default function graphPlugin() {
    return {
        name: 'graph-plugin',

        buildEnd() {
            for (const id of this.getModuleIds()) {
                const info = this.getModuleInfo(id);

                console.log(id);

                for (const dep of info.importedIds) {
                    console.log('  ->', dep);
                }
            }
        }
    };
}

Вывод:

main.js
  -> api.js
  -> utils.js

api.js
  -> config.js

utils.js
  -> config.js

Подобный формат легко преобразуется в JSON или DOT-файл для последующей визуализации.


Генерация графа в формате DOT

DOT используется системой Graphviz.

Пример генерации:

export default function dotGraph() {
    return {
        name: 'dot-graph',

        buildEnd() {
            let output = 'digraph G {\n';

            for (const id of this.getModuleIds()) {
                const info = this.getModuleInfo(id);

                for (const dep of info.importedIds) {
                    output += `"${id}" -> "${dep}";\n`;
                }
            }

            output += '}';

            console.log(output);
        }
    };
}

Результат:

digraph G {
    "main.js" -> "api.js";
    "main.js" -> "utils.js";
    "api.js" -> "config.js";
    "utils.js" -> "config.js";
}

После передачи такого файла в Graphviz можно получить полноценную диаграмму зависимостей проекта.


Поиск циклических зависимостей

Цикл возникает, когда модуль прямо или косвенно импортирует сам себя.

Пример:

a.js
  ↓
b.js
  ↓
c.js
  ↓
a.js

Используя getModuleInfo(), можно реализовать обход графа в глубину.

Упрощённый алгоритм:

function visit(node, visited, stack) {
    if (stack.has(node)) {
        console.log('Cycle:', node);
        return;
    }

    if (visited.has(node)) {
        return;
    }

    visited.add(node);
    stack.add(node);

    const info = this.getModuleInfo(node);

    for (const dep of info.importedIds) {
        visit.call(this, dep, visited, stack);
    }

    stack.delete(node);
}

Такой подход позволяет автоматически обнаруживать архитектурные проблемы.


Поиск неиспользуемых модулей

Инспекция графа позволяет определить файлы, которые не достижимы из входных точек.

Общий алгоритм:

  1. Найти все entry-модули.
  2. Выполнить обход графа.
  3. Сохранить посещённые узлы.
  4. Сравнить их со всеми зарегистрированными модулями.

Непосещённые узлы являются кандидатами на удаление.

Подобный анализ особенно полезен в крупных монорепозиториях.


Получение информации после генерации чанков

После завершения построения графа Rollup предоставляет дополнительные сведения через объект bundle.

Хук:

generateBundle(options, bundle) {
}

Позволяет связать модульный граф с итоговыми чанками.

Пример:

generateBundle(options, bundle) {
    for (const file of Object.values(bundle)) {
        if (file.type === 'chunk') {
            console.log(file.modules);
        }
    }
}

Результат:

{
    '/src/main.js': {...},
    '/src/api.js': {...},
    '/src/utils.js': {...}
}

Так можно выяснить, какие модули попали в конкретный выходной файл.


Создание архитектурных проверок

Инспекция графа позволяет реализовывать правила уровня архитектуры.

Пример запрета импорта из слоя UI в слой Core:

if (
    id.includes('/core/') &&
    dep.includes('/ui/')
) {
    this.error(
        'Core layer cannot depend on UI layer'
    );
}

Подобные проверки выполняются непосредственно во время сборки и предотвращают накопление архитектурного долга.


Производительность инспекции графа

При работе с тысячами модулей анализ графа может стать дорогостоящей операцией.

Распространённые рекомендации:

  • выполнять обход один раз;
  • кешировать результаты getModuleInfo();
  • избегать повторных рекурсивных проходов;
  • использовать множества (Set) для хранения посещённых узлов;
  • проводить тяжёлые вычисления в поздних хуках сборки;
  • минимизировать количество операций ввода-вывода внутри обхода.

Поскольку Rollup уже хранит граф в памяти, получение сведений через getModuleInfo() обычно происходит быстро. Наиболее затратной частью становится пользовательская обработка большого количества узлов и связей, особенно при генерации отчётов, визуализаций и сложных архитектурных проверок.