feat(angular): add ssr flag to host generator (#13398)
This commit is contained in:
parent
cb5350338c
commit
d908521f15
@ -1452,6 +1452,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Whether to generate a host application that uses standalone components.",
|
||||
"default": false
|
||||
},
|
||||
"ssr": {
|
||||
"description": "Whether to configure SSR for the host application",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,5 +1,183 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Host App Generator --ssr should generate the correct files 1`] = `
|
||||
"import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { appRoutes } from './app.routes';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
NxWelcomeComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
RouterModule.forRoot(appRoutes, {initialNavigation: 'enabledBlocking'})
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Host App Generator --ssr should generate the correct files 2`] = `
|
||||
"import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
function bootstrap() {
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
bootstrap();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Host 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[`Host 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');
|
||||
|
||||
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 5`] = `"import('./src/main.server');"`;
|
||||
|
||||
exports[`Host App Generator --ssr should generate the correct files 6`] = `
|
||||
"module.exports = {
|
||||
name: 'test',
|
||||
remotes: []
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Host 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[`Host App Generator --ssr should generate the correct files 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 9`] = `
|
||||
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[`Host App Generator should generate a host app with a remote 1`] = `
|
||||
"const { withModuleFederation } = require('@nrwl/angular/module-federation');
|
||||
const config = require('./module-federation.config');
|
||||
|
||||
@ -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');
|
||||
|
||||
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';
|
||||
@ -0,0 +1,3 @@
|
||||
const { withModuleFederationForSSR } = require('@nrwl/angular/module-federation');
|
||||
const config = require('./module-federation.config');
|
||||
module.exports = withModuleFederationForSSR(config)
|
||||
@ -2,7 +2,10 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
|
||||
import host from './host';
|
||||
import remote from '../remote/remote';
|
||||
import { E2eTestRunner } from '../../utils/test-runners';
|
||||
import { getProjects } from 'nx/src/generators/utils/project-configuration';
|
||||
import {
|
||||
getProjects,
|
||||
readProjectConfiguration,
|
||||
} from 'nx/src/generators/utils/project-configuration';
|
||||
|
||||
describe('Host App Generator', () => {
|
||||
it('should generate a host app with no remotes', async () => {
|
||||
@ -185,4 +188,43 @@ describe('Host App Generator', () => {
|
||||
expect(projects.has('dashboard-e2e')).toBeFalsy();
|
||||
expect(projects.has('remote1-e2e')).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('--ssr', () => {
|
||||
it('should generate the correct files', async () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||
|
||||
// ACT
|
||||
await host(tree, {
|
||||
name: 'test',
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const project = readProjectConfiguration(tree, 'test');
|
||||
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/app.routes.ts`, 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(project.targets.server).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,6 +5,8 @@ import remoteGenerator from '../remote/remote';
|
||||
import { normalizeProjectName } from '../utils/project';
|
||||
import { setupMf } from '../setup-mf/setup-mf';
|
||||
import { E2eTestRunner } from '../../utils/test-runners';
|
||||
import { addSsr } from './lib';
|
||||
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
||||
|
||||
export async function host(tree: Tree, options: Schema) {
|
||||
const projects = getProjects(tree);
|
||||
@ -24,7 +26,7 @@ export async function host(tree: Tree, options: Schema) {
|
||||
|
||||
const appName = normalizeProjectName(options.name, options.directory);
|
||||
|
||||
const installTask = await applicationGenerator(tree, {
|
||||
const appInstallTask = await applicationGenerator(tree, {
|
||||
...options,
|
||||
routing: true,
|
||||
port: 4200,
|
||||
@ -46,6 +48,12 @@ export async function host(tree: Tree, options: Schema) {
|
||||
e2eProjectName: skipE2E ? undefined : `${appName}-e2e`,
|
||||
});
|
||||
|
||||
let installTasks = [appInstallTask];
|
||||
if (options.ssr) {
|
||||
let ssrInstallTask = await addSsr(tree, options, appName);
|
||||
installTasks.push(ssrInstallTask);
|
||||
}
|
||||
|
||||
for (const remote of remotesToGenerate) {
|
||||
await remoteGenerator(tree, {
|
||||
...options,
|
||||
@ -60,7 +68,7 @@ export async function host(tree: Tree, options: Schema) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return installTask;
|
||||
return runTasksInSerial(...installTasks);
|
||||
}
|
||||
|
||||
export default host;
|
||||
|
||||
69
packages/angular/src/generators/host/lib/add-ssr.ts
Normal file
69
packages/angular/src/generators/host/lib/add-ssr.ts
Normal file
@ -0,0 +1,69 @@
|
||||
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,
|
||||
expressVersion,
|
||||
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');"
|
||||
);
|
||||
|
||||
tree.rename(
|
||||
joinPathFragments(project.sourceRoot, 'main.ts'),
|
||||
joinPathFragments(project.sourceRoot, 'bootstrap.ts')
|
||||
);
|
||||
tree.write(
|
||||
joinPathFragments(project.sourceRoot, 'main.ts'),
|
||||
`import("./bootstrap")`
|
||||
);
|
||||
|
||||
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,
|
||||
express: expressVersion,
|
||||
'@module-federation/node': moduleFederationNodeVersion,
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return installTask;
|
||||
}
|
||||
1
packages/angular/src/generators/host/lib/index.ts
Normal file
1
packages/angular/src/generators/host/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './add-ssr';
|
||||
@ -26,4 +26,5 @@ export interface Schema {
|
||||
viewEncapsulation?: 'Emulated' | 'Native' | 'None';
|
||||
skipFormat?: boolean;
|
||||
standalone?: boolean;
|
||||
ssr?: boolean;
|
||||
}
|
||||
|
||||
@ -154,6 +154,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Whether to generate a host application that uses standalone components.",
|
||||
"default": false
|
||||
},
|
||||
"ssr": {
|
||||
"description": "Whether to configure SSR for the host application",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user