feat(angular): add federate-module generator (#19523)
This commit is contained in:
parent
b8f741abad
commit
00ebc0ba4e
@ -6079,6 +6079,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "federate-module",
|
||||||
|
"path": "/nx-api/angular/generators/federate-module",
|
||||||
|
"name": "federate-module",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "init",
|
"id": "init",
|
||||||
"path": "/nx-api/angular/generators/init",
|
"path": "/nx-api/angular/generators/init",
|
||||||
|
|||||||
@ -177,6 +177,15 @@
|
|||||||
"path": "/nx-api/angular/generators/directive",
|
"path": "/nx-api/angular/generators/directive",
|
||||||
"type": "generator"
|
"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": {
|
"/nx-api/angular/generators/init": {
|
||||||
"description": "Initializes the `@nrwl/angular` plugin.",
|
"description": "Initializes the `@nrwl/angular` plugin.",
|
||||||
"file": "generated/packages/angular/generators/init.json",
|
"file": "generated/packages/angular/generators/init.json",
|
||||||
|
|||||||
@ -172,6 +172,15 @@
|
|||||||
"path": "angular/generators/directive",
|
"path": "angular/generators/directive",
|
||||||
"type": "generator"
|
"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.",
|
"description": "Initializes the `@nrwl/angular` plugin.",
|
||||||
"file": "generated/packages/angular/generators/init.json",
|
"file": "generated/packages/angular/generators/init.json",
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
@ -315,6 +315,7 @@
|
|||||||
- [component-story](/nx-api/angular/generators/component-story)
|
- [component-story](/nx-api/angular/generators/component-story)
|
||||||
- [component-test](/nx-api/angular/generators/component-test)
|
- [component-test](/nx-api/angular/generators/component-test)
|
||||||
- [directive](/nx-api/angular/generators/directive)
|
- [directive](/nx-api/angular/generators/directive)
|
||||||
|
- [federate-module](/nx-api/angular/generators/federate-module)
|
||||||
- [init](/nx-api/angular/generators/init)
|
- [init](/nx-api/angular/generators/init)
|
||||||
- [library](/nx-api/angular/generators/library)
|
- [library](/nx-api/angular/generators/library)
|
||||||
- [library-secondary-entry-point](/nx-api/angular/generators/library-secondary-entry-point)
|
- [library-secondary-entry-point](/nx-api/angular/generators/library-secondary-entry-point)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
readJson,
|
readJson,
|
||||||
runCLI,
|
runCLI,
|
||||||
runCommandUntil,
|
runCommandUntil,
|
||||||
|
runE2ETests,
|
||||||
uniq,
|
uniq,
|
||||||
updateFile,
|
updateFile,
|
||||||
} from '@nx/e2e/utils';
|
} from '@nx/e2e/utils';
|
||||||
@ -273,13 +274,19 @@ describe('Angular Module Federation', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// check default generated host is built successfully
|
// check default generated host is built successfully
|
||||||
const buildOutputSwc = runCLI(`build ${hostApp}`);
|
const buildOutputSwc = await runCommandUntil(`build ${hostApp}`, (output) =>
|
||||||
expect(buildOutputSwc).toContain('Successfully ran target build');
|
output.includes('Successfully ran target build')
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(buildOutputSwc.pid);
|
||||||
|
|
||||||
const buildOutputTsNode = runCLI(`build ${hostApp}`, {
|
const buildOutputTsNode = await runCommandUntil(
|
||||||
|
`build ${hostApp}`,
|
||||||
|
(output) => output.includes('Successfully ran target build'),
|
||||||
|
{
|
||||||
env: { NX_PREFER_TS_NODE: 'true' },
|
env: { NX_PREFER_TS_NODE: 'true' },
|
||||||
});
|
}
|
||||||
expect(buildOutputTsNode).toContain('Successfully ran target build');
|
);
|
||||||
|
await killProcessAndPorts(buildOutputTsNode.pid);
|
||||||
|
|
||||||
const processSwc = await runCommandUntil(
|
const processSwc = await runCommandUntil(
|
||||||
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`,
|
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`,
|
||||||
@ -302,4 +309,80 @@ describe('Angular Module Federation', () => {
|
|||||||
|
|
||||||
await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
|
await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
|
||||||
}, 20_000_000);
|
}, 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -76,6 +76,12 @@
|
|||||||
"schema": "./src/generators/convert-to-with-mf/schema.json",
|
"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 %}"
|
"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": {
|
"host": {
|
||||||
"factory": "./src/generators/host/host.compat",
|
"factory": "./src/generators/host/host.compat",
|
||||||
"schema": "./src/generators/host/schema.json",
|
"schema": "./src/generators/host/schema.json",
|
||||||
@ -193,6 +199,12 @@
|
|||||||
"aliases": ["d"],
|
"aliases": ["d"],
|
||||||
"description": "Generate an Angular directive."
|
"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": {
|
"init": {
|
||||||
"factory": "./src/generators/init/init",
|
"factory": "./src/generators/init/init",
|
||||||
"schema": "./src/generators/init/schema.json",
|
"schema": "./src/generators/init/schema.json",
|
||||||
|
|||||||
@ -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-story/component-story';
|
||||||
export * from './src/generators/component/component';
|
export * from './src/generators/component/component';
|
||||||
export * from './src/generators/directive/directive';
|
export * from './src/generators/directive/directive';
|
||||||
|
export * from './src/generators/federate-module/federate-module';
|
||||||
export * from './src/generators/host/host';
|
export * from './src/generators/host/host';
|
||||||
export * from './src/generators/init/init';
|
export * from './src/generators/init/init';
|
||||||
export * from './src/generators/library-secondary-entry-point/library-secondary-entry-point';
|
export * from './src/generators/library-secondary-entry-point/library-secondary-entry-point';
|
||||||
|
|||||||
@ -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));
|
||||||
@ -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)}`;
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
formatFiles,
|
||||||
|
logger,
|
||||||
|
runTasksInSerial,
|
||||||
|
stripIndents,
|
||||||
|
type Tree,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { type Schema } from './schema';
|
||||||
|
import {
|
||||||
|
addFileToRemoteTsconfig,
|
||||||
|
addPathToExposes,
|
||||||
|
addPathToTsConfig,
|
||||||
|
addRemote,
|
||||||
|
} from './lib';
|
||||||
|
|
||||||
|
export async function federateModuleGenerator(tree: Tree, schema: Schema) {
|
||||||
|
if (!tree.exists(schema.path)) {
|
||||||
|
throw new Error(stripIndents`The "path" provided does not exist. Please verify the path is correct and pointing to a file that exists in the workspace.
|
||||||
|
|
||||||
|
Path: ${schema.path}`);
|
||||||
|
}
|
||||||
|
const { tasks, projectRoot, remoteName } = await addRemote(tree, schema);
|
||||||
|
|
||||||
|
addFileToRemoteTsconfig(tree, remoteName, schema.path);
|
||||||
|
|
||||||
|
addPathToExposes(tree, {
|
||||||
|
projectPath: projectRoot,
|
||||||
|
modulePath: schema.path,
|
||||||
|
moduleName: schema.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
addPathToTsConfig(tree, {
|
||||||
|
remoteName,
|
||||||
|
moduleName: schema.name,
|
||||||
|
pathToFile: schema.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!schema.skipFormat) {
|
||||||
|
await formatFiles(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`✅️ Updated module federation config.
|
||||||
|
Now you can use the module from your remote app like this:
|
||||||
|
|
||||||
|
Static import:
|
||||||
|
import { MyComponent } from '${remoteName}/${schema.name}';
|
||||||
|
|
||||||
|
Dynamic import:
|
||||||
|
import('${remoteName}/${schema.name}').then((m) => m.MyComponent);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
return runTasksInSerial(...tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default federateModuleGenerator;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
type Tree,
|
||||||
|
updateJson,
|
||||||
|
readProjectConfiguration,
|
||||||
|
offsetFromRoot,
|
||||||
|
joinPathFragments,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
export function addFileToRemoteTsconfig(
|
||||||
|
tree: Tree,
|
||||||
|
remoteName: string,
|
||||||
|
pathToExpose: string
|
||||||
|
) {
|
||||||
|
const remote = readProjectConfiguration(tree, remoteName);
|
||||||
|
updateJson(tree, remote.targets.build.options.tsConfig, (json) => ({
|
||||||
|
...json,
|
||||||
|
files: [
|
||||||
|
...json.files,
|
||||||
|
joinPathFragments(offsetFromRoot(remote.root), pathToExpose),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
import type {
|
||||||
|
Node,
|
||||||
|
ObjectLiteralExpression,
|
||||||
|
PropertyAssignment,
|
||||||
|
SourceFile,
|
||||||
|
TransformerFactory,
|
||||||
|
Visitor,
|
||||||
|
} from 'typescript';
|
||||||
|
import { joinPathFragments, type Tree } from '@nx/devkit';
|
||||||
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
|
|
||||||
|
let tsModule: typeof import('typescript');
|
||||||
|
|
||||||
|
type AddPathToExposesOptions = {
|
||||||
|
projectPath: string;
|
||||||
|
moduleName: string;
|
||||||
|
modulePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function addPathToExposes(
|
||||||
|
tree: Tree,
|
||||||
|
{ projectPath, moduleName, modulePath }: AddPathToExposesOptions
|
||||||
|
) {
|
||||||
|
const moduleFederationConfigPath = joinPathFragments(
|
||||||
|
projectPath,
|
||||||
|
tree.exists(joinPathFragments(projectPath, 'module-federation.config.ts'))
|
||||||
|
? 'module-federation.config.ts'
|
||||||
|
: 'module-federation.config.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
updateExposesProperty(
|
||||||
|
tree,
|
||||||
|
moduleFederationConfigPath,
|
||||||
|
moduleName,
|
||||||
|
modulePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExposesProperty(
|
||||||
|
tree: Tree,
|
||||||
|
moduleFederationConfigPath: string,
|
||||||
|
moduleName: string,
|
||||||
|
modulePath: string
|
||||||
|
) {
|
||||||
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = tree.read(moduleFederationConfigPath, 'utf-8');
|
||||||
|
const source = tsModule.createSourceFile(
|
||||||
|
moduleFederationConfigPath,
|
||||||
|
fileContent,
|
||||||
|
tsModule.ScriptTarget.ES2015,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const exposesObject = findExposes(source);
|
||||||
|
if (!exposesObject) return;
|
||||||
|
|
||||||
|
const newEntry = createObjectEntry(moduleName, modulePath);
|
||||||
|
const updatedSourceFile = updateExposesPropertyinAST(
|
||||||
|
source,
|
||||||
|
exposesObject,
|
||||||
|
newEntry
|
||||||
|
);
|
||||||
|
writeToConfig(tree, moduleFederationConfigPath, source, updatedSourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findExposes(sourceFile: SourceFile) {
|
||||||
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
|
||||||
|
let exposesObject: ObjectLiteralExpression | null = null;
|
||||||
|
|
||||||
|
const visit = (node: Node) => {
|
||||||
|
if (
|
||||||
|
tsModule.isPropertyAssignment(node) &&
|
||||||
|
tsModule.isIdentifier(node.name) &&
|
||||||
|
node.name.text === 'exposes' &&
|
||||||
|
tsModule.isObjectLiteralExpression(node.initializer)
|
||||||
|
) {
|
||||||
|
exposesObject = node.initializer;
|
||||||
|
} else {
|
||||||
|
tsModule.forEachChild(node, visit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tsModule.forEachChild(sourceFile, visit);
|
||||||
|
|
||||||
|
return exposesObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new property assignment
|
||||||
|
function createObjectEntry(
|
||||||
|
moduleName: string,
|
||||||
|
modulePath: string
|
||||||
|
): PropertyAssignment {
|
||||||
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
|
||||||
|
return tsModule.factory.createPropertyAssignment(
|
||||||
|
tsModule.factory.createStringLiteral(`./${moduleName}`, true),
|
||||||
|
tsModule.factory.createStringLiteral(modulePath, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the exposes property in the AST
|
||||||
|
function updateExposesPropertyinAST(
|
||||||
|
source: SourceFile,
|
||||||
|
exposesObject: ObjectLiteralExpression,
|
||||||
|
newEntry: PropertyAssignment
|
||||||
|
) {
|
||||||
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedExposes = tsModule.factory.updateObjectLiteralExpression(
|
||||||
|
exposesObject,
|
||||||
|
[...exposesObject.properties, newEntry]
|
||||||
|
);
|
||||||
|
|
||||||
|
const transform: TransformerFactory<SourceFile> = (context) => {
|
||||||
|
const visit: Visitor = (node) => {
|
||||||
|
// Comparing nodes indirectly to ensure type compatibility. You must ensure that the nodes are identical.
|
||||||
|
return tsModule.isObjectLiteralExpression(node) && node === exposesObject
|
||||||
|
? updatedExposes
|
||||||
|
: tsModule.visitEachChild(node, visit, context);
|
||||||
|
};
|
||||||
|
return (node) => tsModule.visitNode(node, visit) as SourceFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
return tsModule.transform<SourceFile>(source, [transform]).transformed[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updated AST to the file (module-federation.config.js)
|
||||||
|
function writeToConfig(
|
||||||
|
tree: Tree,
|
||||||
|
filename: string,
|
||||||
|
source: SourceFile,
|
||||||
|
updatedSourceFile: SourceFile
|
||||||
|
) {
|
||||||
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
|
||||||
|
const printer = tsModule.createPrinter();
|
||||||
|
const update = printer.printNode(
|
||||||
|
tsModule.EmitHint.Unspecified,
|
||||||
|
updatedSourceFile,
|
||||||
|
source
|
||||||
|
);
|
||||||
|
tree.write(filename, update);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { readJson, type Tree } from '@nx/devkit';
|
||||||
|
import { addTsConfigPath, getRootTsConfigPathInTree } from '@nx/js';
|
||||||
|
|
||||||
|
type AddPathToTsConfigOptions = {
|
||||||
|
remoteName: string;
|
||||||
|
moduleName: string;
|
||||||
|
pathToFile: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function addPathToTsConfig(
|
||||||
|
tree: Tree,
|
||||||
|
{ remoteName, moduleName, pathToFile }: AddPathToTsConfigOptions
|
||||||
|
) {
|
||||||
|
const rootTsConfig = readJson(tree, getRootTsConfigPathInTree(tree));
|
||||||
|
if (!rootTsConfig.compilerOptions?.paths[`${remoteName}/${moduleName}`]) {
|
||||||
|
addTsConfigPath(tree, `${remoteName}/${moduleName}`, [pathToFile]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { GeneratorCallback, stripIndents, type Tree } from '@nx/devkit';
|
||||||
|
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||||
|
import { lt } from 'semver';
|
||||||
|
import { getRemoteIfExists } from './check-remote-exists';
|
||||||
|
import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
|
||||||
|
import { type Schema } from '../schema';
|
||||||
|
import remoteGenerator from '../../remote/remote';
|
||||||
|
import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners';
|
||||||
|
|
||||||
|
export async function addRemote(tree: Tree, schema: Schema) {
|
||||||
|
const tasks: GeneratorCallback[] = [];
|
||||||
|
const remote = getRemoteIfExists(tree, schema.remote);
|
||||||
|
|
||||||
|
if (!remote) {
|
||||||
|
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
|
||||||
|
|
||||||
|
if (
|
||||||
|
lt(installedAngularVersionInfo.version, '14.1.0') &&
|
||||||
|
schema.standalone
|
||||||
|
) {
|
||||||
|
throw new Error(stripIndents`The "standalone" option is only supported in Angular >= 14.1.0. You are currently using ${installedAngularVersionInfo.version}.
|
||||||
|
You can resolve this error by removing the "standalone" option or by migrating to Angular 14.1.0.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteGeneratorCallback = await remoteGenerator(tree, {
|
||||||
|
name: schema.remote,
|
||||||
|
host: schema.host,
|
||||||
|
standalone: schema.standalone,
|
||||||
|
projectNameAndRootFormat: schema.projectNameAndRootFormat ?? 'derived',
|
||||||
|
unitTestRunner: schema.unitTestRunner ?? UnitTestRunner.Jest,
|
||||||
|
e2eTestRunner: schema.e2eTestRunner ?? E2eTestRunner.Cypress,
|
||||||
|
skipFormat: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.push(remoteGeneratorCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { projectName, projectRoot: remoteRoot } =
|
||||||
|
await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: schema.remote,
|
||||||
|
projectType: 'application',
|
||||||
|
projectNameAndRootFormat: schema.projectNameAndRootFormat ?? 'derived',
|
||||||
|
callingGenerator: '@nx/angular:federate-module',
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectRoot = remote ? remote.root : remoteRoot;
|
||||||
|
const remoteName = remote ? remote.name : projectName;
|
||||||
|
|
||||||
|
// TODO(Colum): add implicit dependency if the path points to a file in a different project
|
||||||
|
|
||||||
|
return {
|
||||||
|
tasks,
|
||||||
|
projectRoot,
|
||||||
|
remoteName,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
type Tree,
|
||||||
|
readProjectConfiguration,
|
||||||
|
joinPathFragments,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
export function getRemoteIfExists(tree: Tree, remote: string) {
|
||||||
|
const remoteProject = getRemote(tree, remote);
|
||||||
|
if (!remoteProject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasModuleFederationConfig =
|
||||||
|
tree.exists(
|
||||||
|
joinPathFragments(remoteProject.root, 'module-federation.config.ts')
|
||||||
|
) ||
|
||||||
|
tree.exists(
|
||||||
|
joinPathFragments(remoteProject.root, 'module-federation.config.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasModuleFederationConfig ? remoteProject : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRemote(tree: Tree, remote: string) {
|
||||||
|
try {
|
||||||
|
return readProjectConfiguration(tree, remote);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export * from './add-remote';
|
||||||
|
export * from './add-path-to-ts-config';
|
||||||
|
export * from './add-path-to-exposes';
|
||||||
|
export * from './add-file-to-remote-tsconfig';
|
||||||
14
packages/angular/src/generators/federate-module/schema.d.ts
vendored
Normal file
14
packages/angular/src/generators/federate-module/schema.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { type ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||||
|
import { UnitTestRunner, E2eTestRunner } from '../utils/testing';
|
||||||
|
|
||||||
|
export interface Schema {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
remote: string;
|
||||||
|
host?: string;
|
||||||
|
projectNameAndRootFormat?: ProjectNameAndRootFormat;
|
||||||
|
unitTestRunner?: UnitTestRunner;
|
||||||
|
e2eTestRunner?: E2eTestRunner;
|
||||||
|
standalone?: boolean;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
}
|
||||||
77
packages/angular/src/generators/federate-module/schema.json
Normal file
77
packages/angular/src/generators/federate-module/schema.json
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user