Skip to content

Commit

Permalink
feat(angular): add flag to include hydration when setting up ssr (#18675
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Coly010 authored Aug 17, 2023
1 parent 7d55f49 commit b9ca7ce
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 34 deletions.
4 changes: 4 additions & 0 deletions docs/generated/packages/angular/generators/setup-ssr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
73 changes: 73 additions & 0 deletions packages/angular/src/generators/setup-ssr/lib/add-hydration.ts
Original file line number Diff line number Diff line change
@@ -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()'
);
}
}
1 change: 1 addition & 0 deletions packages/angular/src/generators/setup-ssr/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
17 changes: 17 additions & 0 deletions packages/angular/src/generators/setup-ssr/lib/validate-options.ts
Original file line number Diff line number Diff line change
@@ -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.`);
}
}
1 change: 1 addition & 0 deletions packages/angular/src/generators/setup-ssr/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export interface Schema {
rootModuleFileName?: string;
rootModuleClassName?: string;
standalone?: boolean;
hydration?: boolean;
skipFormat?: boolean;
}
4 changes: 4 additions & 0 deletions packages/angular/src/generators/setup-ssr/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
132 changes: 102 additions & 30 deletions packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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';
@NgModule({
declarations: [AppComponent, NxWelcomeComponent],
imports: [BrowserModule.withServerTransition({ appId: 'serverApp' })],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
"
`);
"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 {}
"
`);
});

it('should wrap bootstrap call for Angular versions lower than 15.2', async () => {
Expand All @@ -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';
function bootstrap() {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
}
if (document.readyState !== 'loading') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
"
`);
"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 !== 'loading') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
"
`);
});
});
});
22 changes: 18 additions & 4 deletions packages/angular/src/generators/setup-ssr/setup-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
);

Expand Down

1 comment on commit b9ca7ce

@vercel
Copy link

@vercel vercel bot commented on b9ca7ce Aug 17, 2023

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.