diff --git a/docs/generated/packages/angular/generators/host.json b/docs/generated/packages/angular/generators/host.json
index 09c4d288542164..18eef256069feb 100644
--- a/docs/generated/packages/angular/generators/host.json
+++ b/docs/generated/packages/angular/generators/host.json
@@ -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,
diff --git a/docs/generated/packages/angular/generators/remote.json b/docs/generated/packages/angular/generators/remote.json
index b2a772a64c0c78..457fb29289c7ee 100644
--- a/docs/generated/packages/angular/generators/remote.json
+++ b/docs/generated/packages/angular/generators/remote.json
@@ -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,
diff --git a/docs/generated/packages/angular/generators/setup-mf.json b/docs/generated/packages/angular/generators/setup-mf.json
index adc850d79a8f00..d9c47472c1ad8e 100644
--- a/docs/generated/packages/angular/generators/setup-mf.json
+++ b/docs/generated/packages/angular/generators/setup-mf.json
@@ -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"],
diff --git a/e2e/angular-core/src/module-federation.test.ts b/e2e/angular-core/src/module-federation.test.ts
index 5f7b668844bf37..121e8b38705d7e 100644
--- a/e2e/angular-core/src/module-federation.test.ts
+++ b/e2e/angular-core/src/module-federation.test.ts
@@ -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: '',
@@ -131,7 +132,7 @@ describe('Angular Module Federation', () => {
const process = await runCommandUntil(
`serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp1}`,
(output) =>
- output.includes(`listening on localhost:${remotePort}`) &&
+ output.includes(`NX All remotes started`) &&
output.includes(`listening on localhost:${hostPort}`)
);
@@ -164,8 +165,8 @@ describe('Angular Module Federation', () => {
const process = await runCommandUntil(
`serve ${app1} --dev-remotes=${app2}`,
(output) =>
- output.includes(`listening on localhost:${app1Port}`) &&
- output.includes(`listening on localhost:${app2Port}`)
+ output.includes(`NX All remotes started`) &&
+ output.includes(`listening on localhost:${app1Port}`)
);
// port and process cleanup
@@ -235,7 +236,7 @@ describe('Angular Module Federation', () => {
const process = 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}`)
);
diff --git a/packages/angular/package.json b/packages/angular/package.json
index 747bc783362a75..6fb283f136e7fd 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -64,6 +64,7 @@
"@nx/js": "file:../js",
"@nx/linter": "file:../linter",
"@nx/webpack": "file:../webpack",
+ "@nx/web": "file:../web",
"@nx/workspace": "file:../workspace"
},
"peerDependencies": {
diff --git a/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap b/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap
index 8d0e67c7e35a71..8a5311a66028aa 100644
--- a/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap
+++ b/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap
@@ -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';
diff --git a/packages/angular/src/generators/host/files/src/main.server.ts__tmpl__ b/packages/angular/src/generators/host/files/js/src/main.server.ts__tmpl__
similarity index 100%
rename from packages/angular/src/generators/host/files/src/main.server.ts__tmpl__
rename to packages/angular/src/generators/host/files/js/src/main.server.ts__tmpl__
diff --git a/packages/angular/src/generators/host/files/webpack.server.config.js__tmpl__ b/packages/angular/src/generators/host/files/js/webpack.server.config.js__tmpl__
similarity index 100%
rename from packages/angular/src/generators/host/files/webpack.server.config.js__tmpl__
rename to packages/angular/src/generators/host/files/js/webpack.server.config.js__tmpl__
diff --git a/packages/angular/src/generators/host/files/ts/src/main.server.ts__tmpl__ b/packages/angular/src/generators/host/files/ts/src/main.server.ts__tmpl__
new file mode 100644
index 00000000000000..905a0fd39a60f0
--- /dev/null
+++ b/packages/angular/src/generators/host/files/ts/src/main.server.ts__tmpl__
@@ -0,0 +1,66 @@
+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<% if(standalone) { %> bootstrap <% } else { %> { 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(), '<%= browserBundleOutput %>');
+
+ 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({
+ <% if(standalone) { %>bootstrap<% } else { %>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();
+
+<% if(standalone) { %>export default bootstrap;<% } else { %>export * from './bootstrap.server';<% } %>
diff --git a/packages/angular/src/generators/host/files/ts/webpack.server.config.ts__tmpl__ b/packages/angular/src/generators/host/files/ts/webpack.server.config.ts__tmpl__
new file mode 100644
index 00000000000000..1dda6f2cdf3971
--- /dev/null
+++ b/packages/angular/src/generators/host/files/ts/webpack.server.config.ts__tmpl__
@@ -0,0 +1,4 @@
+import {withModuleFederationForSSR} from '@nx/angular/module-federation';
+import config from './module-federation.config';
+
+export default withModuleFederationForSSR(config)
diff --git a/packages/angular/src/generators/host/host.spec.ts b/packages/angular/src/generators/host/host.spec.ts
index 70d7de742b0361..e06ea779b7e9b7 100644
--- a/packages/angular/src/generators/host/host.spec.ts
+++ b/packages/angular/src/generators/host/host.spec.ts
@@ -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(`
+ "
+
+ "
+ `);
+ });
+
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'`);
+ });
});
});
diff --git a/packages/angular/src/generators/host/host.ts b/packages/angular/src/generators/host/host.ts
index 468680d9d91816..3843d68d0fce48 100644
--- a/packages/angular/src/generators/host/host.ts
+++ b/packages/angular/src/generators/host/host.ts
@@ -23,14 +23,19 @@ 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, ...options }: Schema = {
+ ...schema,
+ typescriptConfiguration: schema.typescriptConfiguration ?? true,
+ };
+
const projects = getProjects(tree);
const remotesToGenerate: string[] = [];
@@ -78,11 +83,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 +118,7 @@ export async function hostInternal(tree: Tree, options: Schema) {
host: hostProjectName,
skipFormat: true,
standalone: options.standalone,
+ typescriptConfiguration,
});
}
diff --git a/packages/angular/src/generators/host/lib/add-ssr.ts b/packages/angular/src/generators/host/lib/add-ssr.ts
index 452da0af06f51b..447f9f0bb65487 100644
--- a/packages/angular/src/generators/host/lib/add-ssr.ts
+++ b/packages/angular/src/generators/host/lib/add-ssr.ts
@@ -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,29 @@ export async function addSsr(tree: Tree, options: Schema, appName: string) {
'browser'
);
- generateFiles(tree, join(__dirname, '../files'), project.root, {
- appName,
- browserBundleOutput,
- standalone: options.standalone,
- tmpl: '',
- });
+ const pathToTemplateFiles = typescriptConfiguration ? 'ts' : 'js';
+
+ generateFiles(
+ tree,
+ join(__dirname, '../files', pathToTemplateFiles),
+ project.root,
+ {
+ appName,
+ browserBundleOutput,
+ standalone: options.standalone,
+ 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 =
diff --git a/packages/angular/src/generators/host/schema.d.ts b/packages/angular/src/generators/host/schema.d.ts
index 4ad7be7d2578f8..4e77dceaabcf40 100644
--- a/packages/angular/src/generators/host/schema.d.ts
+++ b/packages/angular/src/generators/host/schema.d.ts
@@ -29,4 +29,5 @@ export interface Schema {
skipFormat?: boolean;
standalone?: boolean;
ssr?: boolean;
+ typescriptConfiguration?: boolean;
}
diff --git a/packages/angular/src/generators/host/schema.json b/packages/angular/src/generators/host/schema.json
index 5c751de9db98be..890a298c2992aa 100644
--- a/packages/angular/src/generators/host/schema.json
+++ b/packages/angular/src/generators/host/schema.json
@@ -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,
diff --git a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap
index d4616f812880f8..abe333132c0a1e 100644
--- a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap
+++ b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap
@@ -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: \`\`,
+})
+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: \`\`,
+})
+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 },
+];
+"
+`;
diff --git a/packages/angular/src/generators/remote/files/base-ts/src/main.server.ts__tmpl__ b/packages/angular/src/generators/remote/files/base-ts/src/main.server.ts__tmpl__
new file mode 100644
index 00000000000000..2551ff8dd4977a
--- /dev/null
+++ b/packages/angular/src/generators/remote/files/base-ts/src/main.server.ts__tmpl__
@@ -0,0 +1,74 @@
+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<% if(standalone) { %> bootstrap <% } else { %> { 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(), '<%= browserBundleOutput %>');
+ const serverBundles = join(process.cwd(), '<%= serverBundleOutput %>');
+
+ 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({
+ <% if(standalone) { %>bootstrap<% } else { %>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();
+
+<% if(standalone) { %>export default bootstrap;<% } else { %>export * from './bootstrap.server';<% } %>
diff --git a/packages/angular/src/generators/remote/files/base-ts/webpack.server.config.ts__tmpl__ b/packages/angular/src/generators/remote/files/base-ts/webpack.server.config.ts__tmpl__
new file mode 100644
index 00000000000000..1dda6f2cdf3971
--- /dev/null
+++ b/packages/angular/src/generators/remote/files/base-ts/webpack.server.config.ts__tmpl__
@@ -0,0 +1,4 @@
+import {withModuleFederationForSSR} from '@nx/angular/module-federation';
+import config from './module-federation.config';
+
+export default withModuleFederationForSSR(config)
diff --git a/packages/angular/src/generators/remote/lib/add-ssr.ts b/packages/angular/src/generators/remote/lib/add-ssr.ts
index ff388769457b60..fbc3e3ecf49070 100644
--- a/packages/angular/src/generators/remote/lib/add-ssr.ts
+++ b/packages/angular/src/generators/remote/lib/add-ssr.ts
@@ -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);
@@ -50,9 +56,11 @@ export async function addSsr(
'server'
);
+ const pathToTemplateFiles = typescriptConfiguration ? 'base-ts' : 'base';
+
generateFiles(
tree,
- joinPathFragments(__dirname, '../files/base'),
+ joinPathFragments(__dirname, `../files/${pathToTemplateFiles}`),
project.root,
{
appName,
@@ -81,7 +89,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 ?? {}),
diff --git a/packages/angular/src/generators/remote/remote.spec.ts b/packages/angular/src/generators/remote/remote.spec.ts
index 2c981d46d8c30e..f96a97af65a49d 100644
--- a/packages/angular/src/generators/remote/remote.spec.ts
+++ b/packages/angular/src/generators/remote/remote.spec.ts
@@ -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);
diff --git a/packages/angular/src/generators/remote/remote.ts b/packages/angular/src/generators/remote/remote.ts
index 633a2ed2eff089..30f43bf4f2b572 100644
--- a/packages/angular/src/generators/remote/remote.ts
+++ b/packages/angular/src/generators/remote/remote.ts
@@ -21,14 +21,19 @@ 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, ...options }: Schema = {
+ ...schema,
+ typescriptConfiguration: schema.typescriptConfiguration ?? true,
+ };
+
const projects = getProjects(tree);
if (options.host && !projects.has(options.host)) {
throw new Error(
@@ -71,6 +76,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 +84,7 @@ export async function remoteInternal(tree: Tree, options: Schema) {
let ssrInstallTask = await addSsr(tree, {
appName: remoteProjectName,
port,
+ typescriptConfiguration,
standalone: options.standalone,
});
installTasks.push(ssrInstallTask);
diff --git a/packages/angular/src/generators/remote/schema.d.ts b/packages/angular/src/generators/remote/schema.d.ts
index e2ec3c49a06d54..a1fe31e6f2f94b 100644
--- a/packages/angular/src/generators/remote/schema.d.ts
+++ b/packages/angular/src/generators/remote/schema.d.ts
@@ -28,4 +28,5 @@ export interface Schema {
skipFormat?: boolean;
standalone?: boolean;
ssr?: boolean;
+ typescriptConfiguration?: boolean;
}
diff --git a/packages/angular/src/generators/remote/schema.json b/packages/angular/src/generators/remote/schema.json
index 2817cdeb89c811..b819e8c7d4af2c 100644
--- a/packages/angular/src/generators/remote/schema.json
+++ b/packages/angular/src/generators/remote/schema.json
@@ -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,
diff --git a/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap b/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap
index 5b107fab4be9e7..422e2f2d06695a 100644
--- a/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap
+++ b/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap
@@ -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';
diff --git a/packages/angular/src/generators/setup-mf/files/ts-webpack/module-federation.config.ts__tmpl__ b/packages/angular/src/generators/setup-mf/files/ts-webpack/module-federation.config.ts__tmpl__
new file mode 100644
index 00000000000000..867ac426a88af7
--- /dev/null
+++ b/packages/angular/src/generators/setup-mf/files/ts-webpack/module-federation.config.ts__tmpl__
@@ -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;
diff --git a/packages/angular/src/generators/setup-mf/files/ts-webpack/webpack.config.ts__tmpl__ b/packages/angular/src/generators/setup-mf/files/ts-webpack/webpack.config.ts__tmpl__
new file mode 100644
index 00000000000000..3109786ab24a18
--- /dev/null
+++ b/packages/angular/src/generators/setup-mf/files/ts-webpack/webpack.config.ts__tmpl__
@@ -0,0 +1,4 @@
+import {withModuleFederation} from '@nx/angular/module-federation';
+import config from './module-federation.config';
+
+export default withModuleFederation(config);
diff --git a/packages/angular/src/generators/setup-mf/files/ts-webpack/webpack.prod.config.ts__tmpl__ b/packages/angular/src/generators/setup-mf/files/ts-webpack/webpack.prod.config.ts__tmpl__
new file mode 100644
index 00000000000000..eb9b8d26e78239
--- /dev/null
+++ b/packages/angular/src/generators/setup-mf/files/ts-webpack/webpack.prod.config.ts__tmpl__
@@ -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'],
+ * ]
+ */
+});
diff --git a/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts b/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts
index 8e2ff440596443..51e8bd4096154f 100644
--- a/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts
+++ b/packages/angular/src/generators/setup-mf/lib/add-remote-to-host.ts
@@ -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?`
);
}
diff --git a/packages/angular/src/generators/setup-mf/lib/change-build-target.ts b/packages/angular/src/generators/setup-mf/lib/change-build-target.ts
index add11f0daeb736..e2fc0596c4bc28 100644
--- a/packages/angular/src/generators/setup-mf/lib/change-build-target.ts
+++ b/packages/angular/src/generators/setup-mf/lib/change-build-target.ts
@@ -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}`,
},
};
diff --git a/packages/angular/src/generators/setup-mf/lib/generate-config.ts b/packages/angular/src/generators/setup-mf/lib/generate-config.ts
index 47bd394d5a8439..24c21e8698c693 100644
--- a/packages/angular/src/generators/setup-mf/lib/generate-config.ts
+++ b/packages/angular/src/generators/setup-mf/lib/generate-config.ts
@@ -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: '',
diff --git a/packages/angular/src/generators/setup-mf/lib/normalize-options.ts b/packages/angular/src/generators/setup-mf/lib/normalize-options.ts
index cafa8510d1ec14..04ec9ae4e9d787 100644
--- a/packages/angular/src/generators/setup-mf/lib/normalize-options.ts
+++ b/packages/angular/src/generators/setup-mf/lib/normalize-options.ts
@@ -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),
};
diff --git a/packages/angular/src/generators/setup-mf/lib/setup-host-if-dynamic.ts b/packages/angular/src/generators/setup-mf/lib/setup-host-if-dynamic.ts
index ed2f4f21a1337c..5717e019318544 100644
--- a/packages/angular/src/generators/setup-mf/lib/setup-host-if-dynamic.ts
+++ b/packages/angular/src/generators/setup-mf/lib/setup-host-if-dynamic.ts
@@ -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);
diff --git a/packages/angular/src/generators/setup-mf/schema.d.ts b/packages/angular/src/generators/setup-mf/schema.d.ts
index aed4d9728004eb..1c0c875f28feb7 100644
--- a/packages/angular/src/generators/setup-mf/schema.d.ts
+++ b/packages/angular/src/generators/setup-mf/schema.d.ts
@@ -14,6 +14,7 @@ export interface Schema {
prefix?: string;
standalone?: boolean;
skipE2E?: boolean;
+ typescriptConfiguration?: boolean;
}
export interface NormalizedOptions extends Schema {
diff --git a/packages/angular/src/generators/setup-mf/schema.json b/packages/angular/src/generators/setup-mf/schema.json
index 2331bb4ba0ffb8..69ab1639c41d6d 100644
--- a/packages/angular/src/generators/setup-mf/schema.json
+++ b/packages/angular/src/generators/setup-mf/schema.json
@@ -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"],
diff --git a/packages/angular/src/generators/setup-mf/setup-mf.spec.ts b/packages/angular/src/generators/setup-mf/setup-mf.spec.ts
index 65c337ccd065fa..ea3569dfc5782d 100644
--- a/packages/angular/src/generators/setup-mf/setup-mf.spec.ts
+++ b/packages/angular/src/generators/setup-mf/setup-mf.spec.ts
@@ -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) => ({