Skip to content

Commit

Permalink
feat(angular): add federate-module generator (#19523)
Browse files Browse the repository at this point in the history
  • Loading branch information
Coly010 authored Oct 12, 2023
1 parent b8f741a commit 00ebc0b
Show file tree
Hide file tree
Showing 19 changed files with 754 additions and 6 deletions.
8 changes: 8 additions & 0 deletions docs/generated/manifests/menus.json
Original file line number Diff line number Diff line change
Expand Up @@ -6079,6 +6079,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "federate-module",
"path": "/nx-api/angular/generators/federate-module",
"name": "federate-module",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "init",
"path": "/nx-api/angular/generators/init",
Expand Down
9 changes: 9 additions & 0 deletions docs/generated/manifests/nx-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@
"path": "/nx-api/angular/generators/directive",
"type": "generator"
},
"/nx-api/angular/generators/federate-module": {
"description": "Create a federated module, which is exposed by a remote and can be subsequently loaded by a host.",
"file": "generated/packages/angular/generators/federate-module.json",
"hidden": false,
"name": "federate-module",
"originalFilePath": "/packages/angular/src/generators/federate-module/schema.json",
"path": "/nx-api/angular/generators/federate-module",
"type": "generator"
},
"/nx-api/angular/generators/init": {
"description": "Initializes the `@nrwl/angular` plugin.",
"file": "generated/packages/angular/generators/init.json",
Expand Down
9 changes: 9 additions & 0 deletions docs/generated/packages-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@
"path": "angular/generators/directive",
"type": "generator"
},
{
"description": "Create a federated module, which is exposed by a remote and can be subsequently loaded by a host.",
"file": "generated/packages/angular/generators/federate-module.json",
"hidden": false,
"name": "federate-module",
"originalFilePath": "/packages/angular/src/generators/federate-module/schema.json",
"path": "angular/generators/federate-module",
"type": "generator"
},
{
"description": "Initializes the `@nrwl/angular` plugin.",
"file": "generated/packages/angular/generators/init.json",
Expand Down
86 changes: 86 additions & 0 deletions docs/generated/packages/angular/generators/federate-module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"name": "federate-module",
"factory": "./src/generators/federate-module/federate-module",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxReactFederateModule",
"title": "Federate Module",
"description": "Create a federated module, which is exposed by a remote and can be subsequently loaded by a host.",
"examples": [
{
"command": "nx g federate-module MyModule --path=./src/component/my-cmp.ts --remote=my-remote-app",
"description": "Create a federated module from my-remote-app, that exposes my-cmp from ./src/component/my-cmp.ts as MyModule."
}
],
"type": "object",
"properties": {
"name": {
"description": "The name of the module.",
"type": "string",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the module?",
"pattern": "^[a-zA-Z][^:]*$",
"x-priority": "important"
},
"path": {
"type": "string",
"description": "The path to locate the federated module.",
"x-prompt": "What is the path to the module to be federated?"
},
"remote": {
"type": "string",
"description": "The name of the remote.",
"x-prompt": "What is/should the remote be named?"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"style": {
"description": "The file extension to be used for style files for the remote if one needs to be created.",
"type": "string",
"default": "css",
"enum": ["css", "scss", "sass", "less"]
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests of the remote if it needs to be created.",
"default": "jest"
},
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
"description": "Test runner to use for end to end (e2e) tests of the remote if it needs to be created.",
"default": "cypress"
},
"standalone": {
"description": "Whether to generate the remote application with standalone components if it needs to be created. _Note: This is only supported in Angular versions >= 14.1.0_",
"type": "boolean",
"default": false
},
"host": {
"type": "string",
"description": "The host / shell application for this remote."
}
},
"required": ["name", "path", "remote"],
"additionalProperties": false,
"presets": []
},
"x-type": "application",
"description": "Create a federated module, which is exposed by a remote and can be subsequently loaded by a host.",
"implementation": "/packages/angular/src/generators/federate-module/federate-module.ts",
"aliases": [],
"hidden": false,
"path": "/packages/angular/src/generators/federate-module/schema.json",
"type": "generator"
}
1 change: 1 addition & 0 deletions docs/shared/reference/sitemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@
- [component-story](/nx-api/angular/generators/component-story)
- [component-test](/nx-api/angular/generators/component-test)
- [directive](/nx-api/angular/generators/directive)
- [federate-module](/nx-api/angular/generators/federate-module)
- [init](/nx-api/angular/generators/init)
- [library](/nx-api/angular/generators/library)
- [library-secondary-entry-point](/nx-api/angular/generators/library-secondary-entry-point)
Expand Down
95 changes: 89 additions & 6 deletions e2e/angular-core/src/module-federation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
readJson,
runCLI,
runCommandUntil,
runE2ETests,
uniq,
updateFile,
} from '@nx/e2e/utils';
Expand Down Expand Up @@ -273,13 +274,19 @@ describe('Angular Module Federation', () => {
);

// check default generated host is built successfully
const buildOutputSwc = runCLI(`build ${hostApp}`);
expect(buildOutputSwc).toContain('Successfully ran target build');
const buildOutputSwc = await runCommandUntil(`build ${hostApp}`, (output) =>
output.includes('Successfully ran target build')
);
await killProcessAndPorts(buildOutputSwc.pid);

const buildOutputTsNode = runCLI(`build ${hostApp}`, {
env: { NX_PREFER_TS_NODE: 'true' },
});
expect(buildOutputTsNode).toContain('Successfully ran target build');
const buildOutputTsNode = await runCommandUntil(
`build ${hostApp}`,
(output) => output.includes('Successfully ran target build'),
{
env: { NX_PREFER_TS_NODE: 'true' },
}
);
await killProcessAndPorts(buildOutputTsNode.pid);

const processSwc = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`,
Expand All @@ -302,4 +309,80 @@ describe('Angular Module Federation', () => {

await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
}, 20_000_000);

it('should federate a module from a library and update an existing remote', async () => {
const lib = uniq('lib');
const remote = uniq('remote');
const module = uniq('module');
const host = uniq('host');

runCLI(
`generate @nx/angular:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
);

