feat(angular): switch default to typescript configuration for module federation (#18998)

This commit is contained in:
Colum Ferry 2023-10-10 15:36:28 +01:00 committed by GitHub
parent 5366d49936
commit 9be869ff7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1632 additions and 45 deletions

View File

@ -170,6 +170,11 @@
"type": "boolean",
"default": false,
"x-priority": "important"
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"additionalProperties": false,

View File

@ -163,6 +163,11 @@
"description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
"type": "boolean",
"default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"additionalProperties": false,

View File

@ -73,6 +73,11 @@
"type": "boolean",
"description": "Whether the application is a standalone application. _Note: This is only supported in Angular versions >= 14.1.0_",
"default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"required": ["appName", "mfType"],

View File

@ -70,7 +70,7 @@ describe('Angular Module Federation', () => {
}Module } from '@${proj}/${sharedLib}';
import { ${
names(secondaryEntry).className
}Module } from '@${proj}/${secondaryEntry}';
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
import { RouterModule } from '@angular/router';
@ -79,7 +79,7 @@ describe('Angular Module Federation', () => {
declarations: [AppComponent, NxWelcomeComponent],
imports: [
BrowserModule,
SharedModule,
${names(sharedLib).className}Module,
RouterModule.forRoot(
[
{
@ -107,14 +107,15 @@ describe('Angular Module Federation', () => {
import { ${names(sharedLib).className}Module } from '@${proj}/${sharedLib}';
import { ${
names(secondaryEntry).className
}Module } from '@${proj}/${secondaryEntry}';
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
import { RemoteEntryComponent } from './entry.component';
import { NxWelcomeComponent } from './nx-welcome.component';
@NgModule({
declarations: [RemoteEntryComponent],
declarations: [RemoteEntryComponent, NxWelcomeComponent],
imports: [
CommonModule,
SharedModule,
${names(sharedLib).className}Module,
RouterModule.forChild([
{
path: '',
@ -128,15 +129,23 @@ describe('Angular Module Federation', () => {
`
);
const process = await runCommandUntil(
const processSwc = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp1}`,
(output) =>
output.includes(`listening on localhost:${remotePort}`) &&
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
output.includes(`listening on localhost:${hostPort}`)
);
await killProcessAndPorts(processSwc.pid, hostPort, remotePort);
// port and process cleanup
await killProcessAndPorts(process.pid, hostPort, remotePort);
const processTsNode = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp1}`,
(output) =>
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
output.includes(`listening on localhost:${hostPort}`),
{ env: { NX_PREFER_TS_NODE: 'true' } }
);
await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
}, 20_000_000);
it('should convert apps to MF successfully', async () => {
@ -161,15 +170,24 @@ describe('Angular Module Federation', () => {
`generate @nx/angular:setup-mf ${app2} --mfType=remote --host=${app1} --port=${app2Port} --no-interactive`
);
const process = await runCommandUntil(
const processSwc = await runCommandUntil(
`serve ${app1} --dev-remotes=${app2}`,
(output) =>
output.includes(`listening on localhost:${app1Port}`) &&
output.includes(`listening on localhost:${app2Port}`)
!output.includes(`Remote '${app2}' failed to serve correctly`) &&
output.includes(`listening on localhost:${app1Port}`)
);
// port and process cleanup
await killProcessAndPorts(process.pid, app1Port, app2Port);
await killProcessAndPorts(processSwc.pid, app1Port, app2Port);
const processTsNode = await runCommandUntil(
`serve ${app1} --dev-remotes=${app2}`,
(output) =>
!output.includes(`Remote '${app2}' failed to serve correctly`) &&
output.includes(`listening on localhost:${app1Port}`),
{ env: { NX_PREFER_TS_NODE: 'true' } }
);
await killProcessAndPorts(processTsNode.pid, app1Port, app2Port);
}, 20_000_000);
it('should scaffold MF + SSR setup successfully', async () => {
@ -189,7 +207,7 @@ describe('Angular Module Federation', () => {
const remote2Port = readJson(join(remote2, 'project.json')).targets.serve
.options.port;
const process = await runCommandUntil(
const processSwc = await runCommandUntil(
`serve-ssr ${host} --port=${hostPort}`,
(output) =>
output.includes(
@ -203,8 +221,34 @@ describe('Angular Module Federation', () => {
)
);
// port and process cleanup
await killProcessAndPorts(process.pid, hostPort, remote1Port, remote2Port);
await killProcessAndPorts(
processSwc.pid,
hostPort,
remote1Port,
remote2Port
);
const processTsNode = await runCommandUntil(
`serve-ssr ${host} --port=${hostPort}`,
(output) =>
output.includes(
`Node Express server listening on http://localhost:${remote1Port}`
) &&
output.includes(
`Node Express server listening on http://localhost:${remote2Port}`
) &&
output.includes(
`Angular Universal Live Development Server is listening`
),
{ env: { NX_PREFER_TS_NODE: 'true' } }
);
await killProcessAndPorts(
processTsNode.pid,
hostPort,
remote1Port,
remote2Port
);
}, 20_000_000);
it('should should support generating host and remote apps with --project-name-and-root-format=derived', async () => {
@ -229,17 +273,33 @@ describe('Angular Module Federation', () => {
);
// check default generated host is built successfully
const buildOutput = runCLI(`build ${hostApp}`);
expect(buildOutput).toContain('Successfully ran target build');
const buildOutputSwc = runCLI(`build ${hostApp}`);
expect(buildOutputSwc).toContain('Successfully ran target build');
const process = await runCommandUntil(
const buildOutputTsNode = runCLI(`build ${hostApp}`, {
env: { NX_PREFER_TS_NODE: 'true' },
});
expect(buildOutputTsNode).toContain('Successfully ran target build');
const processSwc = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`,
(output) =>
output.includes(`listening on localhost:${remotePort}`) &&
!output.includes(`Remote '${remoteApp}' failed to serve correctly`) &&
output.includes(`listening on localhost:${hostPort}`)
);
// port and process cleanup
await killProcessAndPorts(process.pid, hostPort, remotePort);
await killProcessAndPorts(processSwc.pid, hostPort, remotePort);
const processTsNode = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`,
(output) =>
!output.includes(`Remote '${remoteApp}' failed to serve correctly`) &&
output.includes(`listening on localhost:${hostPort}`),
{
env: { NX_PREFER_TS_NODE: 'true' },
}
);
await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
}, 20_000_000);
});

View File

@ -229,7 +229,10 @@ export function runCommandAsync(
export function runCommandUntil(
command: string,
criteria: (output: string) => boolean
criteria: (output: string) => boolean,
opts: RunCmdOpts = {
env: undefined,
}
): Promise<ChildProcess> {
const pm = getPackageManagerCommand();
const p = exec(`${pm.runNx} ${command}`, {
@ -238,6 +241,7 @@ export function runCommandUntil(
env: {
CI: 'true',
...getStrippedEnvironmentVariables(),
...opts.env,
FORCE_COLOR: 'false',
},
});

View File

@ -385,6 +385,401 @@ exports[`Host App Generator --ssr should generate the correct files for standalo
}
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 2`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 3`] = `
"import 'zone.js/dist/zone-node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as cors from 'cors';
import { existsSync } from 'fs';
import { join } from 'path';
import bootstrap from './bootstrap.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const browserBundles = join(process.cwd(), 'dist/test/browser');
server.use(cors());
const indexHtml = existsSync(join(browserBundles, 'index.original.html'))
? 'index.original.html'
: 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap,
})
);
server.set('view engine', 'html');
server.set('views', browserBundles);
// Serve static files from /browser
server.get(
'*.*',
express.static(browserBundles, {
maxAge: '1y',
})
);
// All regular routes use the Universal engine
server.get('*', (req, res) => {
// keep it async to avoid blocking the server thread
res.render(indexHtml, {
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
req,
});
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(\`Node Express server listening on http://localhost:\${port}\`);
});
}
run();
export default bootstrap;
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 4`] = `
"import('./src/main.server');
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 5`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
remotes: [],
};
export default config;
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 6`] = `
"import { withModuleFederationForSSR } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederationForSSR(config);
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 7`] = `
"import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
component: NxWelcomeComponent,
},
];
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 8`] = `
"import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes, withEnabledBlockingInitialNavigation())],
};
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 9`] = `
"import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
"
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 10`] = `
{
"configurations": {
"development": {
"buildOptimizer": false,
"extractLicenses": false,
"optimization": false,
"sourceMap": true,
"vendorChunk": true,
},
"production": {
"outputHashing": "media",
},
},
"defaultConfiguration": "production",
"dependsOn": [
"build",
],
"executor": "@nx/angular:webpack-server",
"options": {
"customWebpackConfig": {
"path": "test/webpack.server.config.ts",
},
"main": "test/server.ts",
"outputPath": "dist/test/server",
"tsConfig": "test/tsconfig.server.json",
},
}
`;
exports[`Host App Generator --ssr should generate the correct files for standalone when --typescript=true 11`] = `
{
"configurations": {
"development": {
"browserTarget": "test:build:development",
"serverTarget": "test:server:development",
},
"production": {
"browserTarget": "test:build:production",
"serverTarget": "test:server:production",
},
},
"defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr",
}
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 1`] = `
"import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { appRoutes } from './app.routes';
import { NxWelcomeComponent } from './nx-welcome.component';
@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [
BrowserModule,
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 2`] = `
"import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 3`] = `
"export { AppServerModule } from './app/app.server.module';
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 4`] = `
"import 'zone.js/dist/zone-node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as cors from 'cors';
import { existsSync } from 'fs';
import { join } from 'path';
import { AppServerModule } from './bootstrap.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const browserBundles = join(process.cwd(), 'dist/test/browser');
server.use(cors());
const indexHtml = existsSync(join(browserBundles, 'index.original.html'))
? 'index.original.html'
: 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModule,
})
);
server.set('view engine', 'html');
server.set('views', browserBundles);
// Serve static files from /browser
server.get(
'*.*',
express.static(browserBundles, {
maxAge: '1y',
})
);
// All regular routes use the Universal engine
server.get('*', (req, res) => {
// keep it async to avoid blocking the server thread
res.render(indexHtml, {
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
req,
});
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(\`Node Express server listening on http://localhost:\${port}\`);
});
}
run();
export * from './bootstrap.server';
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 5`] = `
"import('./src/main.server');
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 6`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
remotes: [],
};
export default config;
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 7`] = `
"import { withModuleFederationForSSR } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederationForSSR(config);
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 8`] = `
"import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
component: NxWelcomeComponent,
},
];
"
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 9`] = `
{
"configurations": {
"development": {
"buildOptimizer": false,
"extractLicenses": false,
"optimization": false,
"sourceMap": true,
"vendorChunk": true,
},
"production": {
"outputHashing": "media",
},
},
"defaultConfiguration": "production",
"dependsOn": [
"build",
],
"executor": "@nx/angular:webpack-server",
"options": {
"customWebpackConfig": {
"path": "test/webpack.server.config.ts",
},
"main": "test/server.ts",
"outputPath": "dist/test/server",
"tsConfig": "test/tsconfig.server.json",
},
}
`;
exports[`Host App Generator --ssr should generate the correct files when --typescript=true 10`] = `
{
"configurations": {
"development": {
"browserTarget": "test:build:development",
"serverTarget": "test:server:development",
},
"production": {
"browserTarget": "test:build:production",
"serverTarget": "test:server:production",
},
},
"defaultConfiguration": "development",
"executor": "@nx/angular:module-federation-dev-ssr",
}
`;
exports[`Host App Generator should generate a host app with a remote 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config');
@ -399,6 +794,22 @@ module.exports = withModuleFederation(config);
"
`;
exports[`Host App Generator should generate a host app with a remote when --typesscript=true 1`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`Host App Generator should generate a host app with a remote when --typesscript=true 2`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`Host App Generator should generate a host app with no remotes 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config');
@ -406,6 +817,14 @@ module.exports = withModuleFederation(config);
"
`;
exports[`Host App Generator should generate a host app with no remotes when --typescript=true 1`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`Host App Generator should generate a host with remotes using standalone components 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';

View File

@ -0,0 +1,4 @@
import {withModuleFederationForSSR} from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederationForSSR(config)

View File

@ -18,11 +18,25 @@ describe('Host App Generator', () => {
// ACT
await generateTestHostApplication(tree, {
name: 'test',
typescriptConfiguration: false,
});
// ASSERT
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
});
it('should generate a host app with no remotes when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestHostApplication(tree, {
name: 'test',
typescriptConfiguration: true,
});
// ASSERT
expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should generate a host app with a remote', async () => {
// ARRANGE
@ -30,18 +44,40 @@ describe('Host App Generator', () => {
await generateTestRemoteApplication(tree, {
name: 'remote',
typescriptConfiguration: false,
});
// ACT
await generateTestHostApplication(tree, {
name: 'test',
remotes: ['remote'],
typescriptConfiguration: false,
});
// ASSERT
expect(tree.read('remote/webpack.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
});
it('should generate a host app with a remote when --typesscript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, {
name: 'remote',
typescriptConfiguration: true,
});
// ACT
await generateTestHostApplication(tree, {
name: 'test',
remotes: ['remote'],
typescriptConfiguration: true,
});
// ASSERT
expect(tree.read('remote/webpack.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should generate a host and any remotes that dont exist with correct routing setup', async () => {
// ARRANGE
@ -52,6 +88,7 @@ describe('Host App Generator', () => {
await generateTestHostApplication(tree, {
name: 'hostApp',
remotes: ['remote1', 'remote2'],
typescriptConfiguration: false,
});
// ASSERT
@ -72,17 +109,49 @@ describe('Host App Generator', () => {
`);
});
it('should generate a host and any remotes that dont exist with correct routing setup when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestHostApplication(tree, {
name: 'hostApp',
remotes: ['remote1', 'remote2'],
typescriptConfiguration: true,
});
// ASSERT
expect(tree.exists('remote1/project.json')).toBeTruthy();
expect(tree.exists('remote2/project.json')).toBeTruthy();
expect(
tree.read('host-app/module-federation.config.ts', 'utf-8')
).toContain(`'remote1', 'remote2'`);
expect(tree.read('host-app/src/app/app.component.html', 'utf-8'))
.toMatchInlineSnapshot(`
"<ul class="remote-menu">
<li><a routerLink="/">Home</a></li>
<li><a routerLink="remote1">Remote1</a></li>
<li><a routerLink="remote2">Remote2</a></li>
</ul>
<router-outlet></router-outlet>
"
`);
});
it('should generate a host, integrate existing remotes and generate any remotes that dont exist', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, {
name: 'remote1',
typescriptConfiguration: false,
});
// ACT
await generateTestHostApplication(tree, {
name: 'hostApp',
remotes: ['remote1', 'remote2', 'remote3'],
typescriptConfiguration: false,
});
// ASSERT
@ -94,11 +163,36 @@ describe('Host App Generator', () => {
).toContain(`'remote1', 'remote2', 'remote3'`);
});
it('should generate a host, integrate existing remotes and generate any remotes that dont exist when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, {
name: 'remote1',
typescriptConfiguration: true,
});
// ACT
await generateTestHostApplication(tree, {
name: 'hostApp',
remotes: ['remote1', 'remote2', 'remote3'],
typescriptConfiguration: true,
});
// ASSERT
expect(tree.exists('remote1/project.json')).toBeTruthy();
expect(tree.exists('remote2/project.json')).toBeTruthy();
expect(tree.exists('remote3/project.json')).toBeTruthy();
expect(
tree.read('host-app/module-federation.config.ts', 'utf-8')
).toContain(`'remote1', 'remote2', 'remote3'`);
});
it('should generate a host, integrate existing remotes and generate any remotes that dont exist, in a directory', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, {
name: 'remote1',
typescriptConfiguration: false,
});
// ACT
@ -106,6 +200,7 @@ describe('Host App Generator', () => {
name: 'hostApp',
directory: 'foo/hostApp',
remotes: ['remote1', 'remote2', 'remote3'],
typescriptConfiguration: false,
});
// ASSERT
@ -117,6 +212,31 @@ describe('Host App Generator', () => {
).toContain(`'remote1', 'remote2', 'remote3'`);
});
it('should generate a host, integrate existing remotes and generate any remotes that dont exist, in a directory when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, {
name: 'remote1',
typescriptConfiguration: true,
});
// ACT
await generateTestHostApplication(tree, {
name: 'hostApp',
directory: 'foo/hostApp',
remotes: ['remote1', 'remote2', 'remote3'],
typescriptConfiguration: true,
});
// ASSERT
expect(tree.exists('remote1/project.json')).toBeTruthy();
expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
expect(
tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
).toContain(`'remote1', 'remote2', 'remote3'`);
});
it('should generate a host with remotes using standalone components', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -197,6 +317,7 @@ describe('Host App Generator', () => {
await generateTestHostApplication(tree, {
name: 'test',
ssr: true,
typescriptConfiguration: false,
});
// ASSERT
@ -223,6 +344,41 @@ describe('Host App Generator', () => {
expect(project.targets['serve-ssr']).toMatchSnapshot();
});
it('should generate the correct files when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestHostApplication(tree, {
name: 'test',
ssr: true,
typescriptConfiguration: true,
});
// ASSERT
const project = readProjectConfiguration(tree, 'test');
expect(
tree.read(`test/src/app/app.module.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`test/src/bootstrap.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/src/bootstrap.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`test/src/main.server.ts`, 'utf-8')).toMatchSnapshot();
expect(tree.read(`test/server.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/module-federation.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/webpack.server.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/app.routes.ts`, 'utf-8')
).toMatchSnapshot();
expect(project.targets.server).toMatchSnapshot();
expect(project.targets['serve-ssr']).toMatchSnapshot();
});
it('should generate the correct files for standalone', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -232,6 +388,7 @@ describe('Host App Generator', () => {
name: 'test',
standalone: true,
ssr: true,
typescriptConfiguration: false,
});
// ASSERT
@ -261,6 +418,46 @@ describe('Host App Generator', () => {
expect(project.targets.server).toMatchSnapshot();
expect(project.targets['serve-ssr']).toMatchSnapshot();
});
it('should generate the correct files for standalone when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestHostApplication(tree, {
name: 'test',
standalone: true,
ssr: true,
typescriptConfiguration: true,
});
// ASSERT
const project = readProjectConfiguration(tree, 'test');
expect(tree.exists(`test/src/app/app.module.ts`)).toBeFalsy();
expect(tree.read(`test/src/bootstrap.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/src/bootstrap.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`test/src/main.server.ts`, 'utf-8')).toMatchSnapshot();
expect(tree.read(`test/server.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/module-federation.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/webpack.server.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/app.routes.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/app.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/app.config.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(project.targets.server).toMatchSnapshot();
expect(project.targets['serve-ssr']).toMatchSnapshot();
});
});
it('should error correctly when Angular version does not support standalone', async () => {
@ -291,6 +488,7 @@ describe('Host App Generator', () => {
await generateTestRemoteApplication(tree, {
name: 'remote1',
projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
});
// ACT
@ -298,6 +496,7 @@ describe('Host App Generator', () => {
name: 'hostApp',
remotes: ['remote1', 'remote2', 'remote3'],
projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
});
// ASSERT
@ -315,6 +514,7 @@ describe('Host App Generator', () => {
await generateTestRemoteApplication(tree, {
name: 'remote1',
projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
});
// ACT
@ -323,6 +523,7 @@ describe('Host App Generator', () => {
directory: 'foo',
remotes: ['remote1', 'remote2', 'remote3'],
projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
});
// ASSERT
@ -333,5 +534,31 @@ describe('Host App Generator', () => {
tree.read('apps/foo/host-app/module-federation.config.js', 'utf-8')
).toContain(`'remote1', 'foo-remote2', 'foo-remote3'`);
});
it('should generate a host, integrate existing remotes and generate any remotes that dont exist, in a directory when --typescript=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, {
name: 'remote1',
projectNameAndRootFormat: 'derived',
typescriptConfiguration: true,
});
// ACT
await generateTestHostApplication(tree, {
name: 'hostApp',
directory: 'foo',
remotes: ['remote1', 'remote2', 'remote3'],
projectNameAndRootFormat: 'derived',
typescriptConfiguration: true,
});
// ASSERT
expect(tree.exists('apps/remote1/project.json')).toBeTruthy();
expect(tree.exists('apps/foo/remote2/project.json')).toBeTruthy();
expect(tree.exists('apps/foo/remote3/project.json')).toBeTruthy();
expect(
tree.read('apps/foo/host-app/module-federation.config.ts', 'utf-8')
).toContain(`'remote1', 'foo-remote2', 'foo-remote3'`);
});
});
});

View File

@ -23,14 +23,16 @@ export async function host(tree: Tree, options: Schema) {
});
}
export async function hostInternal(tree: Tree, options: Schema) {
export async function hostInternal(tree: Tree, schema: Schema) {
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
if (lt(installedAngularVersionInfo.version, '14.1.0') && options.standalone) {
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 { typescriptConfiguration = true, ...options }: Schema = schema;
const projects = getProjects(tree);
const remotesToGenerate: string[] = [];
@ -78,11 +80,17 @@ export async function hostInternal(tree: Tree, options: Schema) {
skipE2E,
e2eProjectName: skipE2E ? undefined : `${hostProjectName}-e2e`,
prefix: options.prefix,
typescriptConfiguration,
});
let installTasks = [appInstallTask];
if (options.ssr) {
let ssrInstallTask = await addSsr(tree, options, hostProjectName);
let ssrInstallTask = await addSsr(
tree,
options,
hostProjectName,
typescriptConfiguration
);
installTasks.push(ssrInstallTask);
}
@ -107,6 +115,7 @@ export async function hostInternal(tree: Tree, options: Schema) {
host: hostProjectName,
skipFormat: true,
standalone: options.standalone,
typescriptConfiguration,
});
}

View File

@ -18,7 +18,12 @@ import {
} from '../../../utils/versions';
import { join } from 'path';
export async function addSsr(tree: Tree, options: Schema, appName: string) {
export async function addSsr(
tree: Tree,
options: Schema,
appName: string,
typescriptConfiguration: boolean
) {
let project = readProjectConfiguration(tree, appName);
await setupSsr(tree, {
@ -40,19 +45,33 @@ export async function addSsr(tree: Tree, options: Schema, appName: string) {
'browser'
);
generateFiles(tree, join(__dirname, '../files'), project.root, {
generateFiles(tree, join(__dirname, '../files/common'), project.root, {
appName,
browserBundleOutput,
standalone: options.standalone,
tmpl: '',
});
const pathToTemplateFiles = typescriptConfiguration ? 'ts' : 'js';
generateFiles(
tree,
join(__dirname, '../files', pathToTemplateFiles),
project.root,
{
tmpl: '',
}
);
// update project.json
project = readProjectConfiguration(tree, appName);
project.targets.server.executor = '@nx/angular:webpack-server';
project.targets.server.options.customWebpackConfig = {
path: joinPathFragments(project.root, 'webpack.server.config.js'),
path: joinPathFragments(
project.root,
`webpack.server.config.${pathToTemplateFiles}`
),
};
project.targets['serve-ssr'].executor =

View File

@ -29,4 +29,5 @@ export interface Schema {
skipFormat?: boolean;
standalone?: boolean;
ssr?: boolean;
typescriptConfiguration?: boolean;
}

View File

@ -173,6 +173,11 @@
"type": "boolean",
"default": false,
"x-priority": "important"
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"additionalProperties": false,

View File

@ -230,6 +230,241 @@ exports[`MF Remote App Generator --ssr should generate the correct files 13`] =
}
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 1`] = `
"import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RouterModule.forRoot(
[
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.module').then(
(m) => m.RemoteEntryModule
),
},
],
{ initialNavigation: 'enabledBlocking' }
),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 2`] = `
"import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 3`] = `
"export { AppServerModule } from './app/app.server.module';
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 4`] = `
"import 'zone.js/dist/zone-node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as cors from 'cors';
import { existsSync } from 'fs';
import { join } from 'path';
import { AppServerModule } from './bootstrap.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const browserBundles = join(process.cwd(), 'dist/test/browser');
const serverBundles = join(process.cwd(), 'dist/test/server');
server.use(cors());
const indexHtml = existsSync(join(browserBundles, 'index.original.html'))
? 'index.original.html'
: 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModule,
})
);
server.set('view engine', 'html');
server.set('views', browserBundles);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
// serve static files
server.use('/', express.static(browserBundles, { maxAge: '1y' }));
server.use('/server', express.static(serverBundles, { maxAge: '1y' }));
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, {
req,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
});
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(\`Node Express server listening on http://localhost:\${port}\`);
/**
* DO NOT REMOVE IF USING @nx/angular:module-federation-dev-ssr executor
* to serve your Host application with this Remote application.
* This message allows Nx to determine when the Remote is ready to be
* consumed by the Host.
*/
process.send && process.send('nx.server.ready');
});
}
run();
export * from './bootstrap.server';
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 5`] = `
"import('./src/main.server');
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 6`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
exposes: {
'./Module': 'test/src/app/remote-entry/entry.module.ts',
},
};
export default config;
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 7`] = `
"import { withModuleFederationForSSR } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederationForSSR(config);
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 8`] = `
"import { Component } from '@angular/core';
@Component({
selector: 'proj-test-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\`,
})
export class RemoteEntryComponent {}
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 9`] = `
"import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.module').then((m) => m.RemoteEntryModule),
},
];
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 10`] = `
"import { Route } from '@angular/router';
import { RemoteEntryComponent } from './entry.component';
export const remoteRoutes: Route[] = [
{ path: '', component: RemoteEntryComponent },
];
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 11`] = `
{
"configurations": {
"development": {
"buildOptimizer": false,
"extractLicenses": false,
"optimization": false,
"sourceMap": true,
"vendorChunk": true,
},
"production": {
"outputHashing": "media",
},
},
"defaultConfiguration": "production",
"dependsOn": [
"build",
],
"executor": "@nx/angular:webpack-server",
"options": {
"customWebpackConfig": {
"path": "test/webpack.server.config.ts",
},
"main": "test/server.ts",
"outputPath": "dist/test/server",
"tsConfig": "test/tsconfig.server.json",
},
}
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 12`] = `
"import { Route } from '@angular/router';
import { RemoteEntryComponent } from './entry.component';
export const remoteRoutes: Route[] = [
{ path: '', component: RemoteEntryComponent },
];
"
`;
exports[`MF Remote App Generator --ssr should generate the correct files when --typescriptConfiguration=true 13`] = `
{
"dependsOn": [
"build",
"server",
],
"executor": "nx:run-commands",
"options": {
"command": "PORT=4201 node dist/test/server/main.js",
},
}
`;
exports[`MF Remote App Generator should generate a remote mf app with a host 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config');
@ -244,6 +479,22 @@ module.exports = withModuleFederation(config);
"
`;
exports[`MF Remote App Generator should generate a remote mf app with a host when --typescriptConfiguration=true 1`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`MF Remote App Generator should generate a remote mf app with a host when --typescriptConfiguration=true 2`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`MF Remote App Generator should generate a remote mf app with no host 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config');
@ -251,6 +502,14 @@ module.exports = withModuleFederation(config);
"
`;
exports[`MF Remote App Generator should generate a remote mf app with no host when --typescriptConfiguration=true 1`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
@ -309,3 +568,66 @@ export const remoteRoutes: Route[] = [
];
"
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components when --typescriptConfiguration=true 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { RemoteEntryComponent } from './app/remote-entry/entry.component';
bootstrapApplication(RemoteEntryComponent, appConfig).catch((err) =>
console.error(err)
);
"
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'test',
exposes: {
'./Routes': 'test/src/app/remote-entry/entry.routes.ts',
},
};
export default config;
"
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components when --typescriptConfiguration=true 3`] = `
"import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NxWelcomeComponent } from './nx-welcome.component';
@Component({
standalone: true,
imports: [CommonModule, NxWelcomeComponent],
selector: 'proj-test-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\`,
})
export class RemoteEntryComponent {}
"
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components when --typescriptConfiguration=true 4`] = `
"import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
loadChildren: () =>
import('./remote-entry/entry.routes').then((m) => m.remoteRoutes),
},
];
"
`;
exports[`MF Remote App Generator should generate the a remote setup for standalone components when --typescriptConfiguration=true 5`] = `
"import { Route } from '@angular/router';
import { RemoteEntryComponent } from './entry.component';
export const remoteRoutes: Route[] = [
{ path: '', component: RemoteEntryComponent },
];
"
`;

View File

@ -0,0 +1,4 @@
import {withModuleFederationForSSR} from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederationForSSR(config)

View File

@ -22,7 +22,13 @@ export async function addSsr(
appName,
port,
standalone,
}: { appName: string; port: number; standalone: boolean }
typescriptConfiguration,
}: {
appName: string;
port: number;
standalone: boolean;
typescriptConfiguration: boolean;
}
) {
let project = readProjectConfiguration(tree, appName);
@ -52,7 +58,7 @@ export async function addSsr(
generateFiles(
tree,
joinPathFragments(__dirname, '../files/base'),
joinPathFragments(__dirname, `../files/common`),
project.root,
{
appName,
@ -63,6 +69,17 @@ export async function addSsr(
}
);
const pathToTemplateFiles = typescriptConfiguration ? 'base-ts' : 'base';
generateFiles(
tree,
joinPathFragments(__dirname, `../files/${pathToTemplateFiles}`),
project.root,
{
tmpl: '',
}
);
if (standalone) {
generateFiles(
tree,
@ -81,7 +98,10 @@ export async function addSsr(
project.targets.server.executor = '@nx/angular:webpack-server';
project.targets.server.options.customWebpackConfig = {
path: joinPathFragments(project.root, 'webpack.server.config.js'),
path: joinPathFragments(
project.root,
`webpack.server.config.${typescriptConfiguration ? 'ts' : 'js'}`
),
};
project.targets['serve-ssr'].options = {
...(project.targets['serve-ssr'].options ?? {}),

View File

@ -23,6 +23,7 @@ describe('MF Remote App Generator', () => {
await generateTestRemoteApplication(tree, {
name: 'test',
port: 4201,
typescriptConfiguration: false,
});
// ASSERT
@ -33,18 +34,35 @@ describe('MF Remote App Generator', () => {
]);
});
it('should generate a remote mf app with no host when --typescriptConfiguration=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestRemoteApplication(tree, {
name: 'test',
port: 4201,
typescriptConfiguration: true,
});
// ASSERT
expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should generate a remote mf app with a host', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestHostApplication(tree, {
name: 'host',
typescriptConfiguration: false,
});
// ACT
await generateTestRemoteApplication(tree, {
name: 'test',
host: 'host',
typescriptConfiguration: false,
});
// ASSERT
@ -52,6 +70,27 @@ describe('MF Remote App Generator', () => {
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
});
it('should generate a remote mf app with a host when --typescriptConfiguration=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestHostApplication(tree, {
name: 'host',
typescriptConfiguration: true,
});
// ACT
await generateTestRemoteApplication(tree, {
name: 'test',
host: 'host',
typescriptConfiguration: true,
});
// ASSERT
expect(tree.read('host/webpack.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should error when a remote app is attempted to be generated with an incorrect host', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -125,6 +164,7 @@ describe('MF Remote App Generator', () => {
await generateTestRemoteApplication(tree, {
name: 'test',
standalone: true,
typescriptConfiguration: false,
});
// ASSERT
@ -150,6 +190,36 @@ describe('MF Remote App Generator', () => {
]);
});
it('should generate the a remote setup for standalone components when --typescriptConfiguration=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestRemoteApplication(tree, {
name: 'test',
standalone: true,
typescriptConfiguration: true,
});
// ASSERT
expect(tree.exists(`test/src/app/app.module.ts`)).toBeFalsy();
expect(tree.exists(`test/src/app/app.component.ts`)).toBeFalsy();
expect(
tree.exists(`test/src/app/remote-entry/entry.module.ts`)
).toBeFalsy();
expect(tree.read(`test/src/bootstrap.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/module-federation.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/remote-entry/entry.component.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`test/src/app/app.routes.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/src/app/remote-entry/entry.routes.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should not generate an e2e project when e2eTestRunner is none', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -217,6 +287,7 @@ describe('MF Remote App Generator', () => {
await generateTestRemoteApplication(tree, {
name: 'test',
ssr: true,
typescriptConfiguration: false,
});
// ASSERT
@ -254,6 +325,53 @@ describe('MF Remote App Generator', () => {
).toMatchSnapshot();
expect(project.targets['static-server']).toMatchSnapshot();
});
it('should generate the correct files when --typescriptConfiguration=true', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await generateTestRemoteApplication(tree, {
name: 'test',
ssr: true,
typescriptConfiguration: true,
});
// ASSERT
const project = readProjectConfiguration(tree, 'test');
expect(
tree.exists(`test/src/app/remote-entry/entry.module.ts`)
).toBeTruthy();
expect(
tree.read(`test/src/app/app.module.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`test/src/bootstrap.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/src/bootstrap.server.ts`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`test/src/main.server.ts`, 'utf-8')).toMatchSnapshot();
expect(tree.read(`test/server.ts`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`test/module-federation.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/webpack.server.config.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/remote-entry/entry.component.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/app.routes.ts`, 'utf-8')
).toMatchSnapshot();
expect(
tree.read(`test/src/app/remote-entry/entry.routes.ts`, 'utf-8')
).toMatchSnapshot();
expect(project.targets.server).toMatchSnapshot();
expect(
tree.read(`test/src/app/remote-entry/entry.routes.ts`, 'utf-8')
).toMatchSnapshot();
expect(project.targets['static-server']).toMatchSnapshot();
});
});
it('should error correctly when Angular version does not support standalone', async () => {
@ -285,6 +403,7 @@ describe('MF Remote App Generator', () => {
name: 'test',
port: 4201,
projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
});
expect(tree.exists('apps/test/webpack.config.js')).toBe(true);
@ -299,6 +418,7 @@ describe('MF Remote App Generator', () => {
port: 4201,
directory: 'shared',
projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
});
expect(tree.exists('apps/shared/test/webpack.config.js')).toBe(true);

View File

@ -21,14 +21,16 @@ export async function remote(tree: Tree, options: Schema) {
});
}
export async function remoteInternal(tree: Tree, options: Schema) {
export async function remoteInternal(tree: Tree, schema: Schema) {
const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree);
if (lt(installedAngularVersionInfo.version, '14.1.0') && options.standalone) {
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 { typescriptConfiguration = true, ...options }: Schema = schema;
const projects = getProjects(tree);
if (options.host && !projects.has(options.host)) {
throw new Error(
@ -71,6 +73,7 @@ export async function remoteInternal(tree: Tree, options: Schema) {
e2eProjectName: skipE2E ? undefined : `${remoteProjectName}-e2e`,
standalone: options.standalone,
prefix: options.prefix,
typescriptConfiguration,
});
let installTasks = [appInstallTask];
@ -78,6 +81,7 @@ export async function remoteInternal(tree: Tree, options: Schema) {
let ssrInstallTask = await addSsr(tree, {
appName: remoteProjectName,
port,
typescriptConfiguration,
standalone: options.standalone,
});
installTasks.push(ssrInstallTask);

View File

@ -28,4 +28,5 @@ export interface Schema {
skipFormat?: boolean;
standalone?: boolean;
ssr?: boolean;
typescriptConfiguration?: boolean;
}

View File

@ -166,6 +166,11 @@
"description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
"type": "boolean",
"default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"additionalProperties": false,

View File

@ -10,6 +10,16 @@ fetch('/assets/module-federation.manifest.json')
"
`;
exports[`Init MF --federationType=dynamic should create a host with the correct configurations when --typescriptConfiguration=true 1`] = `
"import { setRemoteDefinitions } from '@nx/angular/mf';
fetch('/assets/module-federation.manifest.json')
.then((res) => res.json())
.then((definitions) => setRemoteDefinitions(definitions))
.then(() => import('./bootstrap').catch((err) => console.error(err)));
"
`;
exports[`Init MF should add a remote application and add it to a specified host applications router config 1`] = `
"import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
@ -41,6 +51,18 @@ exports[`Init MF should add a remote application and add it to a specified host
"
`;
exports[`Init MF should add a remote application and add it to a specified host applications webpack config that contains a remote application already when --typescriptConfiguration=true 1`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'app1',
remotes: ['remote1', 'remote2'],
};
export default config;
"
`;
exports[`Init MF should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it 1`] = `
"module.exports = {
name: 'app1',
@ -49,6 +71,18 @@ exports[`Init MF should add a remote application and add it to a specified host
"
`;
exports[`Init MF should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it when --typescriptConfiguration=true 1`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'app1',
remotes: ['remote1'],
};
export default config;
"
`;
exports[`Init MF should add a remote to dynamic host correctly 1`] = `
"import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
@ -68,6 +102,25 @@ export const appRoutes: Route[] = [
"
`;
exports[`Init MF should add a remote to dynamic host correctly when --typescriptConfiguration=true 1`] = `
"import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
import { loadRemoteModule } from '@nx/angular/mf';
export const appRoutes: Route[] = [
{
path: 'remote1',
loadChildren: () =>
loadRemoteModule('remote1', './Module').then((m) => m.RemoteEntryModule),
},
{
path: '',
component: NxWelcomeComponent,
},
];
"
`;
exports[`Init MF should create webpack and mf configs correctly 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config');
@ -100,6 +153,48 @@ exports[`Init MF should create webpack and mf configs correctly 4`] = `
"
`;
exports[`Init MF should create webpack and mf configs correctly when --typescriptConfiguration=true 1`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`Init MF should create webpack and mf configs correctly when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'app1',
remotes: [],
};
export default config;
"
`;
exports[`Init MF should create webpack and mf configs correctly when --typescriptConfiguration=true 3`] = `
"import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);
"
`;
exports[`Init MF should create webpack and mf configs correctly when --typescriptConfiguration=true 4`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'remote1',
exposes: {
'./Module': 'remote1/src/app/remote-entry/entry.module.ts',
},
};
export default config;
"
`;
exports[`Init MF should generate the remote entry component correctly when prefix is not provided 1`] = `
"import { Component } from '@angular/core';

View File

@ -0,0 +1,12 @@
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '<%= name %>',<% if(type === 'host') { %>
remotes: [<% remotes.forEach(function(remote) { %>'<%= remote.remoteName %>',<% }); %>]<% } %><% if(type === 'remote') { %>
exposes: {<% if(standalone) { %>
'./Routes': '<%= projectRoot %>/src/app/remote-entry/entry.routes.ts',<% } else { %>
'./Module': '<%= projectRoot %>/src/app/remote-entry/entry.module.ts',<% } %>
},<% } %>
};
export default config;

View File

@ -0,0 +1,4 @@
import {withModuleFederation} from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config);

View File

@ -0,0 +1,16 @@
import {withModuleFederation} from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation({
...config,
/*
* Remote overrides for production.
* Each entry is a pair of a unique name and the URL where it is deployed.
*
* e.g.
* remotes: [
* ['app1', 'https://app1.example.com'],
* ['app2', 'https://app2.example.com'],
* ]
*/
});

View File

@ -35,8 +35,17 @@ export function addRemoteToHost(tree: Tree, options: Schema) {
pathToMFManifest
);
const isHostUsingTypescriptConfig = tree.exists(
joinPathFragments(hostProject.root, 'module-federation.config.ts')
);
if (hostFederationType === 'static') {
addRemoteToStaticHost(tree, options, hostProject);
addRemoteToStaticHost(
tree,
options,
hostProject,
isHostUsingTypescriptConfig
);
} else if (hostFederationType === 'dynamic') {
addRemoteToDynamicHost(tree, options, pathToMFManifest);
}
@ -69,16 +78,19 @@ function determineHostFederationType(
function addRemoteToStaticHost(
tree: Tree,
options: Schema,
hostProject: ProjectConfiguration
hostProject: ProjectConfiguration,
isHostUsingTypescrpt: boolean
) {
const hostMFConfigPath = joinPathFragments(
hostProject.root,
'module-federation.config.js'
isHostUsingTypescrpt
? 'module-federation.config.ts'
: 'module-federation.config.js'
);
if (!hostMFConfigPath || !tree.exists(hostMFConfigPath)) {
throw new Error(
`The selected host application, ${options.host}, does not contain a module-federation.config.js or module-federation.manifest.json file. Are you sure it has been set up as a host application?`
`The selected host application, ${options.host}, does not contain a module-federation.config.{ts,js} or module-federation.manifest.json file. Are you sure it has been set up as a host application?`
);
}

View File

@ -9,18 +9,20 @@ import {
export function changeBuildTarget(host: Tree, options: Schema) {
const appConfig = readProjectConfiguration(host, options.appName);
const configExtName = options.typescriptConfiguration ? 'ts' : 'js';
appConfig.targets.build.executor = '@nx/angular:webpack-browser';
appConfig.targets.build.options = {
...appConfig.targets.build.options,
customWebpackConfig: {
path: `${appConfig.root}/webpack.config.js`,
path: `${appConfig.root}/webpack.config.${configExtName}`,
},
};
appConfig.targets.build.configurations.production = {
...appConfig.targets.build.configurations.production,
customWebpackConfig: {
path: `${appConfig.root}/webpack.prod.config.js`,
path: `${appConfig.root}/webpack.prod.config.${configExtName}`,
},
};

View File

@ -11,7 +11,10 @@ export function generateWebpackConfig(
if (
tree.exists(`${appRoot}/module-federation.config.js`) ||
tree.exists(`${appRoot}/webpack.config.js`) ||
tree.exists(`${appRoot}/webpack.prod.config.js`)
tree.exists(`${appRoot}/webpack.prod.config.js`) ||
tree.exists(`${appRoot}/module-federation.config.ts`) ||
tree.exists(`${appRoot}/webpack.config.ts`) ||
tree.exists(`${appRoot}/webpack.prod.config.ts`)
) {
logger.warn(
`NOTE: We encountered an existing webpack config for the app ${options.appName}. We have overwritten this file with the Module Federation Config.\n
@ -19,9 +22,13 @@ export function generateWebpackConfig(
);
}
const pathToWebpackTemplateFiles = options.typescriptConfiguration
? 'ts-webpack'
: 'webpack';
generateFiles(
tree,
joinPathFragments(__dirname, '../files/webpack'),
joinPathFragments(__dirname, `../files/${pathToWebpackTemplateFiles}`),
appRoot,
{
tmpl: '',

View File

@ -8,6 +8,7 @@ export function normalizeOptions(
): NormalizedOptions {
return {
...options,
typescriptConfiguration: options.typescriptConfiguration ?? true,
federationType: options.federationType ?? 'static',
prefix: options.prefix ?? getProjectPrefix(tree, options.appName),
};

View File

@ -23,7 +23,7 @@ export function setupHostIfDynamic(tree: Tree, options: Schema) {
const pathToProdWebpackConfig = joinPathFragments(
project.root,
'webpack.prod.config.js'
`webpack.prod.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
);
if (tree.exists(pathToProdWebpackConfig)) {
tree.delete(pathToProdWebpackConfig);

View File

@ -14,6 +14,7 @@ export interface Schema {
prefix?: string;
standalone?: boolean;
skipE2E?: boolean;
typescriptConfiguration?: boolean;
}
export interface NormalizedOptions extends Schema {

View File

@ -73,6 +73,11 @@
"type": "boolean",
"description": "Whether the application is a standalone application. _Note: This is only supported in Angular versions >= 14.1.0_",
"default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"required": ["appName", "mfType"],

View File

@ -33,6 +33,7 @@ describe('Init MF', () => {
await setupMf(tree, {
appName: app,
mfType: type,
typescriptConfiguration: false,
});
// ASSERT
@ -51,6 +52,35 @@ describe('Init MF', () => {
}
);
test.each([
['app1', 'host'],
['remote1', 'remote'],
])(
'should create webpack and mf configs correctly when --typescriptConfiguration=true',
async (app, type: 'host' | 'remote') => {
// ACT
await setupMf(tree, {
appName: app,
mfType: type,
typescriptConfiguration: true,
});
// ASSERT
expect(tree.exists(`${app}/module-federation.config.ts`)).toBeTruthy();
expect(tree.exists(`${app}/webpack.config.ts`)).toBeTruthy();
expect(tree.exists(`${app}/webpack.prod.config.ts`)).toBeTruthy();
const webpackContents = tree.read(`${app}/webpack.config.ts`, 'utf-8');
expect(webpackContents).toMatchSnapshot();
const mfConfigContents = tree.read(
`${app}/module-federation.config.ts`,
'utf-8'
);
expect(mfConfigContents).toMatchSnapshot();
}
);
test.each([
['app1', 'host'],
['remote1', 'remote'],
@ -110,6 +140,7 @@ describe('Init MF', () => {
await setupMf(tree, {
appName: app,
mfType: type,
typescriptConfiguration: false,
});
// ASSERT
@ -127,6 +158,34 @@ describe('Init MF', () => {
}
);
test.each([
['app1', 'host'],
['remote1', 'remote'],
])(
'should change the build and serve target and set correct path to webpack config when --typescriptConfiguration=true',
async (app, type: 'host' | 'remote') => {
// ACT
await setupMf(tree, {
appName: app,
mfType: type,
typescriptConfiguration: true,
});
// ASSERT
const { build, serve } = readProjectConfiguration(tree, app).targets;
expect(serve.executor).toEqual(
type === 'host'
? '@nx/angular:module-federation-dev-server'
: '@nx/angular:webpack-dev-server'
);
expect(build.executor).toEqual('@nx/angular:webpack-browser');
expect(build.options.customWebpackConfig.path).toEqual(
`${app}/webpack.config.ts`
);
}
);
it('should not generate a webpack prod file for dynamic host', async () => {
// ACT
await setupMf(tree, {
@ -137,7 +196,7 @@ describe('Init MF', () => {
// ASSERT
const { build } = readProjectConfiguration(tree, 'app1').targets;
expect(tree.exists('app1/webpack.prod.config.js')).toBeFalsy();
expect(tree.exists('app1/webpack.prod.config.ts')).toBeFalsy();
expect(build.configurations.production.customWebpackConfig).toBeUndefined();
});
@ -174,6 +233,7 @@ describe('Init MF', () => {
appName: 'app1',
mfType: 'host',
remotes: ['remote1'],
typescriptConfiguration: false,
});
// ASSERT
@ -185,11 +245,30 @@ describe('Init MF', () => {
expect(mfConfigContents).toContain(`'remote1'`);
});
it('should add the remote config to the host when --remotes flag supplied when --typescriptConfiguration=true', async () => {
// ACT
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
remotes: ['remote1'],
typescriptConfiguration: true,
});
// ASSERT
const mfConfigContents = tree.read(
`app1/module-federation.config.ts`,
'utf-8'
);
expect(mfConfigContents).toContain(`'remote1'`);
});
it('should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it', async () => {
// ARRANGE
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
typescriptConfiguration: false,
});
// ACT
@ -197,6 +276,7 @@ describe('Init MF', () => {
appName: 'remote1',
mfType: 'remote',
host: 'app1',
typescriptConfiguration: false,
});
// ASSERT
@ -204,6 +284,27 @@ describe('Init MF', () => {
expect(hostMfConfig).toMatchSnapshot();
});
it('should add a remote application and add it to a specified host applications webpack config when no other remote has been added to it when --typescriptConfiguration=true', async () => {
// ARRANGE
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
typescriptConfiguration: true,
});
// ACT
await setupMf(tree, {
appName: 'remote1',
mfType: 'remote',
host: 'app1',
typescriptConfiguration: true,
});
// ASSERT
const hostMfConfig = tree.read('app1/module-federation.config.ts', 'utf-8');
expect(hostMfConfig).toMatchSnapshot();
});
it('should add a remote application and add it to a specified host applications webpack config that contains a remote application already', async () => {
// ARRANGE
await generateTestApplication(tree, {
@ -213,6 +314,7 @@ describe('Init MF', () => {
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
typescriptConfiguration: false,
});
await setupMf(tree, {
@ -220,6 +322,7 @@ describe('Init MF', () => {
mfType: 'remote',
host: 'app1',
port: 4201,
typescriptConfiguration: false,
});
// ACT
@ -228,6 +331,7 @@ describe('Init MF', () => {
mfType: 'remote',
host: 'app1',
port: 4202,
typescriptConfiguration: false,
});
// ASSERT
@ -235,6 +339,40 @@ describe('Init MF', () => {
expect(hostMfConfig).toMatchSnapshot();
});
it('should add a remote application and add it to a specified host applications webpack config that contains a remote application already when --typescriptConfiguration=true', async () => {
// ARRANGE
await generateTestApplication(tree, {
name: 'remote2',
});
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
typescriptConfiguration: true,
});
await setupMf(tree, {
appName: 'remote1',
mfType: 'remote',
host: 'app1',
port: 4201,
typescriptConfiguration: true,
});
// ACT
await setupMf(tree, {
appName: 'remote2',
mfType: 'remote',
host: 'app1',
port: 4202,
typescriptConfiguration: true,
});
// ASSERT
const hostMfConfig = tree.read('app1/module-federation.config.ts', 'utf-8');
expect(hostMfConfig).toMatchSnapshot();
});
it('should add a remote application and add it to a specified host applications router config', async () => {
// ARRANGE
await generateTestApplication(tree, {
@ -303,6 +441,7 @@ describe('Init MF', () => {
mfType: 'host',
routing: true,
federationType: 'dynamic',
typescriptConfiguration: false,
});
// ASSERT
@ -314,6 +453,26 @@ describe('Init MF', () => {
).toBeTruthy();
expect(tree.read('app1/src/main.ts', 'utf-8')).toMatchSnapshot();
});
it('should create a host with the correct configurations when --typescriptConfiguration=true', async () => {
// ARRANGE & ACT
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
routing: true,
federationType: 'dynamic',
typescriptConfiguration: true,
});
// ASSERT
expect(tree.read('app1/module-federation.config.ts', 'utf-8')).toContain(
'remotes: []'
);
expect(
tree.exists('app1/src/assets/module-federation.manifest.json')
).toBeTruthy();
expect(tree.read('app1/src/main.ts', 'utf-8')).toMatchSnapshot();
});
});
it('should generate bootstrap with environments for ng14', async () => {
@ -365,6 +524,7 @@ describe('Init MF', () => {
mfType: 'host',
routing: true,
federationType: 'dynamic',
typescriptConfiguration: false,
});
// ACT
@ -374,6 +534,7 @@ describe('Init MF', () => {
port: 4201,
host: 'app1',
routing: true,
typescriptConfiguration: false,
});
// ASSERT
@ -388,6 +549,38 @@ describe('Init MF', () => {
expect(tree.read('app1/src/app/app.routes.ts', 'utf-8')).toMatchSnapshot();
});
it('should add a remote to dynamic host correctly when --typescriptConfiguration=true', async () => {
// ARRANGE
await setupMf(tree, {
appName: 'app1',
mfType: 'host',
routing: true,
federationType: 'dynamic',
typescriptConfiguration: true,
});
// ACT
await setupMf(tree, {
appName: 'remote1',
mfType: 'remote',
port: 4201,
host: 'app1',
routing: true,
typescriptConfiguration: true,
});
// ASSERT
expect(tree.read('app1/module-federation.config.ts', 'utf-8')).toContain(
'remotes: []'
);
expect(
readJson(tree, 'app1/src/assets/module-federation.manifest.json')
).toEqual({
remote1: 'http://localhost:4201',
});
expect(tree.read('app1/src/app/app.routes.ts', 'utf-8')).toMatchSnapshot();
});
it('should throw an error when installed version of angular < 14.1.0 and --standalone is used', async () => {
// ARRANGE
updateJson(tree, 'package.json', (json) => ({