Skip to content

Commit

Permalink
feat: include types from node_modules
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-pribilinskiy committed Jul 24, 2022
1 parent 377ac7e commit 1512c84
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 13 deletions.
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<br>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 <br> `node_modules/@types/__federated_types` | - | - |
| ruanyl/dts-loader | folders in <br> `.wp_federation` | - | - |
| ruanyl/webpack-remote-types-plugin | - | `types/[name]-dts` | download on `beforeRun` and `watchRun` |
| @module-federation/typescript | folders in <br> `dist/@mf-typescript` | `@mf-typescript` | compile and download on `afterCompile` (leads to double compile), <br> redo every 1 minute when idle |
| @cloudbeds/wmf-types-plugin | file in <br> `dist/@types` | `@remote-types` | download on startup, <br> compile `afterEmit`, <br> download every 1 minute or custom interval when idle |


## Installation

Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
3 changes: 2 additions & 1 deletion src/helpers/cloudbedsRemoteManifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
}
Expand Down
42 changes: 38 additions & 4 deletions src/helpers/compileTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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$/, '')
Expand All @@ -105,7 +139,7 @@ export function rewritePathsWithExposedFederatedModules(

typingsUpdated = [
typingsUpdated.replace(RegExp(`"${importPath}"`, 'g'), `"${federatedModulePath}"`),
...aliases.map(createAliasModule),
...exposedModuleNameAliases.map(createAliasModule),
].join('\n');
});

Expand Down
9 changes: 5 additions & 4 deletions src/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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',
}
};
Expand Down
9 changes: 7 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
}
Expand Down
3 changes: 3 additions & 0 deletions src/remote-npm-package-typings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const remoteNpmPackageTypings = {
'@cloudbeds/ui-library': ['@chakra-ui'],
};

0 comments on commit 1512c84

Please sign in to comment.