runCLI(
`generate @nx/js:lib ${lib} --no-interactive --projectNameAndRootFormat=as-provided`
);

// Federate Module
runCLI(
`generate @nx/angular:federate-module ${module} --remote=${remote} --path=${lib}/src/index.ts --no-interactive`
);

updateFile(`${lib}/src/index.ts`, `export { isEven } from './lib/${lib}';`);
updateFile(
`${lib}/src/lib/${lib}.ts`,
`export function isEven(num: number) { return num % 2 === 0; }`
);

// Update Host to use the module
updateFile(
`${host}/src/app/app.component.ts`,
`
import { Component } from '@angular/core';
import { isEven } from '${remote}/${module}';
@Component({
selector: 'proj-root',
template: \`<div class="host">{{title}}</div>\`
})
export class AppComponent {
title = \`shell is \${isEven(2) ? 'even' : 'odd'}\`;
}`
);

// Update e2e test to check the module
updateFile(
`${host}-e2e/src/e2e/app.cy.ts`,
`
describe('${host}', () => {
beforeEach(() => cy.visit('/'));
it('should display contain the remote library', () => {
expect(cy.get('div.host')).to.exist;
expect(cy.get('div.host').contains('shell is even'));
});
});
`
);

// Build host and remote
const buildOutput = await runCommandUntil(`build ${host}`, (output) =>
output.includes('Successfully ran target build')
);
await killProcessAndPorts(buildOutput.pid);
const remoteOutput = await runCommandUntil(`build ${remote}`, (output) =>
output.includes('Successfully ran target build')
);
await killProcessAndPorts(remoteOutput.pid);

