Skip to content

Commit

Permalink
dynamic mf, sharing libs, bootstrap/main
Browse files Browse the repository at this point in the history
  • Loading branch information
manfredsteyer committed Nov 20, 2020
1 parent b4d3837 commit 12528da
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 14 deletions.
4 changes: 1 addition & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
},
"private": true,
"dependencies": {
"cross-spawn": "^7.0.3"
"cross-spawn": "^7.0.3",
"json5": "^2.1.3"
},
"devDependencies": {
"@angular/cli": "^10.1.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/mf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Big thanks to the following people who helped to make this possible:

## Prequisites

- Angular CLI 11 (currently BETA)
- Angular CLI 11

## Motivation 💥

Expand Down
2 changes: 1 addition & 1 deletion packages/mf/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@angular-architects/module-federation",
"version": "1.1.0",
"version": "1.2.0-rc.5",
"license": "MIT",
"repository": {
"type": "GitHub",
Expand Down
1 change: 1 addition & 0 deletions packages/mf/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './utils/dynamic-federation';
36 changes: 31 additions & 5 deletions packages/mf/src/schematics/mf/schematic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ import {
Rule,
externalSchematic,
} from '@angular-devkit/schematics';
import {
updateWorkspace,
} from '@nrwl/workspace';

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

import { createConfig } from '../../create-config';
import { createConfig } from '../../utils/create-config';
import { prodConfig } from './prod-config';
import { MfSchematicSchema } from './schema';

Expand Down Expand Up @@ -50,6 +47,25 @@ export function add(options: MfSchematicSchema): Rule {
return config(options);
}


function makeMainAsync(main: string): Rule {
return async function (tree, context) {

const mainPath = path.dirname(main);
const bootstrapName = path.join(mainPath, 'bootstrap.ts');

if (tree.exists(bootstrapName)) {
console.info(`${bootstrapName} already exists.`);
return;
}

const mainContent = tree.read(main);
tree.create(bootstrapName, mainContent);
tree.overwrite(main, "import('./bootstrap');")

}
}

export default function config (options: MfSchematicSchema): Rule {

return async function (tree) {
Expand Down Expand Up @@ -77,12 +93,21 @@ export default function config (options: MfSchematicSchema): Rule {
const configPath = path.join(projectRoot, 'webpack.config.js').replace(/\\/g, '/');
const configProdPath = path.join(projectRoot, 'webpack.prod.config.js').replace(/\\/g, '/');
const port = parseInt(options.port);
const main = projectConfig.architect.build.options.main;

const relWorkspaceRoot = path.relative(projectRoot, '');
const tsConfigName = tree.exists('tsconfig.base.json') ?
'tsconfig.base.json' : 'tsconfig.json';

const relTsConfigPath = path
.join(relWorkspaceRoot, tsConfigName)
.replace(/\\/g, '/');

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

const webpackConfig = createConfig(projectName, projectRoot, port);
const webpackConfig = createConfig(projectName, relTsConfigPath, projectRoot, port);

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

return chain([
makeMainAsync(main),
externalSchematic('ngx-build-plus', 'ng-add', { project: options.project }),
// updateWorkspace((workspace) => {
// const proj = workspace.projects.get(options.project);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export function createConfig(projectName: string, root: string, port: number): string {
export function createConfig(projectName: string, tsConfigName: string, root: string, port: number): string {

return `const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, '${tsConfigName}'));
module.exports = {
output: {
Expand Down Expand Up @@ -28,10 +33,15 @@ module.exports = {
shared: {
"@angular/core": { singleton: true, strictVersion: true },
"@angular/common": { singleton: true, strictVersion: true },
"@angular/router": { singleton: true, strictVersion: true }
"@angular/router": { singleton: true, strictVersion: true },
// Uncomment for sharing lib of an Angular CLI or Nx workspace
// ...sharedMappings.getDescriptors()
}
})
}),
// Uncomment for sharing lib of an Angular CLI or Nx workspace
// sharedMappings.getPlugin(),
],
};
`;
Expand Down
66 changes: 66 additions & 0 deletions packages/mf/src/utils/dynamic-federation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const moduleMap = {};
const remoteMap = {}
let isDefaultScopeInitialized = false;

async function lookupExposedModule<T>(remoteName: string, exposedModule: string): Promise<T> {
const container = window[remoteName] as Container;
const factory = await container.get(exposedModule);
const Module = factory();
return Module as T;
}

async function initRemote(remoteName: string) {
const container = window[remoteName] as Container;

// Do we still need to initialize the remote?
if (remoteMap[remoteName]) {
return container;
}

// Do we still need to initialize the share scope?
if (!isDefaultScopeInitialized) {
await __webpack_init_sharing__('default');
isDefaultScopeInitialized = true;
}

await container.init(__webpack_share_scopes__.default);
remoteMap[remoteName] = true;
return container;
}

export type LoadRemoteModuleOptions = {
remoteEntry?: string;
remoteName: string;
exposedModule: string
}

export function loadRemoteEntry(remoteEntry: string, remoteName: string): Promise<void> {
return new Promise<any>((resolve, reject) => {

// Is remoteEntry already loaded?
if (moduleMap[remoteEntry]) {
resolve();
return;
}

const script = document.createElement('script');
script.src = remoteEntry;

script.onerror = reject;

script.onload = () => {
initRemote(remoteName);
moduleMap[remoteEntry] = true;
resolve();
}

document.body.append(script);
});
}

export async function loadRemoteModule<T = any>(options: LoadRemoteModuleOptions): Promise<T> {
if (options.remoteEntry) {
await loadRemoteEntry(options.remoteEntry, options.remoteName);
}
return await lookupExposedModule<T>(options.remoteName, options.exposedModule);
}
84 changes: 84 additions & 0 deletions packages/mf/src/utils/shared-mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {NormalModuleReplacementPlugin} from 'webpack';
import * as path from 'path';
import * as fs from 'fs';
import * as JSON5 from 'json5';


interface KeyValuePair {
key: string;
value: string;
}

export class SharedMappings {

private mappings: KeyValuePair[] = [];

register(tsConfigPath: string, shared: string[] = null): void {

if (!path.isAbsolute(tsConfigPath)) {
throw new Error('SharedMappings.register: tsConfigPath needs to be an absolute path!');
}

const tsConfig = JSON5.parse(
fs.readFileSync(tsConfigPath, {encoding: 'UTF8'}));
const mappings = tsConfig?.compilerOptions?.paths;
const rootPath = path.normalize(path.dirname(tsConfigPath));

if (!mappings) {
return;
}

for (const key in mappings) {
if (!shared || shared.length === 0 || shared.includes(key)) {
this.mappings.push({
key,
value: path.normalize(path.join(rootPath, mappings[key][0]))
});
}
}
}

getPlugin(): NormalModuleReplacementPlugin {
return new NormalModuleReplacementPlugin(/./, (req) => {
const from = req.context;
const to = path.normalize(path.join(req.context, req.request));

if (!req.request.startsWith('.')) return;

for (const m of this.mappings) {
const libFolder = path.normalize(path.dirname(m.value));
if (!from.startsWith(libFolder) && to.startsWith(libFolder)) {
req.request = m.key;
// console.log('remapping', { from, to, libFolder });
}
}
});
}

getDescriptors(): object {
const result = {};

for (const m of this.mappings) {
result[m.key] = {
import: m.value,
requiredVersion: false
};
}

return result;
}

getDescriptor(mappedPath: string, requiredVersion: string = null): any {

if (!this.mappings[mappedPath]) {
throw new Error('No mapping found for ' + mappedPath + ' in tsconfig');
}

return ({
[mappedPath]: {
import: this.mappings[mappedPath],
requiredVersion: requiredVersion ?? false
}
});
}
}
10 changes: 10 additions & 0 deletions packages/mf/src/utils/webpack-runtime-api.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type Scope = unknown;
type Factory = () => any;

type Container = {
init(shareScope: Scope): void;
get(module: string): Factory;
};

declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: { default: Scope };
1 change: 1 addition & 0 deletions packages/mf/src/webpack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './utils/shared-mappings';

0 comments on commit 12528da

Please sign in to comment.