@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.
Files changed (59) hide show
  1. package/esm2020/src/application_tokens.mjs +33 -1
  2. package/esm2020/src/change_detection/change_detector_ref.mjs +4 -4
  3. package/esm2020/src/compiler/compiler_facade_interface.mjs +1 -1
  4. package/esm2020/src/core.mjs +2 -2
  5. package/esm2020/src/core_private_export.mjs +2 -1
  6. package/esm2020/src/core_reactivity_export_internal.mjs +1 -1
  7. package/esm2020/src/core_render3_private_export.mjs +1 -2
  8. package/esm2020/src/di/r3_injector.mjs +8 -1
  9. package/esm2020/src/errors.mjs +1 -1
  10. package/esm2020/src/hydration/annotate.mjs +118 -5
  11. package/esm2020/src/hydration/api.mjs +14 -1
  12. package/esm2020/src/hydration/cleanup.mjs +54 -3
  13. package/esm2020/src/hydration/compression.mjs +69 -0
  14. package/esm2020/src/hydration/error_handling.mjs +357 -15
  15. package/esm2020/src/hydration/interfaces.mjs +17 -1
  16. package/esm2020/src/hydration/node_lookup_utils.mjs +199 -7
  17. package/esm2020/src/hydration/utils.mjs +16 -3
  18. package/esm2020/src/hydration/views.mjs +19 -15
  19. package/esm2020/src/linker/destroy_ref.mjs +5 -2
  20. package/esm2020/src/linker/view_container_ref.mjs +8 -7
  21. package/esm2020/src/metadata/directives.mjs +8 -3
  22. package/esm2020/src/render3/instructions/element.mjs +16 -9
  23. package/esm2020/src/render3/instructions/element_container.mjs +2 -5
  24. package/esm2020/src/render3/instructions/element_validation.mjs +2 -2
  25. package/esm2020/src/render3/instructions/projection.mjs +7 -4
  26. package/esm2020/src/render3/instructions/template.mjs +4 -7
  27. package/esm2020/src/render3/instructions/text.mjs +6 -6
  28. package/esm2020/src/render3/interfaces/public_definitions.mjs +1 -1
  29. package/esm2020/src/render3/interfaces/type_checks.mjs +4 -1
  30. package/esm2020/src/render3/node_manipulation.mjs +2 -2
  31. package/esm2020/src/render3/node_selector_matcher.mjs +17 -5
  32. package/esm2020/src/render3/util/view_utils.mjs +12 -1
  33. package/esm2020/src/render3/view_ref.mjs +1 -1
  34. package/esm2020/src/signals/index.mjs +2 -1
  35. package/esm2020/src/signals/src/computed.mjs +3 -3
  36. package/esm2020/src/signals/src/effect.mjs +1 -3
  37. package/esm2020/src/signals/src/signal.mjs +3 -3
  38. package/esm2020/src/signals/src/watch.mjs +3 -3
  39. package/esm2020/src/signals/src/weak_ref.mjs +18 -2
  40. package/esm2020/src/util/ng_dev_mode.mjs +2 -1
  41. package/esm2020/src/version.mjs +1 -1
  42. package/esm2020/testing/src/logger.mjs +3 -3
  43. package/esm2020/testing/src/ng_zone_mock.mjs +3 -3
  44. package/esm2020/testing/src/test_bed_compiler.mjs +12 -7
  45. package/fesm2015/core.mjs +1066 -178
  46. package/fesm2015/core.mjs.map +1 -1
  47. package/fesm2015/testing.mjs +816 -68
  48. package/fesm2015/testing.mjs.map +1 -1
  49. package/fesm2020/core.mjs +1051 -170
  50. package/fesm2020/core.mjs.map +1 -1
  51. package/fesm2020/testing.mjs +810 -67
  52. package/fesm2020/testing.mjs.map +1 -1
  53. package/index.d.ts +128 -216
  54. package/package.json +1 -1
  55. package/schematics/migrations/relative-link-resolution/bundle.js +7 -7
  56. package/schematics/migrations/router-link-with-href/bundle.js +10 -10
  57. package/schematics/ng-generate/standalone-migration/bundle.js +473 -376
  58. package/schematics/ng-generate/standalone-migration/bundle.js.map +2 -2
  59. package/testing/index.d.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Angular v16.0.0-next.3
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 (isProjectionMode && item === 'class') {
2452
- item = attrs[i];
2453
- if (classIndexOf(item.toLowerCase(), cssClassToMatch, 0) !== -1) {
2454
- return true;
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.3');
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
- * Verifies whether a given node matches an expected criteria,
14385
- * based on internal data structure state.
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 validateMatchingNode(node, nodeType, tagName, lView, tNode) {
14388
- validateNodeExists(node);
14389
- if (node.nodeType !== nodeType ||
14390
- node.nodeType === Node.ELEMENT_NODE &&
14391
- node.tagName.toLowerCase() !== tagName?.toLowerCase()) {
14392
- // TODO: improve error message and use RuntimeError instead.
14393
- throw new Error(`Unexpected node found during hydration.`);
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
- * Verifies whether next sibling node exists.
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
- // TODO: improve error message and use RuntimeError instead.
14403
- throw new Error(`Unexpected state: insufficient number of sibling nodes.`);
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
- // TODO: improve error message and use RuntimeError instead.
14409
- throw new Error(`Hydration expected an element to be present at this location.`);
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
- if (tView.firstChild === tNode) {
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.index - HEADER_OFFSET;
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.index - HEADER_OFFSET;
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
- enterSkipHydrationBlock(tNode);
14853
- // Since this isn't hydratable, we need to empty the node
14854
- // so there's no duplicate content after render
14855
- clearElementContents(renderer, native);
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
- // If this container is non-empty, store the first node as a segment head,
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
- if ((tProjectionNode.flags & 32 /* TNodeFlags.isDetached */) !== 32 /* TNodeFlags.isDetached */) {
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
- const view = {
22473
- data: serializedView,
22474
- firstChild: null,
22475
- };
22476
- if (serializedView[NUM_ROOT_NODES] > 0) {
22477
- // Keep reference to the first node in this view,
22478
- // so it can be accessed while invoking template instructions.
22479
- view.firstChild = currentRNode;
22480
- // Move over to the next node after this view, which can
22481
- // either be a first node of the next view or an anchor comment
22482
- // node after the last view in a container.
22483
- currentRNode = siblingAfter(serializedView[NUM_ROOT_NODES], currentRNode);
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 isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock(hostTNode);
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 index = hostTNode.index - HEADER_OFFSET;
22873
- const currentRNode = getSegmentHead(hydrationInfo, index);
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 while processing the imports and exports of an
24719
- // NgModule tree, we cache them in this set so we can skip ones that have already been seen
24720
- // encountered. In some test setups, this caching resulted in 10X runtime improvement.
24721
- const processedNgModuleDefs = new Set();
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 (processedNgModuleDefs.has(def)) {
25468
+ if (processedDefs.has(def)) {
24730
25469
  continue;
24731
25470
  }
24732
- processedNgModuleDefs.add(def);
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