From 42eb096b775d88d7f834ae6c9f14fe0220f8a984 Mon Sep 17 00:00:00 2001 From: Jeevan Mahesha Date: Mon, 2 Sep 2024 21:52:26 +0530 Subject: [PATCH 01/40] docs: update comments to use consistent code formatting for boolean values (#57619) PR Close #57619 --- goldens/public-api/common/index.api.md | 10 +++++----- packages/common/src/directives/ng_for_of.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/goldens/public-api/common/index.api.md b/goldens/public-api/common/index.api.md index cfc2e72fd7d5..f23757633029 100644 --- a/goldens/public-api/common/index.api.md +++ b/goldens/public-api/common/index.api.md @@ -536,20 +536,20 @@ export { NgForOf } // @public (undocumented) export class NgForOfContext = NgIterable> { - // (undocumented) $implicit: T; - constructor($implicit: T, ngForOf: U, index: number, count: number); - // (undocumented) + constructor( + $implicit: T, + ngForOf: U, + index: number, + count: number); count: number; // (undocumented) get even(): boolean; // (undocumented) get first(): boolean; - // (undocumented) index: number; // (undocumented) get last(): boolean; - // (undocumented) ngForOf: U; // (undocumented) get odd(): boolean; diff --git a/packages/common/src/directives/ng_for_of.ts b/packages/common/src/directives/ng_for_of.ts index f7ca5760c5b8..389006145b98 100644 --- a/packages/common/src/directives/ng_for_of.ts +++ b/packages/common/src/directives/ng_for_of.ts @@ -29,24 +29,39 @@ import {RuntimeErrorCode} from '../errors'; */ export class NgForOfContext = NgIterable> { constructor( + /** Reference to the current item from the collection. */ public $implicit: T, + + /** + * The value of the iterable expression. Useful when the expression is + * more complex then a property access, for example when using the async pipe + * (`userStreams | async`). + */ public ngForOf: U, + + /** Returns an index of the current item in the collection. */ public index: number, + + /** Returns total amount of items in the collection. */ public count: number, ) {} + // Indicates whether this is the first item in the collection. get first(): boolean { return this.index === 0; } + // Indicates whether this is the last item in the collection. get last(): boolean { return this.index === this.count - 1; } + // Indicates whether an index of this item in the collection is even. get even(): boolean { return this.index % 2 === 0; } + // Indicates whether an index of this item in the collection is odd. get odd(): boolean { return !this.even; } From dc911c635a7f04c19ae26c6a278444c7bf940359 Mon Sep 17 00:00:00 2001 From: arturovt Date: Tue, 1 Oct 2024 16:23:18 +0300 Subject: [PATCH 02/40] refactor(docs-infra): cleanup `AfterRenderSequence` for reference list (#58030) In this commit, we're replacing the provided injector in `afterNextRender` with a node injector because it was previously mistakenly passing an `EnvironmentInjector`. The `EnvironmentInjector` resolves `DestroyRef` to itself, meaning that `AfterRenderSequence` is essentially never destroyed (since the environment injector is not destroyed either). PR Close #58030 --- .../api-reference-list/api-reference-list.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts b/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts index 5afdd196d190..891b780ae713 100644 --- a/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts +++ b/adev/src/app/features/references/api-reference-list/api-reference-list.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, - EnvironmentInjector, + Injector, afterNextRender, computed, effect, @@ -43,7 +43,7 @@ export default class ApiReferenceList { private readonly apiReferenceManager = inject(ApiReferenceManager); private readonly router = inject(Router); filterInput = viewChild.required(TextField, {read: ElementRef}); - private readonly injector = inject(EnvironmentInjector); + private readonly injector = inject(Injector); private readonly allGroups = this.apiReferenceManager.apiGroups; From 8f6560857cc9dfd38daa878f4683d58970d1feeb Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Tue, 3 Sep 2024 14:28:04 -0600 Subject: [PATCH 03/40] docs: reword when to use model inputs (#57648) PR Close #57648 --- adev/src/content/guide/signals/model.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/adev/src/content/guide/signals/model.md b/adev/src/content/guide/signals/model.md index 10a8558e343d..142d53b08e39 100644 --- a/adev/src/content/guide/signals/model.md +++ b/adev/src/content/guide/signals/model.md @@ -130,9 +130,7 @@ through the template. ## When to use model inputs -Use model inputs in components that exist to modify a value based on user interaction. -Custom form controls, such as a date picker or combobox, should use model inputs for their -primary value. - -Avoid using model inputs as a convenience to avoid introducing an additional class property for -containing local state. +Use model inputs when you want a component to support two-way binding. This is typically +appropriate when a component exists to modify a value based on user interaction. Most commonly, +custom form controls such as a date picker or combobox should use model inputs for their primary +value. From 3d1fd3442eb6b107108ab324ee06d536091677f9 Mon Sep 17 00:00:00 2001 From: vladboisa Date: Sun, 28 Jan 2024 17:01:22 +0300 Subject: [PATCH 04/40] fix(docs-infra): max-height IDE error panel visibility (#54128) Remove max-height: 200px in ul child inline-errors-box, add the overflow & max-height in percentages to the parent for correct visualization Fixes #52760 refactor(docs-infra): correct typo Correct typo in comment feat(docs-infra): modify the height of the editor If error box are displayed, modify the height of the editor PR Close #54128 --- .../src/app/editor/code-editor/code-editor.component.scss | 3 ++- adev/src/app/editor/embedded-editor.component.scss | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/adev/src/app/editor/code-editor/code-editor.component.scss b/adev/src/app/editor/code-editor/code-editor.component.scss index 7606aa56c5bf..f973a3bd7b54 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.scss +++ b/adev/src/app/editor/code-editor/code-editor.component.scss @@ -137,6 +137,8 @@ background-color: color-mix(in srgb, var(--bright-blue), var(--page-background) 90%); border: 1px solid color-mix(in srgb, var(--bright-blue), var(--page-background) 70%); border-radius: 0.25rem; + overflow: auto; + max-height: 25%; button { position: absolute; @@ -162,7 +164,6 @@ margin: 0; margin-inline: 1.25rem; color: var(--tertiary-contrast); - max-height: 200px; overflow: auto; } } diff --git a/adev/src/app/editor/embedded-editor.component.scss b/adev/src/app/editor/embedded-editor.component.scss index 7b54416901b1..b3f209042703 100644 --- a/adev/src/app/editor/embedded-editor.component.scss +++ b/adev/src/app/editor/embedded-editor.component.scss @@ -32,7 +32,13 @@ $width-breakpoint: 950px; } } - // If files are displayed, shpare the space + // If error box are displayed, modify the height of the editor + &:has(.adev-inline-errors-box) { + .adev-code-editor-wrapper { + height: calc(50% - 33px) ; + } + } + // If files are displayed, share the space &:has(.docs-editor-tabs) { .adev-tutorial-code-editor { display: block; From ca5bc0c5bdd26b413b56938545fda648eeb33453 Mon Sep 17 00:00:00 2001 From: vladboisa Date: Thu, 1 Feb 2024 16:10:00 +0300 Subject: [PATCH 05/40] feat(docs-infra): add transition on editor wrapper (#54128) Add the smooth animation when height of the container is changing PR Close #54128 --- adev/src/app/editor/code-editor/code-editor.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/adev/src/app/editor/code-editor/code-editor.component.scss b/adev/src/app/editor/code-editor/code-editor.component.scss index f973a3bd7b54..3b6c94aaee71 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.scss +++ b/adev/src/app/editor/code-editor/code-editor.component.scss @@ -122,6 +122,7 @@ // adjust height for terminal tabs // so that scroll bar & content do not render under tabs height: calc(100% - 49px); + transition: height 0.3s ease; &-hidden { display: none; From 694fe3faabb686112d928d258dd5da47abd2a30c Mon Sep 17 00:00:00 2001 From: vladboisa Date: Thu, 1 Feb 2024 16:16:05 +0300 Subject: [PATCH 06/40] fix(docs-infra): fix calculation of height editor (#54128) Apply the min() function for set the smallest height fix(docs-infra): move height into editor-wrapper Move the calculation rule of height edit into editor-wrapper selector fix(docs-infra): change has selector Change the has selector fix(docs-infra): change selector's for child Changing the selector for test this solution Fix PR Close #54128 --- .../app/editor/code-editor/code-editor.component.scss | 10 ++++++++++ adev/src/app/editor/embedded-editor.component.scss | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/adev/src/app/editor/code-editor/code-editor.component.scss b/adev/src/app/editor/code-editor/code-editor.component.scss index 3b6c94aaee71..621fd810d8f5 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.scss +++ b/adev/src/app/editor/code-editor/code-editor.component.scss @@ -127,6 +127,16 @@ &-hidden { display: none; } + + // If error box are displayed, move inline-errors-box + // to bottom of the editor and adjust height + // so that scroll bar & content do not render under tabs + &:has(~ .adev-inline-errors-box) ~ .adev-inline-errors-box { + bottom: auto; + } + &:has(~ .adev-inline-errors-box) { + height: min(75% - 49px); + } } .adev-inline-errors-box { diff --git a/adev/src/app/editor/embedded-editor.component.scss b/adev/src/app/editor/embedded-editor.component.scss index b3f209042703..30db00077121 100644 --- a/adev/src/app/editor/embedded-editor.component.scss +++ b/adev/src/app/editor/embedded-editor.component.scss @@ -32,12 +32,6 @@ $width-breakpoint: 950px; } } - // If error box are displayed, modify the height of the editor - &:has(.adev-inline-errors-box) { - .adev-code-editor-wrapper { - height: calc(50% - 33px) ; - } - } // If files are displayed, share the space &:has(.docs-editor-tabs) { .adev-tutorial-code-editor { From c18ec3dd26efcaa6e3932d9b59013ec75fd6b309 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Wed, 2 Oct 2024 16:58:30 +0200 Subject: [PATCH 07/40] ci: disable updates for @bazel (#58050) This is to prevent the bot to update #51047 PR Close #58050 --- renovate.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 0fb3d514cd05..4aed7e39a8ff 100644 --- a/renovate.json +++ b/renovate.json @@ -45,7 +45,9 @@ "unist-util-visit", "unist-util-visit-parents", "rules_pkg", - "aspect_bazel_lib" + "aspect_bazel_lib", + "@bazel/runfiles", + "build_bazel_rules_nodejs" ], "packageRules": [ { From 1db1b3d6b84e38bc98db315ccb09b74405fff5dd Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Wed, 21 Aug 2024 01:07:13 +0200 Subject: [PATCH 08/40] refactor(compiler-cli): exclude private computed properties from class member extractions (#57596) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will exclude properties like `[ɵWRITABLE_SIGNAL]` from the `WritableSignal` interface. PR Close #57596 --- .../src/ngtsc/docs/src/class_extractor.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts index 6cf0aa24210e..fc179950db40 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts @@ -110,7 +110,10 @@ class ClassExtractor { protected extractClassMember(memberDeclaration: MemberElement): MemberEntry | undefined { if (this.isMethod(memberDeclaration)) { return this.extractMethod(memberDeclaration); - } else if (this.isProperty(memberDeclaration)) { + } else if ( + this.isProperty(memberDeclaration) && + !this.hasPrivateComputedProperty(memberDeclaration) + ) { return this.extractClassProperty(memberDeclaration); } else if (ts.isAccessor(memberDeclaration)) { return this.extractGetterSetter(memberDeclaration); @@ -375,6 +378,17 @@ class ClassExtractor { const modifiers = this.declaration.modifiers ?? []; return modifiers.some((mod) => mod.kind === ts.SyntaxKind.AbstractKeyword); } + + /** + * Check wether a member has a private computed property name like [ɵWRITABLE_SIGNAL] + * + * This will prevent exposing private computed properties in the docs. + */ + private hasPrivateComputedProperty(property: PropertyLike) { + return ( + ts.isComputedPropertyName(property.name) && property.name.expression.getText().startsWith('ɵ') + ); + } } /** Extractor to pull info for API reference documentation for an Angular directive. */ From 44cb2f81313c10968b11c8303aa1cfe2f641658f Mon Sep 17 00:00:00 2001 From: Jeevan Mahesha Date: Sun, 1 Sep 2024 23:17:21 +0530 Subject: [PATCH 09/40] docs: add documentation for lazy loading a standalone component (#57620) Added a new section in the documentation explaining how to lazy load a standalone component using `loadComponent`. This includes a code example demonstrating the setup in Angular routes. PR Close #57620 --- .../content/guide/routing/common-router-tasks.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/adev/src/content/guide/routing/common-router-tasks.md b/adev/src/content/guide/routing/common-router-tasks.md index 0d5b4fcf60dc..f12949eb3b72 100644 --- a/adev/src/content/guide/routing/common-router-tasks.md +++ b/adev/src/content/guide/routing/common-router-tasks.md @@ -474,6 +474,20 @@ gotoItems(hero: Hero) { You can configure your routes to lazy load modules, which means that Angular only loads modules as needed, rather than loading all modules when the application launches. Additionally, preload parts of your application in the background to improve the user experience. +Any route can lazily load its routed, standalone component by using `loadComponent:` + + + +const routes: Routes = [ + { + path: 'lazy', + loadComponent: () => import('./lazy.component').then(c => c.LazyComponent) + } +]; + +This works as long as the loaded component is standalone. + + For more information on lazy loading and preloading see the dedicated guide [Lazy loading](guide/ngmodules/lazy-loading). ## Preventing unauthorized access From de03e932620c6da75283bfa9235419c295fbb990 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Tue, 1 Oct 2024 00:49:03 +0200 Subject: [PATCH 10/40] build: update ts target to ES2022 and module to ES2022 (#58022) Our integration tests are based on the CLI. CLI build force the target to ES2022 else it logs a warning PR Close #58022 --- integration/animations-async/tsconfig.json | 2 +- integration/animations/tsconfig.json | 4 ++-- integration/cli-elements-universal/tsconfig.json | 4 ++-- integration/cli-hello-world-ivy-i18n/tsconfig.json | 4 ++-- integration/cli-hello-world-lazy/tsconfig.json | 4 ++-- integration/cli-hello-world-mocha/tsconfig.json | 4 ++-- integration/cli-hello-world/tsconfig.json | 4 ++-- integration/cli-signal-inputs/tsconfig.json | 4 ++-- integration/defer/tsconfig.json | 4 ++-- integration/dynamic-compiler/tsconfig.json | 4 ++-- integration/forms/tsconfig.json | 4 ++-- integration/ivy-i18n/tsconfig.json | 4 ++-- integration/ng-add-localize/tsconfig.json | 2 +- integration/ng_elements/tsconfig.json | 2 +- integration/ng_update_migrations/tsconfig.json | 4 ++-- integration/standalone-bootstrap/tsconfig.json | 4 ++-- integration/trusted-types/tsconfig.json | 4 ++-- 17 files changed, 31 insertions(+), 31 deletions(-) diff --git a/integration/animations-async/tsconfig.json b/integration/animations-async/tsconfig.json index 274fbe2affa2..195ad48ae3c4 100644 --- a/integration/animations-async/tsconfig.json +++ b/integration/animations-async/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, "target": "es2022", diff --git a/integration/animations/tsconfig.json b/integration/animations/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/animations/tsconfig.json +++ b/integration/animations/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/cli-elements-universal/tsconfig.json b/integration/cli-elements-universal/tsconfig.json index 6df1071edd7e..f431b70173fb 100644 --- a/integration/cli-elements-universal/tsconfig.json +++ b/integration/cli-elements-universal/tsconfig.json @@ -14,8 +14,8 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2015", - "module": "es2020", + "target": "es2022", + "module": "es2022", "lib": [ "es2018", "dom" diff --git a/integration/cli-hello-world-ivy-i18n/tsconfig.json b/integration/cli-hello-world-ivy-i18n/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/cli-hello-world-ivy-i18n/tsconfig.json +++ b/integration/cli-hello-world-ivy-i18n/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/cli-hello-world-lazy/tsconfig.json b/integration/cli-hello-world-lazy/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/cli-hello-world-lazy/tsconfig.json +++ b/integration/cli-hello-world-lazy/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/cli-hello-world-mocha/tsconfig.json b/integration/cli-hello-world-mocha/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/cli-hello-world-mocha/tsconfig.json +++ b/integration/cli-hello-world-mocha/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/cli-hello-world/tsconfig.json b/integration/cli-hello-world/tsconfig.json index 0d22beecb727..6d01924436cb 100644 --- a/integration/cli-hello-world/tsconfig.json +++ b/integration/cli-hello-world/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": ["node_modules/@types"], "lib": ["es2018", "dom"] }, diff --git a/integration/cli-signal-inputs/tsconfig.json b/integration/cli-signal-inputs/tsconfig.json index 0b56495c0943..d07f2a627156 100644 --- a/integration/cli-signal-inputs/tsconfig.json +++ b/integration/cli-signal-inputs/tsconfig.json @@ -8,10 +8,10 @@ "strict": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": ["node_modules/@types"], "lib": ["es2018", "dom"] }, diff --git a/integration/defer/tsconfig.json b/integration/defer/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/defer/tsconfig.json +++ b/integration/defer/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/dynamic-compiler/tsconfig.json b/integration/dynamic-compiler/tsconfig.json index e6c8da5c89c5..571967b7d41c 100644 --- a/integration/dynamic-compiler/tsconfig.json +++ b/integration/dynamic-compiler/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "es2020", - "module": "es2020", + "target": "es2022", + "module": "es2022", "moduleResolution": "node", "declaration": false, "removeComments": true, diff --git a/integration/forms/tsconfig.json b/integration/forms/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/forms/tsconfig.json +++ b/integration/forms/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/ivy-i18n/tsconfig.json b/integration/ivy-i18n/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/ivy-i18n/tsconfig.json +++ b/integration/ivy-i18n/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/ng-add-localize/tsconfig.json b/integration/ng-add-localize/tsconfig.json index d85741f5e04d..dd149a779268 100644 --- a/integration/ng-add-localize/tsconfig.json +++ b/integration/ng-add-localize/tsconfig.json @@ -17,7 +17,7 @@ "moduleResolution": "node", "importHelpers": true, "target": "es2017", - "module": "es2020", + "module": "es2022", "lib": [ "es2018", "dom" diff --git a/integration/ng_elements/tsconfig.json b/integration/ng_elements/tsconfig.json index e6a7a10295c5..a6773a564417 100644 --- a/integration/ng_elements/tsconfig.json +++ b/integration/ng_elements/tsconfig.json @@ -7,7 +7,7 @@ "module": "es2015", "moduleResolution": "node", "strictNullChecks": true, - "target": "es2015", + "target": "es2022", "sourceMap": false, "experimentalDecorators": true, "outDir": "built", diff --git a/integration/ng_update_migrations/tsconfig.json b/integration/ng_update_migrations/tsconfig.json index 6329b405f6b7..f9b4d004128c 100644 --- a/integration/ng_update_migrations/tsconfig.json +++ b/integration/ng_update_migrations/tsconfig.json @@ -6,12 +6,12 @@ "sourceMap": true, "esModuleInterop": true, "declaration": false, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/standalone-bootstrap/tsconfig.json b/integration/standalone-bootstrap/tsconfig.json index 1dc925081af1..3210a9ea4250 100644 --- a/integration/standalone-bootstrap/tsconfig.json +++ b/integration/standalone-bootstrap/tsconfig.json @@ -7,10 +7,10 @@ "esModuleInterop": true, "declaration": false, "experimentalDecorators": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "node", "importHelpers": true, - "target": "es2015", + "target": "es2022", "typeRoots": [ "node_modules/@types" ], diff --git a/integration/trusted-types/tsconfig.json b/integration/trusted-types/tsconfig.json index 6df1071edd7e..f431b70173fb 100644 --- a/integration/trusted-types/tsconfig.json +++ b/integration/trusted-types/tsconfig.json @@ -14,8 +14,8 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2015", - "module": "es2020", + "target": "es2022", + "module": "es2022", "lib": [ "es2018", "dom" From 69702b17cc27acd9f989346d4796f2b72e17d826 Mon Sep 17 00:00:00 2001 From: arturovt Date: Thu, 19 Sep 2024 19:34:07 +0300 Subject: [PATCH 11/40] docs(zone.js): update docs to enable beforeunload (#57881) In this commit, we update the documentation to reflect the property that allows enabling the default browser `beforeunload` handling, which was added in a previous commit. Additionally, some cosmetic grammar changes have been made in this documentation, as the previous text had some issues. Closes #52256 PR Close #57881 --- packages/zone.js/MODULE.md | 238 +++++++++++++++++++------------------ 1 file changed, 120 insertions(+), 118 deletions(-) diff --git a/packages/zone.js/MODULE.md b/packages/zone.js/MODULE.md index a07cb8827489..1d0954514b49 100644 --- a/packages/zone.js/MODULE.md +++ b/packages/zone.js/MODULE.md @@ -4,145 +4,147 @@ Starting from zone.js v0.8.9, you can choose which web API modules you want to p the below samples show how to disable some modules. You just need to define a few global variables before loading zone.js. -``` - - +```html + + ``` Below is the full list of currently supported modules. -- Common - -|Module Name|Behavior with zone.js patch|How to disable| -|--|--|--| -|Error|stack frames will have the Zone's name information, (By default, Error patch will not be loaded by zone.js)|__Zone_disable_Error = true| -|toString|Function.toString will be patched to return native version of toString|__Zone_disable_toString = true| -|ZoneAwarePromise|Promise.then will be patched as Zone aware MicroTask|__Zone_disable_ZoneAwarePromise = true| -|bluebird|Bluebird will use Zone.scheduleMicroTask as async scheduler. (By default, bluebird patch will not be loaded by zone.js)|__Zone_disable_bluebird = true| - -- Browser - -|Module Name|Behavior with zone.js patch|How to disable| -|--|--|--| -|on_property|target.onProp will become zone aware target.addEventListener(prop)|__Zone_disable_on_property = true| -|timers|setTimeout/setInterval/setImmediate will be patched as Zone MacroTask|__Zone_disable_timers = true| -|requestAnimationFrame|requestAnimationFrame will be patched as Zone MacroTask|__Zone_disable_requestAnimationFrame = true| -|blocking|alert/prompt/confirm will be patched as Zone.run|__Zone_disable_blocking = true| -|EventTarget|target.addEventListener will be patched as Zone aware EventTask|__Zone_disable_EventTarget = true| -|MutationObserver|MutationObserver will be patched as Zone aware operation|__Zone_disable_MutationObserver = true| -|IntersectionObserver|Intersection will be patched as Zone aware operation|__Zone_disable_IntersectionObserver = true| -|FileReader|FileReader will be patched as Zone aware operation|__Zone_disable_FileReader = true| -|canvas|HTMLCanvasElement.toBlob will be patched as Zone aware operation|__Zone_disable_canvas = true| -|IE BrowserTools check|in IE, browser tool will not use zone patched eventListener|__Zone_disable_IE_check = true| -|CrossContext check|in webdriver, enable check event listener is cross context|__Zone_enable_cross_context_check = true| -|XHR|XMLHttpRequest will be patched as Zone aware MacroTask|__Zone_disable_XHR = true| -|geolocation|navigator.geolocation's prototype will be patched as Zone.run|__Zone_disable_geolocation = true| -|PromiseRejectionEvent|PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error|__Zone_disable_PromiseRejectionEvent = true| -|mediaQuery|mediaQuery addListener API will be patched as Zone aware EventTask. (By default, mediaQuery patch will not be loaded by zone.js) |__Zone_disable_mediaQuery = true| -|notification|notification onProperties API will be patched as Zone aware EventTask. (By default, notification patch will not be loaded by zone.js) |__Zone_disable_notification = true| -|MessagePort|MessagePort onProperties APIs will be patched as Zone aware EventTask. (By default, MessagePort patch will not be loaded by zone.js) |__Zone_disable_MessagePort = true| - -- NodeJS - -|Module Name|Behavior with zone.js patch|How to disable| -|--|--|--| -|node_timers|NodeJS patch timer|__Zone_disable_node_timers = true| -|fs|NodeJS patch fs function as macroTask|__Zone_disable_fs = true| -|EventEmitter|NodeJS patch EventEmitter as Zone aware EventTask|__Zone_disable_EventEmitter = true| -|nextTick|NodeJS patch process.nextTick as microTask|__Zone_disable_nextTick = true| -|handleUnhandledPromiseRejection|NodeJS handle unhandledPromiseRejection from ZoneAwarePromise|__Zone_disable_handleUnhandledPromiseRejection = true| -|crypto|NodeJS patch crypto function as macroTask|__Zone_disable_crypto = true| - -- Test Framework - -|Module Name|Behavior with zone.js patch|How to disable| -|--|--|--| -|Jasmine|Jasmine APIs patch|__Zone_disable_jasmine = true| -|Mocha|Mocha APIs patch|__Zone_disable_mocha = true| - -- on_property - -You can also disable specific on_properties by setting `__Zone_ignore_on_properties` as follows: for example, -if you want to disable `window.onmessage` and `HTMLElement.prototype.onclick` from zone.js patching, -you can do like this. - +### Common + +| Module Name | Behavior with zone.js patch | How to disable | +| ---------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| Error | stack frames will have the Zone's name information, (By default, Error patch will not be loaded by zone.js) | \_\_Zone_disable_Error = true | +| toString | Function.toString will be patched to return native version of toString | \_\_Zone_disable_toString = true | +| ZoneAwarePromise | Promise.then will be patched as Zone aware MicroTask | \_\_Zone_disable_ZoneAwarePromise = true | +| bluebird | Bluebird will use Zone.scheduleMicroTask as async scheduler. (By default, bluebird patch will not be loaded by zone.js) | \_\_Zone_disable_bluebird = true | + +### Browser + +| Module Name | Behavior with zone.js patch | How to disable | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| on_property | target.onProp will become zone aware target.addEventListener(prop) | \_\_Zone_disable_on_property = true | +| timers | setTimeout/setInterval/setImmediate will be patched as Zone MacroTask | \_\_Zone_disable_timers = true | +| requestAnimationFrame | requestAnimationFrame will be patched as Zone MacroTask | \_\_Zone_disable_requestAnimationFrame = true | +| blocking | alert/prompt/confirm will be patched as Zone.run | \_\_Zone_disable_blocking = true | +| EventTarget | target.addEventListener will be patched as Zone aware EventTask | \_\_Zone_disable_EventTarget = true | +| MutationObserver | MutationObserver will be patched as Zone aware operation | \_\_Zone_disable_MutationObserver = true | +| IntersectionObserver | Intersection will be patched as Zone aware operation | \_\_Zone_disable_IntersectionObserver = true | +| FileReader | FileReader will be patched as Zone aware operation | \_\_Zone_disable_FileReader = true | +| canvas | HTMLCanvasElement.toBlob will be patched as Zone aware operation | \_\_Zone_disable_canvas = true | +| IE BrowserTools check | in IE, browser tool will not use zone patched eventListener | \_\_Zone_disable_IE_check = true | +| CrossContext check | in webdriver, enable check event listener is cross context | \_\_Zone_enable_cross_context_check = true | +| `beforeunload` | enable the default `beforeunload` handling behavior, where event handlers return strings to prompt the user | **zone_symbol**enable_beforeunload = true | +| XHR | XMLHttpRequest will be patched as Zone aware MacroTask | \_\_Zone_disable_XHR = true | +| geolocation | navigator.geolocation's prototype will be patched as Zone.run | \_\_Zone_disable_geolocation = true | +| PromiseRejectionEvent | PromiseRejectEvent will fire when ZoneAwarePromise has unhandled error | \_\_Zone_disable_PromiseRejectionEvent = true | +| mediaQuery | mediaQuery addListener API will be patched as Zone aware EventTask. (By default, mediaQuery patch will not be loaded by zone.js) | \_\_Zone_disable_mediaQuery = true | +| notification | notification onProperties API will be patched as Zone aware EventTask. (By default, notification patch will not be loaded by zone.js) | \_\_Zone_disable_notification = true | +| MessagePort | MessagePort onProperties APIs will be patched as Zone aware EventTask. (By default, MessagePort patch will not be loaded by zone.js) | \_\_Zone_disable_MessagePort = true | + +### Node.js + +| Module Name | Behavior with zone.js patch | How to disable | +| ------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------- | +| node_timers | NodeJS patch timer | \_\_Zone_disable_node_timers = true | +| fs | NodeJS patch fs function as macroTask | \_\_Zone_disable_fs = true | +| EventEmitter | NodeJS patch EventEmitter as Zone aware EventTask | \_\_Zone_disable_EventEmitter = true | +| nextTick | NodeJS patch process.nextTick as microTask | \_\_Zone_disable_nextTick = true | +| handleUnhandledPromiseRejection | NodeJS handle unhandledPromiseRejection from ZoneAwarePromise | \_\_Zone_disable_handleUnhandledPromiseRejection = true | +| crypto | NodeJS patch crypto function as macroTask | \_\_Zone_disable_crypto = true | + +### Test Framework + +| Module Name | Behavior with zone.js patch | How to disable | +| ----------- | --------------------------- | ------------------------------- | +| Jasmine | Jasmine APIs patch | \_\_Zone_disable_jasmine = true | +| Mocha | Mocha APIs patch | \_\_Zone_disable_mocha = true | + +### `on` properties + +You can also disable specific `on` properties by setting `__Zone_ignore_on_properties` as follows. For example, if you want to disable `window.onmessage` and `HTMLElement.prototype.onclick` from zone.js patching, you can do so like this: + +```html + + ``` - - + +Excluding `on` properties from being patched means that callbacks will always be invoked within the root context, regardless of where the `on` callback has been set. Even if `onclick` is set within a child zone, the callback will be called inside the root zone: + +```ts +Zone.current.fork({ name: 'child' }).run(() => { + document.body.onclick = () => { + console.log(Zone.current); // + }; +}); ``` -- Error +You can find more information on adding unpatched events via `addEventListener`, please refer to [UnpatchedEvents](./STANDARD-APIS.md#unpatchedevents). + +### Error -By default, `zone.js/plugins/zone-error` will not be loaded for performance concern. -This package will provide following functionality. +By default, `zone.js/plugins/zone-error` will not be loaded for performance reasons. +This package provides the following functionality: - 1. Error inherit: handle `extend Error` issue. - ``` - class MyError extends Error {} - const myError = new MyError(); - console.log('is MyError instanceof Error', (myError instanceof Error)); - ``` +1. **Error Inheritance:** Handle the `extend Error` issue: - without `zone-error` patch, the example above will output `false`, with the patch, the result will be `true`. + ```ts + class MyError extends Error {} + const myError = new MyError(); + console.log('is MyError instanceof Error', (myError instanceof Error)); + ``` - 2. ZoneJsInternalStackFrames: remove zone.js stack from `stackTrace`, and add `zone` information. Without this patch, a lot of `zone.js` invocation stack will be shown - in stack frames. + Without the `zone-error` patch, the example above will output `false`. With the patch, the result will be `true`. - ``` - at zone.run (polyfill.bundle.js: 3424) - at zoneDelegate.invokeTask (polyfill.bundle.js: 3424) - at zoneDelegate.runTask (polyfill.bundle.js: 3424) - at zone.drainMicroTaskQueue (polyfill.bundle.js: 3424) - at a.b.c (vendor.bundle.js: 12345 ) - at d.e.f (main.bundle.js: 23456) - ``` +2. **ZoneJsInternalStackFrames:** Remove the zone.js stack from `stackTrace` and add `zone` information. Without this patch, many `zone.js` invocation stacks will be displayed in the stack frames. - with this patch, those zone frames will be removed, - and the zone information `/` will be added + ``` + at zone.run (polyfill.bundle.js: 3424) + at zoneDelegate.invokeTask (polyfill.bundle.js: 3424) + at zoneDelegate.runTask (polyfill.bundle.js: 3424) + at zone.drainMicroTaskQueue (polyfill.bundle.js: 3424) + at a.b.c (vendor.bundle.js: 12345 ) + at d.e.f (main.bundle.js: 23456) + ``` - ``` - at a.b.c (vendor.bundle.js: 12345 ) - at d.e.f (main.bundle.js: 23456 ) - ``` + With this patch, those zone frames will be removed, and the zone information `/` will be added. - The second feature will slow down the `Error` performance, so `zone.js` provide a flag to let you be able to control the behavior. - The flag is `__Zone_Error_ZoneJsInternalStackFrames_policy`. And the available options is: + ``` + at a.b.c (vendor.bundle.js: 12345 ) + at d.e.f (main.bundle.js: 23456 ) + ``` - 1. default: this is the default one, if you load `zone.js/plugins/zone-error` without - setting the flag, `default` will be used, and `ZoneJsInternalStackFrames` will be available - when `new Error()`, you can get a `error.stack` which is `zone stack free`. But this - will slow down `new Error()` a little bit. +The second feature may slow down `Error` performance, so `zone.js` provides a flag that allows you to control this behavior. +The flag is `__Zone_Error_ZoneJsInternalStackFrames_policy`. The available options are: - 2. disable: this will disable `ZoneJsInternalStackFrames` feature, and if you load - `zone.js/plugins/zone-error`, you will only get a `wrapped Error` which can handle - `Error inherit` issue. +1. **default:** This is the default setting. If you load `zone.js/plugins/zone-error` without setting the flag, `default` will be used. In this case, `ZoneJsInternalStackFrames` will be available when using `new Error()`, allowing you to obtain an `error.stack` that is zone-stack-free. However, this may slightly slow down the performance of new `Error()`. - 3. lazy: this is a feature to let you be able to get `ZoneJsInternalStackFrames` feature, - but not impact performance. But as a trade off, you can't get the `zone free stack - frames` by access `error.stack`. You can only get it by access `error.zoneAwareStack`. +2. **disable:** This option will disable the `ZoneJsInternalStackFrames` feature. If you load `zone.js/plugins/zone-error`, you will only receive a wrapped `Error`, which can handle the `Error` inheritance issue. +3. **lazy:** This feature allows you to access `ZoneJsInternalStackFrames` without impacting performance. However, as a trade-off, you won't be able to obtain the zone-free stack frames via `error.stack`. You can only access them through `error.zoneAwareStack`. -- Angular(2+) +### Angular -Angular uses zone.js to manage async operations and decide when to perform change detection. Thus, in Angular, -the following APIs should be patched, otherwise Angular may not work as expected. +Angular uses zone.js to manage asynchronous operations and determine when to perform change detection. Therefore, in Angular, the following APIs should be patched; otherwise, Angular may not work as expected: 1. ZoneAwarePromise 2. timer From 3fb696df96efc4669468430aa0c718945e5e7643 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Fri, 13 Sep 2024 01:31:04 +0000 Subject: [PATCH 12/40] refactor(compiler): update dependency chokidar to v4 (#57945) This commit bump chokidar to the latest major and adjusts to the breaking changes. PR Close #57945 --- package.json | 2 +- packages/compiler-cli/package.json | 2 +- packages/compiler-cli/src/perform_watch.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e2978df611f3..d7c8a64275c9 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "bluebird": "^3.7.2", "canonical-path": "1.0.0", "chalk": "^4.1.0", - "chokidar": "^3.5.1", + "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "d3": "^7.0.0", "diff": "^5.0.0", diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index a93989e10bdf..f7dc9b3e1f52 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -46,7 +46,7 @@ "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", "reflect-metadata": "^0.2.0", - "chokidar": "^3.0.0", + "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "semver": "^7.0.0", "tslib": "^2.3.0", diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index 16087e97196f..ebb4595a61b9 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -88,7 +88,8 @@ export function createPerformWatchHost + /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json|node_modules)/.test(path), ignoreInitial: true, persistent: true, }); From 3b06a09df2c694309f90d173d988ddf52ceee18b Mon Sep 17 00:00:00 2001 From: Stanka Kopalova Date: Thu, 3 Oct 2024 10:51:56 +0200 Subject: [PATCH 13/40] docs: fix wrong title of section (#58060) PR Close #58060 --- .../content/tutorials/first-app/steps/11-details-page/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/tutorials/first-app/steps/11-details-page/README.md b/adev/src/content/tutorials/first-app/steps/11-details-page/README.md index 23453127f748..65f6e8fc9a7e 100644 --- a/adev/src/content/tutorials/first-app/steps/11-details-page/README.md +++ b/adev/src/content/tutorials/first-app/steps/11-details-page/README.md @@ -18,7 +18,7 @@ Route parameters enable you to include dynamic information as a part of your rou - + In lesson 10, you added a second route to `src/app/routes.ts` which includes a special segment that identifies the route parameter, `id`: From 91874a7c4128458b3257b600cc0e3c0da5252247 Mon Sep 17 00:00:00 2001 From: Stanka Kopalova Date: Fri, 4 Oct 2024 11:57:55 +0200 Subject: [PATCH 14/40] docs: add better title name (#58060) PR Close #58060 --- .../content/tutorials/first-app/steps/11-details-page/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/tutorials/first-app/steps/11-details-page/README.md b/adev/src/content/tutorials/first-app/steps/11-details-page/README.md index 65f6e8fc9a7e..be84c8be009c 100644 --- a/adev/src/content/tutorials/first-app/steps/11-details-page/README.md +++ b/adev/src/content/tutorials/first-app/steps/11-details-page/README.md @@ -18,7 +18,7 @@ Route parameters enable you to include dynamic information as a part of your rou - + In lesson 10, you added a second route to `src/app/routes.ts` which includes a special segment that identifies the route parameter, `id`: From 39a851fa558dc3fe59a1cdad915ea9692ee5ede0 Mon Sep 17 00:00:00 2001 From: JoostK Date: Thu, 3 Oct 2024 23:09:51 +0200 Subject: [PATCH 15/40] refactor: change security issue redirect to angular.dev (#58070) Ensure the security issue template points to the correct location. PR Close #58070 --- .github/ISSUE_TEMPLATE/config.yml | 2 +- adev/src/content/guide/security.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7f9f0ab9bd00..fa12e442c085 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Security issue disclosure - url: https://angular.io/guide/security#report-issues + url: https://angular.dev/best-practices/security#report-issues about: Report a security issue in Angular Framework, CDK, Material, or CLI - name: Angular CLI url: https://github.com/angular/angular-cli/issues/new/choose diff --git a/adev/src/content/guide/security.md b/adev/src/content/guide/security.md index c3b5aaaeae63..e49f904e147c 100644 --- a/adev/src/content/guide/security.md +++ b/adev/src/content/guide/security.md @@ -5,6 +5,8 @@ It doesn't cover application-level security, such as authentication and authoriz For more information about the attacks and mitigations described below, see the [Open Web Application Security Project (OWASP) Guide](https://www.owasp.org/index.php/Category:OWASP_Guide_Project). + + Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). [For vulnerabilities in Angular, please submit your report at https://bughunters.google.com](https://bughunters.google.com/report). From ccf2c7f40e3dd33437149028c89dcb1098af4134 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 5 Oct 2024 00:57:07 +0900 Subject: [PATCH 16/40] docs(common): Minor typo in code example (#58085) PR Close #58085 --- .../src/directives/ng_optimized_image/ng_optimized_image.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bbfcd809d14d..dcc82ed5d6c7 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 @@ -258,7 +258,7 @@ export interface ImagePlaceholderConfig { * { * provide: IMAGE_LOADER, * useValue: (config: ImageLoaderConfig) => { - * return `https://example.com/${config.src}-${config.width}.jpg}`; + * return `https://example.com/${config.src}-${config.width}.jpg`; * } * }, * ], From 0e225ce16b7abbd12220d39d5fd9b520a104c92f Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Thu, 3 Oct 2024 23:35:21 +0200 Subject: [PATCH 17/40] docs(docs-infra): Add NgModule exports for directives. (#58071) This information is extracted from the @NgModule Jsdoc tag. fixes #57906 PR Close #58071 --- .../rendering/templates/tab-description.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx index 0e1ee1faf7ef..4637a6b62fa0 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx @@ -10,18 +10,41 @@ import {Fragment, h} from 'preact'; import {DocEntryRenderable} from '../entities/renderables'; import {normalizeTabUrl} from '../transforms/url-transforms'; import {RawHtml} from './raw-html'; +import {CodeSymbol} from './code-symbols'; const DESCRIPTION_TAB_NAME = 'Description'; /** Component to render the description tab. */ export function TabDescription(props: {entry: DocEntryRenderable}) { - if (!props.entry.htmlDescription || props.entry.htmlDescription === props.entry.shortHtmlDescription) { - return (<>); + const exportedBy = props.entry.jsdocTags.filter((t) => t.name === 'ngModule'); + if ( + (!props.entry.htmlDescription || + props.entry.htmlDescription === props.entry.shortHtmlDescription) && + !exportedBy.length + ) { + return <>; } return (
+ + {exportedBy.length ? ( + <> +
+

Exported by

+ +
    + {exportedBy.map((tag) => ( +
  • + +
  • + ))} +
+ + ) : ( + <> + )}
); } From 10081a9d7aaf094e13e30a2082259018c78d35a9 Mon Sep 17 00:00:00 2001 From: vladboisa Date: Fri, 4 Oct 2024 21:42:39 +0200 Subject: [PATCH 18/40] docs: move JSDoc before functions (#58087) Move the JSDoc before functions for correct view of params PR Close #58087 --- packages/common/src/pipes/number_pipe.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/common/src/pipes/number_pipe.ts b/packages/common/src/pipes/number_pipe.ts index cc8a128d3c3d..1edc0ba62a38 100644 --- a/packages/common/src/pipes/number_pipe.ts +++ b/packages/common/src/pipes/number_pipe.ts @@ -83,13 +83,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; export class DecimalPipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} - transform(value: number | string, digitsInfo?: string, locale?: string): string | null; - transform(value: null | undefined, digitsInfo?: string, locale?: string): null; - transform( - value: number | string | null | undefined, - digitsInfo?: string, - locale?: string, - ): string | null; /** * @param value The value to be formatted. * @param digitsInfo Sets digit and decimal representation. @@ -97,6 +90,13 @@ export class DecimalPipe implements PipeTransform { * @param locale Specifies what locale format rules to use. * [See more](#locale). */ + transform(value: number | string, digitsInfo?: string, locale?: string): string | null; + transform(value: null | undefined, digitsInfo?: string, locale?: string): null; + transform( + value: number | string | null | undefined, + digitsInfo?: string, + locale?: string, + ): string | null; transform( value: number | string | null | undefined, digitsInfo?: string, From b35e7f8bb388866705078c1574bfc38fd34b5326 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sat, 5 Oct 2024 17:21:07 +0200 Subject: [PATCH 19/40] docs: add info on `AbstractControl.source` type. (#58094) The source can be of any type and can't be inferred from `T` fixes #58076 PR Close #58094 --- packages/forms/src/model/abstract_model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts index 75cf9f6d1f79..8b03afc6d94f 100644 --- a/packages/forms/src/model/abstract_model.ts +++ b/packages/forms/src/model/abstract_model.ts @@ -91,6 +91,8 @@ export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'; export abstract class ControlEvent { /** * Form control from which this event is originated. + * + * Note: the type of the control can't be infered from T as the event can be emitted by any of child controls */ public abstract readonly source: AbstractControl; } From 7eb264a0176fc2e4f53dd7bf9d8b6c7504ec8e88 Mon Sep 17 00:00:00 2001 From: Luan Gong Date: Mon, 7 Oct 2024 12:04:26 +0800 Subject: [PATCH 20/40] docs: use correct heading in templates guide (#58101) PR Close #58101 --- adev/src/content/guide/templates/whitespace.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adev/src/content/guide/templates/whitespace.md b/adev/src/content/guide/templates/whitespace.md index 8830b5f6b4b6..a0ff607b2a33 100644 --- a/adev/src/content/guide/templates/whitespace.md +++ b/adev/src/content/guide/templates/whitespace.md @@ -44,7 +44,9 @@ In this example, the browser displays only a single space between "Hello" and "w See [How whitespace is handled by HTML, CSS, and in the DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace) for more context on how this works. Angular avoids sending these unnecessary whitespace characters to the browser in the first place by collapsing them to a single character when it compiles the template. -Preserving whitespace + +## Preserving whitespace + You can tell Angular to preserve whitespace in a template by specifying `preserveWhitespaces: true` in the `@Component` decorator for a template. ```angular-ts From f187c3abf8b9547b2692995f344cd7dcb9f32ebc Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 7 Oct 2024 10:10:01 +0200 Subject: [PATCH 21/40] fix(compiler-cli): defer symbols only used in types (#58104) Currently we don't defer any symbols that have references outside of the `import` statement and the `imports` array. This is a bit too aggressive, because it's possible that the symbol is only used for types (e.g. `viewChild('ref')`) which will be stripped when emitting to JS. These changes expand the logic so that references inside type nodes aren't considered. **Note:** one special case is when the symbol used in constructor-based DI (e.g. `constructor(someCmp: SomeCmp)`, because these constructors will be compiled to `directiveInject` calls. We don't need to worry about them, because the compiler introduces an addition `import * as i1 from './some-cmp';` import that it uses to refer to the symbol. Fixes #55991. PR Close #58104 --- .../imports/src/deferred_symbol_tracker.ts | 7 +- .../compiler-cli/test/ngtsc/defer_spec.ts | 171 ++++++++++++++---- 2 files changed, 141 insertions(+), 37 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts index 0375c5162e7d..a45f60e67446 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts @@ -171,7 +171,7 @@ export class DeferredSymbolTracker { } const symbolsMap = this.imports.get(importDecl)!; - for (const [symbol, refs] of symbolsMap) { + for (const refs of symbolsMap.values()) { if (refs === AssumeEager || refs.size > 0) { // There may be still eager references to this symbol. return false; @@ -201,8 +201,9 @@ export class DeferredSymbolTracker { ): Set { const results = new Set(); const visit = (node: ts.Node): void => { - if (node === importDecl) { - // Don't record references from the declaration itself. + // Don't record references from the declaration itself or inside + // type nodes which will be stripped from the JS output. + if (node === importDecl || ts.isTypeNode(node)) { return; } diff --git a/packages/compiler-cli/test/ngtsc/defer_spec.ts b/packages/compiler-cli/test/ngtsc/defer_spec.ts index 0742c944c941..84a2d21cf692 100644 --- a/packages/compiler-cli/test/ngtsc/defer_spec.ts +++ b/packages/compiler-cli/test/ngtsc/defer_spec.ts @@ -79,12 +79,10 @@ runInEachFileSystem(() => { expect(jsContents).not.toContain('import { CmpA }'); }); - it( - 'should include timer scheduler function when ' + '`after` or `minimum` parameters are used', - () => { - env.write( - 'cmp-a.ts', - ` + it('should include timer scheduler function when `after` or `minimum` parameters are used', () => { + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -94,38 +92,37 @@ runInEachFileSystem(() => { }) export class CmpA {} `, - ); + ); - env.write( - '/test.ts', - ` - import { Component } from '@angular/core'; - import { CmpA } from './cmp-a'; + env.write( + '/test.ts', + ` + import { Component } from '@angular/core'; + import { CmpA } from './cmp-a'; - @Component({ - selector: 'test-cmp', - standalone: true, - imports: [CmpA], - template: \` - @defer { - - } @loading (after 500ms; minimum 300ms) { - Loading... - } - \`, - }) - export class TestCmp {} - `, - ); + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA], + template: \` + @defer { + + } @loading (after 500ms; minimum 300ms) { + Loading... + } + \`, + }) + export class TestCmp {} + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain( - 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)', - ); - }, - ); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)', + ); + }); describe('imports', () => { it('should retain regular imports when symbol is eagerly referenced', () => { @@ -652,6 +649,112 @@ runInEachFileSystem(() => { // via dynamic imports and an original import can be removed. expect(jsContents).not.toContain('import CmpA'); }); + + it('should defer symbol that is used only in types', () => { + env.write( + 'cmp.ts', + ` + import { Component } from '@angular/core'; + + @Component({ + standalone: true, + selector: 'cmp', + template: 'Cmp!' + }) + export class Cmp {} + `, + ); + + env.write( + '/test.ts', + ` + import { Component, viewChild } from '@angular/core'; + import { Cmp } from './cmp'; + + const topLevelConst: Cmp = null!; + + @Component({ + standalone: true, + imports: [Cmp], + template: \` + @defer { + + } + \`, + }) + export class TestCmp { + query = viewChild('ref'); + asType: Cmp; + inlineType: {foo: Cmp}; + unionType: string | Cmp | number; + constructor(param: Cmp) {} + inMethod(param: Cmp): Cmp { + let localVar: Cmp | null = null; + return localVar!; + } + } + + function inFunction(param: Cmp): Cmp { + return null!; + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp").then(m => m.Cmp)]'); + expect(jsContents).not.toContain('import { Cmp }'); + }); + + it('should retain symbols used in types and eagerly', () => { + env.write( + 'cmp.ts', + ` + import { Component } from '@angular/core'; + + @Component({ + standalone: true, + selector: 'cmp', + template: 'Cmp!' + }) + export class Cmp {} + `, + ); + + env.write( + '/test.ts', + ` + import { Component, viewChild } from '@angular/core'; + import { Cmp } from './cmp'; + + @Component({ + standalone: true, + imports: [Cmp], + template: \` + @defer { + + } + \`, + }) + export class TestCmp { + // Type-only reference + query = viewChild('ref'); + + // Directy reference + otherQuery = viewChild(Cmp); + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [Cmp]'); + expect(jsContents).toContain('import { Cmp }'); + }); }); it('should detect pipe used in the `when` trigger as an eager dependency', () => { From 02de7fc73167cada93f531f149129fb320620220 Mon Sep 17 00:00:00 2001 From: arturovt Date: Thu, 19 Sep 2024 20:22:46 +0300 Subject: [PATCH 22/40] fix(zone.js): remove `abort` listener once fetch is settled (#57882) This commit updates the `fetch` patch for zone.js. Currently, we're attaching an `abort` event listener on the signal (when it's provided) and never removing it. We should be good citizens and remove event listeners whenever objects need to be properly collected. In Firefox, when saving a heap snapshot and running it through `fxsnapshot`, querying `AbortSignal` will print a so-called "CaptureMap" with a list of "lambdas," indicating that the signal is not garbage collected because of the event listener lambda function. PR Close #57882 --- packages/zone.js/lib/common/fetch.ts | 27 ++++++++++++++-------- packages/zone.js/test/common/fetch.spec.ts | 9 ++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/zone.js/lib/common/fetch.ts b/packages/zone.js/lib/common/fetch.ts index c22edea28fb7..3b8d1a2a5b80 100644 --- a/packages/zone.js/lib/common/fetch.ts +++ b/packages/zone.js/lib/common/fetch.ts @@ -99,22 +99,31 @@ export function patchFetch(Zone: ZoneType): void { options.signal = fetchSignal; args[1] = options; + let onAbort: () => void; if (signal) { const nativeAddEventListener = signal[Zone.__symbol__('addEventListener') as 'addEventListener'] || signal.addEventListener; - nativeAddEventListener.call( - signal, - 'abort', - function () { - ac!.abort(); - }, - {once: true}, - ); + onAbort = () => ac!.abort(); + nativeAddEventListener.call(signal, 'abort', onAbort, {once: true}); } - return createFetchTask('fetch', {fetchArgs: args} as FetchTaskData, fetch, this, args, ac); + return createFetchTask( + 'fetch', + {fetchArgs: args} as FetchTaskData, + fetch, + this, + args, + ac, + ).finally(() => { + // We need to be good citizens and remove the `abort` listener once + // the fetch is settled. The `abort` listener may not be called at all, + // which means the event listener closure would retain a reference to + // the `ac` object even if it goes out of scope. Since browser's garbage + // collectors work differently, some may not be smart enough to collect a signal. + signal?.removeEventListener('abort', onAbort); + }); }; if (OriginalResponse?.prototype) { diff --git a/packages/zone.js/test/common/fetch.spec.ts b/packages/zone.js/test/common/fetch.spec.ts index 7e6c642972c9..ca8b60761ab8 100644 --- a/packages/zone.js/test/common/fetch.spec.ts +++ b/packages/zone.js/test/common/fetch.spec.ts @@ -169,6 +169,10 @@ describe( 'invokeTask:fetch:macroTask', 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask', + + // This is the `finally` task, which is used for cleanup. + 'scheduleTask:Promise.then:microTask', + 'invokeTask:Promise.then:microTask', ]); done(); }, @@ -194,6 +198,11 @@ describe( 'invokeTask:fetch:macroTask', 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask', + + // This is the `finally` task, which is used for cleanup. + 'scheduleTask:Promise.then:microTask', + 'invokeTask:Promise.then:microTask', + // Please refer to the issue link above. Previously, `Response` methods were not // patched by zone.js, and their return values were considered only as // microtasks (not macrotasks). The Angular zone stabilized prematurely, From bf83801b8c99aa96d17f266c29d236c125038b9c Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Mon, 7 Oct 2024 18:04:48 +0000 Subject: [PATCH 23/40] build: update actions/checkout digest to eef6144 (#58108) See associated pull request for more information. PR Close #58108 --- .github/workflows/adev-preview-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/adev-preview-deploy.yml b/.github/workflows/adev-preview-deploy.yml index 93d651bec279..5399fd16d883 100644 --- a/.github/workflows/adev-preview-deploy.yml +++ b/.github/workflows/adev-preview-deploy.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: token: '${{secrets.GITHUB_TOKEN}}' From 99b5417d28ab5621efc8c61ae0cbbfd15f5fa27d Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Mon, 7 Oct 2024 17:03:37 +0000 Subject: [PATCH 24/40] build: update actions/cache digest to 2cdf405 (#58107) See associated pull request for more information. PR Close #58107 --- .github/actions/yarn-install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index 26b3efe2283a..afe572f29af7 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn' runs: using: 'composite' steps: - - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 + - uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4 with: path: | ./node_modules/ From 7b13d34a24c05bd2f7f9fbe675197638cca94683 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Tue, 8 Oct 2024 05:06:28 +0000 Subject: [PATCH 25/40] build: update scorecard action dependencies (#58116) See associated pull request for more information. PR Close #58116 --- .github/workflows/scorecard.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c7314a73ef12..518aabb13114 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -25,7 +25,7 @@ jobs: steps: - name: 'Checkout code' - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false @@ -39,7 +39,7 @@ jobs: # Upload the results as artifacts. - name: 'Upload artifact' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 with: name: SARIF file path: results.sarif @@ -47,6 +47,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: results.sarif From 490786af1079f8e9867ff5cc45edfebfee8fb155 Mon Sep 17 00:00:00 2001 From: Lang Date: Mon, 16 Sep 2024 23:25:49 +0200 Subject: [PATCH 26/40] docs: complete the example in use InjectionToken section (#57839) PR Close #57839 --- .../content/guide/di/dependency-injection-providers.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/adev/src/content/guide/di/dependency-injection-providers.md b/adev/src/content/guide/di/dependency-injection-providers.md index e8065ff70b40..afd9575b6749 100644 --- a/adev/src/content/guide/di/dependency-injection-providers.md +++ b/adev/src/content/guide/di/dependency-injection-providers.md @@ -161,6 +161,10 @@ The following example defines a token, `APP_CONFIG`. of the type `InjectionToken import { InjectionToken } from '@angular/core'; +export interface AppConfig { + title: string; +} + export const APP_CONFIG = new InjectionToken('app.config description'); @@ -169,6 +173,10 @@ The optional type parameter, ``, and the token description, `app.conf Next, register the dependency provider in the component using the `InjectionToken` object of `APP_CONFIG`: +const MY_APP_CONFIG_VARIABLE: AppConfig = { + title: 'Hello', +}; + providers: [{ provide: APP_CONFIG, useValue: MY_APP_CONFIG_VARIABLE }] From 9f2726bb4364c1d8cb34cc97b6cc0ab2649c0c6a Mon Sep 17 00:00:00 2001 From: ColinJolivet Date: Tue, 1 Oct 2024 11:30:53 +0200 Subject: [PATCH 27/40] refactor(docs-infra): add tooltip to the download button in playground (#58065) Add a material tooltip to the download button in the playground in order to clarify what this button does PR Close #58065 --- adev/shared-docs/styles/global-styles.scss | 3 +- .../code-editor/code-editor.component.html | 4 ++- .../code-editor/code-editor.component.spec.ts | 28 ++++++++++++++++++- .../code-editor/code-editor.component.ts | 11 +++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/adev/shared-docs/styles/global-styles.scss b/adev/shared-docs/styles/global-styles.scss index 8093ddb1450b..5c073d2f32ec 100644 --- a/adev/shared-docs/styles/global-styles.scss +++ b/adev/shared-docs/styles/global-styles.scss @@ -64,6 +64,7 @@ $theme: mat.m2-define-light-theme( @include mat.core(); @include mat.tabs-theme($theme); @include mat.button-toggle-theme($theme); +@include mat.tooltip-theme($theme); // Include custom docs styles @include alert.docs-alert(); @@ -116,7 +117,7 @@ $theme: mat.m2-define-light-theme( &.cli { padding-inline-start: 1rem; } - + a { color: inherit; &:hover { diff --git a/adev/src/app/editor/code-editor/code-editor.component.html b/adev/src/app/editor/code-editor/code-editor.component.html index 06b87384d0b0..93d50cde385f 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.html +++ b/adev/src/app/editor/code-editor/code-editor.component.html @@ -89,7 +89,9 @@ class="adev-editor-download-button" type="button" (click)="downloadCurrentCodeEditorState()" - aria-label="Download current code in editor" + aria-label="Download current source code" + matTooltip="Download current source code" + matTooltipPosition="above" > download diff --git a/adev/src/app/editor/code-editor/code-editor.component.spec.ts b/adev/src/app/editor/code-editor/code-editor.component.spec.ts index 0e9099cdef42..65c0d9ba7e6c 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.spec.ts +++ b/adev/src/app/editor/code-editor/code-editor.component.spec.ts @@ -21,6 +21,8 @@ import {CodeEditor, REQUIRED_FILES} from './code-editor.component'; import {CodeMirrorEditor} from './code-mirror-editor.service'; import {FakeChangeDetectorRef} from '@angular/docs'; import {TutorialType} from '@angular/docs'; +import {MatTooltip} from '@angular/material/tooltip'; +import {MatTooltipHarness} from '@angular/material/tooltip/testing'; const files = [ {filename: 'a', content: '', language: {} as any}, @@ -51,7 +53,7 @@ describe('CodeEditor', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CodeEditor, NoopAnimationsModule], + imports: [CodeEditor, NoopAnimationsModule, MatTooltip], providers: [ { provide: CodeMirrorEditor, @@ -200,4 +202,28 @@ describe('CodeEditor', () => { expect(fixture.debugElement.query(By.css('[aria-label="Delete file"]'))).toBeNull(); } }); + + it('should be able to display the tooltip on the download button', async () => { + const tooltip = await loader.getHarness( + MatTooltipHarness.with({selector: '.adev-editor-download-button'}), + ); + expect(await tooltip.isOpen()).toBeFalse(); + await tooltip.show(); + expect(await tooltip.isOpen()).toBeTrue(); + }); + + it('should be able to get the tooltip message on the download button', async () => { + const tooltip = await loader.getHarness( + MatTooltipHarness.with({selector: '.adev-editor-download-button'}), + ); + await tooltip.show(); + expect(await tooltip.getTooltipText()).toBe('Download current source code'); + }); + + it('should not be able to get the tooltip message on the download button when the tooltip is not shown', async () => { + const tooltip = await loader.getHarness( + MatTooltipHarness.with({selector: '.adev-editor-download-button'}), + ); + expect(await tooltip.getTooltipText()).toBe(''); + }); }); diff --git a/adev/src/app/editor/code-editor/code-editor.component.ts b/adev/src/app/editor/code-editor/code-editor.component.ts index 19a6f0510c38..99fa1ddcc79e 100644 --- a/adev/src/app/editor/code-editor/code-editor.component.ts +++ b/adev/src/app/editor/code-editor/code-editor.component.ts @@ -33,6 +33,7 @@ import {StackBlitzOpener} from '../stackblitz-opener.service'; import {ClickOutside, IconComponent} from '@angular/docs'; import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu'; import {IDXLauncher} from '../idx-launcher.service'; +import {MatTooltip} from '@angular/material/tooltip'; export const REQUIRED_FILES = new Set([ 'src/main.ts', @@ -48,7 +49,15 @@ const ANGULAR_DEV = 'https://angular.dev'; templateUrl: './code-editor.component.html', styleUrls: ['./code-editor.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MatTabsModule, IconComponent, ClickOutside, CdkMenu, CdkMenuItem, CdkMenuTrigger], + imports: [ + MatTabsModule, + MatTooltip, + IconComponent, + ClickOutside, + CdkMenu, + CdkMenuItem, + CdkMenuTrigger, + ], }) export class CodeEditor implements AfterViewInit, OnDestroy { @ViewChild('codeEditorWrapper') private codeEditorWrapperRef!: ElementRef; From 9ca9db4f746a9a99b7e7faaa3dd26417a6cd7c79 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sat, 5 Oct 2024 18:45:59 +0200 Subject: [PATCH 28/40] refactor(core): drop `ViewRefTracker` in favor of `ApplicationRef`. (#58096) We can leverage an `import type` to prevent the circular import. PR Close #58096 --- packages/core/src/linker/view_ref.ts | 10 ---------- packages/core/src/render3/view_ref.ts | 7 ++++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/core/src/linker/view_ref.ts b/packages/core/src/linker/view_ref.ts index 55e4b7cdbb76..ef684ba61cdc 100644 --- a/packages/core/src/linker/view_ref.ts +++ b/packages/core/src/linker/view_ref.ts @@ -100,13 +100,3 @@ export abstract class EmbeddedViewRef extends ViewRef { */ abstract get rootNodes(): any[]; } - -/** - * Interface for tracking root `ViewRef`s in `ApplicationRef`. - * - * NOTE: Importing `ApplicationRef` here directly creates circular dependency, which is why we have - * a subset of the `ApplicationRef` interface `ViewRefTracker` here. - */ -export interface ViewRefTracker { - detachView(viewRef: ViewRef): void; -} diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 98db89e1db17..fdabd856d959 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -8,8 +8,9 @@ import {ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling'; +import type {ApplicationRef} from '../core'; import {RuntimeError, RuntimeErrorCode} from '../errors'; -import {EmbeddedViewRef, ViewRefTracker} from '../linker/view_ref'; +import {EmbeddedViewRef} from '../linker/view_ref'; import {removeFromArray} from '../util/array_utils'; import {assertEqual} from '../util/assert'; @@ -43,7 +44,7 @@ import {storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/ interface ChangeDetectorRefInterface extends ChangeDetectorRef {} export class ViewRef implements EmbeddedViewRef, ChangeDetectorRefInterface { - private _appRef: ViewRefTracker | null = null; + private _appRef: ApplicationRef | null = null; private _attachedToViewContainer = false; get rootNodes(): any[] { @@ -349,7 +350,7 @@ export class ViewRef implements EmbeddedViewRef, ChangeDetectorRefInterfac detachViewFromDOM(this._lView[TVIEW], this._lView); } - attachToAppRef(appRef: ViewRefTracker) { + attachToAppRef(appRef: ApplicationRef) { if (this._attachedToViewContainer) { throw new RuntimeError( RuntimeErrorCode.VIEW_ALREADY_ATTACHED, From 46bafb0b0a952d8e9c2a0099f0607354697bbeaa Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 8 Oct 2024 10:17:23 +0200 Subject: [PATCH 29/40] fix(core): clean up afterRender after it is executed (#58119) We stop tracking `afterRender` hooks as soon as they execute, but their on destroy callbacks stay registered until either the injector is destroyed or the user calls `destroy` manually. This was leading to memory leaks in the `@defer` triggers based on top of `afterRender` when placed inside long-lived views, because the callback would execute, but its destroy logic was waiting for the view to be destroyed. These changes resolve the issue by destroying the `AfterRenderRef` once it is executed. PR Close #58119 --- .../core/src/render3/after_render/manager.ts | 3 ++ .../test/acceptance/after_render_hook_spec.ts | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/core/src/render3/after_render/manager.ts b/packages/core/src/render3/after_render/manager.ts index 6c876bbcdd70..3077ea920055 100644 --- a/packages/core/src/render3/after_render/manager.ts +++ b/packages/core/src/render3/after_render/manager.ts @@ -82,6 +82,9 @@ export class AfterRenderImpl { sequence.afterRun(); if (sequence.once) { this.sequences.delete(sequence); + // Destroy the sequence so its on destroy callbacks can be cleaned up + // immediately, instead of waiting until the injector is destroyed. + sequence.destroy(); } } diff --git a/packages/core/test/acceptance/after_render_hook_spec.ts b/packages/core/test/acceptance/after_render_hook_spec.ts index 1a6cca09ae23..00ec5b8299b3 100644 --- a/packages/core/test/acceptance/after_render_hook_spec.ts +++ b/packages/core/test/acceptance/after_render_hook_spec.ts @@ -1394,6 +1394,40 @@ describe('after render hooks', () => { appRef.tick(); }).toThrowError(/NG0103.*(Infinite change detection while refreshing application views)/); }); + + it('should destroy after the hook has run', () => { + let hookRef: AfterRenderRef | null = null; + let afterRenderCount = 0; + + @Component({selector: 'comp'}) + class Comp { + constructor() { + hookRef = afterNextRender(() => { + afterRenderCount++; + }); + } + } + + TestBed.configureTestingModule({ + declarations: [Comp], + ...COMMON_CONFIGURATION, + }); + createAndAttachComponent(Comp); + const appRef = TestBed.inject(ApplicationRef); + const destroySpy = spyOn(hookRef!, 'destroy').and.callThrough(); + expect(afterRenderCount).toBe(0); + expect(destroySpy).not.toHaveBeenCalled(); + + // Run once and ensure that it was called and then cleaned up. + appRef.tick(); + expect(afterRenderCount).toBe(1); + expect(destroySpy).toHaveBeenCalledTimes(1); + + // Make sure we're not retaining it. + appRef.tick(); + expect(afterRenderCount).toBe(1); + expect(destroySpy).toHaveBeenCalledTimes(1); + }); }); describe('server', () => { From 59394ee830c8d930d4a205195586288360880b1b Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Tue, 8 Oct 2024 18:05:37 +0000 Subject: [PATCH 30/40] build: update actions/cache digest to 3624ceb (#58125) See associated pull request for more information. PR Close #58125 --- .github/actions/yarn-install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index afe572f29af7..7455b8479a9c 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn' runs: using: 'composite' steps: - - uses: actions/cache@2cdf405574d6ef1f33a1d12acccd3ae82f47b3f2 # v4 + - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4 with: path: | ./node_modules/ From b40875a2cc28a94015e6392044a03b30c2559999 Mon Sep 17 00:00:00 2001 From: Krzysztof Platis Date: Wed, 9 Oct 2024 14:22:20 +0200 Subject: [PATCH 31/40] fix(platform-server): destroy `PlatformRef` when error happens during the `bootstrap()` phase (#58112) (#58135) The `bootstrap()` phase might fail e.g. due to an rejected promise in some `APP_INIIALIZER`. If `PlatformRef` is not destroyed, then the main app's injector is not destroyed and therefore `ngOnDestroy` hooks of singleton services is not called on the end (failure) of SSR. This could lead to possible memory leaks in custom SSR apps, if their singleton services' `ngOnDestroy` hooks contained an important teardown logic (e.g. unsubscribing from RxJS observable). Note: I needed to fix by the way another thing too: now we destroy `moduleRef` when `platformInjector` is destroyed - by setting a `PLATFORM_DESTROY_LISTENER` Patch port of #58112 PR Close #58135 --- packages/core/src/platform/bootstrap.ts | 20 ++- packages/core/src/platform/platform_ref.ts | 6 +- packages/platform-server/src/utils.ts | 33 ++-- .../platform-server/test/integration_spec.ts | 150 ++++++++++++++++++ 4 files changed, 191 insertions(+), 18 deletions(-) diff --git a/packages/core/src/platform/bootstrap.ts b/packages/core/src/platform/bootstrap.ts index 88ef785f2306..c9d802754770 100644 --- a/packages/core/src/platform/bootstrap.ts +++ b/packages/core/src/platform/bootstrap.ts @@ -26,21 +26,24 @@ import {Injector} from '../di'; import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory'; import {stringify} from '../util/stringify'; -export interface ModuleBootstrapConfig { +export interface BootstrapConfig { + platformInjector: Injector; +} + +export interface ModuleBootstrapConfig extends BootstrapConfig { moduleRef: InternalNgModuleRef; allPlatformModules: NgModuleRef[]; } -export interface ApplicationBootstrapConfig { +export interface ApplicationBootstrapConfig extends BootstrapConfig { r3Injector: R3Injector; - platformInjector: Injector; rootComponent: Type | undefined; } function isApplicationBootstrapConfig( config: ApplicationBootstrapConfig | ModuleBootstrapConfig, ): config is ApplicationBootstrapConfig { - return !!(config as ApplicationBootstrapConfig).platformInjector; + return !(config as ModuleBootstrapConfig).moduleRef; } export function bootstrap( @@ -91,9 +94,9 @@ export function bootstrap( }); }); + // If the whole platform is destroyed, invoke the `destroy` method + // for all bootstrapped applications as well. if (isApplicationBootstrapConfig(config)) { - // If the whole platform is destroyed, invoke the `destroy` method - // for all bootstrapped applications as well. const destroyListener = () => envInjector.destroy(); const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS); onPlatformDestroyListeners.add(destroyListener); @@ -103,9 +106,14 @@ export function bootstrap( onPlatformDestroyListeners.delete(destroyListener); }); } else { + const destroyListener = () => config.moduleRef.destroy(); + const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS); + onPlatformDestroyListeners.add(destroyListener); + config.moduleRef.onDestroy(() => { remove(config.allPlatformModules, config.moduleRef); onErrorSubscription.unsubscribe(); + onPlatformDestroyListeners.delete(destroyListener); }); } diff --git a/packages/core/src/platform/platform_ref.ts b/packages/core/src/platform/platform_ref.ts index 600d00cf84c8..69d4eeef2035 100644 --- a/packages/core/src/platform/platform_ref.ts +++ b/packages/core/src/platform/platform_ref.ts @@ -79,7 +79,11 @@ export class PlatformRef { allAppProviders, ); - return bootstrap({moduleRef, allPlatformModules: this._modules}); + return bootstrap({ + moduleRef, + allPlatformModules: this._modules, + platformInjector: this.injector, + }); } /** diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index 3a3d87edf4b6..4ee30ac9210f 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -194,18 +194,21 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef) } appendServerContextInfo(applicationRef); - const output = platformState.renderToString(); - // Destroy the application in a macrotask, this allows pending promises to be settled and errors - // to be surfaced to the users. - await new Promise((resolve) => { + return platformState.renderToString(); +} + +/** + * Destroy the application in a macrotask, this allows pending promises to be settled and errors + * to be surfaced to the users. + */ +function asyncDestroyPlatform(platformRef: PlatformRef): Promise { + return new Promise((resolve) => { setTimeout(() => { platformRef.destroy(); resolve(); }, 0); }); - - return output; } /** @@ -248,9 +251,13 @@ export async function renderModule( ): Promise { const {document, url, extraProviders: platformProviders} = options; const platformRef = createServerPlatform({document, url, platformProviders}); - const moduleRef = await platformRef.bootstrapModule(moduleType); - const applicationRef = moduleRef.injector.get(ApplicationRef); - return _render(platformRef, applicationRef); + try { + const moduleRef = await platformRef.bootstrapModule(moduleType); + const applicationRef = moduleRef.injector.get(ApplicationRef); + return await _render(platformRef, applicationRef); + } finally { + await asyncDestroyPlatform(platformRef); + } } /** @@ -280,7 +287,11 @@ export async function renderApplication( return runAndMeasurePerf('renderApplication', async () => { const platformRef = createServerPlatform(options); - const applicationRef = await bootstrap(); - return _render(platformRef, applicationRef); + try { + const applicationRef = await bootstrap(); + return await _render(platformRef, applicationRef); + } finally { + await asyncDestroyPlatform(platformRef); + } }); } diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index dcfead07f56d..b2facece402c 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -42,6 +42,9 @@ import { ViewEncapsulation, ɵPendingTasks as PendingTasks, ɵwhenStable as whenStable, + APP_INITIALIZER, + inject, + getPlatform, } from '@angular/core'; import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils'; import {TestBed} from '@angular/core/testing'; @@ -1076,6 +1079,153 @@ class HiddenModule {} ); }, ); + + it( + `should call onOnDestroy of a service after a successful render` + + `(standalone: ${isStandalone})`, + async () => { + let wasServiceNgOnDestroyCalled = false; + + @Injectable({providedIn: 'root'}) + class DestroyableService { + ngOnDestroy() { + wasServiceNgOnDestroyCalled = true; + } + } + + const SuccessfulAppInitializerProviders = [ + { + provide: APP_INITIALIZER, + useFactory: () => { + inject(DestroyableService); + return () => Promise.resolve(); // Success in APP_INITIALIZER + }, + multi: true, + }, + ]; + + @NgModule({ + providers: SuccessfulAppInitializerProviders, + imports: [MyServerAppModule, ServerModule], + bootstrap: [MyServerApp], + }) + class ServerSuccessfulAppInitializerModule {} + + const ServerSuccessfulAppInitializerAppStandalone = getStandaloneBootstrapFn( + createMyServerApp(true), + SuccessfulAppInitializerProviders, + ); + + const options = {document: doc}; + const bootstrap = isStandalone + ? renderApplication(ServerSuccessfulAppInitializerAppStandalone, options) + : renderModule(ServerSuccessfulAppInitializerModule, options); + await bootstrap; + + expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull(); + expect(wasServiceNgOnDestroyCalled) + .withContext('DestroyableService.ngOnDestroy() should be called') + .toBeTrue(); + }, + ); + + it( + `should call onOnDestroy of a service after some APP_INITIALIZER fails ` + + `(standalone: ${isStandalone})`, + async () => { + let wasServiceNgOnDestroyCalled = false; + + @Injectable({providedIn: 'root'}) + class DestroyableService { + ngOnDestroy() { + wasServiceNgOnDestroyCalled = true; + } + } + + const FailingAppInitializerProviders = [ + { + provide: APP_INITIALIZER, + useFactory: () => { + inject(DestroyableService); + return () => Promise.reject('Error in APP_INITIALIZER'); + }, + multi: true, + }, + ]; + + @NgModule({ + providers: FailingAppInitializerProviders, + imports: [MyServerAppModule, ServerModule], + bootstrap: [MyServerApp], + }) + class ServerFailingAppInitializerModule {} + + const ServerFailingAppInitializerAppStandalone = getStandaloneBootstrapFn( + createMyServerApp(true), + FailingAppInitializerProviders, + ); + + const options = {document: doc}; + const bootstrap = isStandalone + ? renderApplication(ServerFailingAppInitializerAppStandalone, options) + : renderModule(ServerFailingAppInitializerModule, options); + await expectAsync(bootstrap).toBeRejectedWith('Error in APP_INITIALIZER'); + + expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull(); + expect(wasServiceNgOnDestroyCalled) + .withContext('DestroyableService.ngOnDestroy() should be called') + .toBeTrue(); + }, + ); + + it( + `should call onOnDestroy of a service after an error happens in a root component's constructor ` + + `(standalone: ${isStandalone})`, + async () => { + let wasServiceNgOnDestroyCalled = false; + + @Injectable({providedIn: 'root'}) + class DestroyableService { + ngOnDestroy() { + wasServiceNgOnDestroyCalled = true; + } + } + + @Component({ + standalone: isStandalone, + selector: 'app', + template: `Works!`, + }) + class MyServerFailingConstructorApp { + constructor() { + inject(DestroyableService); + throw 'Error in constructor of the root component'; + } + } + + @NgModule({ + declarations: [MyServerFailingConstructorApp], + imports: [MyServerAppModule, ServerModule], + bootstrap: [MyServerFailingConstructorApp], + }) + class MyServerFailingConstructorAppModule {} + + const MyServerFailingConstructorAppStandalone = getStandaloneBootstrapFn( + MyServerFailingConstructorApp, + ); + const options = {document: doc}; + const bootstrap = isStandalone + ? renderApplication(MyServerFailingConstructorAppStandalone, options) + : renderModule(MyServerFailingConstructorAppModule, options); + await expectAsync(bootstrap).toBeRejectedWith( + 'Error in constructor of the root component', + ); + expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull(); + expect(wasServiceNgOnDestroyCalled) + .withContext('DestroyableService.ngOnDestroy() should be called') + .toBeTrue(); + }, + ); }); }); From 31fb9d0d52eceeca9cdc3fa637f16e06ad2faf4b Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Sat, 5 Oct 2024 11:21:34 +0200 Subject: [PATCH 32/40] docs: mention autoDetectChanges parameter default value (#58092) It was unclear whether the parameter was necessary, as its default value was not mentioned in the jdsoc. PR Close #58092 --- packages/core/testing/src/component_fixture.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/testing/src/component_fixture.ts b/packages/core/testing/src/component_fixture.ts index a9f7ee6ee357..8793bcfce4fc 100644 --- a/packages/core/testing/src/component_fixture.ts +++ b/packages/core/testing/src/component_fixture.ts @@ -111,6 +111,8 @@ export abstract class ComponentFixture { * Set whether the fixture should autodetect changes. * * Also runs detectChanges once so that any existing change is detected. + * + * @param autoDetect Whether to autodetect changes. By default, `true`. */ abstract autoDetectChanges(autoDetect?: boolean): void; From 7a6fd427d5ad70ad4c50693f54a6e77bf51eea86 Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Thu, 12 Sep 2024 20:05:12 +0000 Subject: [PATCH 33/40] fix(compiler): transform pseudo selectors correctly for the encapsulated view (#57796) fix scoping and transforming logic of the `shimCssText` for the components with encapsulated view: - add support for pseudo selector functions - apply content scoping for inner selectors of `:is()` and `:where()` - allow multiple comma separated selectors inside pseudo selectors Fixes #45686 PR Close #57796 --- packages/compiler/src/shadow_css.ts | 87 +++++++++++++++---- .../test/shadow_css/shadow_css_spec.ts | 79 +++++++++++++++++ 2 files changed, 151 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index f21dc0027022..e46d7c9064e6 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -616,7 +616,7 @@ export class ShadowCss { let selector = rule.selector; let content = rule.content; if (rule.selector[0] !== '@') { - selector = this._scopeSelector(rule.selector, scopeSelector, hostSelector); + selector = this._scopeSelector({selector, scopeSelector, hostSelector}); } else if (scopedAtRuleIdentifiers.some((atRule) => rule.selector.startsWith(atRule))) { content = this._scopeSelectors(rule.content, scopeSelector, hostSelector); } else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) { @@ -656,15 +656,34 @@ export class ShadowCss { }); } - private _scopeSelector(selector: string, scopeSelector: string, hostSelector: string): string { + private _scopeSelector({ + selector, + scopeSelector, + hostSelector, + shouldScope, + }: { + selector: string; + scopeSelector: string; + hostSelector: string; + shouldScope?: boolean; + }): string { + // Split the selector into independent parts by `,` (comma) unless + // comma is within parenthesis, for example `:is(.one, two)`. + const selectorSplitRe = / ?,(?![^\(]*\)) ?/; + return selector - .split(/ ?, ?/) + .split(selectorSplitRe) .map((part) => part.split(_shadowDeepSelectors)) .map((deepParts) => { const [shallowPart, ...otherParts] = deepParts; const applyScope = (shallowPart: string) => { if (this._selectorNeedsScoping(shallowPart, scopeSelector)) { - return this._applySelectorScope(shallowPart, scopeSelector, hostSelector); + return this._applySelectorScope({ + selector: shallowPart, + scopeSelector, + hostSelector, + shouldScope, + }); } else { return shallowPart; } @@ -713,11 +732,17 @@ export class ShadowCss { // return a selector with [name] suffix on each simple selector // e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */ - private _applySelectorScope( - selector: string, - scopeSelector: string, - hostSelector: string, - ): string { + private _applySelectorScope({ + selector, + scopeSelector, + hostSelector, + shouldScope, + }: { + selector: string; + scopeSelector: string; + hostSelector: string; + shouldScope?: boolean; + }): string { const isRe = /\[is=([^\]]*)\]/g; scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]); @@ -746,13 +771,46 @@ export class ShadowCss { return scopedP; }; + // Wraps `_scopeSelectorPart()` to not use it directly on selectors with + // pseudo selector functions like `:where()`. Selectors within pseudo selector + // functions are recursively sent to `_scopeSelector()` with the `shouldScope` + // argument, so the selectors get scoped correctly. + const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => { + let scopedPart = ''; + + const cssPseudoSelectorFunctionMatch = selectorPart.match(_cssPseudoSelectorFunctionPrefix); + if (cssPseudoSelectorFunctionMatch) { + const [cssPseudoSelectorFunction] = cssPseudoSelectorFunctionMatch; + // Unwrap the pseudo selector, to scope its contents. + // For example, `:where(selectorToScope)` -> `selectorToScope`. + const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1); + + const scopedInnerPart = this._scopeSelector({ + selector: selectorToScope, + scopeSelector, + hostSelector, + shouldScope: shouldScopeIndicator, + }); + // Put the result back into the pseudo selector function. + scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`; + } else { + shouldScopeIndicator = + shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator); + scopedPart = shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart; + } + + return scopedPart; + }; + const safeContent = new SafeSelector(selector); selector = safeContent.content(); let scopedSelector = ''; let startIndex = 0; let res: RegExpExecArray | null; - const sep = /( |>|\+|~(?!=))\s*/g; + // Spaces aren't used as a delimeter if they are within parenthesis, for example + // `:where(.one .two)` stays intact. + const sep = /( (?![^\(]*\))|>|\+|~(?!=))\s*/g; // If a selector appears before :host it should not be shimmed as it // matches on ancestor elements and not on elements in the host's shadow @@ -767,7 +825,7 @@ export class ShadowCss { // `:host-context(tag)`) const hasHost = selector.includes(_polyfillHostNoCombinator); // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present - let shouldScope = !hasHost; + let shouldScopeIndicator = shouldScope ?? !hasHost; while ((res = sep.exec(selector)) !== null) { const separator = res[1]; @@ -786,15 +844,13 @@ export class ShadowCss { continue; } - shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator); - const scopedPart = shouldScope ? _scopeSelectorPart(part) : part; + const scopedPart = _pseudoFunctionAwareScopeSelectorPart(part); scopedSelector += `${scopedPart} ${separator} `; startIndex = sep.lastIndex; } const part = selector.substring(startIndex); - shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator); - scopedSelector += shouldScope ? _scopeSelectorPart(part) : part; + scopedSelector += _pseudoFunctionAwareScopeSelectorPart(part); // replace the placeholders with their original values return safeContent.restore(scopedSelector); @@ -862,6 +918,7 @@ class SafeSelector { } } +const _cssPseudoSelectorFunctionPrefix = /^:(where|is)\(/gi; const _cssContentNextSelectorRe = /polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim; const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim; diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index e7e038d1b52e..49ebf51cf938 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -79,6 +79,85 @@ describe('ShadowCss', () => { ); }); + it('should handle pseudo functions correctly', () => { + // :where() + expect(shim(':where(.one) {}', 'contenta', 'hosta')).toEqualCss(':where(.one[contenta]) {}'); + expect(shim(':where(div.one span.two) {}', 'contenta', 'hosta')).toEqualCss( + ':where(div.one[contenta] span.two[contenta]) {}', + ); + expect(shim(':where(.one) .two {}', 'contenta', 'hosta')).toEqualCss( + ':where(.one[contenta]) .two[contenta] {}', + ); + expect(shim(':where(:host) {}', 'contenta', 'hosta')).toEqualCss(':where([hosta]) {}'); + expect(shim(':where(.one) :where(:host) {}', 'contenta', 'hosta')).toEqualCss( + ':where(.one) :where([hosta]) {}', + ); + expect(shim(':where(.one :host) {}', 'contenta', 'hosta')).toEqualCss( + ':where(.one [hosta]) {}', + ); + expect(shim('div :where(.one) {}', 'contenta', 'hosta')).toEqualCss( + 'div[contenta] :where(.one[contenta]) {}', + ); + expect(shim(':host :where(.one .two) {}', 'contenta', 'hosta')).toEqualCss( + '[hosta] :where(.one[contenta] .two[contenta]) {}', + ); + expect(shim(':where(.one, .two) {}', 'contenta', 'hosta')).toEqualCss( + ':where(.one[contenta], .two[contenta]) {}', + ); + + // :is() + expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}'); + expect(shim(':is(.dark :host) {}', 'contenta', 'a-host')).toEqualCss(':is(.dark [a-host]) {}'); + expect(shim(':host:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('[a-host]:is(.foo) {}'); + expect(shim(':is(.foo) {}', 'contenta', 'a-host')).toEqualCss(':is(.foo[contenta]) {}'); + expect(shim(':is(.foo, .bar, .baz) {}', 'contenta', 'a-host')).toEqualCss( + ':is(.foo[contenta], .bar[contenta], .baz[contenta]) {}', + ); + expect(shim(':is(.foo, .bar) :host {}', 'contenta', 'a-host')).toEqualCss( + ':is(.foo, .bar) [a-host] {}', + ); + + // :is() and :where() + expect( + shim( + ':is(.foo, .bar) :is(.baz) :where(.one, .two) :host :where(.three:first-child) {}', + 'contenta', + 'a-host', + ), + ).toEqualCss( + ':is(.foo, .bar) :is(.baz) :where(.one, .two) [a-host] :where(.three[contenta]:first-child) {}', + ); + + // complex selectors + expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}', + ); + expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', + ); + expect(shim(':host:has([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:has([foo],[foo-2]) > div.example-2[contenta] {}', + ); + expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss( + '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', + ); + + // :has() + expect(shim('div:has(a) {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:has(a) {}'); + expect(shim('div:has(a) :host {}', 'contenta', 'hosta')).toEqualCss('div:has(a) [hosta] {}'); + expect(shim(':has(a) :host :has(b) {}', 'contenta', 'hosta')).toEqualCss( + ':has(a) [hosta] [contenta]:has(b) {}', + ); + // Unlike `:is()` or `:where()` the attribute selector isn't placed inside + // of `:has()`. That is deliberate, `[contenta]:has(a)` would select all + // `[contenta]` with `a` inside, while `:has(a[contenta])` would select + // everything that contains `a[contenta]`, targeting elements outside of + // encapsulated scope. + expect(shim(':has(a) :has(b) {}', 'contenta', 'hosta')).toEqualCss( + '[contenta]:has(a) [contenta]:has(b) {}', + ); + }); + it('should handle escaped selector with space (if followed by a hex char)', () => { // When esbuild runs with optimization.minify // selectors are escaped: .über becomes .\fc ber. From 66dcc691f55eafc9de9a233b9bab53284fc13e1b Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Thu, 12 Sep 2024 20:41:45 +0000 Subject: [PATCH 34/40] fix(compiler): allow combinators inside pseudo selectors (#57796) allow css combinators within pseudo selector functions, parsing those correctly. Similarly to previous version, don't break selectors into part if combinators are within parenthesis, for example `:where(.one > .two)` PR Close #57796 --- packages/compiler/src/shadow_css.ts | 4 ++-- packages/compiler/test/shadow_css/shadow_css_spec.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index e46d7c9064e6..b72248562614 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -808,9 +808,9 @@ export class ShadowCss { let scopedSelector = ''; let startIndex = 0; let res: RegExpExecArray | null; - // Spaces aren't used as a delimeter if they are within parenthesis, for example + // Combinators aren't used as a delimeter if they are within parenthesis, for example // `:where(.one .two)` stays intact. - const sep = /( (?![^\(]*\))|>|\+|~(?!=))\s*/g; + const sep = /( |>|\+|~(?!=))(?![^\(]*\))\s*/g; // If a selector appears before :host it should not be shimmed as it // matches on ancestor elements and not on elements in the host's shadow diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 49ebf51cf938..915fad2dc227 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -104,6 +104,15 @@ describe('ShadowCss', () => { expect(shim(':where(.one, .two) {}', 'contenta', 'hosta')).toEqualCss( ':where(.one[contenta], .two[contenta]) {}', ); + expect(shim(':where(.one > .two) {}', 'contenta', 'hosta')).toEqualCss( + ':where(.one[contenta] > .two[contenta]) {}', + ); + expect(shim(':where(> .one) {}', 'contenta', 'hosta')).toEqualCss( + ':where( > .one[contenta]) {}', + ); + expect(shim(':where(:not(.one) ~ .two) {}', 'contenta', 'hosta')).toEqualCss( + ':where([contenta]:not(.one) ~ .two[contenta]) {}', + ); // :is() expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}'); @@ -148,6 +157,9 @@ describe('ShadowCss', () => { expect(shim(':has(a) :host :has(b) {}', 'contenta', 'hosta')).toEqualCss( ':has(a) [hosta] [contenta]:has(b) {}', ); + expect(shim('div:has(~ .one) {}', 'contenta', 'hosta')).toEqualCss( + 'div[contenta]:has(~ .one) {}', + ); // Unlike `:is()` or `:where()` the attribute selector isn't placed inside // of `:has()`. That is deliberate, `[contenta]:has(a)` would select all // `[contenta]` with `a` inside, while `:has(a[contenta])` would select From 48a1437e77be5c3b29b8bbcd1b5d7784fbb67e68 Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Fri, 13 Sep 2024 04:49:28 +0000 Subject: [PATCH 35/40] fix(compiler): fix comment typo (#57796) fix spelling in the comment PR Close #57796 --- packages/compiler/src/shadow_css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index b72248562614..765718ffbd56 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -808,8 +808,8 @@ export class ShadowCss { let scopedSelector = ''; let startIndex = 0; let res: RegExpExecArray | null; - // Combinators aren't used as a delimeter if they are within parenthesis, for example - // `:where(.one .two)` stays intact. + // Combinators aren't used as a delimiter if they are within parenthesis, + // for example `:where(.one .two)` stays intact. const sep = /( |>|\+|~(?!=))(?![^\(]*\))\s*/g; // If a selector appears before :host it should not be shimmed as it From 11692c8dab2a78dc8780ceed301242d51dee7c9c Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Mon, 23 Sep 2024 06:20:32 +0000 Subject: [PATCH 36/40] fix(compiler): add multiple :host and nested selectors support (#57796) add support for nested and deeply nested (up to three levels) selectors, parse multiple :host selectors, scope selectors within pseudo functions PR Close #57796 --- packages/compiler/src/shadow_css.ts | 109 +++++++++++++----- .../test/shadow_css/shadow_css_spec.ts | 65 ++++++++++- 2 files changed, 140 insertions(+), 34 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 765718ffbd56..9e10d60d87e7 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -338,7 +338,7 @@ export class ShadowCss { * captures how many (if any) leading whitespaces are present or a comma * - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*)) * captures two different possible keyframes, ones which are quoted or ones which are valid css - * idents (custom properties excluded) + * indents (custom properties excluded) * - (?=[,\s;]|$) * simply matches the end of the possible keyframe, valid endings are: a comma, a space, a * semicolon or the end of the string @@ -459,7 +459,7 @@ export class ShadowCss { */ private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string { const unscopedRules = this._extractUnscopedRulesFromCssText(cssText); - // replace :host and :host-context -shadowcsshost and -shadowcsshost respectively + // replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively cssText = this._insertPolyfillHostInCssText(cssText); cssText = this._convertColonHost(cssText); cssText = this._convertColonHostContext(cssText); @@ -616,7 +616,12 @@ export class ShadowCss { let selector = rule.selector; let content = rule.content; if (rule.selector[0] !== '@') { - selector = this._scopeSelector({selector, scopeSelector, hostSelector}); + selector = this._scopeSelector({ + selector, + scopeSelector, + hostSelector, + isParentSelector: true, + }); } else if (scopedAtRuleIdentifiers.some((atRule) => rule.selector.startsWith(atRule))) { content = this._scopeSelectors(rule.content, scopeSelector, hostSelector); } else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) { @@ -656,20 +661,30 @@ export class ShadowCss { }); } + private _safeSelector: SafeSelector | undefined; + private _shouldScopeIndicator: boolean | undefined; + + // `isParentSelector` is used to distinguish the selectors which are coming from + // the initial selector string and any nested selectors, parsed recursively, + // for example `selector = 'a:where(.one)'` could be the parent, while recursive call + // would have `selector = '.one'`. private _scopeSelector({ selector, scopeSelector, hostSelector, - shouldScope, + isParentSelector = false, }: { selector: string; scopeSelector: string; hostSelector: string; - shouldScope?: boolean; + isParentSelector?: boolean; }): string { // Split the selector into independent parts by `,` (comma) unless // comma is within parenthesis, for example `:is(.one, two)`. - const selectorSplitRe = / ?,(?![^\(]*\)) ?/; + // Negative lookup after comma allows not splitting inside nested parenthesis, + // up to three levels (((,))). + const selectorSplitRe = + / ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/; return selector .split(selectorSplitRe) @@ -682,7 +697,7 @@ export class ShadowCss { selector: shallowPart, scopeSelector, hostSelector, - shouldScope, + isParentSelector, }); } else { return shallowPart; @@ -716,9 +731,9 @@ export class ShadowCss { if (_polyfillHostRe.test(selector)) { const replaceBy = `[${hostSelector}]`; return selector - .replace(_polyfillHostNoCombinatorRe, (hnc, selector) => { + .replace(_polyfillHostNoCombinatorReGlobal, (_hnc, selector) => { return selector.replace( - /([^:]*)(:*)(.*)/, + /([^:\)]*)(:*)(.*)/, (_: string, before: string, colon: string, after: string) => { return before + replaceBy + colon + after; }, @@ -736,12 +751,12 @@ export class ShadowCss { selector, scopeSelector, hostSelector, - shouldScope, + isParentSelector, }: { selector: string; scopeSelector: string; hostSelector: string; - shouldScope?: boolean; + isParentSelector?: boolean; }): string { const isRe = /\[is=([^\]]*)\]/g; scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]); @@ -757,6 +772,10 @@ export class ShadowCss { if (p.includes(_polyfillHostNoCombinator)) { scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector); + if (_polyfillHostNoCombinatorWithinPseudoFunction.test(p)) { + const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)(.*)/)!; + scopedP = before + attrName + colon + after; + } } else { // remove :host since it should be unnecessary const t = p.replace(_polyfillHostRe, ''); @@ -773,44 +792,66 @@ export class ShadowCss { // Wraps `_scopeSelectorPart()` to not use it directly on selectors with // pseudo selector functions like `:where()`. Selectors within pseudo selector - // functions are recursively sent to `_scopeSelector()` with the `shouldScope` - // argument, so the selectors get scoped correctly. + // functions are recursively sent to `_scopeSelector()`. const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => { let scopedPart = ''; - const cssPseudoSelectorFunctionMatch = selectorPart.match(_cssPseudoSelectorFunctionPrefix); - if (cssPseudoSelectorFunctionMatch) { - const [cssPseudoSelectorFunction] = cssPseudoSelectorFunctionMatch; + const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match( + _cssPrefixWithPseudoSelectorFunction, + ); + if (cssPrefixWithPseudoSelectorFunctionMatch) { + const [cssPseudoSelectorFunction, mainSelector, pseudoSelector] = + cssPrefixWithPseudoSelectorFunctionMatch; + const hasOuterHostNoCombinator = mainSelector.includes(_polyfillHostNoCombinator); + const scopedMainSelector = mainSelector.replace( + _polyfillHostNoCombinatorReGlobal, + `[${hostSelector}]`, + ); + // Unwrap the pseudo selector, to scope its contents. - // For example, `:where(selectorToScope)` -> `selectorToScope`. + // For example, + // - `:where(selectorToScope)` -> `selectorToScope`; + // - `div:is(.foo, .bar)` -> `.foo, .bar`. const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1); + if (selectorToScope.includes(_polyfillHostNoCombinator)) { + this._shouldScopeIndicator = true; + } + const scopedInnerPart = this._scopeSelector({ selector: selectorToScope, scopeSelector, hostSelector, - shouldScope: shouldScopeIndicator, }); + // Put the result back into the pseudo selector function. - scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`; + scopedPart = `${scopedMainSelector}:${pseudoSelector}(${scopedInnerPart})`; + + this._shouldScopeIndicator = this._shouldScopeIndicator || hasOuterHostNoCombinator; } else { - shouldScopeIndicator = - shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator); - scopedPart = shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart; + this._shouldScopeIndicator = + this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator); + scopedPart = this._shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart; } return scopedPart; }; - const safeContent = new SafeSelector(selector); - selector = safeContent.content(); + if (isParentSelector) { + this._safeSelector = new SafeSelector(selector); + selector = this._safeSelector.content(); + } let scopedSelector = ''; let startIndex = 0; let res: RegExpExecArray | null; // Combinators aren't used as a delimiter if they are within parenthesis, // for example `:where(.one .two)` stays intact. - const sep = /( |>|\+|~(?!=))(?![^\(]*\))\s*/g; + // Similarly to selector separation by comma initially, negative lookahead + // is used here to not break selectors within nested parenthesis up to three + // nested layers. + const sep = + /( |>|\+|~(?!=))(?!([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)))\s*/g; // If a selector appears before :host it should not be shimmed as it // matches on ancestor elements and not on elements in the host's shadow @@ -824,8 +865,13 @@ export class ShadowCss { // - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a // `:host-context(tag)`) const hasHost = selector.includes(_polyfillHostNoCombinator); - // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present - let shouldScopeIndicator = shouldScope ?? !hasHost; + // Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator` + // when it is present. The selector has the same level when it is a part of a pseudo + // selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo` + // being scoped. + if (isParentSelector || this._shouldScopeIndicator) { + this._shouldScopeIndicator = !hasHost; + } while ((res = sep.exec(selector)) !== null) { const separator = res[1]; @@ -853,7 +899,8 @@ export class ShadowCss { scopedSelector += _pseudoFunctionAwareScopeSelectorPart(part); // replace the placeholders with their original values - return safeContent.restore(scopedSelector); + // using values stored inside the `safeSelector` instance. + return this._safeSelector!.restore(scopedSelector); } private _insertPolyfillHostInCssText(selector: string): string { @@ -918,7 +965,7 @@ class SafeSelector { } } -const _cssPseudoSelectorFunctionPrefix = /^:(where|is)\(/gi; +const _cssPrefixWithPseudoSelectorFunction = /^([^:]*):(where|is)\(/i; const _cssContentNextSelectorRe = /polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim; const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim; @@ -932,7 +979,11 @@ const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim'); const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim'); const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im'); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; +const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp( + `:.*(.*${_polyfillHostNoCombinator}.*)`, +); const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; +const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g'); const _shadowDOMSelectorsRe = [ /::shadow/g, /::content/g, diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 915fad2dc227..ff70c407b2ae 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -66,6 +66,8 @@ describe('ShadowCss', () => { expect(shim('one[attr="va lue"] {}', 'contenta')).toEqualCss('one[attr="va lue"][contenta] {}'); expect(shim('one[attr] {}', 'contenta')).toEqualCss('one[attr][contenta] {}'); expect(shim('[is="one"] {}', 'contenta')).toEqualCss('[is="one"][contenta] {}'); + expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}'); + expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}'); }); it('should handle escaped sequences in selectors', () => { @@ -89,6 +91,9 @@ describe('ShadowCss', () => { ':where(.one[contenta]) .two[contenta] {}', ); expect(shim(':where(:host) {}', 'contenta', 'hosta')).toEqualCss(':where([hosta]) {}'); + expect(shim(':where(:host) .one {}', 'contenta', 'hosta')).toEqualCss( + ':where([hosta]) .one[contenta] {}', + ); expect(shim(':where(.one) :where(:host) {}', 'contenta', 'hosta')).toEqualCss( ':where(.one) :where([hosta]) {}', ); @@ -113,10 +118,14 @@ describe('ShadowCss', () => { expect(shim(':where(:not(.one) ~ .two) {}', 'contenta', 'hosta')).toEqualCss( ':where([contenta]:not(.one) ~ .two[contenta]) {}', ); + expect(shim(':where([foo]) {}', 'contenta', 'hosta')).toEqualCss(':where([foo][contenta]) {}'); // :is() - expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}'); + expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div:is(.foo[contenta]) {}'); expect(shim(':is(.dark :host) {}', 'contenta', 'a-host')).toEqualCss(':is(.dark [a-host]) {}'); + expect(shim(':is(.dark) :is(:host) {}', 'contenta', 'a-host')).toEqualCss( + ':is(.dark) :is([a-host]) {}', + ); expect(shim(':host:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('[a-host]:is(.foo) {}'); expect(shim(':is(.foo) {}', 'contenta', 'a-host')).toEqualCss(':is(.foo[contenta]) {}'); expect(shim(':is(.foo, .bar, .baz) {}', 'contenta', 'a-host')).toEqualCss( @@ -136,10 +145,34 @@ describe('ShadowCss', () => { ).toEqualCss( ':is(.foo, .bar) :is(.baz) :where(.one, .two) [a-host] :where(.three[contenta]:first-child) {}', ); + expect(shim(':where(:is(a)) {}', 'contenta', 'hosta')).toEqualCss( + ':where(:is(a[contenta])) {}', + ); + expect(shim(':where(:is(a, b)) {}', 'contenta', 'hosta')).toEqualCss( + ':where(:is(a[contenta], b[contenta])) {}', + ); + expect(shim(':where(:host:is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss( + ':where([hosta]:is(.one, .two)) {}', + ); + expect(shim(':where(:host :is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss( + ':where([hosta] :is(.one[contenta], .two[contenta])) {}', + ); + expect(shim(':where(:is(a, b) :is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss( + ':where(:is(a[contenta], b[contenta]) :is(.one[contenta], .two[contenta])) {}', + ); + expect( + shim( + ':where(:where(a:has(.foo), b) :is(.one, .two:where(.foo > .bar))) {}', + 'contenta', + 'hosta', + ), + ).toEqualCss( + ':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two:where(.foo[contenta] > .bar[contenta]))) {}', + ); // complex selectors expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss( - '[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}', + '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', ); expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss( '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', @@ -147,9 +180,6 @@ describe('ShadowCss', () => { expect(shim(':host:has([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss( '[a-host]:has([foo],[foo-2]) > div.example-2[contenta] {}', ); - expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss( - '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', - ); // :has() expect(shim('div:has(a) {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:has(a) {}'); @@ -170,6 +200,31 @@ describe('ShadowCss', () => { ); }); + it('should handle :host inclusions inside pseudo-selectors selectors', () => { + expect(shim('.header:not(.admin) {}', 'contenta', 'hosta')).toEqualCss( + '.header[contenta]:not(.admin) {}', + ); + expect(shim('.header:is(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta')).toEqualCss( + '.header:is([hosta] > .toolbar[contenta], [hosta] ~ .panel[contenta]) {}', + ); + expect( + shim('.header:where(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta'), + ).toEqualCss('.header:where([hosta] > .toolbar[contenta], [hosta] ~ .panel[contenta]) {}'); + expect(shim('.header:not(.admin, :host.super .header) {}', 'contenta', 'hosta')).toEqualCss( + '.header[contenta]:not(.admin, .super[hosta] .header) {}', + ); + expect( + shim('.header:not(.admin, :host.super .header, :host.mega .header) {}', 'contenta', 'hosta'), + ).toEqualCss('.header[contenta]:not(.admin, .super[hosta] .header, .mega[hosta] .header) {}'); + + expect(shim('.one :where(.two, :host) {}', 'contenta', 'hosta')).toEqualCss( + '.one :where(.two[contenta], [hosta]) {}', + ); + expect(shim('.one :where(:host, .two) {}', 'contenta', 'hosta')).toEqualCss( + '.one :where([hosta], .two[contenta]) {}', + ); + }); + it('should handle escaped selector with space (if followed by a hex char)', () => { // When esbuild runs with optimization.minify // selectors are escaped: .über becomes .\fc ber. From aea747ab3bcbca79dbbc7ddfc41e11b9e43952eb Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Mon, 23 Sep 2024 17:55:48 +0000 Subject: [PATCH 37/40] fix(compiler): preserve attributes attached to :host selector (#57796) keep attributes used to scope :host selectors PR Close #57796 --- packages/compiler/src/shadow_css.ts | 3 ++- packages/compiler/test/shadow_css/shadow_css_spec.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 9e10d60d87e7..1d3385695634 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -804,7 +804,7 @@ export class ShadowCss { cssPrefixWithPseudoSelectorFunctionMatch; const hasOuterHostNoCombinator = mainSelector.includes(_polyfillHostNoCombinator); const scopedMainSelector = mainSelector.replace( - _polyfillHostNoCombinatorReGlobal, + _polyfillExactHostNoCombinatorReGlobal, `[${hostSelector}]`, ); @@ -982,6 +982,7 @@ const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp( `:.*(.*${_polyfillHostNoCombinator}.*)`, ); +const _polyfillExactHostNoCombinatorReGlobal = /-shadowcsshost-no-combinator/g; const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g'); const _shadowDOMSelectorsRe = [ diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index ff70c407b2ae..573af9185204 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -67,7 +67,17 @@ describe('ShadowCss', () => { expect(shim('one[attr] {}', 'contenta')).toEqualCss('one[attr][contenta] {}'); expect(shim('[is="one"] {}', 'contenta')).toEqualCss('[is="one"][contenta] {}'); expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}'); + }); + + it('should transform :host with attributes and pseudo selectors', () => { expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}'); + expect(shim(':host(create-first-project) {}', 'contenta', 'hosta')).toEqualCss( + 'create-first-project[hosta] {}', + ); + expect(shim(':host[attr] {}', 'contenta', 'hosta')).toEqualCss('[attr][hosta] {}'); + expect(shim(':host[attr]:where(:not(.cm-button)) {}', 'contenta', 'hosta')).toEqualCss( + '[hosta][attr]:where(:not(.cm-button)) {}', + ); }); it('should handle escaped sequences in selectors', () => { From d325f9b55f248e5bd059645be901f210018f8fa2 Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Tue, 24 Sep 2024 06:55:35 +0000 Subject: [PATCH 38/40] fix(compiler): fix parsing of the :host-context with pseudo selectors (#57796) fix regexp which is used to test for host inside pseudo selectors PR Close #57796 --- packages/compiler/src/shadow_css.ts | 2 +- packages/compiler/test/shadow_css/shadow_css_spec.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 1d3385695634..d5101dbf085d 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -980,7 +980,7 @@ const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuf const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im'); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp( - `:.*(.*${_polyfillHostNoCombinator}.*)`, + `:.*\\(.*${_polyfillHostNoCombinator}.*\\)`, ); const _polyfillExactHostNoCombinatorReGlobal = /-shadowcsshost-no-combinator/g; const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 573af9185204..452e0393547f 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -69,7 +69,7 @@ describe('ShadowCss', () => { expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}'); }); - it('should transform :host with attributes and pseudo selectors', () => { + it('should transform :host and :host-context with attributes and pseudo selectors', () => { expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}'); expect(shim(':host(create-first-project) {}', 'contenta', 'hosta')).toEqualCss( 'create-first-project[hosta] {}', @@ -78,6 +78,11 @@ describe('ShadowCss', () => { expect(shim(':host[attr]:where(:not(.cm-button)) {}', 'contenta', 'hosta')).toEqualCss( '[hosta][attr]:where(:not(.cm-button)) {}', ); + expect( + shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'), + ).toEqualCss( + 'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}', + ); }); it('should handle escaped sequences in selectors', () => { From 21be258be687a300ca22daad823e0b931029db35 Mon Sep 17 00:00:00 2001 From: Georgy Serga Date: Thu, 26 Sep 2024 21:25:44 +0000 Subject: [PATCH 39/40] fix(compiler): scope :host-context inside pseudo selectors, do not decrease specificity (#57796) parse constructions like `:where(:host-context(.foo))` correctly revert logic which lead to decreased specificity if `:where` was applied to another selector, for example `div` is transformed to `div[contenta]` with specificity of (0,1,1) so `div:where(.foo)` should not decrease it leading to `div[contenta]:where(.foo)` with the same specificity (0,1,1) instead of `div:where(.foo[contenta])` with specificity equal to (0,0,1) PR Close #57796 --- packages/compiler/src/shadow_css.ts | 45 ++++++++++--------- .../shadow_css/host_and_host_context_spec.ts | 36 +++++++++++++++ .../test/shadow_css/shadow_css_spec.ts | 40 ++++++++++++----- 3 files changed, 87 insertions(+), 34 deletions(-) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index d5101dbf085d..bbaad8b43cf4 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -539,7 +539,7 @@ export class ShadowCss { * .foo .bar { ... } */ private _convertColonHostContext(cssText: string): string { - return cssText.replace(_cssColonHostContextReGlobal, (selectorText) => { + return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => { // We have captured a selector that contains a `:host-context` rule. // For backward compatibility `:host-context` may contain a comma separated list of selectors. @@ -594,10 +594,12 @@ export class ShadowCss { } // The context selectors now must be combined with each other to capture all the possible - // selectors that `:host-context` can match. See `combineHostContextSelectors()` for more + // selectors that `:host-context` can match. See `_combineHostContextSelectors()` for more // info about how this is done. return contextSelectorGroups - .map((contextSelectors) => combineHostContextSelectors(contextSelectors, selectorText)) + .map((contextSelectors) => + _combineHostContextSelectors(contextSelectors, selectorText, pseudoPrefix), + ) .join(', '); }); } @@ -800,18 +802,12 @@ export class ShadowCss { _cssPrefixWithPseudoSelectorFunction, ); if (cssPrefixWithPseudoSelectorFunctionMatch) { - const [cssPseudoSelectorFunction, mainSelector, pseudoSelector] = - cssPrefixWithPseudoSelectorFunctionMatch; - const hasOuterHostNoCombinator = mainSelector.includes(_polyfillHostNoCombinator); - const scopedMainSelector = mainSelector.replace( - _polyfillExactHostNoCombinatorReGlobal, - `[${hostSelector}]`, - ); - - // Unwrap the pseudo selector, to scope its contents. + const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch; + + // Unwrap the pseudo selector to scope its contents. // For example, // - `:where(selectorToScope)` -> `selectorToScope`; - // - `div:is(.foo, .bar)` -> `.foo, .bar`. + // - `:is(.foo, .bar)` -> `.foo, .bar`. const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1); if (selectorToScope.includes(_polyfillHostNoCombinator)) { @@ -825,9 +821,7 @@ export class ShadowCss { }); // Put the result back into the pseudo selector function. - scopedPart = `${scopedMainSelector}:${pseudoSelector}(${scopedInnerPart})`; - - this._shouldScopeIndicator = this._shouldScopeIndicator || hasOuterHostNoCombinator; + scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`; } else { this._shouldScopeIndicator = this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator); @@ -965,7 +959,8 @@ class SafeSelector { } } -const _cssPrefixWithPseudoSelectorFunction = /^([^:]*):(where|is)\(/i; +const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?'; +const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i; const _cssContentNextSelectorRe = /polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim; const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim; @@ -976,13 +971,15 @@ const _polyfillHost = '-shadowcsshost'; const _polyfillHostContext = '-shadowcsscontext'; const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)'; const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim'); -const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim'); +const _cssColonHostContextReGlobal = new RegExp( + _cssScopedPseudoFunctionPrefix + '(' + _polyfillHostContext + _parenSuffix + ')', + 'gim', +); const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im'); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp( `:.*\\(.*${_polyfillHostNoCombinator}.*\\)`, ); -const _polyfillExactHostNoCombinatorReGlobal = /-shadowcsshost-no-combinator/g; const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/; const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g'); const _shadowDOMSelectorsRe = [ @@ -1235,7 +1232,11 @@ function unescapeQuotes(str: string, isQuoted: boolean): string { * @param contextSelectors an array of context selectors that will be combined. * @param otherSelectors the rest of the selectors that are not context selectors. */ -function combineHostContextSelectors(contextSelectors: string[], otherSelectors: string): string { +function _combineHostContextSelectors( + contextSelectors: string[], + otherSelectors: string, + pseudoPrefix = '', +): string { const hostMarker = _polyfillHostNoCombinator; _polyfillHostRe.lastIndex = 0; // reset the regex to ensure we get an accurate test const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors); @@ -1264,8 +1265,8 @@ function combineHostContextSelectors(contextSelectors: string[], otherSelectors: return combined .map((s) => otherSelectorsHasHost - ? `${s}${otherSelectors}` - : `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`, + ? `${pseudoPrefix}${s}${otherSelectors}` + : `${pseudoPrefix}${s}${hostMarker}${otherSelectors}, ${pseudoPrefix}${s} ${hostMarker}${otherSelectors}`, ) .join(','); } diff --git a/packages/compiler/test/shadow_css/host_and_host_context_spec.ts b/packages/compiler/test/shadow_css/host_and_host_context_spec.ts index c4dc29c372b4..5b5689feb2be 100644 --- a/packages/compiler/test/shadow_css/host_and_host_context_spec.ts +++ b/packages/compiler/test/shadow_css/host_and_host_context_spec.ts @@ -107,6 +107,42 @@ describe('ShadowCss, :host and :host-context', () => { }); describe(':host-context', () => { + it('should transform :host-context with pseudo selectors', () => { + expect( + shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'), + ).toEqualCss( + 'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}', + ); + expect(shim(':where(:host-context(backdrop)) {}', 'contenta', 'hosta')).toEqualCss( + ':where(backdrop[hosta]), :where(backdrop [hosta]) {}', + ); + expect(shim(':where(:host-context(outer1)) :host(bar) {}', 'contenta', 'hosta')).toEqualCss( + ':where(outer1) bar[hosta] {}', + ); + expect( + shim(':where(:host-context(.one)) :where(:host-context(.two)) {}', 'contenta', 'a-host'), + ).toEqualCss( + ':where(.one.two[a-host]), ' + // `one` and `two` both on the host + ':where(.one.two [a-host]), ' + // `one` and `two` are both on the same ancestor + ':where(.one .two[a-host]), ' + // `one` is an ancestor and `two` is on the host + ':where(.one .two [a-host]), ' + // `one` and `two` are both ancestors (in that order) + ':where(.two .one[a-host]), ' + // `two` is an ancestor and `one` is on the host + ':where(.two .one [a-host])' + // `two` and `one` are both ancestors (in that order) + ' {}', + ); + expect( + shim(':where(:host-context(backdrop)) .foo ~ .bar {}', 'contenta', 'hosta'), + ).toEqualCss( + ':where(backdrop[hosta]) .foo[contenta] ~ .bar[contenta], :where(backdrop [hosta]) .foo[contenta] ~ .bar[contenta] {}', + ); + expect(shim(':where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss( + ':where(backdrop) [hosta] {}', + ); + expect(shim('div:where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss( + 'div:where(backdrop) [hosta] {}', + ); + }); + it('should handle tag selector', () => { expect(shim(':host-context(div) {}', 'contenta', 'a-host')).toEqualCss( 'div[a-host], div [a-host] {}', diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css/shadow_css_spec.ts index 452e0393547f..77a0a361a319 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css/shadow_css_spec.ts @@ -69,19 +69,14 @@ describe('ShadowCss', () => { expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}'); }); - it('should transform :host and :host-context with attributes and pseudo selectors', () => { + it('should transform :host with attributes', () => { expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}'); expect(shim(':host(create-first-project) {}', 'contenta', 'hosta')).toEqualCss( 'create-first-project[hosta] {}', ); expect(shim(':host[attr] {}', 'contenta', 'hosta')).toEqualCss('[attr][hosta] {}'); expect(shim(':host[attr]:where(:not(.cm-button)) {}', 'contenta', 'hosta')).toEqualCss( - '[hosta][attr]:where(:not(.cm-button)) {}', - ); - expect( - shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'), - ).toEqualCss( - 'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}', + '[attr][hosta]:where(:not(.cm-button)) {}', ); }); @@ -94,6 +89,27 @@ describe('ShadowCss', () => { expect(shim('.one\\:two .three\\:four {}', 'contenta')).toEqualCss( '.one\\:two[contenta] .three\\:four[contenta] {}', ); + expect(shim('div:where(.one) {}', 'contenta', 'hosta')).toEqualCss( + 'div[contenta]:where(.one) {}', + ); + expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}'); + // See `xit('should parse concatenated pseudo selectors'` + expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss( + ':where(a)[contenta]:where(b) {}', + ); + expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}'); + expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss( + '*[contenta]:where(.one) .foo {}', + ); + }); + + xit('should parse concatenated pseudo selectors', () => { + // Current logic leads to a result with an outer scope + // It could be changed, to not increase specificity + // Requires a more complex parsing + expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss( + ':where(a[contenta]):where(b[contenta]) {}', + ); }); it('should handle pseudo functions correctly', () => { @@ -136,7 +152,7 @@ describe('ShadowCss', () => { expect(shim(':where([foo]) {}', 'contenta', 'hosta')).toEqualCss(':where([foo][contenta]) {}'); // :is() - expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div:is(.foo[contenta]) {}'); + expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}'); expect(shim(':is(.dark :host) {}', 'contenta', 'a-host')).toEqualCss(':is(.dark [a-host]) {}'); expect(shim(':is(.dark) :is(:host) {}', 'contenta', 'a-host')).toEqualCss( ':is(.dark) :is([a-host]) {}', @@ -182,12 +198,12 @@ describe('ShadowCss', () => { 'hosta', ), ).toEqualCss( - ':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two:where(.foo[contenta] > .bar[contenta]))) {}', + ':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}', ); // complex selectors expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss( - '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', + '[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}', ); expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss( '[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}', @@ -220,11 +236,11 @@ describe('ShadowCss', () => { '.header[contenta]:not(.admin) {}', ); expect(shim('.header:is(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta')).toEqualCss( - '.header:is([hosta] > .toolbar[contenta], [hosta] ~ .panel[contenta]) {}', + '.header[contenta]:is([hosta] > .toolbar, [hosta] ~ .panel) {}', ); expect( shim('.header:where(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta'), - ).toEqualCss('.header:where([hosta] > .toolbar[contenta], [hosta] ~ .panel[contenta]) {}'); + ).toEqualCss('.header[contenta]:where([hosta] > .toolbar, [hosta] ~ .panel) {}'); expect(shim('.header:not(.admin, :host.super .header) {}', 'contenta', 'hosta')).toEqualCss( '.header[contenta]:not(.admin, .super[hosta] .header) {}', ); From 2532ffa35e15388fb8cfd613c41b9a202fc1d725 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 10 Oct 2024 09:56:51 +0000 Subject: [PATCH 40/40] release: cut the v18.2.8 release --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cba27e104773..5339cb1c353c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ + +# 18.2.8 (2024-10-10) +### compiler +| Commit | Type | Description | +| -- | -- | -- | +| [11692c8dab](https://github.com/angular/angular/commit/11692c8dab2a78dc8780ceed301242d51dee7c9c) | fix | add multiple :host and nested selectors support ([#57796](https://github.com/angular/angular/pull/57796)) | +| [66dcc691f5](https://github.com/angular/angular/commit/66dcc691f55eafc9de9a233b9bab53284fc13e1b) | fix | allow combinators inside pseudo selectors ([#57796](https://github.com/angular/angular/pull/57796)) | +| [48a1437e77](https://github.com/angular/angular/commit/48a1437e77be5c3b29b8bbcd1b5d7784fbb67e68) | fix | fix comment typo ([#57796](https://github.com/angular/angular/pull/57796)) | +| [d325f9b55f](https://github.com/angular/angular/commit/d325f9b55f248e5bd059645be901f210018f8fa2) | fix | fix parsing of the :host-context with pseudo selectors ([#57796](https://github.com/angular/angular/pull/57796)) | +| [aea747ab3b](https://github.com/angular/angular/commit/aea747ab3bcbca79dbbc7ddfc41e11b9e43952eb) | fix | preserve attributes attached to :host selector ([#57796](https://github.com/angular/angular/pull/57796)) | +| [21be258be6](https://github.com/angular/angular/commit/21be258be687a300ca22daad823e0b931029db35) | fix | scope :host-context inside pseudo selectors, do not decrease specificity ([#57796](https://github.com/angular/angular/pull/57796)) | +| [7a6fd427d5](https://github.com/angular/angular/commit/7a6fd427d5ad70ad4c50693f54a6e77bf51eea86) | fix | transform pseudo selectors correctly for the encapsulated view ([#57796](https://github.com/angular/angular/pull/57796)) | +### compiler-cli +| Commit | Type | Description | +| -- | -- | -- | +| [f187c3abf8](https://github.com/angular/angular/commit/f187c3abf8b9547b2692995f344cd7dcb9f32ebc) | fix | defer symbols only used in types ([#58104](https://github.com/angular/angular/pull/58104)) | +### core +| Commit | Type | Description | +| -- | -- | -- | +| [46bafb0b0a](https://github.com/angular/angular/commit/46bafb0b0a952d8e9c2a0099f0607354697bbeaa) | fix | clean up afterRender after it is executed ([#58119](https://github.com/angular/angular/pull/58119)) | +### platform-server +| Commit | Type | Description | +| -- | -- | -- | +| [b40875a2cc](https://github.com/angular/angular/commit/b40875a2cc28a94015e6392044a03b30c2559999) | fix | destroy `PlatformRef` when error happens during the `bootstrap()` phase ([#58112](https://github.com/angular/angular/pull/58112)) ([#58135](https://github.com/angular/angular/pull/58135)) | + + + # 18.2.7 (2024-10-02) ### common diff --git a/package.json b/package.json index d7c8a64275c9..c6821627c949 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "18.2.7", + "version": "18.2.8", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular",