Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(react): update MFE config and routes when adding remote to host (#…
  • Loading branch information
jaysoo committed Apr 5, 2022
1 parent 9579aec commit 70985b1
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 20 deletions.
6 changes: 5 additions & 1 deletion e2e/react/src/react.mfe.test.ts
Expand Up @@ -22,12 +22,16 @@ describe('React MFE', () => {
const remote3 = uniq('remote3');

runCLI(
`generate @nrwl/react:mfe-host ${shell} --style=css --remotes=${remote1},${remote2},${remote3} --no-interactive`
`generate @nrwl/react:mfe-host ${shell} --style=css --remotes=${remote1},${remote2} --no-interactive`
);
runCLI(
`generate @nrwl/react:mfe-remote ${remote3} --style=css --host=${shell} --no-interactive`
);

checkFilesExist(`apps/${shell}/mfe.config.js`);
checkFilesExist(`apps/${remote1}/mfe.config.js`);
checkFilesExist(`apps/${remote2}/mfe.config.js`);
checkFilesExist(`apps/${remote3}/mfe.config.js`);

await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({
combinedOutput: expect.stringContaining('Test Suites: 1 passed, 1 total'),
Expand Down
@@ -0,0 +1,41 @@
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { addProjectConfiguration, Tree } from '@nrwl/devkit';

import { findFreePort } from './find-free-port';

describe('findFreePort', () => {
it('should return the largest port + 1', () => {
const tree = createTreeWithEmptyWorkspace();
addProject(tree, 'app1', 4200);
addProject(tree, 'app2', 4201);
addProject(tree, 'no-serve');

const port = findFreePort(tree);

expect(port).toEqual(4202);
});

it('should default to port 4200', () => {
const tree = createTreeWithEmptyWorkspace();
addProject(tree, 'no-serve');

const port = findFreePort(tree);

expect(port).toEqual(4200);
});
});

function addProject(tree: Tree, name: string, port?: number) {
addProjectConfiguration(tree, name, {
name: 'app1',
root: '/app1',
targets: port
? {
serve: {
executor: '',
options: { port },
},
}
: {},
});
}
14 changes: 14 additions & 0 deletions packages/react/src/generators/application/lib/find-free-port.ts
@@ -0,0 +1,14 @@
import { Tree } from 'nx/src/config/tree';
import { getProjects } from '@nrwl/devkit';

export function findFreePort(host: Tree) {
const projects = getProjects(host);
let port = -Infinity;
for (const [, p] of projects.entries()) {
const curr = p.targets?.serve?.options?.port;
if (typeof curr === 'number') {
port = Math.max(port, curr);
}
}
return port > 0 ? port + 1 : 4200;
}
@@ -1,6 +1,7 @@
import { NormalizedSchema, Schema } from '../schema';
import { assertValidStyle } from '../../../utils/assertion';
import { names, Tree, normalizePath, getWorkspaceLayout } from '@nrwl/devkit';
import { getWorkspaceLayout, names, normalizePath, Tree } from '@nrwl/devkit';
import { findFreePort } from './find-free-port';

export function normalizeOptions(
host: Tree,
Expand Down Expand Up @@ -34,6 +35,7 @@ export function normalizeOptions(
options.unitTestRunner = options.unitTestRunner ?? 'jest';
options.e2eTestRunner = options.e2eTestRunner ?? 'cypress';
options.compiler = options.compiler ?? 'babel';
options.devServerPort ??= findFreePort(host);

return {
...options,
Expand All @@ -45,6 +47,5 @@ export function normalizeOptions(
fileName,
styledModule,
hasStyles: options.style !== 'none',
devServerPort: options.devServerPort ?? 4200,
};
}
@@ -1 +1,12 @@
module.exports = require('./webpack.config');
const withModuleFederation = require('@nrwl/react/module-federation');
const mfeConfig = require('./mfe.config');

module.exports = withModuleFederation({
...mfeConfig,
// Override remote location for production build.
// Each entry is a pair of an unique name and the URL where it is deployed.
// remotes: [
// ['app1', 'http://app1.example.com/'],
// ['app2', 'http://app2.example.com/'],
// ],
});
101 changes: 95 additions & 6 deletions packages/react/src/generators/mfe-host/lib/update-host-with-remote.ts
@@ -1,8 +1,97 @@
import { Tree } from '@nrwl/devkit';
import { Schema } from '../schema';
import {
applyChangesToString,
joinPathFragments,
logger,
names,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import {
addRemoteDefinition,
addRemoteRoute,
addRemoteToMfeConfig,
} from '../../../mfe/mfe-ast-utils';
import * as ts from 'typescript';

export function updateHostWithRemote(host: Tree, options: Schema) {
// find the host project path
// Update remotes inside ${host_path}/src/remotes.d.ts
// Update remotes inside ${host_path}/mfe.config.js
export function updateHostWithRemote(
host: Tree,
hostName: string,
remoteName: string
) {
const hostConfig = readProjectConfiguration(host, hostName);
const mfeConfigPath = joinPathFragments(hostConfig.root, 'mfe.config.js');
const remoteDefsPath = joinPathFragments(
hostConfig.sourceRoot,
'remotes.d.ts'
);
const appComponentPath = findAppComponentPath(host, hostConfig.sourceRoot);

if (host.exists(mfeConfigPath)) {
// find the host project path
// Update remotes inside ${host_path}/src/remotes.d.ts
let sourceCode = host.read(mfeConfigPath).toString();
const source = ts.createSourceFile(
mfeConfigPath,
sourceCode,
ts.ScriptTarget.Latest,
true
);
host.write(
mfeConfigPath,
applyChangesToString(sourceCode, addRemoteToMfeConfig(source, remoteName))
);
} else {
// TODO(jack): Point to the nx.dev guide when ready.
logger.warn(
`Could not find MFE configuration at ${mfeConfigPath}. Did you generate this project with "@nrwl/react:mfe-host"?`
);
}

if (host.exists(remoteDefsPath)) {
let sourceCode = host.read(remoteDefsPath).toString();
const source = ts.createSourceFile(
mfeConfigPath,
sourceCode,
ts.ScriptTarget.Latest,
true
);
host.write(
remoteDefsPath,
applyChangesToString(sourceCode, addRemoteDefinition(source, remoteName))
);
} else {
logger.warn(
`Could not find remote definitions at ${remoteDefsPath}. Did you generate this project with "@nrwl/react:mfe-host"?`
);
}

if (host.exists(appComponentPath)) {
let sourceCode = host.read(appComponentPath).toString();
const source = ts.createSourceFile(
mfeConfigPath,
sourceCode,
ts.ScriptTarget.Latest,
true
);
host.write(
appComponentPath,
applyChangesToString(
sourceCode,
addRemoteRoute(source, names(remoteName))
)
);
} else {
logger.warn(
`Could not find app component at ${appComponentPath}. Did you generate this project with "@nrwl/react:mfe-host"?`
);
}
}

function findAppComponentPath(host: Tree, sourceRoot: string) {
const locations = ['app/app.tsx', 'app/App.tsx', 'app.tsx', 'App.tsx'];
for (const loc of locations) {
if (host.exists(joinPathFragments(sourceRoot, loc))) {
return joinPathFragments(sourceRoot, loc);
}
}
}
2 changes: 2 additions & 0 deletions packages/react/src/generators/mfe-host/mfe-host.ts
Expand Up @@ -42,3 +42,5 @@ export async function mfeHostGenerator(host: Tree, schema: Schema) {

return initTask;
}

export default mfeHostGenerator;
12 changes: 6 additions & 6 deletions packages/react/src/generators/mfe-remote/mfe-remote.ts
Expand Up @@ -28,6 +28,10 @@ export async function mfeRemoteGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema);
const initApp = await applicationGenerator(host, options);

if (schema.host) {
updateHostWithRemote(host, schema.host, options.name);
}

// Module federation requires bootstrap code to be dynamically imported.
// Renaming original entry file so we can use `import(./bootstrap)` in
// new entry file.
Expand All @@ -38,16 +42,12 @@ export async function mfeRemoteGenerator(host: Tree, schema: Schema) {

addMfeFiles(host, options);
updateMfeProject(host, options);
if (schema.host) {
updateHostWithRemote(host, options);
} else {
// Log that no host has been passed in so we will use the default project as the host (Only if through CLI)
// Since Remotes can be generated from the Host Generator we should probably have some identifier to use
}

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

return runTasksInSerial(initApp);
}

export default mfeRemoteGenerator;
4 changes: 0 additions & 4 deletions packages/react/src/generators/mfe-remote/schema.d.ts
Expand Up @@ -9,10 +9,6 @@ export interface Schema {
directory?: string;
tags?: string;
unitTestRunner: 'jest' | 'none';
/**
* @deprecated
*/
babelJest?: boolean;
e2eTestRunner: 'cypress' | 'none';
linter: Linter;
pascalCaseFiles?: boolean;
Expand Down

0 comments on commit 70985b1

Please sign in to comment.