From d66ebf328882282a11fa04f8c3a6c5cadc553fed Mon Sep 17 00:00:00 2001 From: Daniel Sogl Date: Mon, 9 Dec 2024 21:28:20 +0100 Subject: [PATCH 01/36] docs: update example to use inject function and self closing tags (#59118) PR Close #59118 --- .../guide/routing/common-router-tasks.md | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/adev/src/content/guide/routing/common-router-tasks.md b/adev/src/content/guide/routing/common-router-tasks.md index c9e8488beb793..57f39c217c560 100644 --- a/adev/src/content/guide/routing/common-router-tasks.md +++ b/adev/src/content/guide/routing/common-router-tasks.md @@ -102,7 +102,7 @@ Now that you have defined your routes, add them to your application. First, add - + ``` You also need to add the `RouterLink`, `RouterLinkActive`, and `RouterOutlet` to the `imports` array of `AppComponent`. @@ -110,7 +110,7 @@ You also need to add the `RouterLink`, `RouterLinkActive`, and `RouterOutlet` to ```ts @Component({ selector: 'app-root', - imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive], + imports: [RouterOutlet, RouterLink, RouterLinkActive], templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) @@ -269,7 +269,7 @@ Here, `FirstComponent` has its own ` - + ``` A child route is like any other route, in that it needs both a `path` and a `component`. @@ -328,7 +328,7 @@ HELPFUL: The `title` property follows the same rules as static route `data` and You can also provide a custom title strategy by extending the `TitleStrategy`. ```ts -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class TemplatePageTitleStrategy extends TitleStrategy { constructor(private readonly title: Title) { super(); @@ -345,7 +345,7 @@ export class TemplatePageTitleStrategy extends TitleStrategy { export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), - {provide: TitleStrategy, useClass: TemplatePageTitleStrategy}, + { provide: TitleStrategy, useClass: TemplatePageTitleStrategy }, ] }; ``` @@ -365,7 +365,7 @@ Rather than writing out the whole path to get to `SecondComponent`, use the `../
  • Relative Route to second component
  • - + ``` In addition to `../`, use `./` or no leading slash to specify the current level. @@ -394,12 +394,12 @@ Sometimes, a feature of your application requires accessing a part of a route, s In this example, the route contains an `id` parameter we can use to target a specific hero page. ```ts -import {ApplicationConfig} from "@angular/core"; -import {Routes} from '@angular/router'; -import {HeroListComponent} from './hero-list.component'; +import { ApplicationConfig } from "@angular/core"; +import { Routes } from '@angular/router'; +import { HeroListComponent } from './hero-list.component'; export const routes: Routes = [ - {path: 'hero/:id', component: HeroDetailComponent} + { path: 'hero/:id', component: HeroDetailComponent } ]; export const appConfig: ApplicationConfig = { @@ -410,15 +410,15 @@ export const appConfig: ApplicationConfig = { First, import the following members in the component you want to navigate from. ```ts +import { inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { Observable, switchMap } from 'rxjs'; ``` Next inject the activated route service: ```ts -constructor(private route: ActivatedRoute) {} +private readonly route = inject(ActivatedRoute); ``` Configure the class so that you have an observable, `heroes$`, a `selectedId` to hold the `id` number of the hero, and the heroes in the `ngOnInit()`, add the following code to get the `id` of the selected hero. @@ -449,11 +449,10 @@ import { Observable } from 'rxjs'; Inject `ActivatedRoute` and `Router` in the constructor of the component class so they are available to this component: ```ts -hero$: Observable; +private readonly route = inject(ActivatedRoute); +private readonly router = inject(Router); -constructor( - private route: ActivatedRoute, - private router: Router ) {} +hero$: Observable; ngOnInit() { const heroId = this.route.snapshot.paramMap.get('id'); @@ -601,7 +600,7 @@ You could also redefine the `AppComponent` template with Crisis Center routes ex Dragon Crisis Shark Crisis - + ` }) export class AppComponent {} From 139f61913c0a4cd010d27eb66339abec0efdb04d Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sat, 21 Dec 2024 01:02:55 +0100 Subject: [PATCH 02/36] docs: remove data attributes from api-item-label (#59273) Perf measurments have revealed that `data-*` attributes are slower to set than classes. PR Close #59273 --- .../rendering/templates/header-api.tsx | 6 +- adev/shared-docs/styles/_api-item-label.scss | 36 +++++----- .../api-item-label.component.html | 1 - .../api-item-label.component.spec.ts | 13 +--- .../api-item-label.component.ts | 12 ++-- .../references/pipes/api-label.pipe.ts | 66 +++++++++---------- 6 files changed, 59 insertions(+), 75 deletions(-) delete mode 100644 adev/src/app/features/references/api-item-label/api-item-label.component.html diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/header-api.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/header-api.tsx index c1af0d2abd441..9ca0bcb556308 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/header-api.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/header-api.tsx @@ -36,11 +36,7 @@ export function HeaderApi(props: {entry: DocEntryRenderable; showFullDescription

    {entry.name}

    -
    +
    {getEntryTypeDisplayName(entry.entryType)}
    {entry.isDeprecated && ( diff --git a/adev/shared-docs/styles/_api-item-label.scss b/adev/shared-docs/styles/_api-item-label.scss index dfadc46b5015c..9373a8c4025fd 100644 --- a/adev/shared-docs/styles/_api-item-label.scss +++ b/adev/shared-docs/styles/_api-item-label.scss @@ -14,12 +14,12 @@ background-color 0.3s ease; text-transform: capitalize; - &[data-mode='short'] { + &:not(.full) { height: 22px; width: 22px; } - &[data-mode='full'] { + &.full { font-size: 0.75rem; padding: 0.25rem 0.5rem; } @@ -44,58 +44,58 @@ background: color-mix(in srgb, var(--label-theme) 10%, white); } - &[data-type='undecorated_class'], - &[data-type='class'] { + &.type-undecorated_class, + &.type-class { --label-theme: var(--symbolic-purple); } - &[data-type='constant'], - &[data-type='const'] { + &.type-constant, + &.type-const { --label-theme: var(--symbolic-gray); } - &[data-type='decorator'] { + &.type-decorator { --label-theme: var(--symbolic-blue); } - &[data-type='directive'] { + &.type-directive { --label-theme: var(--symbolic-pink); } - &[data-type='element'] { + &.type-element { --label-theme: var(--symbolic-orange); } - &[data-type='enum'] { + &.type-enum { --label-theme: var(--symbolic-yellow); } - &[data-type='function'] { + &.type-function { --label-theme: var(--symbolic-green); } - &[data-type='interface'] { + &.type-interface { --label-theme: var(--symbolic-cyan); } - &[data-type='pipe'] { + &.type-pipe { --label-theme: var(--symbolic-teal); } - &[data-type='ng_module'] { + &.type-ng_module { --label-theme: var(--symbolic-brown); } - &[data-type='type_alias'] { + &.type-type_alias { --label-theme: var(--symbolic-lime); } - &[data-type='block'] { + &.type-block { --label-theme: var(--vivid-pink); } - &[data-type='developer_preview'], - &[data-type='deprecated'] { + &.type-developer_preview, + &.type-deprecated { --label-theme: var(--hot-red); } } diff --git a/adev/src/app/features/references/api-item-label/api-item-label.component.html b/adev/src/app/features/references/api-item-label/api-item-label.component.html deleted file mode 100644 index 78ca49801f563..0000000000000 --- a/adev/src/app/features/references/api-item-label/api-item-label.component.html +++ /dev/null @@ -1 +0,0 @@ -{{ type() | adevApiLabel: mode() }} diff --git a/adev/src/app/features/references/api-item-label/api-item-label.component.spec.ts b/adev/src/app/features/references/api-item-label/api-item-label.component.spec.ts index 4ea941ac85c76..154789f0b702d 100644 --- a/adev/src/app/features/references/api-item-label/api-item-label.component.spec.ts +++ b/adev/src/app/features/references/api-item-label/api-item-label.component.spec.ts @@ -33,19 +33,8 @@ describe('ApiItemLabel', () => { expect(label).toBe('C'); }); - it('should display full label for Class when labelMode equals full', () => { + it('should display short label for Class', () => { fixture.componentRef.setInput('type', ApiItemType.CLASS); - fixture.componentRef.setInput('mode', 'full'); - fixture.detectChanges(); - - const label = fixture.nativeElement.innerText; - - expect(label).toBe('Class'); - }); - - it('should display short label for Class when labelMode equals short', () => { - fixture.componentRef.setInput('type', ApiItemType.CLASS); - fixture.componentRef.setInput('mode', 'short'); fixture.detectChanges(); const label = fixture.nativeElement.innerText; diff --git a/adev/src/app/features/references/api-item-label/api-item-label.component.ts b/adev/src/app/features/references/api-item-label/api-item-label.component.ts index 244bc008d7a18..1ed5e7cfe97b5 100644 --- a/adev/src/app/features/references/api-item-label/api-item-label.component.ts +++ b/adev/src/app/features/references/api-item-label/api-item-label.component.ts @@ -6,21 +6,21 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ChangeDetectionStrategy, Component, input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; import {ApiItemType} from '../interfaces/api-item-type'; -import {ApiLabel} from '../pipes/api-label.pipe'; +import {ApiLabel, shortLabelsMap} from '../pipes/api-label.pipe'; @Component({ selector: 'docs-api-item-label', - templateUrl: './api-item-label.component.html', + template: `{{ label() }}`, changeDetection: ChangeDetectionStrategy.OnPush, host: { - '[attr.data-type]': 'type()', - '[attr.data-mode]': 'mode()', + '[class]': `clazz()`, }, imports: [ApiLabel], }) export default class ApiItemLabel { readonly type = input.required(); - readonly mode = input.required<'short' | 'full'>(); + readonly label = computed(() => shortLabelsMap[this.type()]); + readonly clazz = computed(() => `type-${this.type()}`); } diff --git a/adev/src/app/features/references/pipes/api-label.pipe.ts b/adev/src/app/features/references/pipes/api-label.pipe.ts index 55998f1a2920a..766d6ff474243 100644 --- a/adev/src/app/features/references/pipes/api-label.pipe.ts +++ b/adev/src/app/features/references/pipes/api-label.pipe.ts @@ -14,39 +14,39 @@ import {ApiItemType} from '../interfaces/api-item-type'; name: 'adevApiLabel', }) export class ApiLabel implements PipeTransform { - private readonly shortLabelsMap: Record = { - [ApiItemType.BLOCK]: 'B', - [ApiItemType.CLASS]: 'C', - [ApiItemType.CONST]: 'K', - [ApiItemType.DECORATOR]: '@', - [ApiItemType.DIRECTIVE]: 'D', - [ApiItemType.ELEMENT]: 'El', - [ApiItemType.ENUM]: 'E', - [ApiItemType.FUNCTION]: 'F', - [ApiItemType.INTERFACE]: 'I', - [ApiItemType.PIPE]: 'P', - [ApiItemType.NG_MODULE]: 'M', - [ApiItemType.TYPE_ALIAS]: 'T', - [ApiItemType.INITIALIZER_API_FUNCTION]: 'IA', - }; - - private readonly fullLabelsMap: Record = { - [ApiItemType.BLOCK]: 'Block', - [ApiItemType.CLASS]: 'Class', - [ApiItemType.CONST]: 'Const', - [ApiItemType.DECORATOR]: 'Decorator', - [ApiItemType.DIRECTIVE]: 'Directive', - [ApiItemType.ELEMENT]: 'Element', - [ApiItemType.ENUM]: 'Enum', - [ApiItemType.FUNCTION]: 'Function', - [ApiItemType.INTERFACE]: 'Interface', - [ApiItemType.PIPE]: 'Pipe', - [ApiItemType.NG_MODULE]: 'Module', - [ApiItemType.TYPE_ALIAS]: 'Type Alias', - [ApiItemType.INITIALIZER_API_FUNCTION]: 'Initializer API', - }; - transform(value: ApiItemType, labelType: 'short' | 'full'): string { - return labelType === 'full' ? this.fullLabelsMap[value] : this.shortLabelsMap[value]; + return labelType === 'full' ? fullLabelsMap[value] : shortLabelsMap[value]; } } + +export const shortLabelsMap: Record = { + [ApiItemType.BLOCK]: 'B', + [ApiItemType.CLASS]: 'C', + [ApiItemType.CONST]: 'K', + [ApiItemType.DECORATOR]: '@', + [ApiItemType.DIRECTIVE]: 'D', + [ApiItemType.ELEMENT]: 'El', + [ApiItemType.ENUM]: 'E', + [ApiItemType.FUNCTION]: 'F', + [ApiItemType.INTERFACE]: 'I', + [ApiItemType.PIPE]: 'P', + [ApiItemType.NG_MODULE]: 'M', + [ApiItemType.TYPE_ALIAS]: 'T', + [ApiItemType.INITIALIZER_API_FUNCTION]: 'IA', +}; + +export const fullLabelsMap: Record = { + [ApiItemType.BLOCK]: 'Block', + [ApiItemType.CLASS]: 'Class', + [ApiItemType.CONST]: 'Const', + [ApiItemType.DECORATOR]: 'Decorator', + [ApiItemType.DIRECTIVE]: 'Directive', + [ApiItemType.ELEMENT]: 'Element', + [ApiItemType.ENUM]: 'Enum', + [ApiItemType.FUNCTION]: 'Function', + [ApiItemType.INTERFACE]: 'Interface', + [ApiItemType.PIPE]: 'Pipe', + [ApiItemType.NG_MODULE]: 'Module', + [ApiItemType.TYPE_ALIAS]: 'Type Alias', + [ApiItemType.INITIALIZER_API_FUNCTION]: 'Initializer API', +}; From def5d4c81eb9f6ac09e8283976ea51072e0b44f0 Mon Sep 17 00:00:00 2001 From: Shai Reznik Date: Sat, 21 Dec 2024 16:25:17 +0200 Subject: [PATCH 03/36] docs: fix wrong link (#59277) PR Close #59277 --- adev/src/content/guide/testing/utility-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/guide/testing/utility-apis.md b/adev/src/content/guide/testing/utility-apis.md index 2bca12952195e..a76e4a9851375 100644 --- a/adev/src/content/guide/testing/utility-apis.md +++ b/adev/src/content/guide/testing/utility-apis.md @@ -105,7 +105,7 @@ Here are the most useful methods for testers. | `autoDetectChanges` | Set this to `true` when you want the fixture to detect changes automatically.
    When autodetect is `true`, the test fixture calls `detectChanges` immediately after creating the component. Then it listens for pertinent zone events and calls `detectChanges` accordingly. When your test code modifies component property values directly, you probably still have to call `fixture.detectChanges` to trigger data binding updates.
    The default is `false`. Testers who prefer fine control over test behavior tend to keep it `false`. | | `checkNoChanges` | Do a change detection run to make sure there are no pending changes. Throws an exceptions if there are. | | `isStable` | If the fixture is currently *stable*, returns `true`. If there are async tasks that have not completed, returns `false`. | -| `whenStable` | Returns a promise that resolves when the fixture is stable.
    To resume testing after completion of asynchronous activity or asynchronous change detection, hook that promise. See [whenStable](guide/testing/components-scenarios#when-stable). | +| `whenStable` | Returns a promise that resolves when the fixture is stable.
    To resume testing after completion of asynchronous activity or asynchronous change detection, hook that promise. See [whenStable](guide/testing/components-scenarios#whenstable). | | `destroy` | Trigger component destruction. | #### `DebugElement` From c197ee000fafbb6ddb4dd6cf4c5620cc92069a9f Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Fri, 27 Dec 2024 11:32:14 -0800 Subject: [PATCH 04/36] docs: add a note about Incremental Hydration to the `@defer` docs (#59320) PR Close #59320 --- adev/src/content/guide/templates/defer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adev/src/content/guide/templates/defer.md b/adev/src/content/guide/templates/defer.md index 96b6c86995829..41dd28e9584bb 100644 --- a/adev/src/content/guide/templates/defer.md +++ b/adev/src/content/guide/templates/defer.md @@ -323,9 +323,9 @@ it('should render a defer block in different states', async () => { ## How does `@defer` work with server-side rendering (SSR) and static-site generation (SSG)? -When rendering an application on the server (either using SSR or SSG), defer blocks always render their `@placeholder` (or nothing if a placeholder is not specified). +By default, when rendering an application on the server (either using SSR or SSG), defer blocks always render their `@placeholder` (or nothing if a placeholder is not specified) and triggers are not invoked. On the client, the content of the `@placeholder` is hydrated and triggers are activated. -Triggers are ignored on the server. +To render the main content of `@defer` blocks on the server (both SSR and SSG), you can enable [the Incremental Hydration feature](/guide/incremental-hydration) and configure `hydrate` triggers for the necessary blocks. ## Best practices for deferring views From a29d855e9e7fa3501f93148c67aedf1a1816870e Mon Sep 17 00:00:00 2001 From: Juan Urquiza Date: Fri, 27 Dec 2024 18:49:14 -0500 Subject: [PATCH 05/36] docs: changed name class (#59322) PR Close #59322 --- adev/src/content/guide/ngmodules/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/guide/ngmodules/overview.md b/adev/src/content/guide/ngmodules/overview.md index d98258103ca7d..1e901e7784e2a 100644 --- a/adev/src/content/guide/ngmodules/overview.md +++ b/adev/src/content/guide/ngmodules/overview.md @@ -178,7 +178,7 @@ import {platformBrowser} from '@angular/platform-browser'; @NgModule({ bootstrap: [MyApplication], }) -export class MyApplciationModule { } +export class MyApplicationModule { } platformBrowser().bootstrapModule(MyApplicationModule); ``` From 1614cfdbb0acf2f000537af81e0d83848718fd94 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sat, 28 Dec 2024 15:49:57 +0100 Subject: [PATCH 06/36] docs: add TS support for 19.1 (#59326) PR Close #59326 --- adev/src/content/reference/versions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/adev/src/content/reference/versions.md b/adev/src/content/reference/versions.md index 112a15aab909d..71c8cacebe843 100644 --- a/adev/src/content/reference/versions.md +++ b/adev/src/content/reference/versions.md @@ -9,6 +9,7 @@ This table covers [Angular versions under active support](reference/releases#act | Angular | Node.js | TypeScript | RxJS | | ------------------ | ------------------------------------ | -------------- | ------------------ | +| 19.1.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.5.0 <5.8.0 | ^6.5.3 \|\| ^7.4.0 | | 19.0.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.5.0 <5.7.0 | ^6.5.3 \|\| ^7.4.0 | | 18.1.x \|\| 18.2.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.6.0 | ^6.5.3 \|\| ^7.4.0 | | 18.0.x | ^18.19.1 \|\| ^20.11.1 \|\| ^22.0.0 | >=5.4.0 <5.5.0 | ^6.5.3 \|\| ^7.4.0 | From 16d4ea32211069901a7a8f7e5ff049442e01ecff Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sun, 5 Jan 2025 19:13:38 +0100 Subject: [PATCH 07/36] docs: fix link to API entry `AngularAppEngine` (#59367) fixes #59342 PR Close #59367 --- adev/src/content/guide/hybrid-rendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/guide/hybrid-rendering.md b/adev/src/content/guide/hybrid-rendering.md index d29e7ce5a097b..38ce6643ae53f 100644 --- a/adev/src/content/guide/hybrid-rendering.md +++ b/adev/src/content/guide/hybrid-rendering.md @@ -271,7 +271,7 @@ IMPORTANT: The above tokens will be `null` in the following scenarios: ## Configuring a non-Node.js Server -The `@angular/ssr` provides essential APIs for server-side rendering your Angular application on platforms other than Node.js. It leverages the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects from the Web API, enabling you to integrate Angular SSR into various server environments. For detailed information and examples, refer to the [`@angular/ssr` API reference](api/ssr/node/AngularAppEngine). +The `@angular/ssr` provides essential APIs for server-side rendering your Angular application on platforms other than Node.js. It leverages the standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects from the Web API, enabling you to integrate Angular SSR into various server environments. For detailed information and examples, refer to the [`@angular/ssr` API reference](api/ssr/AngularAppEngine). ```typescript // server.ts From ad65da61393022a8ec4e463ab3225e14e022637d Mon Sep 17 00:00:00 2001 From: Meehdi Date: Sun, 22 Dec 2024 13:14:09 +0100 Subject: [PATCH 08/36] docs: fix mermaid polygon node text visibility in dark mode (#59285) Fix visibility issue with text inside polygon nodes in mermaid diagrams when using dark mode theme to ensure proper contrast and readability PR Close #59285 --- adev/shared-docs/styles/docs/_mermaid.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/shared-docs/styles/docs/_mermaid.scss b/adev/shared-docs/styles/docs/_mermaid.scss index 27a30aafce31e..77694f877623c 100644 --- a/adev/shared-docs/styles/docs/_mermaid.scss +++ b/adev/shared-docs/styles/docs/_mermaid.scss @@ -58,7 +58,7 @@ fill: var(--page-background) !important; } - .nodeLabel { + .nodeLabel:not(.node:has(polygon) .nodeLabel) { fill: var(--primary-contrast) !important; color: var(--primary-contrast) !important; } From d54deb2ba64c7761206af2f9c75f557cff37dca7 Mon Sep 17 00:00:00 2001 From: Samuel Perez <101076110+Gitrhyme@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:20:51 -0500 Subject: [PATCH 09/36] docs: Add NG0750 to errors list (#59265) This PR adds error NG0750 to Error Encyclopedia. Update adev/src/content/reference/errors/NG0750.md Co-authored-by: Andrew Kushnir <43554145+AndrewKushnir@users.noreply.github.com> Update adev/src/content/reference/errors/NG0750.md Co-authored-by: Andrew Kushnir <43554145+AndrewKushnir@users.noreply.github.com> PR Close #59265 --- adev/src/app/sub-navigation-data.ts | 5 +++++ adev/src/content/reference/errors/NG0750.md | 6 ++++++ adev/src/content/reference/errors/overview.md | 3 ++- goldens/public-api/core/errors.api.md | 2 +- packages/core/src/errors.ts | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 adev/src/content/reference/errors/NG0750.md diff --git a/adev/src/app/sub-navigation-data.ts b/adev/src/app/sub-navigation-data.ts index d08dbede14350..dcaa29f1f761b 100644 --- a/adev/src/app/sub-navigation-data.ts +++ b/adev/src/app/sub-navigation-data.ts @@ -1285,6 +1285,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'errors/NG05000', contentPath: 'reference/errors/NG05000', }, + { + label: 'NG0750: @defer dependencies failed to load', + path: 'errors/NG0750', + contentPath: 'reference/errors/NG0750', + }, { label: 'NG6100: NgModule.id Set to module.id anti-pattern', path: 'errors/NG6100', diff --git a/adev/src/content/reference/errors/NG0750.md b/adev/src/content/reference/errors/NG0750.md new file mode 100644 index 0000000000000..0fff3453249b5 --- /dev/null +++ b/adev/src/content/reference/errors/NG0750.md @@ -0,0 +1,6 @@ +# @defer dependencies failed to load + +This error occurs when loading dependencies for a `@defer` block fails (typically due to poor network conditions) and no `@error` block has been configured to handle the failure state. Having no `@error` block in this scenario may create a poor user experience. + +## Debugging the error +Verify that you added `@error` blocks to your `@defer` blocks to handle failure states. \ No newline at end of file diff --git a/adev/src/content/reference/errors/overview.md b/adev/src/content/reference/errors/overview.md index 5af4d62b59dbc..05c41bbaf1a7d 100644 --- a/adev/src/content/reference/errors/overview.md +++ b/adev/src/content/reference/errors/overview.md @@ -21,6 +21,7 @@ | `NG0505` | [No hydration info in server response](errors/NG0505) | | `NG0506` | [NgZone remains unstable](errors/NG0506) | | `NG0507` | [HTML content was altered after SSR](errors/NG0507) | +| `NG0750` | [@defer dependencies failed to load](errors/NG0750) | | `NG0910` | [Unsafe bindings on an iframe element](errors/NG0910) | | `NG0912` | [Component ID generation collision](errors/NG0912) | | `NG0955` | [Track expression resulted in duplicated keys for a given collection](errors/NG0955) | @@ -29,7 +30,7 @@ | `NG01203` | [Missing value accessor](errors/NG01203) | | `NG02200` | [Missing Iterable Differ](errors/NG02200) | | `NG02800` | [JSONP support in HttpClient configuration](errors/NG02800) | -| `NG05000` | [Hydration with unsupported Zone.js instance.](errors/NG05000) | +| `NG05000` | [Hydration with unsupported Zone.js instance.](errors/NG05000) | | `NG05104` | [Root element was not found.](errors/NG05104) | ## Compiler errors diff --git a/goldens/public-api/core/errors.api.md b/goldens/public-api/core/errors.api.md index a36840c787df0..4e63687cdd008 100644 --- a/goldens/public-api/core/errors.api.md +++ b/goldens/public-api/core/errors.api.md @@ -29,7 +29,7 @@ export const enum RuntimeErrorCode { // (undocumented) CYCLIC_DI_DEPENDENCY = -200, // (undocumented) - DEFER_LOADING_FAILED = 750, + DEFER_LOADING_FAILED = -750, // (undocumented) DUPLICATE_DIRECTIVE = 309, // (undocumented) diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index d18f24cd141b0..f762556a01733 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -97,7 +97,7 @@ export const enum RuntimeErrorCode { MISSING_LOCALE_DATA = 701, // Defer errors (750-799 range) - DEFER_LOADING_FAILED = 750, + DEFER_LOADING_FAILED = -750, // standalone errors IMPORT_PROVIDERS_FROM_STANDALONE = 800, From 863d1613874f5de53b02a33e4f68c2856ad65148 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Sat, 4 Jan 2025 01:39:39 +0000 Subject: [PATCH 10/36] build: update dependency ngx-progressbar to v14 (#59361) See associated pull request for more information. PR Close #59361 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 786bdbe827a24..8f71b4230fd3f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "magic-string": "^0.30.8", "memo-decorator": "^2.0.1", "ngx-flamegraph": "0.0.12", - "ngx-progressbar": "^13.0.0", + "ngx-progressbar": "^14.0.0", "open-in-idx": "^0.1.1", "protractor": "^7.0.0", "reflect-metadata": "^0.2.0", diff --git a/yarn.lock b/yarn.lock index b566b0111d117..c0b5766d235d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12557,12 +12557,12 @@ ngx-flamegraph@0.0.12: dependencies: tslib "^2.0.0" -ngx-progressbar@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/ngx-progressbar/-/ngx-progressbar-13.0.0.tgz#8054ef717218de9778fe4a42f502141438c376e7" - integrity sha512-vzycISa9kddf2eo1qF7WSrHPLFRR0dia2NaxYFCSnvspJ30D69OBN8qV9gZ0BLU+AQib5I3CGhDbjF6QqvKtzA== +ngx-progressbar@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/ngx-progressbar/-/ngx-progressbar-14.0.0.tgz#941dd19a20bf1846f94685cda5f8aa030711934f" + integrity sha512-tDj7h5F2aSI4/XaJjs50FnELVe6qFqyz3vVq22acacd3oDW2EyJB4c+IYaxMf5972OdTw0WL4n6UwQ3dqC+gCA== dependencies: - tslib "^2.0.0" + tslib "^2.3.0" nice-try@^1.0.4: version "1.0.5" From e30eae13656878cd61a1f859d71e0cdc5b413512 Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Sat, 4 Jan 2025 01:37:56 +0000 Subject: [PATCH 11/36] build: update io_bazel_rules_sass digest to aff53ca (#59360) See associated pull request for more information. PR Close #59360 --- WORKSPACE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 8c22277c9ffb2..a5e48b4f1e27b 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -143,10 +143,10 @@ cldr_xml_data_repository( # sass rules http_archive( name = "io_bazel_rules_sass", - sha256 = "1b11ce2e7ced21522c83e6c64e9256eb18cd8d89afb8a69e18e6f3e2d3a138a8", - strip_prefix = "rules_sass-df7d2a95e1fa6e15bdb8a796756e276b2289f29a", + sha256 = "0eae9a0c840e1e0d0b9ace056f8bde06384315315c4e2ebdb5cec722d1d4134b", + strip_prefix = "rules_sass-aff53ca13ff2af82d323adb02a83c45a301e9ae8", urls = [ - "https://github.com/bazelbuild/rules_sass/archive/df7d2a95e1fa6e15bdb8a796756e276b2289f29a.zip", + "https://github.com/bazelbuild/rules_sass/archive/aff53ca13ff2af82d323adb02a83c45a301e9ae8.zip", ], ) From a62905f04a8e25abe59587649702d8078fbf6d9b Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Mon, 6 Jan 2025 08:12:44 +0000 Subject: [PATCH 12/36] build: update all non-major dependencies (#59298) See associated pull request for more information. PR Close #59298 --- .github/workflows/pr.yml | 2 +- package.json | 4 ++-- packages/localize/package.json | 2 +- yarn.lock | 19 +++++++++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 51312a3dd6e14..e524cb9f57aa5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -95,7 +95,7 @@ jobs: - name: Run CI tests for framework run: yarn tsx ./scripts/build/build-packages-dist.mts - name: Archive build artifacts - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: pr-artifacts-${{ github.event.number }} path: dist/packages-dist/ diff --git a/package.json b/package.json index 8f71b4230fd3f..fed24267ab755 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@types/babel__core": "7.20.5", "@types/babel__generator": "7.6.8", "@types/bluebird": "^3.5.27", - "@types/chrome": "^0.0.287", + "@types/chrome": "^0.0.290", "@types/convert-source-map": "^2.0.0", "@types/diff": "^6.0.0", "@types/dom-view-transitions": "^1.0.1", @@ -198,7 +198,7 @@ "cldrjs": "0.5.5", "conventional-changelog": "^6.0.0", "emoji-regex": "^10.3.0", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "fflate": "^0.8.2", "firebase-tools": "^13.0.0", "gsap": "^3.12.3", diff --git a/packages/localize/package.json b/packages/localize/package.json index cea20733ea280..514ab7b16deca 100644 --- a/packages/localize/package.json +++ b/packages/localize/package.json @@ -35,7 +35,7 @@ "dependencies": { "@babel/core": "7.26.0", "@types/babel__core": "7.20.5", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "yargs": "^17.2.1" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index c0b5766d235d7..84ef4df1d11ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3658,10 +3658,10 @@ resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== -"@types/chrome@^0.0.287": - version "0.0.287" - resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.287.tgz#239969b1195b441836d2137125543b5241c41157" - integrity sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ== +"@types/chrome@^0.0.290": + version "0.0.290" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.290.tgz#570e511360d1b92cf24773af0c3b23b6e1f13152" + integrity sha512-N92vsAdlwoWameDQ8D4K0EZXXvxsJ1+gJg+4TWjUUsZ6gpontVmwl1XVtysA3mso45Fcn5UPiX/yqiT8GcBV3A== dependencies: "@types/filesystem" "*" "@types/har-format" "*" @@ -8553,6 +8553,17 @@ fast-glob@3.3.2, fast-glob@^3.2.9, fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" From 895a90e1162f293d3dd52d3b1e827867d926aefb Mon Sep 17 00:00:00 2001 From: Angular Robot Date: Tue, 24 Dec 2024 06:13:05 +0000 Subject: [PATCH 13/36] build: update scorecard action dependencies (#59299) See associated pull request for more information. PR Close #59299 --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d9ba79011e4a2..970aefa63a765 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -39,7 +39,7 @@ jobs: # Upload the results as artifacts. - name: 'Upload artifact' - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 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@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 + uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 with: sarif_file: results.sarif From aa835da9a2092aa58944ea6d69157eeaaf9e7973 Mon Sep 17 00:00:00 2001 From: arturovt Date: Wed, 2 Oct 2024 00:28:51 +0300 Subject: [PATCH 14/36] refactor(docs-infra): allow playground component to be cleaned up properly (#58040) In this commit, we're replacing the `async-await` style in the playground component with the `from()` observable, which allows us to invert a dependency and avoid memory leaks. Because an `async` function has a closure, just like any other function in JavaScript, using `await` captures `this` until the promise is resolved. PR Close #58040 --- .../playground/playground.component.spec.ts | 48 ++++++--------- .../playground/playground.component.ts | 59 ++++++++++++------- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/adev/src/app/features/playground/playground.component.spec.ts b/adev/src/app/features/playground/playground.component.spec.ts index 5979b2026432c..d94a1c37c1fa2 100644 --- a/adev/src/app/features/playground/playground.component.spec.ts +++ b/adev/src/app/features/playground/playground.component.spec.ts @@ -6,58 +6,46 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Component} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {WINDOW} from '@angular/docs'; -import { - EMBEDDED_EDITOR_SELECTOR, - EmbeddedEditor, - NodeRuntimeSandbox, - EmbeddedTutorialManager, -} from '../../editor'; +import {NodeRuntimeSandbox, EmbeddedTutorialManager} from '../../editor'; -import {mockAsyncProvider} from '../../core/services/inject-async'; import TutorialPlayground from './playground.component'; -@Component({ - selector: EMBEDDED_EDITOR_SELECTOR, - template: '
    FakeEmbeddedEditor
    ', -}) -class FakeEmbeddedEditor {} - -class FakeNodeRuntimeSandbox { - init() { - return Promise.resolve(); - } -} - describe('TutorialPlayground', () => { let component: TutorialPlayground; let fixture: ComponentFixture; + const fakeWindow = { + location: { + search: window.location.search, + }, + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [TutorialPlayground], providers: [ + { + provide: WINDOW, + useValue: fakeWindow, + }, { provide: EmbeddedTutorialManager, useValue: { fetchAndSetTutorialFiles: () => {}, }, }, - mockAsyncProvider(NodeRuntimeSandbox, FakeNodeRuntimeSandbox), + { + provide: NodeRuntimeSandbox, + useVaue: { + init: () => {}, + }, + }, ], }); - TestBed.overrideComponent(TutorialPlayground, { - remove: { - imports: [EmbeddedEditor], - }, - add: { - imports: [FakeEmbeddedEditor], - }, - }); - fixture = TestBed.createComponent(TutorialPlayground); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/adev/src/app/features/playground/playground.component.ts b/adev/src/app/features/playground/playground.component.ts index 312443d42dc4e..0f16e388755ef 100644 --- a/adev/src/app/features/playground/playground.component.ts +++ b/adev/src/app/features/playground/playground.component.ts @@ -12,18 +12,21 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + DestroyRef, EnvironmentInjector, PLATFORM_ID, Type, inject, } from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu'; import {IconComponent, PlaygroundTemplate} from '@angular/docs'; +import {forkJoin, switchMap, tap} from 'rxjs'; import {injectAsync} from '../../core/services/inject-async'; -import {EmbeddedTutorialManager} from '../../editor/index'; +import type {EmbeddedTutorialManager, NodeRuntimeSandbox} from '../../editor/index'; import PLAYGROUND_ROUTE_DATA_JSON from '../../../../src/assets/tutorials/playground/routes.json'; -import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu'; @Component({ selector: 'adev-playground', @@ -38,34 +41,44 @@ import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu'; }) export default class PlaygroundComponent implements AfterViewInit { private readonly changeDetectorRef = inject(ChangeDetectorRef); - private readonly embeddedTutorialManager = inject(EmbeddedTutorialManager); private readonly environmentInjector = inject(EnvironmentInjector); - private readonly platformId = inject(PLATFORM_ID); + private readonly destroyRef = inject(DestroyRef); + private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); readonly templates = PLAYGROUND_ROUTE_DATA_JSON.templates; readonly defaultTemplate = PLAYGROUND_ROUTE_DATA_JSON.defaultTemplate; readonly starterTemplate = PLAYGROUND_ROUTE_DATA_JSON.starterTemplate; + protected nodeRuntimeSandbox?: NodeRuntimeSandbox; protected embeddedEditorComponent?: Type; protected selectedTemplate: PlaygroundTemplate = this.defaultTemplate; - async ngAfterViewInit(): Promise { - if (isPlatformBrowser(this.platformId)) { - const [embeddedEditorComponent, nodeRuntimeSandbox] = await Promise.all([ - import('../../editor/index').then((c) => c.EmbeddedEditor), - injectAsync(this.environmentInjector, () => - import('../../editor/index').then((c) => c.NodeRuntimeSandbox), - ), - ]); - - this.embeddedEditorComponent = embeddedEditorComponent; - - this.changeDetectorRef.markForCheck(); - - await this.loadTemplate(this.defaultTemplate.path); - - await nodeRuntimeSandbox.init(); + ngAfterViewInit(): void { + if (!this.isBrowser) { + return; } + + // If using `async-await`, `this` will be captured until the function is executed + // and completed, which can lead to a memory leak if the user navigates away from + // the playground component to another page. + forkJoin({ + nodeRuntimeSandbox: injectAsync(this.environmentInjector, () => + import('../../editor/index').then((c) => c.NodeRuntimeSandbox), + ), + embeddedEditorComponent: import('../../editor/index').then((c) => c.EmbeddedEditor), + }) + .pipe( + tap(({nodeRuntimeSandbox, embeddedEditorComponent}) => { + this.nodeRuntimeSandbox = nodeRuntimeSandbox; + this.embeddedEditorComponent = embeddedEditorComponent; + }), + switchMap(() => this.loadTemplate(this.defaultTemplate.path)), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(() => { + this.changeDetectorRef.markForCheck(); + this.nodeRuntimeSandbox!.init(); + }); } async newProject() { @@ -78,6 +91,10 @@ export default class PlaygroundComponent implements AfterViewInit { } private async loadTemplate(tutorialPath: string) { - await this.embeddedTutorialManager.fetchAndSetTutorialFiles(tutorialPath); + const embeddedTutorialManager = await injectAsync(this.environmentInjector, () => + import('../../editor/index').then((c) => c.EmbeddedTutorialManager), + ); + + await embeddedTutorialManager.fetchAndSetTutorialFiles(tutorialPath); } } From e5866eed2e7e0b58c627a03c19c733c7692149c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandor=20Drie=C3=ABnhuizen?= Date: Mon, 23 Dec 2024 00:30:07 +0100 Subject: [PATCH 15/36] refactor(compiler): incorrect spelling in for loop parse error message (#59289) 'parameter' was spelled as 'paramater'. Fix spelling error in Update r3_control_flow.ts 'parameter' was spelled as 'paramater'. Fix spelling error in r3_template_transform_spec.ts 'parameter' was spelled as 'paramater'. PR Close #59289 --- packages/compiler/src/render3/r3_control_flow.ts | 4 ++-- .../compiler/test/render3/r3_template_transform_spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/render3/r3_control_flow.ts b/packages/compiler/src/render3/r3_control_flow.ts index acbcccd64b250..9a13ddb52e442 100644 --- a/packages/compiler/src/render3/r3_control_flow.ts +++ b/packages/compiler/src/render3/r3_control_flow.ts @@ -386,7 +386,7 @@ function parseForLoopParameters( } errors.push( - new ParseError(param.sourceSpan, `Unrecognized @for loop paramater "${param.expression}"`), + new ParseError(param.sourceSpan, `Unrecognized @for loop parameter "${param.expression}"`), ); } @@ -614,7 +614,7 @@ function parseConditionalBlockParameters( errors.push( new ParseError( param.sourceSpan, - `Unrecognized conditional paramater "${param.expression}"`, + `Unrecognized conditional parameter "${param.expression}"`, ), ); } else if (block.name !== 'if') { diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts index 967be432636a9..b12805c1be4c9 100644 --- a/packages/compiler/test/render3/r3_template_transform_spec.ts +++ b/packages/compiler/test/render3/r3_template_transform_spec.ts @@ -1979,7 +1979,7 @@ describe('R3 template transform', () => { it('should report unrecognized for loop parameters', () => { expect(() => parse(`@for (a of b; foo bar) {hello}`)).toThrowError( - /Unrecognized @for loop paramater "foo bar"/, + /Unrecognized @for loop parameter "foo bar"/, ); }); @@ -2257,7 +2257,7 @@ describe('R3 template transform', () => { parse(` @if (foo; bar) {hello} `), - ).toThrowError(/Unrecognized conditional paramater "bar"/); + ).toThrowError(/Unrecognized conditional parameter "bar"/); }); it('should report an unknown parameter in an else if block', () => { @@ -2265,7 +2265,7 @@ describe('R3 template transform', () => { parse(` @if (foo) {hello} @else if (bar; baz) {goodbye} `), - ).toThrowError(/Unrecognized conditional paramater "baz"/); + ).toThrowError(/Unrecognized conditional parameter "baz"/); }); it('should report an if block that has multiple `as` expressions', () => { From 19ec8266d1ef01ff59dfad55bc54e0f83bb87e67 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Fri, 20 Dec 2024 17:22:23 -0800 Subject: [PATCH 16/36] refactor(platform-server): reduce timeout used in tests (#59275) This commit updates the timeout used in the incremental hydration tests from `101` -> `10` ms, which allows to speed up tests by ~20% (12.5 -> 10 seconds locally). PR Close #59275 --- .../test/incremental_hydration_spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/platform-server/test/incremental_hydration_spec.ts b/packages/platform-server/test/incremental_hydration_spec.ts index 44e0c8b4665b1..9b720c17397d4 100644 --- a/packages/platform-server/test/incremental_hydration_spec.ts +++ b/packages/platform-server/test/incremental_hydration_spec.ts @@ -65,8 +65,8 @@ function dynamicImportOf(type: T, timeout = 0): Promise { * Helper function to await all pending dynamic imports * emulated using `dynamicImportOf` function. */ -function allPendingDynamicImports() { - return dynamicImportOf(null, 101); +function allPendingDynamicImports(timeout?: number) { + return dynamicImportOf(null, timeout ?? 10); } describe('platform-server partial hydration integration', () => { @@ -2364,10 +2364,12 @@ describe('platform-server partial hydration integration', () => { location = inject(Location); } + const dynamicImportTimeout = 5; // ms + const deferDepsInterceptor = { intercept() { return () => { - return [dynamicImportOf(DeferredCmp, 100)]; + return [dynamicImportOf(DeferredCmp, dynamicImportTimeout)]; }; }, }; @@ -2397,10 +2399,12 @@ describe('platform-server partial hydration integration', () => { const routeLink = doc.getElementById('route-link')!; routeLink.click(); - await allPendingDynamicImports(); + // Wait a bit longer than a timeout used to emulate a dynamic import. + await allPendingDynamicImports(dynamicImportTimeout * 2); appRef.tick(); - await allPendingDynamicImports(); + // Wait a bit longer than a timeout used to emulate a dynamic import. + await allPendingDynamicImports(dynamicImportTimeout * 2); await appRef.whenStable(); expect(location.path()).toBe('/other/thing/stuff'); From b2fcad8c1d483fbda12252cbf0c870cdfde1c8b1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 2 Jan 2025 13:28:03 +0100 Subject: [PATCH 17/36] refactor(compiler-cli): expose diagnostic error code (#59353) Exports the error codes so that they can be reused. PR Close #59353 --- packages/compiler-cli/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index f82909c8838bd..420851b6eb136 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -42,6 +42,6 @@ export * from './src/ngtsc/docs/src/entities'; export * from './src/ngtsc/docs'; // Exposed for usage in 1P Angular plugin. -export {isLocalCompilationDiagnostics} from './src/ngtsc/diagnostics'; +export {isLocalCompilationDiagnostics, ErrorCode, ngErrorCode} from './src/ngtsc/diagnostics'; setFileSystem(new NodeJSFileSystem()); From d6ca669bc98704035ae49207dfbbacecbe1af989 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 2 Jan 2025 13:32:10 +0100 Subject: [PATCH 18/36] refactor(migrations): allow compiler options to be customized in tsurge (#59353) Allows for user-defined options to be passed in when creating a program in tsurge. PR Close #59353 --- .../core/schematics/utils/tsurge/base_migration.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/schematics/utils/tsurge/base_migration.ts b/packages/core/schematics/utils/tsurge/base_migration.ts index bc20ed12f7e21..516d5cf9d9ffc 100644 --- a/packages/core/schematics/utils/tsurge/base_migration.ts +++ b/packages/core/schematics/utils/tsurge/base_migration.ts @@ -7,9 +7,10 @@ */ import {absoluteFrom, FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; +import {getRootDirs} from '@angular/compiler-cli/src/ngtsc/util/src/typescript'; import {isShim} from '@angular/compiler-cli/src/ngtsc/shims'; import {BaseProgramInfo, ProgramInfo} from './program_info'; -import {getRootDirs} from '@angular/compiler-cli/src/ngtsc/util/src/typescript'; import {Serializable} from './helpers/serializable'; import {createBaseProgramInfo} from './helpers/create_program'; @@ -40,8 +41,12 @@ export abstract class TsurgeBaseMigration Date: Fri, 3 Jan 2025 09:06:49 +0100 Subject: [PATCH 19/36] feat(migrations): add schematic to clean up unused imports (#59353) In v19 we added a warning about unused standalone imports, however we didn't do anything about existing code which means that users have to clean it up manually. These changes add the `ng g @angular/core:cleanup-unused-imports` schematic which will remove the unused dependencies automatically. There isn't any new detection code since all the manipulations are based on the produced diagnostics, but there's a bit of code to remove the import declarations from the file as well. Fixes #58849. PR Close #59353 --- packages/core/schematics/BUILD.bazel | 5 +- packages/core/schematics/collection.json | 5 + .../cleanup-unused-imports/BUILD.bazel | 32 ++ .../cleanup-unused-imports/README.md | 30 ++ .../cleanup-unused-imports/index.ts | 91 +++++ .../cleanup-unused-imports/schema.json | 7 + .../unused_imports_migration.ts | 337 ++++++++++++++++++ packages/core/schematics/test/BUILD.bazel | 1 + .../cleanup_unused_imports_migration_spec.ts | 256 +++++++++++++ 9 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel create mode 100644 packages/core/schematics/ng-generate/cleanup-unused-imports/README.md create mode 100644 packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts create mode 100644 packages/core/schematics/ng-generate/cleanup-unused-imports/schema.json create mode 100644 packages/core/schematics/ng-generate/cleanup-unused-imports/unused_imports_migration.ts create mode 100644 packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 4cbcb550b67c2..7180546febbc8 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -1,5 +1,5 @@ -load("//tools:defaults.bzl", "pkg_npm") load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") +load("//tools:defaults.bzl", "pkg_npm") exports_files([ "tsconfig.json", @@ -13,6 +13,7 @@ pkg_npm( "collection.json", "migrations.json", "package.json", + "//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files", "//packages/core/schematics/ng-generate/control-flow-migration:static_files", "//packages/core/schematics/ng-generate/inject-migration:static_files", "//packages/core/schematics/ng-generate/output-migration:static_files", @@ -37,6 +38,7 @@ rollup_bundle( "//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration", "//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading", "//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration", + "//packages/core/schematics/ng-generate/cleanup-unused-imports:index.ts": "cleanup-unused-imports", "//packages/core/schematics/ng-generate/signals:index.ts": "signals", "//packages/core/schematics/ng-generate/signal-input-migration:index.ts": "signal-input-migration", "//packages/core/schematics/ng-generate/signal-queries-migration:index.ts": "signal-queries-migration", @@ -56,6 +58,7 @@ rollup_bundle( "//packages/core/schematics/migrations/explicit-standalone-flag", "//packages/core/schematics/migrations/pending-tasks", "//packages/core/schematics/migrations/provide-initializer", + "//packages/core/schematics/ng-generate/cleanup-unused-imports", "//packages/core/schematics/ng-generate/control-flow-migration", "//packages/core/schematics/ng-generate/inject-migration", "//packages/core/schematics/ng-generate/output-migration", diff --git a/packages/core/schematics/collection.json b/packages/core/schematics/collection.json index 9c84bce864094..1225726a17ffd 100644 --- a/packages/core/schematics/collection.json +++ b/packages/core/schematics/collection.json @@ -46,6 +46,11 @@ "description": "Combines all signals-related migrations into a single migration", "factory": "./bundles/signals#migrate", "schema": "./ng-generate/signals/schema.json" + }, + "cleanup-unused-imports": { + "description": "Removes unused imports from standalone components.", + "factory": "./bundles/cleanup-unused-imports#migrate", + "schema": "./ng-generate/cleanup-unused-imports/schema.json" } } } diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel b/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel new file mode 100644 index 0000000000000..fbd7ca8113a0d --- /dev/null +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel @@ -0,0 +1,32 @@ +load("//tools:defaults.bzl", "ts_library") + +package( + default_visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/google3:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], +) + +filegroup( + name = "static_files", + srcs = ["schema.json"], +) + +ts_library( + name = "cleanup-unused-imports", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + deps = [ + "//packages/compiler-cli", + "//packages/compiler-cli/private", + "//packages/compiler-cli/src/ngtsc/core:api", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/core/schematics/utils", + "//packages/core/schematics/utils/tsurge", + "//packages/core/schematics/utils/tsurge/helpers/angular_devkit", + "@npm//@angular-devkit/schematics", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/README.md b/packages/core/schematics/ng-generate/cleanup-unused-imports/README.md new file mode 100644 index 0000000000000..377c465ff43e2 --- /dev/null +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/README.md @@ -0,0 +1,30 @@ +## Cleanup unused imports migration +Automated migration that removes all unused standalone imports across the entire project. It can be +run using: + +```bash +ng generate @angular/core:cleanup-unused-imports +``` + +**Before:** +```typescript +import { Component } from '@angular/core'; +import { UnusedDirective } from './unused'; + +@Component({ + template: 'Hello', + imports: [UnusedDirective], +}) +export class MyComp {} +``` + +**After:** +```typescript +import { Component } from '@angular/core'; + +@Component({ + template: 'Hello', + imports: [], +}) +export class MyComp {} +``` diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts b/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts new file mode 100644 index 0000000000000..cb84a4e14e38c --- /dev/null +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Rule, SchematicsException} from '@angular-devkit/schematics'; + +import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; +import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; +import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; +import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; +import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; +import {CompilationUnitData, UnusedImportsMigration} from './unused_imports_migration'; + +export function migrate(): Rule { + return async (tree, context) => { + const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); + + if (!buildPaths.length && !testPaths.length) { + throw new SchematicsException( + 'Could not find any tsconfig file. Cannot clean up unused imports.', + ); + } + + const fs = new DevkitMigrationFilesystem(tree); + setFileSystem(fs); + + const migration = new UnusedImportsMigration(); + const unitResults: CompilationUnitData[] = []; + const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { + context.logger.info(`Preparing analysis for ${tsconfigPath}`); + + const baseInfo = migration.createProgram(tsconfigPath, fs); + const info = migration.prepareProgram(baseInfo); + + return {info, tsconfigPath}; + }); + + for (const {info, tsconfigPath} of programInfos) { + context.logger.info(`Scanning for unused imports using ${tsconfigPath}`); + unitResults.push(await migration.analyze(info)); + } + + const combined = await synchronouslyCombineUnitData(migration, unitResults); + if (combined === null) { + context.logger.error('Schematic failed unexpectedly with no analysis data'); + return; + } + + const globalMeta = await migration.globalMeta(combined); + const replacementsPerFile: Map = new Map(); + const {replacements} = await migration.migrate(globalMeta); + const changesPerFile = groupReplacementsByFile(replacements); + + for (const [file, changes] of changesPerFile) { + if (!replacementsPerFile.has(file)) { + replacementsPerFile.set(file, changes); + } + } + + for (const [file, changes] of replacementsPerFile) { + const recorder = tree.beginUpdate(file); + for (const c of changes) { + recorder + .remove(c.data.position, c.data.end - c.data.position) + .insertLeft(c.data.position, c.data.toInsert); + } + tree.commitUpdate(recorder); + } + + const { + counters: {removedImports, changedFiles}, + } = await migration.stats(globalMeta); + let statsMessage: string; + + if (removedImports === 0) { + statsMessage = 'Schematic could not find unused imports in the project'; + } else { + statsMessage = + `Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` + + `in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`; + } + + context.logger.info(''); + context.logger.info(statsMessage); + }; +} diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/schema.json b/packages/core/schematics/ng-generate/cleanup-unused-imports/schema.json new file mode 100644 index 0000000000000..e2f8a52ca180c --- /dev/null +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "AngularCleanupUnusedImportsMigration", + "title": "Angular Cleanup Unused Imports Schema", + "type": "object", + "properties": {} +} diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/unused_imports_migration.ts b/packages/core/schematics/ng-generate/cleanup-unused-imports/unused_imports_migration.ts new file mode 100644 index 0000000000000..4933965f95252 --- /dev/null +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/unused_imports_migration.ts @@ -0,0 +1,337 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import ts from 'typescript'; +import { + BaseProgramInfo, + confirmAsSerializable, + MigrationStats, + ProgramInfo, + projectFile, + Replacement, + Serializable, + TextUpdate, + TsurgeFunnelMigration, +} from '../../utils/tsurge'; +import {ErrorCode, FileSystem, ngErrorCode} from '@angular/compiler-cli'; +import {DiagnosticCategoryLabel} from '@angular/compiler-cli/src/ngtsc/core/api'; +import {ImportManager} from '@angular/compiler-cli/private/migrations'; +import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager'; + +/** Data produced by the migration for each compilation unit. */ +export interface CompilationUnitData { + /** Text changes that should be performed. */ + replacements: Replacement[]; + + /** Total number of imports that were removed. */ + removedImports: number; + + /** Total number of files that were changed. */ + changedFiles: number; +} + +/** Tracks the places from which to remove unused imports. */ +interface RemovalLocations { + /** Arrays whose entire contents should be cleared. */ + fullRemovals: Set; + + /** Arrays where only some elements need to be removed. */ + partialRemovals: Map>; + + /** Text of all identifiers that have been removed. */ + allRemovedIdentifiers: Set; +} + +/** Tracks how identifiers are used across a single file. */ +interface UsageAnalysis { + /** + * Data about the symbols imported into the file. The key here is the module from which each + * symbol is imported. Each module contains a map between a local symbol name within the file + * and its original name. + */ + importedSymbols: Map>; + + /** Number of times each identifier string is seen within a file. */ + identifierCounts: Map; +} + +/** Migration that cleans up unused imports from a project. */ +export class UnusedImportsMigration extends TsurgeFunnelMigration< + CompilationUnitData, + CompilationUnitData +> { + private printer = ts.createPrinter(); + + override createProgram(tsconfigAbsPath: string, fs?: FileSystem): BaseProgramInfo { + return super.createProgram(tsconfigAbsPath, fs, { + extendedDiagnostics: { + checks: { + // Ensure that the diagnostic is enabled. + unusedStandaloneImports: DiagnosticCategoryLabel.Warning, + }, + }, + }); + } + + override async analyze(info: ProgramInfo): Promise> { + const nodePositions = new Map>(); + const replacements: Replacement[] = []; + let removedImports = 0; + let changedFiles = 0; + + info.ngCompiler?.getDiagnostics().forEach((diag) => { + if ( + diag.file !== undefined && + diag.start !== undefined && + diag.length !== undefined && + diag.code === ngErrorCode(ErrorCode.UNUSED_STANDALONE_IMPORTS) + ) { + if (!nodePositions.has(diag.file)) { + nodePositions.set(diag.file, new Set()); + } + nodePositions.get(diag.file)!.add(this.getNodeKey(diag.start, diag.length)); + } + }); + + nodePositions.forEach((locations, sourceFile) => { + const resolvedLocations = this.resolveRemovalLocations(sourceFile, locations); + const usageAnalysis = this.analyzeUsages(sourceFile, resolvedLocations); + + if (resolvedLocations.allRemovedIdentifiers.size > 0) { + removedImports += resolvedLocations.allRemovedIdentifiers.size; + changedFiles++; + } + + this.generateReplacements(sourceFile, resolvedLocations, usageAnalysis, info, replacements); + }); + + return confirmAsSerializable({replacements, removedImports, changedFiles}); + } + + override async migrate(globalData: CompilationUnitData) { + return confirmAsSerializable(globalData); + } + + override async combine( + unitA: CompilationUnitData, + unitB: CompilationUnitData, + ): Promise> { + return confirmAsSerializable({ + replacements: [...unitA.replacements, ...unitB.replacements], + removedImports: unitA.removedImports + unitB.removedImports, + changedFiles: unitA.changedFiles + unitB.changedFiles, + }); + } + + override async globalMeta( + combinedData: CompilationUnitData, + ): Promise> { + return confirmAsSerializable(combinedData); + } + + override async stats(globalMetadata: CompilationUnitData): Promise { + return { + counters: { + removedImports: globalMetadata.removedImports, + changedFiles: globalMetadata.changedFiles, + }, + }; + } + + /** Gets a key that can be used to look up a node based on its location. */ + private getNodeKey(start: number, length: number): string { + return `${start}/${length}`; + } + + /** + * Resolves a set of node locations to the actual AST nodes that need to be migrated. + * @param sourceFile File in which to resolve the locations. + * @param locations Location keys that should be resolved. + */ + private resolveRemovalLocations( + sourceFile: ts.SourceFile, + locations: Set, + ): RemovalLocations { + const result: RemovalLocations = { + fullRemovals: new Set(), + partialRemovals: new Map(), + allRemovedIdentifiers: new Set(), + }; + + const walk = (node: ts.Node) => { + if (!ts.isIdentifier(node)) { + node.forEachChild(walk); + return; + } + + // The TS typings don't reflect that the parent can be undefined. + const parent = node.parent as ts.Node | undefined; + + if (!parent) { + return; + } + + if (locations.has(this.getNodeKey(node.getStart(), node.getWidth()))) { + // When the entire array needs to be cleared, the diagnostic is + // reported on the property assignment, rather than an array element. + if ( + ts.isPropertyAssignment(parent) && + parent.name === node && + ts.isArrayLiteralExpression(parent.initializer) + ) { + result.fullRemovals.add(parent.initializer); + parent.initializer.elements.forEach((element) => { + if (ts.isIdentifier(element)) { + result.allRemovedIdentifiers.add(element.text); + } + }); + } else if (ts.isArrayLiteralExpression(parent)) { + if (!result.partialRemovals.has(parent)) { + result.partialRemovals.set(parent, new Set()); + } + result.partialRemovals.get(parent)!.add(node); + result.allRemovedIdentifiers.add(node.text); + } + } + }; + + walk(sourceFile); + + return result; + } + + /** + * Analyzes how identifiers are used across a file. + * @param sourceFile File to be analyzed. + * @param locations Locations that will be changed as a part of this migration. + */ + private analyzeUsages(sourceFile: ts.SourceFile, locations: RemovalLocations): UsageAnalysis { + const {partialRemovals, fullRemovals} = locations; + const result: UsageAnalysis = { + importedSymbols: new Map(), + identifierCounts: new Map(), + }; + + const walk = (node: ts.Node) => { + if ( + ts.isIdentifier(node) && + node.parent && + // Don't track individual identifiers marked for removal. + (!ts.isArrayLiteralExpression(node.parent) || + !partialRemovals.has(node.parent) || + !partialRemovals.get(node.parent)!.has(node)) + ) { + result.identifierCounts.set(node.text, (result.identifierCounts.get(node.text) ?? 0) + 1); + } + + // Don't track identifiers in array literals that are about to be removed. + if (ts.isArrayLiteralExpression(node) && fullRemovals.has(node)) { + return; + } + + if (ts.isImportDeclaration(node)) { + const namedBindings = node.importClause?.namedBindings; + const moduleName = ts.isStringLiteral(node.moduleSpecifier) + ? node.moduleSpecifier.text + : null; + + if (namedBindings && ts.isNamedImports(namedBindings) && moduleName !== null) { + namedBindings.elements.forEach((imp) => { + if (!result.importedSymbols.has(moduleName)) { + result.importedSymbols.set(moduleName, new Map()); + } + const symbolName = (imp.propertyName || imp.name).text; + const localName = imp.name.text; + result.importedSymbols.get(moduleName)!.set(localName, symbolName); + }); + } + + // Don't track identifiers in imports. + return; + } + + // Track identifiers in all other node kinds. + node.forEachChild(walk); + }; + + walk(sourceFile); + + return result; + } + + /** + * Generates text replacements based on the data produced by the migration. + * @param sourceFile File being migrated. + * @param removalLocations Data about nodes being removed. + * @param usages Data about identifier usage. + * @param info Information about the current program. + * @param replacements Array tracking all text replacements. + */ + private generateReplacements( + sourceFile: ts.SourceFile, + removalLocations: RemovalLocations, + usages: UsageAnalysis, + info: ProgramInfo, + replacements: Replacement[], + ): void { + const {fullRemovals, partialRemovals, allRemovedIdentifiers} = removalLocations; + const {importedSymbols, identifierCounts} = usages; + const importManager = new ImportManager(); + + // Replace full arrays with empty ones. This allows preserves more of the user's formatting. + fullRemovals.forEach((node) => { + replacements.push( + new Replacement( + projectFile(sourceFile, info), + new TextUpdate({ + position: node.getStart(), + end: node.getEnd(), + toInsert: '[]', + }), + ), + ); + }); + + // Filter out the unused identifiers from an array. + partialRemovals.forEach((toRemove, node) => { + const newNode = ts.factory.updateArrayLiteralExpression( + node, + node.elements.filter((el) => !toRemove.has(el)), + ); + + replacements.push( + new Replacement( + projectFile(sourceFile, info), + new TextUpdate({ + position: node.getStart(), + end: node.getEnd(), + toInsert: this.printer.printNode(ts.EmitHint.Unspecified, newNode, sourceFile), + }), + ), + ); + }); + + // Attempt to clean up unused import declarations. Note that this isn't foolproof, because we + // do the matching based on identifier text, rather than going through the type checker which + // can be expensive. This should be enough for the vast majority of cases in this schematic + // since we're dealing exclusively with directive/pipe class names which tend to be very + // specific. In the worst case we may end up not removing an import declaration which would + // still be valid code that the user can clean up themselves. + importedSymbols.forEach((names, moduleName) => { + names.forEach((symbolName, localName) => { + // Note that in the `identifierCounts` lookup both zero and undefined + // are valid and mean that the identifiers isn't being used anymore. + if (allRemovedIdentifiers.has(localName) && !identifierCounts.get(localName)) { + importManager.removeImport(sourceFile, symbolName, moduleName); + } + }); + }); + + applyImportManagerChanges(importManager, replacements, [sourceFile], info); + } +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index 090346a78b1ee..a4626cd36ea40 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -21,6 +21,7 @@ jasmine_node_test( "//packages/core/schematics:bundles", "//packages/core/schematics:collection.json", "//packages/core/schematics:migrations.json", + "//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files", "//packages/core/schematics/ng-generate/control-flow-migration:static_files", "//packages/core/schematics/ng-generate/inject-migration:static_files", "//packages/core/schematics/ng-generate/output-migration:static_files", diff --git a/packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts b/packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts new file mode 100644 index 0000000000000..bdb9877dc7652 --- /dev/null +++ b/packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts @@ -0,0 +1,256 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import {runfiles} from '@bazel/runfiles'; +import shx from 'shelljs'; + +describe('cleanup unused imports schematic', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + let previousWorkingDir: string; + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner.runSchematic('cleanup-unused-imports', {}, tree); + } + + function stripWhitespace(content: string) { + return content.replace(/\s+/g, ''); + } + + beforeEach(() => { + runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../collection.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile( + '/tsconfig.json', + JSON.stringify({ + compilerOptions: { + lib: ['es2015'], + strictNullChecks: true, + }, + }), + ); + + writeFile( + '/angular.json', + JSON.stringify({ + version: 1, + projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, + }), + ); + + previousWorkingDir = shx.pwd(); + tmpDirPath = getSystemPath(host.root); + + // Switch into the temporary directory path. This allows us to run + // the schematic against our custom unit test tree. + shx.cd(tmpDirPath); + + writeFile( + 'directives.ts', + ` + import {Directive} from '@angular/core'; + + @Directive({selector: '[one]'}) + export class One {} + + @Directive({selector: '[two]'}) + export class Two {} + + @Directive({selector: '[three]'}) + export class Three {} + `, + ); + }); + + afterEach(() => { + shx.cd(previousWorkingDir); + shx.rm('-r', tmpDirPath); + }); + + it('should clean up an array where some imports are not used', async () => { + writeFile( + 'comp.ts', + ` + import {Component} from '@angular/core'; + import {One, Two, Three} from './directives'; + + @Component({ + imports: [Three, One, Two], + template: '
    ', + }) + export class Comp {} + `, + ); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('comp.ts'))).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + import {One} from './directives'; + + @Component({ + imports: [One], + template: '
    ', + }) + export class Comp {} + `), + ); + }); + + it('should clean up an array where all imports are not used', async () => { + writeFile( + 'comp.ts', + ` + import {Component} from '@angular/core'; + import {One, Two, Three} from './directives'; + + @Component({ + imports: [Three, One, Two], + template: '', + }) + export class Comp {} + `, + ); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('comp.ts'))).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + + @Component({ + imports: [], + template: '', + }) + export class Comp {} + `), + ); + }); + + it('should clean up an array where aliased imports are not used', async () => { + writeFile( + 'comp.ts', + ` + import {Component} from '@angular/core'; + import {One as OneAlias, Two as TwoAlias, Three as ThreeAlias} from './directives'; + + @Component({ + imports: [ThreeAlias, OneAlias, TwoAlias], + template: '
    ', + }) + export class Comp {} + `, + ); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('comp.ts'))).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + import {One as OneAlias} from './directives'; + + @Component({ + imports: [OneAlias], + template: '
    ', + }) + export class Comp {} + `), + ); + }); + + it('should preserve import declaration if unused import is still used within the file', async () => { + writeFile( + 'comp.ts', + ` + import {Component} from '@angular/core'; + import {One} from './directives'; + + @Component({ + imports: [One], + template: '', + }) + export class Comp {} + + @Component({ + imports: [One], + template: '
    ', + }) + export class OtherComp {} + `, + ); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('comp.ts'))).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + import {One} from './directives'; + + @Component({ + imports: [], + template: '', + }) + export class Comp {} + + @Component({ + imports: [One], + template: '
    ', + }) + export class OtherComp {} + `), + ); + }); + + it('should not touch a file where all imports are used', async () => { + const initialContent = ` + import {Component} from '@angular/core'; + import {One, Two, Three} from './directives'; + + @Component({ + imports: [Three, One, Two], + template: '
    ', + }) + export class Comp {} + `; + + writeFile('comp.ts', initialContent); + + await runMigration(); + + expect(tree.readContent('comp.ts')).toBe(initialContent); + }); + + it('should not touch unused import declarations that are not referenced in an `imports` array', async () => { + const initialContent = ` + import {Component} from '@angular/core'; + import {One, Two, Three} from './directives'; + + @Component({template: 'Hello'}) + export class Comp {} + `; + + writeFile('comp.ts', initialContent); + + await runMigration(); + + expect(tree.readContent('comp.ts')).toBe(initialContent); + }); +}); From d0cd74ace79355bef1b35d1bf18be816c15237db Mon Sep 17 00:00:00 2001 From: Sheik Althaf Date: Fri, 22 Nov 2024 14:19:00 +0530 Subject: [PATCH 20/36] refactor(devtools): use signals for template properties in frame manager (#58818) convert the frames and selectedFrame properties to signal so that it can react to changes on OnPush PR Close #58818 --- .../devtools-tabs.component.html | 4 +- .../devtools-tabs/devtools-tabs.component.ts | 2 +- .../lib/devtools-tabs/devtools-tabs.spec.ts | 2 +- .../directive-explorer.component.ts | 6 +- .../ng-devtools/src/lib/frame_manager.ts | 67 ++++++++++--------- .../ng-devtools/src/lib/frame_manager_spec.ts | 51 +++++++------- 6 files changed, 71 insertions(+), 61 deletions(-) diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html index 5625475524e69..cb7f46929dc21 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/devtools-tabs.component.html @@ -24,7 +24,7 @@ class="frame-selector" (change)="emitSelectedFrame($event.target.value)" > - @for (frame of frameManager.frames; track frame.id) { + @for (frame of frameManager.frames(); track frame.id) {