Коротко: Разбираем настройку строгой типизации в microfrontends на Webpack Module Federation: ручные .d.ts, автоматическая синхронизация и плагины.
Архитектура микрофронтендов на базе Webpack 5 Module Federation решает задачу независимого развертывания и изоляции команд. Однако при переходе на TypeScript разработчики неизбежно сталкиваются с архитектурным противоречием. Webpack загружает remote-модули динамически во время выполнения приложения (runtime), а компилятор TypeScript требует информацию о типах на этапе сборки (compile-time).
Без выстроенного процесса доставки типов импорт компонентов из других микрофронтендов либо приводит к ошибке компиляции TS2307: Cannot find module, либо заставляет разработчиков прибегать к директивам @ts-ignore и типу any. В результате разрушается сквозная безопасность типов: изменение сигнатуры пропсов в remote-модуле не вызывает ошибок в host-приложении на этапе билда и проявляется только в виде сбоев у пользователей.
Разберем, почему возникает этот разрыв, как устроены механизмы синхронизации .d.ts и какие подходы позволяют выстроить надежный типобезопасный контракт между микрофронтендами.
Анатомия проблемы: конфликт между Compile-Time и Runtime
Чтобы понять корень проблемы, рассмотрим, как взаимодействуют Webpack и TypeScript при сборке микрофронтенда.
Конфигурация типового ModuleFederationPlugin разделяет микрофронтенды на две роли:
- Remote (поставщик): публикует наружу JS-модули через поле
exposes. - Host (потребитель): подключает удаленные точки входа через поле
remotes.
// host/webpack.config.js
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@https://cdn.example.com/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});
Когда разработчик в Host-приложении пишет:
import RemoteButton from 'remoteApp/Button';
происходят два независимых процесса:
- Компиляция TypeScript (
tsc): компилятор пытается найти файл декларации по физическому или сконфигурированному пути (node_modules,pathsвtsconfig.json). Поскольку модуляremoteApp/Buttonфизически нет на диске в момент сборки,tscгенерирует ошибкуTS2307: Cannot find module 'remoteApp/Button' or its corresponding type declarations. - Сборка Webpack: бандлер заменяет импорт на рантайм-вызов загрузки скрипта через
remoteEntry.jsи контейнер Module Federation.
Webpack отвечает исключительно за сборку JavaScript и разделение зависимостей через shareScope. Он не генерирует и не передает метаданные типов. Задача доставки определений .d.ts полностью ложится на плечи разработчиков инфраструктуры.
Подход 1. Ambient Declarations: ручное описание контрактов
Самый простой способ устранить ошибки компилятора — объявить сигнатуру удаленного модуля вручную в файле глобальных деклараций Host-приложения.
Реализация
В Host-приложении создается файл declarations.d.ts:
// host/src/declarations.d.ts
declare module 'remoteApp/Button' {
export interface ButtonProps {
variant: 'primary' | 'secondary';
onClick: () => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps>;
export default Button;
}
Чтобы TypeScript учитывал этот файл, необходимо убедиться, что он попадает в секцию include в tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"moduleResolution": "NodeNext"
},
"include": ["src/**/*"]
}
Плюсы и минусы подхода
| Преимущества | Недостатки |
|---|---|
| Нулевые требования к инфраструктуре сборки | Человеческий фактор: типы пишутся вручную |
| Быстрый старт на этапе PoC (Proof of Concept) | Рассинхронизация: remote меняет контракт, host не знает об этом |
| Не требует изменений в конфигурации Remote-сборки | Накопление скрытого техдолга и ложное чувство безопасности |
Ручные декларации допустимы только на стадии раннего прототипирования. В продакшене с несколькими продуктовыми командами такой подход гарантированно приводит к рассинхронизации интерфейсов и ошибкам в рантайме.
Подход 2. Автоматическая генерация и синхронизация типов через плагины
Для устранения человеческого фактора процесс генерации и передачи типов автоматизируют. Архитектурный паттерн строится по следующей схеме:
- Remote при сборке компилирует
.d.tsдля файлов, указанных вexposes, и упаковывает их в архив (tarball/zip) или сохраняет в публичную директорию рядом сremoteEntry.js. - Host на этапе предсборки или через Webpack-плагин запрашивает архив с типами по HTTP, распаковывает его в локальную папку
@types/remotesи мапит пути черезtsconfig.json.
Популярный стек для реализации этой схемы: dts-loader / @module-federation/typescript на стороне Remote и webpack-remote-types-plugin на стороне Host.
Шаг 1: Конфигурация Remote
Remote должен генерировать типы именно для экспортируемых модулей. При сборке создается директория с .d.ts, которая отдается dev-сервером или загружается на CDN вместе с JS-бандлами.
// remote/webpack.config.js
const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
Для сборки типов в архив используется скрипт генерации dts (через tsc --emitDeclarationOnly) и упаковки полученной папки dist/types в архив remoteApp-dts.tgz.
Шаг 2: Конфигурация Host через WebpackRemoteTypesPlugin
В Host-приложении плагин на этапе компиляции скачивает актуальный архив типов:
// host/webpack.config.js
const WebpackRemoteTypesPlugin = require('webpack-remote-types-plugin').default;
module.exports = {
plugins: [
new WebpackRemoteTypesPlugin({
remotes: {
remoteApp: 'remoteApp@https://cdn.example.com/remoteApp-dts.tgz',
},
outputDir: './src/@types/remotes',
remoteFileName: '[name]-dts.tgz',
}),
],
};
Шаг 3: Настройка путей в tsconfig.json
Чтобы IDE и компилятор видели скачанные типы без явного указания путей в импортах, настраивается секция paths:
// host/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"remoteApp/*": ["src/@types/remotes/remoteApp/*"]
}
}
}
В результате при вызове import Button from 'remoteApp/Button' компилятор подхватывает оригинальные интерфейсы пропсов, созданные авторами remote-компонента.
Подход 3. Module Federation 2.0 и экосистема @module-federation/enhanced
С развитием спецификации Module Federation и появлением пакета @module-federation/enhanced (развиваемого в рамках экосистемы Module Federation 2.0) типизация микрофронтендов получила нативную поддержку без необходимости писать кастомные скрипты архивации.
Плагин @module-federation/enhanced берет на себя:
- Автоматический анализ блока
exposes; - Генерацию
.d.tsи бандлинг их в формат.d.ts.tar.gz; - Скачивание и резолв типов на стороне Host во время работы dev-сервера и сборки.
Настройка Remote
// remote/webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.tsx',
},
dts: {
generateTypes: {
extractThirdParty: true,
compileInChildProcess: true,
},
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
Настройка Host
// host/webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
dts: {
consumeTypes: {
consumeAPITypes: true,
},
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
};
При запуске сборки плагин автоматически скачивает типы с dev-сервера Remote, сохраняет их в локальную директорию и прозрачно регистрирует их в TypeScript. Это исключает ручную настройку paths в tsconfig.json.
Организация версионирования и CI/CD в кросс-командной разработке
Автоматизация доставки типов решает техническую сторону проблемы, но не исключает организационных рисков. В микрофронтендной архитектуре Remote и Host деплоятся асинхронно. Если команда Remote удалит обязательный пропс или переименует событие, Host-приложение скомпилируется со старыми типами из кэша, но упадет в рантайме после деплоя Remote.
Принципы стабильности контрактов
- Правило расширения (Open-Closed): при изменении интерфейса Remote-модуля новые поля должны быть опциональными (
optional props), а устаревшие свойства помечаются@deprecatedи сохраняются минимум на один релизный цикл:export interface RemoteButtonProps { label: string; onClick: () => void; /** @deprecated Используйте variant="accent" */ isPrimary?: boolean; variant?: 'default' | 'accent'; } - Контрактное тестирование в CI: в пайплайн Host-приложения внедряется шаг валидации типов против актуального CDN/Staging окружения Remote. Если Remote выкатил ломающие изменения в контракт, CI Host-приложения должен упасть на этапе проверки типов (
tsc --noEmit). - Версионирование точек входа: для критических изменений API создается новая точка входа в
exposes(например,'./Button/v2'), что позволяет хостам мигрировать независимо.
Сравнение подходов и чек-лист для Production
Выбор стратегии типизации зависит от размера кодовой базы и используемого инструментария.
| Критерий | Ambient Declarations (ручные) | Плагины архивации (dts-loader / webpack-remote-types-plugin) |
@module-federation/enhanced |
|---|---|---|---|
| Сложность внедрения | Минимальная | Средняя | Низкая (при переходе на MF 2.0) |
| Синхронизация типов | Отсутствует | Автоматическая (по URL/архиву) | Автоматическая (встроена в билд) |
| Поддержка IDE | Требует ручных правок | Через tsconfig.paths |
Из коробки |
| Риск расхождения версий | Критический | Низкий | Минимальный |
| Где применять | Прототипы, POC | Webpack 5 production-проекты | Новые проекты и стек с MF 2.0 |
Чек-лист готовности к production:
- [ ] В кодовой базе Host нет конструкций
// @ts-ignoreиanyна federated-импортах. - [ ] В пайплайне Remote настроена автоматическая генерация
.d.tsпри релизной сборке. - [ ] В CI Host-приложения включен шаг проверки типов
tsc --noEmitс подтягиванием свежих деклараций. - [ ] Настроены таймауты и fallback-значения на случай недоступности сервера с типами при локальной разработке.
- [ ] Команды следуют соглашению об обратной совместимости интерфейсов.
FAQ
Почему TypeScript не видит типы из Remote-приложения по умолчанию?
TypeScript — статический анализатор кода. Он работает исключительно с локальной файловой системой во время сборки. Webpack Module Federation связывает код динамически по сети в браузере. Так как в файловой структуре Host-приложения физически отсутствуют исходники Remote, компилятор считает путь несуществующим.
Стоит ли выносить типы микрофронтендов в отдельный npm-пакет?
Вынос типов в npm-пакет решает проблему валидации, но разрушает ключевое преимущество микрофронтендов — независимость релизного цикла. Команда Remote будет вынуждена публиковать пакет, а команда Host — обновлять зависимости в package.json при каждом изменении контракта. Синхронизация типов через CDN/URL вместе со сборкой устраняет эту задержку.
Как локально разрабатывать Host и Remote с автоматическим обновлением типов?
При локальной разработке Remote-приложение запускается на локальном порту (например, localhost:3001). Плагины синхронизации типов настраиваются на опрос локального URL http://localhost:3001/remoteApp-dts.tgz. При сохранении изменений в Remote и пересборке Host подтягивает обновленный файл определений.
Что делать, если сборка Host падает в CI из-за недоступности dev-сервера с типами?
Для сборки в CI типы Remote-приложений должны забираться со стабильного окружения (Staging/Production CDN или S3-хранилища с версионированными артефактами), а не с временных dev-серверов. Также рекомендуется кэшировать последнюю стабильную версию скачанных типов в артефактах CI.
Можно ли использовать Module Federation с разными версиями TypeScript в микрофронтендах?
Да. Поскольку передаются только скомпилированные .d.ts файлы, внутренние версии компиляторов могут различаться. Главное требование — сгенерированные декларации типов должны соответствовать синтаксису, поддерживаемому версией TypeScript в Host-приложении.
Заключение
Интеграция TypeScript и Module Federation требует явного разделения зон ответственности между сборщиком и компилятором. Использование ручных заглушек ведет к накоплению технического долга и снижает надежность системы.
Внедрение автоматической синхронизации типов через плагины генерации деклараций или переход на @module-federation/enhanced позволяет сохранить преимущества изолированного деплоя микрофронтендов без потери преимуществ строгой типизации. Контракт между командами становится самодокументируемым и проверяемым на всех этапах жизненного цикла приложения.




.svg.webp)



