diff --git a/packages/angular_devkit/build_angular/src/builders/server/index.ts b/packages/angular_devkit/build_angular/src/builders/server/index.ts index 9370037bfa24..e7a057c6793e 100644 --- a/packages/angular_devkit/build_angular/src/builders/server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/server/index.ts @@ -8,7 +8,8 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { runWebpack } from '@angular-devkit/build-webpack'; -import * as path from 'path'; +import { readFile } from 'node:fs/promises'; +import * as path from 'node:path'; import { Observable, concatMap, from } from 'rxjs'; import webpack, { Configuration } from 'webpack'; import { ExecutionTransformer } from '../../transforms'; @@ -191,6 +192,8 @@ async function initialize( // Purge old build disk cache. await purgeStaleBuildCache(context); + await checkTsConfigForPreserveWhitespacesSetting(context, options.tsConfig); + const browserslist = (await import('browserslist')).default; const originalOutputPath = options.outputPath; // Assets are processed directly by the builder except when watching @@ -223,6 +226,32 @@ async function initialize( return { config: transformedConfig, i18n, projectRoot, projectSourceRoot }; } +async function checkTsConfigForPreserveWhitespacesSetting( + context: BuilderContext, + tsConfigPath: string, +): Promise { + // We don't use the `readTsConfig` method on purpose here. + // To only catch cases were `preserveWhitespaces` is set directly in the `tsconfig.server.json`, + // which in the majority of cases will cause a mistmatch between client and server builds. + // Technically we should check if `tsconfig.server.json` and `tsconfig.app.json` values match. + + // But: + // 1. It is not guaranteed that `tsconfig.app.json` is used to build the client side of this app. + // 2. There is no easy way to access the build build config from the server builder. + // 4. This will no longer be an issue with a single compilation model were the same tsconfig is used for both browser and server builds. + const content = await readFile(path.join(context.workspaceRoot, tsConfigPath), 'utf-8'); + const { parse } = await import('jsonc-parser'); + const tsConfig = parse(content, [], { allowTrailingComma: true }); + if (tsConfig.angularCompilerOptions?.preserveWhitespaces !== undefined) { + context.logger.warn( + `"preserveWhitespaces" was set in "${tsConfigPath}". ` + + 'Make sure that this setting is set consistently in both "tsconfig.server.json" for your server side ' + + 'and "tsconfig.app.json" for your client side. A mismatched value will cause hydration to break.\n' + + 'For more information see: https://angular.io/guide/hydration#preserve-whitespaces', + ); + } +} + /** * Add `@angular/platform-server` exports. * This is needed so that DI tokens can be referenced and set at runtime outside of the bundle. diff --git a/packages/angular_devkit/build_angular/src/builders/server/tests/behavior/preserve_whitespaces_check_spec.ts b/packages/angular_devkit/build_angular/src/builders/server/tests/behavior/preserve_whitespaces_check_spec.ts new file mode 100644 index 000000000000..3167310edb7d --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/server/tests/behavior/preserve_whitespaces_check_spec.ts @@ -0,0 +1,47 @@ +/** + * @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.io/license + */ + +import { logging } from '@angular-devkit/core'; +import { execute } from '../../index'; +import { BASE_OPTIONS, SERVER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => { + describe('Behavior: "preserveWhitespaces warning"', () => { + it('should not show warning when "preserveWhitespaces" is not set.', async () => { + harness.useTarget('server', { + ...BASE_OPTIONS, + }); + + const { logs } = await harness.executeOnce(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('"preserveWhitespaces" was set in'), + }), + ); + }); + it('should show warning when "preserveWhitespaces" is set.', async () => { + harness.useTarget('server', { + ...BASE_OPTIONS, + }); + + await harness.modifyFile('src/tsconfig.server.json', (content) => { + const tsconfig = JSON.parse(content); + (tsconfig.angularCompilerOptions ??= {}).preserveWhitespaces = false; + + return JSON.stringify(tsconfig); + }); + + const { logs } = await harness.executeOnce(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('"preserveWhitespaces" was set in'), + }), + ); + }); + }); +});