Skip to content

Commit

Permalink
feat(angular): add --ssr flag to remote generator (#13370)
Browse files Browse the repository at this point in the history
  • Loading branch information
Coly010 committed Nov 24, 2022
1 parent 13602c3 commit 2471768
Show file tree
Hide file tree
Showing 15 changed files with 438 additions and 26 deletions.
5 changes: 5 additions & 0 deletions docs/generated/packages/angular.json
Expand Up @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions e2e/angular-core/src/projects.test.ts
Expand Up @@ -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');
Expand Down
@@ -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: \`<proj-nx-welcome></proj-nx-welcome>\`
})
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');
Expand Down
@@ -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';
@@ -0,0 +1,3 @@
const { withModuleFederationForSSR } = require('@nrwl/angular/module-federation');
const config = require('./module-federation.config');
module.exports = withModuleFederationForSSR(config)
58 changes: 58 additions & 0 deletions 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;
}
@@ -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<number>([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;
}
2 changes: 2 additions & 0 deletions packages/angular/src/generators/remote/lib/index.ts
@@ -0,0 +1,2 @@
export * from './find-next-available-port';
export * from './add-ssr';

1 comment on commit 2471768

@vercel
Copy link

@vercel vercel bot commented on 2471768 Nov 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app
nx-five.vercel.app
nx.dev

Please sign in to comment.