diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd7773a4f7c..bcbb5214da4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ + +# 15.2.2 (2023-03-08) +### migrations +| Commit | Type | Description | +| -- | -- | -- | +| [6207d6f1f0](https://github.com/angular/angular/commit/6207d6f1f0771ff3b74379367e65af665ef0e51c) | fix | add protractor support if protractor imports are detected ([#49274](https://github.com/angular/angular/pull/49274)) | +## Special Thanks +Alan Agius, Andrew Kushnir, Andrew Scott, Kristiyan Kostadinov, Matthieu Riegler, Paul Gschwendtner, Sai Kartheek Bommisetty and Vinit Neogi + + + # 15.2.1 (2023-03-01) ### common diff --git a/aio/content/errors/NG01003.md b/aio/content/errors/NG01101.md similarity index 100% rename from aio/content/errors/NG01003.md rename to aio/content/errors/NG01101.md diff --git a/aio/content/examples/forms-overview/example-config.json b/aio/content/examples/forms-overview/example-config.json index e2cf1e1e4e75..ccf472031b4d 100644 --- a/aio/content/examples/forms-overview/example-config.json +++ b/aio/content/examples/forms-overview/example-config.json @@ -1,6 +1,6 @@ { "tests": [ - {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadless", "--no-watch"]}, + {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadlessNoSandbox", "--no-watch"]}, {"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]} ] } diff --git a/aio/content/examples/http/example-config.json b/aio/content/examples/http/example-config.json index a92715ec1cdb..239710e2cd0f 100644 --- a/aio/content/examples/http/example-config.json +++ b/aio/content/examples/http/example-config.json @@ -1,7 +1,7 @@ { "projectType": "testing", "tests": [ - {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadless", "--no-watch"]}, + {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadlessNoSandbox", "--no-watch"]}, {"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]} ] } diff --git a/aio/content/examples/practical-observable-usage/src/typeahead.spec.ts b/aio/content/examples/practical-observable-usage/src/typeahead.spec.ts index fc3b637e5d12..0f9b0aea3028 100644 --- a/aio/content/examples/practical-observable-usage/src/typeahead.spec.ts +++ b/aio/content/examples/practical-observable-usage/src/typeahead.spec.ts @@ -1,5 +1,3 @@ -/// - import { of } from 'rxjs'; import { docRegionTypeahead } from './typeahead'; @@ -71,7 +69,7 @@ describe('typeahead', () => { // Helpers interface MockTask { - id: NodeJS.Timeout; + id: ReturnType; fn: () => unknown; delay: number; recurring: boolean; @@ -86,12 +84,12 @@ describe('typeahead', () => { static install(mockTime = 0): MockClock['tick'] { const mocked = new this(mockTime); - spyOn(global, 'clearInterval').and.callFake(id => mocked.clearTask(id as MockTask['id'])); - spyOn(global, 'clearTimeout').and.callFake(id => mocked.clearTask(id as MockTask['id'])); - spyOn(global, 'setInterval').and.callFake( + spyOn(globalThis, 'clearInterval').and.callFake(id => mocked.clearTask(id as MockTask['id'])); + spyOn(globalThis, 'clearTimeout').and.callFake(id => mocked.clearTask(id as MockTask['id'])); + spyOn(globalThis, 'setInterval').and.callFake( ((fn: () => unknown, delay: number, ...args: any[]) => mocked.createTask(fn, delay, true, ...args)) as typeof setInterval); - spyOn(global, 'setTimeout').and.callFake( + spyOn(globalThis, 'setTimeout').and.callFake( ((fn: () => unknown, delay: number, ...args: any[]) => mocked.createTask(fn, delay, false, ...args)) as typeof setTimeout); diff --git a/aio/content/examples/setup/example-config.json b/aio/content/examples/setup/example-config.json index e2cf1e1e4e75..ccf472031b4d 100644 --- a/aio/content/examples/setup/example-config.json +++ b/aio/content/examples/setup/example-config.json @@ -1,6 +1,6 @@ { "tests": [ - {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadless", "--no-watch"]}, + {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadlessNoSandbox", "--no-watch"]}, {"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]} ] } diff --git a/aio/content/examples/testing/example-config.json b/aio/content/examples/testing/example-config.json index a92715ec1cdb..239710e2cd0f 100644 --- a/aio/content/examples/testing/example-config.json +++ b/aio/content/examples/testing/example-config.json @@ -1,7 +1,7 @@ { "projectType": "testing", "tests": [ - {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadless", "--no-watch"]}, + {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadlessNoSandbox", "--no-watch"]}, {"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]} ] } diff --git a/aio/content/examples/testing/src/app/app.component.spec.ts b/aio/content/examples/testing/src/app/app.component.spec.ts index a9802da33c4c..102c5faeae96 100644 --- a/aio/content/examples/testing/src/app/app.component.spec.ts +++ b/aio/content/examples/testing/src/app/app.component.spec.ts @@ -1,11 +1,10 @@ // #docplaster -import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; +import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {provideRouter, Router, RouterLink} from '@angular/router'; -import { RouterLinkDirectiveStub } from '../testing'; - -import { AppComponent } from './app.component'; +import {AppComponent} from './app.component'; // #docregion component-stubs @Component({selector: 'app-banner', template: ''}) @@ -29,10 +28,10 @@ describe('AppComponent & TestModule', () => { // #docregion testbed-stubs TestBed .configureTestingModule({ - declarations: [ - AppComponent, RouterLinkDirectiveStub, BannerStubComponent, RouterOutletStubComponent, - WelcomeStubComponent - ] + imports: [RouterLink], + providers: [provideRouter([])], + declarations: + [AppComponent, BannerStubComponent, RouterOutletStubComponent, WelcomeStubComponent] }) // #enddocregion testbed-stubs .compileComponents() @@ -55,8 +54,9 @@ describe('AppComponent & NO_ERRORS_SCHEMA', () => { // #enddocregion no-errors-schema BannerStubComponent, // #docregion no-errors-schema - RouterLinkDirectiveStub ], + providers: [provideRouter([])], + imports: [RouterLink], schemas: [NO_ERRORS_SCHEMA] }) // #enddocregion no-errors-schema, mixed-setup @@ -70,21 +70,25 @@ describe('AppComponent & NO_ERRORS_SCHEMA', () => { }); //////// Testing w/ real root module ////// -// Tricky because we are disabling the router and its configuration -// Better to use RouterTestingModule -import { AppModule } from './app.module'; -import { AppRoutingModule } from './app-routing.module'; +import {AppModule} from './app.module'; +import {AppRoutingModule} from './app-routing.module'; describe('AppComponent & AppModule', () => { beforeEach(waitForAsync(() => { TestBed - .configureTestingModule({imports: [AppModule]}) + .configureTestingModule({ + imports: [AppModule], + }) // Get rid of app's Router configuration otherwise many failures. // Doing so removes Router declarations; add the Router stubs .overrideModule(AppModule, { remove: {imports: [AppRoutingModule]}, - add: {declarations: [RouterLinkDirectiveStub, RouterOutletStubComponent]} + add: { + declarations: [RouterOutletStubComponent], + imports: [RouterLink], + providers: [provideRouter([])], + } }) .compileComponents() @@ -99,7 +103,7 @@ describe('AppComponent & AppModule', () => { }); function tests() { - let routerLinks: RouterLinkDirectiveStub[]; + let routerLinks: RouterLink[]; let linkDes: DebugElement[]; // #docregion test-setup @@ -107,11 +111,11 @@ function tests() { fixture.detectChanges(); // trigger initial data binding // find DebugElements with an attached RouterLinkStubDirective - linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub)); + linkDes = fixture.debugElement.queryAll(By.directive(RouterLink)); // get attached link directive instances // using each DebugElement's injector - routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub)); + routerLinks = linkDes.map(de => de.injector.get(RouterLink)); }); // #enddocregion test-setup @@ -121,26 +125,21 @@ function tests() { // #docregion tests it('can get RouterLinks from template', () => { - expect(routerLinks.length) - .withContext('should have 3 routerLinks') - .toBe(3); - expect(routerLinks[0].linkParams).toBe('/dashboard'); - expect(routerLinks[1].linkParams).toBe('/heroes'); - expect(routerLinks[2].linkParams).toBe('/about'); + expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3); + expect(routerLinks[0].href).toBe('/dashboard'); + expect(routerLinks[1].href).toBe('/heroes'); + expect(routerLinks[2].href).toBe('/about'); }); - it('can click Heroes link in template', () => { - const heroesLinkDe = linkDes[1]; // heroes link DebugElement - const heroesLink = routerLinks[1]; // heroes link directive - - expect(heroesLink.navigatedTo) - .withContext('should not have navigated yet') - .toBeNull(); + it('can click Heroes link in template', fakeAsync(() => { + const heroesLinkDe = linkDes[1]; // heroes link DebugElement - heroesLinkDe.triggerEventHandler('click'); - fixture.detectChanges(); + TestBed.inject(Router).resetConfig([{path: '**', children: []}]); + heroesLinkDe.triggerEventHandler('click', {button: 0}); + tick(); + fixture.detectChanges(); - expect(heroesLink.navigatedTo).toBe('/heroes'); - }); + expect(TestBed.inject(Router).url).toBe('/heroes'); + })); // #enddocregion tests } diff --git a/aio/content/examples/testing/src/app/dashboard/dashboard.component.spec.ts b/aio/content/examples/testing/src/app/dashboard/dashboard.component.spec.ts index 1a896d496f4c..4aa6bcacdeb4 100644 --- a/aio/content/examples/testing/src/app/dashboard/dashboard.component.spec.ts +++ b/aio/content/examples/testing/src/app/dashboard/dashboard.component.spec.ts @@ -1,20 +1,24 @@ -// #docplaster -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { addMatchers, asyncData, click } from '../../testing'; -import { HeroService } from '../model/hero.service'; -import { getTestHeroes } from '../model/testing/test-heroes'; - -import { By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; - -import { DashboardComponent } from './dashboard.component'; -import { DashboardModule } from './dashboard.module'; +import {provideHttpClient} from '@angular/common/http'; +import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {NavigationEnd, provideRouter, Router} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {firstValueFrom} from 'rxjs'; +import {filter} from 'rxjs/operators'; + +import {addMatchers, click} from '../../testing'; +import {HeroService} from '../model/hero.service'; +import {getTestHeroes} from '../model/testing/test-heroes'; + +import {DashboardComponent} from './dashboard.component'; +import {DashboardModule} from './dashboard.module'; beforeEach(addMatchers); let comp: DashboardComponent; -let fixture: ComponentFixture; +let harness: RouterTestingHarness; //////// Deep //////////////// @@ -29,15 +33,15 @@ describe('DashboardComponent (deep)', () => { function clickForDeep() { // get first
- const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero'); + const heroEl: HTMLElement = harness.routeNativeElement!.querySelector('.hero')!; click(heroEl); + return firstValueFrom( + TestBed.inject(Router).events.pipe(filter(e => e instanceof NavigationEnd))); } }); //////// Shallow //////////////// -import { NO_ERRORS_SCHEMA } from '@angular/core'; - describe('DashboardComponent (shallow)', () => { beforeEach(() => { TestBed.configureTestingModule( @@ -50,33 +54,32 @@ describe('DashboardComponent (shallow)', () => { function clickForShallow() { // get first DebugElement - const heroDe = fixture.debugElement.query(By.css('dashboard-hero')); + const heroDe = harness.routeDebugElement!.query(By.css('dashboard-hero')); heroDe.triggerEventHandler('selected', comp.heroes[0]); + return Promise.resolve(); } }); /** Add TestBed providers, compile, and create DashboardComponent */ function compileAndCreate() { beforeEach(waitForAsync(() => { - // #docregion router-spy - const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']); - const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']); - + // #docregion router-harness TestBed .configureTestingModule({ providers: [ - {provide: HeroService, useValue: heroServiceSpy}, {provide: Router, useValue: routerSpy} + provideRouter([{path: '**', component: DashboardComponent}]), + provideHttpClient(), + provideHttpClientTesting(), + HeroService, ] }) - // #enddocregion router-spy .compileComponents() - .then(() => { - fixture = TestBed.createComponent(DashboardComponent); - comp = fixture.componentInstance; - - // getHeroes spy returns observable of test heroes - heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes())); + .then(async () => { + harness = await RouterTestingHarness.create(); + comp = await harness.navigateByUrl('/', DashboardComponent); + TestBed.inject(HttpTestingController).expectOne('api/heroes').flush(getTestHeroes()); }); + // #enddocregion router-harness })); } @@ -84,61 +87,38 @@ function compileAndCreate() { * The (almost) same tests for both. * Only change: the way that the first hero is clicked */ -function tests(heroClick: () => void) { - - it('should NOT have heroes before ngOnInit', () => { - expect(comp.heroes.length) - .withContext('should not have heroes before ngOnInit') - .toBe(0); - }); - - it('should NOT have heroes immediately after ngOnInit', () => { - fixture.detectChanges(); // runs initial lifecycle hooks - - expect(comp.heroes.length) - .withContext('should not have heroes until service promise resolves') - .toBe(0); - }); - +function tests(heroClick: () => Promise) { describe('after get dashboard heroes', () => { let router: Router; - // Trigger component so it gets heroes and binds to them + // Trigger component so it gets heroes and binds to them beforeEach(waitForAsync(() => { - router = fixture.debugElement.injector.get(Router); - fixture.detectChanges(); // runs ngOnInit -> getHeroes - fixture.whenStable() // No need for the `lastPromise` hack! - .then(() => fixture.detectChanges()); // bind to heroes + router = TestBed.inject(Router); + harness.detectChanges(); // runs ngOnInit -> getHeroes })); it('should HAVE heroes', () => { expect(comp.heroes.length) - .withContext('should have heroes after service promise resolves') - .toBeGreaterThan(0); + .withContext('should have heroes after service promise resolves') + .toBeGreaterThan(0); }); it('should DISPLAY heroes', () => { // Find and examine the displayed heroes // Look for them in the DOM by css class - const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero'); - expect(heroes.length) - .withContext('should display 4 heroes') - .toBe(4); + const heroes = harness.routeNativeElement!.querySelectorAll('dashboard-hero'); + expect(heroes.length).withContext('should display 4 heroes').toBe(4); }); // #docregion navigate-test - it('should tell ROUTER to navigate when hero clicked', () => { - heroClick(); // trigger click on first inner
- - // args passed to router.navigateByUrl() spy - const spy = router.navigateByUrl as jasmine.Spy; - const navArgs = spy.calls.first().args[0]; + it('should tell navigate when hero clicked', async () => { + await heroClick(); // trigger click on first inner
// expecting to navigate to id of the component's first hero const id = comp.heroes[0].id; - expect(navArgs) - .withContext('should nav to HeroDetail for first hero') - .toBe('/heroes/' + id); + expect(TestBed.inject(Router).url) + .withContext('should nav to HeroDetail for first hero') + .toEqual(`/heroes/${id}`); }); // #enddocregion navigate-test }); diff --git a/aio/content/examples/testing/src/app/dashboard/dashboard.component.ts b/aio/content/examples/testing/src/app/dashboard/dashboard.component.ts index 9a65b047891c..83512c628f9b 100644 --- a/aio/content/examples/testing/src/app/dashboard/dashboard.component.ts +++ b/aio/content/examples/testing/src/app/dashboard/dashboard.component.ts @@ -1,29 +1,24 @@ // #docregion -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import {Component, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; -import { Hero } from '../model/hero'; -import { HeroService } from '../model/hero.service'; +import {Hero} from '../model/hero'; +import {HeroService} from '../model/hero.service'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', - styleUrls: [ './dashboard.component.css' ] + styleUrls: ['./dashboard.component.css'] }) export class DashboardComponent implements OnInit { - heroes: Hero[] = []; // #docregion ctor - constructor( - private router: Router, - private heroService: HeroService) { - } + constructor(private router: Router, private heroService: HeroService) {} // #enddocregion ctor ngOnInit() { - this.heroService.getHeroes() - .subscribe(heroes => this.heroes = heroes.slice(1, 5)); + this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes.slice(1, 5)); } // #docregion goto-detail @@ -35,7 +30,6 @@ export class DashboardComponent implements OnInit { get title() { const cnt = this.heroes.length; - return cnt === 0 ? 'No Heroes' : - cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`; + return cnt === 0 ? 'No Heroes' : cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`; } } diff --git a/aio/content/examples/testing/src/app/hero/hero-detail.component.no-testbed.spec.ts b/aio/content/examples/testing/src/app/hero/hero-detail.component.no-testbed.spec.ts deleted file mode 100644 index c0c55f56ad2a..000000000000 --- a/aio/content/examples/testing/src/app/hero/hero-detail.component.no-testbed.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Router } from '@angular/router'; - -import { asyncData, ActivatedRouteStub } from '../../testing'; - -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroDetailService } from './hero-detail.service'; -import { Hero } from '../model/hero'; - -////////// Tests //////////////////// - -describe('HeroDetailComponent - no TestBed', () => { - let comp: HeroDetailComponent; - let expectedHero: Hero; - let hds: jasmine.SpyObj; - let router: jasmine.SpyObj; - - beforeEach((done: DoneFn) => { - expectedHero = { id: 42, name: 'Bubba' }; - const activatedRoute = new ActivatedRouteStub({ id: expectedHero.id }); - router = jasmine.createSpyObj('Router', ['navigate']); - - hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']); - hds.getHero.and.returnValue(asyncData(expectedHero)); - hds.saveHero.and.returnValue(asyncData(expectedHero)); - - comp = new HeroDetailComponent(hds, activatedRoute as any, router); - comp.ngOnInit(); - - // OnInit calls HDS.getHero; wait for it to get the fake hero - hds.getHero.calls.first().returnValue.subscribe(() => done()); - }); - - it('should expose the hero retrieved from the service', () => { - expect(comp.hero).toBe(expectedHero); - }); - - it('should navigate when click cancel', () => { - comp.cancel(); - expect(router.navigate.calls.any()) - .withContext('router.navigate called') - .toBe(true); - }); - - it('should save when click save', () => { - comp.save(); - expect(hds.saveHero.calls.any()) - .withContext('HeroDetailService.save called') - .toBe(true); - expect(router.navigate.calls.any()) - .withContext('router.navigate not called yet') - .toBe(false); - }); - - it('should navigate when click save resolves', (done: DoneFn) => { - comp.save(); - // waits for async save to complete before navigating - hds.saveHero.calls.first().returnValue - .subscribe(() => { - expect(router.navigate.calls.any()) - .withContext('router.navigate called') - .toBe(true); - done(); - }); - }); - -}); diff --git a/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts b/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts index 5ef0f102a63f..384be7065a53 100644 --- a/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts +++ b/aio/content/examples/testing/src/app/hero/hero-detail.component.spec.ts @@ -1,27 +1,26 @@ // #docplaster -import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import {provideHttpClient} from '@angular/common/http'; +import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {provideRouter, Router} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; -import { - ActivatedRoute, ActivatedRouteStub, asyncData, click -} from '../../testing'; +import {asyncData, click} from '../../testing'; +import {Hero} from '../model/hero'; +import {SharedModule} from '../shared/shared.module'; -import { Hero } from '../model/hero'; -import { HeroDetailComponent } from './hero-detail.component'; -import { HeroDetailService } from './hero-detail.service'; -import { HeroModule } from './hero.module'; +import {HeroDetailComponent} from './hero-detail.component'; +import {HeroDetailService} from './hero-detail.service'; +import {HeroListComponent} from './hero-list.component'; +import {HeroModule} from './hero.module'; ////// Testing Vars ////// -let activatedRoute: ActivatedRouteStub; let component: HeroDetailComponent; -let fixture: ComponentFixture; +let harness: RouterTestingHarness; let page: Page; ////// Tests ////// describe('HeroDetailComponent', () => { - beforeEach(() => { - activatedRoute = new ActivatedRouteStub(); - }); describe('with HeroModule setup', heroModuleSetup); describe('when override its provided HeroDetailService', overrideSetup); describe('with FormsModule setup', formsModuleSetup); @@ -30,10 +29,11 @@ describe('HeroDetailComponent', () => { /////////////////// +const testHero = getTestHeroes()[0]; function overrideSetup() { // #docregion hds-spy class HeroDetailServiceSpy { - testHero: Hero = {id: 42, name: 'Test Hero'}; + testHero: Hero = {...testHero}; /* emit cloned test hero */ getHero = jasmine.createSpy('getHero').and.callFake( @@ -46,33 +46,22 @@ function overrideSetup() { // #enddocregion hds-spy - // the `id` value is irrelevant because ignored by service stub - beforeEach(() => activatedRoute.setParamMap({id: 99999})); - // #docregion setup-override beforeEach(async () => { - const routerSpy = createRouterSpy(); - await TestBed .configureTestingModule({ imports: [HeroModule], providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: Router, useValue: routerSpy}, - // #enddocregion setup-override + provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]), // HeroDetailService at this level is IRRELEVANT! {provide: HeroDetailService, useValue: {}} - // #docregion setup-override ] }) - - // Override component's own provider // #docregion override-component-method .overrideComponent( HeroDetailComponent, {set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}}) // #enddocregion override-component-method - .compileComponents(); }); // #enddocregion setup-override @@ -81,18 +70,22 @@ function overrideSetup() { let hdsSpy: HeroDetailServiceSpy; beforeEach(async () => { - await createComponent(); + harness = await RouterTestingHarness.create(); + component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent); + page = new Page(); // get the component's injected HeroDetailServiceSpy - hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any; + hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any; + + harness.detectChanges(); }); it('should have called `getHero`', () => { expect(hdsSpy.getHero.calls.count()) - .withContext('getHero called once') - .toBe(1, 'getHero called once'); + .withContext('getHero called once') + .toBe(1, 'getHero called once'); }); - it("should display stub hero's name", () => { + it('should display stub hero\'s name', () => { expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); }); @@ -102,62 +95,43 @@ function overrideSetup() { page.nameInput.value = newName; - page.nameInput.dispatchEvent(new Event('input')); // tell Angular + page.nameInput.dispatchEvent(new Event('input')); // tell Angular - expect(component.hero.name) - .withContext('component hero has new name') - .toBe(newName); + expect(component.hero.name).withContext('component hero has new name').toBe(newName); expect(hdsSpy.testHero.name) - .withContext('service hero unchanged before save') - .toBe(origName); + .withContext('service hero unchanged before save') + .toBe(origName); click(page.saveBtn); - expect(hdsSpy.saveHero.calls.count()) - .withContext('saveHero called once') - .toBe(1); + expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1); tick(); // wait for async save to complete expect(hdsSpy.testHero.name) - .withContext('service hero has new name after save') - .toBe(newName); - expect(page.navigateSpy.calls.any()) - .withContext('router.navigate called') - .toBe(true); - })); - // #enddocregion override-tests - - it('fixture injected service is not the component injected service', - // inject gets the service from the fixture - inject([HeroDetailService], (fixtureService: HeroDetailService) => { - // use `fixture.debugElement.injector` to get service from component - const componentService = fixture.debugElement.injector.get(HeroDetailService); - - expect(fixtureService) - .withContext('service injected from fixture') - .not.toBe(componentService); + .withContext('service hero has new name after save') + .toBe(newName); + expect(TestBed.inject(Router).url).toEqual('/heroes'); })); } //////////////////// -import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service'; +import {getTestHeroes} from '../model/testing/test-hero.service'; const firstHero = getTestHeroes()[0]; function heroModuleSetup() { // #docregion setup-hero-module beforeEach(async () => { - const routerSpy = createRouterSpy(); - await TestBed .configureTestingModule({ imports: [HeroModule], - // #enddocregion setup-hero-module // declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION - // #docregion setup-hero-module providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: HeroService, useClass: TestHeroService}, - {provide: Router, useValue: routerSpy}, + provideRouter([ + {path: 'heroes/:id', component: HeroDetailComponent}, + {path: 'heroes', component: HeroListComponent}, + ]), + provideHttpClient(), + provideHttpClientTesting(), ] }) .compileComponents(); @@ -170,50 +144,35 @@ function heroModuleSetup() { beforeEach(async () => { expectedHero = firstHero; - activatedRoute.setParamMap({id: expectedHero.id}); - await createComponent(); + await createComponent(expectedHero.id); }); - // #docregion selected-tests - it("should display that hero's name", () => { + it('should display that hero\'s name', () => { expect(page.nameDisplay.textContent).toBe(expectedHero.name); }); // #enddocregion route-good-id it('should navigate when click cancel', () => { click(page.cancelBtn); - expect(page.navigateSpy.calls.any()) - .withContext('router.navigate called') - .toBe(true); + expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`); }); it('should save when click save but not navigate immediately', () => { - // Get service injected into component and spy on its`saveHero` method. - // It delegates to fake `HeroService.updateHero` which delivers a safe test result. - const hds = fixture.debugElement.injector.get(HeroDetailService); - const saveSpy = spyOn(hds, 'saveHero').and.callThrough(); - click(page.saveBtn); - expect(saveSpy.calls.any()) - .withContext('HeroDetailService.save called') - .toBe(true); - expect(page.navigateSpy.calls.any()) - .withContext('router.navigate not called') - .toBe(false); + expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'})); + expect(TestBed.inject(Router).url).toEqual('/heroes/41'); }); it('should navigate when click save and save resolves', fakeAsync(() => { click(page.saveBtn); tick(); // wait for async save to complete - expect(page.navigateSpy.calls.any()) - .withContext('router.navigate called') - .toBe(true); + expect(TestBed.inject(Router).url).toEqual('/heroes/41'); })); // #docregion title-case-pipe it('should convert hero name to Title Case', () => { // get the name's input and display elements from the DOM - const hostElement: HTMLElement = fixture.nativeElement; + const hostElement: HTMLElement = harness.routeNativeElement!; const nameInput: HTMLInputElement = hostElement.querySelector('input')!; const nameDisplay: HTMLElement = hostElement.querySelector('span')!; @@ -224,146 +183,97 @@ function heroModuleSetup() { nameInput.dispatchEvent(new Event('input')); // Tell Angular to update the display binding through the title pipe - fixture.detectChanges(); + harness.detectChanges(); expect(nameDisplay.textContent).toBe('Quick Brown Fox'); }); - // #enddocregion title-case-pipe // #enddocregion selected-tests - // #docregion route-good-id - }); - // #enddocregion route-good-id - // #docregion route-no-id - describe('when navigate with no hero id', () => { - beforeEach(async () => { - await createComponent(); - }); - - it('should have hero.id === 0', () => { - expect(component.hero.id).toBe(0); - }); - - it('should display empty hero name', () => { - expect(page.nameDisplay.textContent).toBe(''); - }); + // #enddocregion title-case-pipe }); - // #enddocregion route-no-id // #docregion route-bad-id describe('when navigate to non-existent hero id', () => { beforeEach(async () => { - activatedRoute.setParamMap({id: 99999}); - await createComponent(); + await createComponent(999); }); it('should try to navigate back to hero list', () => { - expect(page.gotoListSpy.calls.any()) - .withContext('comp.gotoList called') - .toBe(true); - expect(page.navigateSpy.calls.any()) - .withContext('router.navigate called') - .toBe(true); + expect(TestBed.inject(Router).url).toEqual('/heroes'); }); }); // #enddocregion route-bad-id - - // Why we must use `fixture.debugElement.injector` in `Page()` - it("cannot use `inject` to get component's provided HeroDetailService", () => { - let service: HeroDetailService; - fixture = TestBed.createComponent(HeroDetailComponent); - expect( - // Throws because `inject` only has access to TestBed's injector - // which is an ancestor of the component's injector - inject([HeroDetailService], (hds: HeroDetailService) => service = hds)) - .toThrowError(/No provider for HeroDetailService/); - - // get `HeroDetailService` with component's own injector - service = fixture.debugElement.injector.get(HeroDetailService); - expect(service) - .withContext('debugElement.injector') - .toBeDefined(); - }); } ///////////////////// -import { FormsModule } from '@angular/forms'; -import { TitleCasePipe } from '../shared/title-case.pipe'; +import {FormsModule} from '@angular/forms'; +import {TitleCasePipe} from '../shared/title-case.pipe'; function formsModuleSetup() { // #docregion setup-forms-module beforeEach(async () => { - const routerSpy = createRouterSpy(); - await TestBed .configureTestingModule({ imports: [FormsModule], declarations: [HeroDetailComponent, TitleCasePipe], providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: HeroService, useClass: TestHeroService}, - {provide: Router, useValue: routerSpy}, + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]), ] }) .compileComponents(); }); // #enddocregion setup-forms-module - it("should display 1st hero's name", waitForAsync(() => { - const expectedHero = firstHero; - activatedRoute.setParamMap({id: expectedHero.id}); - createComponent().then(() => { - expect(page.nameDisplay.textContent).toBe(expectedHero.name); - }); - })); + it('should display 1st hero\'s name', async () => { + const expectedHero = firstHero; + await createComponent(expectedHero.id).then(() => { + expect(page.nameDisplay.textContent).toBe(expectedHero.name); + }); + }); } /////////////////////// -import { SharedModule } from '../shared/shared.module'; function sharedModuleSetup() { // #docregion setup-shared-module beforeEach(async () => { - const routerSpy = createRouterSpy(); - await TestBed .configureTestingModule({ imports: [SharedModule], declarations: [HeroDetailComponent], providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: HeroService, useClass: TestHeroService}, - {provide: Router, useValue: routerSpy}, + provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]), + provideHttpClient(), + provideHttpClientTesting(), ] }) .compileComponents(); }); // #enddocregion setup-shared-module - it("should display 1st hero's name", waitForAsync(() => { - const expectedHero = firstHero; - activatedRoute.setParamMap({id: expectedHero.id}); - createComponent().then(() => { - expect(page.nameDisplay.textContent).toBe(expectedHero.name); - }); - })); + it('should display 1st hero\'s name', async () => { + const expectedHero = firstHero; + await createComponent(expectedHero.id).then(() => { + expect(page.nameDisplay.textContent).toBe(expectedHero.name); + }); + }); } /////////// Helpers ///// -// #docregion create-component /** Create the HeroDetailComponent, initialize it, set test variables */ -function createComponent() { - fixture = TestBed.createComponent(HeroDetailComponent); - component = fixture.componentInstance; - page = new Page(fixture); - - // 1st change detection triggers ngOnInit which gets a hero - fixture.detectChanges(); - return fixture.whenStable().then(() => { - // 2nd change detection displays the async-fetched hero - fixture.detectChanges(); - }); +// #docregion create-component +async function createComponent(id: number) { + harness = await RouterTestingHarness.create(); + component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent); + page = new Page(); + + const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`); + const hero = getTestHeroes().find(h => h.id === Number(id)); + request.flush(hero ? [hero] : []); + harness.detectChanges(); } // #enddocregion create-component @@ -386,30 +296,13 @@ class Page { return this.query('input'); } - gotoListSpy: jasmine.Spy; - navigateSpy: jasmine.Spy; - - constructor(someFixture: ComponentFixture) { - // get the navigate spy from the injected router spy object - const routerSpy = someFixture.debugElement.injector.get(Router) as any; - this.navigateSpy = routerSpy.navigate; - - // spy on component's `gotoList()` method - const someComponent = someFixture.componentInstance; - this.gotoListSpy = spyOn(someComponent, 'gotoList').and.callThrough(); - } - //// query helpers //// private query(selector: string): T { - return fixture.nativeElement.querySelector(selector); + return harness.routeNativeElement!.querySelector(selector)! as T; } private queryAll(selector: string): T[] { - return fixture.nativeElement.querySelectorAll(selector); + return harness.routeNativeElement!.querySelectorAll(selector) as any as T[]; } } // #enddocregion page - -function createRouterSpy() { - return jasmine.createSpyObj('Router', ['navigate']); -} diff --git a/aio/content/examples/testing/src/app/hero/hero-detail.service.ts b/aio/content/examples/testing/src/app/hero/hero-detail.service.ts index 71befa59ccba..16abbc88c0ff 100644 --- a/aio/content/examples/testing/src/app/hero/hero-detail.service.ts +++ b/aio/content/examples/testing/src/app/hero/hero-detail.service.ts @@ -1,30 +1,29 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { Hero } from '../model/hero'; -import { HeroService } from '../model/hero.service'; +import {Hero} from '../model/hero'; +import {HeroService} from '../model/hero.service'; // #docregion prototype -@Injectable() +@Injectable({providedIn: 'root'}) export class HeroDetailService { - constructor(private heroService: HeroService) { } -// #enddocregion prototype + constructor(private heroService: HeroService) {} + // #enddocregion prototype // Returns a clone which caller may modify safely - getHero(id: number | string): Observable { + getHero(id: number|string): Observable { if (typeof id === 'string') { id = parseInt(id, 10); } return this.heroService.getHero(id).pipe( - map(hero => hero ? Object.assign({}, hero) : null) // clone or null + map(hero => hero ? Object.assign({}, hero) : null) // clone or null ); } saveHero(hero: Hero) { return this.heroService.updateHero(hero); } -// #docregion prototype + // #docregion prototype } // #enddocregion prototype diff --git a/aio/content/examples/testing/src/app/model/hero.service.ts b/aio/content/examples/testing/src/app/model/hero.service.ts index 933ae1eee33a..6dbc9137fca9 100644 --- a/aio/content/examples/testing/src/app/model/hero.service.ts +++ b/aio/content/examples/testing/src/app/model/hero.service.ts @@ -1,74 +1,69 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {catchError, map, tap} from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { catchError, map, tap } from 'rxjs/operators'; - -import { Hero } from './hero'; +import {Hero} from './hero'; const httpOptions = { - headers: new HttpHeaders({ 'Content-Type': 'application/json' }) + headers: new HttpHeaders({'Content-Type': 'application/json'}) }; -@Injectable() +@Injectable({providedIn: 'root'}) export class HeroService { - readonly heroesUrl = 'api/heroes'; // URL to web api - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) {} /** GET heroes from the server */ getHeroes(): Observable { return this.http.get(this.heroesUrl) - .pipe( - tap(heroes => this.log('fetched heroes')), - catchError(this.handleError('getHeroes')) - ) as Observable; + .pipe( + tap(heroes => this.log('fetched heroes')), + catchError(this.handleError('getHeroes'))) as Observable; } /** GET hero by id. Return `undefined` when id not found */ - getHero(id: number | string): Observable { + getHero(id: number|string): Observable { if (typeof id === 'string') { id = parseInt(id, 10); } const url = `${this.heroesUrl}/?id=${id}`; - return this.http.get(url) - .pipe( - map(heroes => heroes[0]), // returns a {0|1} element array + return this.http.get(url).pipe( + map(heroes => heroes[0]), // returns a {0|1} element array tap(h => { const outcome = h ? 'fetched' : 'did not find'; this.log(`${outcome} hero id=${id}`); }), - catchError(this.handleError(`getHero id=${id}`)) - ); + catchError(this.handleError(`getHero id=${id}`))); } //////// Save methods ////////// /** POST: add a new hero to the server */ addHero(hero: Hero): Observable { - return this.http.post(this.heroesUrl, hero, httpOptions).pipe( - tap((addedHero) => this.log(`added hero w/ id=${addedHero.id}`)), - catchError(this.handleError('addHero')) - ); + return this.http.post(this.heroesUrl, hero, httpOptions) + .pipe( + tap((addedHero) => this.log(`added hero w/ id=${addedHero.id}`)), + catchError(this.handleError('addHero'))); } /** DELETE: delete the hero from the server */ - deleteHero(hero: Hero | number): Observable { + deleteHero(hero: Hero|number): Observable { const id = typeof hero === 'number' ? hero : hero.id; const url = `${this.heroesUrl}/${id}`; - return this.http.delete(url, httpOptions).pipe( - tap(_ => this.log(`deleted hero id=${id}`)), - catchError(this.handleError('deleteHero')) - ); + return this.http.delete(url, httpOptions) + .pipe( + tap(_ => this.log(`deleted hero id=${id}`)), + catchError(this.handleError('deleteHero'))); } /** PUT: update the hero on the server */ updateHero(hero: Hero): Observable { - return this.http.put(this.heroesUrl, hero, httpOptions).pipe( - tap(_ => this.log(`updated hero id=${hero.id}`)), - catchError(this.handleError('updateHero')) - ); + return this.http.put(this.heroesUrl, hero, httpOptions) + .pipe( + tap(_ => this.log(`updated hero id=${hero.id}`)), + catchError(this.handleError('updateHero'))); } /** * Returns a function that handles Http operation failures. @@ -78,9 +73,8 @@ export class HeroService { */ private handleError(operation = 'operation') { return (error: HttpErrorResponse): Observable => { - // TODO: send the error to remote logging infrastructure - console.error(error); // log to console instead + console.error(error); // log to console instead // If a native error is caught, do not transform it. We only want to // transform response errors that are not wrapped in an `Error`. @@ -92,7 +86,6 @@ export class HeroService { // TODO: better job of transforming error for user consumption throw new Error(`${operation} failed: ${message}`); }; - } private log(message: string) { diff --git a/aio/content/examples/testing/src/testing/activated-route-stub.ts b/aio/content/examples/testing/src/testing/activated-route-stub.ts deleted file mode 100644 index 59fec15af781..000000000000 --- a/aio/content/examples/testing/src/testing/activated-route-stub.ts +++ /dev/null @@ -1,29 +0,0 @@ -// export for convenience. -export { ActivatedRoute } from '@angular/router'; - -// #docregion activated-route-stub -import { convertToParamMap, ParamMap, Params } from '@angular/router'; -import { ReplaySubject } from 'rxjs'; - -/** - * An ActivateRoute test double with a `paramMap` observable. - * Use the `setParamMap()` method to add the next `paramMap` value. - */ -export class ActivatedRouteStub { - // Use a ReplaySubject to share previous values with subscribers - // and pump new values into the `paramMap` observable - private subject = new ReplaySubject(); - - constructor(initialParams?: Params) { - this.setParamMap(initialParams); - } - - /** The mock paramMap observable */ - readonly paramMap = this.subject.asObservable(); - - /** Set the paramMap observable's next value */ - setParamMap(params: Params = {}) { - this.subject.next(convertToParamMap(params)); - } -} -// #enddocregion activated-route-stub diff --git a/aio/content/examples/testing/src/testing/index.ts b/aio/content/examples/testing/src/testing/index.ts index bcea4482a219..3c930d4a3739 100644 --- a/aio/content/examples/testing/src/testing/index.ts +++ b/aio/content/examples/testing/src/testing/index.ts @@ -1,10 +1,8 @@ -import { DebugElement } from '@angular/core'; -import { tick, ComponentFixture } from '@angular/core/testing'; +import {DebugElement} from '@angular/core'; +import {ComponentFixture, tick} from '@angular/core/testing'; export * from './async-observable-helpers'; -export * from './activated-route-stub'; export * from './jasmine-matchers'; -export * from './router-link-directive-stub'; ///// Short utilities ///// @@ -18,12 +16,12 @@ export function advance(f: ComponentFixture): void { // #docregion click-event /** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */ export const ButtonClickEvents = { - left: { button: 0 }, - right: { button: 2 } + left: {button: 0}, + right: {button: 2} }; /** Simulate element click. Defaults to mouse left-button click event. */ -export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void { +export function click(el: DebugElement|HTMLElement, eventObj: any = ButtonClickEvents.left): void { if (el instanceof HTMLElement) { el.click(); } else { diff --git a/aio/content/examples/testing/src/testing/router-link-directive-stub.ts b/aio/content/examples/testing/src/testing/router-link-directive-stub.ts deleted file mode 100644 index e1967ab3bd66..000000000000 --- a/aio/content/examples/testing/src/testing/router-link-directive-stub.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Directive, Input, HostListener } from '@angular/core'; - -// export for convenience. -export { RouterLink} from '@angular/router'; - -/* eslint-disable @angular-eslint/directive-class-suffix, @angular-eslint/directive-selector */ -// #docregion router-link -@Directive({ - selector: '[routerLink]' -}) -export class RouterLinkDirectiveStub { - @Input('routerLink') linkParams: any; - navigatedTo: any = null; - - @HostListener('click') - onClick() { - this.navigatedTo = this.linkParams; - } -} -// #enddocregion router-link - -/// Dummy module to satisfy Angular Language service. Never used. -import { NgModule } from '@angular/core'; - -@NgModule({ - declarations: [ - RouterLinkDirectiveStub - ] -}) -export class RouterStubsModule {} diff --git a/aio/content/examples/toh-pt6/example-config.json b/aio/content/examples/toh-pt6/example-config.json index e2cf1e1e4e75..ccf472031b4d 100644 --- a/aio/content/examples/toh-pt6/example-config.json +++ b/aio/content/examples/toh-pt6/example-config.json @@ -1,6 +1,6 @@ { "tests": [ - {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadless", "--no-watch"]}, + {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadlessNoSandbox", "--no-watch"]}, {"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]} ] } diff --git a/aio/content/extended-diagnostics/NG8107.md b/aio/content/extended-diagnostics/NG8107.md new file mode 100644 index 000000000000..5d1f706d8a63 --- /dev/null +++ b/aio/content/extended-diagnostics/NG8107.md @@ -0,0 +1,86 @@ +@name Optional chain not nullable + +@description + +This diagnostic detects when the left side of an optional chain operation (`.?`) does not include `null` or `undefined` in its type in Angular templates. + + + +import {Component} from '@angular/core'; + +@Component({ + template: `
{{ foo?.bar }}
`, + // … +}) +class MyComponent { + // `foo` is declared as an object which *cannot* be `null` or `undefined`. + foo: { bar: string} = { bar: 'bar'}; +} + +
+ +## What should I do instead? + +Update the template and declared type to be in sync. Double-check the type of the input and confirm whether it is actually expected to be nullable. + +If the input should be nullable, add `null` or `undefined` to its type to indicate this. + + + +import {Component} from '@angular/core'; + +@Component({ + // If `foo` is nullish, `bar` won't be evaluated and the express will return the nullish value (`null` or `undefined`). + template: `
{{ foo?.bar }}
`, + // … +}) +class MyComponent { + foo: { bar: string} | null = { bar: 'bar'}; +} + +
+ +If the input should not be nullable, delete the `?` operator. + + + +import {Component} from '@angular/core'; + +@Component({ + // Template always displays `bar` as `foo` is guaranteed to never be `null` or `undefined` + template: `
{{ foo.bar }}
`, + // … +}) +class MyComponent { + foo: { bar: string} = { bar: 'bar'}; +} + +
+ +## What if I can't avoid this? + +This diagnostic can be disabled by editing the project's `tsconfig.json` file: + + + +{ + "angularCompilerOptions": { + "extendedDiagnostics": { + "checks": { + "optionalChainNotNullable": "suppress" + } + } + } +} + + + +See [extended diagnostic configuration](extended-diagnostics#configuration) for more info. + + + + + + + +@reviewed 2023-03-02 diff --git a/aio/content/extended-diagnostics/index.md b/aio/content/extended-diagnostics/index.md index c8d9dbf6a32a..497f65530324 100644 --- a/aio/content/extended-diagnostics/index.md +++ b/aio/content/extended-diagnostics/index.md @@ -11,9 +11,10 @@ Currently, Angular supports the following extended diagnostics: * [NG8101 - `invalidBananaInBox`](extended-diagnostics/NG8101) * [NG8102 - `nullishCoalescingNotNullable`](extended-diagnostics/NG8102) * [NG8103 - `missingControlFlowDirective`](extended-diagnostics/NG8103) +* [NG8104 - `textAttributeNotBinding`](extended-diagnostics/NG8104) * [NG8105 - `missingNgForOfLet`](extended-diagnostics/NG8105) * [NG8106 - `suffixNotSupported`](extended-diagnostics/NG8106) -* [NG8104 - `textAttributeNotBinding`](extended-diagnostics/NG8104) +* [NG8107 - `optionalChainNotNullable`](extended-diagnostics/NG8107) ## Configuration diff --git a/aio/content/guide/glossary.md b/aio/content/guide/glossary.md index abcb478e15ed..4d98b326dba8 100644 --- a/aio/content/guide/glossary.md +++ b/aio/content/guide/glossary.md @@ -929,7 +929,7 @@ View Engine was deprecated in version 9 and removed in version 13. ## view hierarchy A tree of related views that can be acted on as a unit. -The root view referenced as the *host view* of a component. +The root view is referenced as the *host view* of a component. A host view is the root of a tree of *embedded views*, collected in a `ViewContainerRef` view container attached to an anchor element in the hosting component. The view hierarchy is a key part of Angular [change detection][AioGuideGlossaryChangeDetection]. diff --git a/aio/content/guide/testing-components-scenarios.md b/aio/content/guide/testing-components-scenarios.md index 0d6b99229941..192398a9fe13 100644 --- a/aio/content/guide/testing-components-scenarios.md +++ b/aio/content/guide/testing-components-scenarios.md @@ -812,22 +812,13 @@ Testing the `DashboardComponent` seemed daunting in part because it involves the -Mocking the `HeroService` with a spy is a [familiar story](#component-with-async-service). -But the `Router` has a complicated API and is entwined with other services and application preconditions. -Might it be difficult to mock? - -Fortunately, not in this case because the `DashboardComponent` isn't doing much with the `Router` - -This is often the case with *routing components*. -As a rule you test the component, not the router, and care only if the component navigates with the right address under the given conditions. +Angular provides test helpers to reduce boilerplate and more effectively test code which depends on the Router and HttpClient. -Providing a router spy for *this component* test suite happens to be as easy as providing a `HeroService` spy. + - - -The following test clicks the displayed hero and confirms that `Router.navigateByUrl` is called with the expected url. +The following test clicks the displayed hero and confirms that we navigate to the expected URL. @@ -863,36 +854,9 @@ The [ActivatedRoute in action](guide/router-tutorial-toh#activated-route-in-acti
-Tests can explore how the `HeroDetailComponent` responds to different `id` parameter values by manipulating the `ActivatedRoute` injected into the component's constructor. - -You know how to spy on the `Router` and a data service. - -You'll take a different approach with `ActivatedRoute` because - -* `paramMap` returns an `Observable` that can emit more than one value during a test -* You need the router helper function, `convertToParamMap()`, to create a `ParamMap` -* Other *routed component* tests need a test double for `ActivatedRoute` - -These differences argue for a re-usable stub class. - -#### `ActivatedRouteStub` - -The following `ActivatedRouteStub` class serves as a test double for `ActivatedRoute`. - - - -Consider placing such helpers in a `testing` folder sibling to the `app` folder. -This sample puts `ActivatedRouteStub` in `testing/activated-route-stub.ts`. - -
- -Consider writing a more capable version of this stub class with the [*marble testing library*](#marble-testing). - -
- - +Tests can explore how the `HeroDetailComponent` responds to different `id` parameter values by navigating to different routes. -#### Testing with `ActivatedRouteStub` +#### Testing with the `RouterTestingHarness` Here's a test demonstrating the component's behavior when the observed `id` refers to an existing hero: @@ -907,21 +871,12 @@ Rely on your intuition for now. When the `id` cannot be found, the component should re-route to the `HeroListComponent`. -The test suite setup provided the same router spy [described above](#routing-component) which spies on the router without actually navigating. +The test suite setup provided the same router harness [described above](#routing-component). This test expects the component to try to navigate to the `HeroListComponent`. -While this application doesn't have a route to the `HeroDetailComponent` that omits the `id` parameter, it might add such a route someday. -The component should do something reasonable when there is no `id`. - -In this implementation, the component should create and display a new hero. -New heroes have `id=0` and a blank `name`. -This test confirms that the component behaves as expected: - - - ## Nested component tests Component templates often have nested components, whose templates might contain more components. @@ -932,8 +887,6 @@ The `AppComponent`, for example, displays a navigation bar with anchors and thei -While the `AppComponent` *class* is empty, you might want to write unit tests to confirm that the links are wired properly to the `RouterLink` directives, perhaps for the reasons as explained in the [following section](#why-stubbed-routerlink-tests). - To validate the links, you don't need the `Router` to navigate and you don't need the `` to mark where the `Router` inserts *routed components*. The `BannerComponent` and `WelcomeComponent` \(indicated by `` and ``\) are also irrelevant. @@ -966,8 +919,6 @@ Then declare them in the `TestBed` configuration next to the components, directi The `AppComponent` is the test subject, so of course you declare the real version. -The `RouterLinkDirectiveStub`, [described later](#routerlink), is a test version of the real `RouterLink` that helps with the link tests. - The rest are stubs. @@ -980,7 +931,7 @@ In the second approach, add `NO_ERRORS_SCHEMA` to the `TestBed.schemas` metadata The `NO_ERRORS_SCHEMA` tells the Angular compiler to ignore unrecognized elements and attributes. -The compiler recognizes the `` element and the `routerLink` attribute because you declared a corresponding `AppComponent` and `RouterLinkDirectiveStub` in the `TestBed` configuration. +The compiler recognizes the `` element and the `routerLink` attribute because you declared a corresponding `AppComponent` and `RouterLink` in the `TestBed` configuration. But the compiler won't throw an error when it encounters ``, ``, or ``. It simply renders them as empty tags and the browser ignores them. @@ -1003,31 +954,7 @@ In practice you will combine the two techniques in the same setup, as seen in th -The Angular compiler creates the `BannerStubComponent` for the `` element and applies the `RouterLinkStubDirective` to the anchors with the `routerLink` attribute, but it ignores the `` and `` tags. - - - -## Components with `RouterLink` - -The real `RouterLinkDirective` is quite complicated and entangled with other components and directives of the `RouterModule`. -It requires challenging setup to mock and use in tests. - -The `RouterLinkDirectiveStub` in this sample code replaces the real directive with an alternative version designed to validate the kind of anchor tag wiring seen in the `AppComponent` template. - - - -The URL bound to the `[routerLink]` attribute flows in to the directive's `linkParams` property. - -The `HostListener` wires the click event of the host element \(the `` anchor elements in `AppComponent`\) to the stub directive's `onClick` method. - -Clicking the anchor should trigger the `onClick()` method, which sets the stub's telltale `navigatedTo` property. -Tests inspect `navigatedTo` to confirm that clicking the anchor sets the expected route definition. - -
- -Whether the router is configured properly to navigate with that route definition is a question for a separate set of tests. - -
+The Angular compiler creates the `BannerStubComponent` for the `` element and applies the `RouterLink` to the anchors with the `routerLink` attribute, but it ignores the `` and `` tags.
@@ -1054,39 +981,6 @@ Here are some tests that confirm those links are wired to the `routerLink` direc -
- -The "click" test *in this example* is misleading. -It tests the `RouterLinkDirectiveStub` rather than the *component*. -This is a common failing of directive stubs. - -It has a legitimate purpose in this guide. -It demonstrates how to find a `RouterLink` element, click it, and inspect a result, without engaging the full router machinery. -This is a skill you might need to test a more sophisticated component, one that changes the display, re-calculates parameters, or re-arranges navigation options when the user clicks the link. - -
- - - -#### What good are these tests? - -Stubbed `RouterLink` tests can confirm that a component with links and an outlet is set up properly, that the component has the links it should have, and that they are all pointing in the expected direction. -These tests do not concern whether the application will succeed in navigating to the target component when the user clicks a link. - -Stubbing the RouterLink and RouterOutlet is the best option for such limited testing goals. -Relying on the real router would make them brittle. -They could fail for reasons unrelated to the component. -For example, a navigation guard could prevent an unauthorized user from visiting the `HeroListComponent`. -That's not the fault of the `AppComponent` and no change to that component could cure the failed test. - -A *different* battery of tests can explore whether the application navigates as expected in the presence of conditions that influence guards such as whether the user is authenticated and authorized. - -
- -A future guide update explains how to write such tests with the `RouterTestingModule`. - -
- ## Use a `page` object @@ -1126,9 +1020,6 @@ A `createComponent` method creates a `page` object and fills in the blanks once -The [`HeroDetailComponent` tests](#tests-w-test-double) in an earlier section demonstrate how `createComponent` and `page` keep the tests short and *on message*. -There are no distractions: no waiting for promises to resolve and no searching the DOM for element values to compare. - Here are a few more `HeroDetailComponent` tests to reinforce the point. @@ -1263,8 +1154,8 @@ In addition to the support it receives from the default testing module `CommonMo * `NgModel` and friends in the `FormsModule` to enable two-way data binding * The `TitleCasePipe` from the `shared` folder -* The Router services that these tests are stubbing out -* The Hero data access services that are also stubbed out +* The Router services +* The Hero data access services One approach is to configure the testing module from the individual pieces as in this example: @@ -1297,7 +1188,6 @@ Try a test configuration that imports the `HeroModule` like this one: -That's *really* crisp. Only the *test doubles* in the `providers` remain. Even the `HeroDetailComponent` declaration is gone. diff --git a/aio/content/tutorial/tour-of-heroes/toh-pt5.md b/aio/content/tutorial/tour-of-heroes/toh-pt5.md index 806de68a54d7..67275f59ce5f 100644 --- a/aio/content/tutorial/tour-of-heroes/toh-pt5.md +++ b/aio/content/tutorial/tour-of-heroes/toh-pt5.md @@ -206,7 +206,7 @@ This `getHeroes()` returns the sliced list of heroes at positions 1 and 5, retur To navigate to the dashboard, the router needs an appropriate route. -Import the `DashboardComponent` in the `app-routing-module.ts` file. +Import the `DashboardComponent` in the `app-routing.module.ts` file. diff --git a/aio/package.json b/aio/package.json index d9b3ea0dab81..f455deae104c 100644 --- a/aio/package.json +++ b/aio/package.json @@ -145,7 +145,7 @@ "lunr": "^2.3.9", "npm-run-all": "^4.1.5", "protractor": "~7.0.0", - "puppeteer-core": "19.5.2", + "puppeteer-core": "19.7.2", "rehype-slug": "^4.0.1", "remark": "^12.0.0", "remark-html": "^13.0.0", diff --git a/aio/tools/examples/example-sandbox.mjs b/aio/tools/examples/example-sandbox.mjs index 99227e04152f..cef6b3ce476a 100644 --- a/aio/tools/examples/example-sandbox.mjs +++ b/aio/tools/examples/example-sandbox.mjs @@ -25,7 +25,7 @@ export async function constructExampleSandbox(examplePath, destPath, nodeModules await constructSymlinkedNodeModules(destPath, nodeModulesPath, localPackages); // Add preserveSymlinks fixups to various files --- needed when linkin in local deps - preserveSymlinksWhenUsingLocalPackages(localPackages, destPath); + ensurePreserveSymlinks(destPath); } async function constructSymlinkedNodeModules(examplePath, exampleDepsNodeModules, localPackages) { @@ -132,32 +132,29 @@ function pointBinSymlinksToLocalPackages(linkedNodeModules, exampleDepsNodeModul }); } -// When local packages are symlinked in, node has trouble resolving some peer deps. Setting -// preserveSymlinks in relevant files fixes this. This isn't required without local packages -// because in the worst case we would leak into the original Bazel repository and it would -// still find a node_modules folder for resolution. Add the preserveSymlinks options to various -// files that are used by the cli and systemjs tests (and sometimes both). -function preserveSymlinksWhenUsingLocalPackages(LOCAL_PACKAGES, appDir) { - if (Object.keys(LOCAL_PACKAGES).length === 0) { - return; - } - +/** + * When local packages are symlinked in, node will by default resolve local packages to + * their output location in the `bazel-bin`. This will then cause transitive dependencies + * to be incorrectly resolved from `bazel-bin`, instead of from within the example sandbox. + * + * Setting `preserveSymlinks` in relevant files fixes this. Note that we are intending to + * preserve symlinks in general (regardless of local packages being used), because it + * allows us to safely enable `NODE_PRESERVE_SYMLINKS=1` when executing commands inside. + */ +function ensurePreserveSymlinks(appDir) { // Set preserveSymlinks in angular.json const angularJsonPath = path.join(appDir, 'angular.json'); if (fs.existsSync(angularJsonPath)) { - const angularJson = jsonc.load(angularJsonPath, { - encoding: 'utf-8' - }); + const angularJson = jsonc.load(angularJsonPath, {encoding: 'utf-8'}); angularJson.projects['angular.io-example'].architect.build.options.preserveSymlinks = true; + angularJson.projects['angular.io-example'].architect.test.options.preserveSymlinks = true; fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, undefined, 2)); } // Set preserveSymlinks in any tsconfig.json files const tsConfigPaths = globbySync([path.join(appDir, 'tsconfig*.json')]); for (const tsConfigPath of tsConfigPaths) { - const tsConfig = jsonc.load(tsConfigPath, { - encoding: 'utf-8' - }); + const tsConfig = jsonc.load(tsConfigPath, {encoding: 'utf-8'}); const isRootConfig = !tsConfig.extends; if (isRootConfig) { tsConfig.compilerOptions.preserveSymlinks = true; @@ -167,9 +164,7 @@ function preserveSymlinksWhenUsingLocalPackages(LOCAL_PACKAGES, appDir) { // Call rollup with --preserveSymlinks const packageJsonPath = path.join(appDir, 'package.json'); - const packageJson = jsonc.load(packageJsonPath, { - encoding: 'utf-8' - }); + const packageJson = jsonc.load(packageJsonPath, {encoding: 'utf-8'}); if ('rollup' in packageJson.dependencies || 'rollup' in packageJson.devDependencies) { packageJson.scripts.rollup = 'rollup --preserveSymlinks'; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, undefined, 2)); diff --git a/aio/tools/examples/run-example-e2e.mjs b/aio/tools/examples/run-example-e2e.mjs index 217863299baf..924c49ebdec2 100644 --- a/aio/tools/examples/run-example-e2e.mjs +++ b/aio/tools/examples/run-example-e2e.mjs @@ -1,15 +1,17 @@ import path from 'canonical-path'; import {spawn} from 'cross-spawn'; import fs from 'fs-extra'; +import getPort from 'get-port'; import {globbySync} from 'globby'; import os from 'os'; import shelljs from 'shelljs'; import treeKill from 'tree-kill'; import yargs from 'yargs'; import {hideBin} from 'yargs/helpers' -import getPort from 'get-port'; -import {constructExampleSandbox} from "./example-sandbox.mjs"; -import { getAdjustedChromeBinPathForWindows } from '../windows-chromium-path.js'; + +import {getAdjustedChromeBinPathForWindows} from '../windows-chromium-path.js'; + +import {constructExampleSandbox} from './example-sandbox.mjs'; shelljs.set('-e'); @@ -20,11 +22,17 @@ process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows(); process.env.CHROME_BIN = path.resolve(process.env.CHROME_BIN); process.env.CHROMEDRIVER_BIN = path.resolve(process.env.CHROMEDRIVER_BIN); -const {argv} = yargs(hideBin(process.argv)) - .command("* ") - .option('localPackage', {array: true, type: 'string', default: [], describe: 'Locally built package to substitute, in the form `packageName#packagePath`'}) - .version(false) - .strict(); +const {argv} = + yargs(hideBin(process.argv)) + .command('* ') + .option('localPackage', { + array: true, + type: 'string', + default: [], + describe: 'Locally built package to substitute, in the form `packageName#packagePath`' + }) + .version(false) + .strict(); const EXAMPLE_PATH = path.resolve(argv.examplePath); const NODE = process.execPath; @@ -44,13 +52,15 @@ const MAX_NO_OUTPUT_TIMEOUT = 1000 * 60 * 5; // 5 minutes /** * Run Protractor End-to-End Tests for a Docs Example * - * Usage: node run-example-e2e.mjs [localPackage...] + * Usage: node run-example-e2e.mjs + * [localPackage...] * * Args: * examplePath: path to the example * yarnPath: path to a vendored version of yarn * exampleDepsWorkspaceName: name of bazel workspace containing example node_omodules - * localPackages: a vararg of local packages to substitute in place npm deps, in the form @package/name#pathToPackage. + * localPackages: a vararg of local packages to substitute in place npm deps, in the + * form @package/name#pathToPackage. * * Flags * --retry to retry failed tests (useful for overcoming flakes) @@ -62,9 +72,12 @@ async function runE2e(examplePath) { const maxAttempts = argv.retry || 1; const exampleTestPath = generatePathForExampleTest(exampleName); - try { + console.info('Running example tests in directory: ', exampleTestPath) - await constructExampleSandbox(examplePath, exampleTestPath, path.resolve('..', EXAMPLE_DEPS_WORKSPACE_NAME, 'node_modules'), LOCAL_PACKAGES); + try { + await constructExampleSandbox( + examplePath, exampleTestPath, + path.resolve('..', EXAMPLE_DEPS_WORKSPACE_NAME, 'node_modules'), LOCAL_PACKAGES); let testFn; if (isSystemJsTest(exampleTestPath)) { @@ -74,7 +87,7 @@ async function runE2e(examplePath) { } else { throw new Error(`Unknown e2e test type for example ${exampleName}`); } - + await attempt(testFn, maxAttempts); } catch (e) { console.error(e); @@ -90,7 +103,12 @@ async function attempt(testFn, maxAttempts) { while (true) { attempts++; - passed = await testFn(); + passed = true; + try { + await testFn(); + } catch (e) { + passed = false; + } if (passed || (attempts >= maxAttempts)) break; } @@ -125,13 +143,15 @@ async function runE2eTestsSystemJS(exampleName, appDir) { const appBuildSpawnInfo = spawnExt(NODE, [VENDORED_YARN, config.build], {cwd: appDir}); const appRunSpawnInfo = spawnExt(NODE, [VENDORED_YARN, ...runArgs, '-s'], {cwd: appDir}, true); - let run = runProtractorSystemJS(exampleName, appBuildSpawnInfo.promise, appDir, appRunSpawnInfo); + try { + await runProtractorSystemJS(exampleName, appBuildSpawnInfo.promise, appDir); + } finally { + treeKill(appRunSpawnInfo.proc.pid); + } if (fs.existsSync(appDir + '/aot/index.html')) { - run = run.then((ok) => ok && runProtractorAoT(exampleName, appDir)); + await runProtractorAoT(exampleName, appDir); } - - return run; } // The SystemJS examples spawn an http server and protractor using a hardcoded @@ -147,7 +167,7 @@ async function overrideSystemJsExampleToUseRandomPort(exampleConfig, exampleDir) const isUsingHttpServerLibrary = exampleConfig.run === 'serve:upgrade'; if (isUsingHttpServerLibrary) { // Override the port in http-server by passing as an argument - runArgs = [...runArgs, "-p", freePort]; + runArgs = [...runArgs, '-p', freePort]; } // Override the port in any lite-server config files @@ -162,53 +182,23 @@ async function overrideSystemJsExampleToUseRandomPort(exampleConfig, exampleDir) // Override hardcoded port in protractor.config.js let protractorConfig = fs.readFileSync(path.join(exampleDir, 'protractor.config.js'), 'utf8'); - protractorConfig = protractorConfig.replaceAll('http://localhost:8080', `http://localhost:${freePort}`); + protractorConfig = + protractorConfig.replaceAll('http://localhost:8080', `http://localhost:${freePort}`); fs.writeFileSync(path.join(exampleDir, 'protractor.config.js'), protractorConfig); return runArgs; } -function runProtractorSystemJS(exampleName, prepPromise, appDir, appRunSpawnInfo) { - const specFilename = path.resolve(`${appDir}/${SJS_SPEC_FILENAME}`); - return prepPromise - .catch(() => { - const emsg = `Application at ${appDir} failed to transpile.\n\n`; - console.log(emsg); - return Promise.reject(emsg); - }) - .then(() => { - let transpileError = false; - - // Start protractor. - console.log(`\n\n=========== Running aio example tests for: ${exampleName}`); - const spawnInfo = spawnExt(NODE, [VENDORED_YARN, 'protractor'], {cwd: appDir}); - - spawnInfo.proc.stderr.on('data', function(data) { - transpileError = transpileError || /npm ERR! Exit status 100/.test(data.toString()); - }); - return spawnInfo.promise.catch(function() { - if (transpileError) { - const emsg = `${specFilename} failed to transpile.\n\n`; - console.log(emsg); - } - return Promise.reject(); - }); - }) - .then( - () => finish(appRunSpawnInfo.proc.pid, true), - () => finish(appRunSpawnInfo.proc.pid, false) - ); -} +async function runProtractorSystemJS(exampleName, prepPromise, appDir) { + await prepPromise; -function finish(spawnProcId, ok) { - // Ugh... proc.kill does not work properly on windows with child processes. - // appRun.proc.kill(); - treeKill(spawnProcId); - return ok; + // Wait for the app to be running. Then we can start Protractor tests. + console.log(`\n\n=========== Running aio example tests for: ${exampleName}`); + await spawnExt(NODE, [VENDORED_YARN, 'protractor'], {cwd: appDir}).promise; } // Run e2e tests over the AOT build for projects that examples it. -function runProtractorAoT(exampleName, appDir) { +async function runProtractorAoT(exampleName, appDir) { const aotBuildSpawnInfo = spawnExt(NODE, [VENDORED_YARN, 'build:aot'], {cwd: appDir}); let promise = aotBuildSpawnInfo.promise; @@ -216,20 +206,28 @@ function runProtractorAoT(exampleName, appDir) { if (fs.existsSync(appDir + '/' + copyFileCmd)) { promise = promise.then(() => spawnExt('node', [copyFileCmd], {cwd: appDir}).promise); } + + // Run the server in the background. Will be killed upon test completion. const aotRunSpawnInfo = spawnExt(NODE, [VENDORED_YARN, 'serve:aot'], {cwd: appDir}, true); - return runProtractorSystemJS(exampleName, promise, appDir, aotRunSpawnInfo); + + try { + await runProtractorSystemJS(exampleName, promise, appDir); + } finally { + treeKill(aotRunSpawnInfo.proc.pid); + } } // Start the example in appDir; then run protractor with the specified // fileName; then shut down the example. // All protractor output is appended to the outputFile. // CLI version -function runE2eTestsCLI(exampleName, appDir) { +async function runE2eTestsCLI(exampleName, appDir) { console.log(`\n\n=========== Running aio example tests for: ${exampleName}`); const config = loadExampleConfig(appDir); - // Replace any calls with yarn (which requires yarn to be on the PATH) to instead call our vendored yarn + // Replace any calls with yarn (which requires yarn to be on the PATH) to instead call our + // vendored yarn if (config.tests) { for (let test of config.tests) { if (test.cmd === 'yarn') { @@ -241,9 +239,9 @@ function runE2eTestsCLI(exampleName, appDir) { // `--no-webdriver-update` is needed to preserve the ChromeDriver version already installed. const testCommands = config.tests || [{ - cmd: NODE, + cmd: NODE, args: [ - VENDORED_YARN, + VENDORED_YARN, 'e2e', '--configuration=production', '--protractor-config=e2e/protractor-bazel.conf.js', @@ -252,23 +250,16 @@ function runE2eTestsCLI(exampleName, appDir) { ], }]; - const e2eSpawnPromise = testCommands.reduce((prevSpawnPromise, {cmd, args}) => { - return prevSpawnPromise.then(() => { - const currSpawn = spawnExt( - cmd, args, {cwd: appDir}, false); - return currSpawn.promise.then( - () => finish(currSpawn.proc.pid, true), - () => finish(currSpawn.proc.pid, false), - ) - }); - }, Promise.resolve()); - return e2eSpawnPromise; + for (const {cmd, args} of testCommands) { + await spawnExt(cmd, args, {cwd: appDir}, false).promise; + } } // Returns both a promise and the spawned process so that it can be killed if needed. function spawnExt( - command, args, options, ignoreClose = false, printMessageFn = msg => process.stdout.write(msg)) { + command, args, options, ignoreClose = false, + printMessageFn = msg => process.stdout.write(msg)) { let proc = null; const promise = new Promise((resolveFn, rejectFn) => { let noOutputTimeoutId; @@ -293,7 +284,15 @@ function spawnExt( let descr = command + ' ' + args.join(' '); printMessage(`running: ${descr}\n`); try { - proc = spawn(command, args, options); + proc = spawn(command, args, { + // All NodeJS scripts executed for running example e2e tests should preserve symlinks. + // This is important as otherwise test commands like `yarn ng build` would escape from the + // example sandbox into the `bazel-bin` where ultimately incorrect versions of local + // framework packages might be resolved. e.g. the `@angular/compiler-cli` version is never + // the one locally built. + env: {...process.env, NODE_PRESERVE_SYMLINKS: '1'}, + ...options + }); } catch (e) { console.log(e); return reject(e); diff --git a/aio/tools/examples/shared/boilerplate/cli/e2e/protractor-bazel.conf.js b/aio/tools/examples/shared/boilerplate/cli/e2e/protractor-bazel.conf.js index 05e3032ce183..816c83bd84d4 100644 --- a/aio/tools/examples/shared/boilerplate/cli/e2e/protractor-bazel.conf.js +++ b/aio/tools/examples/shared/boilerplate/cli/e2e/protractor-bazel.conf.js @@ -13,8 +13,12 @@ exports.config = { chromeOptions: { ...config.capabilities.chromeOptions, binary: process.env.CHROME_BIN, - // See /integration/README.md#browser-tests for more info on these args - args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'], + // See /integration/README.md#browser-tests for more info on these args. + // Bazel tests run within a sandbox already and Chrome cannot have its own sandbox too. + args: [ + '--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', + '--hide-scrollbars', '--mute-audio' + ], }, }, }; diff --git a/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js b/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js index 876674beff8a..a52dccf59940 100644 --- a/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js +++ b/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js @@ -3,9 +3,32 @@ module.exports = function processErrorsContainerDoc() { $runAfter: ['extra-docs-added'], $runBefore: ['rendering-docs'], $process(docs) { - const errorsDoc = docs.find(doc => doc.id === 'errors/index'); + const errorsDoc = docs.find((doc) => doc.id === 'errors/index'); errorsDoc.id = 'errors-container'; - errorsDoc.errors = docs.filter(doc => doc.docType === 'error'); - } + errorsDoc.errors = [ + { + title: 'Runtime', + errors: docs + .filter((doc) => doc.docType === 'error' && doc.category === 'runtime') + .sort(byCode), + }, + { + title: 'Compiler', + errors: docs + .filter((doc) => doc.docType === 'error' && doc.category === 'compiler') + .sort(byCode), + }, + ]; + }, }; }; + +/** + * Helper function to sort documents by error codes (NG0100 will come before NG0200) + */ +function byCode(doc1, doc2) { + // slice to drop the 'NG' part of the code. + const code1 = +doc1.code.slice(2); + const code2 = +doc2.code.slice(2); + return code1 > code2 ? 1 : -1; +} diff --git a/aio/tools/transforms/templates/error/errors-container.template.html b/aio/tools/transforms/templates/error/errors-container.template.html index 606a9f671621..18eacec782dc 100644 --- a/aio/tools/transforms/templates/error/errors-container.template.html +++ b/aio/tools/transforms/templates/error/errors-container.template.html @@ -1,18 +1,17 @@ -{% extends 'content.template.html' -%} +{% extends 'content.template.html' -%} {% block content %} +
{$ doc.description | marked $}
-{% block content %} -
- {$ doc.description | marked $} -
- - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/aio/yarn.lock b/aio/yarn.lock index 59047a5b3ee6..f95d27059e8f 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -5117,6 +5117,13 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== +chromium-bidi@0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.4.tgz#44f25d4fa5d2f3debc3fc3948d0657194cac4407" + integrity sha512-4BX5cSaponuvVT1+SbLYTOAgDoVtX/Khoc9UsbFJ/AsPVUeFAM3RiIDFI6XFhLYMi9WmVJqh1ZH+dRpNKkKwiQ== + dependencies: + mitt "3.0.0" + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -5952,10 +5959,10 @@ dev-ip@^1.0.1: resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" integrity sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A== -devtools-protocol@0.0.1068969: - version "0.0.1068969" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1068969.tgz#8b9a4bc48aed1453bed08d62b07481f9abf4d6d8" - integrity sha512-ATFTrPbY1dKYhPPvpjtwWKSK2mIwGmRwX54UASn9THEuIZCe2n9k3vVuMmt6jWeL+e5QaaguEv/pMyR+JQB7VQ== +devtools-protocol@0.0.1094867: + version "0.0.1094867" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1094867.tgz#2ab93908e9376bd85d4e0604aa2651258f13e374" + integrity sha512-pmMDBKiRVjh0uKK6CT1WqZmM3hBVSgD+N2MrgyV1uNizAZMw4tx6i/RTc+/uCsKSCmg0xXx7arCP/OFcIwTsiQ== devtools-protocol@0.0.981744: version "0.0.981744" @@ -10308,6 +10315,11 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" +mitt@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" + integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== + mitt@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" @@ -11527,14 +11539,15 @@ pupa@^2.0.1, pupa@^2.1.1: dependencies: escape-goat "^2.0.0" -puppeteer-core@19.5.2: - version "19.5.2" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.5.2.tgz#9b454b0ef89d3f07e20158dd4ced6ebd85d4dadb" - integrity sha512-Rqk+3kqM+Z2deooTYqcYt8lRtGffJdifWa9td9nbJSjhANWsFouk8kLBNUKycewCCFHM8TZUKS0x28OllavW2A== +puppeteer-core@19.7.2: + version "19.7.2" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.7.2.tgz#deee9ef915829b6a1d1a3a008625c29eeb251161" + integrity sha512-PvI+fXqgP0uGJxkyZcX51bnzjFA73MODZOAv0fSD35yR7tvbqwtMV3/Y+hxQ0AMMwzxkEebP6c7po/muqxJvmQ== dependencies: + chromium-bidi "0.4.4" cross-fetch "3.1.5" debug "4.3.4" - devtools-protocol "0.0.1068969" + devtools-protocol "0.0.1094867" extract-zip "2.0.1" https-proxy-agent "5.0.1" proxy-from-env "1.1.0" diff --git a/devtools/projects/shell-browser/src/app/background.ts b/devtools/projects/shell-browser/src/app/background.ts index d67ffc699c6e..9e7bd12a346d 100644 --- a/devtools/projects/shell-browser/src/app/background.ts +++ b/devtools/projects/shell-browser/src/app/background.ts @@ -26,13 +26,15 @@ const browserAction = (() => { // By default use the black and white icon. // Replace it only when we detect an Angular app. -browserAction.setIcon({ - path: { - 16: chrome.runtime.getURL(`assets/icon-bw16.png`), - 48: chrome.runtime.getURL(`assets/icon-bw48.png`), - 128: chrome.runtime.getURL(`assets/icon-bw128.png`), - }, -}); +browserAction.setIcon( + { + path: { + 16: chrome.runtime.getURL(`assets/icon-bw16.png`), + 48: chrome.runtime.getURL(`assets/icon-bw48.png`), + 128: chrome.runtime.getURL(`assets/icon-bw128.png`), + }, + }, + () => {}); const ports: { [tab: string]: @@ -181,13 +183,15 @@ chrome.runtime.onMessage.addListener((req, sender) => { }); } if (sender && sender.tab && req.isAngular) { - browserAction.setIcon({ - tabId: sender.tab.id, - path: { - 16: chrome.runtime.getURL(`assets/icon16.png`), - 48: chrome.runtime.getURL(`assets/icon48.png`), - 128: chrome.runtime.getURL(`assets/icon128.png`), - }, - }); + browserAction.setIcon( + { + tabId: sender.tab.id, + path: { + 16: chrome.runtime.getURL(`assets/icon16.png`), + 48: chrome.runtime.getURL(`assets/icon48.png`), + 128: chrome.runtime.getURL(`assets/icon128.png`), + }, + }, + () => {}); } }); diff --git a/integration/cli-hello-world-ivy-i18n/package.json b/integration/cli-hello-world-ivy-i18n/package.json index c2a428bfd110..0d25037aa995 100644 --- a/integration/cli-hello-world-ivy-i18n/package.json +++ b/integration/cli-hello-world-ivy-i18n/package.json @@ -24,7 +24,6 @@ "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", "@angular/router": "file:../../dist/packages-dist/router", - "core-js": "file:../../node_modules/core-js", "rxjs": "file:../../node_modules/rxjs", "tslib": "file:../../node_modules/tslib", "zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz" diff --git a/integration/dynamic-compiler/package.json b/integration/dynamic-compiler/package.json index e62192c77169..90dc008b9281 100644 --- a/integration/dynamic-compiler/package.json +++ b/integration/dynamic-compiler/package.json @@ -34,7 +34,6 @@ "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", "@angular/platform-server": "file:../../dist/packages-dist/platform-server", - "core-js": "file:../../node_modules/core-js", "rxjs": "file:../../node_modules/rxjs", "zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz" } diff --git a/integration/npm_package_archives.bzl b/integration/npm_package_archives.bzl index 94a6458a9700..9cec413494b7 100644 --- a/integration/npm_package_archives.bzl +++ b/integration/npm_package_archives.bzl @@ -14,7 +14,6 @@ NPM_PACKAGE_ARCHIVES = [ "@rollup/plugin-node-resolve", "@rollup/plugin-commonjs", "check-side-effects", - "core-js", "google-closure-compiler", "jasmine", "typescript", diff --git a/karma-js.conf.js b/karma-js.conf.js index 92b37ec21891..f2c6df06117b 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -35,7 +35,6 @@ module.exports = function(config) { {pattern: 'node_modules/angular-1.8/angular?(.min).js', included: false, watched: false}, {pattern: 'node_modules/angular-mocks-1.8/angular-mocks.js', included: false, watched: false}, - 'node_modules/core-js-bundle/index.js', 'node_modules/jasmine-ajax/lib/mock-ajax.js', // Static test assets. diff --git a/package.json b/package.json index 7bc1dfe5c9a5..ad4864242957 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "15.2.1", + "version": "15.2.2", "private": true, "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular", @@ -24,7 +24,7 @@ "/ ": "", "postinstall": "node --preserve-symlinks --preserve-symlinks-main ./tools/postinstall-patches.js && patch-package --patch-dir tools/esm-interop/patches/npm", "prepare": "husky install", - "ng-dev": "cross-env TS_NODE_PROJECT=$PWD/.ng-dev/tsconfig.json TS_NODE_TRANSPILE_ONLY=1 node --no-warnings --loader ts-node/esm node_modules/@angular/ng-dev/bundles/cli.mjs", + "ng-dev": "ts-node --esm --project .ng-dev/tsconfig.json --transpile-only node_modules/@angular/ng-dev/bundles/cli.mjs", "build": "ts-node --esm --project scripts/tsconfig.json scripts/build/build-packages-dist.mts", "test": "bazelisk test", "test:ci": "bazelisk test -- //... -//devtools/... -//aio/...", @@ -88,7 +88,7 @@ "@types/babel__traverse": "7.18.3", "@types/base64-js": "1.3.0", "@types/bluebird": "^3.5.27", - "@types/chrome": "^0.0.208", + "@types/chrome": "^0.0.218", "@types/convert-source-map": "^1.5.1", "@types/diff": "^5.0.0", "@types/filesystem": "^0.0.32", @@ -121,7 +121,6 @@ "chalk": "^4.1.0", "chokidar": "^3.5.1", "convert-source-map": "^1.5.1", - "core-js-bundle": "^3.10.2", "d3": "^7.0.0", "dependency-graph": "^0.11.0", "diff": "^5.0.0", @@ -140,8 +139,8 @@ "karma-firefox-launcher": "^2.1.0", "karma-jasmine": "^5.0.0", "karma-requirejs": "^1.1.0", - "karma-sourcemap-loader": "^0.3.7", - "magic-string": "0.27.0", + "karma-sourcemap-loader": "^0.4.0", + "magic-string": "0.30.0", "memo-decorator": "^2.0.1", "ngx-flamegraph": "0.0.11", "nodejs-websocket": "^1.7.2", @@ -186,7 +185,6 @@ "cldr": "7.3.0", "cldrjs": "0.5.5", "conventional-changelog": "^3.1.24", - "cross-env": "^7.0.3", "firebase-tools": "^11.0.0", "glob": "8.1.0", "gulp": "^4.0.2", @@ -199,7 +197,7 @@ "prettier": "^2.5.1", "sauce-connect": "https://saucelabs.com/downloads/sc-4.8.1-linux.tar.gz", "semver": "^7.3.5", - "ts-node": "^10.8.1", + "ts-node": "^10.9.1", "tsec": "0.2.6", "tslint-eslint-rules": "5.4.0", "tslint-no-toplevel-property-access": "0.0.2", diff --git a/packages/animations/browser/src/dsl/animation_timeline_builder.ts b/packages/animations/browser/src/dsl/animation_timeline_builder.ts index e4e9b90bddea..b20800e1ebca 100644 --- a/packages/animations/browser/src/dsl/animation_timeline_builder.ts +++ b/packages/animations/browser/src/dsl/animation_timeline_builder.ts @@ -615,8 +615,7 @@ export class AnimationTimelineContext { export class TimelineBuilder { public duration: number = 0; - // TODO(issue/24571): remove '!'. - public easing!: string|null; + public easing: string|null = null; private _previousKeyframe: ɵStyleDataMap = new Map(); private _currentKeyframe: ɵStyleDataMap = new Map(); private _keyframes = new Map(); diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 9bbd8f5ee71b..eee66263a163 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -1499,8 +1499,7 @@ export class TransitionAnimationPlayer implements AnimationPlayer { private _queuedCallbacks = new Map any)[]>(); public readonly destroyed = false; - // TODO(issue/24571): remove '!'. - public parentPlayer!: AnimationPlayer; + public parentPlayer: AnimationPlayer|null = null; public markedForDestroy: boolean = false; public disabled = false; diff --git a/packages/animations/browser/src/render/web_animations/web_animations_player.ts b/packages/animations/browser/src/render/web_animations/web_animations_player.ts index 59d201eb08e9..bf772744a815 100644 --- a/packages/animations/browser/src/render/web_animations/web_animations_player.ts +++ b/packages/animations/browser/src/render/web_animations/web_animations_player.ts @@ -30,7 +30,7 @@ export class WebAnimationsPlayer implements AnimationPlayer { private _originalOnDoneFns: Function[] = []; private _originalOnStartFns: Function[] = []; - // TODO(issue/24571): remove '!'. + // using non-null assertion because it's re(set) by init(); public readonly domPlayer!: DOMAnimation; public time = 0; @@ -64,8 +64,8 @@ export class WebAnimationsPlayer implements AnimationPlayer { this._initialized = true; const keyframes = this.keyframes; - (this as {domPlayer: DOMAnimation}).domPlayer = - this._triggerWebAnimation(this.element, keyframes, this.options); + // @ts-expect-error overwriting a readonly property + this.domPlayer = this._triggerWebAnimation(this.element, keyframes, this.options); this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : new Map(); this.domPlayer.addEventListener('finish', () => this._onFinish()); } diff --git a/packages/bazel/package.json b/packages/bazel/package.json index 753939d7060a..580f0ad06f65 100644 --- a/packages/bazel/package.json +++ b/packages/bazel/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@microsoft/api-extractor": "^7.24.2", - "magic-string": "^0.27.0", + "magic-string": "^0.30.0", "tsickle": "^0.46.3", "tslib": "^2.3.0" }, diff --git a/packages/common/src/private_export.ts b/packages/common/src/private_export.ts index ed72cf357aff..c4b334cd1773 100644 --- a/packages/common/src/private_export.ts +++ b/packages/common/src/private_export.ts @@ -7,4 +7,3 @@ */ export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter'; -export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location'; diff --git a/packages/compiler-cli/private/tooling.ts b/packages/compiler-cli/private/tooling.ts index 372e00198ca8..90e12bc0a76c 100644 --- a/packages/compiler-cli/private/tooling.ts +++ b/packages/compiler-cli/private/tooling.ts @@ -45,5 +45,5 @@ export function constructorParametersDownlevelTransform(program: ts.Program): const reflectionHost = new TypeScriptReflectionHost(typeChecker); return getDownlevelDecoratorsTransform( typeChecker, reflectionHost, [], /* isCore */ false, - /* enableClosureCompiler */ false, /* skipClassDecorators */ true); + /* enableClosureCompiler */ false); } diff --git a/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts b/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts index a01a4371dcf9..26f46e18140f 100644 --- a/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts +++ b/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts @@ -242,23 +242,20 @@ interface ParameterDecorationInfo { } /** - * Gets a transformer for downleveling Angular decorators. + * Gets a transformer for downleveling Angular constructor parameter and property decorators. + * + * Note that Angular class decorators are never processed as those rely on side effects that + * would otherwise no longer be executed. i.e. the creation of a component definition. + * * @param typeChecker Reference to the program's type checker. * @param host Reflection host that is used for determining decorators. * @param diagnostics List which will be populated with diagnostics if any. * @param isCore Whether the current TypeScript program is for the `@angular/core` package. * @param isClosureCompilerEnabled Whether closure annotations need to be added where needed. - * @param skipClassDecorators Whether class decorators should be skipped from downleveling. - * This is useful for JIT mode where class decorators should be preserved as they could rely - * on immediate execution. e.g. downleveling `@Injectable` means that the injectable factory - * is not created, and injecting the token will not work. If this decorator would not be - * downleveled, the `Injectable` decorator will execute immediately on file load, and - * Angular will generate the corresponding injectable factory. */ export function getDownlevelDecoratorsTransform( typeChecker: ts.TypeChecker, host: ReflectionHost, diagnostics: ts.Diagnostic[], - isCore: boolean, isClosureCompilerEnabled: boolean, - skipClassDecorators: boolean): ts.TransformerFactory { + isCore: boolean, isClosureCompilerEnabled: boolean): ts.TransformerFactory { function addJSDocTypeAnnotation(node: ts.Node, jsdocType: string): void { if (!isClosureCompilerEnabled) { return; @@ -275,26 +272,6 @@ export function getDownlevelDecoratorsTransform( ]); } - /** - * Takes a list of decorator metadata object ASTs and produces an AST for a - * static class property of an array of those metadata objects. - */ - function createDecoratorClassProperty(decoratorList: ts.ObjectLiteralExpression[]) { - const modifier = ts.factory.createToken(ts.SyntaxKind.StaticKeyword); - const initializer = ts.factory.createArrayLiteralExpression(decoratorList, true); - // NB: the .decorators property does not get a @nocollapse property. There - // is no good reason why - it means .decorators is not runtime accessible - // if you compile with collapse properties, whereas propDecorators is, - // which doesn't follow any stringent logic. However this has been the - // case previously, and adding it back in leads to substantial code size - // increases as Closure fails to tree shake these props - // without @nocollapse. - const prop = ts.factory.createPropertyDeclaration( - [modifier], 'decorators', undefined, undefined, initializer); - addJSDocTypeAnnotation(prop, DECORATOR_INVOCATION_JSDOC_TYPE); - return prop; - } - /** * createPropDecoratorsClassProperty creates a static 'propDecorators' * property containing type information for every property that has a @@ -523,36 +500,15 @@ export function getDownlevelDecoratorsTransform( newMembers.push(ts.visitEachChild(member, decoratorDownlevelVisitor, context)); } - // The `ReflectionHost.getDecoratorsOfDeclaration()` method will not return certain kinds of - // decorators that will never be Angular decorators. So we cannot rely on it to capture all - // the decorators that should be kept. Instead we start off with a set of the raw decorators - // on the class, and only remove the ones that have been identified for downleveling. - const decoratorsToKeep = new Set(ts.getDecorators(classDecl)); + // Note: The `ReflectionHost.getDecoratorsOfDeclaration()` method will not + // return all decorators, but only ones that could be possible Angular decorators. const possibleAngularDecorators = host.getDecoratorsOfDeclaration(classDecl) || []; - let hasAngularDecorator = false; - const decoratorsToLower = []; - for (const decorator of possibleAngularDecorators) { - // We only deal with concrete nodes in TypeScript sources, so we don't - // need to handle synthetically created decorators. - const decoratorNode = decorator.node! as ts.Decorator; - const isNgDecorator = isAngularDecorator(decorator, isCore); - - // Keep track if we come across an Angular class decorator. This is used - // to determine whether constructor parameters should be captured or not. - if (isNgDecorator) { - hasAngularDecorator = true; - } + // Keep track if we come across an Angular class decorator. This is used + // to determine whether constructor parameters should be captured or not. + const hasAngularDecorator = + possibleAngularDecorators.some(d => isAngularDecorator(d, isCore)); - if (isNgDecorator && !skipClassDecorators) { - decoratorsToLower.push(extractMetadataFromSingleDecorator(decoratorNode, diagnostics)); - decoratorsToKeep.delete(decoratorNode); - } - } - - if (decoratorsToLower.length) { - newMembers.push(createDecoratorClassProperty(decoratorsToLower)); - } if (classParameters) { if (hasAngularDecorator || classParameters.some(p => !!p.decorators.length)) { // Capture constructor parameters if the class has Angular decorator applied, @@ -568,16 +524,10 @@ export function getDownlevelDecoratorsTransform( const members = ts.setTextRange( ts.factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma), classDecl.members); - const classModifiers = ts.getModifiers(classDecl); - let modifiers: ts.ModifierLike[]|undefined; - - if (decoratorsToKeep.size || classModifiers?.length) { - modifiers = [...decoratorsToKeep, ...(classModifiers || [])]; - } return ts.factory.updateClassDeclaration( - classDecl, modifiers, classDecl.name, classDecl.typeParameters, classDecl.heritageClauses, - members); + classDecl, classDecl.modifiers, classDecl.name, classDecl.typeParameters, + classDecl.heritageClauses, members); } /** diff --git a/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts b/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts index 50385715ee5f..b012bc06ff5f 100644 --- a/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts +++ b/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts @@ -22,7 +22,6 @@ describe('downlevel decorator transform', () => { let context: MockAotContext; let diagnostics: ts.Diagnostic[]; let isClosureEnabled: boolean; - let skipClassDecorators: boolean; beforeEach(() => { diagnostics = []; @@ -34,7 +33,6 @@ describe('downlevel decorator transform', () => { }); host = new MockCompilerHost(context); isClosureEnabled = false; - skipClassDecorators = false; }); function transform( @@ -61,7 +59,7 @@ describe('downlevel decorator transform', () => { ...preTransformers, getDownlevelDecoratorsTransform( program.getTypeChecker(), reflectionHost, diagnostics, - /* isCore */ false, isClosureEnabled, skipClassDecorators) + /* isCore */ false, isClosureEnabled) ] }; let output: string|null = null; @@ -97,13 +95,13 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyService.decorators = [ - { type: core_1.Injectable } - ]; MyService.ctorParameters = () => [ { type: ClassInject } - ];`); - expect(output).not.toContain('tslib'); + ]; + MyService = tslib_1.__decorate([ + (0, core_1.Injectable)() + ], MyService); + `); }); it('should downlevel decorators for @Directive decorated class', () => { @@ -120,13 +118,13 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: ClassInject } - ];`); - expect(output).not.toContain('tslib'); + ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); + `); }); it('should downlevel decorators for @Component decorated class', () => { @@ -143,13 +141,12 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyComp.decorators = [ - { type: core_1.Component, args: [{ template: 'hello' },] } - ]; MyComp.ctorParameters = () => [ { type: ClassInject } - ];`); - expect(output).not.toContain('tslib'); + ]; + MyComp = tslib_1.__decorate([ + (0, core_1.Component)({ template: 'hello' }) + ], MyComp);`); }); it('should downlevel decorators for @Pipe decorated class', () => { @@ -166,13 +163,13 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyPipe.decorators = [ - { type: core_1.Pipe, args: [{ selector: 'hello' },] } - ]; MyPipe.ctorParameters = () => [ { type: ClassInject } - ];`); - expect(output).not.toContain('tslib'); + ]; + MyPipe = tslib_1.__decorate([ + (0, core_1.Pipe)({ selector: 'hello' }) + ], MyPipe); + `); }); it('should not downlevel non-Angular class decorators', () => { @@ -243,10 +240,9 @@ describe('downlevel decorator transform', () => { // would be processed twice, where the downleveled class is revisited accidentally and // caused invalid generation of the `ctorParameters` static class member. it('should not duplicate constructor parameters for classes part of constructor body', () => { - // The bug with duplicated/invalid generation only surfaces when the actual class + // Note: the bug with duplicated/invalid generation only surfaces when the actual class // decorators are preserved and emitted by TypeScript itself. This setting is also // disabled within the CLI. - skipClassDecorators = true; const {output} = transform(` import {Injectable} from '@angular/core'; @@ -324,12 +320,13 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain('const other_file_1 = require("./other-file");'); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: other_file_1.MyOtherClass } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)(), + tslib_1.__metadata("design:paramtypes", [other_file_1.MyOtherClass]) + ], MyDir); `); }); @@ -350,12 +347,12 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain('const other_file_1 = require("./other-file");'); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: other_file_1.MyOtherClass } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); expect(dtsOutput).toContain('import'); }); @@ -375,12 +372,12 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain('const externalFile = require("./other-file");'); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: externalFile.MyOtherClass } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -401,12 +398,12 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain('var other;'); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: other.OtherClass } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -422,12 +419,12 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: Document, decorators: [{ type: core_1.Inject, args: [core_1.DOCUMENT,] }] } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -443,12 +440,12 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: core_1.NgZone, decorators: [{ type: core_1.Optional }] } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -467,10 +464,9 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - /** @type {!Array<{type: !Function, args: (undefined|!Array)}>} */ - MyDir.decorators = [ - { type: core_1.Directive } - ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); expect(output).toContain(dedent` /** @@ -484,7 +480,6 @@ describe('downlevel decorator transform', () => { { type: ClassInject } ]; `); - expect(output).not.toContain('tslib'); }); it('should not retain unused type imports due to decorator downleveling with ' + @@ -496,19 +491,18 @@ describe('downlevel decorator transform', () => { `); const {output} = transform( ` - import {Directive} from '@angular/core'; + import {Directive, Inject} from '@angular/core'; import {ErrorHandler, ClassInject} from './external'; - @Directive() export class MyDir { private _errorHandler: ErrorHandler; - constructor(v: ClassInject) {} + constructor(@Inject(ClassInject) i: ClassInject) {} } `, {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true}); expect(diagnostics.length).toBe(0); - expect(output).not.toContain('tslib'); + expect(output).not.toContain('Directive'); expect(output).not.toContain('ErrorHandler'); }); @@ -521,19 +515,18 @@ describe('downlevel decorator transform', () => { `); const {output} = transform( ` - import {Directive} from '@angular/core'; + import {Directive, Inject} from '@angular/core'; import {ErrorHandler, ClassInject} from './external'; - @Directive() export class MyDir { private _errorHandler: ErrorHandler; - constructor(v: ClassInject) {} + constructor(@Inject(ClassInject) i: ClassInject) {} } `, {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false}); expect(diagnostics.length).toBe(0); - expect(output).not.toContain('tslib'); + expect(output).not.toContain('Directive'); expect(output).not.toContain('ErrorHandler'); }); @@ -558,15 +551,14 @@ describe('downlevel decorator transform', () => { {emitDecoratorMetadata: false}); expect(diagnostics.length).toBe(0); - expect(output).not.toContain('tslib'); expect(output).toContain(`external_1 = require("./external");`); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: external_1.Dep } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -582,12 +574,15 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; + let MyDir = MyDir_1 = class MyDir { + constructor(parentDir) { } + }; MyDir.ctorParameters = () => [ - { type: MyDir, decorators: [{ type: core_1.Optional }, { type: core_1.SkipSelf }, { type: core_1.Inject, args: [MyDir,] }] } + { type: MyDir, decorators: [{ type: core_1.Optional }, { type: core_1.SkipSelf }, { type: core_1.Inject, args: [MyDir_1,] }] } ]; + MyDir = MyDir_1 = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -633,15 +628,15 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).not.toContain('external'); expect(output).toContain(dedent` - MyDir.decorators = [ - { type: core_1.Directive } - ]; MyDir.ctorParameters = () => [ { type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] }, { type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] }, { type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] }, { type: undefined, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] } ]; + MyDir = tslib_1.__decorate([ + (0, core_1.Directive)() + ], MyDir); `); }); @@ -693,13 +688,13 @@ describe('downlevel decorator transform', () => { expect(diagnostics.length).toBe(0); expect(output).toContain(dedent` - MyComp.decorators = [ - { type: core_1.Component, args: [{ template: 'hello' },] } - ]; MyComp.ctorParameters = () => [ { type: Values } - ];`); - expect(output).not.toContain('tslib'); + ]; + MyComp = tslib_1.__decorate([ + (0, core_1.Component)({ template: 'hello' }) + ], MyComp); + `); }); it('should allow for type-only references to be removed with `emitDecoratorMetadata` from custom decorators', @@ -729,97 +724,6 @@ describe('downlevel decorator transform', () => { expect(output).toContain('metadata("design:returntype", Object)'); }); - describe('class decorators skipped', () => { - beforeEach(() => skipClassDecorators = true); - - it('should not downlevel Angular class decorators', () => { - const {output} = transform(` - import {Injectable} from '@angular/core'; - - @Injectable() - export class MyService {} - `); - - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('MyService.decorators'); - expect(output).toContain(dedent` - MyService = tslib_1.__decorate([ - (0, core_1.Injectable)() - ], MyService); - `); - }); - - it('should downlevel constructor parameters', () => { - const {output} = transform(` - import {Injectable} from '@angular/core'; - - @Injectable() - export class InjectClass {} - - @Injectable() - export class MyService { - constructor(dep: InjectClass) {} - } - `); - - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('MyService.decorators'); - expect(output).toContain('MyService.ctorParameters'); - expect(output).toContain(dedent` - MyService.ctorParameters = () => [ - { type: InjectClass } - ]; - MyService = tslib_1.__decorate([ - (0, core_1.Injectable)() - ], MyService); - `); - }); - - it('should downlevel constructor parameter decorators', () => { - const {output} = transform(` - import {Injectable, Inject} from '@angular/core'; - - @Injectable() - export class InjectClass {} - - @Injectable() - export class MyService { - constructor(@Inject('test') dep: InjectClass) {} - } - `); - - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('MyService.decorators'); - expect(output).toContain('MyService.ctorParameters'); - expect(output).toContain(dedent` - MyService.ctorParameters = () => [ - { type: InjectClass, decorators: [{ type: core_1.Inject, args: ['test',] }] } - ]; - MyService = tslib_1.__decorate([ - (0, core_1.Injectable)() - ], MyService); - `); - }); - - it('should downlevel class member Angular decorators', () => { - const {output} = transform(` - import {Injectable, Input} from '@angular/core'; - - export class MyService { - @Input() disabled: boolean; - } - `); - - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('tslib'); - expect(output).toContain(dedent` - MyService.propDecorators = { - disabled: [{ type: core_1.Input }] - }; - `); - }); - }); - describe('transforming multiple files', () => { it('should work correctly for multiple files that import distinct declarations', () => { context.writeFile('foo_service.d.ts', ` @@ -915,7 +819,7 @@ describe('downlevel decorator transform', () => { const transformers: ts.CustomTransformers = { before: [getDownlevelDecoratorsTransform( program.getTypeChecker(), reflectionHost, diagnostics, - /* isCore */ false, isClosureEnabled, skipClassDecorators)] + /* isCore */ false, isClosureEnabled)] }; return {program, transformers}; } diff --git a/packages/core/schematics/ng-generate/standalone-migration/index.ts b/packages/core/schematics/ng-generate/standalone-migration/index.ts index 12f23062f95d..63c9c728bccd 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/index.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/index.ts @@ -12,13 +12,14 @@ import {existsSync, statSync} from 'fs'; import {join, relative} from 'path'; import ts from 'typescript'; +import {ChangesByFile, normalizePath} from '../../utils/change_tracker'; import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; import {canMigrateFile, createProgramOptions} from '../../utils/typescript/compiler_host'; import {pruneNgModules} from './prune-modules'; import {toStandaloneBootstrap} from './standalone-bootstrap'; import {toStandalone} from './to-standalone'; -import {ChangesByFile, knownInternalAliasRemapper, normalizePath} from './util'; +import {knownInternalAliasRemapper} from './util'; enum MigrationMode { toStandalone = 'convert-to-standalone', diff --git a/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts b/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts index 39447886404d..d7f21cdad35f 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts @@ -9,10 +9,11 @@ import {NgtscProgram} from '@angular/compiler-cli'; import ts from 'typescript'; +import {ChangeTracker, ImportRemapper} from '../../utils/change_tracker'; import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; import {closestNode} from '../../utils/typescript/nodes'; -import {ChangeTracker, findClassDeclaration, findLiteralProperty, getNodeLookup, ImportRemapper, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util'; +import {findClassDeclaration, findLiteralProperty, getNodeLookup, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util'; /** Keeps track of the places from which we need to remove AST nodes. */ interface RemovalLocations { diff --git a/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts b/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts index 7b250ddd7d2c..fae719f56c3a 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts @@ -8,15 +8,16 @@ import {NgtscProgram} from '@angular/compiler-cli'; -import {Reference, TemplateTypeChecker} from '@angular/compiler-cli/private/migrations'; +import {TemplateTypeChecker} from '@angular/compiler-cli/private/migrations'; import {dirname, join} from 'path'; import ts from 'typescript'; +import {ChangeTracker, ImportRemapper} from '../../utils/change_tracker'; import {getAngularDecorators} from '../../utils/ng_decorators'; import {closestNode} from '../../utils/typescript/nodes'; import {ComponentImportsRemapper, convertNgModuleDeclarationToStandalone, extractDeclarationsFromModule, findTestObjectsToMigrate, migrateTestDeclarations} from './to-standalone'; -import {ChangeTracker, closestOrSelf, findClassDeclaration, findLiteralProperty, getNodeLookup, getRelativeImportPath, ImportRemapper, isClassReferenceInAngularModule, NamedClassDeclaration, NodeLookup, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util'; +import {closestOrSelf, findClassDeclaration, findLiteralProperty, getNodeLookup, getRelativeImportPath, isClassReferenceInAngularModule, NamedClassDeclaration, NodeLookup, offsetsToNodes, ReferenceResolver, UniqueItemTracker} from './util'; /** Information extracted from a `bootstrapModule` call necessary to migrate it. */ interface BootstrapCallAnalysis { @@ -45,6 +46,12 @@ export function toStandaloneBootstrap( const testObjects = new Set(); const allDeclarations = new Set(); + // `bootstrapApplication` doesn't include Protractor support by default + // anymore so we have to opt the app in, if we detect it being used. + const additionalProviders = hasImport(program, rootFileNames, 'protractor') ? + new Map([['provideProtractorTestingSupport', '@angular/platform-browser']]) : + null; + for (const sourceFile of sourceFiles) { sourceFile.forEachChild(function walk(node) { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && @@ -64,7 +71,8 @@ export function toStandaloneBootstrap( for (const call of bootstrapCalls) { call.declarations.forEach(decl => allDeclarations.add(decl)); - migrateBootstrapCall(call, tracker, referenceResolver, typeChecker, printer); + migrateBootstrapCall( + call, tracker, additionalProviders, referenceResolver, typeChecker, printer); } // The previous migrations explicitly skip over bootstrapped @@ -135,12 +143,15 @@ function analyzeBootstrapCall( * Converts a `bootstrapModule` call to `bootstrapApplication`. * @param analysis Analysis result of the call. * @param tracker Tracker in which to register the changes. + * @param additionalFeatures Additional providers, apart from the auto-detected ones, that should + * be added to the bootstrap call. * @param referenceResolver * @param typeChecker * @param printer */ function migrateBootstrapCall( - analysis: BootstrapCallAnalysis, tracker: ChangeTracker, referenceResolver: ReferenceResolver, + analysis: BootstrapCallAnalysis, tracker: ChangeTracker, + additionalProviders: Map|null, referenceResolver: ReferenceResolver, typeChecker: ts.TypeChecker, printer: ts.Printer) { const sourceFile = analysis.call.getSourceFile(); const moduleSourceFile = analysis.metadata.getSourceFile(); @@ -177,6 +188,13 @@ function migrateBootstrapCall( nodesToCopy, referenceResolver, typeChecker); } + if (additionalProviders) { + additionalProviders.forEach((moduleSpecifier, name) => { + providersInNewCall.push(ts.factory.createCallExpression( + tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined)); + }); + } + if (nodesToCopy.size > 0) { let text = '\n\n'; nodesToCopy.forEach(node => { @@ -703,3 +721,27 @@ function getLastImportEnd(sourceFile: ts.SourceFile): number { return index; } + +/** Checks if any of the program's files has an import of a specific module. */ +function hasImport(program: NgtscProgram, rootFileNames: string[], moduleName: string): boolean { + const tsProgram = program.getTsProgram(); + const deepImportStart = moduleName + '/'; + + for (const fileName of rootFileNames) { + const sourceFile = tsProgram.getSourceFile(fileName); + + if (!sourceFile) { + continue; + } + + for (const statement of sourceFile.statements) { + if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && + (statement.moduleSpecifier.text === moduleName || + statement.moduleSpecifier.text.startsWith(deepImportStart))) { + return true; + } + } + } + + return false; +} diff --git a/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts b/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts index b9add0068495..85dc38ce7236 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts @@ -10,12 +10,13 @@ import {NgtscProgram} from '@angular/compiler-cli'; import {PotentialImport, PotentialImportKind, PotentialImportMode, Reference, TemplateTypeChecker} from '@angular/compiler-cli/private/migrations'; import ts from 'typescript'; +import {ChangesByFile, ChangeTracker, ImportRemapper} from '../../utils/change_tracker'; import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators'; import {getImportSpecifier} from '../../utils/typescript/imports'; import {closestNode} from '../../utils/typescript/nodes'; import {isReferenceToImport} from '../../utils/typescript/symbol'; -import {ChangesByFile, ChangeTracker, findClassDeclaration, findLiteralProperty, ImportRemapper, isClassReferenceInAngularModule, NamedClassDeclaration} from './util'; +import {findClassDeclaration, findLiteralProperty, isClassReferenceInAngularModule, NamedClassDeclaration} from './util'; /** * Function that can be used to prcess the dependencies that diff --git a/packages/core/schematics/ng-generate/standalone-migration/util.ts b/packages/core/schematics/ng-generate/standalone-migration/util.ts index 94be0d3ab626..d852381712fd 100644 --- a/packages/core/schematics/ng-generate/standalone-migration/util.ts +++ b/packages/core/schematics/ng-generate/standalone-migration/util.ts @@ -11,156 +11,21 @@ import {PotentialImport} from '@angular/compiler-cli/private/migrations'; import {dirname, relative} from 'path'; import ts from 'typescript'; -import {ImportManager} from '../../utils/import_manager'; +import {normalizePath} from '../../utils/change_tracker'; import {closestNode} from '../../utils/typescript/nodes'; -/** Mapping between a source file and the changes that have to be applied to it. */ -export type ChangesByFile = ReadonlyMap; - /** Map used to look up nodes based on their positions in a source file. */ export type NodeLookup = Map; /** Utility to type a class declaration with a name. */ export type NamedClassDeclaration = ts.ClassDeclaration&{name: ts.Identifier}; -/** Function that can be used to remap a generated import. */ -export type ImportRemapper = (moduleName: string) => string; - /** Text span of an AST node. */ export type ReferenceSpan = [start: number, end: number]; /** Mapping between a file name and spans for node references inside of it. */ export type ReferencesByFile = Map; -/** Change that needs to be applied to a file. */ -interface PendingChange { - /** Index at which to start changing the file. */ - start: number; - /** - * Amount of text that should be removed after the `start`. - * No text will be removed if omitted. - */ - removeLength?: number; - /** New text that should be inserted. */ - text: string; -} - -/** Tracks changes that have to be made for specific files. */ -export class ChangeTracker { - private readonly _changes = new Map(); - private readonly _importManager = new ImportManager( - currentFile => ({ - addNewImport: (start, text) => this.insertText(currentFile, start, text), - updateExistingImport: (namedBindings, text) => - this.replaceText(currentFile, namedBindings.getStart(), namedBindings.getWidth(), text), - }), - this._printer); - - constructor(private _printer: ts.Printer, private _importRemapper?: ImportRemapper) {} - - /** - * Tracks the insertion of some text. - * @param sourceFile File in which the text is being inserted. - * @param start Index at which the text is insert. - * @param text Text to be inserted. - */ - insertText(sourceFile: ts.SourceFile, index: number, text: string): void { - this._trackChange(sourceFile, {start: index, text}); - } - - /** - * Replaces text within a file. - * @param sourceFile File in which to replace the text. - * @param start Index from which to replace the text. - * @param removeLength Length of the text being replaced. - * @param text Text to be inserted instead of the old one. - */ - replaceText(sourceFile: ts.SourceFile, start: number, removeLength: number, text: string): void { - this._trackChange(sourceFile, {start, removeLength, text}); - } - - /** - * Replaces the text of an AST node with a new one. - * @param oldNode Node to be replaced. - * @param newNode New node to be inserted. - * @param emitHint Hint when formatting the text of the new node. - * @param sourceFileWhenPrinting File to use when printing out the new node. This is important - * when copying nodes from one file to another, because TypeScript might not output literal nodes - * without it. - */ - replaceNode( - oldNode: ts.Node, newNode: ts.Node, emitHint = ts.EmitHint.Unspecified, - sourceFileWhenPrinting?: ts.SourceFile): void { - const sourceFile = oldNode.getSourceFile(); - this.replaceText( - sourceFile, oldNode.getStart(), oldNode.getWidth(), - this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile)); - } - - /** - * Removes the text of an AST node from a file. - * @param node Node whose text should be removed. - */ - removeNode(node: ts.Node): void { - this._trackChange( - node.getSourceFile(), {start: node.getStart(), removeLength: node.getWidth(), text: ''}); - } - - /** - * Adds an import to a file. - * @param sourceFile File to which to add the import. - * @param symbolName Symbol being imported. - * @param moduleName Module from which the symbol is imported. - */ - addImport( - sourceFile: ts.SourceFile, symbolName: string, moduleName: string, - alias: string|null = null): ts.Expression { - if (this._importRemapper) { - moduleName = this._importRemapper(moduleName); - } - - // It's common for paths to be manipulated with Node's `path` utilties which - // can yield a path with back slashes. Normalize them since outputting such - // paths will also cause TS to escape the forward slashes. - moduleName = normalizePath(moduleName); - - return this._importManager.addImportToSourceFile(sourceFile, symbolName, moduleName, alias); - } - - /** - * Gets the changes that should be applied to all the files in the migration. - * The changes are sorted in the order in which they should be applied. - */ - recordChanges(): ChangesByFile { - this._importManager.recordChanges(); - return this._changes; - } - - /** - * Adds a change to a `ChangesByFile` map. - * @param file File that the change is associated with. - * @param change Change to be added. - */ - private _trackChange(file: ts.SourceFile, change: PendingChange): void { - const changes = this._changes.get(file); - - if (changes) { - // Insert the changes in reverse so that they're applied in reverse order. - // This ensures that the offsets of subsequent changes aren't affected by - // previous changes changing the file's text. - const insertIndex = changes.findIndex(current => current.start <= change.start); - - if (insertIndex === -1) { - changes.push(change); - } else { - changes.splice(insertIndex, 0, change); - } - } else { - this._changes.set(file, [change]); - } - } -} - /** Utility class used to track a one-to-many relationship where all the items are unique. */ export class UniqueItemTracker { private _nodes = new Map>(); @@ -384,11 +249,6 @@ export function getRelativeImportPath(fromFile: string, toFile: string): string return normalizePath(path); } -/** Normalizes a path to use posix separators. */ -export function normalizePath(path: string): string { - return path.replace(/\\/g, '/'); -} - /** Function used to remap the generated `imports` for a component to known shorter aliases. */ export function knownInternalAliasRemapper(imports: PotentialImport[]) { return imports.map( diff --git a/packages/core/schematics/test/standalone_migration_spec.ts b/packages/core/schematics/test/standalone_migration_spec.ts index f01447e9bd6f..1421c7d6f287 100644 --- a/packages/core/schematics/test/standalone_migration_spec.ts +++ b/packages/core/schematics/test/standalone_migration_spec.ts @@ -3647,4 +3647,94 @@ describe('standalone migration', () => { }).catch(e => console.error(e)); `)); }); + + it('should add Protractor support if any tests are detected', async () => { + writeFile('main.ts', ` + import {AppModule} from './app/app.module'; + import {platformBrowser} from '@angular/platform-browser'; + + platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e)); + `); + + writeFile('./app/app.module.ts', ` + import {NgModule, Component} from '@angular/core'; + + @Component({selector: 'app', template: 'hello'}) + export class AppComponent {} + + @NgModule({declarations: [AppComponent], bootstrap: [AppComponent]}) + export class AppModule {} + `); + + writeFile('./app/app.e2e.spec.ts', ` + import {browser, by, element} from 'protractor'; + + describe('app', () => { + beforeAll(async () => { + await browser.get(browser.params.testUrl); + }); + + it('should work', async () => { + const rootSelector = element(by.css('app')); + expect(await rootSelector.isPresent()).toBe(true); + }); + }); + `); + + await runMigration('standalone-bootstrap'); + + expect(stripWhitespace(tree.readContent('main.ts'))).toBe(stripWhitespace(` + import {AppComponent} from './app/app.module'; + import {platformBrowser, provideProtractorTestingSupport, bootstrapApplication} from '@angular/platform-browser'; + + bootstrapApplication(AppComponent, { + providers: [provideProtractorTestingSupport()] + }).catch(e => console.error(e)); + `)); + }); + + it('should add Protractor support if any tests with deep imports are detected', async () => { + writeFile('main.ts', ` + import {AppModule} from './app/app.module'; + import {platformBrowser} from '@angular/platform-browser'; + + platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e)); + `); + + writeFile('./app/app.module.ts', ` + import {NgModule, Component} from '@angular/core'; + + @Component({selector: 'app', template: 'hello'}) + export class AppComponent {} + + @NgModule({declarations: [AppComponent], bootstrap: [AppComponent]}) + export class AppModule {} + `); + + writeFile('./app/app.e2e.spec.ts', ` + import {browser, by, element} from 'protractor/some/deep-import'; + + describe('app', () => { + beforeAll(async () => { + await browser.get(browser.params.testUrl); + }); + + it('should work', async () => { + const rootSelector = element(by.css('app')); + expect(await rootSelector.isPresent()).toBe(true); + }); + }); + `); + + await runMigration('standalone-bootstrap'); + + expect(stripWhitespace(tree.readContent('main.ts'))).toBe(stripWhitespace(` + import {AppComponent} from './app/app.module'; + import {platformBrowser, provideProtractorTestingSupport, bootstrapApplication} from '@angular/platform-browser'; + + bootstrapApplication(AppComponent, { + providers: [provideProtractorTestingSupport()] + }).catch(e => console.error(e)); + `)); + }); }); diff --git a/packages/core/schematics/utils/change_tracker.ts b/packages/core/schematics/utils/change_tracker.ts new file mode 100644 index 000000000000..a3364c310a1d --- /dev/null +++ b/packages/core/schematics/utils/change_tracker.ts @@ -0,0 +1,151 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import ts from 'typescript'; + +import {ImportManager} from './import_manager'; + +/** Function that can be used to remap a generated import. */ +export type ImportRemapper = (moduleName: string, inFile: string) => string; + +/** Mapping between a source file and the changes that have to be applied to it. */ +export type ChangesByFile = ReadonlyMap; + +/** Change that needs to be applied to a file. */ +export interface PendingChange { + /** Index at which to start changing the file. */ + start: number; + /** + * Amount of text that should be removed after the `start`. + * No text will be removed if omitted. + */ + removeLength?: number; + /** New text that should be inserted. */ + text: string; +} + +/** Tracks changes that have to be made for specific files. */ +export class ChangeTracker { + private readonly _changes = new Map(); + private readonly _importManager = new ImportManager( + currentFile => ({ + addNewImport: (start, text) => this.insertText(currentFile, start, text), + updateExistingImport: (namedBindings, text) => + this.replaceText(currentFile, namedBindings.getStart(), namedBindings.getWidth(), text), + }), + this._printer); + + constructor(private _printer: ts.Printer, private _importRemapper?: ImportRemapper) {} + + /** + * Tracks the insertion of some text. + * @param sourceFile File in which the text is being inserted. + * @param start Index at which the text is insert. + * @param text Text to be inserted. + */ + insertText(sourceFile: ts.SourceFile, index: number, text: string): void { + this._trackChange(sourceFile, {start: index, text}); + } + + /** + * Replaces text within a file. + * @param sourceFile File in which to replace the text. + * @param start Index from which to replace the text. + * @param removeLength Length of the text being replaced. + * @param text Text to be inserted instead of the old one. + */ + replaceText(sourceFile: ts.SourceFile, start: number, removeLength: number, text: string): void { + this._trackChange(sourceFile, {start, removeLength, text}); + } + + /** + * Replaces the text of an AST node with a new one. + * @param oldNode Node to be replaced. + * @param newNode New node to be inserted. + * @param emitHint Hint when formatting the text of the new node. + * @param sourceFileWhenPrinting File to use when printing out the new node. This is important + * when copying nodes from one file to another, because TypeScript might not output literal nodes + * without it. + */ + replaceNode( + oldNode: ts.Node, newNode: ts.Node, emitHint = ts.EmitHint.Unspecified, + sourceFileWhenPrinting?: ts.SourceFile): void { + const sourceFile = oldNode.getSourceFile(); + this.replaceText( + sourceFile, oldNode.getStart(), oldNode.getWidth(), + this._printer.printNode(emitHint, newNode, sourceFileWhenPrinting || sourceFile)); + } + + /** + * Removes the text of an AST node from a file. + * @param node Node whose text should be removed. + */ + removeNode(node: ts.Node): void { + this._trackChange( + node.getSourceFile(), {start: node.getStart(), removeLength: node.getWidth(), text: ''}); + } + + /** + * Adds an import to a file. + * @param sourceFile File to which to add the import. + * @param symbolName Symbol being imported. + * @param moduleName Module from which the symbol is imported. + */ + addImport( + sourceFile: ts.SourceFile, symbolName: string, moduleName: string, + alias: string|null = null): ts.Expression { + if (this._importRemapper) { + moduleName = this._importRemapper(moduleName, sourceFile.fileName); + } + + // It's common for paths to be manipulated with Node's `path` utilties which + // can yield a path with back slashes. Normalize them since outputting such + // paths will also cause TS to escape the forward slashes. + moduleName = normalizePath(moduleName); + + return this._importManager.addImportToSourceFile(sourceFile, symbolName, moduleName, alias); + } + + /** + * Gets the changes that should be applied to all the files in the migration. + * The changes are sorted in the order in which they should be applied. + */ + recordChanges(): ChangesByFile { + this._importManager.recordChanges(); + return this._changes; + } + + /** + * Adds a change to a `ChangesByFile` map. + * @param file File that the change is associated with. + * @param change Change to be added. + */ + private _trackChange(file: ts.SourceFile, change: PendingChange): void { + const changes = this._changes.get(file); + + if (changes) { + // Insert the changes in reverse so that they're applied in reverse order. + // This ensures that the offsets of subsequent changes aren't affected by + // previous changes changing the file's text. + const insertIndex = changes.findIndex(current => current.start <= change.start); + + if (insertIndex === -1) { + changes.push(change); + } else { + changes.splice(insertIndex, 0, change); + } + } else { + this._changes.set(file, [change]); + } + } +} + +/** Normalizes a path to use posix separators. */ +export function normalizePath(path: string): string { + return path.replace(/\\/g, '/'); +} diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 0a42b6ce816a..4c5d317f0f48 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -12,7 +12,7 @@ import {merge, Observable, Observer, Subscription} from 'rxjs'; import {share} from 'rxjs/operators'; import {ApplicationInitStatus} from './application_init'; -import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens'; +import {PLATFORM_INITIALIZER} from './application_tokens'; import {getCompilerFacade, JitCompilerUsage} from './compiler/compiler_facade'; import {Console} from './console'; import {Injectable} from './di/injectable'; @@ -46,6 +46,8 @@ import {scheduleMicroTask} from './util/microtask'; import {stringify} from './util/stringify'; import {NgZone, NoopNgZone} from './zone/ng_zone'; +const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode; + let _platformInjector: Injector|null = null; /** @@ -63,7 +65,18 @@ export const ALLOW_MULTIPLE_PLATFORMS = new InjectionToken('AllowMultip const PLATFORM_DESTROY_LISTENERS = new InjectionToken>('PlatformDestroyListeners'); -const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode; +/** + * A [DI token](guide/glossary#di-token "DI token definition") that provides a set of callbacks to + * be called for every component that is bootstrapped. + * + * Each callback must take a `ComponentRef` instance and return nothing. + * + * `(componentRef: ComponentRef) => void` + * + * @publicApi + */ +export const APP_BOOTSTRAP_LISTENER = + new InjectionToken) => void>>('appBootstrapListener'); export function compileNgModuleFactory( injector: Injector, options: CompilerOptions, diff --git a/packages/core/src/application_tokens.ts b/packages/core/src/application_tokens.ts index 23f7012fabdb..013bf0541656 100644 --- a/packages/core/src/application_tokens.ts +++ b/packages/core/src/application_tokens.ts @@ -7,7 +7,6 @@ */ import {InjectionToken} from './di'; -import {ComponentRef} from './linker/component_factory'; /** @@ -59,19 +58,6 @@ export const PLATFORM_ID = new InjectionToken('Platform ID', { factory: () => 'unknown', // set a default platform name, when none set explicitly }); -/** - * A [DI token](guide/glossary#di-token "DI token definition") that provides a set of callbacks to - * be called for every component that is bootstrapped. - * - * Each callback must take a `ComponentRef` instance and return nothing. - * - * `(componentRef: ComponentRef) => void` - * - * @publicApi - */ -export const APP_BOOTSTRAP_LISTENER = - new InjectionToken) => void>>('appBootstrapListener'); - /** * A [DI token](guide/glossary#di-token "DI token definition") that indicates the root directory of * the application diff --git a/packages/core/src/change_detection/change_detection.ts b/packages/core/src/change_detection/change_detection.ts index aa90a847c5cd..1f8ae89f0dcb 100644 --- a/packages/core/src/change_detection/change_detection.ts +++ b/packages/core/src/change_detection/change_detection.ts @@ -14,7 +14,7 @@ import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs export {SimpleChange, SimpleChanges} from '../interface/simple_change'; export {devModeEqual} from '../util/comparison'; export {ChangeDetectorRef} from './change_detector_ref'; -export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants'; +export {ChangeDetectionStrategy} from './constants'; export {DefaultIterableDiffer, DefaultIterableDifferFactory} from './differs/default_iterable_differ'; export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ'; export {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs'; diff --git a/packages/core/src/change_detection/constants.ts b/packages/core/src/change_detection/constants.ts index bc81f02f627d..c8a53ab8c7a5 100644 --- a/packages/core/src/change_detection/constants.ts +++ b/packages/core/src/change_detection/constants.ts @@ -30,58 +30,3 @@ export enum ChangeDetectionStrategy { */ Default = 1, } - -/** - * Defines the possible states of the default change detector. - * @see `ChangeDetectorRef` - */ -export enum ChangeDetectorStatus { - /** - * A state in which, after calling `detectChanges()`, the change detector - * state becomes `Checked`, and must be explicitly invoked or reactivated. - */ - CheckOnce, - - /** - * A state in which change detection is skipped until the change detector mode - * becomes `CheckOnce`. - */ - Checked, - - /** - * A state in which change detection continues automatically until explicitly - * deactivated. - */ - CheckAlways, - - /** - * A state in which a change detector sub tree is not a part of the main tree and - * should be skipped. - */ - Detached, - - /** - * Indicates that the change detector encountered an error checking a binding - * or calling a directive lifecycle method and is now in an inconsistent state. Change - * detectors in this state do not detect changes. - */ - Errored, - - /** - * Indicates that the change detector has been destroyed. - */ - Destroyed, -} - -/** - * Reports whether a given strategy is currently the default for change detection. - * @param changeDetectionStrategy The strategy to check. - * @returns True if the given strategy is the current default, false otherwise. - * @see `ChangeDetectorStatus` - * @see `ChangeDetectorRef` - */ -export function isDefaultChangeDetectionStrategy(changeDetectionStrategy: ChangeDetectionStrategy): - boolean { - return changeDetectionStrategy == null || - changeDetectionStrategy === ChangeDetectionStrategy.Default; -} diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index ac1fe45cef8d..76a2d04b45ab 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -15,9 +15,9 @@ export * from './metadata'; export * from './version'; export {TypeDecorator} from './util/decorators'; export * from './di'; -export {createPlatform, assertPlatform, destroyPlatform, getPlatform, BootstrapOptions, PlatformRef, ApplicationRef, createPlatformFactory, NgProbeToken} from './application_ref'; +export {createPlatform, assertPlatform, destroyPlatform, getPlatform, BootstrapOptions, PlatformRef, ApplicationRef, createPlatformFactory, NgProbeToken, APP_BOOTSTRAP_LISTENER} from './application_ref'; export {enableProdMode, isDevMode} from './util/is_dev_mode'; -export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, APP_BOOTSTRAP_LISTENER, ANIMATION_MODULE_TYPE} from './application_tokens'; +export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, ANIMATION_MODULE_TYPE} from './application_tokens'; export {APP_INITIALIZER, ApplicationInitStatus} from './application_init'; export * from './zone'; export * from './render'; diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 91c47f6a2faf..dddc6b97de77 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -9,7 +9,6 @@ export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication} from './application_ref'; export {APP_ID_RANDOM_PROVIDER as ɵAPP_ID_RANDOM_PROVIDER} from './application_tokens'; export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection'; -export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants'; export {Console as ɵConsole} from './console'; export {getDebugNodeR2 as ɵgetDebugNodeR2} from './debug/debug_node'; export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility'; @@ -31,10 +30,6 @@ export {coerceToBoolean as ɵcoerceToBoolean} from './util/coercion'; export {devModeEqual as ɵdevModeEqual} from './util/comparison'; export {makeDecorator as ɵmakeDecorator} from './util/decorators'; export {global as ɵglobal} from './util/global'; -export {isListLikeIterable as ɵisListLikeIterable} from './util/iterable'; export {isObservable as ɵisObservable, isPromise as ɵisPromise, isSubscribable as ɵisSubscribable} from './util/lang'; export {stringify as ɵstringify} from './util/stringify'; export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider_flags'; - -// TODO(alxhub): allows tests to compile, can be removed when tests have been updated. -export const ɵivyEnabled = true; diff --git a/packages/core/src/linker/template_ref.ts b/packages/core/src/linker/template_ref.ts index 389863c15095..b2ff10c68eff 100644 --- a/packages/core/src/linker/template_ref.ts +++ b/packages/core/src/linker/template_ref.ts @@ -79,7 +79,7 @@ const R3TemplateRef = class TemplateRef extends ViewEngineTemplateRef { } override createEmbeddedView(context: T, injector?: Injector): EmbeddedViewRef { - const embeddedTView = this._declarationTContainer.tViews as TView; + const embeddedTView = this._declarationTContainer.tView as TView; const embeddedLView = createLView( this._declarationLView, embeddedTView, context, LViewFlags.CheckAlways, null, embeddedTView.declTNode, null, null, null, null, injector || null); @@ -117,7 +117,7 @@ export function injectTemplateRef(): TemplateRef|null { */ export function createTemplateRef(hostTNode: TNode, hostLView: LView): TemplateRef|null { if (hostTNode.type & TNodeType.Container) { - ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated'); + ngDevMode && assertDefined(hostTNode.tView, 'TView must be allocated'); return new R3TemplateRef( hostLView, hostTNode as TContainerNode, createElementRef(hostTNode, hostLView)); } diff --git a/packages/core/src/render3/i18n/i18n_util.ts b/packages/core/src/render3/i18n/i18n_util.ts index df3e2b5cc0b5..8c5a646290e3 100644 --- a/packages/core/src/render3/i18n/i18n_util.ts +++ b/packages/core/src/render3/i18n/i18n_util.ts @@ -15,6 +15,7 @@ import {LView, TView} from '../interfaces/view'; import {assertTNodeType} from '../node_assert'; import {setI18nHandling} from '../node_manipulation'; import {getInsertInFrontOfRNodeWithI18n, processI18nInsertBefore} from '../node_manipulation_i18n'; + import {addTNodeAndUpdateInsertBeforeIndex} from './i18n_insert_before_index'; @@ -35,7 +36,7 @@ export function getTIcu(tView: TView, index: number): TIcu|null { const value = tView.data[index] as null | TIcu | TIcuContainerNode | string; if (value === null || typeof value === 'string') return null; if (ngDevMode && - !(value.hasOwnProperty('tViews') || value.hasOwnProperty('currentCaseLViewIndex'))) { + !(value.hasOwnProperty('tView') || value.hasOwnProperty('currentCaseLViewIndex'))) { throwError('We expect to get \'null\'|\'TIcu\'|\'TIcuContainer\', but got: ' + value); } // Here the `value.hasOwnProperty('currentCaseLViewIndex')` is a polymorphic read as it can be @@ -66,7 +67,7 @@ export function setTIcu(tView: TView, index: number, tIcu: TIcu): void { const tNode = tView.data[index] as null | TIcuContainerNode; ngDevMode && assertEqual( - tNode === null || tNode.hasOwnProperty('tViews'), true, + tNode === null || tNode.hasOwnProperty('tView'), true, 'We expect to get \'null\'|\'TIcuContainer\''); if (tNode === null) { tView.data[index] = tIcu; diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 96b19703d384..1c1444a8b14d 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -702,13 +702,12 @@ export function storeCleanupWithContext( /** * Constructs a TNode object from the arguments. * - * @param tView `TView` to which this `TNode` belongs (used only in `ngDevMode`) + * @param tView `TView` to which this `TNode` belongs * @param tParent Parent `TNode` * @param type The type of the node * @param index The index of the TNode in TView.data, adjusted for HEADER_OFFSET * @param tagName The tag name of the node * @param attrs The attributes defined on this node - * @param tViews Any TViews attached to this node * @returns the TNode object */ export function createTNode( @@ -758,7 +757,7 @@ export function createTNode( initialInputs: undefined, inputs: null, outputs: null, - tViews: null, + tView: null, next: null, prev: null, projectionNext: null, diff --git a/packages/core/src/render3/instructions/template.ts b/packages/core/src/render3/instructions/template.ts index 43e476ca6126..baca780732ef 100644 --- a/packages/core/src/render3/instructions/template.ts +++ b/packages/core/src/render3/instructions/template.ts @@ -15,6 +15,7 @@ import {HEADER_OFFSET, LView, RENDERER, TView, TViewType} from '../interfaces/vi import {appendChild} from '../node_manipulation'; import {getLView, getTView, setCurrentTNode} from '../state'; import {getConstant} from '../util/view_utils'; + import {addToViewTree, createDirectivesInstances, createLContainer, createTView, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared'; @@ -34,7 +35,7 @@ function templateFirstCreatePass( resolveDirectives(tView, lView, tNode, getConstant(tViewConsts, localRefsIndex)); registerPostOrderHooks(tView, tNode); - const embeddedTView = tNode.tViews = createTView( + const embeddedTView = tNode.tView = createTView( TViewType.Embedded, tNode, templateFn, decls, vars, tView.directiveRegistry, tView.pipeRegistry, null, tView.schemas, tViewConsts); diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 25c2f10127d5..eaa156681560 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -532,26 +532,14 @@ export interface TNode { outputs: PropertyAliases|null; /** - * The TView or TViews attached to this node. - * - * If this TNode corresponds to an LContainer with inline views, the container will - * need to store separate static data for each of its view blocks (TView[]). Otherwise, - * nodes in inline views with the same index as nodes in their parent views will overwrite - * each other, as they are in the same template. - * - * Each index in this array corresponds to the static data for a certain - * view. So if you had V(0) and V(1) in a container, you might have: - * - * [ - * [{tagName: 'div', attrs: ...}, null], // V(0) TView - * [{tagName: 'button', attrs ...}, null] // V(1) TView + * The TView attached to this node. * * If this TNode corresponds to an LContainer with a template (e.g. structural * directive), the template's TView will be stored here. * - * If this TNode corresponds to an element, tViews will be null . + * If this TNode corresponds to an element, tView will be `null`. */ - tViews: TView|TView[]|null; + tView: TView|null; /** * The next sibling node. Necessary so we can propagate through the root nodes of a view @@ -774,7 +762,7 @@ export interface TElementNode extends TNode { * retrieved using viewData[HOST_NODE]). */ parent: TElementNode|TElementContainerNode|null; - tViews: null; + tView: null; /** * If this is a component TNode with projection, this will be an array of projected @@ -800,7 +788,7 @@ export interface TTextNode extends TNode { * retrieved using LView.node). */ parent: TElementNode|TElementContainerNode|null; - tViews: null; + tView: null; projection: null; } @@ -822,7 +810,7 @@ export interface TContainerNode extends TNode { * - They are dynamically created */ parent: TElementNode|TElementContainerNode|null; - tViews: TView|TView[]|null; + tView: TView|null; projection: null; value: null; } @@ -833,7 +821,7 @@ export interface TElementContainerNode extends TNode { index: number; child: TElementNode|TTextNode|TContainerNode|TElementContainerNode|TProjectionNode|null; parent: TElementNode|TElementContainerNode|null; - tViews: null; + tView: null; projection: null; } @@ -843,7 +831,7 @@ export interface TIcuContainerNode extends TNode { index: number; child: null; parent: TElementNode|TElementContainerNode|null; - tViews: null; + tView: null; projection: null; value: TIcu; } @@ -858,7 +846,7 @@ export interface TProjectionNode extends TNode { * retrieved using LView.node). */ parent: TElementNode|TElementContainerNode|null; - tViews: null; + tView: null; /** Index of the projection node. (See TNode.projection for more info.) */ projection: number; diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 99cc7991be59..34f90917c6f9 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -125,7 +125,6 @@ export function compileComponent(type: Type, metadata: Component): void { encapsulation, interpolation: metadata.interpolation, viewProviders: metadata.viewProviders || null, - isStandalone: !!metadata.standalone, }; compilationDepth++; diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 04b4e0bce953..fc165cc60fb2 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -282,7 +282,7 @@ describe('instructions', () => { }); describe('performance counters', () => { - it('should create tViews only once for each nested level', () => { + it('should create tView only once for each nested level', () => { @Component({ selector: 'nested-loops', standalone: true, diff --git a/packages/core/test/render3/is_shape_of.ts b/packages/core/test/render3/is_shape_of.ts index b40b7dbdd9e8..c09ade9d5c98 100644 --- a/packages/core/test/render3/is_shape_of.ts +++ b/packages/core/test/render3/is_shape_of.ts @@ -165,7 +165,7 @@ const ShapeOfTNode: ShapeOf = { initialInputs: true, inputs: true, outputs: true, - tViews: true, + tView: true, next: true, prev: true, projectionNext: true, diff --git a/packages/elements/test/BUILD.bazel b/packages/elements/test/BUILD.bazel index c9e36d84b3ea..db65bbf67b20 100644 --- a/packages/elements/test/BUILD.bazel +++ b/packages/elements/test/BUILD.bazel @@ -30,7 +30,6 @@ filegroup( testonly = True, # do not sort srcs = [ - "@npm//:node_modules/core-js-bundle/index.js", # Required for browsers that do not natively support Custom Elements. "@npm//:node_modules/@webcomponents/custom-elements/custom-elements.min.js", "@npm//:node_modules/reflect-metadata/Reflect.js", diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index e2e2ce769f67..f4eeb68cedb8 100644 --- a/packages/forms/src/form_builder.ts +++ b/packages/forms/src/form_builder.ts @@ -112,7 +112,7 @@ export class FormBuilder { /** * @description - * Returns a FormBuilder in which automatically constructed @see FormControl} elements + * Returns a FormBuilder in which automatically constructed `FormControl` elements * have `{nonNullable: true}` and are non-nullable. * * **Constructing non-nullable controls** @@ -413,7 +413,7 @@ export abstract class NonNullableFormBuilder { } /** - * UntypedFormBuilder is the same as @see FormBuilder, but it provides untyped controls. + * UntypedFormBuilder is the same as `FormBuilder`, but it provides untyped controls. */ @Injectable({providedIn: 'root'}) export class UntypedFormBuilder extends FormBuilder { diff --git a/packages/forms/src/model/form_array.ts b/packages/forms/src/model/form_array.ts index ad0eb86cf98b..6acd52e55f9e 100644 --- a/packages/forms/src/model/form_array.ts +++ b/packages/forms/src/model/form_array.ts @@ -523,7 +523,7 @@ interface UntypedFormArrayCtor { } /** - * UntypedFormArray is a non-strongly-typed version of @see FormArray, which + * UntypedFormArray is a non-strongly-typed version of `FormArray`, which * permits heterogenous controls. */ export type UntypedFormArray = FormArray; diff --git a/packages/forms/src/model/form_control.ts b/packages/forms/src/model/form_control.ts index 32baac2154ee..89663d30df76 100644 --- a/packages/forms/src/model/form_control.ts +++ b/packages/forms/src/model/form_control.ts @@ -555,7 +555,7 @@ interface UntypedFormControlCtor { } /** - * UntypedFormControl is a non-strongly-typed version of @see FormControl. + * UntypedFormControl is a non-strongly-typed version of `FormControl`. */ export type UntypedFormControl = FormControl; diff --git a/packages/forms/src/model/form_group.ts b/packages/forms/src/model/form_group.ts index aa612aa29260..6d59adee7ee2 100644 --- a/packages/forms/src/model/form_group.ts +++ b/packages/forms/src/model/form_group.ts @@ -598,7 +598,7 @@ interface UntypedFormGroupCtor { } /** - * UntypedFormGroup is a non-strongly-typed version of @see FormGroup. + * UntypedFormGroup is a non-strongly-typed version of `FormGroup`. */ export type UntypedFormGroup = FormGroup; diff --git a/packages/platform-browser/src/dom/events/event_manager.ts b/packages/platform-browser/src/dom/events/event_manager.ts index c8cfa17fc332..f19ace737dc6 100644 --- a/packages/platform-browser/src/dom/events/event_manager.ts +++ b/packages/platform-browser/src/dom/events/event_manager.ts @@ -32,7 +32,9 @@ export class EventManager { * Initializes an instance of the event-manager service. */ constructor(@Inject(EVENT_MANAGER_PLUGINS) plugins: EventManagerPlugin[], private _zone: NgZone) { - plugins.forEach(p => p.manager = this); + plugins.forEach((plugin) => { + plugin.manager = this; + }); this._plugins = plugins.slice().reverse(); } @@ -94,7 +96,7 @@ export class EventManager { export abstract class EventManagerPlugin { constructor(private _doc: any) {} - // TODO(issue/24571): remove '!'. + // Using non-null assertion because it's set by EventManager's constructor manager!: EventManager; abstract supports(eventName: string): boolean; diff --git a/packages/platform-browser/src/dom/util.ts b/packages/platform-browser/src/dom/util.ts index f4c36de9aaf0..66d737843c45 100644 --- a/packages/platform-browser/src/dom/util.ts +++ b/packages/platform-browser/src/dom/util.ts @@ -8,18 +8,6 @@ import {ɵglobal as global} from '@angular/core'; -const CAMEL_CASE_REGEXP = /([A-Z])/g; -const DASH_CASE_REGEXP = /-([a-z])/g; - - -export function camelCaseToDashCase(input: string): string { - return input.replace(CAMEL_CASE_REGEXP, (...m: string[]) => '-' + m[1].toLowerCase()); -} - -export function dashCaseToCamelCase(input: string): string { - return input.replace(DASH_CASE_REGEXP, (...m: string[]) => m[1].toUpperCase()); -} - /** * Exports the value under a given `name` in the global property `ng`. For example `ng.probe` if * `name` is `'probe'`. diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index 00fb979d43e1..71636f07d253 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -7,7 +7,7 @@ */ import {ResourceLoader} from '@angular/compiler'; -import {Compiler, Component, ComponentFactoryResolver, CUSTOM_ELEMENTS_SCHEMA, Directive, Inject, Injectable, InjectionToken, Injector, Input, NgModule, Optional, Pipe, SkipSelf, Type, ɵstringify as stringify} from '@angular/core'; +import {Compiler, Component, ComponentFactoryResolver, CUSTOM_ELEMENTS_SCHEMA, Directive, Inject, Injectable, InjectionToken, Injector, Input, NgModule, Optional, Pipe, SkipSelf, Type} from '@angular/core'; import {fakeAsync, getTestBed, inject, TestBed, tick, waitForAsync, withModule} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -92,7 +92,6 @@ class TestViewProvidersComp { @Directive({selector: '[someDir]', host: {'[title]': 'someDir'}}) class SomeDirective { - // TODO(issue/24571): remove '!'. @Input() someDir!: string; } @@ -762,7 +761,6 @@ const bTok = new InjectionToken('b'); testDir = this; } - // TODO(issue/24571): remove '!'. @Input('test') test!: string; } diff --git a/packages/zone.js/karma-build-jasmine.es2015.conf.js b/packages/zone.js/karma-build-jasmine.es2015.conf.js index 6ab875d89d27..16eedcd7ae3e 100644 --- a/packages/zone.js/karma-build-jasmine.es2015.conf.js +++ b/packages/zone.js/karma-build-jasmine.es2015.conf.js @@ -1,11 +1,5 @@ module.exports = function(config) { require('./karma-build-jasmine.conf.js')(config); - for (let i = 0; i < config.files.length; i++) { - if (config.files[i] === 'node_modules/core-js-bundle/index.js') { - config.files.splice(i, 1); - break; - } - } config.client.entrypoint = 'browser_es2015_entry_point'; }; diff --git a/packages/zone.js/karma-build.conf.js b/packages/zone.js/karma-build.conf.js index 6405bd4cd55f..943a610509fd 100644 --- a/packages/zone.js/karma-build.conf.js +++ b/packages/zone.js/karma-build.conf.js @@ -8,7 +8,6 @@ module.exports = function(config) { require('./karma-base.conf.js')(config); - config.files.push('node_modules/core-js-bundle/index.js'); config.files.push('build/test/browser-env-setup.js'); config.files.push('build/test/wtf_mock.js'); config.files.push('build/test/test_fake_polyfill.js'); diff --git a/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js b/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js index 874076ad58ed..8f78f82a9154 100644 --- a/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js +++ b/packages/zone.js/karma-dist-sauce-jasmine.es2015.conf.js @@ -2,13 +2,6 @@ module.exports = function(config) { require('./karma-dist-jasmine.conf.js')(config); require('./sauce.es2015.conf')(config); - const files = config.files; - config.files = []; - for (let i = 0; i < files.length; i++) { - if (files[i] !== 'node_modules/core-js-bundle/index.js' || files[i] === 'build/test/main.js') { - config.files.push(files[i]); - } - } config.files.push('build/test/wtf_mock.js'); config.files.push('build/test/test_fake_polyfill.js'); config.files.push('build/test/custom_error.js'); diff --git a/packages/zone.js/karma-dist.conf.js b/packages/zone.js/karma-dist.conf.js index 94de5abc606b..8349c7d07498 100644 --- a/packages/zone.js/karma-dist.conf.js +++ b/packages/zone.js/karma-dist.conf.js @@ -8,7 +8,6 @@ module.exports = function(config) { require('./karma-base.conf.js')(config); - config.files.push('node_modules/core-js-bundle/index.js'); config.files.push('build/test/browser-env-setup.js'); config.files.push('build/test/wtf_mock.js'); config.files.push('build/test/test_fake_polyfill.js'); diff --git a/packages/zone.js/karma-evergreen-dist.conf.js b/packages/zone.js/karma-evergreen-dist.conf.js index 420399449565..d7b15b15bc31 100644 --- a/packages/zone.js/karma-evergreen-dist.conf.js +++ b/packages/zone.js/karma-evergreen-dist.conf.js @@ -8,14 +8,6 @@ module.exports = function(config) { require('./karma-base.conf.js')(config); - const files = config.files; - config.files = []; - for (let i = 0; i < files.length; i++) { - if (files[i] !== 'node_modules/core-js-bundle/index.js') { - config.files.push(files[i]); - } - } - config.files.push('build/test/browser-env-setup.js'); config.files.push('build/test/wtf_mock.js'); config.files.push('build/test/test_fake_polyfill.js'); diff --git a/packages/zone.js/test/webdriver/test.html b/packages/zone.js/test/webdriver/test.html index be600e790368..b619bae1fc7b 100644 --- a/packages/zone.js/test/webdriver/test.html +++ b/packages/zone.js/test/webdriver/test.html @@ -1,7 +1,6 @@ - diff --git a/tools/legacy-saucelabs/downlevel_decorator_transform.ts b/tools/legacy-saucelabs/downlevel_decorator_transform.ts index e675dff608bf..b5f654ffc051 100644 --- a/tools/legacy-saucelabs/downlevel_decorator_transform.ts +++ b/tools/legacy-saucelabs/downlevel_decorator_transform.ts @@ -23,5 +23,5 @@ export function legacyCompilationDownlevelDecoratorTransform(program: ts.Program // Note: `isCore` is set to `true` since we also process the core package. return getDownlevelDecoratorsTransform( typeChecker, reflectionHost, [], /* isCore */ true, - /* enableClosureCompiler */ false, /* skipClassDecorators */ true); + /* enableClosureCompiler */ false); } diff --git a/yarn.lock b/yarn.lock index 6fd26532263d..7852fc97b096 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4095,10 +4095,10 @@ "@types/node" "*" "@types/responselike" "*" -"@types/chrome@^0.0.208": - version "0.0.208" - resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.208.tgz#c52992e46723c783d3fd84a8b90dd8b3e87af67f" - integrity sha512-VDU/JnXkF5qaI7WBz14Azpa2VseZTgML0ia/g/B1sr9OfdOnHiH/zZ7P7qCDqxSlkqJh76/bPc8jLFcx8rHJmw== +"@types/chrome@^0.0.218": + version "0.0.218" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.218.tgz#d354cf0ac85204c17cfbe03ba7eb8f58c75d03a4" + integrity sha512-GC/c9B3Eo3h3l+fV5G6A9cRMEZOYo46E21mjHHfkXz+/A5uypXstMVAMk04IeGY2DJ7PxVxbn26oFBPZOa4Dkw== dependencies: "@types/filesystem" "*" "@types/har-format" "*" @@ -6974,11 +6974,6 @@ copy-webpack-plugin@11.0.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -core-js-bundle@^3.10.2: - version "3.23.4" - resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.23.4.tgz#fb4ea6b4d5a0ba3166da1c8936bc18b9328a292b" - integrity sha512-E5kP515mQPnHj0LL0Z4UyQ8VHrhugJaq3GOropUnR/fU3QA3Xxs3vRgt6CwNDpgHYiruDa0jqHwNHwk5FbcwFg== - core-js-compat@^3.25.1: version "3.25.3" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.3.tgz#d6a442a03f4eade4555d4e640e6a06151dd95d38" @@ -7067,13 +7062,6 @@ cross-env@^5.1.3: dependencies: cross-spawn "^6.0.5" -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -9897,7 +9885,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.2.10, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@4.2.10, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -11398,12 +11386,12 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -karma-sourcemap-loader@^0.3.7: - version "0.3.8" - resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz#d4bae72fb7a8397328a62b75013d2df937bdcf9c" - integrity sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g== +karma-sourcemap-loader@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.2.10" karma@~6.4.0: version "6.4.0" @@ -11969,13 +11957,6 @@ madge@^6.0.0: typescript "^3.9.5" walkdir "^0.4.1" -magic-string@0.27.0, magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" @@ -11990,6 +11971,13 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" +magic-string@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.13" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -16177,10 +16165,10 @@ ts-graphviz@^1.5.0: resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.5.2.tgz#e0e803abe9dad923fa413208720776e3084e518b" integrity sha512-/V4N6wIvE+mcQicUfqSNxzFUX4xNccpwi3fGafEUSqGBSZYp9HVP8RZdjTfxCQvTjavxqUrEDB5ucEqKPLgp+w== -ts-node@^10.8.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.2.tgz#3185b75228cef116bf82ffe8762594f54b2a23f2" - integrity sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA== +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== dependencies: "@cspotcode/source-map-support" "^0.8.0" "@tsconfig/node10" "^1.0.7"