-
+
{$ option.name $}{%- if option.isOptional %}?{% endif -%}
+ {%- if option.deprecated !== undefined %}
+ deprecated {% endif %}
{%- if option.developerPreview %}
developer preview
diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts
index 53610e058b6f03..7dcd9167dbd4cc 100644
--- a/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts
+++ b/devtools/projects/ng-devtools-backend/src/lib/component-tree.ts
@@ -88,54 +88,57 @@ export function getDirectivesFromElement(element: HTMLElement):
export const getLatestComponentState =
(query: ComponentExplorerViewQuery, directiveForest?: ComponentTreeNode[]):
- {directiveProperties: DirectivesProperties;}|undefined => {
- // if a directive forest is passed in we don't have to build the forest again.
- directiveForest = directiveForest ?? buildDirectiveForest();
-
- const node = queryDirectiveForest(query.selectedElement, directiveForest);
- if (!node) {
- return;
- }
-
- const directiveProperties: DirectivesProperties = {};
-
- const injector = ngDebug().getInjector(node.nativeElement);
-
- const resolutionPathWithProviders = getInjectorResolutionPath(injector).map(
- injector => ({injector, providers: getInjectorProviders(injector)}));
-
-
- const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => {
- const {instance, name} = dir;
- const metadata = getDirectiveMetadata(instance);
- metadata.dependencies = getDependenciesForDirective(
- injector, resolutionPathWithProviders, instance.constructor);
-
- if (query.propertyQuery.type === PropertyQueryTypes.All) {
- directiveProperties[dir.name] = {
- props: serializeDirectiveState(instance),
- metadata,
- };
- }
-
- if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
- directiveProperties[name] = {
- props: deeplySerializeSelectedProperties(
- instance, query.propertyQuery.properties[name] || []),
- metadata,
- };
- }
- };
+ {directiveProperties: DirectivesProperties;}|
+ undefined => {
+ // if a directive forest is passed in we don't have to build the forest again.
+ directiveForest = directiveForest ?? buildDirectiveForest();
+
+ const node = queryDirectiveForest(query.selectedElement, directiveForest);
+ if (!node) {
+ return;
+ }
+
+ const directiveProperties: DirectivesProperties = {};
- node.directives.forEach((dir) => populateResultSet(dir));
- if (node.component) {
- populateResultSet(node.component);
- }
+ const injector = ngDebug().getInjector(node.nativeElement);
+
+ let resolutionPathWithProviders: {injector: Injector; providers: ProviderRecord[];}[] = [];
+ if (hasDiDebugAPIs()) {
+ resolutionPathWithProviders = getInjectorResolutionPath(injector).map(
+ injector => ({injector, providers: getInjectorProviders(injector)}));
+ }
+
+ const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => {
+ const {instance, name} = dir;
+ const metadata = getDirectiveMetadata(instance);
+ metadata.dependencies = getDependenciesForDirective(
+ injector, resolutionPathWithProviders, instance.constructor);
+
+ if (query.propertyQuery.type === PropertyQueryTypes.All) {
+ directiveProperties[dir.name] = {
+ props: serializeDirectiveState(instance),
+ metadata,
+ };
+ }
- return {
- directiveProperties,
+ if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
+ directiveProperties[name] = {
+ props: deeplySerializeSelectedProperties(
+ instance, query.propertyQuery.properties[name] || []),
+ metadata,
};
- };
+ }
+ };
+
+ node.directives.forEach((dir) => populateResultSet(dir));
+ if (node.component) {
+ populateResultSet(node.component);
+ }
+
+ return {
+ directiveProperties,
+ };
+ };
export function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null {
let id: string;
diff --git a/devtools/projects/shell-browser/src/manifest/manifest.chrome.json b/devtools/projects/shell-browser/src/manifest/manifest.chrome.json
index 1dc05c652cf39e..68106d51a9b1a9 100644
--- a/devtools/projects/shell-browser/src/manifest/manifest.chrome.json
+++ b/devtools/projects/shell-browser/src/manifest/manifest.chrome.json
@@ -3,8 +3,8 @@
"short_name": "Angular DevTools",
"name": "Angular DevTools",
"description": "Angular DevTools extends Chrome DevTools adding Angular specific debugging and profiling capabilities.",
- "version": "1.0.7",
- "version_name": "1.0.7",
+ "version": "1.0.8",
+ "version_name": "1.0.8",
"minimum_chrome_version": "102",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
diff --git a/devtools/projects/shell-browser/src/manifest/manifest.firefox.json b/devtools/projects/shell-browser/src/manifest/manifest.firefox.json
index f619f4dd6bef0c..70bf635d3e3014 100644
--- a/devtools/projects/shell-browser/src/manifest/manifest.firefox.json
+++ b/devtools/projects/shell-browser/src/manifest/manifest.firefox.json
@@ -3,7 +3,7 @@
"short_name": "Angular DevTools",
"name": "Angular DevTools",
"description": "Angular DevTools extends Firefox DevTools adding Angular specific debugging and profiling capabilities.",
- "version": "1.0.7",
+ "version": "1.0.8",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"icons": {
"16": "assets/icon16.png",
diff --git a/goldens/public-api/localize/tools/index.md b/goldens/public-api/localize/tools/index.md
index 4ff6062cefa6c5..7116d7448403d6 100644
--- a/goldens/public-api/localize/tools/index.md
+++ b/goldens/public-api/localize/tools/index.md
@@ -4,6 +4,10 @@
```ts
+///
+///
+///
+
import { AbsoluteFsPath } from '@angular/compiler-cli/private/localize';
import { Element as Element_2 } from '@angular/compiler';
import { Logger } from '@angular/compiler-cli/private/localize';
diff --git a/integration/typings_test_ts53/BUILD.bazel b/integration/typings_test_ts53/BUILD.bazel
new file mode 100644
index 00000000000000..015e2928e71a63
--- /dev/null
+++ b/integration/typings_test_ts53/BUILD.bazel
@@ -0,0 +1,9 @@
+load("//integration:index.bzl", "ng_integration_test")
+
+ng_integration_test(
+ name = "test",
+ # Special case for `typings_test_ts53` test as we want to pin
+ # `typescript` at version 5.3.x for that test and not link to the
+ # root @npm//typescript package.
+ pinned_npm_packages = ["typescript"],
+)
diff --git a/integration/typings_test_ts53/include-all.ts b/integration/typings_test_ts53/include-all.ts
new file mode 100644
index 00000000000000..196ed86c520126
--- /dev/null
+++ b/integration/typings_test_ts53/include-all.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+
+
+import * as animations from '@angular/animations';
+import * as animationsBrowser from '@angular/animations/browser';
+import * as animationsBrowserTesting from '@angular/animations/browser/testing';
+import * as common from '@angular/common';
+import * as commonHttp from '@angular/common/http';
+import * as commonTesting from '@angular/common/testing';
+import * as commonHttpTesting from '@angular/common/http/testing';
+import * as compiler from '@angular/compiler';
+import * as core from '@angular/core';
+import * as coreTesting from '@angular/core/testing';
+import * as elements from '@angular/elements';
+import * as forms from '@angular/forms';
+import * as localize from '@angular/localize';
+import * as platformBrowser from '@angular/platform-browser';
+import * as platformBrowserDynamic from '@angular/platform-browser-dynamic';
+import * as platformBrowserDynamicTesting from '@angular/platform-browser-dynamic/testing';
+import * as platformBrowserAnimations from '@angular/platform-browser/animations';
+import * as platformBrowserTesting from '@angular/platform-browser/testing';
+import * as platformServer from '@angular/platform-server';
+import * as platformServerInit from '@angular/platform-server/init';
+import * as platformServerTesting from '@angular/platform-server/testing';
+import * as router from '@angular/router';
+import * as routerTesting from '@angular/router/testing';
+import * as routerUpgrade from '@angular/router/upgrade';
+import * as serviceWorker from '@angular/service-worker';
+import * as upgrade from '@angular/upgrade';
+import * as upgradeStatic from '@angular/upgrade/static';
+import * as upgradeTesting from '@angular/upgrade/static/testing';
+
+export default {
+ animations,
+ animationsBrowser,
+ animationsBrowserTesting,
+ common,
+ commonTesting,
+ commonHttp,
+ commonHttpTesting,
+ compiler,
+ core,
+ coreTesting,
+ elements,
+ forms,
+ localize,
+ platformBrowser,
+ platformBrowserTesting,
+ platformBrowserDynamic,
+ platformBrowserDynamicTesting,
+ platformBrowserAnimations,
+ platformServer,
+ platformServerInit,
+ platformServerTesting,
+ router,
+ routerTesting,
+ routerUpgrade,
+ serviceWorker,
+ upgrade,
+ upgradeStatic,
+ upgradeTesting,
+};
diff --git a/integration/typings_test_ts53/package.json b/integration/typings_test_ts53/package.json
new file mode 100644
index 00000000000000..a06d293ef24f81
--- /dev/null
+++ b/integration/typings_test_ts53/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "angular-integration",
+ "description": "Assert that users with TypeScript 5.3 can type-check an Angular application",
+ "version": "0.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@angular/animations": "file:../../dist/packages-dist/animations",
+ "@angular/common": "file:../../dist/packages-dist/common",
+ "@angular/compiler": "file:../../dist/packages-dist/compiler",
+ "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
+ "@angular/core": "file:../../dist/packages-dist/core",
+ "@angular/elements": "file:../../dist/packages-dist/elements",
+ "@angular/forms": "file:../../dist/packages-dist/forms",
+ "@angular/localize": "file:../../dist/packages-dist/localize",
+ "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
+ "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
+ "@angular/platform-server": "file:../../dist/packages-dist/platform-server",
+ "@angular/router": "file:../../dist/packages-dist/router",
+ "@angular/service-worker": "file:../../dist/packages-dist/service-worker",
+ "@angular/upgrade": "file:../../dist/packages-dist/upgrade",
+ "@types/jasmine": "file:../../node_modules/@types/jasmine",
+ "rxjs": "file:../../node_modules/rxjs",
+ "typescript": "5.3.1-rc",
+ "zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
+ },
+ "scripts": {
+ "test": "tsc"
+ }
+}
diff --git a/integration/typings_test_ts53/tsconfig.json b/integration/typings_test_ts53/tsconfig.json
new file mode 100644
index 00000000000000..30e25c22097341
--- /dev/null
+++ b/integration/typings_test_ts53/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "experimentalDecorators": true,
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "outDir": "./dist/out-tsc",
+ "rootDir": ".",
+ "target": "es5",
+ "lib": [
+ "es5",
+ "dom",
+ "es2015.collection",
+ "es2015.iterable",
+ "es2015.promise"
+ ],
+ "types": [],
+ },
+ "files": [
+ "include-all.ts",
+ "node_modules/@types/jasmine/index.d.ts"
+ ]
+}
diff --git a/integration/typings_test_ts53/yarn.lock b/integration/typings_test_ts53/yarn.lock
new file mode 100644
index 00000000000000..22508712a5f6a7
--- /dev/null
+++ b/integration/typings_test_ts53/yarn.lock
@@ -0,0 +1,789 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ampproject/remapping@^2.2.0":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
+ integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@angular/animations@file:../../dist/packages-dist/animations":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/common@file:../../dist/packages-dist/common":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/compiler-cli@file:../../dist/packages-dist/compiler-cli":
+ version "17.1.0-next.0"
+ dependencies:
+ "@babel/core" "7.23.2"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+ chokidar "^3.0.0"
+ convert-source-map "^1.5.1"
+ reflect-metadata "^0.1.2"
+ semver "^7.0.0"
+ tslib "^2.3.0"
+ yargs "^17.2.1"
+
+"@angular/compiler@file:../../dist/packages-dist/compiler":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/core@file:../../dist/packages-dist/core":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/elements@file:../../dist/packages-dist/elements":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/forms@file:../../dist/packages-dist/forms":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/localize@file:../../dist/packages-dist/localize":
+ version "17.1.0-next.0"
+ dependencies:
+ "@babel/core" "7.23.2"
+ fast-glob "3.3.1"
+ yargs "^17.2.1"
+
+"@angular/platform-browser-dynamic@file:../../dist/packages-dist/platform-browser-dynamic":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/platform-browser@file:../../dist/packages-dist/platform-browser":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/platform-server@file:../../dist/packages-dist/platform-server":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+ xhr2 "^0.2.0"
+
+"@angular/router@file:../../dist/packages-dist/router":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/service-worker@file:../../dist/packages-dist/service-worker":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@angular/upgrade@file:../../dist/packages-dist/upgrade":
+ version "17.1.0-next.0"
+ dependencies:
+ tslib "^2.3.0"
+
+"@babel/code-frame@^7.22.13":
+ version "7.22.13"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
+ integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+ dependencies:
+ "@babel/highlight" "^7.22.13"
+ chalk "^2.4.2"
+
+"@babel/compat-data@^7.22.9":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc"
+ integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==
+
+"@babel/core@7.23.2":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94"
+ integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.0"
+ "@babel/helper-compilation-targets" "^7.22.15"
+ "@babel/helper-module-transforms" "^7.23.0"
+ "@babel/helpers" "^7.23.2"
+ "@babel/parser" "^7.23.0"
+ "@babel/template" "^7.22.15"
+ "@babel/traverse" "^7.23.2"
+ "@babel/types" "^7.23.0"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
+"@babel/generator@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
+ integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
+ dependencies:
+ "@babel/types" "^7.23.0"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jsesc "^2.5.1"
+
+"@babel/helper-compilation-targets@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52"
+ integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==
+ dependencies:
+ "@babel/compat-data" "^7.22.9"
+ "@babel/helper-validator-option" "^7.22.15"
+ browserslist "^4.21.9"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
+"@babel/helper-environment-visitor@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
+ integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
+
+"@babel/helper-function-name@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
+ integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
+ dependencies:
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.23.0"
+
+"@babel/helper-hoist-variables@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
+ integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
+"@babel/helper-module-imports@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
+ integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
+ dependencies:
+ "@babel/types" "^7.22.15"
+
+"@babel/helper-module-transforms@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e"
+ integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-module-imports" "^7.22.15"
+ "@babel/helper-simple-access" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/helper-validator-identifier" "^7.22.20"
+
+"@babel/helper-simple-access@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de"
+ integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
+"@babel/helper-split-export-declaration@^7.22.6":
+ version "7.22.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
+ integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
+"@babel/helper-string-parser@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
+ integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
+
+"@babel/helper-validator-identifier@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
+ integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
+
+"@babel/helper-validator-option@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040"
+ integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==
+
+"@babel/helpers@^7.23.2":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767"
+ integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==
+ dependencies:
+ "@babel/template" "^7.22.15"
+ "@babel/traverse" "^7.23.2"
+ "@babel/types" "^7.23.0"
+
+"@babel/highlight@^7.22.13":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
+ integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.22.20"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
+ integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
+
+"@babel/template@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
+ integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/parser" "^7.22.15"
+ "@babel/types" "^7.22.15"
+
+"@babel/traverse@^7.23.2":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
+ integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.0"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-hoist-variables" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/parser" "^7.23.0"
+ "@babel/types" "^7.23.0"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
+"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
+ integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
+"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+ integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
+"@jridgewell/set-array@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+ integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.20"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
+ integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@types/jasmine@file:../../node_modules/@types/jasmine":
+ version "5.1.1"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-styles@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+braces@^3.0.2, braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
+browserslist@^4.21.9:
+ version "4.22.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619"
+ integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==
+ dependencies:
+ caniuse-lite "^1.0.30001541"
+ electron-to-chromium "^1.4.535"
+ node-releases "^2.0.13"
+ update-browserslist-db "^1.0.13"
+
+caniuse-lite@^1.0.30001541:
+ version "1.0.30001561"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz#752f21f56f96f1b1a52e97aae98c57c562d5d9da"
+ integrity sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==
+
+chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chokidar@^3.0.0:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+cliui@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
+ integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.1"
+ wrap-ansi "^7.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+convert-source-map@^1.5.1:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
+ integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
+convert-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
+ integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
+debug@^4.1.0:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+electron-to-chromium@^1.4.535:
+ version "1.4.577"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.577.tgz#a732f11cf4532be96e5e3f1197dcda54c2cec7ad"
+ integrity sha512-/5xHPH6f00SxhHw6052r+5S1xO7gHNc89hV7tqlvnStvKbSrDqc/u6AlwPvVWWNj+s4/KL6T6y8ih+nOY0qYNA==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+fast-glob@3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
+ integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fastq@^1.6.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
+ integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+ dependencies:
+ reusify "^1.0.4"
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+fsevents@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+json5@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+node-releases@^2.0.13:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
+ integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+reflect-metadata@^0.1.2:
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
+ integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+"rxjs@file:../../node_modules/rxjs":
+ version "6.6.7"
+ dependencies:
+ tslib "^1.9.0"
+
+semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.0.0:
+ version "7.5.4"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
+ integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
+ dependencies:
+ lru-cache "^6.0.0"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+tslib@^1.9.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tslib@^2.3.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
+ integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
+
+typescript@5.3.1-rc:
+ version "5.3.1-rc"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.1-rc.tgz#c307d4b69ea0c1c2cd17d4dd7700d30f7197f829"
+ integrity sha512-NVq/AufFc6KVjmVPcuVwdCkhTQlTcMEyRYJPvaGhPvj+X80MYUF+90qf0//uvINPb2ULg9m91/gbdIOhN2cZqA==
+
+update-browserslist-db@^1.0.13:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
+ integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==
+ dependencies:
+ escalade "^3.1.1"
+ picocolors "^1.0.0"
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+xhr2@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.2.1.tgz#4e73adc4f9cfec9cbd2157f73efdce3a5f108a93"
+ integrity sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yargs-parser@^21.1.1:
+ version "21.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+ integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs@^17.2.1:
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
+ dependencies:
+ cliui "^8.0.1"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.3"
+ y18n "^5.0.5"
+ yargs-parser "^21.1.1"
+
+"zone.js@file:../../dist/zone.js-dist/archive/zone.js.tgz":
+ version "0.14.2"
+ resolved "file:../../dist/zone.js-dist/archive/zone.js.tgz#60b949ae4663cb0a4e6296217d9d45be86875ae3"
+ dependencies:
+ tslib "^2.3.0"
diff --git a/package.json b/package.json
index 01be5230408a45..2bd82c813761db 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"@types/chrome": "^0.0.248",
"@types/convert-source-map": "^2.0.0",
"@types/diff": "^5.0.0",
+ "@types/dom-navigation": "^1.0.2",
"@types/dom-view-transitions": "^1.0.1",
"@types/hammerjs": "2.0.43",
"@types/jasmine": "^5.0.0",
@@ -151,7 +152,7 @@
"todomvc-common": "^1.0.5",
"tslib": "^2.3.0",
"tslint": "6.1.3",
- "typescript": "5.2.2",
+ "typescript": "5.3.1-rc",
"webtreemap": "^2.0.1",
"xhr2": "0.2.1",
"yargs": "^17.2.1"
diff --git a/packages/bazel/package.json b/packages/bazel/package.json
index 954acd68433e71..8db22b03bef9cb 100644
--- a/packages/bazel/package.json
+++ b/packages/bazel/package.json
@@ -35,7 +35,7 @@
"rollup": "^2.56.3",
"rollup-plugin-sourcemaps": "^0.6.3",
"terser": "^5.9.0",
- "typescript": ">=5.2 <5.3"
+ "typescript": ">=5.2 <5.4"
},
"peerDependenciesMeta": {
"terser": {
diff --git a/packages/common/BUILD.bazel b/packages/common/BUILD.bazel
index 6e429f2d8bc2dc..79d51e45a7a399 100644
--- a/packages/common/BUILD.bazel
+++ b/packages/common/BUILD.bazel
@@ -31,6 +31,7 @@ ng_module(
),
deps = [
"//packages/core",
+ "@npm//@types/dom-navigation",
"@npm//rxjs",
],
)
diff --git a/packages/common/http/src/transfer_cache.ts b/packages/common/http/src/transfer_cache.ts
index 7f7eb772e1ead7..4310038157b214 100644
--- a/packages/common/http/src/transfer_cache.ts
+++ b/packages/common/http/src/transfer_cache.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {APP_BOOTSTRAP_LISTENER, ApplicationRef, inject, InjectionToken, makeStateKey, Provider, StateKey, TransferState, ɵformatRuntimeError as formatRuntimeError, ɵperformanceMark as performanceMark, ɵtruncateMiddle as truncateMiddle, ɵwhenStable as whenStable} from '@angular/core';
+import {APP_BOOTSTRAP_LISTENER, ApplicationRef, inject, InjectionToken, makeStateKey, Provider, StateKey, TransferState, ɵformatRuntimeError as formatRuntimeError, ɵperformanceMarkFeature as performanceMarkFeature, ɵtruncateMiddle as truncateMiddle, ɵwhenStable as whenStable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {tap} from 'rxjs/operators';
@@ -227,7 +227,7 @@ export function withHttpTransferCache(cacheOptions: HttpTransferCacheOptions): P
{
provide: CACHE_OPTIONS,
useFactory: (): CacheOptions => {
- performanceMark('mark_use_counter', {detail: {feature: 'NgHttpTransferCache'}});
+ performanceMarkFeature('NgHttpTransferCache');
return {isCacheActive: true, ...cacheOptions};
}
},
diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
index 3ac4f538b64b50..1e8aaabc7f0939 100644
--- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
+++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {booleanAttribute, Directive, ElementRef, inject, Injector, Input, NgZone, numberAttribute, OnChanges, OnDestroy, OnInit, PLATFORM_ID, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵIMAGE_CONFIG as IMAGE_CONFIG, ɵIMAGE_CONFIG_DEFAULTS as IMAGE_CONFIG_DEFAULTS, ɵImageConfig as ImageConfig, ɵperformanceMark as performanceMark, ɵRuntimeError as RuntimeError, ɵSafeValue as SafeValue, ɵunwrapSafeValue as unwrapSafeValue} from '@angular/core';
+import {booleanAttribute, Directive, ElementRef, inject, Injector, Input, NgZone, numberAttribute, OnChanges, OnDestroy, OnInit, PLATFORM_ID, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵIMAGE_CONFIG as IMAGE_CONFIG, ɵIMAGE_CONFIG_DEFAULTS as IMAGE_CONFIG_DEFAULTS, ɵImageConfig as ImageConfig, ɵperformanceMarkFeature as performanceMarkFeature, ɵRuntimeError as RuntimeError, ɵSafeValue as SafeValue, ɵunwrapSafeValue as unwrapSafeValue} from '@angular/core';
import {RuntimeErrorCode} from '../../errors';
import {isPlatformServer} from '../../platform_id';
@@ -302,7 +302,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
/** @nodoc */
ngOnInit() {
- performanceMark('mark_use_counter', {'detail': {'feature': 'NgOptimizedImage'}});
+ performanceMarkFeature('NgOptimizedImage');
if (ngDevMode) {
const ngZone = this.injector.get(NgZone);
diff --git a/packages/common/src/navigation/platform_navigation.ts b/packages/common/src/navigation/platform_navigation.ts
new file mode 100644
index 00000000000000..5030414afaba73
--- /dev/null
+++ b/packages/common/src/navigation/platform_navigation.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Injectable} from '@angular/core';
+
+/**
+ * This class wraps the platform Navigation API which allows server-specific and test
+ * implementations.
+ */
+@Injectable({providedIn: 'platform', useFactory: () => window.navigation})
+export abstract class PlatformNavigation implements Navigation {
+ abstract entries(): NavigationHistoryEntry[];
+ abstract currentEntry: NavigationHistoryEntry|null;
+ abstract updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;
+ abstract transition: NavigationTransition|null;
+ abstract canGoBack: boolean;
+ abstract canGoForward: boolean;
+ abstract navigate(url: string, options?: NavigationNavigateOptions|undefined): NavigationResult;
+ abstract reload(options?: NavigationReloadOptions|undefined): NavigationResult;
+ abstract traverseTo(key: string, options?: NavigationOptions|undefined): NavigationResult;
+ abstract back(options?: NavigationOptions|undefined): NavigationResult;
+ abstract forward(options?: NavigationOptions|undefined): NavigationResult;
+ abstract onnavigate: ((this: Navigation, ev: NavigateEvent) => any)|null;
+ abstract onnavigatesuccess: ((this: Navigation, ev: Event) => any)|null;
+ abstract onnavigateerror: ((this: Navigation, ev: ErrorEvent) => any)|null;
+ abstract oncurrententrychange:
+ ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)|null;
+ abstract addEventListener(type: unknown, listener: unknown, options?: unknown): void;
+ abstract removeEventListener(type: unknown, listener: unknown, options?: unknown): void;
+ abstract dispatchEvent(event: Event): boolean;
+}
diff --git a/packages/common/test/navigation/fake_platform_navigation.spec.ts b/packages/common/test/navigation/fake_platform_navigation.spec.ts
new file mode 100644
index 00000000000000..954e43c561ab3f
--- /dev/null
+++ b/packages/common/test/navigation/fake_platform_navigation.spec.ts
@@ -0,0 +1,1995 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {ExperimentalNavigationInterceptOptions, FakeNavigateEvent, FakeNavigation, FakeNavigationCurrentEntryChangeEvent,} from '../../testing/src/navigation/fake_navigation';
+
+interface Locals {
+ navigation: FakeNavigation;
+ navigateEvents: FakeNavigateEvent[];
+ navigationCurrentEntryChangeEvents: FakeNavigationCurrentEntryChangeEvent[];
+ popStateEvents: PopStateEvent[];
+ pendingInterceptOptions: ExperimentalNavigationInterceptOptions[];
+ nextNavigateEvent: () => Promise;
+ setExtraNavigateCallback: (callback: (event: FakeNavigateEvent) => void) => void;
+}
+
+describe('navigation', () => {
+ let locals: Locals;
+
+ const popStateListener = (event: Event) => {
+ const popStateEvent = event as PopStateEvent;
+ locals.popStateEvents.push(popStateEvent);
+ };
+
+ beforeEach(() => {
+ window.addEventListener('popstate', popStateListener);
+ });
+
+ afterEach(() => {
+ window.removeEventListener('popstate', popStateListener);
+ });
+
+ beforeEach(() => {
+ const navigation = new FakeNavigation(window, 'https://test.com');
+ const navigateEvents: FakeNavigateEvent[] = [];
+ let nextNavigateEventResolve!: (value: FakeNavigateEvent) => void;
+ let nextNavigateEventPromise = new Promise((resolve) => {
+ nextNavigateEventResolve = resolve;
+ });
+ const navigationCurrentEntryChangeEvents: FakeNavigationCurrentEntryChangeEvent[] = [];
+ const popStateEvents: PopStateEvent[] = [];
+ const pendingInterceptOptions: ExperimentalNavigationInterceptOptions[] = [];
+ let extraNavigateCallback:|((event: FakeNavigateEvent) => void)|undefined = undefined;
+
+ navigation.addEventListener('navigate', (event: Event) => {
+ const navigateEvent = event as FakeNavigateEvent;
+ nextNavigateEventResolve(navigateEvent);
+ nextNavigateEventPromise = new Promise((resolve) => {
+ nextNavigateEventResolve = resolve;
+ });
+ locals.navigateEvents.push(navigateEvent);
+ const interceptOptions = pendingInterceptOptions.shift();
+ if (interceptOptions) {
+ navigateEvent.intercept(interceptOptions);
+ }
+ extraNavigateCallback?.(navigateEvent);
+ });
+ navigation.addEventListener('currententrychange', (event: Event) => {
+ const currentNavigationEntryChangeEvent = event as FakeNavigationCurrentEntryChangeEvent;
+ locals.navigationCurrentEntryChangeEvents.push(
+ currentNavigationEntryChangeEvent,
+ );
+ });
+ locals = {
+ navigation,
+ navigateEvents,
+ navigationCurrentEntryChangeEvents,
+ popStateEvents,
+ pendingInterceptOptions,
+ nextNavigateEvent() {
+ return nextNavigateEventPromise;
+ },
+ setExtraNavigateCallback(callback: (event: FakeNavigateEvent) => void) {
+ extraNavigateCallback = callback;
+ },
+ };
+ });
+
+ const setUpEntries = async ({hash = false} = {}) => {
+ locals.pendingInterceptOptions.push({});
+ const pathPrefix = hash ? '#' : '/';
+ const firstPageEntry = await locals.navigation
+ .navigate(
+ `${pathPrefix}page1`,
+ {state: {page1: true}},
+ )
+ .finished;
+ locals.pendingInterceptOptions.push({});
+ const secondPageEntry = await locals.navigation
+ .navigate(
+ `${pathPrefix}page2`,
+ {state: {page2: true}},
+ )
+ .finished;
+ locals.pendingInterceptOptions.push({});
+ const thirdPageEntry = await locals.navigation
+ .navigate(
+ `${pathPrefix}page3`,
+ {state: {page3: true}},
+ )
+ .finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+ return [firstPageEntry, secondPageEntry, thirdPageEntry];
+ };
+
+ const setUpEntriesWithHistory = ({hash = false} = {}) => {
+ const pathPrefix = hash ? '#' : '/';
+ locals.navigation.pushState(
+ {state: {page1: true}},
+ '',
+ `${pathPrefix}page1`,
+ );
+ const firstPageEntry = locals.navigation.currentEntry;
+ locals.navigation.pushState(
+ {state: {page2: true}},
+ '',
+ `${pathPrefix}page2`,
+ );
+ const secondPageEntry = locals.navigation.currentEntry;
+ locals.navigation.pushState(
+ {state: {page3: true}},
+ '',
+ `${pathPrefix}page3`,
+ );
+ const thirdPageEntry = locals.navigation.currentEntry;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+ return [firstPageEntry, secondPageEntry, thirdPageEntry];
+ };
+
+ it('disposes', async () => {
+ expect(locals.navigation.isDisposed()).toBeFalse();
+ const navigateEvents: Event[] = [];
+ locals.navigation.addEventListener('navigate', (event: Event) => {
+ navigateEvents.push(event);
+ });
+ const navigationCurrentEntryChangeEvents: Event[] = [];
+ locals.navigation.addEventListener('currententrychange', (event: Event) => {
+ navigationCurrentEntryChangeEvents.push(event);
+ });
+
+ await locals.navigation.navigate('#page1').finished;
+ expect(navigateEvents.length).toBe(1);
+ expect(navigationCurrentEntryChangeEvents.length).toBe(1);
+ locals.navigation.dispose();
+ // After a dispose, a different singleton.
+ expect(locals.navigation.isDisposed()).toBeTrue();
+ await locals.navigation.navigate('#page2').finished;
+ // Listeners are disposed.
+ expect(navigateEvents.length).toBe(1);
+ expect(navigationCurrentEntryChangeEvents.length).toBe(1);
+ });
+
+ describe('navigate', () => {
+ it('push URL', async () => {
+ const initialEntry = locals.navigation.currentEntry;
+ locals.pendingInterceptOptions.push({});
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'push',
+ userInitiated: false,
+ signal: jasmine.any(AbortSignal),
+ destination: jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: null,
+ id: null,
+ index: -1,
+ sameDocument: false,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState()).toBeUndefined();
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '1',
+ id: '1',
+ index: 1,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigation.currentEntry).toBe(committedEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'push',
+ from: jasmine.objectContaining({
+ url: initialEntry.url,
+ key: initialEntry.key,
+ id: initialEntry.id,
+ index: initialEntry.index,
+ sameDocument: initialEntry.sameDocument,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toBe(
+ initialEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ const finishedEntry = await finished;
+ expect(committedEntry).toBe(finishedEntry);
+ expect(locals.navigation.currentEntry).toBe(finishedEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ });
+
+ it('replace URL', async () => {
+ const initialEntry = locals.navigation.currentEntry;
+ locals.pendingInterceptOptions.push({});
+ const {committed, finished} = locals.navigation.navigate('/test', {
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'replace',
+ userInitiated: false,
+ signal: jasmine.any(AbortSignal),
+ destination: jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: null,
+ id: null,
+ index: -1,
+ sameDocument: false,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState()).toBeUndefined();
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '0',
+ id: '1',
+ index: 0,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigation.currentEntry).toBe(committedEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'replace',
+ from: jasmine.objectContaining({
+ url: initialEntry.url,
+ key: initialEntry.key,
+ id: initialEntry.id,
+ index: initialEntry.index,
+ sameDocument: initialEntry.sameDocument,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toBe(
+ initialEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ const finishedEntry = await finished;
+ expect(committedEntry).toBe(finishedEntry);
+ expect(locals.navigation.currentEntry).toBe(finishedEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ });
+
+ it('push URL with state', async () => {
+ locals.pendingInterceptOptions.push({});
+ const state = {test: true};
+ const {committed, finished} = locals.navigation.navigate('/test', {
+ state,
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.getState()).toEqual(state);
+ const committedEntry = await committed;
+ expect(committedEntry.getState()).toEqual(state);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const finishedEntry = await finished;
+ expect(committedEntry).toBe(finishedEntry);
+ });
+
+ it('replace URL with state', async () => {
+ locals.pendingInterceptOptions.push({});
+ const state = {test: true};
+ const {committed, finished} = locals.navigation.navigate('/test', {
+ state,
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.getState()).toEqual(state);
+ const committedEntry = await committed;
+ expect(committedEntry.getState()).toEqual(state);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const finishedEntry = await finished;
+ expect(committedEntry).toBe(finishedEntry);
+ });
+
+ it('push URL with hashchange', async () => {
+ const {committed, finished} = locals.navigation.navigate('#test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.url).toBe('https://test.com/#test');
+ expect(navigateEvent.hashChange).toBeTrue();
+ const committedEntry = await committed;
+ expect(committedEntry.url).toBe('https://test.com/#test');
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const finishedEntry = await finished;
+ expect(committedEntry).toBe(finishedEntry);
+ });
+
+ it('replace URL with hashchange', async () => {
+ const {committed, finished} = locals.navigation.navigate('#test', {
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.url).toBe('https://test.com/#test');
+ expect(navigateEvent.hashChange).toBeTrue();
+ const committedEntry = await committed;
+ expect(committedEntry.url).toBe('https://test.com/#test');
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const finishedEntry = await finished;
+ expect(committedEntry).toBe(finishedEntry);
+ });
+
+ it('push URL with info', async () => {
+ locals.pendingInterceptOptions.push({});
+ const info = {test: true};
+ const {finished, committed} = locals.navigation.navigate('/test', {info});
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.info).toBe(info);
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ });
+
+ it('replace URL with info', async () => {
+ locals.pendingInterceptOptions.push({});
+ const info = {test: true};
+ const {finished, committed} = locals.navigation.navigate('/test', {
+ info,
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.info).toBe(info);
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ });
+
+ it('push URL with handler', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ });
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '1',
+ id: '1',
+ index: 1,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ handlerFinishedResolve(undefined);
+ await expectAsync(finished).toBeResolvedTo(committedEntry);
+ });
+
+ it('replace URL with handler', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ });
+ const {committed, finished} = locals.navigation.navigate('/test', {
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '0',
+ id: '1',
+ index: 0,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ handlerFinishedResolve(undefined);
+ await expectAsync(finished).toBeResolvedTo(committedEntry);
+ });
+
+ it('deferred commit', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ commit: 'after-transition',
+ });
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ await expectAsync(committed).toBePending();
+ expect(locals.navigation.currentEntry.url).toBe('https://test.com/');
+ locals.navigateEvents[0].commit();
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '1',
+ id: '1',
+ index: 1,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ handlerFinishedResolve(undefined);
+ await expectAsync(finished).toBeResolvedTo(committedEntry);
+ });
+
+ it('deferred commit early resolve', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ commit: 'after-transition',
+ });
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ await expectAsync(committed).toBePending();
+ expect(locals.navigation.currentEntry.url).toBe('https://test.com/');
+ handlerFinishedResolve(undefined);
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '1',
+ id: '1',
+ index: 1,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBeResolvedTo(committedEntry);
+ });
+
+ it('deferred commit during dispatch throws', async () => {
+ locals.pendingInterceptOptions.push({
+ commit: 'after-transition',
+ });
+ let called = false;
+ locals.setExtraNavigateCallback((event: FakeNavigateEvent) => {
+ expect(() => {
+ called = true;
+ event.commit();
+ }).toThrowError(DOMException);
+ });
+ const {finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ navigateEvent.commit();
+ await finished;
+ expect(called).toBeTrue();
+ });
+
+ it('deferred commit with immediate throws', async () => {
+ locals.pendingInterceptOptions.push({});
+ const {finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(() => {
+ navigateEvent.commit();
+ }).toThrowError(DOMException);
+ await finished;
+ });
+
+ it('deferred commit twice throws', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ commit: 'after-transition',
+ });
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ await expectAsync(committed).toBePending();
+ expect(locals.navigation.currentEntry.url).toBe('https://test.com/');
+ navigateEvent.commit();
+ expect(() => {
+ navigateEvent.commit();
+ }).toThrowError(DOMException);
+ handlerFinishedResolve(undefined);
+ await finished;
+ });
+
+ it('deferred commit resolves on finished', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ commit: 'after-transition',
+ });
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ await expectAsync(committed).toBePending();
+ expect(locals.navigation.currentEntry.url).toBe('https://test.com/');
+ handlerFinishedResolve(undefined);
+ const committedEntry = await committed;
+ expect(committedEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '1',
+ id: '1',
+ index: 1,
+ sameDocument: true,
+ }),
+ );
+ expect(committedEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBeResolvedTo(committedEntry);
+ });
+
+ it('push with interruption', async () => {
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise(() => {}),
+ });
+
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ await expectAsync(finished).toBeRejectedWithError(DOMException);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ await interruptResult.committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ await interruptResult.finished;
+ });
+
+ it('replace with interruption', async () => {
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise(() => {}),
+ });
+
+ const {committed, finished} = locals.navigation.navigate('/test', {
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ await expectAsync(finished).toBeRejectedWithError(DOMException);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ await interruptResult.committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ await interruptResult.finished;
+ });
+
+ it('push with handler reject', async () => {
+ let handlerFinishedReject!: (reason: unknown) => void;
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise((resolve, reject) => {
+ handlerFinishedReject = reject;
+ }),
+ });
+
+ const {committed, finished} = locals.navigation.navigate('/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ const error = new Error('rejected');
+ handlerFinishedReject(error);
+ await expectAsync(finished).toBeRejectedWith(error);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ });
+
+ it('replace with reject', async () => {
+ let handlerFinishedReject!: (reason: unknown) => void;
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise((resolve, reject) => {
+ handlerFinishedReject = reject;
+ }),
+ });
+
+ const {committed, finished} = locals.navigation.navigate('/test', {
+ history: 'replace',
+ });
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ const error = new Error('rejected');
+ handlerFinishedReject(error);
+ await expectAsync(finished).toBeRejectedWith(error);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ });
+ });
+
+ describe('traversal', () => {
+ it('traverses back', async () => {
+ expect(locals.navigation.canGoBack).toBeFalse();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ const [firstPageEntry, , thirdPageEntry] = await setUpEntries();
+
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ const {committed, finished} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'traverse',
+ signal: jasmine.any(AbortSignal),
+ userInitiated: false,
+ destination: jasmine.objectContaining({
+ url: firstPageEntry.url!,
+ key: firstPageEntry.key,
+ id: firstPageEntry.id,
+ index: firstPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState())
+ .toEqual(
+ firstPageEntry.getState(),
+ );
+ const committedEntry = await committed;
+ expect(committedEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ from: jasmine.objectContaining({
+ url: thirdPageEntry.url!,
+ key: thirdPageEntry.key,
+ id: thirdPageEntry.id,
+ index: thirdPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toEqual(
+ thirdPageEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeTrue();
+ const finishedEntry = await finished;
+ expect(finishedEntry).toBe(firstPageEntry);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ });
+
+ it('traverses forward', async () => {
+ expect(locals.navigation.canGoBack).toBeFalse();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ const [firstPageEntry, , thirdPageEntry] = await setUpEntries();
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ await locals.navigation.traverseTo(firstPageEntry.key).finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeTrue();
+
+ const {committed, finished} = locals.navigation.traverseTo(
+ thirdPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'traverse',
+ signal: jasmine.any(AbortSignal),
+ userInitiated: false,
+ destination: jasmine.objectContaining({
+ url: thirdPageEntry.url!,
+ key: thirdPageEntry.key,
+ id: thirdPageEntry.id,
+ index: thirdPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState())
+ .toEqual(
+ thirdPageEntry.getState(),
+ );
+ const committedEntry = await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ from: jasmine.objectContaining({
+ url: firstPageEntry.url!,
+ key: firstPageEntry.key,
+ id: firstPageEntry.id,
+ index: firstPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toEqual(
+ firstPageEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ expect(committedEntry).toBe(thirdPageEntry);
+ const finishedEntry = await finished;
+ expect(finishedEntry).toBe(thirdPageEntry);
+ expect(locals.navigation.currentEntry).toBe(thirdPageEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ });
+
+ it('traverses back with hashchange', async () => {
+ const [firstPageEntry] = await setUpEntries({hash: true});
+
+ const {finished, committed} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.hashChange).toBeTrue();
+ await committed;
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ });
+
+ it('traverses forward with hashchange', async () => {
+ const [firstPageEntry, thirdPageEntry] = await setUpEntries({hash: true});
+ await locals.navigation.traverseTo(firstPageEntry.key).finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+
+ const {finished, committed} = locals.navigation.traverseTo(
+ thirdPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.hashChange).toBeTrue();
+ await committed;
+ expect(locals.navigation.currentEntry).toBe(thirdPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ expect(locals.navigation.currentEntry).toBe(thirdPageEntry);
+ });
+
+ it('traverses with info', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ const info = {test: true};
+ const {finished, committed} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ {info},
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.info).toBe(info);
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ });
+
+ it('traverses with history state', async () => {
+ const [firstPageEntry] = setUpEntriesWithHistory();
+
+ const {finished, committed} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.destination.getHistoryState())
+ .toEqual(
+ firstPageEntry.getHistoryState(),
+ );
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toEqual(firstPageEntry.getHistoryState());
+ await finished;
+ });
+
+ it('traverses with handler', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ });
+ const {committed, finished} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const committedEntry = await committed;
+ expect(committedEntry).toBe(firstPageEntry);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await expectAsync(finished).toBePending();
+ handlerFinishedResolve(undefined);
+ await expectAsync(finished).toBeResolvedTo(firstPageEntry);
+ });
+
+ it('traverses with interruption', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise(() => {}),
+ });
+ const {committed, finished} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ await committed;
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ expect(navigateEvent.signal.aborted).toBeFalse();
+ await expectAsync(finished).toBePending();
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ await expectAsync(finished).toBeRejectedWithError(DOMException);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ await interruptResult.committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ await interruptResult.finished;
+ });
+
+ it('traverses with reject', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ let handlerFinishedReject!: (reason: unknown) => void;
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise((resolve, reject) => {
+ handlerFinishedReject = reject;
+ }),
+ });
+
+ const {committed, finished} = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const navigateEvent = await locals.nextNavigateEvent();
+ await committed;
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ expect(navigateEvent.signal.aborted).toBeFalse();
+ await expectAsync(finished).toBePending();
+ const error = new Error('rejected');
+ handlerFinishedReject(error);
+ await expectAsync(finished).toBeRejectedWith(error);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ });
+
+ it('traverses to non-existent', async () => {
+ const {committed, finished} = locals.navigation.traverseTo('non-existent');
+ await expectAsync(committed).toBeRejectedWithError(DOMException);
+ await expectAsync(finished).toBeRejectedWithError(DOMException);
+ expect(locals.navigateEvents.length).toBe(0);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(0);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('back', async () => {
+ const [, secondPageEntry, thirdPageEntry] = await setUpEntries();
+ const {committed, finished} = locals.navigation.back();
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'traverse',
+ signal: jasmine.any(AbortSignal),
+ userInitiated: false,
+ destination: jasmine.objectContaining({
+ url: secondPageEntry.url!,
+ key: secondPageEntry.key,
+ id: secondPageEntry.id,
+ index: secondPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState())
+ .toEqual(
+ secondPageEntry.getState(),
+ );
+ const committedEntry = await committed;
+ expect(committedEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ from: jasmine.objectContaining({
+ url: thirdPageEntry.url!,
+ key: thirdPageEntry.key,
+ id: thirdPageEntry.id,
+ index: thirdPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toEqual(
+ thirdPageEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ const finishedEntry = await finished;
+ expect(finishedEntry).toBe(secondPageEntry);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ });
+
+ it('back with info', async () => {
+ await setUpEntries();
+ const info = {test: true};
+ const {committed, finished} = locals.navigation.back({info});
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.info).toBe(info);
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ });
+
+ it('back out of bounds', async () => {
+ const {committed, finished} = locals.navigation.back();
+ await expectAsync(committed).toBeRejectedWithError(DOMException);
+ await expectAsync(finished).toBeRejectedWithError(DOMException);
+ expect(locals.navigateEvents.length).toBe(0);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(0);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('forward', async () => {
+ const [firstPageEntry, secondPageEntry] = await setUpEntries();
+ await locals.navigation.traverseTo(firstPageEntry.key).finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+
+ const {committed, finished} = locals.navigation.forward();
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'traverse',
+ signal: jasmine.any(AbortSignal),
+ userInitiated: false,
+ destination: jasmine.objectContaining({
+ url: secondPageEntry.url!,
+ key: secondPageEntry.key,
+ id: secondPageEntry.id,
+ index: secondPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState())
+ .toEqual(
+ secondPageEntry.getState(),
+ );
+ const committedEntry = await committed;
+ expect(committedEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ from: jasmine.objectContaining({
+ url: firstPageEntry.url!,
+ key: firstPageEntry.key,
+ id: firstPageEntry.id,
+ index: firstPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toEqual(
+ firstPageEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ const finishedEntry = await finished;
+ expect(finishedEntry).toBe(secondPageEntry);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ });
+
+ it('forward with info', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ await locals.navigation.traverseTo(firstPageEntry.key).finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+
+ const info = {test: true};
+ const {committed, finished} = locals.navigation.forward({info});
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.info).toBe(info);
+ await committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await finished;
+ });
+
+ it('forward out of bounds', async () => {
+ const {committed, finished} = locals.navigation.forward();
+ await expectAsync(committed).toBeRejectedWithError(DOMException);
+ await expectAsync(finished).toBeRejectedWithError(DOMException);
+ expect(locals.navigateEvents.length).toBe(0);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(0);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('traverse synchronously', async () => {
+ const [, secondPageEntry] = await setUpEntries();
+ locals.navigation.setSynchronousTraversalsForTesting(true);
+
+ const {committed, finished} = locals.navigation.back();
+ // Synchronously navigates.
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ await expectAsync(committed).toBeResolvedTo(secondPageEntry);
+ await expectAsync(finished).toBeResolvedTo(secondPageEntry);
+ });
+
+ it('traversal current entry', async () => {
+ const {committed, finished} = locals.navigation.traverseTo(
+ locals.navigation.currentEntry.key,
+ );
+ await expectAsync(committed).toBeResolvedTo(
+ locals.navigation.currentEntry,
+ );
+ await expectAsync(finished).toBeResolvedTo(
+ locals.navigation.currentEntry,
+ );
+ expect(locals.navigateEvents.length).toBe(0);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(0);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('second traversal to same entry', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ const traverseResult = locals.navigation.traverseTo(firstPageEntry.key);
+ const duplicateTraverseResult = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ expect(traverseResult.committed).toBe(duplicateTraverseResult.committed);
+ expect(traverseResult.finished).toBe(duplicateTraverseResult.finished);
+ await Promise.all([
+ traverseResult.committed,
+ duplicateTraverseResult.committed,
+ ]);
+ // Only one NavigationCurrentEntryChangeEvent for duplicate traversals
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ await Promise.all([
+ traverseResult.finished,
+ duplicateTraverseResult.finished,
+ ]);
+ // Only one NavigateEvent for duplicate traversals.
+ expect(locals.navigateEvents.length).toBe(1);
+ });
+
+ it('queues traverses', async () => {
+ const [firstPageEntry, secondPageEntry] = await setUpEntries();
+
+ const firstTraverseResult = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const secondTraverseResult = locals.navigation.traverseTo(
+ secondPageEntry.key,
+ );
+
+ const firstTraverseCommittedEntry = await firstTraverseResult.committed;
+ expect(firstTraverseCommittedEntry).toBe(firstPageEntry);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ const firstNavigateEvent = locals.navigateEvents[0];
+ expect(firstNavigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ destination: jasmine.objectContaining({
+ key: firstPageEntry.key,
+ }),
+ }),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const firstTraverseFinishedEntry = await firstTraverseResult.finished;
+ expect(firstTraverseFinishedEntry).toBe(firstPageEntry);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+
+ const secondTraverseCommittedEntry = await secondTraverseResult.committed;
+ expect(secondTraverseCommittedEntry).toBe(secondPageEntry);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigateEvents.length).toBe(2);
+ const secondNavigateEvent = locals.navigateEvents[1];
+ expect(secondNavigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ destination: jasmine.objectContaining({
+ key: secondPageEntry.key,
+ }),
+ }),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ const secondTraverseFinishedEntry = await secondTraverseResult.finished;
+ expect(secondTraverseFinishedEntry).toBe(secondPageEntry);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ });
+ });
+
+ describe('integration', () => {
+ it('queues traverses after navigate', async () => {
+ const [firstPageEntry, secondPageEntry] = await setUpEntries();
+
+ const firstTraverseResult = locals.navigation.traverseTo(
+ firstPageEntry.key,
+ );
+ const secondTraverseResult = locals.navigation.traverseTo(
+ secondPageEntry.key,
+ );
+ locals.pendingInterceptOptions.push({});
+ const navigateResult = locals.navigation.navigate('/page4', {
+ state: {page4: true},
+ });
+
+ const navigateResultCommittedEntry = await navigateResult.committed;
+ expect(navigateResultCommittedEntry.url).toBe('https://test.com/page4');
+ expect(locals.navigation.currentEntry).toBe(navigateResultCommittedEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ const firstNavigateEvent = locals.navigateEvents[0];
+ expect(firstNavigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'push',
+ destination: jasmine.objectContaining({
+ url: 'https://test.com/page4',
+ }),
+ }),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const navigateResultFinishedEntry = await navigateResult.finished;
+ expect(navigateResultFinishedEntry).toBe(navigateResultCommittedEntry);
+ expect(locals.navigation.currentEntry).toBe(navigateResultCommittedEntry);
+
+ const firstTraverseCommittedEntry = await firstTraverseResult.committed;
+ expect(firstTraverseCommittedEntry).toBe(firstPageEntry);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigateEvents.length).toBe(2);
+ const secondNavigateEvent = locals.navigateEvents[1];
+ expect(secondNavigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ destination: jasmine.objectContaining({
+ key: firstPageEntry.key,
+ }),
+ }),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ const firstTraverseFinishedEntry = await firstTraverseResult.finished;
+ expect(firstTraverseFinishedEntry).toBe(firstPageEntry);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+
+ const secondTraverseCommittedEntry = await secondTraverseResult.committed;
+ expect(secondTraverseCommittedEntry).toBe(secondPageEntry);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigateEvents.length).toBe(3);
+ const thirdNavigateEvent = locals.navigateEvents[2];
+ expect(thirdNavigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ destination: jasmine.objectContaining({
+ key: secondPageEntry.key,
+ }),
+ }),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(3);
+ expect(locals.popStateEvents.length).toBe(3);
+ const secondTraverseFinishedEntry = await secondTraverseResult.finished;
+ expect(secondTraverseFinishedEntry).toBe(secondPageEntry);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ });
+ });
+
+ describe('history API', () => {
+ describe('push and replace', () => {
+ it('push URL', async () => {
+ const initialEntry = locals.navigation.currentEntry;
+
+ locals.navigation.pushState(undefined, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'push',
+ userInitiated: false,
+ signal: jasmine.any(AbortSignal),
+ destination: jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: null,
+ id: null,
+ index: -1,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState()).toBeUndefined();
+ expect(navigateEvent.destination.getHistoryState()).toBeUndefined();
+ const currentEntry = locals.navigation.currentEntry;
+ expect(currentEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '1',
+ id: '1',
+ index: 1,
+ sameDocument: true,
+ }),
+ );
+ expect(currentEntry.getState()).toBeUndefined();
+ expect(currentEntry.getHistoryState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'push',
+ from: jasmine.objectContaining({
+ url: initialEntry.url,
+ key: initialEntry.key,
+ id: initialEntry.id,
+ index: initialEntry.index,
+ sameDocument: initialEntry.sameDocument,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toBe(
+ initialEntry.getState(),
+ );
+ expect(currentEntryChangeEvent.from.getHistoryState()).toBeNull();
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('replace URL', async () => {
+ const initialEntry = locals.navigation.currentEntry;
+
+ locals.navigation.replaceState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'replace',
+ userInitiated: false,
+ signal: jasmine.any(AbortSignal),
+ destination: jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: null,
+ id: null,
+ index: -1,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState()).toBeUndefined();
+ expect(navigateEvent.destination.getHistoryState()).toBeNull();
+ const currentEntry = locals.navigation.currentEntry;
+ expect(currentEntry)
+ .toEqual(
+ jasmine.objectContaining({
+ url: 'https://test.com/test',
+ key: '0',
+ id: '1',
+ index: 0,
+ sameDocument: true,
+ }),
+ );
+ expect(currentEntry.getState()).toBeUndefined();
+ expect(currentEntry.getHistoryState()).toBeNull();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'replace',
+ from: jasmine.objectContaining({
+ url: initialEntry.url,
+ key: initialEntry.key,
+ id: initialEntry.id,
+ index: initialEntry.index,
+ sameDocument: initialEntry.sameDocument,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toBe(
+ initialEntry.getState(),
+ );
+ expect(currentEntryChangeEvent.from.getHistoryState()).toBeNull();
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('push URL with history state', async () => {
+ locals.pendingInterceptOptions.push({});
+ const state = {test: true};
+ locals.navigation.pushState(state, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.getHistoryState()).toEqual(state);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.navigation.currentEntry.getHistoryState()).toEqual(state);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('replace URL with history state', async () => {
+ locals.pendingInterceptOptions.push({});
+ const state = {test: true};
+ locals.navigation.replaceState(state, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.getHistoryState()).toEqual(state);
+ expect(locals.navigation.currentEntry.getHistoryState()).toEqual(state);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('push URL with hashchange', async () => {
+ locals.navigation.pushState(null, '', '#test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.url).toBe('https://test.com/#test');
+ expect(navigateEvent.hashChange).toBeTrue();
+ expect(locals.navigation.currentEntry.url)
+ .toBe(
+ 'https://test.com/#test',
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('replace URL with hashchange', async () => {
+ locals.navigation.replaceState(null, '', '#test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(navigateEvent.destination.url).toBe('https://test.com/#test');
+ expect(navigateEvent.hashChange).toBeTrue();
+ expect(locals.navigation.currentEntry.url)
+ .toBe(
+ 'https://test.com/#test',
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ });
+
+ it('push URL with handler', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ });
+ locals.navigation.pushState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const currentEntry = locals.navigation.currentEntry;
+ expect(currentEntry.url).toBe('https://test.com/test');
+ expect(currentEntry.key).toBe('1');
+ expect(currentEntry.id).toBe('1');
+ expect(currentEntry.index).toBe(1);
+ expect(currentEntry.sameDocument).toBeTrue();
+ expect(currentEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ handlerFinishedResolve(undefined);
+ expect(locals.navigation.currentEntry).toBe(currentEntry);
+ });
+
+ it('replace URL with handler', async () => {
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ });
+ locals.navigation.replaceState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const currentEntry = locals.navigation.currentEntry;
+ expect(currentEntry.url).toBe('https://test.com/test');
+ expect(currentEntry.key).toBe('0');
+ expect(currentEntry.id).toBe('1');
+ expect(currentEntry.index).toBe(0);
+ expect(currentEntry.sameDocument).toBeTrue();
+ expect(currentEntry.getState()).toBeUndefined();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ handlerFinishedResolve(undefined);
+ expect(locals.navigation.currentEntry).toBe(currentEntry);
+ });
+
+ it('push with interruption', async () => {
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise(() => {}),
+ });
+
+ locals.navigation.pushState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ await interruptResult.committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(1);
+ await interruptResult.finished;
+ });
+
+ it('replace with interruption', async () => {
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise(() => {}),
+ });
+
+ locals.navigation.replaceState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ await interruptResult.committed;
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(1);
+ await interruptResult.finished;
+ });
+
+ it('push with handler reject', async () => {
+ let handlerFinishedReject!: (reason: unknown) => void;
+ const handlerPromise = new Promise((resolve, reject) => {
+ handlerFinishedReject = reject;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerPromise,
+ });
+
+ locals.navigation.pushState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ const error = new Error('rejected');
+ handlerFinishedReject(error);
+ await expectAsync(handlerPromise).toBeRejectedWith(error);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ });
+
+ it('replace with reject', async () => {
+ let handlerFinishedReject!: (reason: unknown) => void;
+ const handlerPromise = new Promise((resolve, reject) => {
+ handlerFinishedReject = reject;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerPromise,
+ });
+
+ locals.navigation.replaceState(null, '', '/test');
+ expect(locals.navigateEvents.length).toBe(1);
+ const navigateEvent = locals.navigateEvents[0];
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(0);
+ const error = new Error('rejected');
+ handlerFinishedReject(error);
+ await expectAsync(handlerPromise).toBeRejectedWith(error);
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ });
+ });
+
+ describe('traversal', () => {
+ it('go back', async () => {
+ expect(locals.navigation.canGoBack).toBeFalse();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ const [firstPageEntry, , thirdPageEntry] = await setUpEntries();
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ locals.navigation.go(-2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'traverse',
+ signal: jasmine.any(AbortSignal),
+ userInitiated: false,
+ destination: jasmine.objectContaining({
+ url: firstPageEntry.url!,
+ key: firstPageEntry.key,
+ id: firstPageEntry.id,
+ index: firstPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState())
+ .toEqual(
+ firstPageEntry.getState(),
+ );
+ expect(navigateEvent.destination.getHistoryState())
+ .toEqual(
+ firstPageEntry.getHistoryState(),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ from: jasmine.objectContaining({
+ url: thirdPageEntry.url!,
+ key: thirdPageEntry.key,
+ id: thirdPageEntry.id,
+ index: thirdPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toEqual(
+ thirdPageEntry.getState(),
+ );
+ expect(currentEntryChangeEvent.from.getHistoryState())
+ .toEqual(
+ thirdPageEntry.getHistoryState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeTrue();
+ });
+
+ it('go forward', async () => {
+ expect(locals.navigation.canGoBack).toBeFalse();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ const [firstPageEntry, , thirdPageEntry] = await setUpEntries();
+ await locals.navigation.traverseTo(firstPageEntry.key).finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeTrue();
+
+ locals.navigation.go(2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ canIntercept: true,
+ hashChange: false,
+ info: undefined,
+ navigationType: 'traverse',
+ signal: jasmine.any(AbortSignal),
+ userInitiated: false,
+ destination: jasmine.objectContaining({
+ url: thirdPageEntry.url!,
+ key: thirdPageEntry.key,
+ id: thirdPageEntry.id,
+ index: thirdPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(navigateEvent.destination.getState())
+ .toEqual(
+ thirdPageEntry.getState(),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ const currentEntryChangeEvent = locals.navigationCurrentEntryChangeEvents[0];
+ expect(currentEntryChangeEvent)
+ .toEqual(
+ jasmine.objectContaining({
+ navigationType: 'traverse',
+ from: jasmine.objectContaining({
+ url: firstPageEntry.url!,
+ key: firstPageEntry.key,
+ id: firstPageEntry.id,
+ index: firstPageEntry.index,
+ sameDocument: true,
+ }),
+ }),
+ );
+ expect(currentEntryChangeEvent.from.getState())
+ .toEqual(
+ firstPageEntry.getState(),
+ );
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toBeNull();
+ expect(locals.navigation.currentEntry).toBe(thirdPageEntry);
+ expect(locals.navigateEvents.length).toBe(1);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ expect(locals.navigation.canGoBack).toBeTrue();
+ expect(locals.navigation.canGoForward).toBeFalse();
+ });
+
+ it('go back with hashchange', async () => {
+ await setUpEntries({hash: true});
+
+ locals.navigation.go(-2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.hashChange).toBeTrue();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ });
+
+ it('go back with history state', async () => {
+ const [firstPageEntry] = setUpEntriesWithHistory();
+
+ locals.navigation.go(-2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.destination.getHistoryState())
+ .toEqual(
+ firstPageEntry.getHistoryState(),
+ );
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const popStateEvent = locals.popStateEvents[0];
+ expect(popStateEvent.state).toEqual(firstPageEntry.getHistoryState());
+ });
+
+ it('go forward with hashchange', async () => {
+ const [firstPageEntry] = await setUpEntries({hash: true});
+ await locals.navigation.traverseTo(firstPageEntry.key).finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+
+ locals.navigation.go(2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(navigateEvent.hashChange).toBeTrue();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ });
+
+ it('go with handler', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ let handlerFinishedResolve!: (
+ value: Promise|undefined,
+ ) => void;
+ const handlerFinished = new Promise((resolve) => {
+ handlerFinishedResolve = resolve;
+ });
+ locals.pendingInterceptOptions.push({
+ handler: () => handlerFinished,
+ });
+ locals.navigation.go(-2);
+ await locals.nextNavigateEvent();
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ handlerFinishedResolve(undefined);
+ });
+
+ it('go with interruption', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise(() => {}),
+ });
+ locals.navigation.go(-2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ expect(navigateEvent.signal.aborted).toBeFalse();
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ await interruptResult.committed;
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ await interruptResult.finished;
+ });
+
+ it('go with reject', async () => {
+ const [firstPageEntry] = await setUpEntries();
+ let handlerFinishedReject!: (reason: unknown) => void;
+ locals.pendingInterceptOptions.push({
+ handler: () => new Promise((resolve, reject) => {
+ handlerFinishedReject = reject;
+ }),
+ });
+
+ locals.navigation.go(-2);
+ const navigateEvent = await locals.nextNavigateEvent();
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ expect(navigateEvent.signal.aborted).toBeFalse();
+ const error = new Error('rejected');
+ handlerFinishedReject(error);
+ await new Promise((resolve) => {
+ navigateEvent.signal.addEventListener('abort', resolve);
+ });
+ expect(navigateEvent.signal.aborted).toBeTrue();
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ });
+
+ it('go synchronously', async () => {
+ const [, secondPageEntry] = await setUpEntries();
+ locals.navigation.setSynchronousTraversalsForTesting(true);
+
+ locals.navigation.go(-1);
+ // Synchronously navigates.
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ await expectAsync(locals.nextNavigateEvent()).toBePending();
+ });
+
+ it('go out of bounds', async () => {
+ locals.navigation.go(-1);
+ await expectAsync(locals.nextNavigateEvent()).toBePending();
+ locals.navigation.go(1);
+ await expectAsync(locals.nextNavigateEvent()).toBePending();
+ });
+
+ it('go queues', async () => {
+ const [firstPageEntry, secondPageEntry] = await setUpEntries();
+
+ locals.navigation.go(-1);
+ locals.navigation.go(-1);
+ const firstNavigateEvent = await locals.nextNavigateEvent();
+ expect(firstNavigateEvent.destination.key).toBe(secondPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const secondNavigateEvent = await locals.nextNavigateEvent();
+ expect(secondNavigateEvent.destination.key).toBe(firstPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ });
+
+ it('go queues both directions', async () => {
+ const [firstPageEntry, secondPageEntry] = await setUpEntries();
+
+ locals.navigation.go(-2);
+ locals.navigation.go(1);
+ const firstNavigateEvent = await locals.nextNavigateEvent();
+ expect(firstNavigateEvent.destination.key).toBe(firstPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const secondNavigateEvent = await locals.nextNavigateEvent();
+ expect(secondNavigateEvent.destination.key).toBe(secondPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ });
+
+ it('go queues with back', async () => {
+ const [firstPageEntry, secondPageEntry] = await setUpEntries();
+
+ locals.navigation.back();
+ locals.navigation.go(-1);
+ const firstNavigateEvent = await locals.nextNavigateEvent();
+ expect(firstNavigateEvent.destination.key).toBe(secondPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const secondNavigateEvent = await locals.nextNavigateEvent();
+ expect(secondNavigateEvent.destination.key).toBe(firstPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(firstPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ });
+
+ it('go queues with forward', async () => {
+ const [, secondPageEntry, thirdPageEntry] = await setUpEntries();
+ await locals.navigation.back().finished;
+ locals.navigateEvents.length = 0;
+ locals.navigationCurrentEntryChangeEvents.length = 0;
+ locals.popStateEvents.length = 0;
+
+ locals.navigation.forward();
+ locals.navigation.go(-1);
+ const firstNavigateEvent = await locals.nextNavigateEvent();
+ expect(firstNavigateEvent.destination.key).toBe(thirdPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(thirdPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const secondNavigateEvent = await locals.nextNavigateEvent();
+ expect(secondNavigateEvent.destination.key).toBe(secondPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ });
+
+ it('go after synchronous navigate', async () => {
+ const [, secondPageEntry, thirdPageEntry] = await setUpEntries();
+
+ // Back to /page2
+ locals.navigation.go(-1);
+ // Push /interrupt on top of current /page3
+ locals.pendingInterceptOptions.push({});
+ const interruptResult = locals.navigation.navigate('/interrupt');
+ // Back from /interrupt to /page3.
+ locals.navigation.go(-1);
+ const interruptEntry = await interruptResult.finished;
+ expect(locals.navigation.currentEntry).toBe(interruptEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(1);
+ expect(locals.popStateEvents.length).toBe(1);
+ const firstNavigateEvent = await locals.nextNavigateEvent();
+ expect(firstNavigateEvent.destination.key).toBe(secondPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(secondPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(2);
+ expect(locals.popStateEvents.length).toBe(2);
+ const secondNavigateEvent = await locals.nextNavigateEvent();
+ expect(secondNavigateEvent.destination.key).toBe(thirdPageEntry.key);
+ expect(locals.navigation.currentEntry).toBe(thirdPageEntry);
+ expect(locals.navigationCurrentEntryChangeEvents.length).toBe(3);
+ expect(locals.popStateEvents.length).toBe(3);
+ });
+ });
+ });
+});
diff --git a/packages/common/testing/src/navigation/fake_navigation.ts b/packages/common/testing/src/navigation/fake_navigation.ts
new file mode 100644
index 00000000000000..d7fd4f394f0cce
--- /dev/null
+++ b/packages/common/testing/src/navigation/fake_navigation.ts
@@ -0,0 +1,969 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+// Prevents deletion of `Event` from `globalThis` during module loading.
+const Event = globalThis.Event;
+
+/**
+ * Fake implementation of user agent history and navigation behavior. This is a
+ * high-fidelity implementation of browser behavior that attempts to emulate
+ * things like traversal delay.
+ */
+export class FakeNavigation implements Navigation {
+ /**
+ * The fake implementation of an entries array. Only same-document entries
+ * allowed.
+ */
+ private readonly entriesArr: FakeNavigationHistoryEntry[] = [];
+
+ /**
+ * The current active entry index into `entriesArr`.
+ */
+ private currentEntryIndex = 0;
+
+ /**
+ * The current navigate event.
+ */
+ private navigateEvent: InternalFakeNavigateEvent|undefined = undefined;
+
+ /**
+ * A Map of pending traversals, so that traversals to the same entry can be
+ * re-used.
+ */
+ private readonly traversalQueue = new Map();
+
+ /**
+ * A Promise that resolves when the previous traversals have finished. Used to
+ * simulate the cross-process communication necessary for traversals.
+ */
+ private nextTraversal = Promise.resolve();
+
+ /**
+ * A prospective current active entry index, which includes unresolved
+ * traversals. Used by `go` to determine where navigations are intended to go.
+ */
+ private prospectiveEntryIndex = 0;
+
+ /**
+ * A test-only option to make traversals synchronous, rather than emulate
+ * cross-process communication.
+ */
+ private synchronousTraversals = false;
+
+ /** Whether to allow a call to setInitialEntryForTesting. */
+ private canSetInitialEntry = true;
+
+ /** `EventTarget` to dispatch events. */
+ private eventTarget: EventTarget = this.window.document.createElement('div');
+
+ /** The next unique id for created entries. Replace recreates this id. */
+ private nextId = 0;
+
+ /** The next unique key for created entries. Replace inherits this id. */
+ private nextKey = 0;
+
+ /** Whether this fake is disposed. */
+ private disposed = false;
+
+ /** Equivalent to `navigation.currentEntry`. */
+ get currentEntry(): FakeNavigationHistoryEntry {
+ return this.entriesArr[this.currentEntryIndex];
+ }
+
+ get canGoBack(): boolean {
+ return this.currentEntryIndex > 0;
+ }
+
+ get canGoForward(): boolean {
+ return this.currentEntryIndex < this.entriesArr.length - 1;
+ }
+
+ constructor(private readonly window: Window, private readonly baseURI: string) {
+ // First entry.
+ this.setInitialEntryForTesting('.');
+ }
+
+ /**
+ * Sets the initial entry.
+ */
+ setInitialEntryForTesting(
+ url: string,
+ options: {
+ historyState: unknown;
+ // Allows setting the URL without resolving it against the base.
+ absoluteUrl?: boolean;
+ state?: unknown;
+ } = {historyState: null},
+ ) {
+ if (!this.canSetInitialEntry) {
+ throw new Error(
+ 'setInitialEntryForTesting can only be called before any ' +
+ 'navigation has occurred',
+ );
+ }
+ const currentInitialEntry = this.entriesArr[0];
+ this.entriesArr[0] = new FakeNavigationHistoryEntry(
+ options.absoluteUrl ? url : new URL(url, this.baseURI).toString(),
+ {
+ index: 0,
+ key: currentInitialEntry?.key ?? String(this.nextKey++),
+ id: currentInitialEntry?.id ?? String(this.nextId++),
+ sameDocument: true,
+ historyState: options?.historyState,
+ state: options.state,
+ },
+ );
+ }
+
+ /** Returns whether the initial entry is still eligible to be set. */
+ canSetInitialEntryForTesting(): boolean {
+ return this.canSetInitialEntry;
+ }
+
+ /**
+ * Sets whether to emulate traversals as synchronous rather than
+ * asynchronous.
+ */
+ setSynchronousTraversalsForTesting(synchronousTraversals: boolean) {
+ this.synchronousTraversals = synchronousTraversals;
+ }
+
+ /** Equivalent to `navigation.entries()`. */
+ entries(): FakeNavigationHistoryEntry[] {
+ return this.entriesArr.slice();
+ }
+
+ /** Equivalent to `navigation.navigate()`. */
+ navigate(
+ url: string,
+ options?: NavigationNavigateOptions,
+ ): FakeNavigationResult {
+ const fromUrl = new URL(this.currentEntry.url!, this.baseURI);
+ const toUrl = new URL(url, this.baseURI);
+
+ let navigationType: NavigationTypeString;
+ if (!options?.history || options.history === 'auto') {
+ // Auto defaults to push, but if the URLs are the same, is a replace.
+ if (fromUrl.toString() === toUrl.toString()) {
+ navigationType = 'replace';
+ } else {
+ navigationType = 'push';
+ }
+ } else {
+ navigationType = options.history;
+ }
+
+ const hashChange = isHashChange(fromUrl, toUrl);
+
+ const destination = new FakeNavigationDestination({
+ url: toUrl.toString(),
+ state: options?.state,
+ sameDocument: hashChange,
+ historyState: null,
+ });
+ const result = new InternalNavigationResult();
+
+ this.userAgentNavigate(destination, result, {
+ navigationType,
+ cancelable: true,
+ canIntercept: true,
+ // Always false for navigate().
+ userInitiated: false,
+ hashChange,
+ info: options?.info,
+ });
+
+ return {
+ committed: result.committed,
+ finished: result.finished,
+ };
+ }
+
+ /** Equivalent to `history.pushState()`. */
+ pushState(data: unknown, title: string, url?: string): void {
+ this.pushOrReplaceState('push', data, title, url);
+ }
+
+ /** Equivalent to `history.replaceState()`. */
+ replaceState(data: unknown, title: string, url?: string): void {
+ this.pushOrReplaceState('replace', data, title, url);
+ }
+
+ private pushOrReplaceState(
+ navigationType: NavigationTypeString,
+ data: unknown,
+ _title: string,
+ url?: string,
+ ): void {
+ const fromUrl = new URL(this.currentEntry.url!, this.baseURI);
+ const toUrl = url ? new URL(url, this.baseURI) : fromUrl;
+
+ const hashChange = isHashChange(fromUrl, toUrl);
+
+ const destination = new FakeNavigationDestination({
+ url: toUrl.toString(),
+ sameDocument: true,
+ historyState: data,
+ });
+ const result = new InternalNavigationResult();
+
+ this.userAgentNavigate(destination, result, {
+ navigationType,
+ cancelable: true,
+ canIntercept: true,
+ // Always false for pushState() or replaceState().
+ userInitiated: false,
+ hashChange,
+ skipPopState: true,
+ });
+ }
+
+ /** Equivalent to `navigation.traverseTo()`. */
+ traverseTo(key: string, options?: NavigationOptions): FakeNavigationResult {
+ const fromUrl = new URL(this.currentEntry.url!, this.baseURI);
+ const entry = this.findEntry(key);
+ if (!entry) {
+ const domException = new DOMException(
+ 'Invalid key',
+ 'InvalidStateError',
+ );
+ const committed = Promise.reject(domException);
+ const finished = Promise.reject(domException);
+ committed.catch(() => {});
+ finished.catch(() => {});
+ return {
+ committed,
+ finished,
+ };
+ }
+ if (entry === this.currentEntry) {
+ return {
+ committed: Promise.resolve(this.currentEntry),
+ finished: Promise.resolve(this.currentEntry),
+ };
+ }
+ if (this.traversalQueue.has(entry.key)) {
+ const existingResult = this.traversalQueue.get(entry.key)!;
+ return {
+ committed: existingResult.committed,
+ finished: existingResult.finished,
+ };
+ }
+
+ const hashChange = isHashChange(fromUrl, new URL(entry.url!, this.baseURI));
+ const destination = new FakeNavigationDestination({
+ url: entry.url!,
+ state: entry.getState(),
+ historyState: entry.getHistoryState(),
+ key: entry.key,
+ id: entry.id,
+ index: entry.index,
+ sameDocument: entry.sameDocument,
+ });
+ this.prospectiveEntryIndex = entry.index;
+ const result = new InternalNavigationResult();
+ this.traversalQueue.set(entry.key, result);
+ this.runTraversal(() => {
+ this.traversalQueue.delete(entry.key);
+ this.userAgentNavigate(destination, result, {
+ navigationType: 'traverse',
+ cancelable: true,
+ canIntercept: true,
+ // Always false for traverseTo().
+ userInitiated: false,
+ hashChange,
+ info: options?.info,
+ });
+ });
+ return {
+ committed: result.committed,
+ finished: result.finished,
+ };
+ }
+
+ /** Equivalent to `navigation.back()`. */
+ back(options?: NavigationOptions): FakeNavigationResult {
+ if (this.currentEntryIndex === 0) {
+ const domException = new DOMException(
+ 'Cannot go back',
+ 'InvalidStateError',
+ );
+ const committed = Promise.reject(domException);
+ const finished = Promise.reject(domException);
+ committed.catch(() => {});
+ finished.catch(() => {});
+ return {
+ committed,
+ finished,
+ };
+ }
+ const entry = this.entriesArr[this.currentEntryIndex - 1];
+ return this.traverseTo(entry.key, options);
+ }
+
+ /** Equivalent to `navigation.forward()`. */
+ forward(options?: NavigationOptions): FakeNavigationResult {
+ if (this.currentEntryIndex === this.entriesArr.length - 1) {
+ const domException = new DOMException(
+ 'Cannot go forward',
+ 'InvalidStateError',
+ );
+ const committed = Promise.reject(domException);
+ const finished = Promise.reject(domException);
+ committed.catch(() => {});
+ finished.catch(() => {});
+ return {
+ committed,
+ finished,
+ };
+ }
+ const entry = this.entriesArr[this.currentEntryIndex + 1];
+ return this.traverseTo(entry.key, options);
+ }
+
+ /**
+ * Equivalent to `history.go()`.
+ * Note that this method does not actually work precisely to how Chrome
+ * does, instead choosing a simpler model with less unexpected behavior.
+ * Chrome has a few edge case optimizations, for instance with repeated
+ * `back(); forward()` chains it collapses certain traversals.
+ */
+ go(direction: number): void {
+ const targetIndex = this.prospectiveEntryIndex + direction;
+ if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
+ return;
+ }
+ this.prospectiveEntryIndex = targetIndex;
+ this.runTraversal(() => {
+ // Check again that destination is in the entries array.
+ if (targetIndex >= this.entriesArr.length || targetIndex < 0) {
+ return;
+ }
+ const fromUrl = new URL(this.currentEntry.url!, this.baseURI);
+ const entry = this.entriesArr[targetIndex];
+ const hashChange = isHashChange(fromUrl, new URL(entry.url!, this.baseURI));
+ const destination = new FakeNavigationDestination({
+ url: entry.url!,
+ state: entry.getState(),
+ historyState: entry.getHistoryState(),
+ key: entry.key,
+ id: entry.id,
+ index: entry.index,
+ sameDocument: entry.sameDocument,
+ });
+ const result = new InternalNavigationResult();
+ this.userAgentNavigate(destination, result, {
+ navigationType: 'traverse',
+ cancelable: true,
+ canIntercept: true,
+ // Always false for go().
+ userInitiated: false,
+ hashChange,
+ });
+ });
+ }
+
+ /** Runs a traversal synchronously or asynchronously */
+ private runTraversal(traversal: () => void) {
+ if (this.synchronousTraversals) {
+ traversal();
+ return;
+ }
+
+ // Each traversal occupies a single timeout resolution.
+ // This means that Promises added to commit and finish should resolve
+ // before the next traversal.
+ this.nextTraversal = this.nextTraversal.then(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ traversal();
+ });
+ });
+ });
+ }
+
+ /** Equivalent to `navigation.addEventListener()`. */
+ addEventListener(
+ type: string,
+ callback: EventListenerOrEventListenerObject,
+ options?: AddEventListenerOptions|boolean,
+ ) {
+ this.eventTarget.addEventListener(type, callback, options);
+ }
+
+ /** Equivalent to `navigation.removeEventListener()`. */
+ removeEventListener(
+ type: string,
+ callback: EventListenerOrEventListenerObject,
+ options?: EventListenerOptions|boolean,
+ ) {
+ this.eventTarget.removeEventListener(type, callback, options);
+ }
+
+ /** Equivalent to `navigation.dispatchEvent()` */
+ dispatchEvent(event: Event): boolean {
+ return this.eventTarget.dispatchEvent(event);
+ }
+
+ /** Cleans up resources. */
+ dispose() {
+ // Recreate eventTarget to release current listeners.
+ // `document.createElement` because NodeJS `EventTarget` is incompatible with Domino's `Event`.
+ this.eventTarget = this.window.document.createElement('div');
+ this.disposed = true;
+ }
+
+ /** Returns whether this fake is disposed. */
+ isDisposed() {
+ return this.disposed;
+ }
+
+ /** Implementation for all navigations and traversals. */
+ private userAgentNavigate(
+ destination: FakeNavigationDestination,
+ result: InternalNavigationResult,
+ options: InternalNavigateOptions,
+ ) {
+ // The first navigation should disallow any future calls to set the initial
+ // entry.
+ this.canSetInitialEntry = false;
+ if (this.navigateEvent) {
+ this.navigateEvent.cancel(
+ new DOMException('Navigation was aborted', 'AbortError'),
+ );
+ this.navigateEvent = undefined;
+ }
+
+ const navigateEvent = createFakeNavigateEvent({
+ navigationType: options.navigationType,
+ cancelable: options.cancelable,
+ canIntercept: options.canIntercept,
+ userInitiated: options.userInitiated,
+ hashChange: options.hashChange,
+ signal: result.signal,
+ destination,
+ info: options.info,
+ sameDocument: destination.sameDocument,
+ skipPopState: options.skipPopState,
+ result,
+ userAgentCommit: () => {
+ this.userAgentCommit();
+ },
+ });
+
+ this.navigateEvent = navigateEvent;
+ this.eventTarget.dispatchEvent(navigateEvent);
+ navigateEvent.dispatchedNavigateEvent();
+ if (navigateEvent.commitOption === 'immediate') {
+ navigateEvent.commit(/* internal= */ true);
+ }
+ }
+
+ /** Implementation to commit a navigation. */
+ private userAgentCommit() {
+ if (!this.navigateEvent) {
+ return;
+ }
+ const from = this.currentEntry;
+ if (!this.navigateEvent.sameDocument) {
+ const error = new Error('Cannot navigate to a non-same-document URL.');
+ this.navigateEvent.cancel(error);
+ throw error;
+ }
+ if (this.navigateEvent.navigationType === 'push' ||
+ this.navigateEvent.navigationType === 'replace') {
+ this.userAgentPushOrReplace(this.navigateEvent.destination, {
+ navigationType: this.navigateEvent.navigationType,
+ });
+ } else if (this.navigateEvent.navigationType === 'traverse') {
+ this.userAgentTraverse(this.navigateEvent.destination);
+ }
+ this.navigateEvent.userAgentNavigated(this.currentEntry);
+ const currentEntryChangeEvent = createFakeNavigationCurrentEntryChangeEvent(
+ {from, navigationType: this.navigateEvent.navigationType},
+ );
+ this.eventTarget.dispatchEvent(currentEntryChangeEvent);
+ if (!this.navigateEvent.skipPopState) {
+ const popStateEvent = createPopStateEvent({
+ state: this.navigateEvent.destination.getHistoryState(),
+ });
+ this.window.dispatchEvent(popStateEvent);
+ }
+ }
+
+ /** Implementation for a push or replace navigation. */
+ private userAgentPushOrReplace(
+ destination: FakeNavigationDestination,
+ {navigationType}: {navigationType: NavigationTypeString},
+ ) {
+ if (navigationType === 'push') {
+ this.currentEntryIndex++;
+ this.prospectiveEntryIndex = this.currentEntryIndex;
+ }
+ const index = this.currentEntryIndex;
+ const key = navigationType === 'push' ? String(this.nextKey++) : this.currentEntry.key;
+ const entry = new FakeNavigationHistoryEntry(destination.url, {
+ id: String(this.nextId++),
+ key,
+ index,
+ sameDocument: true,
+ state: destination.getState(),
+ historyState: destination.getHistoryState(),
+ });
+ if (navigationType === 'push') {
+ this.entriesArr.splice(index, Infinity, entry);
+ } else {
+ this.entriesArr[index] = entry;
+ }
+ }
+
+ /** Implementation for a traverse navigation. */
+ private userAgentTraverse(destination: FakeNavigationDestination) {
+ this.currentEntryIndex = destination.index;
+ }
+
+ /** Utility method for finding entries with the given `key`. */
+ private findEntry(key: string) {
+ for (const entry of this.entriesArr) {
+ if (entry.key === key) return entry;
+ }
+ return undefined;
+ }
+
+ set onnavigate(_handler: ((this: Navigation, ev: NavigateEvent) => any)|null) {
+ throw new Error('unimplemented');
+ }
+
+ get onnavigate(): ((this: Navigation, ev: NavigateEvent) => any)|null {
+ throw new Error('unimplemented');
+ }
+
+ set oncurrententrychange(_handler:
+ ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)|
+ null) {
+ throw new Error('unimplemented');
+ }
+
+ get oncurrententrychange():
+ ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)|null {
+ throw new Error('unimplemented');
+ }
+
+ set onnavigatesuccess(_handler: ((this: Navigation, ev: Event) => any)|null) {
+ throw new Error('unimplemented');
+ }
+
+ get onnavigatesuccess(): ((this: Navigation, ev: Event) => any)|null {
+ throw new Error('unimplemented');
+ }
+
+ set onnavigateerror(_handler: ((this: Navigation, ev: ErrorEvent) => any)|null) {
+ throw new Error('unimplemented');
+ }
+
+ get onnavigateerror(): ((this: Navigation, ev: ErrorEvent) => any)|null {
+ throw new Error('unimplemented');
+ }
+
+ get transition(): NavigationTransition|null {
+ throw new Error('unimplemented');
+ }
+
+ updateCurrentEntry(_options: NavigationUpdateCurrentEntryOptions): void {
+ throw new Error('unimplemented');
+ }
+
+ reload(_options?: NavigationReloadOptions): NavigationResult {
+ throw new Error('unimplemented');
+ }
+}
+
+/**
+ * Fake equivalent of the `NavigationResult` interface with
+ * `FakeNavigationHistoryEntry`.
+ */
+interface FakeNavigationResult extends NavigationResult {
+ readonly committed: Promise;
+ readonly finished: Promise;
+}
+
+/**
+ * Fake equivalent of `NavigationHistoryEntry`.
+ */
+export class FakeNavigationHistoryEntry implements NavigationHistoryEntry {
+ readonly sameDocument;
+
+ readonly id: string;
+ readonly key: string;
+ readonly index: number;
+ private readonly state: unknown;
+ private readonly historyState: unknown;
+
+ // tslint:disable-next-line:no-any
+ ondispose: ((this: NavigationHistoryEntry, ev: Event) => any)|null = null;
+
+ constructor(
+ readonly url: string|null,
+ {
+ id,
+ key,
+ index,
+ sameDocument,
+ state,
+ historyState,
+ }: {
+ id: string; key: string; index: number; sameDocument: boolean; historyState: unknown;
+ state?: unknown;
+ },
+ ) {
+ this.id = id;
+ this.key = key;
+ this.index = index;
+ this.sameDocument = sameDocument;
+ this.state = state;
+ this.historyState = historyState;
+ }
+
+ getState(): unknown {
+ // Budget copy.
+ return this.state ? JSON.parse(JSON.stringify(this.state)) : this.state;
+ }
+
+ getHistoryState(): unknown {
+ // Budget copy.
+ return this.historyState ? JSON.parse(JSON.stringify(this.historyState)) : this.historyState;
+ }
+
+ addEventListener(
+ type: string,
+ callback: EventListenerOrEventListenerObject,
+ options?: AddEventListenerOptions|boolean,
+ ) {
+ throw new Error('unimplemented');
+ }
+
+ removeEventListener(
+ type: string,
+ callback: EventListenerOrEventListenerObject,
+ options?: EventListenerOptions|boolean,
+ ) {
+ throw new Error('unimplemented');
+ }
+
+ dispatchEvent(event: Event): boolean {
+ throw new Error('unimplemented');
+ }
+}
+
+/** `NavigationInterceptOptions` with experimental commit option. */
+export interface ExperimentalNavigationInterceptOptions extends NavigationInterceptOptions {
+ commit?: 'immediate'|'after-transition';
+}
+
+/** `NavigateEvent` with experimental commit function. */
+export interface ExperimentalNavigateEvent extends NavigateEvent {
+ intercept(options?: ExperimentalNavigationInterceptOptions): void;
+
+ commit(): void;
+}
+
+/**
+ * Fake equivalent of `NavigateEvent`.
+ */
+export interface FakeNavigateEvent extends ExperimentalNavigateEvent {
+ readonly destination: FakeNavigationDestination;
+}
+
+interface InternalFakeNavigateEvent extends FakeNavigateEvent {
+ readonly sameDocument: boolean;
+ readonly skipPopState?: boolean;
+ readonly commitOption: 'after-transition'|'immediate';
+ readonly result: InternalNavigationResult;
+
+ commit(internal?: boolean): void;
+ cancel(reason: Error): void;
+ dispatchedNavigateEvent(): void;
+ userAgentNavigated(entry: FakeNavigationHistoryEntry): void;
+}
+
+/**
+ * Create a fake equivalent of `NavigateEvent`. This is not a class because ES5
+ * transpiled JavaScript cannot extend native Event.
+ */
+function createFakeNavigateEvent({
+ cancelable,
+ canIntercept,
+ userInitiated,
+ hashChange,
+ navigationType,
+ signal,
+ destination,
+ info,
+ sameDocument,
+ skipPopState,
+ result,
+ userAgentCommit,
+}: {
+ cancelable: boolean; canIntercept: boolean; userInitiated: boolean; hashChange: boolean;
+ navigationType: NavigationTypeString;
+ signal: AbortSignal;
+ destination: FakeNavigationDestination;
+ info: unknown;
+ sameDocument: boolean;
+ skipPopState?: boolean; result: InternalNavigationResult; userAgentCommit: () => void;
+}) {
+ const event = new Event('navigate', {bubbles: false, cancelable}) as {
+ -readonly[P in keyof InternalFakeNavigateEvent]: InternalFakeNavigateEvent[P];
+ };
+ event.canIntercept = canIntercept;
+ event.userInitiated = userInitiated;
+ event.hashChange = hashChange;
+ event.navigationType = navigationType;
+ event.signal = signal;
+ event.destination = destination;
+ event.info = info;
+ event.downloadRequest = null;
+ event.formData = null;
+
+ event.sameDocument = sameDocument;
+ event.skipPopState = skipPopState;
+ event.commitOption = 'immediate';
+
+ let handlerFinished: Promise|undefined = undefined;
+ let interceptCalled = false;
+ let dispatchedNavigateEvent = false;
+ let commitCalled = false;
+
+ event.intercept = function(
+ this: InternalFakeNavigateEvent,
+ options?: ExperimentalNavigationInterceptOptions,
+ ): void {
+ interceptCalled = true;
+ event.sameDocument = true;
+ const handler = options?.handler;
+ if (handler) {
+ handlerFinished = handler();
+ }
+ if (options?.commit) {
+ event.commitOption = options.commit;
+ }
+ if (options?.focusReset !== undefined || options?.scroll !== undefined) {
+ throw new Error('unimplemented');
+ }
+ };
+
+ event.scroll = function(this: InternalFakeNavigateEvent): void {
+ throw new Error('unimplemented');
+ };
+
+ event.commit = function(this: InternalFakeNavigateEvent, internal = false) {
+ if (!internal && !interceptCalled) {
+ throw new DOMException(
+ `Failed to execute 'commit' on 'NavigateEvent': intercept() must be ` +
+ `called before commit().`,
+ 'InvalidStateError',
+ );
+ }
+ if (!dispatchedNavigateEvent) {
+ throw new DOMException(
+ `Failed to execute 'commit' on 'NavigateEvent': commit() may not be ` +
+ `called during event dispatch.`,
+ 'InvalidStateError',
+ );
+ }
+ if (commitCalled) {
+ throw new DOMException(
+ `Failed to execute 'commit' on 'NavigateEvent': commit() already ` +
+ `called.`,
+ 'InvalidStateError',
+ );
+ }
+ commitCalled = true;
+
+ userAgentCommit();
+ };
+
+ // Internal only.
+ event.cancel = function(this: InternalFakeNavigateEvent, reason: Error) {
+ result.committedReject(reason);
+ result.finishedReject(reason);
+ };
+
+ // Internal only.
+ event.dispatchedNavigateEvent = function(this: InternalFakeNavigateEvent) {
+ dispatchedNavigateEvent = true;
+ if (event.commitOption === 'after-transition') {
+ // If handler finishes before commit, call commit.
+ handlerFinished?.then(
+ () => {
+ if (!commitCalled) {
+ event.commit(/* internal */ true);
+ }
+ },
+ () => {},
+ );
+ }
+ Promise.all([result.committed, handlerFinished])
+ .then(
+ ([entry]) => {
+ result.finishedResolve(entry);
+ },
+ (reason) => {
+ result.finishedReject(reason);
+ },
+ );
+ };
+
+ // Internal only.
+ event.userAgentNavigated = function(
+ this: InternalFakeNavigateEvent,
+ entry: FakeNavigationHistoryEntry,
+ ) {
+ result.committedResolve(entry);
+ };
+
+ return event as InternalFakeNavigateEvent;
+}
+
+/** Fake equivalent of `NavigationCurrentEntryChangeEvent`. */
+export interface FakeNavigationCurrentEntryChangeEvent extends NavigationCurrentEntryChangeEvent {
+ readonly from: FakeNavigationHistoryEntry;
+}
+
+/**
+ * Create a fake equivalent of `NavigationCurrentEntryChange`. This does not use
+ * a class because ES5 transpiled JavaScript cannot extend native Event.
+ */
+function createFakeNavigationCurrentEntryChangeEvent({
+ from,
+ navigationType,
+}: {from: FakeNavigationHistoryEntry; navigationType: NavigationTypeString;}) {
+ const event = new Event('currententrychange', {
+ bubbles: false,
+ cancelable: false,
+ }) as {
+ -readonly[P in keyof NavigationCurrentEntryChangeEvent]: NavigationCurrentEntryChangeEvent[P];
+ };
+ event.from = from;
+ event.navigationType = navigationType;
+ return event as FakeNavigationCurrentEntryChangeEvent;
+}
+
+/**
+ * Create a fake equivalent of `PopStateEvent`. This does not use a class
+ * because ES5 transpiled JavaScript cannot extend native Event.
+ */
+function createPopStateEvent({state}: {state: unknown}) {
+ const event = new Event('popstate', {
+ bubbles: false,
+ cancelable: false,
+ }) as {-readonly[P in keyof PopStateEvent]: PopStateEvent[P]};
+ event.state = state;
+ return event as PopStateEvent;
+}
+
+/**
+ * Fake equivalent of `NavigationDestination`.
+ */
+export class FakeNavigationDestination implements NavigationDestination {
+ readonly url: string;
+ readonly sameDocument: boolean;
+ readonly key: string|null;
+ readonly id: string|null;
+ readonly index: number;
+
+ private readonly state?: unknown;
+ private readonly historyState: unknown;
+
+ constructor({
+ url,
+ sameDocument,
+ historyState,
+ state,
+ key = null,
+ id = null,
+ index = -1,
+ }: {
+ url: string; sameDocument: boolean; historyState: unknown;
+ state?: unknown;
+ key?: string | null;
+ id?: string | null;
+ index?: number;
+ }) {
+ this.url = url;
+ this.sameDocument = sameDocument;
+ this.state = state;
+ this.historyState = historyState;
+ this.key = key;
+ this.id = id;
+ this.index = index;
+ }
+
+ getState(): unknown {
+ return this.state;
+ }
+
+ getHistoryState(): unknown {
+ return this.historyState;
+ }
+}
+
+/** Utility function to determine whether two UrlLike have the same hash. */
+function isHashChange(from: URL, to: URL): boolean {
+ return (
+ to.hash !== from.hash && to.hostname === from.hostname && to.pathname === from.pathname &&
+ to.search === from.search);
+}
+
+/** Internal utility class for representing the result of a navigation. */
+class InternalNavigationResult {
+ committedResolve!: (entry: FakeNavigationHistoryEntry) => void;
+ committedReject!: (reason: Error) => void;
+ finishedResolve!: (entry: FakeNavigationHistoryEntry) => void;
+ finishedReject!: (reason: Error) => void;
+ readonly committed: Promise;
+ readonly finished: Promise;
+ get signal(): AbortSignal {
+ return this.abortController.signal;
+ }
+ private readonly abortController = new AbortController();
+
+ constructor() {
+ this.committed = new Promise(
+ (resolve, reject) => {
+ this.committedResolve = resolve;
+ this.committedReject = reject;
+ },
+ );
+
+ this.finished = new Promise(
+ async (resolve, reject) => {
+ this.finishedResolve = resolve;
+ this.finishedReject = (reason: Error) => {
+ reject(reason);
+ this.abortController.abort(reason);
+ };
+ },
+ );
+ // All rejections are handled.
+ this.committed.catch(() => {});
+ this.finished.catch(() => {});
+ }
+}
+
+/** Internal options for performing a navigate. */
+interface InternalNavigateOptions {
+ navigationType: NavigationTypeString;
+ cancelable: boolean;
+ canIntercept: boolean;
+ userInitiated: boolean;
+ hashChange: boolean;
+ info?: unknown;
+ skipPopState?: boolean;
+}
diff --git a/packages/common/testing/src/navigation/provide_fake_platform_navigation.ts b/packages/common/testing/src/navigation/provide_fake_platform_navigation.ts
new file mode 100644
index 00000000000000..e6a71ac67ab5ea
--- /dev/null
+++ b/packages/common/testing/src/navigation/provide_fake_platform_navigation.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Provider} from '@angular/core';
+
+// @ng_package: ignore-cross-repo-import
+import {PlatformNavigation} from '../../../src/navigation/platform_navigation';
+
+import {FakeNavigation} from './fake_navigation';
+
+/**
+ * Return a provider for the `FakeNavigation` in place of the real Navigation API.
+ *
+ * @internal
+ */
+export function provideFakePlatformNavigation(): Provider[] {
+ return [
+ {
+ provide: PlatformNavigation,
+ useFactory: () => {
+ return new FakeNavigation(window, 'https://test.com');
+ }
+ },
+ ];
+}
diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json
index 94f94fe533a642..4c9c4147f428f8 100644
--- a/packages/compiler-cli/package.json
+++ b/packages/compiler-cli/package.json
@@ -54,7 +54,7 @@
},
"peerDependencies": {
"@angular/compiler": "0.0.0-PLACEHOLDER",
- "typescript": ">=5.2 <5.3"
+ "typescript": ">=5.2 <5.4"
},
"repository": {
"type": "git",
diff --git a/packages/compiler-cli/src/ngtsc/core/src/host.ts b/packages/compiler-cli/src/ngtsc/core/src/host.ts
index 0b1ad3b8fecd51..555d9dd936709c 100644
--- a/packages/compiler-cli/src/ngtsc/core/src/host.ts
+++ b/packages/compiler-cli/src/ngtsc/core/src/host.ts
@@ -61,6 +61,7 @@ export class DelegatingCompilerHost implements
hasInvalidatedResolutions;
resolveModuleNameLiterals;
resolveTypeReferenceDirectiveReferences;
+ jsDocParsingMode;
constructor(protected delegate: ExtendedTsCompilerHost) {
// Excluded are 'getSourceFile' and 'fileExists', which are actually implemented by
@@ -96,6 +97,9 @@ export class DelegatingCompilerHost implements
this.resolveModuleNameLiterals = this.delegateMethod('resolveModuleNameLiterals');
this.resolveTypeReferenceDirectiveReferences =
this.delegateMethod('resolveTypeReferenceDirectiveReferences');
+ // TODO(crisbeto): can be removed when we drop support for TS 5.2.
+ // @ts-ignore
+ this.jsDocParsingMode = this.delegateMethod('jsDocParsingMode');
}
private delegateMethod(name: M):
diff --git a/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts
index e9b7d3171d6cb0..426d73baf30af8 100644
--- a/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts
+++ b/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts
@@ -43,7 +43,10 @@ function extractEnumMembers(
function getEnumMemberValue(memberNode: ts.EnumMember): string {
// If the enum member has a child number literal or string literal,
// we use that literal as the "value" of the member.
- const literal =
- memberNode.getChildren().find(n => ts.isNumericLiteral(n) || ts.isStringLiteral(n));
+ const literal = memberNode.getChildren().find(n => {
+ return ts.isNumericLiteral(n) || ts.isStringLiteral(n) ||
+ (ts.isPrefixUnaryExpression(n) && n.operator === ts.SyntaxKind.MinusToken &&
+ ts.isNumericLiteral(n.operand));
+ });
return literal?.getText() ?? '';
}
diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts
index 07d84bb5831ba2..23349cc0e43fa0 100644
--- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts
@@ -198,6 +198,10 @@ runInEachFileSystem(() => {
expect(evaluate('const a = null;', 'a')).toEqual(null);
});
+ it('supports negative numbers', () => {
+ expect(evaluate('const a = -123;', 'a')).toEqual(-123);
+ });
+
it('supports destructuring array variable declarations', () => {
const code = `
const [a, b, c, d] = [0, 1, 2, 3];
diff --git a/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts b/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts
index 1a539faea9d748..5ce7dd44e07ffc 100644
--- a/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts
+++ b/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts
@@ -46,6 +46,7 @@ export class DelegatingCompilerHost implements
hasInvalidatedResolutions;
resolveModuleNameLiterals;
resolveTypeReferenceDirectiveReferences;
+ jsDocParsingMode;
constructor(protected delegate: ts.CompilerHost) {
// Excluded are 'getSourceFile', 'fileExists' and 'writeFile', which are actually implemented by
@@ -75,6 +76,9 @@ export class DelegatingCompilerHost implements
this.resolveModuleNameLiterals = this.delegateMethod('resolveModuleNameLiterals');
this.resolveTypeReferenceDirectiveReferences =
this.delegateMethod('resolveTypeReferenceDirectiveReferences');
+ // TODO(crisbeto): can be removed when we drop support for TS 5.2.
+ // @ts-ignore
+ this.jsDocParsingMode = this.delegateMethod('jsDocParsingMode');
}
private delegateMethod(name: M): ts.CompilerHost[M] {
diff --git a/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts
new file mode 100644
index 00000000000000..d1603700625f50
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts
@@ -0,0 +1,23 @@
+/*!
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import ts from 'typescript';
+
+/**
+ * Creates a TypeScript node representing a numeric value.
+ */
+export function tsNumericExpression(value: number): ts.NumericLiteral|ts.PrefixUnaryExpression {
+ // As of TypeScript 5.3 negative numbers are represented as `prefixUnaryOperator` and passing a
+ // negative number (even as a string) into `createNumericLiteral` will result in an error.
+ if (value < 0) {
+ const operand = ts.factory.createNumericLiteral(Math.abs(value));
+ return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, operand);
+ }
+
+ return ts.factory.createNumericLiteral(value);
+}
diff --git a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
index c0bb3e8e84bfe1..42653912341e65 100644
--- a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
+++ b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
@@ -14,6 +14,7 @@ import {ReflectionHost} from '../../reflection';
import {Context} from './context';
import {ImportManager} from './import_manager';
+import {tsNumericExpression} from './ts_util';
import {TypeEmitter} from './type_emitter';
@@ -133,7 +134,7 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
return ts.factory.createLiteralTypeNode(
ast.value ? ts.factory.createTrue() : ts.factory.createFalse());
} else if (typeof ast.value === 'number') {
- return ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral(ast.value));
+ return ts.factory.createLiteralTypeNode(tsNumericExpression(ast.value));
} else {
return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(ast.value));
}
diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts
index 368ac0e6f848bd..1d5edd60264534 100644
--- a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts
+++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts
@@ -8,6 +8,7 @@
import ts from 'typescript';
import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './api/ast_factory';
+import {tsNumericExpression} from './ts_util';
/**
* Different optimizers use different annotations on a function or method call to indicate its pure
@@ -161,7 +162,7 @@ export class TypeScriptAstFactory implements AstFactory {
expect(generate(literal)).toEqual('42');
});
+ it('should create a negative number literal', () => {
+ const {generate} = setupStatements();
+ const literal = factory.createLiteral(-42);
+ expect(ts.isPrefixUnaryExpression(literal)).toBe(true);
+ expect(generate(literal)).toEqual('-42');
+ });
+
it('should create a number literal for `NaN`', () => {
const {generate} = setupStatements();
const literal = factory.createLiteral(NaN);
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts
index 3809cf6e03c9b0..c339183ee45fc5 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts
@@ -50,7 +50,7 @@ function buildDiagnosticForSignal(
const templateMapping =
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!;
- const errorString = `${node.name} is a function and should be invoked : ${node.name}()`;
+ const errorString = `${node.name} is a function and should be invoked: ${node.name}()`;
const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString);
return [diagnostic];
}
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
index b3a3e545b86a60..87d0c43fa9927e 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
@@ -9,6 +9,7 @@
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../diagnostics';
import {TemplateCheckFactory} from './api';
+import {factory as interpolatedSignalNotInvoked} from './checks/interpolated_signal_not_invoked';
import {factory as invalidBananaInBoxFactory} from './checks/invalid_banana_in_box';
import {factory as missingControlFlowDirectiveFactory} from './checks/missing_control_flow_directive';
import {factory as missingNgForOfLetFactory} from './checks/missing_ngforof_let';
@@ -21,11 +22,8 @@ export {ExtendedTemplateCheckerImpl} from './src/extended_template_checker';
export const ALL_DIAGNOSTIC_FACTORIES:
readonly TemplateCheckFactory[] = [
- invalidBananaInBoxFactory,
- nullishCoalescingNotNullableFactory,
- optionalChainNotNullableFactory,
- missingControlFlowDirectiveFactory,
- textAttributeNotBindingFactory,
- missingNgForOfLetFactory,
- suffixNotSupportedFactory,
+ invalidBananaInBoxFactory, nullishCoalescingNotNullableFactory,
+ optionalChainNotNullableFactory, missingControlFlowDirectiveFactory,
+ textAttributeNotBindingFactory, missingNgForOfLetFactory, suffixNotSupportedFactory,
+ interpolatedSignalNotInvoked
];
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
index a6eb52bf2db7ff..ee39ae51c89426 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts
@@ -12,7 +12,7 @@ import ts from 'typescript';
import {TypeCheckingConfig} from '../api';
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
-import {tsCastToAny} from './ts_util';
+import {tsCastToAny, tsNumericExpression} from './ts_util';
export const NULL_AS_ANY = ts.factory.createAsExpression(
ts.factory.createNull(), ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
@@ -198,7 +198,7 @@ class AstTranslator implements AstVisitor {
} else if (typeof ast.value === 'string') {
node = ts.factory.createStringLiteral(ast.value);
} else if (typeof ast.value === 'number') {
- node = ts.factory.createNumericLiteral(ast.value);
+ node = tsNumericExpression(ast.value);
} else if (typeof ast.value === 'boolean') {
node = ast.value ? ts.factory.createTrue() : ts.factory.createFalse();
} else {
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts
index 52b382f207b375..7b496f0f529b6a 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts
@@ -77,11 +77,21 @@ export function tsCreateElement(tagName: string): ts.Expression {
* Unlike with `tsCreateVariable`, the type of the variable is explicitly specified.
*/
export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.VariableStatement {
+ let initializer: ts.Expression = ts.factory.createNonNullExpression(ts.factory.createNull());
+
+ // When we create a variable like `var _t1: boolean = null!`, TypeScript actually infers `_t1`
+ // to be `never`, instead of a `boolean`. To work around it, we cast the value to boolean again
+ // in the initializer, e.g. `var _t1: boolean = null! as boolean;`.
+ if (type.kind === ts.SyntaxKind.BooleanKeyword) {
+ initializer = ts.factory.createAsExpression(
+ initializer, ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword));
+ }
+
const decl = ts.factory.createVariableDeclaration(
/* name */ id,
/* exclamationToken */ undefined,
/* type */ type,
- /* initializer */ ts.factory.createNonNullExpression(ts.factory.createNull()));
+ /* initializer */ initializer);
return ts.factory.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */[decl]);
@@ -136,3 +146,17 @@ export function isAccessExpression(node: ts.Node): node is ts.ElementAccessExpre
ts.PropertyAccessExpression {
return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node);
}
+
+/**
+ * Creates a TypeScript node representing a numeric value.
+ */
+export function tsNumericExpression(value: number): ts.NumericLiteral|ts.PrefixUnaryExpression {
+ // As of TypeScript 5.3 negative numbers are represented as `prefixUnaryOperator` and passing a
+ // negative number (even as a string) into `createNumericLiteral` will result in an error.
+ if (value < 0) {
+ const operand = ts.factory.createNumericLiteral(Math.abs(value));
+ return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, operand);
+ }
+
+ return ts.factory.createNumericLiteral(value);
+}
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
index ae6c8acf4407e3..dda37e07da0faf 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts
@@ -1572,6 +1572,18 @@ class Scope {
*/
private statements: ts.Statement[] = [];
+ /**
+ * Names of the for loop context variables and their types.
+ */
+ private static readonly forLoopContextVariableTypes = new Map([
+ ['$first', ts.SyntaxKind.BooleanKeyword],
+ ['$last', ts.SyntaxKind.BooleanKeyword],
+ ['$even', ts.SyntaxKind.BooleanKeyword],
+ ['$odd', ts.SyntaxKind.BooleanKeyword],
+ ['$index', ts.SyntaxKind.NumberKeyword],
+ ['$count', ts.SyntaxKind.NumberKeyword],
+ ]);
+
private constructor(
private tcb: Context, private parent: Scope|null = null,
private guard: ts.Expression|null = null) {}
@@ -1628,9 +1640,12 @@ class Scope {
new TcbBlockVariableOp(
tcb, scope, ts.factory.createIdentifier(scopedNode.item.name), scopedNode.item));
- for (const variable of Object.values(scopedNode.contextVariables)) {
- // Note: currently all context variables are assumed to be number types.
- const type = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
+ for (const [name, variable] of Object.entries(scopedNode.contextVariables)) {
+ if (!this.forLoopContextVariableTypes.has(name)) {
+ throw new Error(`Unrecognized for loop context variable ${name}`);
+ }
+
+ const type = ts.factory.createKeywordTypeNode(this.forLoopContextVariableTypes.get(name)!);
this.registerVariable(
scope, variable, new TcbBlockImplicitVariableOp(tcb, scope, type, variable));
}
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
index d4f92b5e5aa7b9..04acceb7582c73 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
@@ -1560,10 +1560,10 @@ describe('type check blocks', () => {
const result = tcb(TEMPLATE);
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
expect(result).toContain('var _t2: number = null!;');
- expect(result).toContain('var _t3: number = null!;');
- expect(result).toContain('var _t4: number = null!;');
- expect(result).toContain('var _t5: number = null!;');
- expect(result).toContain('var _t6: number = null!;');
+ expect(result).toContain('var _t3: boolean = null! as boolean;');
+ expect(result).toContain('var _t4: boolean = null! as boolean;');
+ expect(result).toContain('var _t5: boolean = null! as boolean;');
+ expect(result).toContain('var _t6: boolean = null! as boolean;');
expect(result).toContain('var _t7: number = null!;');
expect(result).toContain('"" + (_t2) + (_t3) + (_t4) + (_t5) + (_t6) + (_t7)');
});
@@ -1578,10 +1578,10 @@ describe('type check blocks', () => {
const result = tcb(TEMPLATE);
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
expect(result).toContain('var _t2: number = null!;');
- expect(result).toContain('var _t3: number = null!;');
- expect(result).toContain('var _t4: number = null!;');
- expect(result).toContain('var _t5: number = null!;');
- expect(result).toContain('var _t6: number = null!;');
+ expect(result).toContain('var _t3: boolean = null! as boolean;');
+ expect(result).toContain('var _t4: boolean = null! as boolean;');
+ expect(result).toContain('var _t5: boolean = null! as boolean;');
+ expect(result).toContain('var _t6: boolean = null! as boolean;');
expect(result).toContain('var _t7: number = null!;');
expect(result).toContain('"" + (_t2) + (_t3) + (_t4) + (_t5) + (_t6) + (_t7)');
});
diff --git a/packages/compiler-cli/src/typescript_support.ts b/packages/compiler-cli/src/typescript_support.ts
index 7c1190ca699949..6df2ecec9c18c7 100644
--- a/packages/compiler-cli/src/typescript_support.ts
+++ b/packages/compiler-cli/src/typescript_support.ts
@@ -26,7 +26,7 @@ const MIN_TS_VERSION = '5.2.0';
* Note: this check is disabled in g3, search for
* `angularCompilerOptions.disableTypeScriptVersionCheck` config param value in g3.
*/
-const MAX_TS_VERSION = '5.3.0';
+const MAX_TS_VERSION = '5.4.0';
/**
* The currently used version of TypeScript, which can be adjusted for testing purposes using
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/TEST_CASES.json
index b4281ce99373d3..595a262081e573 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/TEST_CASES.json
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/TEST_CASES.json
@@ -50,13 +50,19 @@
],
"expectations": [
{
+ "files": [
+ {
+ "generated": "bare_icu.js",
+ "expected": "bare_icu.template.js",
+ "templatePipelineExpected": "bare_icu.pipeline.js"
+ }
+ ],
"extraChecks": [
"verifyPlaceholdersIntegrity",
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
},
{
"description": "should support interpolation with custom interpolation config",
@@ -70,8 +76,7 @@
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
},
{
"description": "should handle icus with html",
@@ -85,8 +90,7 @@
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
},
{
"description": "should handle icus with expressions",
@@ -100,8 +104,7 @@
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
},
{
"description": "should handle multiple icus in one block",
@@ -115,8 +118,7 @@
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
},
{
"description": "should handle multiple icus that share same placeholder",
@@ -220,8 +222,7 @@
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
},
{
"description": "should produce proper messages when `select` or `plural` keywords have spaces after them",
@@ -235,8 +236,7 @@
"verifyUniqueConsts"
]
}
- ],
- "skipForTemplatePipeline": true
+ ]
}
]
}
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.pipeline.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.pipeline.js
new file mode 100644
index 00000000000000..5949574631e5a1
--- /dev/null
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.pipeline.js
@@ -0,0 +1,64 @@
+function $MyComponent_div_2_Template$(rf, ctx) {
+ if (rf & 1) {
+ $r3$.ɵɵelementStart(0, "div", 5);
+ $r3$.ɵɵtext(1, " ");
+ $r3$.ɵɵi18n(2, 1);
+ $r3$.ɵɵtext(3, " ");
+ $r3$.ɵɵelementEnd();
+ }
+ if (rf & 2) {
+ const $ctx_r0$ = $r3$.ɵɵnextContext();
+ $r3$.ɵɵadvance(2);
+ $r3$.ɵɵi18nExp($ctx_r0$.age);
+ $r3$.ɵɵi18nApply(2);
+ }
+}
+…
+function $MyComponent_div_3_Template$(rf, ctx) {
+ if (rf & 1) {
+ $r3$.ɵɵelementStart(0, "div", 6);
+ $r3$.ɵɵtext(1, " You have ");
+ $r3$.ɵɵi18n(2, 2);
+ $r3$.ɵɵtext(3, ". ");
+ $r3$.ɵɵelementEnd();
+ }
+ if (rf & 2) {
+ const $ctx_r1$ = $r3$.ɵɵnextContext();
+ $r3$.ɵɵadvance(2);
+ $r3$.ɵɵi18nExp($ctx_r1$.count)($ctx_r1$.count);
+ $r3$.ɵɵi18nApply(2);
+ }
+}
+…
+decls: 4,
+vars: 3,
+consts: () => {
+ __i18nIcuMsg__('{VAR_SELECT, select, male {male} female {female} other {other}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {}) __i18nIcuMsg__('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}', [['INTERPOLATION', String.raw`\uFFFD1\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
+ return [
+ $i18n_0$,
+ $i18n_1$,
+ $i18n_2$,
+ ["title", "icu only", __AttributeMarker.Template__, "ngIf"],
+ ["title", "icu and text", __AttributeMarker.Template__, "ngIf"],
+ ["title", "icu only"],
+ ["title", "icu and text"]
+ ];
+},
+template: function MyComponent_Template(rf, ctx) {
+ if (rf & 1) {
+ $r3$.ɵɵelementStart(0, "div");
+ $r3$.ɵɵi18n(1, 0);
+ $r3$.ɵɵelementEnd();
+ $r3$.ɵɵtemplate(2, $MyComponent_div_2_Template$, 4, 1, "div", 3)(3, $MyComponent_div_3_Template$, 4, 2, "div", 4);
+ }
+ if (rf & 2) {
+ $r3$.ɵɵadvance(1);
+ $r3$.ɵɵi18nExp(ctx.gender);
+ $r3$.ɵɵi18nApply(1);
+ $r3$.ɵɵadvance(1);
+ $r3$.ɵɵproperty("ngIf", ctx.visible);
+ $r3$.ɵɵadvance(1);
+ $r3$.ɵɵproperty("ngIf", ctx.available);
+ }
+}
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.template.js
similarity index 93%
rename from packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.js
rename to packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.template.js
index 3e697e2ba62813..0aa9bceb8f1589 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/bare_icu.template.js
@@ -34,7 +34,7 @@ decls: 4,
vars: 3,
consts: () => {
__i18nIcuMsg__('{VAR_SELECT, select, male {male} female {female} other {other}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {}) __i18nIcuMsg__('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
- __i18nIcuMsg__('{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}', [ ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['INTERPOLATION', String.raw`\uFFFD1\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}', [['INTERPOLATION', String.raw`\uFFFD1\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
return [
$i18n_0$,
["title", "icu only", __AttributeMarker.Template__, "ngIf"],
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/custom_interpolation.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/custom_interpolation.js
index 09d4f83e45a7c5..b959cb3736cdf8 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/custom_interpolation.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/custom_interpolation.js
@@ -1,5 +1,5 @@
consts: () => {
- __i18nIcuMsg__('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{INTERPOLATION}}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['INTERPOLATION', String.raw`\uFFFD1\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{INTERPOLATION}}}', [['INTERPOLATION', String.raw`\uFFFD1\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
return [
$i18n_0$
];
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/expressions.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/expressions.js
index 20e8f1d00de90d..cedd046b70c87e 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/expressions.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/expressions.js
@@ -1,7 +1,7 @@
decls: 2,
vars: 2,
consts: () => {
- __i18nIcuMsg__('{VAR_SELECT, select, male {male of age: {INTERPOLATION}} female {female} other {other}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['INTERPOLATION', String.raw`\uFFFD1\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, male {male of age: {INTERPOLATION}} female {female} other {other}}', [['INTERPOLATION', String.raw`\uFFFD1\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
return [
$i18n_0$
];
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/html_content.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/html_content.js
index ba0b050456358b..b797dbdde7543e 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/html_content.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/html_content.js
@@ -1,7 +1,7 @@
decls: 5,
vars: 1,
consts: () => {
- __i18nIcuMsg__('{VAR_SELECT, select, male {male - {START_BOLD_TEXT}male{CLOSE_BOLD_TEXT}} female {female {START_BOLD_TEXT}female{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}{START_ITALIC_TEXT}other{CLOSE_ITALIC_TEXT}{CLOSE_TAG_DIV}}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['START_BOLD_TEXT', ''], ['CLOSE_BOLD_TEXT', ' '], ['START_ITALIC_TEXT', ''], ['CLOSE_ITALIC_TEXT', ' '], ['START_TAG_DIV', ''], ['CLOSE_TAG_DIV', '
']], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, male {male - {START_BOLD_TEXT}male{CLOSE_BOLD_TEXT}} female {female {START_BOLD_TEXT}female{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}{START_ITALIC_TEXT}other{CLOSE_ITALIC_TEXT}{CLOSE_TAG_DIV}}}', [['CLOSE_BOLD_TEXT', ''], ['CLOSE_ITALIC_TEXT', ''], ['CLOSE_TAG_DIV', ''], ['START_BOLD_TEXT', ''], ['START_ITALIC_TEXT', ''], ['START_TAG_DIV', ' '], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
__i18nMsg__(' {$icu} {$startBoldText}Other content{$closeBoldText}{$startTagDiv}{$startItalicText}Another content{$closeItalicText}{$closeTagDiv}', [['closeBoldText', String.raw`\uFFFD/#2\uFFFD`], ['closeItalicText', String.raw`\uFFFD/#4\uFFFD`], ['closeTagDiv', String.raw`\uFFFD/#3\uFFFD`], ['icu', '$I18N_1$', '4731057199984078679'], ['startBoldText', String.raw`\uFFFD#2\uFFFD`], ['startItalicText', String.raw`\uFFFD#4\uFFFD`], ['startTagDiv', String.raw`\uFFFD#3\uFFFD`]], {original_code: {'closeBoldText': '', 'closeItalicText': '', 'closeTagDiv': '
', 'icu': '{gender, select, male {male - male } female {female female } other {other
}}', 'startBoldText': '', 'startItalicText': '', 'startTagDiv': ''}}, {})
return [
$i18n_1$,
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_with_interpolations.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_with_interpolations.js
index e4c60035750a5d..75b8c8bc9afccc 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_with_interpolations.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/icu_with_interpolations.js
@@ -15,8 +15,8 @@ function MyComponent_span_2_Template(rf, ctx) {
decls: 3,
vars: 4,
consts: () => {
- __i18nIcuMsg__('{VAR_SELECT, select, male {male {INTERPOLATION}} female {female {INTERPOLATION_1}} other {other}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['INTERPOLATION', String.raw`\uFFFD1\uFFFD`], ['INTERPOLATION_1', String.raw`\uFFFD2\uFFFD`]], {})
- __i18nIcuMsg__('{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}', [['VAR_SELECT', String.raw`\uFFFD0:1\uFFFD`], ['INTERPOLATION', String.raw`\uFFFD1:1\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, male {male {INTERPOLATION}} female {female {INTERPOLATION_1}} other {other}}', [['INTERPOLATION', String.raw`\uFFFD1\uFFFD`], ['INTERPOLATION_1', String.raw`\uFFFD2\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}', [['INTERPOLATION', String.raw`\uFFFD1:1\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0:1\uFFFD`]], {})
__i18nMsg__(' {$icu} {$startTagSpan} {$icu_1} {$closeTagSpan}', [['closeTagSpan', String.raw`\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD`], ['icu', '$i18n_0$', '567200399523107034'], ['icu_1', '$i18n_1$', '5762277079421427850'], ['startTagSpan', String.raw`\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFD`]], {original_code: {'closeTagSpan': '', 'icu': '{gender, select, male {male {{ weight }}} female {female {{ height }}} other {other}}', 'icu_1': '{age, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {{ otherAge }}}}', 'startTagSpan': '
'}}, {})
return [
$i18n_2$,
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/named_interpolations.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/named_interpolations.js
index de0f5526adb1a5..8b57015ff1b2ca 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/named_interpolations.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/named_interpolations.js
@@ -1,7 +1,7 @@
decls: 2,
vars: 4,
consts: () => {
- __i18nIcuMsg__('{VAR_SELECT, select, male {male {PH_A}} female {female {PH_B}} other {other {PH_WITH_SPACES}}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['PH_A', String.raw`\uFFFD1\uFFFD`], ['PH_B', String.raw`\uFFFD2\uFFFD`], ['PH_WITH_SPACES', String.raw`\uFFFD3\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_SELECT, select, male {male {PH_A}} female {female {PH_B}} other {other {PH_WITH_SPACES}}}', [['PH_WITH_SPACES', String.raw`\uFFFD3\uFFFD`], ['PH_A', String.raw`\uFFFD1\uFFFD`], ['PH_B', String.raw`\uFFFD2\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
return [
$i18n_0$
];
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/nested_icu_in_other_block.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/nested_icu_in_other_block.js
index 74a2a7e5f914a4..67d3cfc1e8013e 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/nested_icu_in_other_block.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/nested_icu_in_other_block.js
@@ -1,7 +1,7 @@
decls: 2,
vars: 3,
consts: () => {
- __i18nIcuMsg__('{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}} !} other {other - {INTERPOLATION}}}', [['VAR_SELECT', String.raw`\uFFFD0\uFFFD`], ['VAR_PLURAL', String.raw`\uFFFD1\uFFFD`], ['INTERPOLATION', String.raw`\uFFFD2\uFFFD`]], {})
+ __i18nIcuMsg__('{VAR_PLURAL, plural, =0 {zero} =2 {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}} !} other {other - {INTERPOLATION}}}', [['INTERPOLATION', String.raw`\uFFFD2\uFFFD`], ['VAR_PLURAL', String.raw`\uFFFD1\uFFFD`], ['VAR_SELECT', String.raw`\uFFFD0\uFFFD`]], {})
return [
$i18n_0$
];
diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
index 00b576b753c2ef..e3a9291589bec4 100644
--- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
@@ -4477,10 +4477,10 @@ suppress
const diags = env.driveDiagnostics();
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
]);
});
@@ -4513,10 +4513,10 @@ suppress
const diags = env.driveDiagnostics();
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
- `Argument of type 'number' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
+ `Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
]);
});
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index 8906de8b49bfd1..2a89c7ddd73434 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -1118,7 +1118,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// inside ICUs)
// - all ICU vars (such as `VAR_SELECT` or `VAR_PLURAL`) are replaced with correct values
const transformFn = (raw: o.ReadVarExpr) => {
- const params = {...vars, ...placeholders};
+ // Sort the map entries in the compiled output. This makes it easy to acheive identical output
+ // in the template pipeline compiler.
+ const params = Object.fromEntries(Object.entries({...vars, ...placeholders}).sort());
const formatted = formatI18nPlaceholderNamesInMap(params, /* useCamelCase */ false);
return invokeInstruction(null, R3.i18nPostprocess, [raw, mapLiteral(formatted, true)]);
};
diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts
index e0f1c0c0e298f1..d5436126eacf96 100644
--- a/packages/compiler/src/template/pipeline/ir/src/enums.ts
+++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts
@@ -223,12 +223,12 @@ export enum OpKind {
/**
* An instruction to create an ICU expression.
*/
- Icu,
+ IcuStart,
/**
* An instruction to update an ICU expression.
*/
- IcuUpdate,
+ IcuEnd,
/**
* An i18n context containing information needed to generate an i18n message.
@@ -500,12 +500,12 @@ export enum I18nParamValueFlags {
/**
* This value represtents an element tag.
*/
- ElementTag = 0b001,
+ ElementTag = 0b1,
/**
* This value represents a template tag.
*/
- TemplateTag = 0b0010,
+ TemplateTag = 0b10,
/**
* This value represents the opening of a tag.
@@ -516,6 +516,11 @@ export enum I18nParamValueFlags {
* This value represents the closing of a tag.
*/
CloseTag = 0b1000,
+
+ /**
+ * This value represents an i18n expression index.
+ */
+ ExpressionIndex = 0b10000
}
/**
diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts
index 43268414b3c2bc..50540946072ed6 100644
--- a/packages/compiler/src/template/pipeline/ir/src/expression.ts
+++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts
@@ -1014,8 +1014,8 @@ export function transformExpressionsInOp(
case OpKind.I18nContext:
case OpKind.I18nEnd:
case OpKind.I18nStart:
- case OpKind.Icu:
- case OpKind.IcuUpdate:
+ case OpKind.IcuEnd:
+ case OpKind.IcuStart:
case OpKind.Namespace:
case OpKind.Pipe:
case OpKind.Projection:
diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts
index 98c887fa10c047..5e9661b6a2a1c5 100644
--- a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts
+++ b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts
@@ -26,7 +26,7 @@ export type CreateOp = ListEndOp|StatementOp|ElementOp|Eleme
ElementEndOp|ContainerOp|ContainerStartOp|ContainerEndOp|TemplateOp|EnableBindingsOp|
DisableBindingsOp|TextOp|ListenerOp|PipeOp|VariableOp|NamespaceOp|ProjectionDefOp|
ProjectionOp|ExtractedAttributeOp|DeferOp|DeferOnOp|RepeaterCreateOp|I18nMessageOp|I18nOp|
- I18nStartOp|I18nEndOp|IcuOp|I18nContextOp;
+ I18nStartOp|I18nEndOp|IcuStartOp|IcuEndOp|I18nContextOp;
/**
* An operation representing the creation of an element or container.
@@ -1001,10 +1001,10 @@ export function createI18nEndOp(xref: XrefId): I18nEndOp {
}
/**
- * An op that represents an ICU expression.
+ * An op that represents the start of an ICU expression.
*/
-export interface IcuOp extends Op {
- kind: OpKind.Icu;
+export interface IcuStartOp extends Op {
+ kind: OpKind.IcuStart;
/**
* The ID of the ICU.
@@ -1016,11 +1016,6 @@ export interface IcuOp extends Op {
*/
message: i18n.Message;
- /**
- * The ICU associated with this op.
- */
- icu: i18n.Icu;
-
/**
* Placeholder used to reference this ICU in other i18n messages.
*/
@@ -1035,16 +1030,15 @@ export interface IcuOp extends Op {
}
/**
- * Creates an op to create an ICU expression.
+ * Creates an ICU start op.
*/
-export function createIcuOp(
- xref: XrefId, message: i18n.Message, icu: i18n.Icu, messagePlaceholder: string,
- sourceSpan: ParseSourceSpan): IcuOp {
+export function createIcuStartOp(
+ xref: XrefId, message: i18n.Message, messagePlaceholder: string,
+ sourceSpan: ParseSourceSpan): IcuStartOp {
return {
- kind: OpKind.Icu,
+ kind: OpKind.IcuStart,
xref,
message,
- icu,
messagePlaceholder,
context: null,
sourceSpan,
@@ -1052,6 +1046,29 @@ export function createIcuOp(
};
}
+/**
+ * An op that represents the end of an ICU expression.
+ */
+export interface IcuEndOp extends Op {
+ kind: OpKind.IcuEnd;
+
+ /**
+ * The ID of the corresponding IcuStartOp.
+ */
+ xref: XrefId;
+}
+
+/**
+ * Creates an ICU end op.
+ */
+export function createIcuEndOp(xref: XrefId): IcuEndOp {
+ return {
+ kind: OpKind.IcuEnd,
+ xref,
+ ...NEW_OP,
+ };
+}
+
/**
* An i18n context that is used to generate snippets of a full translated message.
* A separate context is created in a few different scenarios:
diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts
index dd46b10bc2b0bf..b06533c05894b6 100644
--- a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts
+++ b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts
@@ -7,7 +7,6 @@
*/
import {SecurityContext} from '../../../../../core';
-import * as i18n from '../../../../../i18n/i18n_ast';
import * as o from '../../../../../output/output_ast';
import {ParseSourceSpan} from '../../../../../parse_util';
import {BindingKind, I18nParamResolutionTime, OpKind} from '../enums';
@@ -24,7 +23,7 @@ import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared';
*/
export type UpdateOp = ListEndOp|StatementOp|PropertyOp|AttributeOp|StylePropOp|
ClassPropOp|StyleMapOp|ClassMapOp|InterpolateTextOp|AdvanceOp|VariableOp|BindingOp|
- HostPropertyOp|ConditionalOp|I18nExpressionOp|I18nApplyOp|IcuUpdateOp|RepeaterOp|DeferWhenOp;
+ HostPropertyOp|ConditionalOp|I18nExpressionOp|I18nApplyOp|RepeaterOp|DeferWhenOp;
/**
* A logical operation to perform string interpolation on a text node.
@@ -49,7 +48,7 @@ export interface InterpolateTextOp extends Op, ConsumesVarsTrait {
/**
* The i18n placeholders associated with this interpolation.
*/
- i18nPlaceholders: i18n.Placeholder[];
+ i18nPlaceholders: string[];
sourceSpan: ParseSourceSpan;
}
@@ -58,7 +57,7 @@ export interface InterpolateTextOp extends Op, ConsumesVarsTrait {
* Create an `InterpolationTextOp`.
*/
export function createInterpolateTextOp(
- xref: XrefId, interpolation: Interpolation, i18nPlaceholders: i18n.Placeholder[],
+ xref: XrefId, interpolation: Interpolation, i18nPlaceholders: string[],
sourceSpan: ParseSourceSpan): InterpolateTextOp {
return {
kind: OpKind.InterpolateText,
@@ -697,29 +696,3 @@ export function createI18nApplyOp(
...NEW_OP,
};
}
-
-/**
- * An op that represents updating an ICU expression.
- */
-export interface IcuUpdateOp extends Op {
- kind: OpKind.IcuUpdate;
-
- /**
- * The ID of the ICU being updated.
- */
- xref: XrefId;
-
- sourceSpan: ParseSourceSpan;
-}
-
-/**
- * Creates an op to update an ICU expression.
- */
-export function createIcuUpdateOp(xref: XrefId, sourceSpan: ParseSourceSpan): IcuUpdateOp {
- return {
- kind: OpKind.IcuUpdate,
- xref,
- sourceSpan,
- ...NEW_OP,
- };
-}
diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts
index 0ded17a42098e3..77a29b6dfade2a 100644
--- a/packages/compiler/src/template/pipeline/src/emit.ts
+++ b/packages/compiler/src/template/pipeline/src/emit.ts
@@ -24,7 +24,6 @@ import {generateConditionalExpressions} from './phases/conditionals';
import {collectElementConsts} from './phases/const_collection';
import {createDeferDepsFns} from './phases/create_defer_deps_fns';
import {createI18nContexts} from './phases/create_i18n_contexts';
-import {createI18nIcuExpressions} from './phases/create_i18n_icu_expressions';
import {configureDeferInstructions} from './phases/defer_configs';
import {resolveDeferTargetNames} from './phases/defer_resolve_targets';
import {collapseEmptyInstructions} from './phases/empty_elements';
@@ -61,6 +60,7 @@ import {resolveContexts} from './phases/resolve_contexts';
import {resolveDollarEvent} from './phases/resolve_dollar_event';
import {resolveI18nElementPlaceholders} from './phases/resolve_i18n_element_placeholders';
import {resolveI18nExpressionPlaceholders} from './phases/resolve_i18n_expression_placeholders';
+import {resolveI18nIcuPlaceholders} from './phases/resolve_i18n_icu_placeholders';
import {resolveNames} from './phases/resolve_names';
import {resolveSanitizers} from './phases/resolve_sanitizers';
import {saveAndRestoreView} from './phases/save_restore_view';
@@ -103,7 +103,6 @@ const phases: Phase[] = [
{kind: Kind.Tmpl, fn: createPipes},
{kind: Kind.Tmpl, fn: configureDeferInstructions},
{kind: Kind.Tmpl, fn: extractI18nText},
- {kind: Kind.Tmpl, fn: createI18nIcuExpressions},
{kind: Kind.Tmpl, fn: applyI18nExpressions},
{kind: Kind.Tmpl, fn: createVariadicPipes},
{kind: Kind.Both, fn: generatePureLiteralStructures},
@@ -127,6 +126,7 @@ const phases: Phase[] = [
{kind: Kind.Tmpl, fn: createDeferDepsFns},
{kind: Kind.Tmpl, fn: resolveI18nElementPlaceholders},
{kind: Kind.Tmpl, fn: resolveI18nExpressionPlaceholders},
+ {kind: Kind.Tmpl, fn: resolveI18nIcuPlaceholders},
{kind: Kind.Tmpl, fn: mergeI18nContexts},
{kind: Kind.Tmpl, fn: extractI18nMessages},
{kind: Kind.Tmpl, fn: generateTrackFns},
diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts
index 10344db9dd3305..fd5a2bfd5ddf2a 100644
--- a/packages/compiler/src/template/pipeline/src/ingest.ts
+++ b/packages/compiler/src/template/pipeline/src/ingest.ts
@@ -251,7 +251,8 @@ function ingestText(unit: ViewCompilationUnit, text: t.Text): void {
/**
* Ingest an interpolated text node from the AST into the given `ViewCompilation`.
*/
-function ingestBoundText(unit: ViewCompilationUnit, text: t.BoundText): void {
+function ingestBoundText(
+ unit: ViewCompilationUnit, text: t.BoundText, i18nPlaceholders?: string[]): void {
let value = text.value;
if (value instanceof e.ASTWithSource) {
value = value.ast;
@@ -265,10 +266,17 @@ function ingestBoundText(unit: ViewCompilationUnit, text: t.BoundText): void {
`Unhandled i18n metadata type for text interpolation: ${text.i18n?.constructor.name}`);
}
- const i18nPlaceholders = text.i18n instanceof i18n.Container ?
- text.i18n.children.filter(
- (node): node is i18n.Placeholder => node instanceof i18n.Placeholder) :
- [];
+ if (i18nPlaceholders === undefined) {
+ i18nPlaceholders = text.i18n instanceof i18n.Container ?
+ text.i18n.children
+ .filter((node): node is i18n.Placeholder => node instanceof i18n.Placeholder)
+ .map(placeholder => placeholder.name) :
+ [];
+ }
+ if (i18nPlaceholders.length > 0 && i18nPlaceholders.length !== value.expressions.length) {
+ throw Error(`Unexpected number of i18n placeholders (${
+ value.expressions.length}) for BoundText with ${value.expressions.length} expressions`);
+ }
const textXref = unit.job.allocateXrefId();
unit.create.push(ir.createTextOp(textXref, '', text.sourceSpan));
@@ -482,9 +490,21 @@ function ingestDeferBlock(unit: ViewCompilationUnit, deferBlock: t.DeferredBlock
function ingestIcu(unit: ViewCompilationUnit, icu: t.Icu) {
if (icu.i18n instanceof i18n.Message && isSingleI18nIcu(icu.i18n)) {
const xref = unit.job.allocateXrefId();
- unit.create.push(ir.createIcuOp(
- xref, icu.i18n, icu.i18n.nodes[0], icuFromI18nMessage(icu.i18n).name, null!));
- unit.update.push(ir.createIcuUpdateOp(xref, null!));
+ const icuNode = icu.i18n.nodes[0];
+ unit.create.push(ir.createIcuStartOp(xref, icu.i18n, icuFromI18nMessage(icu.i18n).name, null!));
+ const expressionPlaceholder = icuNode.expressionPlaceholder?.trimEnd();
+ if (expressionPlaceholder === undefined || icu.vars[expressionPlaceholder] === undefined) {
+ throw Error('ICU should have a text binding');
+ }
+ ingestBoundText(unit, icu.vars[expressionPlaceholder], [expressionPlaceholder]);
+ for (const [placeholder, text] of Object.entries(icu.placeholders)) {
+ if (text instanceof t.BoundText) {
+ ingestBoundText(unit, text, [placeholder]);
+ } else {
+ ingestText(unit, text);
+ }
+ }
+ unit.create.push(ir.createIcuEndOp(xref));
} else {
throw Error(`Unhandled i18n metadata type for ICU: ${icu.i18n?.constructor.name}`);
}
diff --git a/packages/compiler/src/template/pipeline/src/phases/apply_i18n_expressions.ts b/packages/compiler/src/template/pipeline/src/phases/apply_i18n_expressions.ts
index 23e4700080ca5f..6528585129a270 100644
--- a/packages/compiler/src/template/pipeline/src/phases/apply_i18n_expressions.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/apply_i18n_expressions.ts
@@ -13,10 +13,19 @@ import {CompilationJob} from '../compilation';
* Adds apply operations after i18n expressions.
*/
export function applyI18nExpressions(job: CompilationJob): void {
+ const i18nContexts = new Map();
+ for (const unit of job.units) {
+ for (const op of unit.create) {
+ if (op.kind === ir.OpKind.I18nContext) {
+ i18nContexts.set(op.xref, op);
+ }
+ }
+ }
+
for (const unit of job.units) {
for (const op of unit.update) {
// Only add apply after expressions that are not followed by more expressions.
- if (op.kind === ir.OpKind.I18nExpression && needsApplication(op)) {
+ if (op.kind === ir.OpKind.I18nExpression && needsApplication(i18nContexts, op)) {
// TODO: what should be the source span for the apply op?
ir.OpList.insertAfter(ir.createI18nApplyOp(op.target, op.handle, null!), op);
}
@@ -27,13 +36,15 @@ export function applyI18nExpressions(job: CompilationJob): void {
/**
* Checks whether the given expression op needs to be followed with an apply op.
*/
-function needsApplication(op: ir.I18nExpressionOp) {
+function needsApplication(i18nContexts: Map, op: ir.I18nExpressionOp) {
// If the next op is not another expression, we need to apply.
if (op.next?.kind !== ir.OpKind.I18nExpression) {
return true;
}
- // If the next op is an expression targeting a different i18n context, we need to apply.
- if (op.next.context !== op.context) {
+ // If the next op is an expression targeting a different i18n block, we need to apply.
+ const context = i18nContexts.get(op.context)!;
+ const nextContext = i18nContexts.get(op.next.context)!;
+ if (context.i18nBlock !== nextContext.i18nBlock) {
return true;
}
return false;
diff --git a/packages/compiler/src/template/pipeline/src/phases/create_i18n_contexts.ts b/packages/compiler/src/template/pipeline/src/phases/create_i18n_contexts.ts
index 15b6bcf130d6b2..3a562bfffed817 100644
--- a/packages/compiler/src/template/pipeline/src/phases/create_i18n_contexts.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/create_i18n_contexts.ts
@@ -35,7 +35,7 @@ export function createI18nContexts(job: CompilationJob) {
case ir.OpKind.I18nEnd:
currentI18nOp = null;
break;
- case ir.OpKind.Icu:
+ case ir.OpKind.IcuStart:
// If an ICU represents a different message than its containing block, we give it its own
// i18n context.
if (currentI18nOp === null) {
diff --git a/packages/compiler/src/template/pipeline/src/phases/create_i18n_icu_expressions.ts b/packages/compiler/src/template/pipeline/src/phases/create_i18n_icu_expressions.ts
deleted file mode 100644
index cc62b17ab0b603..00000000000000
--- a/packages/compiler/src/template/pipeline/src/phases/create_i18n_icu_expressions.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import * as ir from '../../ir';
-import {CompilationJob} from '../compilation';
-
-/**
- * Replace the ICU update ops with i18n expression ops.
- */
-export function createI18nIcuExpressions(job: CompilationJob) {
- const icus = new Map();
- const i18nContexts = new Map();
- const i18nBlocks = new Map();
-
- // Collect maps of ops that need to be referenced to create the I18nExpressionOps.
- for (const unit of job.units) {
- for (const op of unit.create) {
- switch (op.kind) {
- case ir.OpKind.Icu:
- icus.set(op.xref, op);
- break;
- case ir.OpKind.I18nContext:
- i18nContexts.set(op.xref, op);
- break;
- case ir.OpKind.I18nStart:
- i18nBlocks.set(op.xref, op);
- break;
- }
- }
-
- // Replace each IcuUpdateOp with an I18nExpressionOp.
- for (const op of unit.update) {
- switch (op.kind) {
- case ir.OpKind.IcuUpdate:
- const icuOp = icus.get(op.xref);
- if (icuOp?.icu.expressionPlaceholder === undefined) {
- throw Error('ICU should have an i18n placeholder');
- }
- if (icuOp.context === null) {
- throw Error('ICU should have its i18n context set');
- }
- const i18nContext = i18nContexts.get(icuOp.context)!;
- const i18nBlock = i18nBlocks.get(i18nContext.i18nBlock)!;
- ir.OpList.replace(
- op,
- ir.createI18nExpressionOp(
- i18nContext.xref, i18nBlock.xref, i18nBlock.handle,
- new ir.LexicalReadExpr(icuOp.icu.expression), icuOp.icu.expressionPlaceholder,
- // ICU-based i18n Expressions are resolved during post-processing.
- ir.I18nParamResolutionTime.Postproccessing, null!));
- break;
- }
- }
- }
-}
diff --git a/packages/compiler/src/template/pipeline/src/phases/extract_i18n_messages.ts b/packages/compiler/src/template/pipeline/src/phases/extract_i18n_messages.ts
index 4f08a4c7fa26fa..13dce2a4905909 100644
--- a/packages/compiler/src/template/pipeline/src/phases/extract_i18n_messages.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/extract_i18n_messages.ts
@@ -91,18 +91,23 @@ export function extractI18nMessages(job: CompilationJob): void {
// Extract messages from ICUs with their own sub-context.
for (const unit of job.units) {
for (const op of unit.create) {
- if (op.kind === ir.OpKind.Icu) {
- if (!op.context) {
- throw Error('ICU op should have its context set.');
- }
- if (!i18nBlockContexts.has(op.context)) {
- const i18nContext = i18nContexts.get(op.context)!;
- const subMessage = createI18nMessage(job, i18nContext, op.messagePlaceholder);
- unit.create.push(subMessage);
- const parentMessage = i18nBlockMessages.get(i18nContext.i18nBlock);
- parentMessage?.subMessages.push(subMessage.xref);
- }
- ir.OpList.remove(op);
+ switch (op.kind) {
+ case ir.OpKind.IcuStart:
+ if (!op.context) {
+ throw Error('ICU op should have its context set.');
+ }
+ if (!i18nBlockContexts.has(op.context)) {
+ const i18nContext = i18nContexts.get(op.context)!;
+ const subMessage = createI18nMessage(job, i18nContext, op.messagePlaceholder);
+ unit.create.push(subMessage);
+ const parentMessage = i18nBlockMessages.get(i18nContext.i18nBlock);
+ parentMessage?.subMessages.push(subMessage.xref);
+ }
+ ir.OpList.remove(op);
+ break;
+ case ir.OpKind.IcuEnd:
+ ir.OpList.remove(op);
+ break;
}
}
}
@@ -130,7 +135,7 @@ function createI18nMessage(
*/
function formatParams(params: Map): Map {
const result = new Map();
- for (const [placeholder, placeholderValues] of [...params].sort()) {
+ for (const [placeholder, placeholderValues] of params) {
const serializedValues = formatParamValues(placeholderValues);
if (serializedValues !== null) {
result.set(placeholder, o.literal(formatParamValues(placeholderValues)));
@@ -156,6 +161,11 @@ function formatParamValues(values: ir.I18nParamValue[]): string|null {
* Formats a single `I18nParamValue` into a string
*/
function formatValue(value: ir.I18nParamValue): string {
+ // If there are no special flags, just return the raw value.
+ if (value.flags === ir.I18nParamValueFlags.None) {
+ return `${value.value}`;
+ }
+
let tagMarker = '';
let closeMarker = '';
if (value.flags & ir.I18nParamValueFlags.ElementTag) {
diff --git a/packages/compiler/src/template/pipeline/src/phases/i18n_const_collection.ts b/packages/compiler/src/template/pipeline/src/phases/i18n_const_collection.ts
index b5a2664e564eef..a6677033907e3a 100644
--- a/packages/compiler/src/template/pipeline/src/phases/i18n_const_collection.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/i18n_const_collection.ts
@@ -83,6 +83,9 @@ function collectMessage(
messageOp.params.set(subMessage.messagePlaceholder!, subMessageVar);
}
+ // Sort the params for consistency with TemaplateDefinitionBuilder output.
+ messageOp.params = new Map([...messageOp.params.entries()].sort());
+
// Check that the message has all of its parameters filled out.
assertAllParamsResolved(messageOp);
@@ -97,6 +100,9 @@ function collectMessage(
// If nescessary, add a post-processing step and resolve any placeholder params that are
// set in post-processing.
if (messageOp.needsPostprocessing) {
+ // Sort the post-processing params for consistency with TemaplateDefinitionBuilder output.
+ messageOp.postprocessingParams = new Map([...messageOp.postprocessingParams.entries()].sort());
+
const extraTransformFnParams: o.Expression[] = [];
if (messageOp.postprocessingParams.size > 0) {
extraTransformFnParams.push(o.literalMap(
@@ -198,12 +204,14 @@ function i18nGenerateClosureVar(
* Asserts that all of the message's placeholders have values.
*/
function assertAllParamsResolved(op: ir.I18nMessageOp): asserts op is ir.I18nMessageOp {
- for (const placeholder in op.message.placeholders) {
+ for (let placeholder in op.message.placeholders) {
+ placeholder = placeholder.trimEnd();
if (!op.params.has(placeholder) && !op.postprocessingParams.has(placeholder)) {
throw Error(`Failed to resolve i18n placeholder: ${placeholder}`);
}
}
- for (const placeholder in op.message.placeholderToMessage) {
+ for (let placeholder in op.message.placeholderToMessage) {
+ placeholder = placeholder.trimEnd();
if (!op.params.has(placeholder) && !op.postprocessingParams.has(placeholder)) {
throw Error(`Failed to resolve i18n message placeholder: ${placeholder}`);
}
diff --git a/packages/compiler/src/template/pipeline/src/phases/i18n_text_extraction.ts b/packages/compiler/src/template/pipeline/src/phases/i18n_text_extraction.ts
index 439b8df35b3082..207bafc75ba814 100644
--- a/packages/compiler/src/template/pipeline/src/phases/i18n_text_extraction.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/i18n_text_extraction.ts
@@ -17,7 +17,9 @@ export function extractI18nText(job: CompilationJob): void {
// Remove all text nodes within i18n blocks, their content is already captured in the i18n
// message.
let currentI18n: ir.I18nStartOp|null = null;
+ let currentIcu: ir.IcuStartOp|null = null;
const textNodeI18nBlocks = new Map();
+ const textNodeIcus = new Map();
for (const op of unit.create) {
switch (op.kind) {
case ir.OpKind.I18nStart:
@@ -29,9 +31,19 @@ export function extractI18nText(job: CompilationJob): void {
case ir.OpKind.I18nEnd:
currentI18n = null;
break;
+ case ir.OpKind.IcuStart:
+ if (op.context === null) {
+ throw Error('Icu op should have its context set.');
+ }
+ currentIcu = op;
+ break;
+ case ir.OpKind.IcuEnd:
+ currentIcu = null;
+ break;
case ir.OpKind.Text:
if (currentI18n !== null) {
textNodeI18nBlocks.set(op.xref, currentI18n);
+ textNodeIcus.set(op.xref, currentIcu);
ir.OpList.remove(op);
}
break;
@@ -48,15 +60,18 @@ export function extractI18nText(job: CompilationJob): void {
}
const i18nOp = textNodeI18nBlocks.get(op.target)!;
+ const icuOp = textNodeIcus.get(op.target);
+ const contextId = icuOp ? icuOp.context : i18nOp.context;
+ const resolutionTime = icuOp ? ir.I18nParamResolutionTime.Postproccessing :
+ ir.I18nParamResolutionTime.Creation;
const ops: ir.UpdateOp[] = [];
for (let i = 0; i < op.interpolation.expressions.length; i++) {
const expr = op.interpolation.expressions[i];
- const placeholder = op.i18nPlaceholders[i];
// For now, this i18nExpression depends on the slot context of the enclosing i18n block.
// Later, we will modify this, and advance to a different point.
ops.push(ir.createI18nExpressionOp(
- i18nOp.context!, i18nOp.xref, i18nOp.handle, expr, placeholder.name,
- ir.I18nParamResolutionTime.Creation, expr.sourceSpan ?? op.sourceSpan));
+ contextId!, i18nOp.xref, i18nOp.handle, expr, op.i18nPlaceholders[i],
+ resolutionTime, expr.sourceSpan ?? op.sourceSpan));
}
ir.OpList.replaceWithMany(op as ir.UpdateOp, ops);
break;
diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_element_placeholders.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_element_placeholders.ts
index 736056622f10c0..90e9b0f290985e 100644
--- a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_element_placeholders.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_element_placeholders.ts
@@ -122,7 +122,7 @@ function getSubTemplateIndexForTemplateTag(
/** Add a param value to the given params map. */
function addParam(
params: Map, placeholder: string, value: string|number,
- subTemplateIndex: number|null, flags = ir.I18nParamValueFlags.None) {
+ subTemplateIndex: number|null, flags: ir.I18nParamValueFlags) {
const values = params.get(placeholder) ?? [];
values.push({value, subTemplateIndex, flags});
params.set(placeholder, values);
diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_expression_placeholders.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_expression_placeholders.ts
index f99955e673955f..b6bc11512753a5 100644
--- a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_expression_placeholders.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_expression_placeholders.ts
@@ -29,25 +29,28 @@ export function resolveI18nExpressionPlaceholders(job: ComponentCompilationJob)
}
}
- // Keep track of the next available expression index per i18n context.
+ // Keep track of the next available expression index per i18n block.
const expressionIndices = new Map();
for (const unit of job.units) {
for (const op of unit.update) {
if (op.kind === ir.OpKind.I18nExpression) {
- const index = expressionIndices.get(op.context) || 0;
const i18nContext = i18nContexts.get(op.context)!;
+ const index = expressionIndices.get(i18nContext.i18nBlock) || 0;
const subTemplateIndex = subTemplateIndicies.get(i18nContext.i18nBlock)!;
// Add the expression index in the appropriate params map.
const params = op.resolutionTime === ir.I18nParamResolutionTime.Creation ?
i18nContext.params :
i18nContext.postprocessingParams;
const values = params.get(op.i18nPlaceholder) || [];
- values.push(
- {value: index, subTemplateIndex: subTemplateIndex, flags: ir.I18nParamValueFlags.None});
+ values.push({
+ value: index,
+ subTemplateIndex: subTemplateIndex,
+ flags: ir.I18nParamValueFlags.ExpressionIndex
+ });
params.set(op.i18nPlaceholder, values);
- expressionIndices.set(op.context, index + 1);
+ expressionIndices.set(i18nContext.i18nBlock, index + 1);
}
}
}
diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts
new file mode 100644
index 00000000000000..595660908f3e9e
--- /dev/null
+++ b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import * as i18n from '../../../../i18n/i18n_ast';
+import * as ir from '../../ir';
+import {CompilationJob} from '../compilation';
+
+/**
+ * Resolves placeholders for element tags inside of an ICU.
+ */
+export function resolveI18nIcuPlaceholders(job: CompilationJob) {
+ const contextOps = new Map();
+ for (const unit of job.units) {
+ for (const op of unit.create) {
+ switch (op.kind) {
+ case ir.OpKind.I18nContext:
+ contextOps.set(op.xref, op);
+ break;
+ }
+ }
+ }
+
+ for (const unit of job.units) {
+ for (const op of unit.create) {
+ switch (op.kind) {
+ case ir.OpKind.IcuStart:
+ if (op.context === null) {
+ throw Error('Icu should have its i18n context set.');
+ }
+ const i18nContext = contextOps.get(op.context)!;
+ for (const node of op.message.nodes) {
+ node.visit(new ResolveIcuPlaceholdersVisitor(i18nContext.postprocessingParams));
+ }
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Visitor for i18n AST that resolves ICU params into the given map.
+ */
+class ResolveIcuPlaceholdersVisitor extends i18n.RecurseVisitor {
+ constructor(private readonly params: Map) {
+ super();
+ }
+
+ override visitTagPlaceholder(placeholder: i18n.TagPlaceholder) {
+ super.visitTagPlaceholder(placeholder);
+
+ // Add the start and end source span for tag placeholders. These need to be recorded for
+ // elements inside ICUs. The slots for the elements were recorded separately under the i18n
+ // block's context as part of the `resolveI18nElementPlaceholders` phase.
+ if (placeholder.startName && placeholder.startSourceSpan &&
+ !this.params.has(placeholder.startName)) {
+ this.params.set(placeholder.startName, [{
+ value: placeholder.startSourceSpan?.toString(),
+ subTemplateIndex: null,
+ flags: ir.I18nParamValueFlags.None
+ }]);
+ }
+ if (placeholder.closeName && placeholder.endSourceSpan &&
+ !this.params.has(placeholder.closeName)) {
+ this.params.set(placeholder.closeName, [{
+ value: placeholder.endSourceSpan?.toString(),
+ subTemplateIndex: null,
+ flags: ir.I18nParamValueFlags.None
+ }]);
+ }
+ }
+}
diff --git a/packages/compiler/src/template/pipeline/src/phases/wrap_icus.ts b/packages/compiler/src/template/pipeline/src/phases/wrap_icus.ts
index ff7bc08eef1062..ca119ba157100f 100644
--- a/packages/compiler/src/template/pipeline/src/phases/wrap_icus.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/wrap_icus.ts
@@ -15,6 +15,7 @@ import {CompilationJob} from '../compilation';
export function wrapI18nIcus(job: CompilationJob): void {
for (const unit of job.units) {
let currentI18nOp: ir.I18nStartOp|null = null;
+ let addedI18nId: ir.XrefId|null = null;
for (const op of unit.create) {
switch (op.kind) {
case ir.OpKind.I18nStart:
@@ -23,11 +24,16 @@ export function wrapI18nIcus(job: CompilationJob): void {
case ir.OpKind.I18nEnd:
currentI18nOp = null;
break;
- case ir.OpKind.Icu:
+ case ir.OpKind.IcuStart:
if (currentI18nOp === null) {
- const id = job.allocateXrefId();
- ir.OpList.insertBefore(ir.createI18nStartOp(id, op.message), op);
- ir.OpList.insertAfter(ir.createI18nEndOp(id), op);
+ addedI18nId = job.allocateXrefId();
+ ir.OpList.insertBefore(ir.createI18nStartOp(addedI18nId, op.message), op);
+ }
+ break;
+ case ir.OpKind.IcuEnd:
+ if (addedI18nId !== null) {
+ ir.OpList.insertAfter(ir.createI18nEndOp(addedI18nId), op);
+ addedI18nId = null;
}
break;
}
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
index 8b5ffd27b3d00c..49ed4af7b51a6f 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts
@@ -64,7 +64,7 @@ export function migrateIf(template: string): {migrated: string, errors: MigrateE
return {migrated: result, errors};
}
-export function migrateNgIf(etm: ElementToMigrate, tmpl: string, offset: number): Result {
+function migrateNgIf(etm: ElementToMigrate, tmpl: string, offset: number): Result {
const matchThen = etm.attr.value.match(/;\s*then/gm);
const matchElse = etm.attr.value.match(/;\s*else/gm);
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/index.ts b/packages/core/schematics/ng-generate/control-flow-migration/index.ts
index cf20669cb391f3..f5664072185baf 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/index.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/index.ts
@@ -13,11 +13,9 @@ import {normalizePath} from '../../utils/change_tracker';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
-import {migrateFor} from './fors';
-import {migrateIf} from './ifs';
-import {migrateSwitch} from './switches';
+import {migrateTemplate} from './migration';
import {AnalyzedFile, MigrateError} from './types';
-import {analyze, processNgTemplates} from './util';
+import {analyze} from './util';
interface Options {
path: string;
@@ -88,17 +86,7 @@ function runControlFlowMigration(
const template = content.slice(start, end);
const length = (end ?? content.length) - start;
- const ifResult = migrateIf(template);
- const forResult = migrateFor(ifResult.migrated);
- const switchResult = migrateSwitch(forResult.migrated);
-
- const errors = [
- ...ifResult.errors,
- ...forResult.errors,
- ...switchResult.errors,
- ];
-
- const migrated = processNgTemplates(switchResult.migrated);
+ const {migrated, errors} = migrateTemplate(template);
if (migrated !== null) {
update.remove(start, length);
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/migration.ts b/packages/core/schematics/ng-generate/control-flow-migration/migration.ts
new file mode 100644
index 00000000000000..3d8568015bde77
--- /dev/null
+++ b/packages/core/schematics/ng-generate/control-flow-migration/migration.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {migrateFor} from './fors';
+import {migrateIf} from './ifs';
+import {migrateSwitch} from './switches';
+import {MigrateError} from './types';
+import {processNgTemplates} from './util';
+
+/**
+ * Actually migrates a given template to the new syntax
+ */
+export function migrateTemplate(template: string): {migrated: string, errors: MigrateError[]} {
+ const ifResult = migrateIf(template);
+ const forResult = migrateFor(ifResult.migrated);
+ const switchResult = migrateSwitch(forResult.migrated);
+
+ const migrated = processNgTemplates(switchResult.migrated);
+
+ const errors = [
+ ...ifResult.errors,
+ ...forResult.errors,
+ ...switchResult.errors,
+ ];
+ return {migrated, errors};
+}
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/types.ts b/packages/core/schematics/ng-generate/control-flow-migration/types.ts
index adfaa76cb0f4d7..2c9c01cad7f73c 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/types.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/types.ts
@@ -75,6 +75,9 @@ export class ElementToMigrate {
}
}
+/**
+ * Represents an ng-template inside a template being migrated to new control flow
+ */
export class Template {
el: Element;
count: number = 0;
diff --git a/packages/core/schematics/ng-generate/control-flow-migration/util.ts b/packages/core/schematics/ng-generate/control-flow-migration/util.ts
index d612c43817b628..a8c1d2b42d4a9e 100644
--- a/packages/core/schematics/ng-generate/control-flow-migration/util.ts
+++ b/packages/core/schematics/ng-generate/control-flow-migration/util.ts
@@ -82,6 +82,9 @@ function getNestedCount(etm: ElementToMigrate, aggregator: number[]) {
}
}
+/**
+ * parses the template string into the Html AST
+ */
export function parseTemplate(template: string): ParseTreeResult|null {
let parsed: ParseTreeResult;
try {
@@ -108,6 +111,9 @@ export function parseTemplate(template: string): ParseTreeResult|null {
return parsed;
}
+/**
+ * calculates the level of nesting of the items in the collector
+ */
export function calculateNesting(
visitor: ElementCollector|TemplateCollector, hasLineBreaks: boolean): void {
// start from top of template
@@ -133,10 +139,16 @@ function escapeRegExp(val: string) {
return val.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
+/**
+ * determines if a given template string contains line breaks
+ */
export function hasLineBreaks(template: string): boolean {
return /\r|\n/.test(template);
}
+/**
+ * properly adjusts template offsets based on current nesting levels
+ */
export function reduceNestingOffset(
el: ElementToMigrate, nestLevel: number, offset: number, postOffsets: number[]): number {
if (el.nestCount <= nestLevel) {
@@ -178,6 +190,9 @@ function wrapIntoI18nContainer(i18nAttr: Attribute, content: string) {
return `${content} `;
}
+/**
+ * Counts, replaces, and removes any necessary ng-templates post control flow migration
+ */
export function processNgTemplates(template: string): string {
// count usage
const templates = countTemplateUsage(template);
@@ -201,6 +216,10 @@ export function processNgTemplates(template: string): string {
return template;
}
+/**
+ * retrieves the original block of text in the template for length comparison during migration
+ * processing
+ */
export function getOriginals(
etm: ElementToMigrate, tmpl: string, offset: number): {start: string, end: string} {
// original opening block
@@ -221,6 +240,9 @@ export function getOriginals(
return {start, end: ''};
}
+/**
+ * builds the proper contents of what goes inside a given control flow block after migration
+ */
export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
{start: string, middle: string, end: string} {
const i18nAttr = etm.el.attrs.find(x => x.name === 'i18n');
diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts
index e02fe02af9e08d..224bfba7cc5b9a 100644
--- a/packages/core/src/core_private_export.ts
+++ b/packages/core/src/core_private_export.ts
@@ -41,6 +41,6 @@ export {booleanAttribute, numberAttribute} from './util/coercion';
export {devModeEqual as ɵdevModeEqual} from './util/comparison';
export {global as ɵglobal} from './util/global';
export {isPromise as ɵisPromise, isSubscribable as ɵisSubscribable} from './util/lang';
-export {performanceMark as ɵperformanceMark} from './util/performance';
+export {performanceMarkFeature as ɵperformanceMarkFeature} from './util/performance';
export {stringify as ɵstringify, truncateMiddle as ɵtruncateMiddle} from './util/stringify';
export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider_flags';
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index a3a2b1ffb15ae8..5bdad81e62f01d 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -316,6 +316,6 @@ export {
export { AfterRenderEventManager as ɵAfterRenderEventManager, internalAfterNextRender as ɵinternalAfterNextRender } from './render3/after_render_hooks';
export {depsTracker as ɵdepsTracker, USE_RUNTIME_DEPS_TRACKER_FOR_JIT as ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT} from './render3/deps_tracker/deps_tracker';
export {generateStandaloneInDeclarationsError as ɵgenerateStandaloneInDeclarationsError} from './render3/jit/module';
-export {getAsyncClassMetadata as ɵgetAsyncClassMetadata} from './render3/metadata';
+export {getAsyncClassMetadataFn as ɵgetAsyncClassMetadataFn} from './render3/metadata';
// clang-format on
diff --git a/packages/core/src/defer/instructions.ts b/packages/core/src/defer/instructions.ts
index 64f04c20ef332a..bb31ab813881f6 100644
--- a/packages/core/src/defer/instructions.ts
+++ b/packages/core/src/defer/instructions.ts
@@ -29,7 +29,7 @@ import {isPlatformBrowser} from '../render3/util/misc_utils';
import {getConstant, getTNode, removeLViewOnDestroy, storeLViewOnDestroy} from '../render3/util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer, shouldAddViewToDom} from '../render3/view_manipulation';
import {assertDefined, throwError} from '../util/assert';
-import {performanceMark} from '../util/performance';
+import {performanceMarkFeature} from '../util/performance';
import {invokeAllTriggerCleanupFns, invokeTriggerCleanupFns, storeTriggerCleanupFn} from './cleanup';
import {onHover, onInteraction, onViewport, registerDomTrigger} from './dom_triggers';
@@ -130,7 +130,7 @@ export function ɵɵdefer(
ɵɵtemplate(index, null, 0, 0);
if (tView.firstCreatePass) {
- performanceMark('mark_use_counter', {detail: {feature: 'NgDefer'}});
+ performanceMarkFeature('NgDefer');
const tDetails: TDeferBlockDetails = {
primaryTmplIndex,
diff --git a/packages/core/src/hydration/api.ts b/packages/core/src/hydration/api.ts
index cb38f59516ff72..d95b51cfb01965 100644
--- a/packages/core/src/hydration/api.ts
+++ b/packages/core/src/hydration/api.ts
@@ -20,7 +20,7 @@ import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
import {getDocument} from '../render3/interfaces/document';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {TransferState} from '../transfer_state';
-import {performanceMark} from '../util/performance';
+import {performanceMarkFeature} from '../util/performance';
import {NgZone} from '../zone';
import {cleanupDehydratedViews} from './cleanup';
@@ -137,7 +137,7 @@ export function withDomHydration(): EnvironmentProviders {
}
}
if (isEnabled) {
- performanceMark('mark_use_counter', {detail: {feature: 'NgHydration'}});
+ performanceMarkFeature('NgHydration');
}
return isEnabled;
},
diff --git a/packages/core/src/image_performance_warning.ts b/packages/core/src/image_performance_warning.ts
index 0409dc5d4b8aca..b7490789627949 100644
--- a/packages/core/src/image_performance_warning.ts
+++ b/packages/core/src/image_performance_warning.ts
@@ -155,12 +155,12 @@ function logLazyLCPWarning(src: string) {
console.warn(formatRuntimeError(
RuntimeErrorCode.IMAGE_PERFORMANCE_WARNING,
`An image with src ${src} is the Largest Contentful Paint (LCP) element ` +
- `but was given a "loading" value of "lazy", which can negatively impact` +
+ `but was given a "loading" value of "lazy", which can negatively impact ` +
`application loading performance. This warning can be addressed by ` +
`changing the loading value of the LCP image to "eager", or by using the ` +
`NgOptimizedImage directive's prioritization utilities. For more ` +
`information about addressing or disabling this warning, see ` +
- `https://angular.io/errors/NG2965`));
+ `https://angular.io/errors/NG0913`));
}
function logOversizedImageWarning(src: string) {
@@ -169,5 +169,5 @@ function logOversizedImageWarning(src: string) {
`An image with src ${src} has intrinsic file dimensions much larger than its ` +
`rendered size. This can negatively impact application loading performance. ` +
`For more information about addressing or disabling this warning, see ` +
- `https://angular.io/errors/NG2965`));
+ `https://angular.io/errors/NG0913`));
}
diff --git a/packages/core/src/linker/component_factory.ts b/packages/core/src/linker/component_factory.ts
index 30700a126ad71a..8d1f9f7eb2b5ff 100644
--- a/packages/core/src/linker/component_factory.ts
+++ b/packages/core/src/linker/component_factory.ts
@@ -83,8 +83,6 @@ export abstract class ComponentRef {
* Instantiate a factory for a given type of component with `resolveComponentFactory()`.
* Use the resulting `ComponentFactory.create()` method to create a component of that type.
*
- * @see [Dynamic Components](guide/dynamic-component-loader)
- *
* @publicApi
*
* @deprecated Angular no longer requires Component factories. Please use other APIs where
diff --git a/packages/core/src/render3/after_render_hooks.ts b/packages/core/src/render3/after_render_hooks.ts
index ec9ceb36a907b5..58f45f8f6d93a0 100644
--- a/packages/core/src/render3/after_render_hooks.ts
+++ b/packages/core/src/render3/after_render_hooks.ts
@@ -13,7 +13,7 @@ import {ErrorHandler} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {DestroyRef} from '../linker/destroy_ref';
import {assertGreaterThan} from '../util/assert';
-import {performanceMark} from '../util/performance';
+import {performanceMarkFeature} from '../util/performance';
import {NgZone} from '../zone';
import {isPlatformBrowser} from './util/misc_utils';
@@ -225,7 +225,7 @@ export function afterRender(callback: VoidFunction, options?: AfterRenderOptions
return NOOP_AFTER_RENDER_REF;
}
- performanceMark('mark_use_counter', {detail: {feature: 'NgAfterRender'}});
+ performanceMarkFeature('NgAfterRender');
const afterRenderEventManager = injector.get(AfterRenderEventManager);
// Lazily initialize the handler implementation, if necessary. This is so that it can be
@@ -301,7 +301,7 @@ export function afterNextRender(
return NOOP_AFTER_RENDER_REF;
}
- performanceMark('mark_use_counter', {detail: {feature: 'NgAfterNextRender'}});
+ performanceMarkFeature('NgAfterNextRender');
const afterRenderEventManager = injector.get(AfterRenderEventManager);
// Lazily initialize the handler implementation, if necessary. This is so that it can be
diff --git a/packages/core/src/render3/features/standalone_feature.ts b/packages/core/src/render3/features/standalone_feature.ts
index 0ad4dfc035d177..1c2d813cdd2748 100644
--- a/packages/core/src/render3/features/standalone_feature.ts
+++ b/packages/core/src/render3/features/standalone_feature.ts
@@ -10,7 +10,7 @@ import {ɵɵdefineInjectable as defineInjectable} from '../../di/interface/defs'
import {internalImportProvidersFrom} from '../../di/provider_collection';
import {EnvironmentInjector} from '../../di/r3_injector';
import {OnDestroy} from '../../interface/lifecycle_hooks';
-import {performanceMark} from '../../util/performance';
+import {performanceMarkFeature} from '../../util/performance';
import {ComponentDef} from '../interfaces/definition';
import {createEnvironmentInjector} from '../ng_module_ref';
@@ -61,9 +61,6 @@ class StandaloneService implements OnDestroy {
});
}
-const PERF_MARK_STANDALONE = {
- detail: {feature: 'NgStandalone'}
-};
/**
* A feature that acts as a setup code for the {@link StandaloneService}.
@@ -76,7 +73,7 @@ const PERF_MARK_STANDALONE = {
* @codeGenApi
*/
export function ɵɵStandaloneFeature(definition: ComponentDef) {
- performanceMark('mark_use_counter', PERF_MARK_STANDALONE);
+ performanceMarkFeature('NgStandalone');
definition.getStandaloneInjector = (parentInjector: EnvironmentInjector) => {
return parentInjector.get(StandaloneService).getOrCreateStandaloneInjector(definition);
};
diff --git a/packages/core/src/render3/instructions/control_flow.ts b/packages/core/src/render3/instructions/control_flow.ts
index 4240990e290c52..11acac874342c7 100644
--- a/packages/core/src/render3/instructions/control_flow.ts
+++ b/packages/core/src/render3/instructions/control_flow.ts
@@ -12,7 +12,7 @@ import {TrackByFunction} from '../../change_detection';
import {DehydratedContainerView} from '../../hydration/interfaces';
import {findMatchingDehydratedView} from '../../hydration/views';
import {assertDefined} from '../../util/assert';
-import {performanceMark} from '../../util/performance';
+import {performanceMarkFeature} from '../../util/performance';
import {assertLContainer, assertLView, assertTNode} from '../assert';
import {bindingUpdated} from '../bindings';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
@@ -27,10 +27,6 @@ import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContain
import {ɵɵtemplate} from './template';
-const PERF_MARK_CONTROL_FLOW = {
- detail: {feature: 'NgControlFlow'}
-};
-
/**
* The conditional instruction represents the basic building block on the runtime side to support
* built-in "if" and "switch". On the high level this instruction is responsible for adding and
@@ -43,7 +39,7 @@ const PERF_MARK_CONTROL_FLOW = {
* @codeGenApi
*/
export function ɵɵconditional(containerIndex: number, matchingTemplateIndex: number, value?: T) {
- performanceMark('mark_use_counter', PERF_MARK_CONTROL_FLOW);
+ performanceMarkFeature('NgControlFlow');
const hostLView = getLView();
const bindingIndex = nextBindingIndex();
@@ -149,7 +145,7 @@ export function ɵɵrepeaterCreate(
tagName: string|null, attrsIndex: number|null, trackByFn: TrackByFunction,
trackByUsesComponentInstance?: boolean, emptyTemplateFn?: ComponentTemplate,
emptyDecls?: number, emptyVars?: number): void {
- performanceMark('mark_use_counter', PERF_MARK_CONTROL_FLOW);
+ performanceMarkFeature('NgControlFlow');
const hasEmptyBlock = emptyTemplateFn !== undefined;
const hostLView = getLView();
const boundTrackBy = trackByUsesComponentInstance ?
diff --git a/packages/core/src/render3/list_reconciliation.ts b/packages/core/src/render3/list_reconciliation.ts
index 3ac497324b5c37..db7b594ae83ce1 100644
--- a/packages/core/src/render3/list_reconciliation.ts
+++ b/packages/core/src/render3/list_reconciliation.ts
@@ -238,8 +238,11 @@ export function reconcile(
liveCollection.destroy(liveCollection.detach(liveEndIdx--));
}
+
// - destroy items that were detached but never attached again.
- detachedItems?.forEach(item => liveCollection.destroy(item));
+ detachedItems?.forEach(item => {
+ liveCollection.destroy(item);
+ });
}
function attachPreviouslyDetached(
@@ -285,8 +288,7 @@ class MultiMap {
delete(key: K): boolean {
const listOfKeys = this.map.get(key);
if (listOfKeys !== undefined) {
- // THINK: pop from the end or shift from the front? "Correct" vs. "slow".
- listOfKeys.pop();
+ listOfKeys.shift();
return true;
}
return false;
diff --git a/packages/core/src/render3/metadata.ts b/packages/core/src/render3/metadata.ts
index 1b9402a0b92801..48935849f4b28b 100644
--- a/packages/core/src/render3/metadata.ts
+++ b/packages/core/src/render3/metadata.ts
@@ -16,24 +16,26 @@ interface TypeWithMetadata extends Type {
}
/**
- * The name of a field that Angular monkey-patches onto a class
- * to keep track of the Promise that represents dependency loading
- * state.
+ * The name of a field that Angular monkey-patches onto a component
+ * class to store a function that loads defer-loadable dependencies
+ * and applies metadata to a class.
*/
-const ASYNC_COMPONENT_METADATA = '__ngAsyncComponentMetadata__';
+const ASYNC_COMPONENT_METADATA_FN = '__ngAsyncComponentMetadataFn__';
/**
- * If a given component has unresolved async metadata - this function returns a reference to
- * a Promise that represents dependency loading. Otherwise - this function returns `null`.
+ * If a given component has unresolved async metadata - returns a reference
+ * to a function that applies component metadata after resolving defer-loadable
+ * dependencies. Otherwise - this function returns `null`.
*/
-export function getAsyncClassMetadata(type: Type): Promise>>|null {
- const componentClass = type as any; // cast to `any`, so that we can monkey-patch it
- return componentClass[ASYNC_COMPONENT_METADATA] ?? null;
+export function getAsyncClassMetadataFn(type: Type): (() => Promise>>)|
+ null {
+ const componentClass = type as any; // cast to `any`, so that we can read a monkey-patched field
+ return componentClass[ASYNC_COMPONENT_METADATA_FN] ?? null;
}
/**
* Handles the process of applying metadata info to a component class in case
- * component template had defer blocks (thus some dependencies became deferrable).
+ * component template has defer blocks (thus some dependencies became deferrable).
*
* @param type Component class where metadata should be added
* @param dependencyLoaderFn Function that loads dependencies
@@ -41,19 +43,18 @@ export function getAsyncClassMetadata(type: Type): Promise, dependencyLoaderFn: () => Array>>,
- metadataSetterFn: (...types: Type[]) => void): Promise>> {
+ metadataSetterFn: (...types: Type[]) => void): () => Promise>> {
const componentClass = type as any; // cast to `any`, so that we can monkey-patch it
- componentClass[ASYNC_COMPONENT_METADATA] =
+ componentClass[ASYNC_COMPONENT_METADATA_FN] = () =>
Promise.all(dependencyLoaderFn()).then(dependencies => {
metadataSetterFn(...dependencies);
// Metadata is now set, reset field value to indicate that this component
// can by used/compiled synchronously.
- componentClass[ASYNC_COMPONENT_METADATA] = null;
+ componentClass[ASYNC_COMPONENT_METADATA_FN] = null;
return dependencies;
});
-
- return componentClass[ASYNC_COMPONENT_METADATA];
+ return componentClass[ASYNC_COMPONENT_METADATA_FN];
}
/**
diff --git a/packages/core/src/util/performance.ts b/packages/core/src/util/performance.ts
index 4c3dcf2c53389b..77b9b86176d536 100644
--- a/packages/core/src/util/performance.ts
+++ b/packages/core/src/util/performance.ts
@@ -6,17 +6,20 @@
* found in the LICENSE file at https://angular.io/license
*/
+const markedFeatures = new Set();
+
// tslint:disable:ban
/**
- * A guarded `performance.mark`.
+ * A guarded `performance.mark` for feature marking.
*
* This method exists because while all supported browser and node.js version supported by Angular
* support performance.mark API. This is not the case for other environments such as JSDOM and
* Cloudflare workers.
*/
-export function performanceMark(
- markName: string,
- markOptions?: PerformanceMarkOptions|undefined,
- ): PerformanceMark|undefined {
- return performance?.mark?.(markName, markOptions);
+export function performanceMarkFeature(feature: string): void {
+ if (markedFeatures.has(feature)) {
+ return;
+ }
+ markedFeatures.add(feature);
+ performance?.mark?.('mark_use_counter', {detail: {feature}});
}
diff --git a/packages/core/test/acceptance/control_flow_for_spec.ts b/packages/core/test/acceptance/control_flow_for_spec.ts
index 2ae74b9bf31849..0b007c22cacc1d 100644
--- a/packages/core/test/acceptance/control_flow_for_spec.ts
+++ b/packages/core/test/acceptance/control_flow_for_spec.ts
@@ -277,6 +277,52 @@ describe('control flow - for', () => {
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('5(0)|3(1)|7(2)|');
});
+
+ it('should correctly attach and detach views with duplicated keys', () => {
+ const BEFORE = [
+ {'name': 'Task 14', 'id': 14},
+ {'name': 'Task 14', 'id': 14},
+ {'name': 'Task 70', 'id': 70},
+ {'name': 'Task 34', 'id': 34},
+
+ ];
+
+ const AFTER = [
+ {'name': 'Task 70', 'id': 70},
+ {'name': 'Task 14', 'id': 14},
+ {'name': 'Task 28', 'id': 28},
+ ];
+
+ @Component({
+ standalone: true,
+ template: ``,
+ selector: 'child-cmp',
+ })
+ class ChildCmp {
+ }
+
+ @Component({
+ standalone: true,
+ imports: [ChildCmp],
+ template: `
+ @for(task of tasks; track task.id) {
+
+ }
+ `,
+ })
+ class TestComponent {
+ tasks = BEFORE;
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+
+ const cmp = fixture.componentInstance;
+ const nativeElement = fixture.debugElement.nativeElement;
+ cmp.tasks = AFTER;
+ fixture.detectChanges();
+ expect(nativeElement.querySelectorAll('child-cmp').length).toBe(3);
+ });
});
describe('content projection', () => {
diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json
index 0c37e3c53a985b..4fd3a46c886ad4 100644
--- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json
@@ -407,9 +407,6 @@
{
"name": "PARAM_REGEX"
},
- {
- "name": "PERF_MARK_STANDALONE"
- },
{
"name": "PLATFORM_DESTROY_LISTENERS"
},
@@ -1211,6 +1208,9 @@
{
"name": "markViewForRefresh"
},
+ {
+ "name": "markedFeatures"
+ },
{
"name": "maybeWrapInNotSelector"
},
diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json
index 22cfd53707105d..2caad4a19a0d1a 100644
--- a/packages/core/test/bundling/defer/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json
@@ -374,9 +374,6 @@
{
"name": "Observable"
},
- {
- "name": "PERF_MARK_STANDALONE"
- },
{
"name": "PLATFORM_DESTROY_LISTENERS"
},
@@ -2144,6 +2141,9 @@
{
"name": "markViewForRefresh"
},
+ {
+ "name": "markedFeatures"
+ },
{
"name": "maybeWrapInNotSelector"
},
@@ -2199,7 +2199,7 @@
"name": "onLeave"
},
{
- "name": "performanceMark"
+ "name": "performanceMarkFeature"
},
{
"name": "populateDehydratedViewsInLContainer"
diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json
index 60a7798efc3873..9ce67584be42e5 100644
--- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json
@@ -380,9 +380,6 @@
{
"name": "Observable"
},
- {
- "name": "PERF_MARK_STANDALONE"
- },
{
"name": "PLATFORM_DESTROY_LISTENERS"
},
@@ -1109,6 +1106,9 @@
{
"name": "markViewForRefresh"
},
+ {
+ "name": "markedFeatures"
+ },
{
"name": "maybeWrapInNotSelector"
},
@@ -1173,7 +1173,7 @@
"name": "onLeave"
},
{
- "name": "performanceMark"
+ "name": "performanceMarkFeature"
},
{
"name": "populateDehydratedViewsInLContainerImpl"
diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json
index 3da6ad301f0bdf..473795c544022f 100644
--- a/packages/core/test/bundling/router/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/router/bundle.golden_symbols.json
@@ -536,9 +536,6 @@
{
"name": "OutletInjector"
},
- {
- "name": "PERF_MARK_STANDALONE"
- },
{
"name": "PLATFORM_DESTROY_LISTENERS"
},
@@ -1709,6 +1706,9 @@
{
"name": "markViewForRefresh"
},
+ {
+ "name": "markedFeatures"
+ },
{
"name": "match"
},
diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json
index 0a87cc24fdd89a..08acc73fd23ead 100644
--- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json
@@ -296,9 +296,6 @@
{
"name": "Observable"
},
- {
- "name": "PERF_MARK_STANDALONE"
- },
{
"name": "PLATFORM_DESTROY_LISTENERS"
},
@@ -890,6 +887,9 @@
{
"name": "markViewForRefresh"
},
+ {
+ "name": "markedFeatures"
+ },
{
"name": "maybeWrapInNotSelector"
},
diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts
index 932fde35663067..711d935b3b385d 100644
--- a/packages/core/testing/src/test_bed.ts
+++ b/packages/core/testing/src/test_bed.ts
@@ -27,7 +27,7 @@ import {
ɵconvertToBitFlags as convertToBitFlags,
ɵDeferBlockBehavior as DeferBlockBehavior,
ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible,
- ɵgetAsyncClassMetadata as getAsyncClassMetadata,
+ ɵgetAsyncClassMetadataFn as getAsyncClassMetadataFn,
ɵgetUnknownElementStrictMode as getUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode as getUnknownPropertyStrictMode,
ɵRender3ComponentFactory as ComponentFactory,
@@ -616,7 +616,7 @@ export class TestBedImpl implements TestBed {
const rootElId = `root${_nextRootElementId++}`;
testComponentRenderer.insertRootElement(rootElId);
- if (getAsyncClassMetadata(type)) {
+ if (getAsyncClassMetadataFn(type)) {
throw new Error(
`Component '${type.name}' has unresolved metadata. ` +
`Please call \`await TestBed.compileComponents()\` before running this test.`);
diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts
index 2822bec89e29e4..1039a77822165e 100644
--- a/packages/core/testing/src/test_bed_compiler.ts
+++ b/packages/core/testing/src/test_bed_compiler.ts
@@ -7,7 +7,7 @@
*/
import {ResourceLoader} from '@angular/compiler';
-import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, provideZoneChangeDetection, resolveForwardRef, StaticProvider, Type, ɵclearResolutionOfComponentResourcesQueue, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDEFER_BLOCK_CONFIG as DEFER_BLOCK_CONFIG, ɵDeferBlockBehavior as DeferBlockBehavior, ɵdepsTracker as depsTracker, ɵDirectiveDef as DirectiveDef, ɵgenerateStandaloneInDeclarationsError, ɵgetAsyncClassMetadata as getAsyncClassMetadata, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisComponentDefPendingResolution, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵresolveComponentResources, ɵrestoreComponentResolutionQueue, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core';
+import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, provideZoneChangeDetection, resolveForwardRef, StaticProvider, Type, ɵclearResolutionOfComponentResourcesQueue, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDEFER_BLOCK_CONFIG as DEFER_BLOCK_CONFIG, ɵDeferBlockBehavior as DeferBlockBehavior, ɵdepsTracker as depsTracker, ɵDirectiveDef as DirectiveDef, ɵgenerateStandaloneInDeclarationsError, ɵgetAsyncClassMetadataFn as getAsyncClassMetadataFn, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisComponentDefPendingResolution, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵresolveComponentResources, ɵrestoreComponentResolutionQueue, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core';
import {ComponentDef, ComponentType} from '../../src/render3';
@@ -28,7 +28,7 @@ function isTestingModuleOverride(value: unknown): value is TestingModuleOverride
function assertNoStandaloneComponents(
types: Type[], resolver: Resolver, location: string) {
types.forEach(type => {
- if (!getAsyncClassMetadata(type)) {
+ if (!getAsyncClassMetadataFn(type)) {
const component = resolver.resolve(type);
if (component && component.standalone) {
throw new Error(ɵgenerateStandaloneInDeclarationsError(type, location));
@@ -262,9 +262,9 @@ export class TestBedCompiler {
const promises = [];
for (const component of this.pendingComponents) {
- const asyncMetadataPromise = getAsyncClassMetadata(component);
- if (asyncMetadataPromise) {
- promises.push(asyncMetadataPromise);
+ const asyncMetadataFn = getAsyncClassMetadataFn(component);
+ if (asyncMetadataFn) {
+ promises.push(asyncMetadataFn());
}
}
@@ -382,7 +382,7 @@ export class TestBedCompiler {
// Compile all queued components, directives, pipes.
let needsAsyncResources = false;
this.pendingComponents.forEach(declaration => {
- if (getAsyncClassMetadata(declaration)) {
+ if (getAsyncClassMetadataFn(declaration)) {
throw new Error(
`Component '${declaration.name}' has unresolved metadata. ` +
`Please call \`await TestBed.compileComponents()\` before running this test.`);
diff --git a/packages/language-service/plugin-factory.ts b/packages/language-service/plugin-factory.ts
index 58c5c51408f0ab..d913d16cbd2f23 100644
--- a/packages/language-service/plugin-factory.ts
+++ b/packages/language-service/plugin-factory.ts
@@ -24,7 +24,9 @@ export const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule =
return plugin.create(info);
},
getExternalFiles(project: ts.server.Project): string[] {
- return plugin?.getExternalFiles?.(project) ?? [];
+ // TODO(crisbeto): hardcoded value `2` to be replaced with `ts.ProgramUpdateLevel.Full`
+ // after we drop support for TS 5.2.
+ return plugin?.getExternalFiles?.(project, 2) ?? [];
},
onConfigurationChanged(config: PluginConfig): void {
plugin?.onConfigurationChanged?.(config);
diff --git a/packages/localize/package.json b/packages/localize/package.json
index 69e12ae192df5a..001aca6eca908d 100644
--- a/packages/localize/package.json
+++ b/packages/localize/package.json
@@ -35,6 +35,8 @@
],
"dependencies": {
"@babel/core": "7.23.2",
+ "@types/babel__core": "7.20.2",
+ "@types/babel__traverse": "7.20.2",
"fast-glob": "3.3.1",
"yargs": "^17.2.1"
},
diff --git a/packages/tsconfig.json b/packages/tsconfig.json
index e64978daf6b21d..ccdee15f31de8f 100644
--- a/packages/tsconfig.json
+++ b/packages/tsconfig.json
@@ -32,7 +32,7 @@
"lib": ["es2020", "dom", "dom.iterable"],
"skipDefaultLibCheck": true,
"skipLibCheck": true,
- "types": ["angular"]
+ "types": ["angular", "dom-navigation"]
},
"bazelOptions": {
"suppressTsconfigOverrideWarnings": true
diff --git a/yarn.lock b/yarn.lock
index 8682d9dc37d641..4784f0b4d53eed 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3172,6 +3172,11 @@
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.7.tgz#471bad8be0d911ed04d115863402920f3a84079c"
integrity sha512-adBosR2GntaQQiuHnfRN9HtxYpoHHJBcdyz7VSXhjpSAmtvIfu/S1fjTqwuIx/Ypba6LCZdfWIqPYx2BR5TneQ==
+"@types/dom-navigation@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@types/dom-navigation/-/dom-navigation-1.0.2.tgz#b8aad644eaddb65f2c7d87b582e6773fd68e06bd"
+ integrity sha512-5OchCMi8lg4FYeDG6cW+IpBn8iTKbgdGbC0Nj6er01tiw0c4WOUnTEQXSu1OqnfK13k+oaFvNQ8QAju5+2uiBw==
+
"@types/dom-view-transitions@^1.0.1":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@types/dom-view-transitions/-/dom-view-transitions-1.0.3.tgz#d69fd4512de1c2aa8e01321d5e734b7e447a097c"
@@ -14812,6 +14817,11 @@ typescript@5.2.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
+typescript@5.3.1-rc:
+ version "5.3.1-rc"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.1-rc.tgz#c307d4b69ea0c1c2cd17d4dd7700d30f7197f829"
+ integrity sha512-NVq/AufFc6KVjmVPcuVwdCkhTQlTcMEyRYJPvaGhPvj+X80MYUF+90qf0//uvINPb2ULg9m91/gbdIOhN2cZqA==
+
typescript@^3.9.10, typescript@^3.9.7:
version "3.9.10"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"