Skip to content

Commit 78916f6

Browse files
committed
2 parents bf4a63e + 6a1512a commit 78916f6

File tree

6 files changed

+264
-62
lines changed

6 files changed

+264
-62
lines changed

libs/mf-tools/README.md

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,67 @@ bootstrap(AppModule, {
9191

9292
> Use this bootstrap helper for **both**, your shell and your micro frontends!
9393
94+
## Sharing Zone.js
95+
96+
In order to share zone.js, call our ``shareNgZone`` helper when starting the shell.
97+
98+
```typescript
99+
import { Component, NgZone } from '@angular/core';
100+
import { shareNgZone } from '@angular-architects/module-federation-tools';
101+
102+
@Component({
103+
selector: 'app-root',
104+
templateUrl: './app.component.html',
105+
})
106+
export class AppComponent {
107+
title = 'shell';
108+
109+
constructor(private ngZone: NgZone) {
110+
shareNgZone(ngZone);
111+
}
112+
113+
}
114+
```
115+
116+
The micro frontends will pick it up, if they are bootstrapped with the ``bootstrap`` helper (see above).
117+
118+
## Details on ngZone and Platform sharing
119+
120+
> In a multi version micro frontend strategy, it is important to load the zone.js bundle to the window object only once. Also, one need to make sure that only one instance of the ngZone is used by all the micro frontends.
121+
122+
If you share `@angular/core` and therefore also have one technical reference to the BrowserPlatform, that is used by more than one micro frondend, Angular's default setup is, to support only one platform instance per shared version. Be aware that you **need** to create multi platform instances in case of different versions, but also in case the version is the same, but `@angular/core` is not shared, but packed into the micro frontend's bundles directly (like in Angular's default way w/o module federation).
123+
124+
Naturally, such technical details are hard to get into. Therefore the `bootstrap()` function of this package helps to implement your multi version strategy w/o the need of implementing those low-level aspects on your own.
125+
126+
Some optional flags are offered to provide options for custom behavior of the `bootstrap()` function:
127+
128+
- `ngZoneSharing: false`: Deactivate ngZone sharing in the window object (not recommended):
129+
```typescript
130+
bootstrap(AppModule, {
131+
production: environment.production,
132+
ngZoneSharing: false // defaults to true
133+
});
134+
```
135+
- `platformSharing: false`: Deactivate Platform sharing in the window object (not recommended):
136+
```typescript
137+
bootstrap(AppModule, {
138+
production: environment.production,
139+
platformSharing: false // defaults to true
140+
});
141+
```
142+
- Possible, if dependencies are not shared or each bootstrapped remote app uses a different version.
143+
- `activeLegacyMode: false`: Deactivates the legacy mode that provides backwards compatibility for Platform sharing:
144+
```typescript
145+
bootstrap(AppModule, {
146+
production: environment.production,
147+
activeLegacyMode: false // defaults to true
148+
});
149+
```
150+
- If all your micro frontends use `@angular-architects/module-federation-tools` in version `^12.6.0`, `^13.1.0` or any newer major version you can switch off the legacy mode manually.
151+
- Those versions introduced new features on how to share the Platform in the window object.
152+
- This allows to use the `bootstrap()` function even in such cases, where the same version is packed into different micro frontend bundles.
153+
154+
94155
## Routing to Web Components
95156

96157
The ``WebComponentWrapper`` helps you to route to web components:
@@ -216,30 +277,6 @@ events = {
216277
<mft-wc-wrapper [options]="item" [props]="props" [events]="events"></mft-wc-wrapper>
217278
```
218279
219-
## Sharing Zone.js
220-
221-
In order to share zone.js, call our ``shareNgZone`` helper when starting the shell.
222-
223-
```typescript
224-
import { Component, NgZone } from '@angular/core';
225-
import { shareNgZone } from '@angular-architects/module-federation-tools';
226-
227-
@Component({
228-
selector: 'app-root',
229-
templateUrl: './app.component.html',
230-
})
231-
export class AppComponent {
232-
title = 'shell';
233-
234-
constructor(private ngZone: NgZone) {
235-
shareNgZone(ngZone);
236-
}
237-
238-
}
239-
```
240-
241-
The micro frontends will pick it up, if they are bootstrapped with the ``bootstrap`` helper (see above).
242-
243280
## More about the underlying ideas
244281
245282
Please find more information on the underlying ideas in this [blog article](https://www.angulararchitects.io/aktuelles/multi-framework-and-version-micro-frontends-with-module-federation-the-good-the-bad-the-ugly).
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const packageNamespace = '@angular-architects/module-federation-tools';
2+
3+
function getGlobalState<T>(): T {
4+
const globalState = (window as unknown as { [packageNamespace]: T });
5+
globalState[packageNamespace] = globalState[packageNamespace] || ({} as unknown as T);
6+
return globalState[packageNamespace];
7+
}
8+
9+
export function getGlobalStateSlice<T>(): T;
10+
export function getGlobalStateSlice<T, R>(selector: (globalState: T) => R): R;
11+
export function getGlobalStateSlice<T, R>(
12+
selector?: (globalState: T) => R
13+
): R | T {
14+
const globalState = getGlobalState<T>();
15+
return selector ? selector(globalState) : globalState;
16+
}
17+
18+
export function setGlobalStateSlice<T>(slice: T): T {
19+
return Object.assign(
20+
getGlobalState<T>(),
21+
slice
22+
);
23+
}
Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
import { CompilerOptions, enableProdMode, NgModuleRef, NgZone, PlatformRef, Type } from "@angular/core";
1+
import { CompilerOptions, enableProdMode, NgModuleRef, NgZone, PlatformRef, Type, Version } from "@angular/core";
22
import { platformBrowser } from "@angular/platform-browser";
33
import { VERSION } from '@angular/core';
4+
import { getGlobalStateSlice, setGlobalStateSlice } from "../utils/global-state";
45

56
export type Options = {
67
production: boolean,
78
platformFactory?: () => PlatformRef,
89
compilerOptions?: CompilerOptions & BootstrapOptions,
9-
version?: () => string,
10+
version?: () => string | Version,
11+
/**
12+
* Opt-out of ngZone sharing.
13+
* Not recommanded.
14+
* Default value true.
15+
*/
16+
ngZoneSharing?: boolean,
17+
/**
18+
* Opt-out of platformSharing sharing.
19+
* Possible, if dependencies are not shared or each bootstrapped
20+
* remote app uses a different version.
21+
* Default value true.
22+
*/
23+
platformSharing?: boolean,
24+
/**
25+
* Deactivate support for legacy mode.
26+
* Only recommanded if all used implementations depend on
27+
* @angular-architects/module-federation-tools > 13.0.1.
28+
* Default value true.
29+
*/
30+
activeLegacyMode?: boolean
1031
};
1132

1233
declare interface BootstrapOptions {
@@ -15,9 +36,9 @@ declare interface BootstrapOptions {
1536
ngZoneRunCoalescing?: boolean;
1637
}
1738

18-
export type PlatformCache = {
19-
platform: Map<unknown, PlatformRef>
20-
};
39+
let ngZoneSharing = true;
40+
let platformSharing = true;
41+
let legacyMode = true;
2142

2243
export function getMajor(version: string): string {
2344
const pre = version.match(/\d+/)[0];
@@ -34,47 +55,111 @@ export function getMajor(version: string): string {
3455
return pre;
3556
}
3657

37-
function getPlatformCache(): PlatformCache {
38-
const platformCache = window as unknown as PlatformCache;
39-
platformCache.platform = platformCache.platform || new Map<unknown, PlatformRef>();
58+
/**
59+
* LEGACY IMPLEMENTATIONS START
60+
*
61+
* Can be deprecated in later major releases.
62+
*
63+
* To increase backwards compatibility legacy and current namespaces
64+
* within the window object are used.
65+
*/
66+
67+
export type LegacyPlatformCache = {
68+
platform: Record<string, PlatformRef>;
69+
};
70+
71+
function getLegacyPlatformCache(): LegacyPlatformCache {
72+
const platformCache = window as unknown as LegacyPlatformCache;
73+
platformCache.platform = platformCache.platform || {};
4074
return platformCache;
4175
}
4276

43-
function getNgZone(): NgZone {
77+
function getLegacyPlatform(key: string): PlatformRef {
78+
const platform = getLegacyPlatformCache().platform[key];
79+
/**
80+
* If dependencies are not shared, platform with same version is different
81+
* and shared platform will not be returned.
82+
*/
83+
return platform instanceof PlatformRef ? platform : null;
84+
}
85+
86+
function setLegacyPlatform(key: string, platform: PlatformRef): void {
87+
getLegacyPlatformCache().platform[key] = platform;
88+
}
89+
90+
function getLegacyNgZone(): NgZone {
4491
return window['ngZone'];
45-
}
92+
}
4693

47-
export function shareNgZone(zone: NgZone): void {
94+
function setLegacyNgZone(zone: NgZone): void {
4895
window['ngZone'] = zone;
4996
}
5097

51-
export function bootstrap<M>(module: Type<M>, options: Options): Promise<NgModuleRef<M>> {
98+
/**
99+
* LEGACY IMPLEMENTATIONS END
100+
*/
52101

53-
if (!options.platformFactory) {
54-
options.platformFactory = () => platformBrowser();
102+
function getPlatformCache(): Map<Version, PlatformRef> {
103+
return getGlobalStateSlice(
104+
(state: { platformCache: Map<Version, PlatformRef> }) => state.platformCache
105+
) || setGlobalStateSlice({
106+
platformCache: new Map<Version, PlatformRef>()
107+
}).platformCache;
108+
}
109+
110+
function setPlatform(version: Version, platform: PlatformRef): void {
111+
if (platformSharing) {
112+
legacyMode && setLegacyPlatform(version.full, platform);
113+
getPlatformCache().set(version, platform);
55114
}
115+
}
56116

57-
if (!options.compilerOptions?.ngZone) {
58-
options.compilerOptions = options.compilerOptions || {};
59-
options.compilerOptions.ngZone = getNgZone();
117+
function getPlatform(options: Options): PlatformRef {
118+
if (!platformSharing) {
119+
return options.platformFactory();
60120
}
61121

62-
// if (!options.version) {
63-
// options.version = () => VERSION.full;
64-
// }
122+
const versionResult = options.version();
123+
const version = versionResult === VERSION.full ? VERSION : versionResult;
124+
const versionKey = typeof version === 'string' ? version : version.full;
65125

66-
// const key = options.version();
67-
const platformCache = getPlatformCache();
126+
let platform =
127+
getPlatformCache().get(version as Version) ||
128+
(legacyMode && getLegacyPlatform(versionKey));
68129

69-
let platform = platformCache.platform.get(VERSION);
70130
if (!platform) {
71131
platform = options.platformFactory();
72-
platformCache.platform.set(VERSION, platform);
132+
setPlatform(VERSION, platform);
133+
}
134+
135+
return platform;
136+
}
73137

74-
if (options.production) {
75-
enableProdMode();
76-
}
138+
function getNgZone(): NgZone {
139+
return getGlobalStateSlice(
140+
(state: { ngZone: NgZone }) => state.ngZone
141+
) || getLegacyNgZone();
142+
}
143+
144+
export function shareNgZone(zone: NgZone): void {
145+
if (ngZoneSharing) {
146+
legacyMode && setLegacyNgZone(zone);
147+
setGlobalStateSlice({ ngZone: zone });
77148
}
149+
}
78150

79-
return platform.bootstrapModule(module, options.compilerOptions);
80-
}
151+
export function bootstrap<M>(module: Type<M>, options: Options): Promise<NgModuleRef<M>> {
152+
ngZoneSharing = options.ngZoneSharing !== false;
153+
platformSharing = options.platformSharing !== false;
154+
legacyMode = options.activeLegacyMode !== false;
155+
options.platformFactory = options.platformFactory || (() => platformBrowser());
156+
options.version = options.version || (() => VERSION);
157+
options.production && enableProdMode();
158+
159+
if (ngZoneSharing && !options.compilerOptions?.ngZone) {
160+
options.compilerOptions = options.compilerOptions || {};
161+
options.compilerOptions.ngZone = getNgZone();
162+
}
163+
164+
return getPlatform(options).bootstrapModule(module, options.compilerOptions);
165+
}

libs/mf/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ plugins: [
226226

227227
The helper function share adds some additional options for the shared dependencies:
228228

229-
```json
229+
```typescript
230230
shared: share({
231231
"@angular/common": {
232232
singleton: true,
@@ -282,7 +282,7 @@ shared: share({
282282

283283
The ``shareAll`` helper shares all your dependencies defined in your ``package.json``. The ``package.json`` is look up as described above:
284284

285-
```json
285+
```typescript
286286
shared: {
287287
...shareAll({
288288
singleton: true,
@@ -401,20 +401,20 @@ If you get the warning _No required version specified and unable to automaticall
401401

402402
To avoid this warning you can specify to used version by hand:
403403

404-
```json
404+
```typescript
405405
shared: {
406406
"@angular/common": {
407407
singleton: true,
408408
strictVersion: true,
409-
requireVersion: '12.0.0
409+
requireVersion: '12.0.0'
410410
},
411411
[...]
412412
},
413413
```
414414

415415
You can also use our ``share`` helper that infers the version number from your ``package.json`` when setting ``requireVersion`` to ``'auto'``:
416416

417-
```json
417+
```typescript
418418
shared: share({
419419
"@angular/common": {
420420
singleton: true,

libs/mf/src/utils/share-utils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import callsite = require('callsite');
22
import path = require('path');
33
import fs = require('fs');
4+
import { SharedConfig } from './webpack.types';
45

56
let inferVersion = false;
67

7-
type VersionMap = { [packageName:string]: string };
8+
type VersionMap = Record<string, string>;
89
type IncludeSecondariesOptions = { skip: string | string[] } | boolean;
9-
type Config = { [setting:string]: unknown };
10+
type CustomSharedConfig = SharedConfig & { includeSecondaries: IncludeSecondariesOptions };
11+
type ConfigObject = Record<string, CustomSharedConfig>;
12+
type Config = (string | ConfigObject)[] | ConfigObject;
1013

1114
function findPackageJson(folder: string): string {
1215
while (
@@ -127,7 +130,7 @@ export function setInferVersion(infer: boolean): void {
127130
inferVersion = infer;
128131
}
129132

130-
export function share(shareObjects: Config[], packageJsonPath = ''): Config {
133+
export function share(shareObjects: Config, packageJsonPath = ''): Config {
131134

132135
if (!packageJsonPath) {
133136
const stack = callsite();
@@ -164,4 +167,3 @@ export function share(shareObjects: Config[], packageJsonPath = ''): Config {
164167

165168
return result;
166169
}
167-

0 commit comments

Comments
 (0)