Skip to content

Commit

Permalink
feat(react): add Generator for MFE Host
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicholas Cunningham authored and Nicholas Cunningham committed Mar 19, 2022
1 parent e54e36e commit ecaca46
Show file tree
Hide file tree
Showing 25 changed files with 537 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/react/generators.json
Expand Up @@ -162,6 +162,12 @@
"schema": "./src/generators/hook/schema.json",
"description": "Create a hook",
"aliases": "c"
},

"mfe-host": {
"factory": "./src/generators/mfe-host/mfe-host#mfeHostGenerator",
"schema": "./src/generators/mfe-host/schema.json",
"description": "Generate a Host React Application"
}
}
}
1 change: 1 addition & 0 deletions packages/react/index.ts
Expand Up @@ -16,4 +16,5 @@ export { reduxGenerator } from './src/generators/redux/redux';
export { storiesGenerator } from './src/generators/stories/stories';
export { storybookConfigurationGenerator } from './src/generators/storybook-configuration/configuration';
export { storybookMigration5to6Generator } from './src/generators/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6';
export { mfeHostGenerator } from './src/generators/mfe-host/mfe-host';
export type { SupportedStyles } from './typings/style';
139 changes: 139 additions & 0 deletions packages/react/plugins/with-module-federation/index.ts
@@ -0,0 +1,139 @@
import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import { readWorkspaceJson } from '@nrwl/workspace/src/core/file-utils';
import {
ProjectGraph,
ProjectGraphDependency,
} from '@nrwl/tao/src/shared/project-graph';
import { WorkspaceJsonConfiguration } from '@nrwl/tao/src/shared/workspace';

const { ModuleFederationPlugin } = require('webpack').container;
const reactWebpackConfig = require('../webpack');

export type MFERemotes = string[] | [remoteName: string, remoteUrl: string][];

export type MFELibrary = { type: string; name: string };

export interface SharedLibraryConfig {
singleton?: boolean;
strictVersion?: boolean;
requiredVersion?: string;
eager?: boolean;
}

export interface MfeOptions {
name: string;
filename: string;
remotes?: string[];
library?: MFELibrary;
exposes?: Record<string, string>;
shared?: (
library: string,
config: SharedLibraryConfig
) => undefined | false | SharedLibraryConfig;
}

function getSharedDependencies(graph: ProjectGraph<any>, options: MfeOptions) {
const dependencies = graph.dependencies[options.name];
const sharedDependencies = dependencies.reduce((acc, dep) => {
//npm libs
if (graph.externalNodes[dep.target]) {
const externalNode = graph.externalNodes[dep.target];
acc[externalNode.data.packageName] = {
singleton: true,
requiredVersion: externalNode.data.version,
};
}

// workspace libs
if (graph.nodes[dep.target]) {
const node = graph.nodes[dep.target];
if (node.data.projectType !== 'application') {
acc[node.name] = {
requiredVersion: false,
};
}
}
return acc;
}, {});

if (options.shared) {
for (const [library, config] of Object.entries(sharedDependencies)) {
const shouldKeepDependency = options.shared(library, config);
if (shouldKeepDependency == false) {
delete sharedDependencies[library];
continue;
}
sharedDependencies[library] = shouldKeepDependency ?? config;
}
}
return sharedDependencies;
}

function getRemotes(options: MfeOptions, ws: WorkspaceJsonConfiguration) {
return options.remotes.reduce((acc, name) => {
const project = ws.projects[name];

if (!project) {
throw Error(
`Cannot find remote project "${options.name}". Check that the name is correct in mfe.config.js`
);
}
const serveOptions = project?.targets?.serve.options;
if (serveOptions) {
acc[name] = `${name}@//${serveOptions.host ?? 'localhost'}:${
serveOptions.port ?? 4200
}/remoteEntry.js`;
}
return acc;
}, {});
}

// This is probably something linked to a Schema
function withModuleFederation(options: MfeOptions) {
const ws = readWorkspaceJson();
const graph = readCachedProjectGraph();
const project = ws.projects[options.name];

if (!project) {
throw Error(
`Cannot find project "${options.name}". Check that the name is correct in mfe.config.js`
);
}

const mfeOptions = {
name: options.name,
filename: 'remoteEntry.js',
exposes: {},
} as any;

if (options.library) {
mfeOptions.library = options.library;
}

mfeOptions.shared = getSharedDependencies(graph, options);

if (options.remotes) {
mfeOptions.remotes = getRemotes(options, ws);
}

if (options.exposes) {
mfeOptions.exposes = options.exposes;
}

return (config, options) => {
config = reactWebpackConfig(config);
config.output.uniqueName = options.name;
config.output.publicPath = 'auto';

config.optimization = {
runtimeChunk: false,
minimize: false,
};

config.plugins.push(new ModuleFederationPlugin(mfeOptions));

return config;
};
}

module.exports = withModuleFederation;
@@ -0,0 +1,13 @@
{
"presets": [
[
"@nrwl/react/babel", {
"runtime": "automatic"<% if (style === '@emotion/styled') { %>,<% } %>
<% if (style === '@emotion/styled') { %>"importSource": "@emotion/react" <% } %>
}
]
],
"plugins": [
<% if (style === 'styled-components') { %>["styled-components", { "pure": true, "ssr": true }]<% } %><% if (style === 'styled-jsx') { %>"styled-jsx/babel"<% } %><% if (style === '@emotion/styled') { %>"@emotion/babel-plugin"<% } %>
]
}
@@ -0,0 +1,16 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.

