feat(angular): add flag to include hydration when setting up ssr (#18675)

This commit is contained in:
Colum Ferry 2023-08-17 07:59:15 -07:00 committed by GitHub
parent 7d55f49e7d
commit b9ca7ce09f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 31 deletions

View File

@ -53,6 +53,10 @@
"type": "boolean",
"description": "Use Standalone Components to bootstrap SSR. _Note: This is only supported in Angular versions >= 14.1.0_."
},
"hydration": {
"type": "boolean",
"description": "Set up Hydration for the SSR application. _Note: This is only supported in Angular versions >= 16.0.0_."
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting the workspace after the generator completes.",

View File

@ -0,0 +1,73 @@
import {
joinPathFragments,
readProjectConfiguration,
type Tree,
} from '@nx/devkit';
import { type Schema } from '../schema';
import {
addProviderToAppConfig,
addProviderToModule,
} from '../../../utils/nx-devkit/ast-utils';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { SourceFile } from 'typescript';
import { insertImport } from '@nx/js';
let tsModule: typeof import('typescript');
export function addHydration(tree: Tree, options: Schema) {
const projectConfig = readProjectConfiguration(tree, options.project);
if (!tsModule) {
tsModule = ensureTypescript();
}
const addImport = (
source: SourceFile,
symbolName: string,
packageName: string,
filePath: string,
isDefault = false
): SourceFile => {
return insertImport(
tree,
source,
filePath,
symbolName,
packageName,
isDefault
);
};
const pathToClientConfigFile = options.standalone
? joinPathFragments(projectConfig.sourceRoot, 'app/app.config.ts')
: joinPathFragments(projectConfig.sourceRoot, 'app/app.module.ts');
const sourceText = tree.read(pathToClientConfigFile, 'utf-8');
let sourceFile = tsModule.createSourceFile(
pathToClientConfigFile,
sourceText,
tsModule.ScriptTarget.Latest,
true
);
sourceFile = addImport(
sourceFile,
'provideClientHydration',
'@angular/platform-browser',
pathToClientConfigFile
);
if (options.standalone) {
addProviderToAppConfig(
tree,
pathToClientConfigFile,
'provideClientHydration()'
);
} else {
addProviderToModule(
tree,
sourceFile,
pathToClientConfigFile,
'provideClientHydration()'
);
}
}

View File

@ -3,3 +3,4 @@ export * from './normalize-options';
export * from './update-app-module';
export * from './update-project-config';
export * from './validate-options';
export * from './add-hydration';

View File

@ -15,5 +15,6 @@ export function normalizeOptions(tree: Tree, options: Schema) {
rootModuleClassName: options.rootModuleClassName ?? 'AppServerModule',
skipFormat: options.skipFormat ?? false,
standalone: options.standalone ?? isStandaloneApp,
hydration: options.hydration ?? false,
};
}

View File

@ -1,11 +1,28 @@
import type { Tree } from '@nx/devkit';
import { stripIndents } from '@nx/devkit';
import {
validateProject,
validateStandaloneOption,
} from '../../utils/validations';
import type { Schema } from '../schema';
import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
import { lt } from 'semver';
export function validateOptions(tree: Tree, options: Schema): void {
validateProject(tree, options.project);
validateStandaloneOption(tree, options.standalone);
validateHydrationOption(tree, options.hydration);
}
function validateHydrationOption(tree: Tree, hydration: boolean): void {
if (!hydration) {
return;
}
const installedAngularVersion = getInstalledAngularVersionInfo(tree).version;
if (lt(installedAngularVersion, '16.0.0')) {
throw new Error(stripIndents`The "hydration" option is only supported in Angular >= 16.0.0. You are currently using "${installedAngularVersion}".
You can resolve this error by removing the "hydration" option or by migrating to Angular 16.0.0.`);
}
}

View File

@ -7,5 +7,6 @@ export interface Schema {
rootModuleFileName?: string;
rootModuleClassName?: string;
standalone?: boolean;
hydration?: boolean;
skipFormat?: boolean;
}

View File

@ -53,6 +53,10 @@
"type": "boolean",
"description": "Use Standalone Components to bootstrap SSR. _Note: This is only supported in Angular versions >= 14.1.0_."
},
"hydration": {
"type": "boolean",
"description": "Set up Hydration for the SSR application. _Note: This is only supported in Angular versions >= 16.0.0_."
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting the workspace after the generator completes.",

View File

@ -237,6 +237,78 @@ describe('setupSSR', () => {
`);
});
it('should add hydration correctly for NgModule apps', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestApplication(tree, {
name: 'app1',
});
// ACT
await setupSsr(tree, { project: 'app1', hydration: true });
// ASSERT
expect(tree.read('apps/app1/src/app/app.module.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import {
BrowserModule,
provideClientHydration,
} from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [BrowserModule],
providers: [provideClientHydration()],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`);
});
it('should add hydration correctly to standalone', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestApplication(tree, {
name: 'app1',
standalone: true,
});
// ACT
await setupSsr(tree, { project: 'app1', hydration: true });
// ASSERT
expect(tree.read('apps/app1/src/app/app.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration()],
};
"
`);
expect(tree.read('apps/app1/src/app/app.config.server.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"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);
"
`);
});
describe('compat', () => {
it('should install the correct versions when using older versions of Angular', async () => {
// ARRANGE
@ -319,20 +391,20 @@ describe('setupSSR', () => {
// ASSERT
expect(tree.read('apps/app1/src/app/app.module.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
"import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [BrowserModule.withServerTransition({ appId: 'serverApp' })],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`);
@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [BrowserModule.withServerTransition({ appId: 'serverApp' })],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`);
});
it('should wrap bootstrap call for Angular versions lower than 15.2', async () => {
@ -352,22 +424,22 @@ describe('setupSSR', () => {
// ASSERT
expect(tree.read('apps/app1/src/main.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
"import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
function bootstrap() {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
}
function bootstrap() {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
}
if (document.readyState !== 'loading') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
"
`);
if (document.readyState !== 'loading') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
"
`);
});
});
});

View File

@ -4,8 +4,12 @@ import {
formatFiles,
installPackagesTask,
} from '@nx/devkit';
import { versions } from '../utils/version-utils';
import {
getInstalledPackageVersionInfo,
versions,
} from '../utils/version-utils';
import {
addHydration,
generateSSRFiles,
normalizeOptions,
updateAppModule,
@ -25,16 +29,26 @@ export async function setupSsr(tree: Tree, schema: Schema) {
updateAppModule(tree, options);
}
if (options.hydration) {
addHydration(tree, options);
}
const pkgVersions = versions(tree);
addDependenciesToPackageJson(
tree,
{
'@nguniversal/express-engine': pkgVersions.ngUniversalVersion,
'@angular/platform-server': pkgVersions.angularVersion,
'@nguniversal/express-engine':
getInstalledPackageVersionInfo(tree, '@nguniversal/express-engine')
?.version ?? pkgVersions.ngUniversalVersion,
'@angular/platform-server':
getInstalledPackageVersionInfo(tree, '@angular/platform-server')
?.version ?? pkgVersions.angularVersion,
},
{
'@nguniversal/builders': pkgVersions.ngUniversalVersion,
'@nguniversal/builders':
getInstalledPackageVersionInfo(tree, '@nguniversal/builders')
?.version ?? pkgVersions.ngUniversalVersion,
}
);