feat(angular): add flag to include hydration when setting up ssr (#18675)
This commit is contained in:
parent
7d55f49e7d
commit
b9ca7ce09f
@ -53,6 +53,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Use Standalone Components to bootstrap SSR. _Note: This is only supported in Angular versions >= 14.1.0_."
|
"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": {
|
"skipFormat": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Skip formatting the workspace after the generator completes.",
|
"description": "Skip formatting the workspace after the generator completes.",
|
||||||
|
|||||||
@ -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()'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,3 +3,4 @@ export * from './normalize-options';
|
|||||||
export * from './update-app-module';
|
export * from './update-app-module';
|
||||||
export * from './update-project-config';
|
export * from './update-project-config';
|
||||||
export * from './validate-options';
|
export * from './validate-options';
|
||||||
|
export * from './add-hydration';
|
||||||
|
|||||||
@ -15,5 +15,6 @@ export function normalizeOptions(tree: Tree, options: Schema) {
|
|||||||
rootModuleClassName: options.rootModuleClassName ?? 'AppServerModule',
|
rootModuleClassName: options.rootModuleClassName ?? 'AppServerModule',
|
||||||
skipFormat: options.skipFormat ?? false,
|
skipFormat: options.skipFormat ?? false,
|
||||||
standalone: options.standalone ?? isStandaloneApp,
|
standalone: options.standalone ?? isStandaloneApp,
|
||||||
|
hydration: options.hydration ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,28 @@
|
|||||||
import type { Tree } from '@nx/devkit';
|
import type { Tree } from '@nx/devkit';
|
||||||
|
import { stripIndents } from '@nx/devkit';
|
||||||
import {
|
import {
|
||||||
validateProject,
|
validateProject,
|
||||||
validateStandaloneOption,
|
validateStandaloneOption,
|
||||||
} from '../../utils/validations';
|
} from '../../utils/validations';
|
||||||
import type { Schema } from '../schema';
|
import type { Schema } from '../schema';
|
||||||
|
import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
|
||||||
|
import { lt } from 'semver';
|
||||||
|
|
||||||
export function validateOptions(tree: Tree, options: Schema): void {
|
export function validateOptions(tree: Tree, options: Schema): void {
|
||||||
validateProject(tree, options.project);
|
validateProject(tree, options.project);
|
||||||
validateStandaloneOption(tree, options.standalone);
|
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.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,5 +7,6 @@ export interface Schema {
|
|||||||
rootModuleFileName?: string;
|
rootModuleFileName?: string;
|
||||||
rootModuleClassName?: string;
|
rootModuleClassName?: string;
|
||||||
standalone?: boolean;
|
standalone?: boolean;
|
||||||
|
hydration?: boolean;
|
||||||
skipFormat?: boolean;
|
skipFormat?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Use Standalone Components to bootstrap SSR. _Note: This is only supported in Angular versions >= 14.1.0_."
|
"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": {
|
"skipFormat": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Skip formatting the workspace after the generator completes.",
|
"description": "Skip formatting the workspace after the generator completes.",
|
||||||
|
|||||||
@ -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', () => {
|
describe('compat', () => {
|
||||||
it('should install the correct versions when using older versions of Angular', async () => {
|
it('should install the correct versions when using older versions of Angular', async () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
|
|||||||
@ -4,8 +4,12 @@ import {
|
|||||||
formatFiles,
|
formatFiles,
|
||||||
installPackagesTask,
|
installPackagesTask,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { versions } from '../utils/version-utils';
|
|
||||||
import {
|
import {
|
||||||
|
getInstalledPackageVersionInfo,
|
||||||
|
versions,
|
||||||
|
} from '../utils/version-utils';
|
||||||
|
import {
|
||||||
|
addHydration,
|
||||||
generateSSRFiles,
|
generateSSRFiles,
|
||||||
normalizeOptions,
|
normalizeOptions,
|
||||||
updateAppModule,
|
updateAppModule,
|
||||||
@ -25,16 +29,26 @@ export async function setupSsr(tree: Tree, schema: Schema) {
|
|||||||
updateAppModule(tree, options);
|
updateAppModule(tree, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.hydration) {
|
||||||
|
addHydration(tree, options);
|
||||||
|
}
|
||||||
|
|
||||||
const pkgVersions = versions(tree);
|
const pkgVersions = versions(tree);
|
||||||
|
|
||||||
addDependenciesToPackageJson(
|
addDependenciesToPackageJson(
|
||||||
tree,
|
tree,
|
||||||
{
|
{
|
||||||
'@nguniversal/express-engine': pkgVersions.ngUniversalVersion,
|
'@nguniversal/express-engine':
|
||||||
'@angular/platform-server': pkgVersions.angularVersion,
|
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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user