diff --git a/docs/generated/packages/angular.json b/docs/generated/packages/angular.json
index a7a9802d3c..81b9ee63a5 100644
--- a/docs/generated/packages/angular.json
+++ b/docs/generated/packages/angular.json
@@ -1195,6 +1195,11 @@
"description": "Whether to generate a remote application with standalone components.",
"type": "boolean",
"default": false
+ },
+ "ssr": {
+ "description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
+ "type": "boolean",
+ "default": false
}
},
"additionalProperties": false,
diff --git a/e2e/angular-core/src/projects.test.ts b/e2e/angular-core/src/projects.test.ts
index ce90019b6f..52747988dd 100644
--- a/e2e/angular-core/src/projects.test.ts
+++ b/e2e/angular-core/src/projects.test.ts
@@ -363,6 +363,40 @@ describe('Angular Projects', () => {
expect(buildOutput).toContain('Successfully ran target build');
}, 300000);
+ it('MF - should serve a ssr remote app successfully', async () => {
+ // ARRANGE
+ const remoteApp1 = uniq('remote');
+ // generate remote apps
+ runCLI(
+ `generate @nrwl/angular:remote ${remoteApp1} --ssr --no-interactive`
+ );
+
+ let process: ChildProcess;
+
+ try {
+ process = await runCommandUntil(`serve-ssr ${remoteApp1}`, (output) => {
+ return (
+ output.includes(`Browser application bundle generation complete.`) &&
+ output.includes(`Server application bundle generation complete.`) &&
+ output.includes(
+ `Angular Universal Live Development Server is listening`
+ )
+ );
+ });
+ } catch (err) {
+ console.error(err);
+ }
+
+ // port and process cleanup
+ try {
+ if (process && process.pid) {
+ await promisifiedTreeKill(process.pid, 'SIGKILL');
+ }
+ } catch (err) {
+ expect(err).toBeFalsy();
+ }
+ }, 300000);
+
it('Custom Webpack Config for SSR - should serve the app correctly', async () => {
// ARRANGE
const ssrApp = uniq('app');
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 02633d0173..4effca8f98 100644
--- a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap
+++ b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap
@@ -1,5 +1,187 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`MF Remote App Generator --ssr should generate the correct files 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.withServerTransition({ appId: 'serverApp' }),
+ 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 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 3`] = `
+"/***************************************************************************************************
+ * Initialize the server environment - for example, adding DOM built-in types to the global scope.
+ *
+ * NOTE:
+ * This import must come before any imports (direct or transitive) that rely on DOM built-ins being
+ * available, such as \`@angular/elements\`.
+ */
+import '@angular/platform-server/init';
+
+export { AppServerModule } from './app/app.server.module';
+export { renderModule } from '@angular/platform-server';"
+`;
+
+exports[`MF Remote App Generator --ssr should generate the correct files 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/apps/test/browser');
+ const serverBundles = join(process.cwd(), 'dist/apps/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}\`);
+ });
+}
+
+run();
+
+export * from './bootstrap.server';"
+`;
+
+exports[`MF Remote App Generator --ssr should generate the correct files 5`] = `"import('./src/main.server');"`;
+
+exports[`MF Remote App Generator --ssr should generate the correct files 6`] = `
+"module.exports = {
+ name: 'test',
+ exposes: {
+ './Module': 'apps/test/src/app/remote-entry/entry.module.ts',
+ },
+}"
+`;
+
+exports[`MF Remote App Generator --ssr should generate the correct files 7`] = `
+"const { withModuleFederationForSSR } = require('@nrwl/angular/module-federation');
+const config = require('./module-federation.config');
+module.exports = withModuleFederationForSSR(config)"
+`;
+
+exports[`MF Remote App Generator --ssr should generate the correct files 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 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 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 11`] = `
+Object {
+ "configurations": Object {
+ "development": Object {
+ "extractLicenses": false,
+ "optimization": false,
+ "sourceMap": true,
+ },
+ "production": Object {
+ "outputHashing": "media",
+ },
+ },
+ "defaultConfiguration": "production",
+ "executor": "@nrwl/angular:webpack-server",
+ "options": Object {
+ "customWebpackConfig": Object {
+ "path": "apps/test/webpack.server.config.js",
+ },
+ "main": "apps/test/server.ts",
+ "outputPath": "dist/apps/test/server",
+ "tsConfig": "apps/test/tsconfig.server.json",
+ },
+}
+`;
+
exports[`MF Remote App Generator should generate a remote mf app with a host 1`] = `
"const { withModuleFederation } = require('@nrwl/angular/module-federation');
const config = require('./module-federation.config');
diff --git a/packages/angular/src/generators/remote/files/src/main.server.ts__tmpl__ b/packages/angular/src/generators/remote/files/src/main.server.ts__tmpl__
new file mode 100644
index 0000000000..5c5b49f19b
--- /dev/null
+++ b/packages/angular/src/generators/remote/files/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 { 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/apps/<%= appName %>/browser');
+ const serverBundles = join(process.cwd(), 'dist/apps/<%= appName %>/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}`);
+ });
+}
+
+run();
+
+export * from './bootstrap.server';
\ No newline at end of file
diff --git a/packages/angular/src/generators/remote/files/webpack.server.config.js__tmpl__ b/packages/angular/src/generators/remote/files/webpack.server.config.js__tmpl__
new file mode 100644
index 0000000000..a859109e6d
--- /dev/null
+++ b/packages/angular/src/generators/remote/files/webpack.server.config.js__tmpl__
@@ -0,0 +1,3 @@
+const { withModuleFederationForSSR } = require('@nrwl/angular/module-federation');
+const config = require('./module-federation.config');
+module.exports = withModuleFederationForSSR(config)
\ No newline at end of file
diff --git a/packages/angular/src/generators/remote/lib/add-ssr.ts b/packages/angular/src/generators/remote/lib/add-ssr.ts
new file mode 100644
index 0000000000..599a4d0aef
--- /dev/null
+++ b/packages/angular/src/generators/remote/lib/add-ssr.ts
@@ -0,0 +1,58 @@
+import type { Tree } from '@nrwl/devkit';
+import {
+ addDependenciesToPackageJson,
+ generateFiles,
+ joinPathFragments,
+ readProjectConfiguration,
+ updateProjectConfiguration,
+} from '@nrwl/devkit';
+import type { Schema } from '../schema';
+
+import setupSsr from '../../setup-ssr/setup-ssr';
+import {
+ corsVersion,
+ moduleFederationNodeVersion,
+} from '../../../utils/versions';
+
+export async function addSsr(tree: Tree, options: Schema, appName: string) {
+ let project = readProjectConfiguration(tree, appName);
+
+ await setupSsr(tree, {
+ project: appName,
+ });
+
+ tree.rename(
+ joinPathFragments(project.sourceRoot, 'main.server.ts'),
+ joinPathFragments(project.sourceRoot, 'bootstrap.server.ts')
+ );
+ tree.write(
+ joinPathFragments(project.root, 'server.ts'),
+ "import('./src/main.server');"
+ );
+
+ generateFiles(tree, joinPathFragments(__dirname, '../files'), project.root, {
+ appName,
+ tmpl: '',
+ });
+
+ // update project.json
+ project = readProjectConfiguration(tree, appName);
+
+ project.targets.server.executor = '@nrwl/angular:webpack-server';
+ project.targets.server.options.customWebpackConfig = {
+ path: joinPathFragments(project.root, 'webpack.server.config.js'),
+ };
+
+ updateProjectConfiguration(tree, appName, project);
+
+ const installTask = addDependenciesToPackageJson(
+ tree,
+ {
+ cors: corsVersion,
+ '@module-federation/node': moduleFederationNodeVersion,
+ },
+ {}
+ );
+
+ return installTask;
+}
diff --git a/packages/angular/src/generators/remote/lib/find-next-available-port.ts b/packages/angular/src/generators/remote/lib/find-next-available-port.ts
new file mode 100644
index 0000000000..ebee855f00
--- /dev/null
+++ b/packages/angular/src/generators/remote/lib/find-next-available-port.ts
@@ -0,0 +1,18 @@
+import type { Tree } from '@nrwl/devkit';
+import { readProjectConfiguration } from '@nrwl/devkit';
+import { getMFProjects } from '../../../utils/get-mf-projects';
+
+export function findNextAvailablePort(tree: Tree) {
+ const mfProjects = getMFProjects(tree);
+
+ const ports = new Set([4200]);
+ for (const mfProject of mfProjects) {
+ const { targets } = readProjectConfiguration(tree, mfProject);
+ const port = targets?.serve?.options?.port ?? 4200;
+ ports.add(port);
+ }
+
+ const nextAvailablePort = Math.max(...ports) + 1;
+
+ return nextAvailablePort;
+}
diff --git a/packages/angular/src/generators/remote/lib/index.ts b/packages/angular/src/generators/remote/lib/index.ts
new file mode 100644
index 0000000000..ec1360ed68
--- /dev/null
+++ b/packages/angular/src/generators/remote/lib/index.ts
@@ -0,0 +1,2 @@
+export * from './find-next-available-port';
+export * from './add-ssr';
diff --git a/packages/angular/src/generators/remote/remote.spec.ts b/packages/angular/src/generators/remote/remote.spec.ts
index d075a34329..c638d8fc0b 100644
--- a/packages/angular/src/generators/remote/remote.spec.ts
+++ b/packages/angular/src/generators/remote/remote.spec.ts
@@ -195,4 +195,52 @@ describe('MF Remote App Generator', () => {
'proj-test-entry'
);
});
+
+ describe('--ssr', () => {
+ it('should generate the correct files', async () => {
+ // ARRANGE
+ const tree = createTreeWithEmptyWorkspace();
+
+ // ACT
+ await remote(tree, {
+ name: 'test',
+ ssr: true,
+ });
+
+ // ASSERT
+ const project = readProjectConfiguration(tree, 'test');
+ expect(
+ tree.exists(`apps/test/src/app/remote-entry/entry.module.ts`)
+ ).toBeTruthy();
+ expect(
+ tree.read(`apps/test/src/app/app.module.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/src/bootstrap.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/src/bootstrap.server.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/src/main.server.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(tree.read(`apps/test/server.ts`, 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/module-federation.config.js`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/webpack.server.config.js`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/src/app/remote-entry/entry.component.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/src/app/app.routes.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read(`apps/test/src/app/remote-entry/entry.routes.ts`, 'utf-8')
+ ).toMatchSnapshot();
+ expect(project.targets.server).toMatchSnapshot();
+ });
+ });
});
diff --git a/packages/angular/src/generators/remote/remote.ts b/packages/angular/src/generators/remote/remote.ts
index 7ded7fd742..ca583e11db 100644
--- a/packages/angular/src/generators/remote/remote.ts
+++ b/packages/angular/src/generators/remote/remote.ts
@@ -1,30 +1,11 @@
-import {
- formatFiles,
- getProjects,
- readProjectConfiguration,
- Tree,
-} from '@nrwl/devkit';
+import { formatFiles, getProjects, Tree } from '@nrwl/devkit';
import type { Schema } from './schema';
import applicationGenerator from '../application/application';
-import { getMFProjects } from '../../utils/get-mf-projects';
import { normalizeProjectName } from '../utils/project';
import { setupMf } from '../setup-mf/setup-mf';
import { E2eTestRunner } from '../../utils/test-runners';
-
-function findNextAvailablePort(tree: Tree) {
- const mfProjects = getMFProjects(tree);
-
- const ports = new Set([4200]);
- for (const mfProject of mfProjects) {
- const { targets } = readProjectConfiguration(tree, mfProject);
- const port = targets?.serve?.options?.port ?? 4200;
- ports.add(port);
- }
-
- const nextAvailablePort = Math.max(...ports) + 1;
-
- return nextAvailablePort;
-}
+import { addSsr, findNextAvailablePort } from './lib';
+import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
export async function remote(tree: Tree, options: Schema) {
const projects = getProjects(tree);
@@ -37,7 +18,7 @@ export async function remote(tree: Tree, options: Schema) {
const appName = normalizeProjectName(options.name, options.directory);
const port = options.port ?? findNextAvailablePort(tree);
- const installTask = await applicationGenerator(tree, {
+ const appInstallTask = await applicationGenerator(tree, {
...options,
routing: true,
skipDefaultProject: true,
@@ -60,11 +41,17 @@ export async function remote(tree: Tree, options: Schema) {
standalone: options.standalone,
});
+ let installTasks = [appInstallTask];
+ if (options.ssr) {
+ let ssrInstallTask = await addSsr(tree, options, appName);
+ installTasks.push(ssrInstallTask);
+ }
+
if (!options.skipFormat) {
await formatFiles(tree);
}
- return installTask;
+ return runTasksInSerial(...installTasks);
}
export default remote;
diff --git a/packages/angular/src/generators/remote/schema.d.ts b/packages/angular/src/generators/remote/schema.d.ts
index 51a2aae188..de11d136c1 100644
--- a/packages/angular/src/generators/remote/schema.d.ts
+++ b/packages/angular/src/generators/remote/schema.d.ts
@@ -25,4 +25,5 @@ export interface Schema {
viewEncapsulation?: 'Emulated' | 'Native' | 'None';
skipFormat?: boolean;
standalone?: boolean;
+ ssr?: boolean;
}
diff --git a/packages/angular/src/generators/remote/schema.json b/packages/angular/src/generators/remote/schema.json
index cd9f32ea75..059612aa2b 100644
--- a/packages/angular/src/generators/remote/schema.json
+++ b/packages/angular/src/generators/remote/schema.json
@@ -149,6 +149,11 @@
"description": "Whether to generate a remote application with standalone components.",
"type": "boolean",
"default": false
+ },
+ "ssr": {
+ "description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
+ "type": "boolean",
+ "default": false
}
},
"additionalProperties": false,
diff --git a/packages/angular/src/generators/setup-ssr/lib/update-app-module.ts b/packages/angular/src/generators/setup-ssr/lib/update-app-module.ts
index 1a9c55c25e..da91838683 100644
--- a/packages/angular/src/generators/setup-ssr/lib/update-app-module.ts
+++ b/packages/angular/src/generators/setup-ssr/lib/update-app-module.ts
@@ -32,7 +32,7 @@ export function updateAppModule(tree: Tree, schema: Schema) {
browserModuleNode.getEnd()
)}.withServerTransition({ appId: '${schema.appId}' })${fileContents.slice(
browserModuleNode.getEnd(),
- -1
+ fileContents.length
)}`;
tree.write(pathToAppModule, newFileContents);
diff --git a/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts b/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts
index ca54bc3fc2..c26522d08c 100644
--- a/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts
+++ b/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts
@@ -113,7 +113,8 @@ describe('setupSSR', () => {
providers: [],
bootstrap: [AppComponent]
})
- export class AppModule { }"
+ export class AppModule { }
+ "
`);
const packageJson = readJson(tree, 'package.json');
const dependencies = {
diff --git a/packages/angular/src/utils/versions.ts b/packages/angular/src/utils/versions.ts
index 5d7b7f57cd..cdfa452367 100644
--- a/packages/angular/src/utils/versions.ts
+++ b/packages/angular/src/utils/versions.ts
@@ -10,6 +10,8 @@ export const angularJsVersion = '1.7.9';
export const tsLibVersion = '^2.3.0';
export const ngUniversalVersion = '~15.0.0';
+export const corsVersion = '~2.8.5';
+export const moduleFederationNodeVersion = '~0.9.6';
export const angularEslintVersion = '~15.0.0';
export const tailwindVersion = '^3.0.2';