if (runE2ETests()) {
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid);
}
}, 500_000);
});
12 changes: 12 additions & 0 deletions packages/angular/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
"schema": "./src/generators/convert-to-with-mf/schema.json",
"description": "Converts an old micro frontend configuration to use the new `withModuleFederation` helper. It will run successfully if the following conditions are met: \n - Is either a host or remote application \n - Shared npm package configurations have not been modified \n - Name used to identify the Micro Frontend application matches the project name \n\n{% callout type=\"warning\" title=\"Overrides\" %}This generator will overwrite your webpack config. If you have additional custom configuration in your config file, it will be lost!{% /callout %}"
},
"federate-module": {
"factory": "./src/generators/federate-module/federate-module.compat",
"schema": "./src/generators/federate-module/schema.json",
"x-type": "application",
"description": "Create a federated module, which is exposed by a remote and can be subsequently loaded by a host."
},
"host": {
"factory": "./src/generators/host/host.compat",
"schema": "./src/generators/host/schema.json",
Expand Down Expand Up @@ -193,6 +199,12 @@
"aliases": ["d"],
"description": "Generate an Angular directive."
},
"federate-module": {
"factory": "./src/generators/federate-module/federate-module",
"schema": "./src/generators/federate-module/schema.json",
"x-type": "application",
"description": "Create a federated module, which is exposed by a remote and can be subsequently loaded by a host."
},
"init": {
"factory": "./src/generators/init/init",
"schema": "./src/generators/init/schema.json",
Expand Down
1 change: 1 addition & 0 deletions packages/angular/generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './src/generators/component-cypress-spec/component-cypress-spec';
export * from './src/generators/component-story/component-story';
export * from './src/generators/component/component';
export * from './src/generators/directive/directive';
export * from './src/generators/federate-module/federate-module';
export * from './src/generators/host/host';
export * from './src/generators/init/init';
export * from './src/generators/library-secondary-entry-point/library-secondary-entry-point';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { convertNxGenerator } from '@nx/devkit';
import { warnForSchematicUsage } from '../utils/warn-for-schematic-usage';
import federateModule from './federate-module';

export default warnForSchematicUsage(convertNxGenerator(federateModule));
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { getProjects, Tree } from '@nx/devkit';
import { Schema } from './schema';
import { Schema as remoteSchma } from '../remote/schema';
import { federateModuleGenerator } from './federate-module';
import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
import { Linter } from '@nx/linter';
import remoteGenerator from '../remote/remote';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';

describe('federate-module', () => {
let schema: Schema = {
name: 'my-federated-module',
remote: 'my-remote',
path: 'apps/my-remote/src/my-federated-module.ts',
};

describe('no remote', () => {
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });

beforeEach(() => {
tree.write(schema.path, `export const isEven = true;`);
});

it('should generate a remote and e2e and should contain an entry for the new path for module federation', async () => {
await federateModuleGenerator(tree, schema);

const projects = getProjects(tree);

expect(projects.get('my-remote').root).toEqual('apps/my-remote');

expect(tree.exists('apps/my-remote/module-federation.config.ts')).toBe(
true
);

const content = tree.read(
'apps/my-remote/module-federation.config.ts',
'utf-8'
);
expect(content).toContain(
`'./my-federated-module': 'apps/my-remote/src/my-federated-module.ts'`
);

const tsconfig = JSON.parse(tree.read('tsconfig.base.json', 'utf-8'));
expect(
tsconfig.compilerOptions.paths['my-remote/my-federated-module']
).toEqual(['apps/my-remote/src/my-federated-module.ts']);
});
});

describe('with remote', () => {
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
let remoteSchema: remoteSchma = {
name: 'my-remote',
e2eTestRunner: E2eTestRunner.Cypress,
skipFormat: true,
linter: Linter.EsLint,
style: 'css',
unitTestRunner: UnitTestRunner.Jest,
};

beforeEach(async () => {
remoteSchema.name = uniq('remote');
await remoteGenerator(tree, remoteSchema);
tree.write(schema.path, `export const isEven = true;`);
});

it('should append the new path to the module federation config', async () => {
let content = tree.read(
`apps/${remoteSchema.name}/module-federation.config.ts`,
'utf-8'
);

expect(content).not.toContain(
`'./my-federated-module': 'apps/my-remote/src/my-federated-module.ts'`
);

await federateModuleGenerator(tree, {
...schema,
remote: remoteSchema.name,
});

content = tree.read(
`apps/${remoteSchema.name}/module-federation.config.ts`,
'utf-8'
);
expect(content).toContain(
`'./my-federated-module': 'apps/my-remote/src/my-federated-module.ts'`
);

const tsconfig = JSON.parse(tree.read('tsconfig.base.json', 'utf-8'));
expect(
tsconfig.compilerOptions.paths[
`${remoteSchema.name}/my-federated-module`
]
).toEqual(['apps/my-remote/src/my-federated-module.ts']);
});
});
});

function uniq(prefix: string) {
return `${prefix}${Math.floor(Math.random() * 10000000)}`;
}
Loading

1 comment on commit 00ebc0b

@vercel
Copy link

@vercel vercel bot commented on 00ebc0b Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-five.vercel.app
nx.dev
nx-dev-git-master-nrwl.vercel.app
nx-dev-nrwl.vercel.app

Please sign in to comment.