diff --git a/README.md b/README.md index 1c3fdc5..04dd061 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,50 @@ compiled remote types from other federated microapps into _src/@types/remotes_ f Global type definitions from _src/@types/*.d.ts_ are included in compilation. -By default, types are compiled after every webpack compilation. Remote types are downloaded -on webpack build startup and after compilation with a 1-minute interval when idle. +- [@touk/federated-types](https://github.com/touk/federated-types) + a fork of [pixability/federated-types](https://github.com/pixability/federated-types) +- [ruanyl/dts-loader](https://github.com/ruanyl/dts-loader) +- [ruanyl/webpack-remote-types-plugin](https://github.com/ruanyl/webpack-remote-types-plugin), a wmf remotes-aware + downloader + of typings that can be used also with files emitted using `@touk/federated-types` + . [Example](https://github.com/jrandeniya/federated-types-sample). +- [@module-federation/typescript](https://app.privjs.com/buy/packageDetail?pkg=@module-federation/typescript) + from the creator of Webpack Module Federation, Zack Jackson (aka [ScriptAlchemy](https://twitter.com/ScriptedAlchemy)) + +Zack Jackson was asked for help with +[several issues](https://github.com/module-federation/module-federation-examples/issues/20#issuecomment-1153131082) +around his plugin. There was a hope that he can suggest some solutions to the exposed problems, to no avail. +After a month of waiting this package was built. + +## Feature comparison tables + +| Feature | @touk/
federated-types | ruanyl/dts-loader | ruanyl/webpack-remote-types-plugin | @module-federation/typescript | @cloudbeds/wmf-types-plugin | +|------------------------------------|---------------------------|-------------------|------------------------------------|-------------------------------|-----------------------------| +| Webpack Plugin | - | + | + | + | + | +| Standalone | + | - | - | - | + | +| Polyrepo support | - | + | + | + | + | +| Runtime microapp imports | - | - | - | - | + | +| Support typings from node_modules | - | - | - | - | + | +| Webpack aliases | - | - | - | - | + | +| Exposed aliases | + | + | + | - | + | +| Excessive recompilation prevention | - | - | - | - | + | + +*_Runtime microapp imports_ refers to templated remote URLs that are resolved in runtime using +[module-federation/external-remotes-plugin](https://github.com/module-federation/external-remotes-plugin) + +*_Synchronization_ refers to [webpack compile hooks](https://webpack.js.org/api/compiler-hooks/) + +*_Excessive recompilation_ refers to the fact that the plugin is not smart enough to detect when the typings file is changed. +Every time a `d.ts` file is downloaded, webpack recompiles the whole bundle because the watcher compares the timestamp only, which is updated on every download. + +| Package | Emitted destination | Download destination | Synchronization/[compile hooks](https://webpack.js.org/api/compiler-hooks/) | +|------------------------------------|------------------------------------------------------|----------------------|----------------------------------------------------------------------------------------------------------| +| @touk/federated-types | file in
`node_modules/@types/__federated_types` | - | - | +| ruanyl/dts-loader | folders in
`.wp_federation` | - | - | +| ruanyl/webpack-remote-types-plugin | - | `types/[name]-dts` | download on `beforeRun` and `watchRun` | +| @module-federation/typescript | folders in
`dist/@mf-typescript` | `@mf-typescript` | compile and download on `afterCompile` (leads to double compile),
redo every 1 minute when idle | +| @cloudbeds/wmf-types-plugin | file in
`dist/@types` | `@remote-types` | download on startup,
compile `afterEmit`,
download every 1 minute or custom interval when idle | + ## Installation diff --git a/src/constants.ts b/src/constants.ts index 7f84e45..b29797c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,3 +12,7 @@ export const CLOUDBEDS_DEV_FRONTEND_ASSETS_DOMAIN = 'https://cb-front.cloudbeds- export const CLOUDBEDS_MFD_COMMON_MANIFEST_FILE_NAME = 'mfd-common-remote-entry.json'; export const CLOUDBEDS_REMOTES_MANIFEST_FILE_NAME = 'remote-entries.json'; export const CLOUDBEDS_DEPLOYMENT_ENV_WITH_DISABLED_REMOTE_TYPES_DOWNLOAD = 'devbox'; + +export enum CloudbedsMicrofrontend { + Common = 'mfdCommon', +} diff --git a/src/helpers/cloudbedsRemoteManifests.ts b/src/helpers/cloudbedsRemoteManifests.ts index f7b935c..c76f854 100644 --- a/src/helpers/cloudbedsRemoteManifests.ts +++ b/src/helpers/cloudbedsRemoteManifests.ts @@ -2,6 +2,7 @@ import { CLOUDBEDS_DEV_FRONTEND_ASSETS_DOMAIN, CLOUDBEDS_MFD_COMMON_MANIFEST_FILE_NAME, CLOUDBEDS_REMOTES_MANIFEST_FILE_NAME, + CloudbedsMicrofrontend, } from '../constants'; import { ModuleFederationTypesPluginOptions, RemoteManifestUrls } from '../types'; @@ -12,7 +13,7 @@ export function getRemoteManifestUrls(options?: ModuleFederationTypesPluginOptio baseUrl = `${CLOUDBEDS_DEV_FRONTEND_ASSETS_DOMAIN}/remotes/dev-ga`; } return { - mfdCommon: `${baseUrl}/${CLOUDBEDS_MFD_COMMON_MANIFEST_FILE_NAME}`, + [CloudbedsMicrofrontend.Common]: `${baseUrl}/${CLOUDBEDS_MFD_COMMON_MANIFEST_FILE_NAME}`, registry: `${baseUrl}/${CLOUDBEDS_REMOTES_MANIFEST_FILE_NAME}`, ...options?.remoteManifestUrls, } diff --git a/src/helpers/compileTypes.ts b/src/helpers/compileTypes.ts index c5272d6..ed0ac66 100644 --- a/src/helpers/compileTypes.ts +++ b/src/helpers/compileTypes.ts @@ -59,6 +59,40 @@ export function compileTypes(exposedComponents: string[], outFile: string): Comp }; } +export function includeTypesFromNodeModules(federationConfig: FederationConfig, typings: string): string { + const logger = getLogger(); + let typingsWithNpmPackages = typings; + + const exposedNpmPackages = Object.entries(federationConfig.exposes) + .filter(([, path]) => !path.startsWith('.') || path.startsWith('./node_modules/')) + .map(([exposedModuleKey, exposeTargetPath]) => [ + exposedModuleKey.replace(/^\.\//, ''), + exposeTargetPath.replace('./node_modules/', ''), + ]); + + // language=TypeScript + const createNpmModule = (exposedModuleKey: string, packageName: string) => ` + declare module "${federationConfig.name}/${exposedModuleKey}" { + export * from "${packageName}" + } + `; + + if (exposedNpmPackages.length) { + logger.log('Including typings for npm packages:', exposedNpmPackages); + } + + try { + exposedNpmPackages.forEach(([exposedModuleKey, packageName]) => { + typingsWithNpmPackages += `\n${createNpmModule(exposedModuleKey, packageName)}`; + }); + } catch (err) { + logger.warn('Typings was not included for npm package:', (err as Dict)?.url); + logger.log(err); + } + + return typingsWithNpmPackages; +} + export function rewritePathsWithExposedFederatedModules( federationConfig: FederationConfig, outFile: string, @@ -86,12 +120,12 @@ export function rewritePathsWithExposedFederatedModules( // Replace and prefix paths by exposed remote names moduleImportPaths.forEach((importPath) => { - const [exposePath, ...aliases] = Object.keys(federationConfig.exposes) + const [exposedModuleKey, ...exposedModuleNameAliases] = Object.keys(federationConfig.exposes) .filter(key => federationConfig.exposes[key].endsWith(substituteAliases(importPath))) .map(key => key.replace(/^\.\//, '')); - let federatedModulePath = exposePath - ? `${federationConfig.name}/${exposePath}` + let federatedModulePath = exposedModuleKey + ? `${federationConfig.name}/${exposedModuleKey}` : `@not-for-import/${federationConfig.name}/${importPath}`; federatedModulePath = federatedModulePath.replace(/\/index$/, '') @@ -105,7 +139,7 @@ export function rewritePathsWithExposedFederatedModules( typingsUpdated = [ typingsUpdated.replace(RegExp(`"${importPath}"`, 'g'), `"${federatedModulePath}"`), - ...aliases.map(createAliasModule), + ...exposedModuleNameAliases.map(createAliasModule), ].join('\n'); }); diff --git a/src/plugin.spec.ts b/src/plugin.spec.ts index 153878e..d5764f7 100644 --- a/src/plugin.spec.ts +++ b/src/plugin.spec.ts @@ -3,6 +3,7 @@ import webpack, { Compilation, Compiler } from 'webpack'; import { downloadTypes } from './helpers/downloadTypes'; import { ModuleFederationTypesPlugin } from './plugin'; import { ModuleFederationPluginOptions, ModuleFederationTypesPluginOptions } from './types'; +import { CloudbedsMicrofrontend } from './constants'; jest.mock('./helpers/downloadTypes'); @@ -48,15 +49,15 @@ describe('ModuleFederationTypesPlugin', () => { test('remoteManifestUrls setting initiates download of remote entry manifest files on startup', () => { const moduleFederationPluginOptions = { - name: 'mfdCommon', + name: 'mfdDashboard', remotes: { - mfdCommon: 'mfdCommon@[mfdCommon]/remoteEntry.js', - mfdTranslations: 'mfdTranslations@[mfdTranslations]/remoteEntry.js', + [CloudbedsMicrofrontend.Common]: `${CloudbedsMicrofrontend.Common}@[mfdCommonUrl]/remoteEntry.js`, + mfdTranslations: 'mfdTranslations@[mfdTranslationsUrl]/remoteEntry.js', } }; const typesPluginOptions = { remoteManifestUrls: { - mfdCommon: 'https://example.com/mfd-common-remote-entries.json', + [CloudbedsMicrofrontend.Common]: 'https://example.com/mfd-common-remote-entries.json', registry: 'https://example.com/remote-entries.json', } }; diff --git a/src/plugin.ts b/src/plugin.ts index 2316b0a..09bea9d 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -8,7 +8,11 @@ import { DIR_EMITTED_TYPES } from './constants'; import { getRemoteManifestUrls } from './helpers/cloudbedsRemoteManifests'; -import { compileTypes, rewritePathsWithExposedFederatedModules } from './helpers/compileTypes'; +import { + compileTypes, + includeTypesFromNodeModules, + rewritePathsWithExposedFederatedModules +} from './helpers/compileTypes'; import { downloadTypes } from './helpers/downloadTypes'; import { getLoggerHint, setLogger } from './helpers/logger'; import { isEveryUrlValid } from './helpers/validation'; @@ -62,7 +66,8 @@ export class ModuleFederationTypesPlugin implements WebpackPluginInstance { const compileTypesHook = () => { const { isSuccess, typeDefinitions } = compileTypes(exposes as string[], outFile); if (isSuccess) { - rewritePathsWithExposedFederatedModules(federationPluginOptions as FederationConfig, outFile, typeDefinitions); + const typings = includeTypesFromNodeModules(federationPluginOptions as FederationConfig, typeDefinitions); + rewritePathsWithExposedFederatedModules(federationPluginOptions as FederationConfig, outFile, typings); } else { logger.warn('Failed to compile types for exposed modules.', getLoggerHint(compiler)); } diff --git a/src/remote-npm-package-typings.ts b/src/remote-npm-package-typings.ts new file mode 100644 index 0000000..5f1cc75 --- /dev/null +++ b/src/remote-npm-package-typings.ts @@ -0,0 +1,3 @@ +export const remoteNpmPackageTypings = { + '@cloudbeds/ui-library': ['@chakra-ui'], +};