Skip to content

Commit 3bcbfec

Browse files
AndrewKushnirdylhunn
authored andcommitted
refactor(platform-browser): log a warning when a custom or a noop ZoneJS is used with hydration (angular#49944)
Hydration relies on a signal from ZoneJS when it becomes stable inside an application, so that Angular can start 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" ZoneJS 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. This commit adds a warning (non-blocking) for those cases. PR Close angular#49944
1 parent eb5bc95 commit 3bcbfec

File tree

9 files changed

+188
-4
lines changed

9 files changed

+188
-4
lines changed

aio/content/errors/NG5000.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@name Hydration with unsupported Zone.js instance.
2+
@category runtime
3+
@shortDescription Hydration was enabled with unsupported Zone.js instance.
4+
5+
@description
6+
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)).
7+
8+
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).
9+
10+
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.
11+
12+
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.
13+
14+
More information about hydration can be found in the [hydration guide](guide/hydration).

aio/content/guide/hydration.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ After you've followed these steps and have started up your server, load your app
6565

6666
</div>
6767

68-
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.
68+
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.
6969

7070
<a id="constraints"></a>
7171

@@ -110,6 +110,13 @@ If you choose to set this setting in your tsconfig, we recommend to set it only
110110

111111
</div>
112112

113+
### Custom or Noop Zone.js are not yet supported
114+
115+
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.
116+
117+
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.
118+
119+
113120
<a id="errors"></a>
114121

115122
## Errors
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## API Report File for "angular-srcs"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
// @public
8+
export const enum RuntimeErrorCode {
9+
// (undocumented)
10+
UNSUPPORTED_ZONEJS_INSTANCE = -5000
11+
}
12+
13+
// (No @packageDocumentation comment for this package)
14+
15+
```

packages/animations/browser/src/errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
/**
10+
* The list of error codes used in runtime code of the `animations` package.
11+
* Reserved error code range: 3000-3999.
12+
*/
913
export const enum RuntimeErrorCode {
1014
// Invalid values
1115
INVALID_TIMING_VALUE = 3000,

packages/core/src/errors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ import {ERROR_DETAILS_PAGE_BASE_URL} from './error_details_base_url';
1717
* error codes which have guides, which might leak into runtime code.
1818
*
1919
* Full list of available error guides can be found at https://angular.io/errors.
20+
*
21+
* Error code ranges per package:
22+
* - core (this package): 100-999
23+
* - forms: 1000-1999
24+
* - common: 2000-2999
25+
* - animations: 3000-3999
26+
* - router: 4000-4999
27+
* - platform-browser: 5000-5500
2028
*/
2129
export const enum RuntimeErrorCode {
2230
// Change Detection Errors

packages/platform-browser/BUILD.bazel

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tools:defaults.bzl", "api_golden_test_npm_package", "ng_module", "ng_package", "tsec_test")
1+
load("//tools:defaults.bzl", "api_golden_test", "api_golden_test_npm_package", "ng_module", "ng_package", "tsec_test")
22

33
package(default_visibility = ["//visibility:public"])
44

@@ -62,6 +62,16 @@ api_golden_test_npm_package(
6262
npm_package = "angular/packages/platform-browser/npm_package",
6363
)
6464

65+
api_golden_test(
66+
name = "platform-browser_errors",
67+
data = [
68+
"//goldens:public-api",
69+
"//packages/platform-browser",
70+
],
71+
entry_point = "angular/packages/platform-browser/src/errors.d.ts",
72+
golden = "angular/goldens/public-api/platform-browser/errors.md",
73+
)
74+
6575
filegroup(
6676
name = "files_for_docgen",
6777
srcs = glob([
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* The list of error codes used in runtime code of the `platform-browser` package.
11+
* Reserved error code range: 5000-5500.
12+
*/
13+
export const enum RuntimeErrorCode {
14+
// Hydration Errors
15+
UNSUPPORTED_ZONEJS_INSTANCE = -5000,
16+
}

packages/platform-browser/src/hydration.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
*/
88

99
import {ɵwithHttpTransferCache as withHttpTransferCache} from '@angular/common/http';
10-
import {EnvironmentProviders, makeEnvironmentProviders, Provider, ɵwithDomHydration as withDomHydration} from '@angular/core';
10+
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, makeEnvironmentProviders, NgZone, Provider, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵwithDomHydration as withDomHydration} from '@angular/core';
11+
12+
import {RuntimeErrorCode} from './errors';
1113

1214
/**
1315
* The list of features as an enum to uniquely type each `HydrationFeature`.
@@ -91,6 +93,33 @@ export function withNoHttpTransferCache():
9193
return hydrationFeature(HydrationFeatureKind.NoHttpTransferCache);
9294
}
9395

96+
/**
97+
* Returns an `ENVIRONMENT_INITIALIZER` token setup with a function
98+
* that verifies whether compatible ZoneJS was used in an application
99+
* and logs a warning in a console if it's not the case.
100+
*/
101+
function provideZoneJsCompatibilityDetector(): Provider[] {
102+
return [{
103+
provide: ENVIRONMENT_INITIALIZER,
104+
useValue: () => {
105+
const ngZone = inject(NgZone);
106+
// Checking `ngZone instanceof NgZone` would be insufficient here,
107+
// because custom implementations might use NgZone as a base class.
108+
if (ngZone.constructor !== NgZone) {
109+
const console = inject(Console);
110+
const message = formatRuntimeError(
111+
RuntimeErrorCode.UNSUPPORTED_ZONEJS_INSTANCE,
112+
'Angular detected that hydration was enabled for an application ' +
113+
'that uses a custom or a noop Zone.js implementation. ' +
114+
'This is not yet a fully supported configuration.');
115+
// tslint:disable-next-line:no-console
116+
console.warn(message);
117+
}
118+
},
119+
multi: true,
120+
}];
121+
}
122+
94123
/**
95124
* Sets up providers necessary to enable hydration functionality for the application.
96125
* By default, the function enables the recommended set of features for the optimal
@@ -142,6 +171,7 @@ export function provideClientHydration(...features: HydrationFeature<HydrationFe
142171
}
143172

144173
return makeEnvironmentProviders([
174+
(typeof ngDevMode !== 'undefined' && ngDevMode) ? provideZoneJsCompatibilityDetector() : [],
145175
(featuresKind.has(HydrationFeatureKind.NoDomReuseFeature) ? [] : withDomHydration()),
146176
(featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) ? [] : withHttpTransferCache()),
147177
providers,

packages/platform-server/test/hydration_spec.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {Console} from '@angular/core/src/console';
1515
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
1616
import {getComponentDef} from '@angular/core/src/render3/definition';
1717
import {unescapeTransferStateContent} from '@angular/core/src/transfer_state';
18+
import {NoopNgZone} from '@angular/core/src/zone/ng_zone';
1819
import {TestBed} from '@angular/core/testing';
1920
import {bootstrapApplication, HydrationFeature, HydrationFeatureKind, provideClientHydration, withNoDomReuse} from '@angular/platform-browser';
2021
import {provideRouter, RouterOutlet, Routes} from '@angular/router';
@@ -587,10 +588,17 @@ describe('platform-server integration', () => {
587588

588589
resetTViewsFor(SimpleComponent, NestedComponent);
589590

590-
const appRef = await hydrate(html, SimpleComponent);
591+
const appRef = await hydrate(html, SimpleComponent, [withDebugConsole()]);
591592
const compRef = getComponentRef<SimpleComponent>(appRef);
592593
appRef.tick();
593594

595+
// Make sure there are no extra logs in case
596+
// default NgZone is setup for an application.
597+
verifyHasNoLog(
598+
appRef,
599+
'NG05000: Angular detected that hydration was enabled for an application ' +
600+
'that uses a custom or a noop Zone.js implementation.');
601+
594602
const clientRootNode = compRef.location.nativeElement;
595603
verifyAllNodesClaimedForHydration(clientRootNode);
596604
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
@@ -3990,6 +3998,78 @@ describe('platform-server integration', () => {
39903998
});
39913999
});
39924000

4001+
describe('unsupported Zone.js config', () => {
4002+
it('should log a warning when a noop zone is used', async () => {
4003+
@Component({
4004+
standalone: true,
4005+
selector: 'app',
4006+
template: `Hi!`,
4007+
})
4008+
class SimpleComponent {
4009+
}
4010+
4011+
const html = await ssr(SimpleComponent);
4012+
const ssrContents = getAppContents(html);
4013+
4014+
expect(ssrContents).toContain('<app ngh');
4015+
4016+
resetTViewsFor(SimpleComponent);
4017+
4018+
const appRef = await hydrate(html, SimpleComponent, [
4019+
{provide: NgZone, useValue: new NoopNgZone()},
4020+
withDebugConsole(),
4021+
]);
4022+
const compRef = getComponentRef<SimpleComponent>(appRef);
4023+
appRef.tick();
4024+
4025+
verifyHasLog(
4026+
appRef,
4027+
'NG05000: Angular detected that hydration was enabled for an application ' +
4028+
'that uses a custom or a noop Zone.js implementation.');
4029+
4030+
const clientRootNode = compRef.location.nativeElement;
4031+
4032+
verifyAllNodesClaimedForHydration(clientRootNode);
4033+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
4034+
});
4035+
4036+
it('should log a warning when a custom zone is used', async () => {
4037+
@Component({
4038+
standalone: true,
4039+
selector: 'app',
4040+
template: `Hi!`,
4041+
})
4042+
class SimpleComponent {
4043+
}
4044+
4045+
const html = await ssr(SimpleComponent);
4046+
const ssrContents = getAppContents(html);
4047+
4048+
expect(ssrContents).toContain('<app ngh');
4049+
4050+
resetTViewsFor(SimpleComponent);
4051+
4052+
class CustomNgZone extends NgZone {}
4053+
4054+
const appRef = await hydrate(html, SimpleComponent, [
4055+
{provide: NgZone, useValue: new CustomNgZone({})},
4056+
withDebugConsole(),
4057+
]);
4058+
const compRef = getComponentRef<SimpleComponent>(appRef);
4059+
appRef.tick();
4060+
4061+
verifyHasLog(
4062+
appRef,
4063+
'NG05000: Angular detected that hydration was enabled for an application ' +
4064+
'that uses a custom or a noop Zone.js implementation.');
4065+
4066+
const clientRootNode = compRef.location.nativeElement;
4067+
4068+
verifyAllNodesClaimedForHydration(clientRootNode);
4069+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
4070+
});
4071+
});
4072+
39934073
describe('error handling', () => {
39944074
it('should handle text node mismatch', async () => {
39954075
@Component({

0 commit comments

Comments
 (0)