diff --git a/aio/content/errors/NG5000.md b/aio/content/errors/NG5000.md
new file mode 100644
index 000000000000..f5d2f078ac3a
--- /dev/null
+++ b/aio/content/errors/NG5000.md
@@ -0,0 +1,14 @@
+@name Hydration with unsupported Zone.js instance.
+@category runtime
+@shortDescription Hydration was enabled with unsupported Zone.js instance.
+
+@description
+This warning means that the hydration was enabled for an application that was configured to use an unsupported version of Zone.js: either a custom or a "noop" one (see more info [here](api/core/BootstrapOptions#ngZone)).
+
+Hydration relies on a signal from Zone.js when it becomes stable inside an application, so that Angular can start the serialization process on the server or post-hydration cleanup on the client (to remove DOM nodes that remained unclaimed).
+
+Providing a custom or a "noop" Zone.js implementation may lead to a different timing of the "stable" event, thus triggering the serialization or the cleanup too early or too late. This is not yet a fully supported configuration.
+
+If you use a custom Zone.js implementation, make sure that the "onStable" event is emitted at the right time and does not result in incorrect application behavior with hydration.
+
+More information about hydration can be found in the [hydration guide](guide/hydration).
diff --git a/aio/content/guide/hydration.md b/aio/content/guide/hydration.md
index e3f76e259543..0940d95ecc6b 100644
--- a/aio/content/guide/hydration.md
+++ b/aio/content/guide/hydration.md
@@ -65,7 +65,7 @@ After you've followed these steps and have started up your server, load your app
-You can confirm hydration is enabled by opening Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated. Note: Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
+While running an application in dev mode, you can confirm hydration is enabled by opening the Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated. Note: Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
@@ -110,6 +110,13 @@ If you choose to set this setting in your tsconfig, we recommend to set it only
+### Custom or Noop Zone.js are not yet supported
+
+Hydration relies on a signal from Zone.js when it becomes stable inside an application, so that Angular can start the serialization process on the server or post-hydration cleanup on the client to remove DOM nodes that remained unclaimed.
+
+Providing a custom or a "noop" Zone.js implementation may lead to a different timing of the "stable" event, thus triggering the serialization or the cleanup too early or too late. This is not yet a fully supported configuration and you may need to adjust the timing of the `onStable` event in the custom Zone.js implementation.
+
+
## Errors
diff --git a/goldens/public-api/platform-browser/errors.md b/goldens/public-api/platform-browser/errors.md
new file mode 100644
index 000000000000..c987c255cb58
--- /dev/null
+++ b/goldens/public-api/platform-browser/errors.md
@@ -0,0 +1,15 @@
+## API Report File for "angular-srcs"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+
+// @public
+export const enum RuntimeErrorCode {
+ // (undocumented)
+ UNSUPPORTED_ZONEJS_INSTANCE = -5000
+}
+
+// (No @packageDocumentation comment for this package)
+
+```
diff --git a/packages/animations/browser/src/errors.ts b/packages/animations/browser/src/errors.ts
index 827ddfe61c82..a457cf6f0698 100644
--- a/packages/animations/browser/src/errors.ts
+++ b/packages/animations/browser/src/errors.ts
@@ -6,6 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
+/**
+ * The list of error codes used in runtime code of the `animations` package.
+ * Reserved error code range: 3000-3999.
+ */
export const enum RuntimeErrorCode {
// Invalid values
INVALID_TIMING_VALUE = 3000,
diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts
index 9998a87c6f07..2512c99dfa5a 100644
--- a/packages/core/src/errors.ts
+++ b/packages/core/src/errors.ts
@@ -17,6 +17,14 @@ import {ERROR_DETAILS_PAGE_BASE_URL} from './error_details_base_url';
* error codes which have guides, which might leak into runtime code.
*
* Full list of available error guides can be found at https://angular.io/errors.
+ *
+ * Error code ranges per package:
+ * - core (this package): 100-999
+ * - forms: 1000-1999
+ * - common: 2000-2999
+ * - animations: 3000-3999
+ * - router: 4000-4999
+ * - platform-browser: 5000-5500
*/
export const enum RuntimeErrorCode {
// Change Detection Errors
diff --git a/packages/platform-browser/BUILD.bazel b/packages/platform-browser/BUILD.bazel
index d123e5664471..d9a5e0306f45 100644
--- a/packages/platform-browser/BUILD.bazel
+++ b/packages/platform-browser/BUILD.bazel
@@ -1,4 +1,4 @@
-load("//tools:defaults.bzl", "api_golden_test_npm_package", "ng_module", "ng_package", "tsec_test")
+load("//tools:defaults.bzl", "api_golden_test", "api_golden_test_npm_package", "ng_module", "ng_package", "tsec_test")
package(default_visibility = ["//visibility:public"])
@@ -62,6 +62,16 @@ api_golden_test_npm_package(
npm_package = "angular/packages/platform-browser/npm_package",
)
+api_golden_test(
+ name = "platform-browser_errors",
+ data = [
+ "//goldens:public-api",
+ "//packages/platform-browser",
+ ],
+ entry_point = "angular/packages/platform-browser/src/errors.d.ts",
+ golden = "angular/goldens/public-api/platform-browser/errors.md",
+)
+
filegroup(
name = "files_for_docgen",
srcs = glob([
diff --git a/packages/platform-browser/src/errors.ts b/packages/platform-browser/src/errors.ts
new file mode 100644
index 000000000000..33de84188767
--- /dev/null
+++ b/packages/platform-browser/src/errors.ts
@@ -0,0 +1,16 @@
+/**
+ * @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
+ */
+
+/**
+ * The list of error codes used in runtime code of the `platform-browser` package.
+ * Reserved error code range: 5000-5500.
+ */
+export const enum RuntimeErrorCode {
+ // Hydration Errors
+ UNSUPPORTED_ZONEJS_INSTANCE = -5000,
+}
diff --git a/packages/platform-browser/src/hydration.ts b/packages/platform-browser/src/hydration.ts
index 79f335bdb1b3..b7054165ebc8 100644
--- a/packages/platform-browser/src/hydration.ts
+++ b/packages/platform-browser/src/hydration.ts
@@ -7,7 +7,9 @@
*/
import {ɵwithHttpTransferCache as withHttpTransferCache} from '@angular/common/http';
-import {EnvironmentProviders, makeEnvironmentProviders, Provider, ɵwithDomHydration as withDomHydration} from '@angular/core';
+import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, makeEnvironmentProviders, NgZone, Provider, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵwithDomHydration as withDomHydration} from '@angular/core';
+
+import {RuntimeErrorCode} from './errors';
/**
* The list of features as an enum to uniquely type each `HydrationFeature`.
@@ -91,6 +93,33 @@ export function withNoHttpTransferCache():
return hydrationFeature(HydrationFeatureKind.NoHttpTransferCache);
}
+/**
+ * Returns an `ENVIRONMENT_INITIALIZER` token setup with a function
+ * that verifies whether compatible ZoneJS was used in an application
+ * and logs a warning in a console if it's not the case.
+ */
+function provideZoneJsCompatibilityDetector(): Provider[] {
+ return [{
+ provide: ENVIRONMENT_INITIALIZER,
+ useValue: () => {
+ const ngZone = inject(NgZone);
+ // Checking `ngZone instanceof NgZone` would be insufficient here,
+ // because custom implementations might use NgZone as a base class.
+ if (ngZone.constructor !== NgZone) {
+ const console = inject(Console);
+ const message = formatRuntimeError(
+ RuntimeErrorCode.UNSUPPORTED_ZONEJS_INSTANCE,
+ 'Angular detected that hydration was enabled for an application ' +
+ 'that uses a custom or a noop Zone.js implementation. ' +
+ 'This is not yet a fully supported configuration.');
+ // tslint:disable-next-line:no-console
+ console.warn(message);
+ }
+ },
+ multi: true,
+ }];
+}
+
/**
* Sets up providers necessary to enable hydration functionality for the application.
* By default, the function enables the recommended set of features for the optimal
@@ -142,6 +171,7 @@ export function provideClientHydration(...features: HydrationFeature {
resetTViewsFor(SimpleComponent, NestedComponent);
- const appRef = await hydrate(html, SimpleComponent);
+ const appRef = await hydrate(html, SimpleComponent, [withDebugConsole()]);
const compRef = getComponentRef(appRef);
appRef.tick();
+ // Make sure there are no extra logs in case
+ // default NgZone is setup for an application.
+ verifyHasNoLog(
+ appRef,
+ 'NG05000: Angular detected that hydration was enabled for an application ' +
+ 'that uses a custom or a noop Zone.js implementation.');
+
const clientRootNode = compRef.location.nativeElement;
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
@@ -3990,6 +3998,78 @@ describe('platform-server integration', () => {
});
});
+ describe('unsupported Zone.js config', () => {
+ it('should log a warning when a noop zone is used', async () => {
+ @Component({
+ standalone: true,
+ selector: 'app',
+ template: `Hi!`,
+ })
+ class SimpleComponent {
+ }
+
+ const html = await ssr(SimpleComponent);
+ const ssrContents = getAppContents(html);
+
+ expect(ssrContents).toContain('(appRef);
+ appRef.tick();
+
+ verifyHasLog(
+ appRef,
+ 'NG05000: Angular detected that hydration was enabled for an application ' +
+ 'that uses a custom or a noop Zone.js implementation.');
+
+ const clientRootNode = compRef.location.nativeElement;
+
+ verifyAllNodesClaimedForHydration(clientRootNode);
+ verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
+ });
+
+ it('should log a warning when a custom zone is used', async () => {
+ @Component({
+ standalone: true,
+ selector: 'app',
+ template: `Hi!`,
+ })
+ class SimpleComponent {
+ }
+
+ const html = await ssr(SimpleComponent);
+ const ssrContents = getAppContents(html);
+
+ expect(ssrContents).toContain('(appRef);
+ appRef.tick();
+
+ verifyHasLog(
+ appRef,
+ 'NG05000: Angular detected that hydration was enabled for an application ' +
+ 'that uses a custom or a noop Zone.js implementation.');
+
+ const clientRootNode = compRef.location.nativeElement;
+
+ verifyAllNodesClaimedForHydration(clientRootNode);
+ verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
+ });
+ });
+
describe('error handling', () => {
it('should handle text node mismatch', async () => {
@Component({