last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.
@@ -0,0 +1,26 @@
import { render } from '@testing-library/react';
<% if (routing) { %>
import { BrowserRouter } from 'react-router-dom';
<% } %>

import App from './app';

describe('App', () => {
it('should render successfully', () => {
<% if (routing) { %>
const { baseElement } = render(<BrowserRouter><App /></BrowserRouter>);
<% } else { %>
const { baseElement } = render(<App />);
<% } %>
expect(baseElement).toBeTruthy();
});

it('should have a greeting as the title', () => {
<% if (routing) { %>
const { getByText } = render(<BrowserRouter><App /></BrowserRouter>);
<% } else { %>
const { getByText } = render(<App />);
<% } %>
expect(getByText(/Welcome <%= projectName %>/gi)).toBeTruthy();
});
});
Empty file.
@@ -0,0 +1,3 @@
export const environment = {
production: true
};
@@ -0,0 +1,6 @@
// This file can be replaced during build by using the `fileReplacements` array.
// When building for production, this file is replaced with `environment.prod.ts`.

export const environment = {
production: false
};
Binary file not shown.
14 changes: 14 additions & 0 deletions packages/react/src/generators/mfe-host/files/common/src/index.html
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title><%= className %></title>
<base href="/" />

<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="packages/react/src/generators/mfe-host/files/common/src/favicon.ico" />
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,7 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"types": ["node"]
},
"files": [
<% if (style === 'styled-jsx') { %>"<%= offsetFromRoot %>node_modules/@nrwl/react/typings/styled-jsx.d.ts",<% } %>
"<%= offsetFromRoot %>node_modules/@nrwl/react/typings/cssmodule.d.ts",
"<%= offsetFromRoot %>node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", "**/*.spec.js", "**/*.test.js", "**/*.spec.jsx", "**/*.test.jsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}
@@ -0,0 +1,17 @@
{
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
<% if (style === '@emotion/styled') { %>"jsxImportSource": "@emotion/react",<% } %>
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
]
}
@@ -0,0 +1,6 @@
module.exports = {
name: '<%= projectName %>',
remotes: [
<% remotes.forEach(function(remote) {%> "<%= remote %>", <% }); %>
],
}
@@ -0,0 +1,7 @@
<% if (strict) { %>import { StrictMode } from 'react';<% } %>
import * as ReactDOM from 'react-dom';
<% if (routing) { %>import { BrowserRouter } from 'react-router-dom';<% } %>

import App from './app/<%= fileName %>';

ReactDOM.render(<% if (strict) { %><StrictMode><% } %><% if (routing) { %><BrowserRouter><% } %><App /><% if (routing) { %></BrowserRouter><% } %><% if (strict) { %></StrictMode><% } %>, document.getElementById('root'));
@@ -0,0 +1 @@
import('./bootstrap');
@@ -0,0 +1,4 @@
// Declare your remote Modules here
// Example declare module 'about/Module';

<% remotes.forEach(function(remote) { %>declare module '<%= remote %>/Module';<% }); %>
@@ -0,0 +1,6 @@
const withModuleFederation = require('@nrwl/react/plugins/with-module-federation');
const mfeConfig = require('./mfe.config');

module.exports = withModuleFederation({
...mfeConfig,
});
@@ -0,0 +1 @@
module.exports = require('./webpack.config');
25 changes: 25 additions & 0 deletions packages/react/src/generators/mfe-host/lib/add-mfe.ts
@@ -0,0 +1,25 @@
import { NormalizedSchema } from '@nrwl/react/src/generators/application/schema';
import { generateFiles, names } from '@nrwl/devkit';
import { join } from 'path';

export function addMFEFiles(host, options: NormalizedSchema) {
if (host.exists(`apps/${options.projectName}/src/main.tsx`)) {
host.rename(
`apps/${options.projectName}/src/main.tsx`,
`apps/${options.projectName}/src/bootstrap.tsx`
);
}

const templateVariables = {
...names(options.name),
...options,
tmpl: '',
};

generateFiles(
host,
join(__dirname, `../files/mfe`),
options.appProjectRoot,
templateVariables
);
}
Empty file.
35 changes: 35 additions & 0 deletions packages/react/src/generators/mfe-host/mfe-host.ts
@@ -0,0 +1,35 @@
import {
formatFiles,
generateFiles,
names,
Tree,
updateJson,
} from '@nrwl/devkit';
import { Schema } from './schema';
import applicationGenerator from '../application/application';
import { normalizeOptions } from '@nrwl/react/src/generators/application/lib/normalize-options';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { addMFEFiles } from '@nrwl/react/src/generators/mfe-host/lib/add-mfe';
import { NormalizedSchema } from '@nrwl/react/src/generators/application/schema';

export function updateProjectJson(host: Tree, options: NormalizedSchema) {
updateJson(host, `${options.appProjectRoot}/project.json`, (json) => {
json.targets.build.options[
'webpackConfig'
] = `${options.appProjectRoot}/webpack.config.js`;
return json;
});
}

export async function mfeHostGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema);
const initApp = await applicationGenerator(host, options);
addMFEFiles(host, options);
updateProjectJson(host, options);

if (!options.skipFormat) {
await formatFiles(host);
}

return runTasksInSerial(initApp);
}

0 comments on commit ecaca46

Please sign in to comment.