Skip to content

Commit 12528da

Browse files
committed
dynamic mf, sharing libs, bootstrap/main
1 parent b4d3837 commit 12528da

File tree

11 files changed

+211
-14
lines changed

11 files changed

+211
-14
lines changed

package-lock.json

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
},
2828
"private": true,
2929
"dependencies": {
30-
"cross-spawn": "^7.0.3"
30+
"cross-spawn": "^7.0.3",
31+
"json5": "^2.1.3"
3132
},
3233
"devDependencies": {
3334
"@angular/cli": "^10.1.6",

packages/mf/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Big thanks to the following people who helped to make this possible:
1111

1212
## Prequisites
1313

14-
- Angular CLI 11 (currently BETA)
14+
- Angular CLI 11
1515

1616
## Motivation 💥
1717

packages/mf/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angular-architects/module-federation",
3-
"version": "1.1.0",
3+
"version": "1.2.0-rc.5",
44
"license": "MIT",
55
"repository": {
66
"type": "GitHub",

packages/mf/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './utils/dynamic-federation';

packages/mf/src/schematics/mf/schematic.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ import {
33
Rule,
44
externalSchematic,
55
} from '@angular-devkit/schematics';
6-
import {
7-
updateWorkspace,
8-
} from '@nrwl/workspace';
96

107
import { spawn } from 'cross-spawn';
118
import * as path from 'path';
129

13-
import { createConfig } from '../../create-config';
10+
import { createConfig } from '../../utils/create-config';
1411
import { prodConfig } from './prod-config';
1512
import { MfSchematicSchema } from './schema';
1613

@@ -50,6 +47,25 @@ export function add(options: MfSchematicSchema): Rule {
5047
return config(options);
5148
}
5249

50+
51+
function makeMainAsync(main: string): Rule {
52+
return async function (tree, context) {
53+
54+
const mainPath = path.dirname(main);
55+
const bootstrapName = path.join(mainPath, 'bootstrap.ts');
56+
57+
if (tree.exists(bootstrapName)) {
58+
console.info(`${bootstrapName} already exists.`);
59+
return;
60+
}
61+
62+
const mainContent = tree.read(main);
63+
tree.create(bootstrapName, mainContent);
64+
tree.overwrite(main, "import('./bootstrap');")
65+
66+
}
67+
}
68+
5369
export default function config (options: MfSchematicSchema): Rule {
5470

5571
return async function (tree) {
@@ -77,12 +93,21 @@ export default function config (options: MfSchematicSchema): Rule {
7793
const configPath = path.join(projectRoot, 'webpack.config.js').replace(/\\/g, '/');
7894
const configProdPath = path.join(projectRoot, 'webpack.prod.config.js').replace(/\\/g, '/');
7995
const port = parseInt(options.port);
96+
const main = projectConfig.architect.build.options.main;
97+
98+
const relWorkspaceRoot = path.relative(projectRoot, '');
99+
const tsConfigName = tree.exists('tsconfig.base.json') ?
100+
'tsconfig.base.json' : 'tsconfig.json';
101+
102+
const relTsConfigPath = path
103+
.join(relWorkspaceRoot, tsConfigName)
104+
.replace(/\\/g, '/');
80105

81106
if (isNaN(port)) {
82107
throw new Error(`Port must be a number!`);
83108
}
84109

85-
const webpackConfig = createConfig(projectName, projectRoot, port);
110+
const webpackConfig = createConfig(projectName, relTsConfigPath, projectRoot, port);
86111

87112
tree.create(configPath, webpackConfig);
88113
tree.create(configProdPath, prodConfig);
@@ -97,6 +122,7 @@ export default function config (options: MfSchematicSchema): Rule {
97122
tree.overwrite('angular.json', JSON.stringify(workspace, null, '\t'));
98123

99124
return chain([
125+
makeMainAsync(main),
100126
externalSchematic('ngx-build-plus', 'ng-add', { project: options.project }),
101127
// updateWorkspace((workspace) => {
102128
// const proj = workspace.projects.get(options.project);

packages/mf/src/create-config.ts renamed to packages/mf/src/utils/create-config.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
export function createConfig(projectName: string, root: string, port: number): string {
1+
export function createConfig(projectName: string, tsConfigName: string, root: string, port: number): string {
22

33
return `const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
4+
const mf = require("@angular-architects/module-federation/webpack");
5+
const path = require("path");
6+
7+
const sharedMappings = new mf.SharedMappings();
8+
sharedMappings.register(path.join(__dirname, '${tsConfigName}'));
49
510
module.exports = {
611
output: {
@@ -28,10 +33,15 @@ module.exports = {
2833
shared: {
2934
"@angular/core": { singleton: true, strictVersion: true },
3035
"@angular/common": { singleton: true, strictVersion: true },
31-
"@angular/router": { singleton: true, strictVersion: true }
36+
"@angular/router": { singleton: true, strictVersion: true },
37+
38+
// Uncomment for sharing lib of an Angular CLI or Nx workspace
39+
// ...sharedMappings.getDescriptors()
3240
}
3341
34-
})
42+
}),
43+
// Uncomment for sharing lib of an Angular CLI or Nx workspace
44+
// sharedMappings.getPlugin(),
3545
],
3646
};
3747
`;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const moduleMap = {};
2+
const remoteMap = {}
3+
let isDefaultScopeInitialized = false;
4+
5+
async function lookupExposedModule<T>(remoteName: string, exposedModule: string): Promise<T> {
6+
const container = window[remoteName] as Container;
7+
const factory = await container.get(exposedModule);
8+
const Module = factory();
9+
return Module as T;
10+
}
11+
12+
async function initRemote(remoteName: string) {
13+
const container = window[remoteName] as Container;
14+
15+
// Do we still need to initialize the remote?
16+
if (remoteMap[remoteName]) {
17+
return container;
18+
}
19+
20+
// Do we still need to initialize the share scope?
21+
if (!isDefaultScopeInitialized) {
22+
await __webpack_init_sharing__('default');
23+
isDefaultScopeInitialized = true;
24+
}
25+
26+
await container.init(__webpack_share_scopes__.default);
27+
remoteMap[remoteName] = true;
28+
return container;
29+
}
30+
31+
export type LoadRemoteModuleOptions = {
32+
remoteEntry?: string;
33+
remoteName: string;
34+
exposedModule: string
35+
}
36+
37+
export function loadRemoteEntry(remoteEntry: string, remoteName: string): Promise<void> {
38+
return new Promise<any>((resolve, reject) => {
39+
40+
// Is remoteEntry already loaded?
41+
if (moduleMap[remoteEntry]) {
42+
resolve();
43+
return;
44+
}
45+
46+
const script = document.createElement('script');
47+
script.src = remoteEntry;
48+
49+
script.onerror = reject;
50+
51+
script.onload = () => {
52+
initRemote(remoteName);
53+
moduleMap[remoteEntry] = true;
54+
resolve();
55+
}
56+
57+
document.body.append(script);
58+
});
59+
}
60+
61+
export async function loadRemoteModule<T = any>(options: LoadRemoteModuleOptions): Promise<T> {
62+
if (options.remoteEntry) {
63+
await loadRemoteEntry(options.remoteEntry, options.remoteName);
64+
}
65+
return await lookupExposedModule<T>(options.remoteName, options.exposedModule);
66+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {NormalModuleReplacementPlugin} from 'webpack';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
import * as JSON5 from 'json5';
5+
6+
7+
interface KeyValuePair {
8+
key: string;
9+
value: string;
10+
}
11+
12+
export class SharedMappings {
13+
14+
private mappings: KeyValuePair[] = [];
15+
16+
register(tsConfigPath: string, shared: string[] = null): void {
17+
18+
if (!path.isAbsolute(tsConfigPath)) {
19+
throw new Error('SharedMappings.register: tsConfigPath needs to be an absolute path!');
20+
}
21+
22+
const tsConfig = JSON5.parse(
23+
fs.readFileSync(tsConfigPath, {encoding: 'UTF8'}));
24+
const mappings = tsConfig?.compilerOptions?.paths;
25+
const rootPath = path.normalize(path.dirname(tsConfigPath));
26+
27+
if (!mappings) {
28+
return;
29+
}
30+
31+
for (const key in mappings) {
32+
if (!shared || shared.length === 0 || shared.includes(key)) {
33+
this.mappings.push({
34+
key,
35+
value: path.normalize(path.join(rootPath, mappings[key][0]))
36+
});
37+
}
38+
}
39+
}
40+
41+
getPlugin(): NormalModuleReplacementPlugin {
42+
return new NormalModuleReplacementPlugin(/./, (req) => {
43+
const from = req.context;
44+
const to = path.normalize(path.join(req.context, req.request));
45+
46+
if (!req.request.startsWith('.')) return;
47+
48+
for (const m of this.mappings) {
49+
const libFolder = path.normalize(path.dirname(m.value));
50+
if (!from.startsWith(libFolder) && to.startsWith(libFolder)) {
51+
req.request = m.key;
52+
// console.log('remapping', { from, to, libFolder });
53+
}
54+
}
55+
});
56+
}
57+
58+
getDescriptors(): object {
59+
const result = {};
60+
61+
for (const m of this.mappings) {
62+
result[m.key] = {
63+
import: m.value,
64+
requiredVersion: false
65+
};
66+
}
67+
68+
return result;
69+
}
70+
71+
getDescriptor(mappedPath: string, requiredVersion: string = null): any {
72+
73+
if (!this.mappings[mappedPath]) {
74+
throw new Error('No mapping found for ' + mappedPath + ' in tsconfig');
75+
}
76+
77+
return ({
78+
[mappedPath]: {
79+
import: this.mappings[mappedPath],
80+
requiredVersion: requiredVersion ?? false
81+
}
82+
});
83+
}
84+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Scope = unknown;
2+
type Factory = () => any;
3+
4+
type Container = {
5+
init(shareScope: Scope): void;
6+
get(module: string): Factory;
7+
};
8+
9+
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
10+
declare const __webpack_share_scopes__: { default: Scope };

0 commit comments

Comments
 (0)