@angular/core 16.0.0-next.3 → 16.0.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2020/src/application_tokens.mjs +33 -1
- package/esm2020/src/change_detection/change_detector_ref.mjs +4 -4
- package/esm2020/src/compiler/compiler_facade_interface.mjs +1 -1
- package/esm2020/src/core.mjs +2 -2
- package/esm2020/src/core_private_export.mjs +2 -1
- package/esm2020/src/core_reactivity_export_internal.mjs +1 -1
- package/esm2020/src/core_render3_private_export.mjs +1 -2
- package/esm2020/src/di/r3_injector.mjs +8 -1
- package/esm2020/src/errors.mjs +1 -1
- package/esm2020/src/hydration/annotate.mjs +118 -5
- package/esm2020/src/hydration/api.mjs +14 -1
- package/esm2020/src/hydration/cleanup.mjs +54 -3
- package/esm2020/src/hydration/compression.mjs +69 -0
- package/esm2020/src/hydration/error_handling.mjs +357 -15
- package/esm2020/src/hydration/interfaces.mjs +17 -1
- package/esm2020/src/hydration/node_lookup_utils.mjs +199 -7
- package/esm2020/src/hydration/utils.mjs +16 -3
- package/esm2020/src/hydration/views.mjs +19 -15
- package/esm2020/src/linker/destroy_ref.mjs +5 -2
- package/esm2020/src/linker/view_container_ref.mjs +8 -7
- package/esm2020/src/metadata/directives.mjs +8 -3
- package/esm2020/src/render3/instructions/element.mjs +16 -9
- package/esm2020/src/render3/instructions/element_container.mjs +2 -5
- package/esm2020/src/render3/instructions/element_validation.mjs +2 -2
- package/esm2020/src/render3/instructions/projection.mjs +7 -4
- package/esm2020/src/render3/instructions/template.mjs +4 -7
- package/esm2020/src/render3/instructions/text.mjs +6 -6
- package/esm2020/src/render3/interfaces/public_definitions.mjs +1 -1
- package/esm2020/src/render3/interfaces/type_checks.mjs +4 -1
- package/esm2020/src/render3/node_manipulation.mjs +2 -2
- package/esm2020/src/render3/node_selector_matcher.mjs +17 -5
- package/esm2020/src/render3/util/view_utils.mjs +12 -1
- package/esm2020/src/render3/view_ref.mjs +1 -1
- package/esm2020/src/signals/index.mjs +2 -1
- package/esm2020/src/signals/src/computed.mjs +3 -3
- package/esm2020/src/signals/src/effect.mjs +1 -3
- package/esm2020/src/signals/src/signal.mjs +3 -3
- package/esm2020/src/signals/src/watch.mjs +3 -3
- package/esm2020/src/signals/src/weak_ref.mjs +18 -2
- package/esm2020/src/util/ng_dev_mode.mjs +2 -1
- package/esm2020/src/version.mjs +1 -1
- package/esm2020/testing/src/logger.mjs +3 -3
- package/esm2020/testing/src/ng_zone_mock.mjs +3 -3
- package/esm2020/testing/src/test_bed_compiler.mjs +12 -7
- package/fesm2015/core.mjs +1066 -178
- package/fesm2015/core.mjs.map +1 -1
- package/fesm2015/testing.mjs +816 -68
- package/fesm2015/testing.mjs.map +1 -1
- package/fesm2020/core.mjs +1051 -170
- package/fesm2020/core.mjs.map +1 -1
- package/fesm2020/testing.mjs +810 -67
- package/fesm2020/testing.mjs.map +1 -1
- package/index.d.ts +128 -216
- package/package.json +1 -1
- package/schematics/migrations/relative-link-resolution/bundle.js +7 -7
- package/schematics/migrations/router-link-with-href/bundle.js +10 -10
- package/schematics/ng-generate/standalone-migration/bundle.js +473 -376
- package/schematics/ng-generate/standalone-migration/bundle.js.map +2 -2
- package/testing/index.d.ts +1 -1
package/fesm2020/testing.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Angular v16.0.0-next.
|
|
2
|
+
* @license Angular v16.0.0-next.4
|
|
3
3
|
* (c) 2010-2022 Google LLC. https://angular.io/
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getDebugNode, RendererFactory2 as RendererFactory2$1, InjectionToken as InjectionToken$1, ɵstringify, ɵReflectionCapabilities, Directive, Component, Pipe, NgModule, ɵgetInjectableDef, resolveForwardRef as resolveForwardRef$1, ɵNG_COMP_DEF, ɵRender3NgModuleRef, ApplicationInitStatus, LOCALE_ID as LOCALE_ID$1, ɵDEFAULT_LOCALE_ID, ɵsetLocaleId, ɵRender3ComponentFactory, ɵcompileComponent, ɵNG_DIR_DEF, ɵcompileDirective, ɵNG_PIPE_DEF, ɵcompilePipe, ɵNG_MOD_DEF, ɵtransitiveScopesFor, ɵpatchComponentDefWithScope, ɵNG_INJ_DEF, ɵcompileNgModuleDefs, NgZone, ɵprovideNgZoneChangeDetection, Compiler, COMPILER_OPTIONS, ɵNgModuleFactory, ɵisEnvironmentProviders, ModuleWithComponentFactories, ɵconvertToBitFlags, Injector as Injector$1, InjectFlags as InjectFlags$1, ɵsetAllowDuplicateNgModuleIdsForTest, ɵresetCompiledComponents, ɵsetUnknownElementStrictMode as ɵsetUnknownElementStrictMode$1, ɵsetUnknownPropertyStrictMode as ɵsetUnknownPropertyStrictMode$1, ɵgetUnknownElementStrictMode as ɵgetUnknownElementStrictMode$1, ɵgetUnknownPropertyStrictMode as ɵgetUnknownPropertyStrictMode$1, EnvironmentInjector as EnvironmentInjector$1, ɵflushModuleScopingQueueAsMuchAsPossible } from '@angular/core';
|
|
8
8
|
import { ResourceLoader } from '@angular/compiler';
|
|
9
9
|
import { Subject, Subscription } from 'rxjs';
|
|
10
|
+
import { first } from 'rxjs/operators';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Wraps a test function in an asynchronous test zone. The test will automatically
|
|
@@ -1638,6 +1639,7 @@ function ngDevModeResetPerfCounters() {
|
|
|
1638
1639
|
hydratedNodes: 0,
|
|
1639
1640
|
hydratedComponents: 0,
|
|
1640
1641
|
dehydratedViewsRemoved: 0,
|
|
1642
|
+
dehydratedViewsCleanupRuns: 0,
|
|
1641
1643
|
};
|
|
1642
1644
|
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
|
|
1643
1645
|
const allowNgDevModeTrue = locationString.indexOf('ngDevMode=false') === -1;
|
|
@@ -2446,12 +2448,19 @@ function isCssClassMatching(attrs, cssClassToMatch, isProjectionMode) {
|
|
|
2446
2448
|
ngDevMode &&
|
|
2447
2449
|
assertEqual(cssClassToMatch, cssClassToMatch.toLowerCase(), 'Class name expected to be lowercase.');
|
|
2448
2450
|
let i = 0;
|
|
2451
|
+
// Indicates whether we are processing value from the implicit
|
|
2452
|
+
// attribute section (i.e. before the first marker in the array).
|
|
2453
|
+
let isImplicitAttrsSection = true;
|
|
2449
2454
|
while (i < attrs.length) {
|
|
2450
2455
|
let item = attrs[i++];
|
|
2451
|
-
if (
|
|
2452
|
-
|
|
2453
|
-
if (
|
|
2454
|
-
|
|
2456
|
+
if (typeof item === 'string' && isImplicitAttrsSection) {
|
|
2457
|
+
const value = attrs[i++];
|
|
2458
|
+
if (isProjectionMode && item === 'class') {
|
|
2459
|
+
// We found a `class` attribute in the implicit attribute section,
|
|
2460
|
+
// check if it matches the value of the `cssClassToMatch` argument.
|
|
2461
|
+
if (classIndexOf(value.toLowerCase(), cssClassToMatch, 0) !== -1) {
|
|
2462
|
+
return true;
|
|
2463
|
+
}
|
|
2455
2464
|
}
|
|
2456
2465
|
}
|
|
2457
2466
|
else if (item === 1 /* AttributeMarker.Classes */) {
|
|
@@ -2463,6 +2472,11 @@ function isCssClassMatching(attrs, cssClassToMatch, isProjectionMode) {
|
|
|
2463
2472
|
}
|
|
2464
2473
|
return false;
|
|
2465
2474
|
}
|
|
2475
|
+
else if (typeof item === 'number') {
|
|
2476
|
+
// We've came across a first marker, which indicates
|
|
2477
|
+
// that the implicit attribute section is over.
|
|
2478
|
+
isImplicitAttrsSection = false;
|
|
2479
|
+
}
|
|
2466
2480
|
}
|
|
2467
2481
|
return false;
|
|
2468
2482
|
}
|
|
@@ -3297,6 +3311,9 @@ function isComponentDef(def) {
|
|
|
3297
3311
|
function isRootView(target) {
|
|
3298
3312
|
return (target[FLAGS] & 256 /* LViewFlags.IsRoot */) !== 0;
|
|
3299
3313
|
}
|
|
3314
|
+
function isProjectionTNode(tNode) {
|
|
3315
|
+
return (tNode.type & 16 /* TNodeType.Projection */) === 16 /* TNodeType.Projection */;
|
|
3316
|
+
}
|
|
3300
3317
|
|
|
3301
3318
|
// [Assert functions do not constraint type when they are guarded by a truthy
|
|
3302
3319
|
// expression.](https://github.com/microsoft/TypeScript/issues/37295)
|
|
@@ -3710,6 +3727,17 @@ function storeLViewOnDestroy(lView, onDestroyCallback) {
|
|
|
3710
3727
|
}
|
|
3711
3728
|
lView[ON_DESTROY_HOOKS].push(onDestroyCallback);
|
|
3712
3729
|
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Removes previously registered LView-specific destroy callback.
|
|
3732
|
+
*/
|
|
3733
|
+
function removeLViewOnDestroy(lView, onDestroyCallback) {
|
|
3734
|
+
if (lView[ON_DESTROY_HOOKS] === null)
|
|
3735
|
+
return;
|
|
3736
|
+
const destroyCBIdx = lView[ON_DESTROY_HOOKS].indexOf(onDestroyCallback);
|
|
3737
|
+
if (destroyCBIdx !== -1) {
|
|
3738
|
+
lView[ON_DESTROY_HOOKS].splice(destroyCBIdx, 1);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3713
3741
|
|
|
3714
3742
|
const instructionState = {
|
|
3715
3743
|
lFrame: createLFrame(null),
|
|
@@ -8805,6 +8833,7 @@ class R3Injector extends EnvironmentInjector {
|
|
|
8805
8833
|
}
|
|
8806
8834
|
onDestroy(callback) {
|
|
8807
8835
|
this._onDestroyHooks.push(callback);
|
|
8836
|
+
return () => this.removeOnDestroy(callback);
|
|
8808
8837
|
}
|
|
8809
8838
|
runInContext(fn) {
|
|
8810
8839
|
this.assertNotDestroyed();
|
|
@@ -8979,6 +9008,12 @@ class R3Injector extends EnvironmentInjector {
|
|
|
8979
9008
|
return this.injectorDefTypes.has(providedIn);
|
|
8980
9009
|
}
|
|
8981
9010
|
}
|
|
9011
|
+
removeOnDestroy(callback) {
|
|
9012
|
+
const destroyCBIdx = this._onDestroyHooks.indexOf(callback);
|
|
9013
|
+
if (destroyCBIdx !== -1) {
|
|
9014
|
+
this._onDestroyHooks.splice(destroyCBIdx, 1);
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
8982
9017
|
}
|
|
8983
9018
|
function injectableDefOrInjectorDefFactory(token) {
|
|
8984
9019
|
// Most tokens will have an injectable def directly on them, which specifies a factory directly.
|
|
@@ -9164,6 +9199,37 @@ const PACKAGE_ROOT_URL = new InjectionToken('Application Packages Root URL');
|
|
|
9164
9199
|
* @publicApi
|
|
9165
9200
|
*/
|
|
9166
9201
|
const ANIMATION_MODULE_TYPE = new InjectionToken('AnimationModuleType');
|
|
9202
|
+
// TODO(crisbeto): link to CSP guide here.
|
|
9203
|
+
/**
|
|
9204
|
+
* Token used to configure the [Content Security Policy](https://web.dev/strict-csp/) nonce that
|
|
9205
|
+
* Angular will apply when inserting inline styles. If not provided, Angular will look up its value
|
|
9206
|
+
* from the `ngCspNonce` attribute of the application root node.
|
|
9207
|
+
*
|
|
9208
|
+
* @publicApi
|
|
9209
|
+
*/
|
|
9210
|
+
const CSP_NONCE = new InjectionToken('CSP nonce', {
|
|
9211
|
+
providedIn: 'root',
|
|
9212
|
+
factory: () => {
|
|
9213
|
+
// Ideally we wouldn't have to use `querySelector` here since we know that the nonce will be on
|
|
9214
|
+
// the root node, but because the token value is used in renderers, it has to be available
|
|
9215
|
+
// *very* early in the bootstrapping process. This should be a fairly shallow search, because
|
|
9216
|
+
// the app won't have been added to the DOM yet. Some approaches that were considered:
|
|
9217
|
+
// 1. Find the root node through `ApplicationRef.components[i].location` - normally this would
|
|
9218
|
+
// be enough for our purposes, but the token is injected very early so the `components` array
|
|
9219
|
+
// isn't populated yet.
|
|
9220
|
+
// 2. Find the root `LView` through the current `LView` - renderers are a prerequisite to
|
|
9221
|
+
// creating the `LView`. This means that no `LView` will have been entered when this factory is
|
|
9222
|
+
// invoked for the root component.
|
|
9223
|
+
// 3. Have the token factory return `() => string` which is invoked when a nonce is requested -
|
|
9224
|
+
// the slightly later execution does allow us to get an `LView` reference, but the fact that
|
|
9225
|
+
// it is a function means that it could be executed at *any* time (including immediately) which
|
|
9226
|
+
// may lead to weird bugs.
|
|
9227
|
+
// 4. Have the `ComponentFactory` read the attribute and provide it to the injector under the
|
|
9228
|
+
// hood - has the same problem as #1 and #2 in that the renderer is used to query for the root
|
|
9229
|
+
// node and the nonce value needs to be available when the renderer is created.
|
|
9230
|
+
return getDocument().body.querySelector('[ngCspNonce]')?.getAttribute('ngCspNonce') || null;
|
|
9231
|
+
},
|
|
9232
|
+
});
|
|
9167
9233
|
|
|
9168
9234
|
function escapeTransferStateContent(text) {
|
|
9169
9235
|
const escapedText = {
|
|
@@ -9306,6 +9372,19 @@ function retrieveTransferredState(doc, appId) {
|
|
|
9306
9372
|
return initialState;
|
|
9307
9373
|
}
|
|
9308
9374
|
|
|
9375
|
+
/** Encodes that the node lookup should start from the host node of this component. */
|
|
9376
|
+
const REFERENCE_NODE_HOST = 'h';
|
|
9377
|
+
/** Encodes that the node lookup should start from the document body node. */
|
|
9378
|
+
const REFERENCE_NODE_BODY = 'b';
|
|
9379
|
+
/**
|
|
9380
|
+
* Describes navigation steps that the runtime logic need to perform,
|
|
9381
|
+
* starting from a given (known) element.
|
|
9382
|
+
*/
|
|
9383
|
+
var NodeNavigationStep;
|
|
9384
|
+
(function (NodeNavigationStep) {
|
|
9385
|
+
NodeNavigationStep["FirstChild"] = "f";
|
|
9386
|
+
NodeNavigationStep["NextSibling"] = "n";
|
|
9387
|
+
})(NodeNavigationStep || (NodeNavigationStep = {}));
|
|
9309
9388
|
/**
|
|
9310
9389
|
* Keys within serialized view data structure to represent various
|
|
9311
9390
|
* parts. See the `SerializedView` interface below for additional information.
|
|
@@ -9313,8 +9392,11 @@ function retrieveTransferredState(doc, appId) {
|
|
|
9313
9392
|
const ELEMENT_CONTAINERS = 'e';
|
|
9314
9393
|
const TEMPLATES = 't';
|
|
9315
9394
|
const CONTAINERS = 'c';
|
|
9395
|
+
const MULTIPLIER = 'x';
|
|
9316
9396
|
const NUM_ROOT_NODES = 'r';
|
|
9317
9397
|
const TEMPLATE_ID = 'i'; // as it's also an "id"
|
|
9398
|
+
const NODES = 'n';
|
|
9399
|
+
const DISCONNECTED_NODES = 'd';
|
|
9318
9400
|
|
|
9319
9401
|
/**
|
|
9320
9402
|
* The name of the key used in the TransferState collection,
|
|
@@ -9504,10 +9586,23 @@ function calcSerializedContainerSize(hydrationInfo, index) {
|
|
|
9504
9586
|
const views = getSerializedContainerViews(hydrationInfo, index) ?? [];
|
|
9505
9587
|
let numNodes = 0;
|
|
9506
9588
|
for (let view of views) {
|
|
9507
|
-
numNodes += view[NUM_ROOT_NODES];
|
|
9589
|
+
numNodes += view[NUM_ROOT_NODES] * (view[MULTIPLIER] ?? 1);
|
|
9508
9590
|
}
|
|
9509
9591
|
return numNodes;
|
|
9510
9592
|
}
|
|
9593
|
+
/**
|
|
9594
|
+
* Checks whether a node is annotated as "disconnected", i.e. not present
|
|
9595
|
+
* in the DOM at serialization time. We should not attempt hydration for
|
|
9596
|
+
* such nodes and instead, use a regular "creation mode".
|
|
9597
|
+
*/
|
|
9598
|
+
function isDisconnectedNode(hydrationInfo, index) {
|
|
9599
|
+
// Check if we are processing disconnected info for the first time.
|
|
9600
|
+
if (typeof hydrationInfo.disconnectedNodes === 'undefined') {
|
|
9601
|
+
const nodeIds = hydrationInfo.data[DISCONNECTED_NODES];
|
|
9602
|
+
hydrationInfo.disconnectedNodes = nodeIds ? (new Set(nodeIds)) : null;
|
|
9603
|
+
}
|
|
9604
|
+
return !!hydrationInfo.disconnectedNodes?.has(index);
|
|
9605
|
+
}
|
|
9511
9606
|
|
|
9512
9607
|
/**
|
|
9513
9608
|
* Represents a component created by a `ComponentFactory`.
|
|
@@ -9688,7 +9783,7 @@ class Version {
|
|
|
9688
9783
|
/**
|
|
9689
9784
|
* @publicApi
|
|
9690
9785
|
*/
|
|
9691
|
-
const VERSION = new Version('16.0.0-next.
|
|
9786
|
+
const VERSION = new Version('16.0.0-next.4');
|
|
9692
9787
|
|
|
9693
9788
|
// This default value is when checking the hierarchy for a token.
|
|
9694
9789
|
//
|
|
@@ -14380,40 +14475,443 @@ function detectChanges(component) {
|
|
|
14380
14475
|
detectChangesInternal(view[TVIEW], view, component);
|
|
14381
14476
|
}
|
|
14382
14477
|
|
|
14478
|
+
const AT_THIS_LOCATION = '<-- AT THIS LOCATION';
|
|
14383
14479
|
/**
|
|
14384
|
-
*
|
|
14385
|
-
*
|
|
14480
|
+
* Retrieves a user friendly string for a given TNodeType for use in
|
|
14481
|
+
* friendly error messages
|
|
14482
|
+
*
|
|
14483
|
+
* @param tNodeType
|
|
14484
|
+
* @returns
|
|
14386
14485
|
*/
|
|
14387
|
-
function
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
14486
|
+
function getFriendlyStringFromTNodeType(tNodeType) {
|
|
14487
|
+
switch (tNodeType) {
|
|
14488
|
+
case 4 /* TNodeType.Container */:
|
|
14489
|
+
return 'view container';
|
|
14490
|
+
case 2 /* TNodeType.Element */:
|
|
14491
|
+
return 'element';
|
|
14492
|
+
case 8 /* TNodeType.ElementContainer */:
|
|
14493
|
+
return 'ng-container';
|
|
14494
|
+
case 32 /* TNodeType.Icu */:
|
|
14495
|
+
return 'icu';
|
|
14496
|
+
case 64 /* TNodeType.Placeholder */:
|
|
14497
|
+
return 'i18n';
|
|
14498
|
+
case 16 /* TNodeType.Projection */:
|
|
14499
|
+
return 'projection';
|
|
14500
|
+
case 1 /* TNodeType.Text */:
|
|
14501
|
+
return 'text';
|
|
14502
|
+
default:
|
|
14503
|
+
// This should not happen as we cover all possible TNode types above.
|
|
14504
|
+
return '<unknown>';
|
|
14505
|
+
}
|
|
14506
|
+
}
|
|
14507
|
+
/**
|
|
14508
|
+
* Validates that provided nodes match during the hydration process.
|
|
14509
|
+
*/
|
|
14510
|
+
function validateMatchingNode(node, nodeType, tagName, lView, tNode, isViewContainerAnchor = false) {
|
|
14511
|
+
if (!node ||
|
|
14512
|
+
(node.nodeType !== nodeType ||
|
|
14513
|
+
(node.nodeType === Node.ELEMENT_NODE &&
|
|
14514
|
+
node.tagName.toLowerCase() !== tagName?.toLowerCase()))) {
|
|
14515
|
+
const expectedNode = shortRNodeDescription(nodeType, tagName, null);
|
|
14516
|
+
let header = `During hydration Angular expected ${expectedNode} but `;
|
|
14517
|
+
const hostComponentDef = getDeclarationComponentDef(lView);
|
|
14518
|
+
const componentClassName = hostComponentDef?.type?.name;
|
|
14519
|
+
const expected = `Angular expected this DOM:\n\n${describeExpectedDom(lView, tNode, isViewContainerAnchor)}\n\n`;
|
|
14520
|
+
let actual = '';
|
|
14521
|
+
if (!node) {
|
|
14522
|
+
// No node found during hydration.
|
|
14523
|
+
header += `the node was not found.\n\n`;
|
|
14524
|
+
}
|
|
14525
|
+
else {
|
|
14526
|
+
const actualNode = shortRNodeDescription(node.nodeType, node.tagName ?? null, node.textContent ?? null);
|
|
14527
|
+
header += `found ${actualNode}.\n\n`;
|
|
14528
|
+
actual = `Actual DOM is:\n\n${describeDomFromNode(node)}\n\n`;
|
|
14529
|
+
}
|
|
14530
|
+
const footer = getHydrationErrorFooter(componentClassName);
|
|
14531
|
+
const message = header + expected + actual + getHydrationAttributeNote() + footer;
|
|
14532
|
+
throw new RuntimeError(500 /* RuntimeErrorCode.HYDRATION_NODE_MISMATCH */, message);
|
|
14394
14533
|
}
|
|
14395
14534
|
}
|
|
14396
14535
|
/**
|
|
14397
|
-
*
|
|
14536
|
+
* Validates that a given node has sibling nodes
|
|
14398
14537
|
*/
|
|
14399
14538
|
function validateSiblingNodeExists(node) {
|
|
14400
14539
|
validateNodeExists(node);
|
|
14401
14540
|
if (!node.nextSibling) {
|
|
14402
|
-
|
|
14403
|
-
|
|
14541
|
+
const header = 'During hydration Angular expected more sibling nodes to be present.\n\n';
|
|
14542
|
+
const actual = `Actual DOM is:\n\n${describeDomFromNode(node)}\n\n`;
|
|
14543
|
+
const footer = getHydrationErrorFooter();
|
|
14544
|
+
const message = header + actual + footer;
|
|
14545
|
+
throw new RuntimeError(501 /* RuntimeErrorCode.HYDRATION_MISSING_SIBLINGS */, message);
|
|
14404
14546
|
}
|
|
14405
14547
|
}
|
|
14548
|
+
/**
|
|
14549
|
+
* Validates that a node exists or throws
|
|
14550
|
+
*/
|
|
14406
14551
|
function validateNodeExists(node) {
|
|
14407
14552
|
if (!node) {
|
|
14408
|
-
|
|
14409
|
-
|
|
14553
|
+
throw new RuntimeError(502 /* RuntimeErrorCode.HYDRATION_MISSING_NODE */, `Hydration expected an element to be present at this location.`);
|
|
14554
|
+
}
|
|
14555
|
+
}
|
|
14556
|
+
/**
|
|
14557
|
+
* Builds the hydration error message when a node is not found
|
|
14558
|
+
*
|
|
14559
|
+
* @param lView the LView where the node exists
|
|
14560
|
+
* @param tNode the TNode
|
|
14561
|
+
*/
|
|
14562
|
+
function nodeNotFoundError(lView, tNode) {
|
|
14563
|
+
const header = 'During serialization, Angular was unable to find an element in the DOM:\n\n';
|
|
14564
|
+
const expected = `${describeExpectedDom(lView, tNode, false)}\n\n`;
|
|
14565
|
+
const footer = getHydrationErrorFooter();
|
|
14566
|
+
throw new RuntimeError(502 /* RuntimeErrorCode.HYDRATION_MISSING_NODE */, header + expected + footer);
|
|
14567
|
+
}
|
|
14568
|
+
/**
|
|
14569
|
+
* Builds a hydration error message when a node is not found at a path location
|
|
14570
|
+
*
|
|
14571
|
+
* @param host the Host Node
|
|
14572
|
+
* @param path the path to the node
|
|
14573
|
+
*/
|
|
14574
|
+
function nodeNotFoundAtPathError(host, path) {
|
|
14575
|
+
const header = `During hydration Angular was unable to locate a node ` +
|
|
14576
|
+
`using the "${path}" path, starting from the ${describeRNode(host)} node.\n\n`;
|
|
14577
|
+
const footer = getHydrationErrorFooter();
|
|
14578
|
+
throw new RuntimeError(502 /* RuntimeErrorCode.HYDRATION_MISSING_NODE */, header + footer);
|
|
14579
|
+
}
|
|
14580
|
+
/**
|
|
14581
|
+
* Builds the hydration error message in the case that dom nodes are created outside of
|
|
14582
|
+
* the Angular context and are being used as projected nodes
|
|
14583
|
+
*
|
|
14584
|
+
* @param lView the LView
|
|
14585
|
+
* @param tNode the TNode
|
|
14586
|
+
* @returns an error
|
|
14587
|
+
*/
|
|
14588
|
+
function unsupportedProjectionOfDomNodes(rNode) {
|
|
14589
|
+
const header = 'During serialization, Angular detected DOM nodes ' +
|
|
14590
|
+
'that were created outside of Angular context and provided as projectable nodes ' +
|
|
14591
|
+
'(likely via `ViewContainerRef.createComponent` or `createComponent` APIs). ' +
|
|
14592
|
+
'Hydration is not supported for such cases, consider refactoring the code to avoid ' +
|
|
14593
|
+
'this pattern or using `ngSkipHydration` on the host element of the component.\n\n';
|
|
14594
|
+
const actual = `${describeDomFromNode(rNode)}\n\n`;
|
|
14595
|
+
const message = header + actual + getHydrationAttributeNote();
|
|
14596
|
+
return new RuntimeError(503 /* RuntimeErrorCode.UNSUPPORTED_PROJECTION_DOM_NODES */, message);
|
|
14597
|
+
}
|
|
14598
|
+
/**
|
|
14599
|
+
* Builds the hydration error message in the case that ngSkipHydration was used on a
|
|
14600
|
+
* node that is not a component host element or host binding
|
|
14601
|
+
*
|
|
14602
|
+
* @param rNode the HTML Element
|
|
14603
|
+
* @returns an error
|
|
14604
|
+
*/
|
|
14605
|
+
function invalidSkipHydrationHost(rNode) {
|
|
14606
|
+
const header = 'The `ngSkipHydration` flag is applied on a node ' +
|
|
14607
|
+
'that doesn\'t act as a component host. Hydration can be ' +
|
|
14608
|
+
'skipped only on per-component basis.\n\n';
|
|
14609
|
+
const actual = `${describeDomFromNode(rNode)}\n\n`;
|
|
14610
|
+
const footer = 'Please move the `ngSkipHydration` attribute to the component host element.';
|
|
14611
|
+
const message = header + actual + footer;
|
|
14612
|
+
return new RuntimeError(504 /* RuntimeErrorCode.INVALID_SKIP_HYDRATION_HOST */, message);
|
|
14613
|
+
}
|
|
14614
|
+
/**
|
|
14615
|
+
* Builds the hydration error message in the case that a user is attempting to enable
|
|
14616
|
+
* hydration on internationalized nodes, which is not yet supported.
|
|
14617
|
+
*
|
|
14618
|
+
* @param rNode the HTML Element
|
|
14619
|
+
* @returns an error
|
|
14620
|
+
*/
|
|
14621
|
+
function notYetSupportedI18nBlockError(rNode) {
|
|
14622
|
+
const header = 'Hydration for nodes marked with `i18n` is not yet supported. ' +
|
|
14623
|
+
'You can opt-out a component that uses `i18n` in a template using ' +
|
|
14624
|
+
'the `ngSkipHydration` attribute or fall back to the previous ' +
|
|
14625
|
+
'hydration logic (which re-creates the application structure).\n\n';
|
|
14626
|
+
const actual = `${describeDomFromNode(rNode)}\n\n`;
|
|
14627
|
+
const message = header + actual;
|
|
14628
|
+
return new RuntimeError(518 /* RuntimeErrorCode.HYDRATION_I18N_NOT_YET_SUPPORTED */, message);
|
|
14629
|
+
}
|
|
14630
|
+
// Stringification methods
|
|
14631
|
+
/**
|
|
14632
|
+
* Stringifies a given TNode's attributes
|
|
14633
|
+
*
|
|
14634
|
+
* @param tNode a provided TNode
|
|
14635
|
+
* @returns string
|
|
14636
|
+
*/
|
|
14637
|
+
function stringifyTNodeAttrs(tNode) {
|
|
14638
|
+
const results = [];
|
|
14639
|
+
if (tNode.attrs) {
|
|
14640
|
+
for (let i = 0; i < tNode.attrs.length;) {
|
|
14641
|
+
const attrName = tNode.attrs[i++];
|
|
14642
|
+
// Once we reach the first flag, we know that the list of
|
|
14643
|
+
// attributes is over.
|
|
14644
|
+
if (typeof attrName == 'number') {
|
|
14645
|
+
break;
|
|
14646
|
+
}
|
|
14647
|
+
const attrValue = tNode.attrs[i++];
|
|
14648
|
+
results.push(`${attrName}="${shorten(attrValue)}"`);
|
|
14649
|
+
}
|
|
14650
|
+
}
|
|
14651
|
+
return results.join(' ');
|
|
14652
|
+
}
|
|
14653
|
+
/**
|
|
14654
|
+
* The list of internal attributes that should be filtered out while
|
|
14655
|
+
* producing an error message.
|
|
14656
|
+
*/
|
|
14657
|
+
const internalAttrs = new Set(['ngh', 'ng-version', 'ng-server-context']);
|
|
14658
|
+
/**
|
|
14659
|
+
* Stringifies an HTML Element's attributes
|
|
14660
|
+
*
|
|
14661
|
+
* @param rNode an HTML Element
|
|
14662
|
+
* @returns string
|
|
14663
|
+
*/
|
|
14664
|
+
function stringifyRNodeAttrs(rNode) {
|
|
14665
|
+
const results = [];
|
|
14666
|
+
for (let i = 0; i < rNode.attributes.length; i++) {
|
|
14667
|
+
const attr = rNode.attributes[i];
|
|
14668
|
+
if (internalAttrs.has(attr.name))
|
|
14669
|
+
continue;
|
|
14670
|
+
results.push(`${attr.name}="${shorten(attr.value)}"`);
|
|
14671
|
+
}
|
|
14672
|
+
return results.join(' ');
|
|
14673
|
+
}
|
|
14674
|
+
// Methods for Describing the DOM
|
|
14675
|
+
/**
|
|
14676
|
+
* Converts a tNode to a helpful readable string value for use in error messages
|
|
14677
|
+
*
|
|
14678
|
+
* @param tNode a given TNode
|
|
14679
|
+
* @param innerContent the content of the node
|
|
14680
|
+
* @returns string
|
|
14681
|
+
*/
|
|
14682
|
+
function describeTNode(tNode, innerContent = '…') {
|
|
14683
|
+
switch (tNode.type) {
|
|
14684
|
+
case 1 /* TNodeType.Text */:
|
|
14685
|
+
const content = tNode.value ? `(${tNode.value})` : '';
|
|
14686
|
+
return `#text${content}`;
|
|
14687
|
+
case 2 /* TNodeType.Element */:
|
|
14688
|
+
const attrs = stringifyTNodeAttrs(tNode);
|
|
14689
|
+
const tag = tNode.value.toLowerCase();
|
|
14690
|
+
return `<${tag}${attrs ? ' ' + attrs : ''}>${innerContent}</${tag}>`;
|
|
14691
|
+
case 8 /* TNodeType.ElementContainer */:
|
|
14692
|
+
return '<!-- ng-container -->';
|
|
14693
|
+
case 4 /* TNodeType.Container */:
|
|
14694
|
+
return '<!-- container -->';
|
|
14695
|
+
default:
|
|
14696
|
+
const typeAsString = getFriendlyStringFromTNodeType(tNode.type);
|
|
14697
|
+
return `#node(${typeAsString})`;
|
|
14698
|
+
}
|
|
14699
|
+
}
|
|
14700
|
+
/**
|
|
14701
|
+
* Converts an RNode to a helpful readable string value for use in error messages
|
|
14702
|
+
*
|
|
14703
|
+
* @param rNode a given RNode
|
|
14704
|
+
* @param innerContent the content of the node
|
|
14705
|
+
* @returns string
|
|
14706
|
+
*/
|
|
14707
|
+
function describeRNode(rNode, innerContent = '…') {
|
|
14708
|
+
const node = rNode;
|
|
14709
|
+
switch (node.nodeType) {
|
|
14710
|
+
case Node.ELEMENT_NODE:
|
|
14711
|
+
const tag = node.tagName.toLowerCase();
|
|
14712
|
+
const attrs = stringifyRNodeAttrs(node);
|
|
14713
|
+
return `<${tag}${attrs ? ' ' + attrs : ''}>${innerContent}</${tag}>`;
|
|
14714
|
+
case Node.TEXT_NODE:
|
|
14715
|
+
const content = node.textContent ? shorten(node.textContent) : '';
|
|
14716
|
+
return `#text${content ? `(${content})` : ''}`;
|
|
14717
|
+
case Node.COMMENT_NODE:
|
|
14718
|
+
return `<!-- ${shorten(node.textContent ?? '')} -->`;
|
|
14719
|
+
default:
|
|
14720
|
+
return `#node(${node.nodeType})`;
|
|
14721
|
+
}
|
|
14722
|
+
}
|
|
14723
|
+
/**
|
|
14724
|
+
* Builds the string containing the expected DOM present given the LView and TNode
|
|
14725
|
+
* values for a readable error message
|
|
14726
|
+
*
|
|
14727
|
+
* @param lView the lView containing the DOM
|
|
14728
|
+
* @param tNode the tNode
|
|
14729
|
+
* @param isViewContainerAnchor boolean
|
|
14730
|
+
* @returns string
|
|
14731
|
+
*/
|
|
14732
|
+
function describeExpectedDom(lView, tNode, isViewContainerAnchor) {
|
|
14733
|
+
const spacer = ' ';
|
|
14734
|
+
let content = '';
|
|
14735
|
+
if (tNode.prev) {
|
|
14736
|
+
content += spacer + '…\n';
|
|
14737
|
+
content += spacer + describeTNode(tNode.prev) + '\n';
|
|
14738
|
+
}
|
|
14739
|
+
else if (tNode.type && tNode.type & 12 /* TNodeType.AnyContainer */) {
|
|
14740
|
+
content += spacer + '…\n';
|
|
14741
|
+
}
|
|
14742
|
+
if (isViewContainerAnchor) {
|
|
14743
|
+
content += spacer + describeTNode(tNode) + '\n';
|
|
14744
|
+
content += spacer + `<!-- container --> ${AT_THIS_LOCATION}\n`;
|
|
14745
|
+
}
|
|
14746
|
+
else {
|
|
14747
|
+
content += spacer + describeTNode(tNode) + ` ${AT_THIS_LOCATION}\n`;
|
|
14748
|
+
}
|
|
14749
|
+
content += spacer + '…\n';
|
|
14750
|
+
const parentRNode = tNode.type ? getParentRElement(lView[TVIEW], tNode, lView) : null;
|
|
14751
|
+
if (parentRNode) {
|
|
14752
|
+
content = describeRNode(parentRNode, '\n' + content);
|
|
14753
|
+
}
|
|
14754
|
+
return content;
|
|
14755
|
+
}
|
|
14756
|
+
/**
|
|
14757
|
+
* Builds the string containing the DOM present around a given RNode for a
|
|
14758
|
+
* readable error message
|
|
14759
|
+
*
|
|
14760
|
+
* @param node the RNode
|
|
14761
|
+
* @returns string
|
|
14762
|
+
*/
|
|
14763
|
+
function describeDomFromNode(node) {
|
|
14764
|
+
const spacer = ' ';
|
|
14765
|
+
let content = '';
|
|
14766
|
+
const currentNode = node;
|
|
14767
|
+
if (currentNode.previousSibling) {
|
|
14768
|
+
content += spacer + '…\n';
|
|
14769
|
+
content += spacer + describeRNode(currentNode.previousSibling) + '\n';
|
|
14770
|
+
}
|
|
14771
|
+
content += spacer + describeRNode(currentNode) + ` ${AT_THIS_LOCATION}\n`;
|
|
14772
|
+
if (node.nextSibling) {
|
|
14773
|
+
content += spacer + '…\n';
|
|
14774
|
+
}
|
|
14775
|
+
if (node.parentNode) {
|
|
14776
|
+
content = describeRNode(currentNode.parentNode, '\n' + content);
|
|
14777
|
+
}
|
|
14778
|
+
return content;
|
|
14779
|
+
}
|
|
14780
|
+
/**
|
|
14781
|
+
* Shortens the description of a given RNode by its type for readability
|
|
14782
|
+
*
|
|
14783
|
+
* @param nodeType the type of node
|
|
14784
|
+
* @param tagName the node tag name
|
|
14785
|
+
* @param textContent the text content in the node
|
|
14786
|
+
* @returns string
|
|
14787
|
+
*/
|
|
14788
|
+
function shortRNodeDescription(nodeType, tagName, textContent) {
|
|
14789
|
+
switch (nodeType) {
|
|
14790
|
+
case Node.ELEMENT_NODE:
|
|
14791
|
+
return `<${tagName.toLowerCase()}>`;
|
|
14792
|
+
case Node.TEXT_NODE:
|
|
14793
|
+
const content = textContent ? ` (with the "${shorten(textContent)}" content)` : '';
|
|
14794
|
+
return `a text node${content}`;
|
|
14795
|
+
case Node.COMMENT_NODE:
|
|
14796
|
+
return 'a comment node';
|
|
14797
|
+
default:
|
|
14798
|
+
return `#node(nodeType=${nodeType})`;
|
|
14799
|
+
}
|
|
14800
|
+
}
|
|
14801
|
+
/**
|
|
14802
|
+
* Builds the footer hydration error message
|
|
14803
|
+
*
|
|
14804
|
+
* @param componentClassName the name of the component class
|
|
14805
|
+
* @returns string
|
|
14806
|
+
*/
|
|
14807
|
+
function getHydrationErrorFooter(componentClassName) {
|
|
14808
|
+
const componentInfo = componentClassName ? `the "${componentClassName}"` : 'corresponding';
|
|
14809
|
+
return `To fix this problem:\n` +
|
|
14810
|
+
` * check ${componentInfo} component for hydration-related issues\n` +
|
|
14811
|
+
` * or skip hydration by adding the \`ngSkipHydration\` attribute ` +
|
|
14812
|
+
`to its host node in a template`;
|
|
14813
|
+
}
|
|
14814
|
+
/**
|
|
14815
|
+
* An attribute related note for hydration errors
|
|
14816
|
+
*/
|
|
14817
|
+
function getHydrationAttributeNote() {
|
|
14818
|
+
return 'Note: attributes are only displayed to better represent the DOM' +
|
|
14819
|
+
' but have no effect on hydration mismatches.\n\n';
|
|
14820
|
+
}
|
|
14821
|
+
// Node string utility functions
|
|
14822
|
+
/**
|
|
14823
|
+
* Strips all newlines out of a given string
|
|
14824
|
+
*
|
|
14825
|
+
* @param input a string to be cleared of new line characters
|
|
14826
|
+
* @returns
|
|
14827
|
+
*/
|
|
14828
|
+
function stripNewlines(input) {
|
|
14829
|
+
return input.replace(/\s+/gm, '');
|
|
14830
|
+
}
|
|
14831
|
+
/**
|
|
14832
|
+
* Reduces a string down to a maximum length of characters with ellipsis for readability
|
|
14833
|
+
*
|
|
14834
|
+
* @param input a string input
|
|
14835
|
+
* @param maxLength a maximum length in characters
|
|
14836
|
+
* @returns string
|
|
14837
|
+
*/
|
|
14838
|
+
function shorten(input, maxLength = 50) {
|
|
14839
|
+
if (!input) {
|
|
14840
|
+
return '';
|
|
14841
|
+
}
|
|
14842
|
+
input = stripNewlines(input);
|
|
14843
|
+
return input.length > maxLength ? `${input.substring(0, maxLength - 1)}…` : input;
|
|
14844
|
+
}
|
|
14845
|
+
|
|
14846
|
+
/**
|
|
14847
|
+
* Regexp that extracts a reference node information from the compressed node location.
|
|
14848
|
+
* The reference node is represented as either:
|
|
14849
|
+
* - a number which points to an LView slot
|
|
14850
|
+
* - the `b` char which indicates that the lookup should start from the `document.body`
|
|
14851
|
+
* - the `h` char to start lookup from the component host node (`lView[HOST]`)
|
|
14852
|
+
*/
|
|
14853
|
+
const REF_EXTRACTOR_REGEXP = new RegExp(`^(\\d+)*(${REFERENCE_NODE_BODY}|${REFERENCE_NODE_HOST})*(.*)`);
|
|
14854
|
+
/**
|
|
14855
|
+
* Helper function that takes a reference node location and a set of navigation steps
|
|
14856
|
+
* (from the reference node) to a target node and outputs a string that represents
|
|
14857
|
+
* a location.
|
|
14858
|
+
*
|
|
14859
|
+
* For example, given: referenceNode = 'b' (body) and path = ['firstChild', 'firstChild',
|
|
14860
|
+
* 'nextSibling'], the function returns: `bf2n`.
|
|
14861
|
+
*/
|
|
14862
|
+
function compressNodeLocation(referenceNode, path) {
|
|
14863
|
+
const result = [referenceNode];
|
|
14864
|
+
for (const segment of path) {
|
|
14865
|
+
const lastIdx = result.length - 1;
|
|
14866
|
+
if (lastIdx > 0 && result[lastIdx - 1] === segment) {
|
|
14867
|
+
// An empty string in a count slot represents 1 occurrence of an instruction.
|
|
14868
|
+
const value = (result[lastIdx] || 1);
|
|
14869
|
+
result[lastIdx] = value + 1;
|
|
14870
|
+
}
|
|
14871
|
+
else {
|
|
14872
|
+
// Adding a new segment to the path.
|
|
14873
|
+
// Using an empty string in a counter field to avoid encoding `1`s
|
|
14874
|
+
// into the path, since they are implicit (e.g. `f1n1` vs `fn`), so
|
|
14875
|
+
// it's enough to have a single char in this case.
|
|
14876
|
+
result.push(segment, '');
|
|
14877
|
+
}
|
|
14410
14878
|
}
|
|
14879
|
+
return result.join('');
|
|
14880
|
+
}
|
|
14881
|
+
/**
|
|
14882
|
+
* Helper function that reverts the `compressNodeLocation` and transforms a given
|
|
14883
|
+
* string into an array where at 0th position there is a reference node info and
|
|
14884
|
+
* after that it contains information (in pairs) about a navigation step and the
|
|
14885
|
+
* number of repetitions.
|
|
14886
|
+
*
|
|
14887
|
+
* For example, the path like 'bf2n' will be transformed to:
|
|
14888
|
+
* ['b', 'firstChild', 2, 'nextSibling', 1].
|
|
14889
|
+
*
|
|
14890
|
+
* This information is later consumed by the code that navigates the DOM to find
|
|
14891
|
+
* a given node by its location.
|
|
14892
|
+
*/
|
|
14893
|
+
function decompressNodeLocation(path) {
|
|
14894
|
+
const matches = path.match(REF_EXTRACTOR_REGEXP);
|
|
14895
|
+
const [_, refNodeId, refNodeName, rest] = matches;
|
|
14896
|
+
// If a reference node is represented by an index, transform it to a number.
|
|
14897
|
+
const ref = refNodeId ? parseInt(refNodeId, 10) : refNodeName;
|
|
14898
|
+
const steps = [];
|
|
14899
|
+
// Match all segments in a path.
|
|
14900
|
+
for (const [_, step, count] of rest.matchAll(/(f|n)(\d*)/g)) {
|
|
14901
|
+
const repeat = parseInt(count, 10) || 1;
|
|
14902
|
+
steps.push(step, repeat);
|
|
14903
|
+
}
|
|
14904
|
+
return [ref, ...steps];
|
|
14411
14905
|
}
|
|
14412
14906
|
|
|
14413
14907
|
/** Whether current TNode is a first node in an <ng-container>. */
|
|
14414
14908
|
function isFirstElementInNgContainer(tNode) {
|
|
14415
14909
|
return !tNode.prev && tNode.parent?.type === 8 /* TNodeType.ElementContainer */;
|
|
14416
14910
|
}
|
|
14911
|
+
/** Returns an instruction index (subtracting HEADER_OFFSET). */
|
|
14912
|
+
function getNoOffsetIndex(tNode) {
|
|
14913
|
+
return tNode.index - HEADER_OFFSET;
|
|
14914
|
+
}
|
|
14417
14915
|
/**
|
|
14418
14916
|
* Locate a node in DOM tree that corresponds to a given TNode.
|
|
14419
14917
|
*
|
|
@@ -14425,7 +14923,13 @@ function isFirstElementInNgContainer(tNode) {
|
|
|
14425
14923
|
*/
|
|
14426
14924
|
function locateNextRNode(hydrationInfo, tView, lView, tNode) {
|
|
14427
14925
|
let native = null;
|
|
14428
|
-
|
|
14926
|
+
const noOffsetIndex = getNoOffsetIndex(tNode);
|
|
14927
|
+
const nodes = hydrationInfo.data[NODES];
|
|
14928
|
+
if (nodes?.[noOffsetIndex]) {
|
|
14929
|
+
// We know the exact location of the node.
|
|
14930
|
+
native = locateRNodeByPath(nodes[noOffsetIndex], lView);
|
|
14931
|
+
}
|
|
14932
|
+
else if (tView.firstChild === tNode) {
|
|
14429
14933
|
// We create a first node in this view, so we use a reference
|
|
14430
14934
|
// to the first child in this DOM segment.
|
|
14431
14935
|
native = hydrationInfo.firstChild;
|
|
@@ -14438,7 +14942,7 @@ function locateNextRNode(hydrationInfo, tView, lView, tNode) {
|
|
|
14438
14942
|
assertDefined(previousTNode, 'Unexpected state: current TNode does not have a connection ' +
|
|
14439
14943
|
'to the previous node or a parent node.');
|
|
14440
14944
|
if (isFirstElementInNgContainer(tNode)) {
|
|
14441
|
-
const noOffsetParentIndex = tNode.parent
|
|
14945
|
+
const noOffsetParentIndex = getNoOffsetIndex(tNode.parent);
|
|
14442
14946
|
native = getSegmentHead(hydrationInfo, noOffsetParentIndex);
|
|
14443
14947
|
}
|
|
14444
14948
|
else {
|
|
@@ -14452,7 +14956,7 @@ function locateNextRNode(hydrationInfo, tView, lView, tNode) {
|
|
|
14452
14956
|
// represented in the DOM as `<div></div>...<!--container-->`.
|
|
14453
14957
|
// In this case, there are nodes *after* this element and we need to skip
|
|
14454
14958
|
// all of them to reach an element that we are looking for.
|
|
14455
|
-
const noOffsetPrevSiblingIndex = previousTNode
|
|
14959
|
+
const noOffsetPrevSiblingIndex = getNoOffsetIndex(previousTNode);
|
|
14456
14960
|
const segmentHead = getSegmentHead(hydrationInfo, noOffsetPrevSiblingIndex);
|
|
14457
14961
|
if (previousTNode.type === 2 /* TNodeType.Element */ && segmentHead) {
|
|
14458
14962
|
const numRootNodesToSkip = calcSerializedContainerSize(hydrationInfo, noOffsetPrevSiblingIndex);
|
|
@@ -14480,6 +14984,183 @@ function siblingAfter(skip, from) {
|
|
|
14480
14984
|
}
|
|
14481
14985
|
return currentNode;
|
|
14482
14986
|
}
|
|
14987
|
+
/**
|
|
14988
|
+
* Helper function to produce a string representation of the navigation steps
|
|
14989
|
+
* (in terms of `nextSibling` and `firstChild` navigations). Used in error
|
|
14990
|
+
* messages in dev mode.
|
|
14991
|
+
*/
|
|
14992
|
+
function stringifyNavigationInstructions(instructions) {
|
|
14993
|
+
const container = [];
|
|
14994
|
+
let i = 0;
|
|
14995
|
+
while (i < instructions.length) {
|
|
14996
|
+
const step = instructions[i++];
|
|
14997
|
+
const repeat = instructions[i++];
|
|
14998
|
+
for (let r = 0; r < repeat; r++) {
|
|
14999
|
+
container.push(step === NodeNavigationStep.FirstChild ? 'firstChild' : 'nextSibling');
|
|
15000
|
+
}
|
|
15001
|
+
}
|
|
15002
|
+
return container.join('.');
|
|
15003
|
+
}
|
|
15004
|
+
/**
|
|
15005
|
+
* Helper function that navigates from a starting point node (the `from` node)
|
|
15006
|
+
* using provided set of navigation instructions (within `path` argument).
|
|
15007
|
+
*/
|
|
15008
|
+
function navigateToNode(from, instructions) {
|
|
15009
|
+
let node = from;
|
|
15010
|
+
let i = 0;
|
|
15011
|
+
while (i < instructions.length) {
|
|
15012
|
+
const step = instructions[i++];
|
|
15013
|
+
const repeat = instructions[i++];
|
|
15014
|
+
for (let r = 0; r < repeat; r++) {
|
|
15015
|
+
if (ngDevMode && !node) {
|
|
15016
|
+
throw nodeNotFoundAtPathError(from, stringifyNavigationInstructions(instructions));
|
|
15017
|
+
}
|
|
15018
|
+
switch (step) {
|
|
15019
|
+
case NodeNavigationStep.FirstChild:
|
|
15020
|
+
node = node.firstChild;
|
|
15021
|
+
break;
|
|
15022
|
+
case NodeNavigationStep.NextSibling:
|
|
15023
|
+
node = node.nextSibling;
|
|
15024
|
+
break;
|
|
15025
|
+
}
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
if (ngDevMode && !node) {
|
|
15029
|
+
throw nodeNotFoundAtPathError(from, stringifyNavigationInstructions(instructions));
|
|
15030
|
+
}
|
|
15031
|
+
return node;
|
|
15032
|
+
}
|
|
15033
|
+
/**
|
|
15034
|
+
* Locates an RNode given a set of navigation instructions (which also contains
|
|
15035
|
+
* a starting point node info).
|
|
15036
|
+
*/
|
|
15037
|
+
function locateRNodeByPath(path, lView) {
|
|
15038
|
+
const [referenceNode, ...navigationInstructions] = decompressNodeLocation(path);
|
|
15039
|
+
let ref;
|
|
15040
|
+
if (referenceNode === REFERENCE_NODE_HOST) {
|
|
15041
|
+
ref = lView[0];
|
|
15042
|
+
}
|
|
15043
|
+
else if (referenceNode === REFERENCE_NODE_BODY) {
|
|
15044
|
+
ref = ɵɵresolveBody(lView[0]);
|
|
15045
|
+
}
|
|
15046
|
+
else {
|
|
15047
|
+
const parentElementId = Number(referenceNode);
|
|
15048
|
+
ref = unwrapRNode(lView[parentElementId + HEADER_OFFSET]);
|
|
15049
|
+
}
|
|
15050
|
+
return navigateToNode(ref, navigationInstructions);
|
|
15051
|
+
}
|
|
15052
|
+
/**
|
|
15053
|
+
* Generate a list of DOM navigation operations to get from node `start` to node `finish`.
|
|
15054
|
+
*
|
|
15055
|
+
* Note: assumes that node `start` occurs before node `finish` in an in-order traversal of the DOM
|
|
15056
|
+
* tree. That is, we should be able to get from `start` to `finish` purely by using `.firstChild`
|
|
15057
|
+
* and `.nextSibling` operations.
|
|
15058
|
+
*/
|
|
15059
|
+
function navigateBetween(start, finish) {
|
|
15060
|
+
if (start === finish) {
|
|
15061
|
+
return [];
|
|
15062
|
+
}
|
|
15063
|
+
else if (start.parentElement == null || finish.parentElement == null) {
|
|
15064
|
+
return null;
|
|
15065
|
+
}
|
|
15066
|
+
else if (start.parentElement === finish.parentElement) {
|
|
15067
|
+
return navigateBetweenSiblings(start, finish);
|
|
15068
|
+
}
|
|
15069
|
+
else {
|
|
15070
|
+
// `finish` is a child of its parent, so the parent will always have a child.
|
|
15071
|
+
const parent = finish.parentElement;
|
|
15072
|
+
const parentPath = navigateBetween(start, parent);
|
|
15073
|
+
const childPath = navigateBetween(parent.firstChild, finish);
|
|
15074
|
+
if (!parentPath || !childPath)
|
|
15075
|
+
return null;
|
|
15076
|
+
return [
|
|
15077
|
+
// First navigate to `finish`'s parent
|
|
15078
|
+
...parentPath,
|
|
15079
|
+
// Then to its first child.
|
|
15080
|
+
NodeNavigationStep.FirstChild,
|
|
15081
|
+
// And finally from that node to `finish` (maybe a no-op if we're already there).
|
|
15082
|
+
...childPath,
|
|
15083
|
+
];
|
|
15084
|
+
}
|
|
15085
|
+
}
|
|
15086
|
+
/**
|
|
15087
|
+
* Calculates a path between 2 sibling nodes (generates a number of `NextSibling` navigations).
|
|
15088
|
+
*/
|
|
15089
|
+
function navigateBetweenSiblings(start, finish) {
|
|
15090
|
+
const nav = [];
|
|
15091
|
+
let node = null;
|
|
15092
|
+
for (node = start; node != null && node !== finish; node = node.nextSibling) {
|
|
15093
|
+
nav.push(NodeNavigationStep.NextSibling);
|
|
15094
|
+
}
|
|
15095
|
+
return node === null ? [] : nav;
|
|
15096
|
+
}
|
|
15097
|
+
/**
|
|
15098
|
+
* Calculates a path between 2 nodes in terms of `nextSibling` and `firstChild`
|
|
15099
|
+
* navigations:
|
|
15100
|
+
* - the `from` node is a known node, used as an starting point for the lookup
|
|
15101
|
+
* (the `fromNodeName` argument is a string representation of the node).
|
|
15102
|
+
* - the `to` node is a node that the runtime logic would be looking up,
|
|
15103
|
+
* using the path generated by this function.
|
|
15104
|
+
*/
|
|
15105
|
+
function calcPathBetween(from, to, fromNodeName) {
|
|
15106
|
+
const path = navigateBetween(from, to);
|
|
15107
|
+
return path === null ? null : compressNodeLocation(fromNodeName, path);
|
|
15108
|
+
}
|
|
15109
|
+
/**
|
|
15110
|
+
* Invoked at serialization time (on the server) when a set of navigation
|
|
15111
|
+
* instructions needs to be generated for a TNode.
|
|
15112
|
+
*/
|
|
15113
|
+
function calcPathForNode(tNode, lView) {
|
|
15114
|
+
const parentTNode = tNode.parent;
|
|
15115
|
+
let parentIndex;
|
|
15116
|
+
let parentRNode;
|
|
15117
|
+
let referenceNodeName;
|
|
15118
|
+
if (parentTNode === null) {
|
|
15119
|
+
// No parent TNode - use host element as a reference node.
|
|
15120
|
+
parentIndex = referenceNodeName = REFERENCE_NODE_HOST;
|
|
15121
|
+
parentRNode = lView[HOST];
|
|
15122
|
+
}
|
|
15123
|
+
else {
|
|
15124
|
+
// Use parent TNode as a reference node.
|
|
15125
|
+
parentIndex = parentTNode.index;
|
|
15126
|
+
parentRNode = unwrapRNode(lView[parentIndex]);
|
|
15127
|
+
referenceNodeName = renderStringify(parentIndex - HEADER_OFFSET);
|
|
15128
|
+
}
|
|
15129
|
+
let rNode = unwrapRNode(lView[tNode.index]);
|
|
15130
|
+
if (tNode.type & 12 /* TNodeType.AnyContainer */) {
|
|
15131
|
+
// For <ng-container> nodes, instead of serializing a reference
|
|
15132
|
+
// to the anchor comment node, serialize a location of the first
|
|
15133
|
+
// DOM element. Paired with the container size (serialized as a part
|
|
15134
|
+
// of `ngh.containers`), it should give enough information for runtime
|
|
15135
|
+
// to hydrate nodes in this container.
|
|
15136
|
+
const firstRNode = getFirstNativeNode(lView, tNode);
|
|
15137
|
+
// If container is not empty, use a reference to the first element,
|
|
15138
|
+
// otherwise, rNode would point to an anchor comment node.
|
|
15139
|
+
if (firstRNode) {
|
|
15140
|
+
rNode = firstRNode;
|
|
15141
|
+
}
|
|
15142
|
+
}
|
|
15143
|
+
let path = calcPathBetween(parentRNode, rNode, referenceNodeName);
|
|
15144
|
+
if (path === null && parentRNode !== rNode) {
|
|
15145
|
+
// Searching for a path between elements within a host node failed.
|
|
15146
|
+
// Trying to find a path to an element starting from the `document.body` instead.
|
|
15147
|
+
//
|
|
15148
|
+
// Important note: this type of reference is relatively unstable, since Angular
|
|
15149
|
+
// may not be able to control parts of the page that the runtime logic navigates
|
|
15150
|
+
// through. This is mostly needed to cover "portals" use-case (like menus, dialog boxes,
|
|
15151
|
+
// etc), where nodes are content-projected (including direct DOM manipulations) outside
|
|
15152
|
+
// of the host node. The better solution is to provide APIs to work with "portals",
|
|
15153
|
+
// at which point this code path would not be needed.
|
|
15154
|
+
const body = parentRNode.ownerDocument.body;
|
|
15155
|
+
path = calcPathBetween(body, rNode, REFERENCE_NODE_BODY);
|
|
15156
|
+
if (path === null) {
|
|
15157
|
+
// If the path is still empty, it's likely that this node is detached and
|
|
15158
|
+
// won't be found during hydration.
|
|
15159
|
+
throw nodeNotFoundError(lView, tNode);
|
|
15160
|
+
}
|
|
15161
|
+
}
|
|
15162
|
+
return path;
|
|
15163
|
+
}
|
|
14483
15164
|
|
|
14484
15165
|
function templateFirstCreatePass(index, tView, lView, templateFn, decls, vars, tagName, attrsIndex, localRefsIndex) {
|
|
14485
15166
|
ngDevMode && assertFirstCreatePass(tView);
|
|
@@ -14556,7 +15237,7 @@ function createContainerAnchorImpl(tView, lView, tNode, index) {
|
|
|
14556
15237
|
*/
|
|
14557
15238
|
function locateOrCreateContainerAnchorImpl(tView, lView, tNode, index) {
|
|
14558
15239
|
const hydrationInfo = lView[HYDRATION];
|
|
14559
|
-
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1();
|
|
15240
|
+
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1() || isDisconnectedNode(hydrationInfo, index);
|
|
14560
15241
|
lastNodeWasCreated(isNodeCreationMode);
|
|
14561
15242
|
// Regular creation mode.
|
|
14562
15243
|
if (isNodeCreationMode) {
|
|
@@ -14565,11 +15246,8 @@ function locateOrCreateContainerAnchorImpl(tView, lView, tNode, index) {
|
|
|
14565
15246
|
// Hydration mode, looking up existing elements in DOM.
|
|
14566
15247
|
const currentRNode = locateNextRNode(hydrationInfo, tView, lView, tNode);
|
|
14567
15248
|
ngDevMode && validateNodeExists(currentRNode);
|
|
15249
|
+
setSegmentHead(hydrationInfo, index, currentRNode);
|
|
14568
15250
|
const viewContainerSize = calcSerializedContainerSize(hydrationInfo, index);
|
|
14569
|
-
// If this container is non-empty, store the first node as a segment head,
|
|
14570
|
-
// otherwise, this node is an anchor and segment head doesn't exist (thus `null`).
|
|
14571
|
-
const segmentHead = viewContainerSize > 0 ? currentRNode : null;
|
|
14572
|
-
setSegmentHead(hydrationInfo, index, segmentHead);
|
|
14573
15251
|
const comment = siblingAfter(viewContainerSize, currentRNode);
|
|
14574
15252
|
if (ngDevMode) {
|
|
14575
15253
|
validateMatchingNode(comment, Node.COMMENT_NODE, null, lView, tNode);
|
|
@@ -14825,7 +15503,7 @@ let _locateOrCreateElementNode = (tView, lView, tNode, renderer, name, index) =>
|
|
|
14825
15503
|
*/
|
|
14826
15504
|
function locateOrCreateElementNodeImpl(tView, lView, tNode, renderer, name, index) {
|
|
14827
15505
|
const hydrationInfo = lView[HYDRATION];
|
|
14828
|
-
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1();
|
|
15506
|
+
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1() || isDisconnectedNode(hydrationInfo, index);
|
|
14829
15507
|
lastNodeWasCreated(isNodeCreationMode);
|
|
14830
15508
|
// Regular creation mode.
|
|
14831
15509
|
if (isNodeCreationMode) {
|
|
@@ -14849,10 +15527,17 @@ function locateOrCreateElementNodeImpl(tView, lView, tNode, renderer, name, inde
|
|
|
14849
15527
|
// Checks if the skip hydration attribute is present during hydration so we know to
|
|
14850
15528
|
// skip attempting to hydrate this block.
|
|
14851
15529
|
if (hydrationInfo && hasNgSkipHydrationAttr(tNode)) {
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
15530
|
+
if (isComponentHost(tNode)) {
|
|
15531
|
+
enterSkipHydrationBlock(tNode);
|
|
15532
|
+
// Since this isn't hydratable, we need to empty the node
|
|
15533
|
+
// so there's no duplicate content after render
|
|
15534
|
+
clearElementContents(renderer, native);
|
|
15535
|
+
}
|
|
15536
|
+
else if (ngDevMode) {
|
|
15537
|
+
// If this is not a component host, throw an error.
|
|
15538
|
+
// Hydration can be skipped on per-component basis only.
|
|
15539
|
+
throw invalidSkipHydrationHost(native);
|
|
15540
|
+
}
|
|
14856
15541
|
}
|
|
14857
15542
|
return native;
|
|
14858
15543
|
}
|
|
@@ -14986,10 +15671,7 @@ function locateOrCreateElementContainerNode(tView, lView, tNode, index) {
|
|
|
14986
15671
|
ngDevMode &&
|
|
14987
15672
|
assertNumber(ngContainerSize, 'Unexpected state: hydrating an <ng-container>, ' +
|
|
14988
15673
|
'but no hydration info is available.');
|
|
14989
|
-
|
|
14990
|
-
// otherwise, this node is an anchor and segment head doesn't exist (thus `null`).
|
|
14991
|
-
const segmentHead = ngContainerSize > 0 ? currentRNode : null;
|
|
14992
|
-
setSegmentHead(hydrationInfo, index, segmentHead);
|
|
15674
|
+
setSegmentHead(hydrationInfo, index, currentRNode);
|
|
14993
15675
|
comment = siblingAfter(ngContainerSize, currentRNode);
|
|
14994
15676
|
if (ngDevMode) {
|
|
14995
15677
|
validateMatchingNode(comment, Node.COMMENT_NODE, null, lView, tNode);
|
|
@@ -15371,7 +16053,10 @@ function ɵɵprojection(nodeIndex, selectorIndex = 0, attrs) {
|
|
|
15371
16053
|
tProjectionNode.projection = selectorIndex;
|
|
15372
16054
|
// `<ng-content>` has no content
|
|
15373
16055
|
setCurrentTNodeAsNotParent();
|
|
15374
|
-
|
|
16056
|
+
const hydrationInfo = lView[HYDRATION];
|
|
16057
|
+
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1();
|
|
16058
|
+
if (isNodeCreationMode &&
|
|
16059
|
+
(tProjectionNode.flags & 32 /* TNodeFlags.isDetached */) !== 32 /* TNodeFlags.isDetached */) {
|
|
15375
16060
|
// re-distribution of projectable nodes is stored on a component's view level
|
|
15376
16061
|
applyProjection(tView, lView, tProjectionNode);
|
|
15377
16062
|
}
|
|
@@ -17334,7 +18019,7 @@ function ɵɵtext(index, value = '') {
|
|
|
17334
18019
|
const tNode = tView.firstCreatePass ?
|
|
17335
18020
|
getOrCreateTNode(tView, adjustedIndex, 1 /* TNodeType.Text */, value, null) :
|
|
17336
18021
|
tView.data[adjustedIndex];
|
|
17337
|
-
const textNative = _locateOrCreateTextNode(tView, lView, tNode, value);
|
|
18022
|
+
const textNative = _locateOrCreateTextNode(tView, lView, tNode, value, index);
|
|
17338
18023
|
lView[adjustedIndex] = textNative;
|
|
17339
18024
|
if (wasLastNodeCreated()) {
|
|
17340
18025
|
appendChild(tView, lView, textNative, tNode);
|
|
@@ -17342,7 +18027,7 @@ function ɵɵtext(index, value = '') {
|
|
|
17342
18027
|
// Text nodes are self closing.
|
|
17343
18028
|
setCurrentTNode(tNode, false);
|
|
17344
18029
|
}
|
|
17345
|
-
let _locateOrCreateTextNode = (tView, lView, tNode, value) => {
|
|
18030
|
+
let _locateOrCreateTextNode = (tView, lView, tNode, value, index) => {
|
|
17346
18031
|
lastNodeWasCreated(true);
|
|
17347
18032
|
return createTextNode(lView[RENDERER], value);
|
|
17348
18033
|
};
|
|
@@ -17350,9 +18035,9 @@ let _locateOrCreateTextNode = (tView, lView, tNode, value) => {
|
|
|
17350
18035
|
* Enables hydration code path (to lookup existing elements in DOM)
|
|
17351
18036
|
* in addition to the regular creation mode of text nodes.
|
|
17352
18037
|
*/
|
|
17353
|
-
function locateOrCreateTextNodeImpl(tView, lView, tNode, value) {
|
|
18038
|
+
function locateOrCreateTextNodeImpl(tView, lView, tNode, value, index) {
|
|
17354
18039
|
const hydrationInfo = lView[HYDRATION];
|
|
17355
|
-
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1();
|
|
18040
|
+
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock$1() || isDisconnectedNode(hydrationInfo, index);
|
|
17356
18041
|
lastNodeWasCreated(isNodeCreationMode);
|
|
17357
18042
|
// Regular creation mode.
|
|
17358
18043
|
if (isNodeCreationMode) {
|
|
@@ -22460,6 +23145,54 @@ function removeDehydratedView(dehydratedView, renderer) {
|
|
|
22460
23145
|
}
|
|
22461
23146
|
}
|
|
22462
23147
|
}
|
|
23148
|
+
/**
|
|
23149
|
+
* Walks over all views within this LContainer invokes dehydrated views
|
|
23150
|
+
* cleanup function for each one.
|
|
23151
|
+
*/
|
|
23152
|
+
function cleanupLContainer(lContainer) {
|
|
23153
|
+
removeDehydratedViews(lContainer);
|
|
23154
|
+
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
|
|
23155
|
+
cleanupLView(lContainer[i]);
|
|
23156
|
+
}
|
|
23157
|
+
}
|
|
23158
|
+
/**
|
|
23159
|
+
* Walks over `LContainer`s and components registered within
|
|
23160
|
+
* this LView and invokes dehydrated views cleanup function for each one.
|
|
23161
|
+
*/
|
|
23162
|
+
function cleanupLView(lView) {
|
|
23163
|
+
const tView = lView[TVIEW];
|
|
23164
|
+
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
|
|
23165
|
+
if (isLContainer(lView[i])) {
|
|
23166
|
+
const lContainer = lView[i];
|
|
23167
|
+
cleanupLContainer(lContainer);
|
|
23168
|
+
}
|
|
23169
|
+
else if (Array.isArray(lView[i])) {
|
|
23170
|
+
// This is a component, enter the `cleanupLView` recursively.
|
|
23171
|
+
cleanupLView(lView[i]);
|
|
23172
|
+
}
|
|
23173
|
+
}
|
|
23174
|
+
}
|
|
23175
|
+
/**
|
|
23176
|
+
* Walks over all views registered within the ApplicationRef and removes
|
|
23177
|
+
* all dehydrated views from all `LContainer`s along the way.
|
|
23178
|
+
*/
|
|
23179
|
+
function cleanupDehydratedViews(appRef) {
|
|
23180
|
+
// Wait once an app becomes stable and cleanup all views that
|
|
23181
|
+
// were not claimed during the application bootstrap process.
|
|
23182
|
+
// The timing is similar to when we kick off serialization on the server.
|
|
23183
|
+
return appRef.isStable.pipe(first((isStable) => isStable)).toPromise().then(() => {
|
|
23184
|
+
const viewRefs = appRef._views;
|
|
23185
|
+
for (const viewRef of viewRefs) {
|
|
23186
|
+
const lView = getComponentLViewForHydration(viewRef);
|
|
23187
|
+
// An `lView` might be `null` if a `ViewRef` represents
|
|
23188
|
+
// an embedded view (not a component view).
|
|
23189
|
+
if (lView !== null && lView[HOST] !== null) {
|
|
23190
|
+
cleanupLView(lView);
|
|
23191
|
+
ngDevMode && ngDevMode.dehydratedViewsCleanupRuns++;
|
|
23192
|
+
}
|
|
23193
|
+
}
|
|
23194
|
+
});
|
|
23195
|
+
}
|
|
22463
23196
|
|
|
22464
23197
|
/**
|
|
22465
23198
|
* Given a current DOM node and a serialized information about the views
|
|
@@ -22469,20 +23202,24 @@ function removeDehydratedView(dehydratedView, renderer) {
|
|
|
22469
23202
|
function locateDehydratedViewsInContainer(currentRNode, serializedViews) {
|
|
22470
23203
|
const dehydratedViews = [];
|
|
22471
23204
|
for (const serializedView of serializedViews) {
|
|
22472
|
-
|
|
22473
|
-
|
|
22474
|
-
|
|
22475
|
-
|
|
22476
|
-
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
23205
|
+
// Repeats a view multiple times as needed, based on the serialized information
|
|
23206
|
+
// (for example, for *ngFor-produced views).
|
|
23207
|
+
for (let i = 0; i < (serializedView[MULTIPLIER] ?? 1); i++) {
|
|
23208
|
+
const view = {
|
|
23209
|
+
data: serializedView,
|
|
23210
|
+
firstChild: null,
|
|
23211
|
+
};
|
|
23212
|
+
if (serializedView[NUM_ROOT_NODES] > 0) {
|
|
23213
|
+
// Keep reference to the first node in this view,
|
|
23214
|
+
// so it can be accessed while invoking template instructions.
|
|
23215
|
+
view.firstChild = currentRNode;
|
|
23216
|
+
// Move over to the next node after this view, which can
|
|
23217
|
+
// either be a first node of the next view or an anchor comment
|
|
23218
|
+
// node after the last view in a container.
|
|
23219
|
+
currentRNode = siblingAfter(serializedView[NUM_ROOT_NODES], currentRNode);
|
|
23220
|
+
}
|
|
23221
|
+
dehydratedViews.push(view);
|
|
22484
23222
|
}
|
|
22485
|
-
dehydratedViews.push(view);
|
|
22486
23223
|
}
|
|
22487
23224
|
return [currentRNode, dehydratedViews];
|
|
22488
23225
|
}
|
|
@@ -22863,21 +23600,22 @@ function locateOrCreateAnchorNode(lContainer, hostLView, hostTNode, slotValue) {
|
|
|
22863
23600
|
if (lContainer[NATIVE] && lContainer[DEHYDRATED_VIEWS])
|
|
22864
23601
|
return;
|
|
22865
23602
|
const hydrationInfo = hostLView[HYDRATION];
|
|
22866
|
-
const
|
|
23603
|
+
const noOffsetIndex = hostTNode.index - HEADER_OFFSET;
|
|
23604
|
+
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock(hostTNode) ||
|
|
23605
|
+
isDisconnectedNode(hydrationInfo, noOffsetIndex);
|
|
22867
23606
|
// Regular creation mode.
|
|
22868
23607
|
if (isNodeCreationMode) {
|
|
22869
23608
|
return createAnchorNode(lContainer, hostLView, hostTNode, slotValue);
|
|
22870
23609
|
}
|
|
22871
23610
|
// Hydration mode, looking up an anchor node and dehydrated views in DOM.
|
|
22872
|
-
const
|
|
22873
|
-
const
|
|
22874
|
-
const serializedViews = hydrationInfo.data[CONTAINERS]?.[index];
|
|
23611
|
+
const currentRNode = getSegmentHead(hydrationInfo, noOffsetIndex);
|
|
23612
|
+
const serializedViews = hydrationInfo.data[CONTAINERS]?.[noOffsetIndex];
|
|
22875
23613
|
ngDevMode &&
|
|
22876
23614
|
assertDefined(serializedViews, 'Unexpected state: no hydration info available for a given TNode, ' +
|
|
22877
23615
|
'which represents a view container.');
|
|
22878
23616
|
const [commentNode, dehydratedViews] = locateDehydratedViewsInContainer(currentRNode, serializedViews);
|
|
22879
23617
|
if (ngDevMode) {
|
|
22880
|
-
validateMatchingNode(commentNode, Node.COMMENT_NODE, null, hostLView, hostTNode);
|
|
23618
|
+
validateMatchingNode(commentNode, Node.COMMENT_NODE, null, hostLView, hostTNode, true);
|
|
22881
23619
|
// Do not throw in case this node is already claimed (thus `false` as a second
|
|
22882
23620
|
// argument). If this container is created based on an `<ng-template>`, the comment
|
|
22883
23621
|
// node would be already claimed from the `template` instruction. If an element acts
|
|
@@ -24715,10 +25453,11 @@ class TestBedCompiler {
|
|
|
24715
25453
|
}
|
|
24716
25454
|
}
|
|
24717
25455
|
queueTypesFromModulesArray(arr) {
|
|
24718
|
-
// Because we may encounter the same NgModule
|
|
24719
|
-
// NgModule
|
|
24720
|
-
// encountered. In some test setups, this caching
|
|
24721
|
-
|
|
25456
|
+
// Because we may encounter the same NgModule or a standalone Component while processing
|
|
25457
|
+
// the dependencies of an NgModule or a standalone Component, we cache them in this set so we
|
|
25458
|
+
// can skip ones that have already been seen encountered. In some test setups, this caching
|
|
25459
|
+
// resulted in 10X runtime improvement.
|
|
25460
|
+
const processedDefs = new Set();
|
|
24722
25461
|
const queueTypesFromModulesArrayRecur = (arr) => {
|
|
24723
25462
|
for (const value of arr) {
|
|
24724
25463
|
if (Array.isArray(value)) {
|
|
@@ -24726,10 +25465,10 @@ class TestBedCompiler {
|
|
|
24726
25465
|
}
|
|
24727
25466
|
else if (hasNgModuleDef(value)) {
|
|
24728
25467
|
const def = value.ɵmod;
|
|
24729
|
-
if (
|
|
25468
|
+
if (processedDefs.has(def)) {
|
|
24730
25469
|
continue;
|
|
24731
25470
|
}
|
|
24732
|
-
|
|
25471
|
+
processedDefs.add(def);
|
|
24733
25472
|
// Look through declarations, imports, and exports, and queue
|
|
24734
25473
|
// everything found there.
|
|
24735
25474
|
this.queueTypeArray(maybeUnwrapFn(def.declarations), value);
|
|
@@ -24742,6 +25481,10 @@ class TestBedCompiler {
|
|
|
24742
25481
|
else if (isStandaloneComponent(value)) {
|
|
24743
25482
|
this.queueType(value, null);
|
|
24744
25483
|
const def = getComponentDef(value);
|
|
25484
|
+
if (processedDefs.has(def)) {
|
|
25485
|
+
continue;
|
|
25486
|
+
}
|
|
25487
|
+
processedDefs.add(def);
|
|
24745
25488
|
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
|
|
24746
25489
|
dependencies.forEach((dependency) => {
|
|
24747
25490
|
// Note: in AOT, the `dependencies` might also contain regular
|