mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(platform-browser): log a warning when a custom or a noop ZoneJS is used with hydration (#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 #49944
This commit is contained in:
parent
eb5bc95dbf
commit
3bcbfecb78
9 changed files with 188 additions and 4 deletions
14
aio/content/errors/NG5000.md
Normal file
14
aio/content/errors/NG5000.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -65,7 +65,7 @@ After you've followed these steps and have started up your server, load your app
|
|||
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
<a id="constraints"></a>
|
||||
|
||||
|
|
@ -110,6 +110,13 @@ If you choose to set this setting in your tsconfig, we recommend to set it only
|
|||
|
||||
</div>
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
<a id="errors"></a>
|
||||
|
||||
## Errors
|
||||
|
|
|
|||
15
goldens/public-api/platform-browser/errors.md
Normal file
15
goldens/public-api/platform-browser/errors.md
Normal file
|
|
@ -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)
|
||||
|
||||
```
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
16
packages/platform-browser/src/errors.ts
Normal file
16
packages/platform-browser/src/errors.ts
Normal file
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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<HydrationFe
|
|||
}
|
||||
|
||||
return makeEnvironmentProviders([
|
||||
(typeof ngDevMode !== 'undefined' && ngDevMode) ? provideZoneJsCompatibilityDetector() : [],
|
||||
(featuresKind.has(HydrationFeatureKind.NoDomReuseFeature) ? [] : withDomHydration()),
|
||||
(featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) ? [] : withHttpTransferCache()),
|
||||
providers,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {Console} from '@angular/core/src/console';
|
|||
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
|
||||
import {getComponentDef} from '@angular/core/src/render3/definition';
|
||||
import {unescapeTransferStateContent} from '@angular/core/src/transfer_state';
|
||||
import {NoopNgZone} from '@angular/core/src/zone/ng_zone';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {bootstrapApplication, HydrationFeature, HydrationFeatureKind, provideClientHydration, withNoDomReuse} from '@angular/platform-browser';
|
||||
import {provideRouter, RouterOutlet, Routes} from '@angular/router';
|
||||
|
|
@ -587,10 +588,17 @@ describe('platform-server integration', () => {
|
|||
|
||||
resetTViewsFor(SimpleComponent, NestedComponent);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const appRef = await hydrate(html, SimpleComponent, [withDebugConsole()]);
|
||||
const compRef = getComponentRef<SimpleComponent>(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('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent, [
|
||||
{provide: NgZone, useValue: new NoopNgZone()},
|
||||
withDebugConsole(),
|
||||
]);
|
||||
const compRef = getComponentRef<SimpleComponent>(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('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent);
|
||||
|
||||
class CustomNgZone extends NgZone {}
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent, [
|
||||
{provide: NgZone, useValue: new CustomNgZone({})},
|
||||
withDebugConsole(),
|
||||
]);
|
||||
const compRef = getComponentRef<SimpleComponent>(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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue