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", "type": "boolean",
"default": false, "default": false,
"x-priority": "important" "x-priority": "important"
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"additionalProperties": false, "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.", "description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

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

View File

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

View File

@ -229,7 +229,10 @@ export function runCommandAsync(
export function runCommandUntil( export function runCommandUntil(
command: string, command: string,
criteria: (output: string) => boolean criteria: (output: string) => boolean,
opts: RunCmdOpts = {
env: undefined,
}
): Promise<ChildProcess> { ): Promise<ChildProcess> {
const pm = getPackageManagerCommand(); const pm = getPackageManagerCommand();
const p = exec(`${pm.runNx} ${command}`, { const p = exec(`${pm.runNx} ${command}`, {
@ -238,6 +241,7 @@ export function runCommandUntil(
env: { env: {
CI: 'true', CI: 'true',
...getStrippedEnvironmentVariables(), ...getStrippedEnvironmentVariables(),
...opts.env,
FORCE_COLOR: 'false', 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`] = ` exports[`Host App Generator should generate a host app with a remote 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation'); "const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config'); 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`] = ` exports[`Host App Generator should generate a host app with no remotes 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation'); "const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config'); 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`] = ` exports[`Host App Generator should generate a host with remotes using standalone components 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; 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 // ACT
await generateTestHostApplication(tree, { await generateTestHostApplication(tree, {
name: 'test', name: 'test',
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot(); 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 () => { it('should generate a host app with a remote', async () => {
// ARRANGE // ARRANGE
@ -30,18 +44,40 @@ describe('Host App Generator', () => {
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'remote', name: 'remote',
typescriptConfiguration: false,
}); });
// ACT // ACT
await generateTestHostApplication(tree, { await generateTestHostApplication(tree, {
name: 'test', name: 'test',
remotes: ['remote'], remotes: ['remote'],
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
expect(tree.read('remote/webpack.config.js', 'utf-8')).toMatchSnapshot(); expect(tree.read('remote/webpack.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/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 () => { it('should generate a host and any remotes that dont exist with correct routing setup', async () => {
// ARRANGE // ARRANGE
@ -52,6 +88,7 @@ describe('Host App Generator', () => {
await generateTestHostApplication(tree, { await generateTestHostApplication(tree, {
name: 'hostApp', name: 'hostApp',
remotes: ['remote1', 'remote2'], remotes: ['remote1', 'remote2'],
typescriptConfiguration: false,
}); });
// ASSERT // 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 () => { it('should generate a host, integrate existing remotes and generate any remotes that dont exist', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'remote1', name: 'remote1',
typescriptConfiguration: false,
}); });
// ACT // ACT
await generateTestHostApplication(tree, { await generateTestHostApplication(tree, {
name: 'hostApp', name: 'hostApp',
remotes: ['remote1', 'remote2', 'remote3'], remotes: ['remote1', 'remote2', 'remote3'],
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -94,11 +163,36 @@ describe('Host App Generator', () => {
).toContain(`'remote1', 'remote2', 'remote3'`); ).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 () => { it('should generate a host, integrate existing remotes and generate any remotes that dont exist, in a directory', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'remote1', name: 'remote1',
typescriptConfiguration: false,
}); });
// ACT // ACT
@ -106,6 +200,7 @@ describe('Host App Generator', () => {
name: 'hostApp', name: 'hostApp',
directory: 'foo/hostApp', directory: 'foo/hostApp',
remotes: ['remote1', 'remote2', 'remote3'], remotes: ['remote1', 'remote2', 'remote3'],
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -117,6 +212,31 @@ describe('Host App Generator', () => {
).toContain(`'remote1', 'remote2', 'remote3'`); ).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 () => { it('should generate a host with remotes using standalone components', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -197,6 +317,7 @@ describe('Host App Generator', () => {
await generateTestHostApplication(tree, { await generateTestHostApplication(tree, {
name: 'test', name: 'test',
ssr: true, ssr: true,
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -223,6 +344,41 @@ describe('Host App Generator', () => {
expect(project.targets['serve-ssr']).toMatchSnapshot(); 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 () => { it('should generate the correct files for standalone', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -232,6 +388,7 @@ describe('Host App Generator', () => {
name: 'test', name: 'test',
standalone: true, standalone: true,
ssr: true, ssr: true,
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -261,6 +418,46 @@ describe('Host App Generator', () => {
expect(project.targets.server).toMatchSnapshot(); expect(project.targets.server).toMatchSnapshot();
expect(project.targets['serve-ssr']).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 () => { it('should error correctly when Angular version does not support standalone', async () => {
@ -291,6 +488,7 @@ describe('Host App Generator', () => {
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'remote1', name: 'remote1',
projectNameAndRootFormat: 'derived', projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
}); });
// ACT // ACT
@ -298,6 +496,7 @@ describe('Host App Generator', () => {
name: 'hostApp', name: 'hostApp',
remotes: ['remote1', 'remote2', 'remote3'], remotes: ['remote1', 'remote2', 'remote3'],
projectNameAndRootFormat: 'derived', projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -315,6 +514,7 @@ describe('Host App Generator', () => {
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'remote1', name: 'remote1',
projectNameAndRootFormat: 'derived', projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
}); });
// ACT // ACT
@ -323,6 +523,7 @@ describe('Host App Generator', () => {
directory: 'foo', directory: 'foo',
remotes: ['remote1', 'remote2', 'remote3'], remotes: ['remote1', 'remote2', 'remote3'],
projectNameAndRootFormat: 'derived', projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -333,5 +534,31 @@ describe('Host App Generator', () => {
tree.read('apps/foo/host-app/module-federation.config.js', 'utf-8') tree.read('apps/foo/host-app/module-federation.config.js', 'utf-8')
).toContain(`'remote1', 'foo-remote2', 'foo-remote3'`); ).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); 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}. 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.`); 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 projects = getProjects(tree);
const remotesToGenerate: string[] = []; const remotesToGenerate: string[] = [];
@ -78,11 +80,17 @@ export async function hostInternal(tree: Tree, options: Schema) {
skipE2E, skipE2E,
e2eProjectName: skipE2E ? undefined : `${hostProjectName}-e2e`, e2eProjectName: skipE2E ? undefined : `${hostProjectName}-e2e`,
prefix: options.prefix, prefix: options.prefix,
typescriptConfiguration,
}); });
let installTasks = [appInstallTask]; let installTasks = [appInstallTask];
if (options.ssr) { if (options.ssr) {
let ssrInstallTask = await addSsr(tree, options, hostProjectName); let ssrInstallTask = await addSsr(
tree,
options,
hostProjectName,
typescriptConfiguration
);
installTasks.push(ssrInstallTask); installTasks.push(ssrInstallTask);
} }
@ -107,6 +115,7 @@ export async function hostInternal(tree: Tree, options: Schema) {
host: hostProjectName, host: hostProjectName,
skipFormat: true, skipFormat: true,
standalone: options.standalone, standalone: options.standalone,
typescriptConfiguration,
}); });
} }

View File

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

View File

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

View File

@ -173,6 +173,11 @@
"type": "boolean", "type": "boolean",
"default": false, "default": false,
"x-priority": "important" "x-priority": "important"
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"additionalProperties": false, "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`] = ` exports[`MF Remote App Generator should generate a remote mf app with a host 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation'); "const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config'); 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`] = ` exports[`MF Remote App Generator should generate a remote mf app with no host 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation'); "const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config'); 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`] = ` exports[`MF Remote App Generator should generate the a remote setup for standalone components 1`] = `
"import { bootstrapApplication } from '@angular/platform-browser'; "import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; 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, appName,
port, port,
standalone, standalone,
}: { appName: string; port: number; standalone: boolean } typescriptConfiguration,
}: {
appName: string;
port: number;
standalone: boolean;
typescriptConfiguration: boolean;
}
) { ) {
let project = readProjectConfiguration(tree, appName); let project = readProjectConfiguration(tree, appName);
@ -52,7 +58,7 @@ export async function addSsr(
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, '../files/base'), joinPathFragments(__dirname, `../files/common`),
project.root, project.root,
{ {
appName, 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) { if (standalone) {
generateFiles( generateFiles(
tree, tree,
@ -81,7 +98,10 @@ export async function addSsr(
project.targets.server.executor = '@nx/angular:webpack-server'; project.targets.server.executor = '@nx/angular:webpack-server';
project.targets.server.options.customWebpackConfig = { 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 = {
...(project.targets['serve-ssr'].options ?? {}), ...(project.targets['serve-ssr'].options ?? {}),

View File

@ -23,6 +23,7 @@ describe('MF Remote App Generator', () => {
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'test', name: 'test',
port: 4201, port: 4201,
typescriptConfiguration: false,
}); });
// ASSERT // 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 () => { it('should generate a remote mf app with a host', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestHostApplication(tree, { await generateTestHostApplication(tree, {
name: 'host', name: 'host',
typescriptConfiguration: false,
}); });
// ACT // ACT
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'test', name: 'test',
host: 'host', host: 'host',
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -52,6 +70,27 @@ describe('MF Remote App Generator', () => {
expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot(); 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 () => { it('should error when a remote app is attempted to be generated with an incorrect host', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -125,6 +164,7 @@ describe('MF Remote App Generator', () => {
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'test', name: 'test',
standalone: true, standalone: true,
typescriptConfiguration: false,
}); });
// ASSERT // 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 () => { it('should not generate an e2e project when e2eTestRunner is none', async () => {
// ARRANGE // ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -217,6 +287,7 @@ describe('MF Remote App Generator', () => {
await generateTestRemoteApplication(tree, { await generateTestRemoteApplication(tree, {
name: 'test', name: 'test',
ssr: true, ssr: true,
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -254,6 +325,53 @@ describe('MF Remote App Generator', () => {
).toMatchSnapshot(); ).toMatchSnapshot();
expect(project.targets['static-server']).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 () => { it('should error correctly when Angular version does not support standalone', async () => {
@ -285,6 +403,7 @@ describe('MF Remote App Generator', () => {
name: 'test', name: 'test',
port: 4201, port: 4201,
projectNameAndRootFormat: 'derived', projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
}); });
expect(tree.exists('apps/test/webpack.config.js')).toBe(true); expect(tree.exists('apps/test/webpack.config.js')).toBe(true);
@ -299,6 +418,7 @@ describe('MF Remote App Generator', () => {
port: 4201, port: 4201,
directory: 'shared', directory: 'shared',
projectNameAndRootFormat: 'derived', projectNameAndRootFormat: 'derived',
typescriptConfiguration: false,
}); });
expect(tree.exists('apps/shared/test/webpack.config.js')).toBe(true); 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); 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}. 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.`); 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 projects = getProjects(tree);
if (options.host && !projects.has(options.host)) { if (options.host && !projects.has(options.host)) {
throw new Error( throw new Error(
@ -71,6 +73,7 @@ export async function remoteInternal(tree: Tree, options: Schema) {
e2eProjectName: skipE2E ? undefined : `${remoteProjectName}-e2e`, e2eProjectName: skipE2E ? undefined : `${remoteProjectName}-e2e`,
standalone: options.standalone, standalone: options.standalone,
prefix: options.prefix, prefix: options.prefix,
typescriptConfiguration,
}); });
let installTasks = [appInstallTask]; let installTasks = [appInstallTask];
@ -78,6 +81,7 @@ export async function remoteInternal(tree: Tree, options: Schema) {
let ssrInstallTask = await addSsr(tree, { let ssrInstallTask = await addSsr(tree, {
appName: remoteProjectName, appName: remoteProjectName,
port, port,
typescriptConfiguration,
standalone: options.standalone, standalone: options.standalone,
}); });
installTasks.push(ssrInstallTask); installTasks.push(ssrInstallTask);

View File

@ -28,4 +28,5 @@ export interface Schema {
skipFormat?: boolean; skipFormat?: boolean;
standalone?: boolean; standalone?: boolean;
ssr?: 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.", "description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
} }
}, },
"additionalProperties": false, "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`] = ` 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 { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router'; 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`] = ` 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 = { "module.exports = {
name: 'app1', 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`] = ` exports[`Init MF should add a remote to dynamic host correctly 1`] = `
"import { NxWelcomeComponent } from './nx-welcome.component'; "import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router'; 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`] = ` exports[`Init MF should create webpack and mf configs correctly 1`] = `
"const { withModuleFederation } = require('@nx/angular/module-federation'); "const { withModuleFederation } = require('@nx/angular/module-federation');
const config = require('./module-federation.config'); 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`] = ` exports[`Init MF should generate the remote entry component correctly when prefix is not provided 1`] = `
"import { Component } from '@angular/core'; "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 pathToMFManifest
); );
const isHostUsingTypescriptConfig = tree.exists(
joinPathFragments(hostProject.root, 'module-federation.config.ts')
);
if (hostFederationType === 'static') { if (hostFederationType === 'static') {
addRemoteToStaticHost(tree, options, hostProject); addRemoteToStaticHost(
tree,
options,
hostProject,
isHostUsingTypescriptConfig
);
} else if (hostFederationType === 'dynamic') { } else if (hostFederationType === 'dynamic') {
addRemoteToDynamicHost(tree, options, pathToMFManifest); addRemoteToDynamicHost(tree, options, pathToMFManifest);
} }
@ -69,16 +78,19 @@ function determineHostFederationType(
function addRemoteToStaticHost( function addRemoteToStaticHost(
tree: Tree, tree: Tree,
options: Schema, options: Schema,
hostProject: ProjectConfiguration hostProject: ProjectConfiguration,
isHostUsingTypescrpt: boolean
) { ) {
const hostMFConfigPath = joinPathFragments( const hostMFConfigPath = joinPathFragments(
hostProject.root, hostProject.root,
'module-federation.config.js' isHostUsingTypescrpt
? 'module-federation.config.ts'
: 'module-federation.config.js'
); );
if (!hostMFConfigPath || !tree.exists(hostMFConfigPath)) { if (!hostMFConfigPath || !tree.exists(hostMFConfigPath)) {
throw new Error( 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) { export function changeBuildTarget(host: Tree, options: Schema) {
const appConfig = readProjectConfiguration(host, options.appName); const appConfig = readProjectConfiguration(host, options.appName);
const configExtName = options.typescriptConfiguration ? 'ts' : 'js';
appConfig.targets.build.executor = '@nx/angular:webpack-browser'; appConfig.targets.build.executor = '@nx/angular:webpack-browser';
appConfig.targets.build.options = { appConfig.targets.build.options = {
...appConfig.targets.build.options, ...appConfig.targets.build.options,
customWebpackConfig: { customWebpackConfig: {
path: `${appConfig.root}/webpack.config.js`, path: `${appConfig.root}/webpack.config.${configExtName}`,
}, },
}; };
appConfig.targets.build.configurations.production = { appConfig.targets.build.configurations.production = {
...appConfig.targets.build.configurations.production, ...appConfig.targets.build.configurations.production,
customWebpackConfig: { 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 ( if (
tree.exists(`${appRoot}/module-federation.config.js`) || tree.exists(`${appRoot}/module-federation.config.js`) ||
tree.exists(`${appRoot}/webpack.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( 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 `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( generateFiles(
tree, tree,
joinPathFragments(__dirname, '../files/webpack'), joinPathFragments(__dirname, `../files/${pathToWebpackTemplateFiles}`),
appRoot, appRoot,
{ {
tmpl: '', tmpl: '',

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,7 @@ describe('Init MF', () => {
await setupMf(tree, { await setupMf(tree, {
appName: app, appName: app,
mfType: type, mfType: type,
typescriptConfiguration: false,
}); });
// ASSERT // 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([ test.each([
['app1', 'host'], ['app1', 'host'],
['remote1', 'remote'], ['remote1', 'remote'],
@ -110,6 +140,7 @@ describe('Init MF', () => {
await setupMf(tree, { await setupMf(tree, {
appName: app, appName: app,
mfType: type, mfType: type,
typescriptConfiguration: false,
}); });
// ASSERT // 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 () => { it('should not generate a webpack prod file for dynamic host', async () => {
// ACT // ACT
await setupMf(tree, { await setupMf(tree, {
@ -137,7 +196,7 @@ describe('Init MF', () => {
// ASSERT // ASSERT
const { build } = readProjectConfiguration(tree, 'app1').targets; 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(); expect(build.configurations.production.customWebpackConfig).toBeUndefined();
}); });
@ -174,6 +233,7 @@ describe('Init MF', () => {
appName: 'app1', appName: 'app1',
mfType: 'host', mfType: 'host',
remotes: ['remote1'], remotes: ['remote1'],
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -185,11 +245,30 @@ describe('Init MF', () => {
expect(mfConfigContents).toContain(`'remote1'`); 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 () => { 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 // ARRANGE
await setupMf(tree, { await setupMf(tree, {
appName: 'app1', appName: 'app1',
mfType: 'host', mfType: 'host',
typescriptConfiguration: false,
}); });
// ACT // ACT
@ -197,6 +276,7 @@ describe('Init MF', () => {
appName: 'remote1', appName: 'remote1',
mfType: 'remote', mfType: 'remote',
host: 'app1', host: 'app1',
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -204,6 +284,27 @@ describe('Init MF', () => {
expect(hostMfConfig).toMatchSnapshot(); 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 () => { it('should add a remote application and add it to a specified host applications webpack config that contains a remote application already', async () => {
// ARRANGE // ARRANGE
await generateTestApplication(tree, { await generateTestApplication(tree, {
@ -213,6 +314,7 @@ describe('Init MF', () => {
await setupMf(tree, { await setupMf(tree, {
appName: 'app1', appName: 'app1',
mfType: 'host', mfType: 'host',
typescriptConfiguration: false,
}); });
await setupMf(tree, { await setupMf(tree, {
@ -220,6 +322,7 @@ describe('Init MF', () => {
mfType: 'remote', mfType: 'remote',
host: 'app1', host: 'app1',
port: 4201, port: 4201,
typescriptConfiguration: false,
}); });
// ACT // ACT
@ -228,6 +331,7 @@ describe('Init MF', () => {
mfType: 'remote', mfType: 'remote',
host: 'app1', host: 'app1',
port: 4202, port: 4202,
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -235,6 +339,40 @@ describe('Init MF', () => {
expect(hostMfConfig).toMatchSnapshot(); 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 () => { it('should add a remote application and add it to a specified host applications router config', async () => {
// ARRANGE // ARRANGE
await generateTestApplication(tree, { await generateTestApplication(tree, {
@ -303,6 +441,7 @@ describe('Init MF', () => {
mfType: 'host', mfType: 'host',
routing: true, routing: true,
federationType: 'dynamic', federationType: 'dynamic',
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -314,6 +453,26 @@ describe('Init MF', () => {
).toBeTruthy(); ).toBeTruthy();
expect(tree.read('app1/src/main.ts', 'utf-8')).toMatchSnapshot(); 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 () => { it('should generate bootstrap with environments for ng14', async () => {
@ -365,6 +524,7 @@ describe('Init MF', () => {
mfType: 'host', mfType: 'host',
routing: true, routing: true,
federationType: 'dynamic', federationType: 'dynamic',
typescriptConfiguration: false,
}); });
// ACT // ACT
@ -374,6 +534,7 @@ describe('Init MF', () => {
port: 4201, port: 4201,
host: 'app1', host: 'app1',
routing: true, routing: true,
typescriptConfiguration: false,
}); });
// ASSERT // ASSERT
@ -388,6 +549,38 @@ describe('Init MF', () => {
expect(tree.read('app1/src/app/app.routes.ts', 'utf-8')).toMatchSnapshot(); 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 () => { it('should throw an error when installed version of angular < 14.1.0 and --standalone is used', async () => {
// ARRANGE // ARRANGE
updateJson(tree, 'package.json', (json) => ({ updateJson(tree, 'package.json', (json) => ({