diff --git a/packages/angular/build/src/utils/index-file/index-html-generator.ts b/packages/angular/build/src/utils/index-file/index-html-generator.ts index bf40e2e7acac..3e7cdb167ced 100644 --- a/packages/angular/build/src/utils/index-file/index-html-generator.ts +++ b/packages/angular/build/src/utils/index-file/index-html-generator.ts @@ -14,6 +14,7 @@ import { addEventDispatchContract } from './add-event-dispatch-contract'; import { CrossOriginValue, Entrypoint, FileInfo, augmentIndexHtml } from './augment-index-html'; import { InlineCriticalCssProcessor } from './inline-critical-css'; import { InlineFontsProcessor } from './inline-fonts'; +import { addNgcmAttribute } from './ngcm-attribute'; import { addNonce } from './nonce'; type IndexHtmlGeneratorPlugin = ( @@ -82,6 +83,7 @@ export class IndexHtmlGenerator { // SSR plugins if (options.generateDedicatedSSRContent) { + this.csrPlugins.push(addNgcmAttributePlugin()); this.ssrPlugins.push(addEventDispatchContractPlugin(), addNoncePlugin()); } } @@ -203,3 +205,7 @@ function postTransformPlugin({ options }: IndexHtmlGenerator): IndexHtmlGenerato function addEventDispatchContractPlugin(): IndexHtmlGeneratorPlugin { return (html) => addEventDispatchContract(html); } + +function addNgcmAttributePlugin(): IndexHtmlGeneratorPlugin { + return (html) => addNgcmAttribute(html); +} diff --git a/packages/angular/build/src/utils/index-file/ngcm-attribute.ts b/packages/angular/build/src/utils/index-file/ngcm-attribute.ts new file mode 100644 index 000000000000..af74778915e6 --- /dev/null +++ b/packages/angular/build/src/utils/index-file/ngcm-attribute.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { htmlRewritingStream } from './html-rewriting-stream'; + +/** + * Defines a name of an attribute that is added to the `` tag + * in the `index.html` file in case a given route was configured + * with `RenderMode.Client`. 'cm' is an abbreviation for "Client Mode". + * + * @see https://github.com/angular/angular/pull/58004 + */ +const CLIENT_RENDER_MODE_FLAG = 'ngcm'; + +/** + * Transforms the provided HTML by adding the `ngcm` attribute to the `` tag. + * This is used in the client-side rendered (CSR) version of `index.html` to prevent hydration warnings. + * + * @param html The HTML markup to be transformed. + * @returns A promise that resolves to the transformed HTML string with the necessary modifications. + */ +export async function addNgcmAttribute(html: string): Promise { + const { rewriter, transformedContent } = await htmlRewritingStream(html); + + rewriter.on('startTag', (tag) => { + if ( + tag.tagName === 'body' && + !tag.attrs.some((attr) => attr.name === CLIENT_RENDER_MODE_FLAG) + ) { + tag.attrs.push({ name: CLIENT_RENDER_MODE_FLAG, value: '' }); + } + + rewriter.emitStartTag(tag); + }); + + return transformedContent(); +} diff --git a/packages/angular/build/src/utils/index-file/ngcm-attribute_spec.ts b/packages/angular/build/src/utils/index-file/ngcm-attribute_spec.ts new file mode 100644 index 000000000000..2eec900c862d --- /dev/null +++ b/packages/angular/build/src/utils/index-file/ngcm-attribute_spec.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { addNgcmAttribute } from './ngcm-attribute'; + +describe('addNgcmAttribute', () => { + it('should add the ngcm attribute to the tag', async () => { + const result = await addNgcmAttribute(` + + +

hello world!

+ + `); + + expect(result).toContain('

hello world!

'); + }); + + it('should not override an existing ngcm attribute', async () => { + const result = await addNgcmAttribute(` + + +

hello world!

+ + `); + + expect(result).toContain('

hello world!

'); + }); +});