Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e80f0e526 | |||
f3dd6c224c | |||
863acb6c21 | |||
989e8a1f99 | |||
84d1ba792b | |||
b32126c335 | |||
d5e09f4d62 | |||
c25c57c3a3 | |||
692e34d4a2 | |||
071348eb72 | |||
8803f9f4dc | |||
aa816d3887 | |||
a5ba40a78b | |||
cb83b8a887 | |||
8bb726e899 | |||
88662a540d | |||
9b32a5917c | |||
ffd1691ba9 | |||
afd4417a7b | |||
2f53bbba20 | |||
a4f99f4de9 | |||
0711128d28 |
@ -656,6 +656,18 @@ jobs:
|
||||
- run: yarn tsc -p packages
|
||||
- run: yarn tsc -p modules
|
||||
- run: yarn bazel build //packages/zone.js:npm_package
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- run:
|
||||
name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
command: |
|
||||
yarn bazel build //packages/core/test:downleveled_es5_fixture
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/all/@angular/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
- run:
|
||||
# Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready.
|
||||
name: Waiting for Saucelabs tunnel to connect
|
||||
|
@ -7,18 +7,6 @@ export const commitMessage: CommitMessageConfig = {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 20,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: [
|
||||
'build',
|
||||
'ci',
|
||||
'docs',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
'test',
|
||||
],
|
||||
scopes: [
|
||||
'animations',
|
||||
'bazel',
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,3 +1,18 @@
|
||||
<a name="10.0.10"></a>
|
||||
## 10.0.10 (2020-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** Allow scrolling when browser supports scrollTo ([#38468](https://github.com/angular/angular/issues/38468)) ([b32126c](https://github.com/angular/angular/commit/b32126c)), closes [#30630](https://github.com/angular/angular/issues/30630)
|
||||
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([863acb6](https://github.com/angular/angular/commit/863acb6)), closes [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **core:** error if CSS custom property in host binding has number in name ([#38432](https://github.com/angular/angular/issues/38432)) ([cb83b8a](https://github.com/angular/angular/commit/cb83b8a)), closes [#37292](https://github.com/angular/angular/issues/37292)
|
||||
* **core:** fix multiple nested views removal from ViewContainerRef ([#38317](https://github.com/angular/angular/issues/38317)) ([d5e09f4](https://github.com/angular/angular/commit/d5e09f4)), closes [#38201](https://github.com/angular/angular/issues/38201)
|
||||
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([f3dd6c2](https://github.com/angular/angular/commit/f3dd6c2)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
|
||||
* **router:** ensure routerLinkActive updates when associated routerLinks change ([#38349](https://github.com/angular/angular/issues/38349)) ([989e8a1](https://github.com/angular/angular/commit/989e8a1)), closes [#18469](https://github.com/angular/angular/issues/18469)
|
||||
|
||||
|
||||
|
||||
<a name="10.0.9"></a>
|
||||
## 10.0.9 (2020-08-12)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@ -11,7 +11,7 @@ import { HeroService } from '../hero.service';
|
||||
styleUrls: [ './hero-detail.component.css' ]
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
@Input() hero: Hero;
|
||||
hero: Hero;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
|
@ -14,12 +14,16 @@ The CLI does not support running Angular itself in a web worker.
|
||||
|
||||
To add a web worker to an existing project, use the Angular CLI `ng generate` command.
|
||||
|
||||
`ng generate web-worker` *location*
|
||||
```bash
|
||||
ng generate web-worker <location>
|
||||
```
|
||||
|
||||
You can add a web worker anywhere in your application.
|
||||
For example, to add a web worker to the root component, `src/app/app.component.ts`, run the following command.
|
||||
|
||||
`ng generate web-worker app`
|
||||
```bash
|
||||
ng generate web-worker app
|
||||
```
|
||||
|
||||
The command performs the following actions.
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -12,20 +12,22 @@ import { LocationService } from 'app/shared/location.service';
|
||||
describe('ContributorListComponent', () => {
|
||||
|
||||
let component: ContributorListComponent;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let contributorService: TestContributorService;
|
||||
let locationService: TestLocationService;
|
||||
let contributorGroups: ContributorGroup[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
ContributorListComponent,
|
||||
{provide: ContributorService, useClass: TestContributorService },
|
||||
{provide: LocationService, useClass: TestLocationService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
|
||||
{provide: ContributorService, useClass: TestContributorService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService);
|
||||
contributorService = injector.get(ContributorService);
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
|
||||
contributorGroups = contributorService.testContributors;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -12,20 +12,22 @@ import { Category } from './resource.model';
|
||||
describe('ResourceListComponent', () => {
|
||||
|
||||
let component: ResourceListComponent;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let resourceService: TestResourceService;
|
||||
let locationService: TestLocationService;
|
||||
let categories: Category[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
ResourceListComponent,
|
||||
{provide: ResourceService, useClass: TestResourceService },
|
||||
{provide: LocationService, useClass: TestLocationService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ResourceListComponent, deps: [ResourceService, LocationService] },
|
||||
{provide: ResourceService, useClass: TestResourceService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService);
|
||||
resourceService = injector.get(ResourceService);
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
resourceService = injector.get(ResourceService) as unknown as TestResourceService;
|
||||
categories = resourceService.testCategories;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector, NgZone } from '@angular/core';
|
||||
import { Injector, NgZone } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { SearchService } from './search.service';
|
||||
@ -6,7 +6,7 @@ import { WebWorkerClient } from 'app/shared/web-worker';
|
||||
|
||||
describe('SearchService', () => {
|
||||
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let service: SearchService;
|
||||
let sendMessageSpy: jasmine.Spy;
|
||||
let mockWorker: WebWorkerClient;
|
||||
@ -16,10 +16,13 @@ describe('SearchService', () => {
|
||||
mockWorker = { sendMessage: sendMessageSpy } as any;
|
||||
spyOn(WebWorkerClient, 'create').and.returnValue(mockWorker);
|
||||
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
SearchService,
|
||||
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }) }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: SearchService, deps: [NgZone]},
|
||||
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }), deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
service = injector.get(SearchService);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { environment } from 'environments/environment';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
@ -15,7 +15,7 @@ describe('Deployment service', () => {
|
||||
it('should get the mode from the `mode` query parameter if available', () => {
|
||||
const injector = getInjector();
|
||||
|
||||
const locationService: MockLocationService = injector.get(LocationService);
|
||||
const locationService = injector.get(LocationService) as unknown as MockLocationService;
|
||||
locationService.search.and.returnValue({ mode: 'bar' });
|
||||
|
||||
const deployment = injector.get(Deployment);
|
||||
@ -25,8 +25,8 @@ describe('Deployment service', () => {
|
||||
});
|
||||
|
||||
function getInjector() {
|
||||
return ReflectiveInjector.resolveAndCreate([
|
||||
Deployment,
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService('') }
|
||||
]);
|
||||
return Injector.create({providers: [
|
||||
{ provide: Deployment, deps: [LocationService] },
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService(''), deps: [] }
|
||||
]});
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { GaService } from 'app/shared/ga.service';
|
||||
import { WindowToken } from 'app/shared/window';
|
||||
|
||||
describe('GaService', () => {
|
||||
let gaService: GaService;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let gaSpy: jasmine.Spy;
|
||||
let mockWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
gaSpy = jasmine.createSpy('ga');
|
||||
mockWindow = { ga: gaSpy };
|
||||
injector = ReflectiveInjector.resolveAndCreate([GaService, { provide: WindowToken, useFactory: () => mockWindow }]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: GaService, deps: [WindowToken] },
|
||||
{ provide: WindowToken, useFactory: () => mockWindow, deps: [] }
|
||||
]});
|
||||
|
||||
gaService = injector.get(GaService);
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { Location, LocationStrategy, PlatformLocation } from '@angular/common';
|
||||
import { MockLocationStrategy } from '@angular/common/testing';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -9,26 +9,28 @@ import { LocationService } from './location.service';
|
||||
import { ScrollService } from './scroll.service';
|
||||
|
||||
describe('LocationService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let location: MockLocationStrategy;
|
||||
let service: LocationService;
|
||||
let swUpdates: MockSwUpdatesService;
|
||||
let scrollService: MockScrollService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
LocationService,
|
||||
Location,
|
||||
{ provide: GaService, useClass: TestGaService },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService },
|
||||
{ provide: ScrollService, useClass: MockScrollService }
|
||||
]);
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: LocationService, deps: [GaService, Location, ScrollService, PlatformLocation, SwUpdatesService] },
|
||||
{ provide: Location, deps: [LocationStrategy, PlatformLocation] },
|
||||
{ provide: GaService, useClass: TestGaService, deps: [] },
|
||||
{ provide: LocationStrategy, useClass: MockLocationStrategy, deps: [] },
|
||||
{ provide: PlatformLocation, useClass: MockPlatformLocation, deps: [] },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService, deps: [] },
|
||||
{ provide: ScrollService, useClass: MockScrollService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
location = injector.get(LocationStrategy);
|
||||
service = injector.get(LocationService);
|
||||
swUpdates = injector.get(SwUpdatesService);
|
||||
location = injector.get(LocationStrategy) as unknown as MockLocationStrategy;
|
||||
service = injector.get(LocationService);
|
||||
swUpdates = injector.get(SwUpdatesService) as unknown as MockSwUpdatesService;
|
||||
scrollService = injector.get(ScrollService);
|
||||
});
|
||||
|
||||
@ -380,7 +382,7 @@ describe('LocationService', () => {
|
||||
let platformLocation: MockPlatformLocation;
|
||||
|
||||
beforeEach(() => {
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
platformLocation = injector.get(PlatformLocation) as unknown as MockPlatformLocation;
|
||||
});
|
||||
|
||||
it('should call replaceState on PlatformLocation', () => {
|
||||
@ -577,7 +579,7 @@ describe('LocationService', () => {
|
||||
let gaLocationChanged: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
const gaService = injector.get(GaService);
|
||||
const gaService = injector.get(GaService) as unknown as TestGaService;
|
||||
gaLocationChanged = gaService.locationChanged;
|
||||
// execute currentPath observable so that gaLocationChanged is called
|
||||
service.currentPath.subscribe();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
|
||||
import { ErrorHandler, Injector } from '@angular/core';
|
||||
import { Logger } from './logger.service';
|
||||
|
||||
describe('logger service', () => {
|
||||
@ -10,10 +10,10 @@ describe('logger service', () => {
|
||||
beforeEach(() => {
|
||||
logSpy = spyOn(console, 'log');
|
||||
warnSpy = spyOn(console, 'warn');
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
Logger,
|
||||
{ provide: ErrorHandler, useClass: MockErrorHandler }
|
||||
]);
|
||||
const injector = Injector.create({providers: [
|
||||
{ provide: Logger, deps: [ErrorHandler] },
|
||||
{ provide: ErrorHandler, useClass: MockErrorHandler, deps: [] }
|
||||
]});
|
||||
logger = injector.get(Logger);
|
||||
errorHandler = injector.get(ErrorHandler);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
|
||||
import { ErrorHandler, Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { WindowToken } from 'app/shared/window';
|
||||
import { AppModule } from 'app/app.module';
|
||||
@ -14,11 +14,12 @@ describe('ReportingErrorHandler service', () => {
|
||||
onerrorSpy = jasmine.createSpy('onerror');
|
||||
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
|
||||
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
|
||||
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }) }
|
||||
]);
|
||||
handler = injector.get(ErrorHandler);
|
||||
const injector = Injector.create({providers: [
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [WindowToken] },
|
||||
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }), deps: [] }
|
||||
]});
|
||||
|
||||
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
|
||||
});
|
||||
|
||||
it('should be registered on the AppModule', () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Injector, ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@ -151,11 +151,11 @@ describe('ScrollSpyService', () => {
|
||||
let scrollSpyService: ScrollSpyService;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: DOCUMENT, useValue: { body: {} } },
|
||||
{ provide: ScrollService, useValue: { topOffset: 50 } },
|
||||
ScrollSpyService
|
||||
]);
|
||||
{ provide: ScrollSpyService, deps: [DOCUMENT, ScrollService] }
|
||||
]});
|
||||
|
||||
scrollSpyService = injector.get(ScrollSpyService);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
||||
import {ReflectiveInjector} from '@angular/core';
|
||||
import {Injector} from '@angular/core';
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import {ScrollService, topMargin} from './scroll.service';
|
||||
@ -15,7 +15,7 @@ describe('ScrollService', () => {
|
||||
};
|
||||
|
||||
const topOfPageElem = {} as Element;
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let document: MockDocument;
|
||||
let platformLocation: MockPlatformLocation;
|
||||
let scrollService: ScrollService;
|
||||
@ -41,21 +41,25 @@ describe('ScrollService', () => {
|
||||
jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy}
|
||||
]);
|
||||
injector = Injector.create( {
|
||||
providers: [
|
||||
{
|
||||
provide: ScrollService,
|
||||
useFactory: createScrollService,
|
||||
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
|
||||
},
|
||||
{provide: Location, useClass: SpyLocation, deps: [] },
|
||||
{provide: DOCUMENT, useClass: MockDocument, deps: []},
|
||||
{provide: PlatformLocation, useClass: MockPlatformLocation, deps: []},
|
||||
{provide: ViewportScroller, useValue: viewportScrollerStub},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy, deps: []}
|
||||
]
|
||||
});
|
||||
|
||||
platformLocation = injector.get(PlatformLocation);
|
||||
document = injector.get(DOCUMENT);
|
||||
document = injector.get(DOCUMENT) as unknown as MockDocument;
|
||||
scrollService = injector.get(ScrollService);
|
||||
location = injector.get(Location);
|
||||
location = injector.get(Location) as unknown as SpyLocation;
|
||||
|
||||
spyOn(window, 'scrollBy');
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { Injector } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@ -7,7 +7,7 @@ import { ScrollItem, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-s
|
||||
import { TocItem, TocService } from './toc.service';
|
||||
|
||||
describe('TocService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let scrollSpyService: MockScrollSpyService;
|
||||
let tocService: TocService;
|
||||
let lastTocList: TocItem[];
|
||||
@ -21,13 +21,14 @@ describe('TocService', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: DomSanitizer, useClass: TestDomSanitizer },
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: DomSanitizer, useClass: TestDomSanitizer, deps: [] },
|
||||
{ provide: DOCUMENT, useValue: document },
|
||||
{ provide: ScrollSpyService, useClass: MockScrollSpyService },
|
||||
TocService,
|
||||
]);
|
||||
scrollSpyService = injector.get(ScrollSpyService);
|
||||
{ provide: ScrollSpyService, useClass: MockScrollSpyService, deps: [] },
|
||||
{ provide: TocService, deps: [DOCUMENT, DomSanitizer, ScrollSpyService] },
|
||||
]});
|
||||
|
||||
scrollSpyService = injector.get(ScrollSpyService) as unknown as MockScrollSpyService;
|
||||
tocService = injector.get(TocService);
|
||||
tocService.tocList.subscribe(tocList => lastTocList = tocList);
|
||||
});
|
||||
@ -330,7 +331,7 @@ describe('TocService', () => {
|
||||
});
|
||||
|
||||
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
|
||||
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
|
||||
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer) as unknown as TestDomSanitizer;
|
||||
expect(domSanitizer.bypassSecurityTrustHtml)
|
||||
.toHaveBeenCalledWith('Setup to develop <i>locally</i>.');
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ApplicationRef, ReflectiveInjector } from '@angular/core';
|
||||
import { ApplicationRef, Injector } from '@angular/core';
|
||||
import { discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -8,7 +8,7 @@ import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
describe('SwUpdatesService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let injector: Injector;
|
||||
let appRef: MockApplicationRef;
|
||||
let service: SwUpdatesService;
|
||||
let swu: MockSwUpdate;
|
||||
@ -21,16 +21,16 @@ describe('SwUpdatesService', () => {
|
||||
// run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper
|
||||
// to call them inside each test's zone.
|
||||
const setup = (isSwUpdateEnabled: boolean) => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: ApplicationRef, useClass: MockApplicationRef },
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled) },
|
||||
SwUpdatesService
|
||||
]);
|
||||
injector = Injector.create({providers: [
|
||||
{ provide: ApplicationRef, useClass: MockApplicationRef, deps: [] },
|
||||
{ provide: Logger, useClass: MockLogger, deps: [] },
|
||||
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled), deps: [] },
|
||||
{ provide: SwUpdatesService, deps: [ApplicationRef, Logger, SwUpdate] }
|
||||
]});
|
||||
|
||||
appRef = injector.get(ApplicationRef);
|
||||
appRef = injector.get(ApplicationRef) as unknown as MockApplicationRef;
|
||||
service = injector.get(SwUpdatesService);
|
||||
swu = injector.get(SwUpdate);
|
||||
swu = injector.get(SwUpdate) as unknown as MockSwUpdate;
|
||||
checkInterval = (service as any).checkInterval;
|
||||
};
|
||||
const tearDown = () => service.ngOnDestroy();
|
||||
|
@ -12,9 +12,12 @@
|
||||
// If a category becomes empty (e.g. BS and required), then the corresponding job must be commented
|
||||
// out in the CI configuration.
|
||||
var CIconfiguration = {
|
||||
'Chrome': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'Firefox': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
'FirefoxESR': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
|
||||
// Chrome and Firefox run as part of the Bazel browser tests, so we do not run them as
|
||||
// part of the legacy Saucelabs tests.
|
||||
'Chrome': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
'Firefox': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
|
||||
// Set ESR as a not required browser as it fails for Ivy acceptance tests.
|
||||
'FirefoxESR': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
|
||||
// Disabled because using the "beta" channel of Chrome can cause non-deterministic CI results.
|
||||
// e.g. a new chrome beta version has been released, but the Saucelabs selenium server does
|
||||
// not provide a chromedriver version that is compatible with the new beta.
|
||||
|
@ -5,7 +5,10 @@ ts_library(
|
||||
name = "commit-message",
|
||||
srcs = [
|
||||
"cli.ts",
|
||||
"commit-message-draft.ts",
|
||||
"config.ts",
|
||||
"parse.ts",
|
||||
"restore-commit-message.ts",
|
||||
"validate.ts",
|
||||
"validate-file.ts",
|
||||
"validate-range.ts",
|
||||
@ -23,9 +26,12 @@ ts_library(
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "validate-test",
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = ["validate.spec.ts"],
|
||||
srcs = [
|
||||
"parse.spec.ts",
|
||||
"validate.spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":commit-message",
|
||||
"//dev-infra/utils",
|
||||
@ -40,7 +46,6 @@ jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||
deps = [
|
||||
":commit-message",
|
||||
":validate-test",
|
||||
"test_lib",
|
||||
],
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import * as yargs from 'yargs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
|
||||
@ -16,6 +17,28 @@ import {validateCommitRange} from './validate-range';
|
||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
return localYargs.help()
|
||||
.strict()
|
||||
.command(
|
||||
'restore-commit-message-draft', false, {
|
||||
'file-env-variable': {
|
||||
type: 'string',
|
||||
conflicts: ['file'],
|
||||
required: true,
|
||||
description:
|
||||
'The key for the environment variable which holds the arguments for the ' +
|
||||
'prepare-commit-msg hook as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
|
||||
coerce: arg => {
|
||||
const [file, source] = (process.env[arg] || '').split(' ');
|
||||
if (!file) {
|
||||
throw new Error(`Provided environment variable "${arg}" was not found.`);
|
||||
}
|
||||
return [file, source];
|
||||
},
|
||||
}
|
||||
},
|
||||
args => {
|
||||
restoreCommitMessage(args.fileEnvVariable[0], args.fileEnvVariable[1]);
|
||||
})
|
||||
.command(
|
||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||
'file': {
|
||||
|
30
dev-infra/commit-message/commit-message-draft.ts
Normal file
30
dev-infra/commit-message/commit-message-draft.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs';
|
||||
|
||||
/** Load the commit message draft from the file system if it exists. */
|
||||
export function loadCommitMessageDraft(basePath: string) {
|
||||
const commitMessageDraftPath = `${basePath}.ngDevSave`;
|
||||
if (existsSync(commitMessageDraftPath)) {
|
||||
return readFileSync(commitMessageDraftPath).toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Remove the commit message draft from the file system. */
|
||||
export function deleteCommitMessageDraft(basePath: string) {
|
||||
const commitMessageDraftPath = `${basePath}.ngDevSave`;
|
||||
if (existsSync(commitMessageDraftPath)) {
|
||||
unlinkSync(commitMessageDraftPath);
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the commit message draft to the file system for later retrieval. */
|
||||
export function saveCommitMessageDraft(basePath: string, commitMessage: string) {
|
||||
writeFileSync(`${basePath}.ngDevSave`, commitMessage);
|
||||
}
|
@ -12,7 +12,6 @@ export interface CommitMessageConfig {
|
||||
maxLineLength: number;
|
||||
minBodyLength: number;
|
||||
minBodyLengthTypeExcludes?: string[];
|
||||
types: string[];
|
||||
scopes: string[];
|
||||
}
|
||||
|
||||
@ -30,3 +29,46 @@ export function getCommitMessageConfig() {
|
||||
assertNoErrors(errors);
|
||||
return config as Required<typeof config>;
|
||||
}
|
||||
|
||||
/** Scope requirement level to be set for each commit type. */
|
||||
export enum ScopeRequirement {
|
||||
Required,
|
||||
Optional,
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
/** A commit type */
|
||||
export interface CommitType {
|
||||
scope: ScopeRequirement;
|
||||
}
|
||||
|
||||
/** The valid commit types for Angular commit messages. */
|
||||
export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
||||
build: {
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
ci: {
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
docs: {
|
||||
scope: ScopeRequirement.Optional,
|
||||
},
|
||||
feat: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
fix: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
perf: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
refactor: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
release: {
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
test: {
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
};
|
||||
|
85
dev-infra/commit-message/parse.spec.ts
Normal file
85
dev-infra/commit-message/parse.spec.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @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 {parseCommitMessage, ParsedCommitMessage} from './parse';
|
||||
|
||||
|
||||
const commitValues = {
|
||||
prefix: '',
|
||||
type: 'fix',
|
||||
scope: 'changed-area',
|
||||
summary: 'This is a short summary of the change',
|
||||
body: 'This is a longer description of the change Closes #1',
|
||||
};
|
||||
|
||||
function buildCommitMessage(params = {}) {
|
||||
const {prefix, type, scope, summary, body} = {...commitValues, ...params};
|
||||
return `${prefix}${type}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n${body}`;
|
||||
}
|
||||
|
||||
|
||||
describe('commit message parsing:', () => {
|
||||
it('parses the scope', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).scope).toBe(commitValues.scope);
|
||||
});
|
||||
|
||||
it('parses the type', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).type).toBe(commitValues.type);
|
||||
});
|
||||
|
||||
it('parses the header', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).header)
|
||||
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
|
||||
});
|
||||
|
||||
it('parses the body', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).body).toBe(commitValues.body);
|
||||
});
|
||||
|
||||
it('parses the body without Github linking', () => {
|
||||
const body = 'This has linking\nCloses #1';
|
||||
const message = buildCommitMessage({body});
|
||||
expect(parseCommitMessage(message).bodyWithoutLinking).toBe('This has linking\n');
|
||||
});
|
||||
|
||||
it('parses the subject', () => {
|
||||
const message = buildCommitMessage();
|
||||
expect(parseCommitMessage(message).subject).toBe(commitValues.summary);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a fixup', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isFixup).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'fixup! '});
|
||||
expect(parseCommitMessage(message2).isFixup).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a revert', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isRevert).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'revert: '});
|
||||
expect(parseCommitMessage(message2).isRevert).toBe(true);
|
||||
|
||||
const message3 = buildCommitMessage({prefix: 'revert '});
|
||||
expect(parseCommitMessage(message3).isRevert).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies if a commit is a squash', () => {
|
||||
const message1 = buildCommitMessage();
|
||||
expect(parseCommitMessage(message1).isSquash).toBe(false);
|
||||
|
||||
const message2 = buildCommitMessage({prefix: 'squash! '});
|
||||
expect(parseCommitMessage(message2).isSquash).toBe(true);
|
||||
});
|
||||
});
|
73
dev-infra/commit-message/parse.ts
Normal file
73
dev-infra/commit-message/parse.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/** A parsed commit message. */
|
||||
export interface ParsedCommitMessage {
|
||||
header: string;
|
||||
body: string;
|
||||
bodyWithoutLinking: string;
|
||||
type: string;
|
||||
scope: string;
|
||||
subject: string;
|
||||
isFixup: boolean;
|
||||
isSquash: boolean;
|
||||
isRevert: boolean;
|
||||
}
|
||||
|
||||
/** Regex determining if a commit is a fixup. */
|
||||
const FIXUP_PREFIX_RE = /^fixup! /i;
|
||||
/** Regex finding all github keyword links. */
|
||||
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
|
||||
/** Regex determining if a commit is a squash. */
|
||||
const SQUASH_PREFIX_RE = /^squash! /i;
|
||||
/** Regex determining if a commit is a revert. */
|
||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||
/** Regex determining the scope of a commit if provided. */
|
||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||
/** Regex determining the entire header line of the commit. */
|
||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||
/** Regex determining the body of the commit. */
|
||||
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
let type = '';
|
||||
let scope = '';
|
||||
let subject = '';
|
||||
|
||||
if (COMMIT_HEADER_RE.test(commitMsg)) {
|
||||
header = COMMIT_HEADER_RE.exec(commitMsg)![1]
|
||||
.replace(FIXUP_PREFIX_RE, '')
|
||||
.replace(SQUASH_PREFIX_RE, '');
|
||||
}
|
||||
if (COMMIT_BODY_RE.test(commitMsg)) {
|
||||
body = COMMIT_BODY_RE.exec(commitMsg)![1];
|
||||
bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, '');
|
||||
}
|
||||
|
||||
if (TYPE_SCOPE_RE.test(header)) {
|
||||
const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!;
|
||||
type = parsedCommitHeader[1];
|
||||
scope = parsedCommitHeader[2];
|
||||
subject = parsedCommitHeader[3];
|
||||
}
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
bodyWithoutLinking,
|
||||
type,
|
||||
scope,
|
||||
subject,
|
||||
isFixup: FIXUP_PREFIX_RE.test(commitMsg),
|
||||
isSquash: SQUASH_PREFIX_RE.test(commitMsg),
|
||||
isRevert: REVERT_PREFIX_RE.test(commitMsg),
|
||||
};
|
||||
}
|
48
dev-infra/commit-message/restore-commit-message.ts
Normal file
48
dev-infra/commit-message/restore-commit-message.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @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 {info} from 'console';
|
||||
import {writeFileSync} from 'fs';
|
||||
|
||||
import {loadCommitMessageDraft} from './commit-message-draft';
|
||||
|
||||
/**
|
||||
* Restore the commit message draft to the git to be used as the default commit message.
|
||||
*
|
||||
* The source provided may be one of the sources described in
|
||||
* https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export function restoreCommitMessage(
|
||||
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
|
||||
if (!!source) {
|
||||
info('Skipping commit message restoration attempt');
|
||||
if (source === 'message') {
|
||||
info('A commit message was already provided via the command with a -m or -F flag');
|
||||
}
|
||||
if (source === 'template') {
|
||||
info('A commit message was already provided via the -t flag or config.template setting');
|
||||
}
|
||||
if (source === 'squash') {
|
||||
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
|
||||
}
|
||||
if (source === 'commit') {
|
||||
info('A commit message was already provided through a revision specified via --fixup, -c,');
|
||||
info('-C or --amend flag');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
/** A draft of a commit message. */
|
||||
const commitMessage = loadCommitMessageDraft(filePath);
|
||||
|
||||
// If the commit message draft has content, restore it into the provided filepath.
|
||||
if (commitMessage) {
|
||||
writeFileSync(filePath, commitMessage);
|
||||
}
|
||||
// Exit the process
|
||||
process.exit(0);
|
||||
}
|
@ -11,6 +11,7 @@ import {resolve} from 'path';
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
|
||||
import {validateCommitMessage} from './validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
@ -18,8 +19,12 @@ export function validateFile(filePath: string) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
if (validateCommitMessage(commitMessage)) {
|
||||
info('√ Valid commit message');
|
||||
deleteCommitMessageDraft(filePath);
|
||||
return;
|
||||
}
|
||||
// On all invalid commit messages, the commit message should be saved as a draft to be
|
||||
// restored on the next commit attempt.
|
||||
saveCommitMessageDraft(filePath, commitMessage);
|
||||
// If the validation did not return true, exit as a failure.
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -8,7 +8,8 @@
|
||||
import {info} from '../utils/console';
|
||||
import {exec} from '../utils/shelljs';
|
||||
|
||||
import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
import {parseCommitMessage} from './parse';
|
||||
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
|
||||
|
||||
// Whether the provided commit is a fixup commit.
|
||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||
|
@ -18,13 +18,6 @@ const config: {commitMessage: CommitMessageConfig} = {
|
||||
commitMessage: {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 0,
|
||||
types: [
|
||||
'feat',
|
||||
'fix',
|
||||
'refactor',
|
||||
'release',
|
||||
'style',
|
||||
],
|
||||
scopes: [
|
||||
'common',
|
||||
'compiler',
|
||||
@ -33,7 +26,7 @@ const config: {commitMessage: CommitMessageConfig} = {
|
||||
]
|
||||
}
|
||||
};
|
||||
const TYPES = config.commitMessage.types.join(', ');
|
||||
const TYPES = Object.keys(validateConfig.COMMIT_TYPES).join(', ');
|
||||
const SCOPES = config.commitMessage.scopes.join(', ');
|
||||
const INVALID = false;
|
||||
const VALID = true;
|
||||
@ -47,7 +40,8 @@ describe('validate-commit-message.js', () => {
|
||||
lastError = '';
|
||||
|
||||
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig').and.returnValue(config);
|
||||
spyOn(validateConfig, 'getCommitMessageConfig')
|
||||
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
|
||||
});
|
||||
|
||||
describe('validateMessage()', () => {
|
||||
@ -55,16 +49,16 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('fixup! release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('squash! release(packaging): something')).toBe(VALID);
|
||||
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
|
||||
expect(validateCommitMessage('Revert: "release(packaging): something"')).toBe(VALID);
|
||||
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
@ -110,8 +104,8 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
|
||||
|
||||
expect(validateCommitMessage('style(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'style(webworker): something'));
|
||||
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
|
||||
|
||||
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
||||
@ -119,12 +113,12 @@ describe('validate-commit-message.js', () => {
|
||||
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
|
||||
|
||||
expect(validateCommitMessage('release(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'release(angular): something'));
|
||||
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
|
||||
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
|
||||
});
|
||||
|
||||
it('should allow empty scope', () => {
|
||||
expect(validateCommitMessage('fix: blablabla')).toBe(VALID);
|
||||
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
|
||||
expect(lastError).toBe('');
|
||||
});
|
||||
|
||||
@ -243,7 +237,6 @@ describe('validate-commit-message.js', () => {
|
||||
maxLineLength: 120,
|
||||
minBodyLength: 30,
|
||||
minBodyLengthTypeExcludes: ['docs'],
|
||||
types: ['fix', 'docs'],
|
||||
scopes: ['core']
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
import {error} from '../utils/console';
|
||||
|
||||
import {getCommitMessageConfig} from './config';
|
||||
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
import {parseCommitMessage} from './parse';
|
||||
|
||||
/** Options for commit message validation. */
|
||||
export interface ValidateCommitMessageOptions {
|
||||
@ -15,53 +16,9 @@ export interface ValidateCommitMessageOptions {
|
||||
nonFixupCommitHeaders?: string[];
|
||||
}
|
||||
|
||||
const FIXUP_PREFIX_RE = /^fixup! /i;
|
||||
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
|
||||
const SQUASH_PREFIX_RE = /^squash! /i;
|
||||
const REVERT_PREFIX_RE = /^revert:? /i;
|
||||
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
|
||||
const COMMIT_HEADER_RE = /^(.*)/i;
|
||||
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
/** Regex matching a URL for an entire commit body line. */
|
||||
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string) {
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
let type = '';
|
||||
let scope = '';
|
||||
let subject = '';
|
||||
|
||||
if (COMMIT_HEADER_RE.test(commitMsg)) {
|
||||
header = COMMIT_HEADER_RE.exec(commitMsg)![1]
|
||||
.replace(FIXUP_PREFIX_RE, '')
|
||||
.replace(SQUASH_PREFIX_RE, '');
|
||||
}
|
||||
if (COMMIT_BODY_RE.test(commitMsg)) {
|
||||
body = COMMIT_BODY_RE.exec(commitMsg)![1];
|
||||
bodyWithoutLinking = body.replace(GITHUB_LINKING_RE, '');
|
||||
}
|
||||
|
||||
if (TYPE_SCOPE_RE.test(header)) {
|
||||
const parsedCommitHeader = TYPE_SCOPE_RE.exec(header)!;
|
||||
type = parsedCommitHeader[1];
|
||||
scope = parsedCommitHeader[2];
|
||||
subject = parsedCommitHeader[3];
|
||||
}
|
||||
return {
|
||||
header,
|
||||
body,
|
||||
bodyWithoutLinking,
|
||||
type,
|
||||
scope,
|
||||
subject,
|
||||
isFixup: FIXUP_PREFIX_RE.test(commitMsg),
|
||||
isSquash: SQUASH_PREFIX_RE.test(commitMsg),
|
||||
isRevert: REVERT_PREFIX_RE.test(commitMsg),
|
||||
};
|
||||
}
|
||||
|
||||
/** Validate a commit message against using the local repo's config. */
|
||||
export function validateCommitMessage(
|
||||
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
|
||||
@ -129,8 +86,26 @@ export function validateCommitMessage(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.types.includes(commit.type)) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
|
||||
|
||||
|
||||
if (COMMIT_TYPES[commit.type] === undefined) {
|
||||
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
|
||||
Object.keys(COMMIT_TYPES).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The scope requirement level for the provided type of the commit message. */
|
||||
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
|
||||
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
|
||||
commit.scope}' was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
|
||||
printError(
|
||||
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,10 @@ ts_library(
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/cli-progress",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//cli-progress",
|
||||
"@npm//inquirer",
|
||||
"@npm//multimatch",
|
||||
"@npm//shelljs",
|
||||
"@npm//yargs",
|
||||
|
@ -6,9 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {error, info} from '../utils/console';
|
||||
import {error, info, promptConfirm} from '../utils/console';
|
||||
|
||||
import {runFormatterInParallel} from './run-commands-parallel';
|
||||
|
||||
@ -57,11 +55,7 @@ export async function checkFiles(files: string[]) {
|
||||
// If the command is run in a non-CI environment, prompt to format the files immediately.
|
||||
let runFormatter = false;
|
||||
if (!process.env['CI']) {
|
||||
runFormatter = (await prompt({
|
||||
type: 'confirm',
|
||||
name: 'runFormatter',
|
||||
message: 'Format the files now?',
|
||||
})).runFormatter;
|
||||
runFormatter = await promptConfirm('Format the files now?', true);
|
||||
}
|
||||
|
||||
if (runFormatter) {
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest';
|
||||
import {prompt} from 'inquirer';
|
||||
|
||||
import {parseCommitMessage} from '../../../commit-message/validate';
|
||||
import {parseCommitMessage} from '../../../commit-message/parse';
|
||||
import {GitClient} from '../../../utils/git';
|
||||
import {GithubApiMergeMethod} from '../config';
|
||||
import {PullRequestFailure} from '../failures';
|
||||
|
@ -9,7 +9,7 @@
|
||||
"ts-circular-deps": "./ts-circular-dependencies/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/benchpress": "0.2.0",
|
||||
"@angular/benchpress": "0.2.1",
|
||||
"@octokit/graphql": "<from-root>",
|
||||
"@octokit/types": "<from-root>",
|
||||
"brotli": "<from-root>",
|
||||
|
5
goldens/public-api/router/router.d.ts
vendored
5
goldens/public-api/router/router.d.ts
vendored
@ -370,7 +370,7 @@ export declare class RouterEvent {
|
||||
url: string);
|
||||
}
|
||||
|
||||
export declare class RouterLink {
|
||||
export declare class RouterLink implements OnChanges {
|
||||
fragment: string;
|
||||
preserveFragment: boolean;
|
||||
/** @deprecated */ set preserveQueryParams(value: boolean);
|
||||
@ -386,6 +386,7 @@ export declare class RouterLink {
|
||||
};
|
||||
get urlTree(): UrlTree;
|
||||
constructor(router: Router, route: ActivatedRoute, tabIndex: string, renderer: Renderer2, el: ElementRef);
|
||||
ngOnChanges(changes: SimpleChanges): void;
|
||||
onClick(): boolean;
|
||||
}
|
||||
|
||||
@ -421,7 +422,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
target: string;
|
||||
get urlTree(): UrlTree;
|
||||
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
|
||||
ngOnChanges(changes: {}): any;
|
||||
ngOnChanges(changes: SimpleChanges): any;
|
||||
ngOnDestroy(): any;
|
||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
|
||||
}
|
||||
|
@ -53,7 +53,10 @@ INTEGRATION_TESTS = {
|
||||
},
|
||||
"dynamic-compiler": {"tags": ["no-ivy-aot"]},
|
||||
"hello_world__closure": {
|
||||
"commands": "payload_size_tracking",
|
||||
# TODO: Re-enable the payload_size_tracking command:
|
||||
# We should define ngDevMode to false in Closure, but --define only works in the global scope.
|
||||
# With ngDevMode not being set to false, this size tracking test provides little value but a lot of
|
||||
# headache to continue updating the size.
|
||||
"tags": ["no-ivy-aot"],
|
||||
},
|
||||
"hello_world__systemjs_umd": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.0.9",
|
||||
"version": "10.0.10",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -208,7 +208,8 @@
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn -s ng-dev format staged",
|
||||
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS"
|
||||
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS",
|
||||
"prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
* @returns The position in screen coordinates.
|
||||
*/
|
||||
getScrollPosition(): [number, number] {
|
||||
if (this.supportScrollRestoration()) {
|
||||
if (this.supportsScrolling()) {
|
||||
return [this.window.scrollX, this.window.scrollY];
|
||||
} else {
|
||||
return [0, 0];
|
||||
@ -100,7 +100,7 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
* @param position The new position in screen coordinates.
|
||||
*/
|
||||
scrollToPosition(position: [number, number]): void {
|
||||
if (this.supportScrollRestoration()) {
|
||||
if (this.supportsScrolling()) {
|
||||
this.window.scrollTo(position[0], position[1]);
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,7 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
* @param anchor The ID of the anchor element.
|
||||
*/
|
||||
scrollToAnchor(anchor: string): void {
|
||||
if (this.supportScrollRestoration()) {
|
||||
if (this.supportsScrolling()) {
|
||||
const elSelected =
|
||||
this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
|
||||
if (elSelected) {
|
||||
@ -163,6 +163,14 @@ export class BrowserViewportScroller implements ViewportScroller {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private supportsScrolling(): boolean {
|
||||
try {
|
||||
return !!this.window.scrollTo;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
|
||||
|
@ -15,21 +15,30 @@ describe('BrowserViewportScroller', () => {
|
||||
let windowSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
windowSpy = jasmine.createSpyObj('window', ['history']);
|
||||
windowSpy.scrollTo = 1;
|
||||
windowSpy = jasmine.createSpyObj('window', ['history', 'scrollTo']);
|
||||
windowSpy.history.scrollRestoration = 'auto';
|
||||
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
|
||||
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
|
||||
});
|
||||
|
||||
describe('setHistoryScrollRestoration', () => {
|
||||
it('should not crash when scrollRestoration is not writable', () => {
|
||||
function createNonWritableScrollRestoration() {
|
||||
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
|
||||
value: 'auto',
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
it('should not crash when scrollRestoration is not writable', () => {
|
||||
createNonWritableScrollRestoration();
|
||||
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should still allow scrolling if scrollRestoration is not writable', () => {
|
||||
createNonWritableScrollRestoration();
|
||||
scroller.scrollToPosition([10, 10]);
|
||||
expect(windowSpy.scrollTo as jasmine.Spy).toHaveBeenCalledWith(10, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrollToAnchor', () => {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
|
||||
|
||||
import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
|
||||
@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
return Array.from(constructor.parameters);
|
||||
}
|
||||
|
||||
if (isSynthesizedConstructor(constructor)) {
|
||||
if (this.isSynthesizedConstructor(constructor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
|
||||
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
|
||||
}
|
||||
|
||||
///////////// Host Private Helpers /////////////
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* Additionally, we handle synthetic delegate constructors that are emitted when TypeScript
|
||||
* downlevel's ES2015 synthetically generated to ES5. These vary slightly from the default
|
||||
* structure mentioned above because the ES2015 output uses a spread operator, for delegating
|
||||
* to the parent constructor, that is preserved through a TypeScript helper in ES5. e.g.
|
||||
*
|
||||
* ```
|
||||
* return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* Such constructs can be still considered as synthetic delegate constructors as they are
|
||||
* the product of a common TypeScript to ES5 synthetic constructor, just being downleveled
|
||||
* to ES5 using `tsc`. See: https://github.com/angular/angular/issues/38453.
|
||||
*
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
private isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return this.isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
this.isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly and are
|
||||
* being assigned to a common `_this` variable. The following patterns we intend to match:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
private isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return this.isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly and
|
||||
* are being returned. The following patterns correspond to synthetic super return calls:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
private isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return this.isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies synthesized super calls which pass-through function arguments directly. The
|
||||
* synthetic delegate super call match the following patterns we intend to match:
|
||||
*
|
||||
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
|
||||
* ```
|
||||
* _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
private isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) {
|
||||
return isSuperNotNull(left.left) && this.isSuperApplyCall(left.right);
|
||||
} else {
|
||||
return this.isSuperApplyCall(left);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression corresponds to a `super` call passing through
|
||||
* function arguments without any modification. e.g.
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* Additionally, we also handle cases where `arguments` are wrapped by a TypeScript spread helper.
|
||||
* This can happen if ES2015 class output contain auto-generated constructors due to class
|
||||
* members. The ES2015 output will be using `super(...arguments)` to delegate to the superclass,
|
||||
* but once downleveled to ES5, the spread operator will be persisted through a TypeScript spread
|
||||
* helper. For example:
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, __spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* More details can be found in: https://github.com/angular/angular/issues/38453.
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
private isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsExpr = expression.arguments[1];
|
||||
|
||||
// If the super is directly invoked with `arguments`, return `true`. This represents the
|
||||
// common TypeScript output where the delegate constructor super call matches the following
|
||||
// pattern: `super.apply(this, arguments)`.
|
||||
if (isArgumentsIdentifier(argumentsExpr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The other scenario we intend to detect: The `arguments` variable might be wrapped with the
|
||||
// TypeScript spread helper (either through tslib or inlined). This can happen if an explicit
|
||||
// delegate constructor uses `super(...arguments)` in ES2015 and is downleveled to ES5 using
|
||||
// `--downlevelIteration`. The output in such cases would not directly pass the function
|
||||
// `arguments` to the `super` call, but wrap it in a TS spread helper. The output would match
|
||||
// the following pattern: `super.apply(this, tslib.__spread(arguments))`. We check for such
|
||||
// constructs below, but perform the detection of the call expression definition as last as
|
||||
// that is the most expensive operation here.
|
||||
if (!ts.isCallExpression(argumentsExpr) || argumentsExpr.arguments.length !== 1 ||
|
||||
!isArgumentsIdentifier(argumentsExpr.arguments[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const argumentsCallExpr = argumentsExpr.expression;
|
||||
let argumentsCallDeclaration: Declaration|null = null;
|
||||
|
||||
// The `__spread` helper could be globally available, or accessed through a namespaced
|
||||
// import. Hence we support a property access here as long as it resolves to the actual
|
||||
// known TypeScript spread helper.
|
||||
if (ts.isIdentifier(argumentsCallExpr)) {
|
||||
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr);
|
||||
} else if (
|
||||
ts.isPropertyAccessExpression(argumentsCallExpr) &&
|
||||
ts.isIdentifier(argumentsCallExpr.name)) {
|
||||
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr.name);
|
||||
}
|
||||
|
||||
return argumentsCallDeclaration !== null &&
|
||||
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
|
||||
}
|
||||
}
|
||||
|
||||
///////////// Internal Helpers /////////////
|
||||
@ -422,103 +635,8 @@ function reflectArrayElement(element: ts.Expression) {
|
||||
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form:
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
|
||||
|
||||
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
|
||||
function isArgumentsIdentifier(expression: ts.Expression): boolean {
|
||||
return ts.isIdentifier(expression) && expression.text === 'arguments';
|
||||
}
|
||||
|
||||
function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
@ -526,31 +644,6 @@ function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
isSuperIdentifier(expression.left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, arguments)
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsArgument = expression.arguments[1];
|
||||
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
|
||||
}
|
||||
|
||||
function isBinaryExpr(
|
||||
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
|
||||
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
|
||||
|
@ -1456,6 +1456,210 @@ exports.MissingClass2 = MissingClass2;
|
||||
expect(decorators[0].args).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function getConstructorParameters(
|
||||
constructor: string, mode?: 'inlined'|'inlined_with_suffix'|'imported') {
|
||||
let fileHeader = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeader = `const tslib = require('tslib');`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeader =
|
||||
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeader =
|
||||
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
}
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeader}
|
||||
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
|
||||
exports.TestClass = TestClass;`,
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host =
|
||||
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinitionOfFunction()', () => {
|
||||
|
@ -1417,86 +1417,236 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('synthesized constructors', () => {
|
||||
function getConstructorParameters(constructor: string) {
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
function getConstructorParameters(
|
||||
constructor: string,
|
||||
mode?: 'inlined'|'inlined_with_suffix'|'imported'|'imported_namespace') {
|
||||
let fileHeader = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeader = `import {__spread} from 'tslib';`;
|
||||
break;
|
||||
case 'imported_namespace':
|
||||
fileHeader = `import * as tslib from 'tslib';`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeader =
|
||||
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeader =
|
||||
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
|
||||
break;
|
||||
}
|
||||
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeader}
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`);
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}`);
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}`);
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
}`);
|
||||
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using namespace imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported_namespace');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using namespace imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported_namespace');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(returned parameters `decorators.args`)', () => {
|
||||
it('should be an empty array if param decorator has no `args` property', () => {
|
||||
loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]);
|
||||
|
@ -1564,6 +1564,231 @@ runInEachFileSystem(() => {
|
||||
expect(decorators[0].args).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function getConstructorParameters(
|
||||
constructor: string, mode: 'inlined'|'inlined_with_suffix'|'imported' = 'imported') {
|
||||
let fileHeaderWithUmd = '';
|
||||
|
||||
switch (mode) {
|
||||
case 'imported':
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tslib'))) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports', 'tslib'], factory) :
|
||||
(factory(global.test, global.tslib));
|
||||
}(this, (function (exports, tslib) { 'use strict';
|
||||
`;
|
||||
break;
|
||||
case 'inlined':
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
|
||||
(factory(global.test));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
|
||||
var __spread = (this && this.__spread) || function (...args) { /* ... */ }
|
||||
`;
|
||||
break;
|
||||
case 'inlined_with_suffix':
|
||||
fileHeaderWithUmd = `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :
|
||||
(factory(global.test));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
|
||||
var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
const file = {
|
||||
name: _('/synthesized_constructors.js'),
|
||||
contents: `
|
||||
${fileHeaderWithUmd}
|
||||
var TestClass = /** @class */ (function (_super) {
|
||||
__extends(TestClass, _super);
|
||||
${constructor}
|
||||
return TestClass;
|
||||
}(null));
|
||||
|
||||
exports.TestClass = TestClass;
|
||||
})));
|
||||
`,
|
||||
};
|
||||
|
||||
loadTestFiles([file]);
|
||||
const bundle = makeTestBundleProgram(file.name);
|
||||
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
|
||||
const classNode =
|
||||
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
|
||||
return host.getConstructorParameters(classNode);
|
||||
}
|
||||
|
||||
describe('TS -> ES5: synthesized constructors', () => {
|
||||
it('recognizes _this assignment from super call', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes super call as return statement', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass(arg) {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not consider manual super calls as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`
|
||||
function TestClass() {
|
||||
return _super.call(this) || this;
|
||||
}
|
||||
`);
|
||||
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not consider empty constructors as synthesized', () => {
|
||||
const parameters = getConstructorParameters(`function TestClass() {}`);
|
||||
expect(parameters!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453.
|
||||
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, __spread$1(arguments)) || this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
return _super.apply(this, tslib_1.__spread(arguments)) || this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
describe('with class member assignment', () => {
|
||||
it('recognizes delegate super call using inline spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using inline spread helper with suffix', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, __spread$1(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'inlined_with_suffix');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('recognizes delegate super call using imported spread helper', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this = _super.apply(this, tslib_1.__spread(arguments)) || this;
|
||||
_this.synthesizedProperty = null;
|
||||
return _this;
|
||||
}`,
|
||||
'imported');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case where a unique name was generated for _super or _this', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass() {
|
||||
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
|
||||
_this_1._this = null;
|
||||
_this_1._super = null;
|
||||
return _this_1;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters).toBeNull();
|
||||
});
|
||||
|
||||
it('does not consider constructors with parameters as synthesized', () => {
|
||||
const parameters = getConstructorParameters(
|
||||
`
|
||||
function TestClass(arg) {
|
||||
return _super.apply(this, __spread(arguments)) || this;
|
||||
}`,
|
||||
'inlined');
|
||||
|
||||
expect(parameters!.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinitionOfFunction()', () => {
|
||||
|
@ -172,7 +172,6 @@ export function readCommandLineAndConfiguration(
|
||||
emitFlags: api.EmitFlags.Default
|
||||
};
|
||||
}
|
||||
const allDiagnostics: Diagnostics = [];
|
||||
const config = readConfiguration(project, cmdConfig.options);
|
||||
const options = {...config.options, ...existingOptions};
|
||||
if (options.locale) {
|
||||
|
@ -17,14 +17,45 @@ import {GetterFn, MethodFn, SetterFn} from './types';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Attention: These regex has to hold even if the code is minified!
|
||||
/*
|
||||
* #########################
|
||||
* Attention: These Regular expressions have to hold even if the code is minified!
|
||||
* ##########################
|
||||
*/
|
||||
export const DELEGATE_CTOR = /^function\s+\S+\(\)\s*{[\s\S]+\.apply\(this,\s*arguments\)/;
|
||||
export const INHERITED_CLASS = /^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{/;
|
||||
export const INHERITED_CLASS_WITH_CTOR =
|
||||
|
||||
/**
|
||||
* Regular expression that detects pass-through constructors for ES5 output. This Regex
|
||||
* intends to capture the common delegation pattern emitted by TypeScript and Babel. Also
|
||||
* it intends to capture the pattern where existing constructors have been downleveled from
|
||||
* ES2015 to ES5 using TypeScript w/ downlevel iteration. e.g.
|
||||
*
|
||||
* ```
|
||||
* function MyClass() {
|
||||
* var _this = _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* function MyClass() {
|
||||
* var _this = _super.apply(this, __spread(arguments)) || this;
|
||||
* ```
|
||||
*
|
||||
* More details can be found in: https://github.com/angular/angular/issues/38453.
|
||||
*/
|
||||
export const ES5_DELEGATE_CTOR =
|
||||
/^function\s+\S+\(\)\s*{[\s\S]+\.apply\(this,\s*(arguments|[^()]+\(arguments\))\)/;
|
||||
/** Regular expression that detects ES2015 classes which extend from other classes. */
|
||||
export const ES2015_INHERITED_CLASS = /^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{/;
|
||||
/**
|
||||
* Regular expression that detects ES2015 classes which extend from other classes and
|
||||
* have an explicit constructor defined.
|
||||
*/
|
||||
export const ES2015_INHERITED_CLASS_WITH_CTOR =
|
||||
/^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{[\s\S]*constructor\s*\(/;
|
||||
export const INHERITED_CLASS_WITH_DELEGATE_CTOR =
|
||||
/**
|
||||
* Regular expression that detects ES2015 classes which extend from other classes
|
||||
* and inherit a constructor.
|
||||
*/
|
||||
export const ES2015_INHERITED_CLASS_WITH_DELEGATE_CTOR =
|
||||
/^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{[\s\S]*constructor\s*\(\)\s*{\s*super\(\.\.\.arguments\)/;
|
||||
|
||||
/**
|
||||
@ -36,8 +67,9 @@ export const INHERITED_CLASS_WITH_DELEGATE_CTOR =
|
||||
* an initialized instance property.
|
||||
*/
|
||||
export function isDelegateCtor(typeStr: string): boolean {
|
||||
return DELEGATE_CTOR.test(typeStr) || INHERITED_CLASS_WITH_DELEGATE_CTOR.test(typeStr) ||
|
||||
(INHERITED_CLASS.test(typeStr) && !INHERITED_CLASS_WITH_CTOR.test(typeStr));
|
||||
return ES5_DELEGATE_CTOR.test(typeStr) ||
|
||||
ES2015_INHERITED_CLASS_WITH_DELEGATE_CTOR.test(typeStr) ||
|
||||
(ES2015_INHERITED_CLASS.test(typeStr) && !ES2015_INHERITED_CLASS_WITH_CTOR.test(typeStr));
|
||||
}
|
||||
|
||||
export class ReflectionCapabilities implements PlatformReflectionCapabilities {
|
||||
|
@ -349,17 +349,6 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView|u
|
||||
return viewToDetach;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a view from a container, i.e. detaches it and then destroys the underlying LView.
|
||||
*
|
||||
* @param lContainer The container from which to remove a view
|
||||
* @param removeIndex The index of the view to remove
|
||||
*/
|
||||
export function removeView(lContainer: LContainer, removeIndex: number) {
|
||||
const detachedView = detachView(lContainer, removeIndex);
|
||||
detachedView && destroyLView(detachedView[TVIEW], detachedView);
|
||||
}
|
||||
|
||||
/**
|
||||
* A standalone function which destroys an LView,
|
||||
* conducting clean up (e.g. removing listeners, calling onDestroys).
|
||||
|
@ -210,7 +210,8 @@ export function consumeStyleKey(text: string, startIndex: number, endIndex: numb
|
||||
let ch: number;
|
||||
while (startIndex < endIndex &&
|
||||
((ch = text.charCodeAt(startIndex)) === CharCode.DASH || ch === CharCode.UNDERSCORE ||
|
||||
((ch & CharCode.UPPER_CASE) >= CharCode.A && (ch & CharCode.UPPER_CASE) <= CharCode.Z))) {
|
||||
((ch & CharCode.UPPER_CASE) >= CharCode.A && (ch & CharCode.UPPER_CASE) <= CharCode.Z) ||
|
||||
(ch >= CharCode.ZERO && ch <= CharCode.NINE))) {
|
||||
startIndex++;
|
||||
}
|
||||
return startIndex;
|
||||
|
@ -22,12 +22,12 @@ import {assertLContainer} from './assert';
|
||||
import {getParentInjectorLocation, NodeInjector} from './di';
|
||||
import {addToViewTree, createLContainer, createLView, renderView} from './instructions/shared';
|
||||
import {CONTAINER_HEADER_OFFSET, LContainer, VIEW_REFS} from './interfaces/container';
|
||||
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
||||
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
|
||||
import {isProceduralRenderer, RComment, RElement} from './interfaces/renderer';
|
||||
import {isComponentHost, isLContainer, isLView, isRootView} from './interfaces/type_checks';
|
||||
import {DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, LView, LViewFlags, PARENT, QUERIES, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||
import {assertNodeOfPossibleTypes} from './node_assert';
|
||||
import {addRemoveViewFromContainer, appendChild, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation';
|
||||
import {addRemoveViewFromContainer, appendChild, destroyLView, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode} from './node_manipulation';
|
||||
import {getParentInjectorTNode} from './node_util';
|
||||
import {getLView, getPreviousOrParentTNode} from './state';
|
||||
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
|
||||
@ -304,8 +304,18 @@ export function createContainerRef(
|
||||
remove(index?: number): void {
|
||||
this.allocateContainerIfNeeded();
|
||||
const adjustedIdx = this._adjustIndex(index, -1);
|
||||
removeView(this._lContainer, adjustedIdx);
|
||||
removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
|
||||
const detachedView = detachView(this._lContainer, adjustedIdx);
|
||||
|
||||
if (detachedView) {
|
||||
// Before destroying the view, remove it from the container's array of `ViewRef`s.
|
||||
// This ensures the view container length is updated before calling
|
||||
// `destroyLView`, which could recursively call view container methods that
|
||||
// rely on an accurate container length.
|
||||
// (e.g. a method on this view container being called by a child directive's OnDestroy
|
||||
// lifecycle hook)
|
||||
removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
|
||||
destroyLView(detachedView[TVIEW], detachedView);
|
||||
}
|
||||
}
|
||||
|
||||
detach(index?: number): viewEngine_ViewRef|null {
|
||||
|
@ -22,6 +22,8 @@ export const enum CharCode {
|
||||
SEMI_COLON = 59, // ";"
|
||||
BACK_SLASH = 92, // "\\"
|
||||
AT_SIGN = 64, // "@"
|
||||
ZERO = 48, // "0"
|
||||
NINE = 57, // "9"
|
||||
A = 65, // "A"
|
||||
U = 85, // "U"
|
||||
R = 82, // "R"
|
||||
|
@ -15,6 +15,23 @@ circular_dependency_test(
|
||||
deps = ["//packages/core/testing"],
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "downleveled_es5_fixture",
|
||||
srcs = ["reflection/es2015_inheritance_fixture.ts"],
|
||||
outs = ["reflection/es5_downleveled_inheritance_fixture.js"],
|
||||
cmd = """
|
||||
es2015_out_dir="/tmp/downleveled_es5_fixture/"
|
||||
es2015_out_file="$$es2015_out_dir/es2015_inheritance_fixture.js"
|
||||
|
||||
# Build the ES2015 output and then downlevel it to ES5.
|
||||
$(execpath @npm//typescript/bin:tsc) $< --outDir $$es2015_out_dir --target ES2015 \
|
||||
--types --module umd
|
||||
$(execpath @npm//typescript/bin:tsc) --outFile $@ $$es2015_out_file --allowJs \
|
||||
--types --target ES5
|
||||
""",
|
||||
tools = ["@npm//typescript/bin:tsc"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
@ -22,6 +39,7 @@ ts_library(
|
||||
["**/*.ts"],
|
||||
exclude = [
|
||||
"**/*_node_only_spec.ts",
|
||||
"reflection/es2015_inheritance_fixture.ts",
|
||||
],
|
||||
),
|
||||
# Visible to //:saucelabs_unit_tests_poc target
|
||||
@ -73,6 +91,9 @@ ts_library(
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node_es5"],
|
||||
data = [
|
||||
":downleveled_es5_fixture",
|
||||
],
|
||||
shard_count = 4,
|
||||
deps = [
|
||||
":test_lib",
|
||||
@ -87,6 +108,7 @@ jasmine_node_test(
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_web",
|
||||
runtime_deps = [":downleveled_es5_fixture"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
|
@ -214,28 +214,51 @@ describe('styling', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('css variables', () => {
|
||||
onlyInIvy('css variables').it('should support css variables', () => {
|
||||
onlyInIvy('CSS variables are only supported in Ivy').describe('css variables', () => {
|
||||
const supportsCssVariables = typeof getComputedStyle !== 'undefined' &&
|
||||
typeof CSS !== 'undefined' && typeof CSS.supports !== 'undefined' &&
|
||||
CSS.supports('color', 'var(--fake-var)');
|
||||
|
||||
it('should support css variables', () => {
|
||||
// This test only works in browsers which support CSS variables.
|
||||
if (!(typeof getComputedStyle !== 'undefined' && typeof CSS !== 'undefined' &&
|
||||
typeof CSS.supports !== 'undefined' && CSS.supports('color', 'var(--fake-var)')))
|
||||
if (!supportsCssVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div [style.--my-var]=" '100px' ">
|
||||
<span style="width: var(--my-var)">CONTENT</span>
|
||||
</div>`
|
||||
<div [style.--my-var]="'100px'">
|
||||
<span style="width: var(--my-var)">CONTENT</span>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
class Cmp {
|
||||
}
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
// document.body.appendChild(fixture.nativeElement);
|
||||
fixture.detectChanges();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('span') as HTMLElement;
|
||||
expect(getComputedStyle(span).getPropertyValue('width')).toEqual('100px');
|
||||
});
|
||||
|
||||
it('should support css variables with numbers in their name inside a host binding', () => {
|
||||
// This test only works in browsers which support CSS variables.
|
||||
if (!supportsCssVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Component({template: `<h1 style="width: var(--my-1337-var)">Hello</h1>`})
|
||||
class Cmp {
|
||||
@HostBinding('style') style = '--my-1337-var: 100px;';
|
||||
}
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
const header = fixture.nativeElement.querySelector('h1') as HTMLElement;
|
||||
expect(getComputedStyle(header).getPropertyValue('width')).toEqual('100px');
|
||||
});
|
||||
});
|
||||
|
||||
modifiedInIvy('shadow bindings include static portion')
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {computeMsgId} from '@angular/compiler';
|
||||
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, Injector, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
|
||||
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, Injector, NgModule, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
|
||||
import {Input} from '@angular/core/src/metadata';
|
||||
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
|
||||
@ -936,6 +936,62 @@ describe('ViewContainerRef', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('dependant views', () => {
|
||||
it('should not throw when view removes another view upon removal', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div *ngIf="visible" [template]="parent">I host a template</div>
|
||||
<ng-template #parent>
|
||||
<div [template]="child">I host a child template</div>
|
||||
</ng-template>
|
||||
<ng-template #child>
|
||||
I am child template
|
||||
</ng-template>
|
||||
`
|
||||
})
|
||||
class AppComponent {
|
||||
visible = true;
|
||||
|
||||
constructor(private readonly vcr: ViewContainerRef) {}
|
||||
|
||||
add<C>(template: TemplateRef<C>): EmbeddedViewRef<C> {
|
||||
return this.vcr.createEmbeddedView(template);
|
||||
}
|
||||
|
||||
remove<C>(viewRef: EmbeddedViewRef<C>) {
|
||||
this.vcr.remove(this.vcr.indexOf(viewRef));
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: '[template]'})
|
||||
class TemplateDirective<C> implements OnInit, OnDestroy {
|
||||
@Input() template !: TemplateRef<C>;
|
||||
ref!: EmbeddedViewRef<C>;
|
||||
|
||||
constructor(private readonly host: AppComponent) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.ref = this.host.add(this.template);
|
||||
this.ref.detectChanges();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.host.remove(this.ref);
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [AppComponent, TemplateDirective],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
fixture.componentRef.instance.visible = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createEmbeddedView (incl. insert)', () => {
|
||||
it('should work on elements', () => {
|
||||
@Component({
|
||||
|
22
packages/core/test/reflection/es2015_inheritance_fixture.ts
Normal file
22
packages/core/test/reflection/es2015_inheritance_fixture.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// AMD module name is required so that this file can be loaded in the Karma tests.
|
||||
/// <amd-module name="angular/packages/core/test/reflection/es5_downleveled_inheritance_fixture" />
|
||||
|
||||
class Parent {}
|
||||
|
||||
export class ChildNoCtor extends Parent {}
|
||||
export class ChildWithCtor extends Parent {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
export class ChildNoCtorPrivateProps extends Parent {
|
||||
x = 10;
|
||||
}
|
@ -201,6 +201,16 @@ class TestObj {
|
||||
expect(isDelegateCtor(ChildWithCtor.toString())).toBe(false);
|
||||
});
|
||||
|
||||
// See: https://github.com/angular/angular/issues/38453
|
||||
it('should support ES2015 downleveled classes', () => {
|
||||
const {ChildNoCtor, ChildNoCtorPrivateProps, ChildWithCtor} =
|
||||
require('./es5_downleveled_inheritance_fixture');
|
||||
|
||||
expect(isDelegateCtor(ChildNoCtor.toString())).toBe(true);
|
||||
expect(isDelegateCtor(ChildNoCtorPrivateProps.toString())).toBe(true);
|
||||
expect(isDelegateCtor(ChildWithCtor.toString())).toBe(false);
|
||||
});
|
||||
|
||||
it('should support ES2015 classes when minified', () => {
|
||||
// These classes are ES2015 in minified form
|
||||
const ChildNoCtorMinified = 'class ChildNoCtor extends Parent{}';
|
||||
|
@ -7,7 +7,13 @@ ts_library(
|
||||
srcs = glob(["*.ts"]),
|
||||
deps = [
|
||||
"//packages/compiler-cli",
|
||||
"//packages/language-service/ivy/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/core",
|
||||
"//packages/compiler-cli/src/ngtsc/core:api",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/incremental",
|
||||
"//packages/compiler-cli/src/ngtsc/shims",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
@ -1,17 +0,0 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//packages/language-service/ivy:__pkg__"])
|
||||
|
||||
ts_library(
|
||||
name = "compiler",
|
||||
srcs = glob(["*.ts"]),
|
||||
deps = [
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/src/ngtsc/core",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/incremental",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck/api",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
@ -1,2 +0,0 @@
|
||||
All files in this directory are temporary. This is created to simulate the final
|
||||
form of the Ivy compiler that supports language service.
|
@ -1,124 +0,0 @@
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CompilerOptions} from '@angular/compiler-cli';
|
||||
import {NgCompiler, NgCompilerHost} from '@angular/compiler-cli/src/ngtsc/core';
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
|
||||
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
||||
import {TypeCheckingProgramStrategy, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
import {makeCompilerHostFromProject} from './compiler_host';
|
||||
|
||||
interface AnalysisResult {
|
||||
compiler: NgCompiler;
|
||||
program: ts.Program;
|
||||
}
|
||||
|
||||
export class Compiler {
|
||||
private tsCompilerHost: ts.CompilerHost;
|
||||
private lastKnownProgram: ts.Program|null = null;
|
||||
private readonly strategy: TypeCheckingProgramStrategy;
|
||||
|
||||
constructor(private readonly project: ts.server.Project, private options: CompilerOptions) {
|
||||
this.tsCompilerHost = makeCompilerHostFromProject(project);
|
||||
this.strategy = createTypeCheckingProgramStrategy(project);
|
||||
// Do not retrieve the program in constructor because project is still in
|
||||
// the process of loading, and not all data members have been initialized.
|
||||
}
|
||||
|
||||
setCompilerOptions(options: CompilerOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
analyze(): AnalysisResult|undefined {
|
||||
const inputFiles = this.project.getRootFiles();
|
||||
const ngCompilerHost =
|
||||
NgCompilerHost.wrap(this.tsCompilerHost, inputFiles, this.options, this.lastKnownProgram);
|
||||
const program = this.strategy.getProgram();
|
||||
const compiler = new NgCompiler(
|
||||
ngCompilerHost, this.options, program, this.strategy,
|
||||
new PatchedProgramIncrementalBuildStrategy(), this.lastKnownProgram);
|
||||
try {
|
||||
// This is the only way to force the compiler to update the typecheck file
|
||||
// in the program. We have to do try-catch because the compiler immediately
|
||||
// throws if it fails to parse any template in the entire program!
|
||||
const d = compiler.getDiagnostics();
|
||||
if (d.length) {
|
||||
// There could be global compilation errors. It's useful to print them
|
||||
// out in development.
|
||||
console.error(d.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to analyze program', e.message);
|
||||
return;
|
||||
}
|
||||
this.lastKnownProgram = compiler.getNextProgram();
|
||||
return {
|
||||
compiler,
|
||||
program: this.lastKnownProgram,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTypeCheckingProgramStrategy(project: ts.server.Project):
|
||||
TypeCheckingProgramStrategy {
|
||||
return {
|
||||
supportsInlineOperations: false,
|
||||
shimPathForComponent(component: ts.ClassDeclaration): AbsoluteFsPath {
|
||||
return TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(component.getSourceFile()));
|
||||
},
|
||||
getProgram(): ts.Program {
|
||||
const program = project.getLanguageService().getProgram();
|
||||
if (!program) {
|
||||
throw new Error('Language service does not have a program!');
|
||||
}
|
||||
return program;
|
||||
},
|
||||
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode) {
|
||||
if (updateMode !== UpdateMode.Complete) {
|
||||
throw new Error(`Incremental update mode is currently not supported`);
|
||||
}
|
||||
for (const [fileName, newText] of contents) {
|
||||
const scriptInfo = getOrCreateTypeCheckScriptInfo(project, fileName);
|
||||
const snapshot = scriptInfo.getSnapshot();
|
||||
const length = snapshot.getLength();
|
||||
scriptInfo.editContent(0, length, newText);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getOrCreateTypeCheckScriptInfo(
|
||||
project: ts.server.Project, tcf: string): ts.server.ScriptInfo {
|
||||
// First check if there is already a ScriptInfo for the tcf
|
||||
const {projectService} = project;
|
||||
let scriptInfo = projectService.getScriptInfo(tcf);
|
||||
if (!scriptInfo) {
|
||||
// ScriptInfo needs to be opened by client to be able to set its user-defined
|
||||
// content. We must also provide file content, otherwise the service will
|
||||
// attempt to fetch the content from disk and fail.
|
||||
scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(
|
||||
ts.server.toNormalizedPath(tcf),
|
||||
true, // openedByClient
|
||||
'', // fileContent
|
||||
ts.ScriptKind.TS, // scriptKind
|
||||
);
|
||||
if (!scriptInfo) {
|
||||
throw new Error(`Failed to create script info for ${tcf}`);
|
||||
}
|
||||
}
|
||||
// Add ScriptInfo to project if it's missing. A ScriptInfo needs to be part of
|
||||
// the project so that it becomes part of the program.
|
||||
if (!project.containsScriptInfo(scriptInfo)) {
|
||||
project.addRoot(scriptInfo);
|
||||
}
|
||||
return scriptInfo;
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
export function makeCompilerHostFromProject(project: ts.server.Project): ts.CompilerHost {
|
||||
const compilerHost: ts.CompilerHost = {
|
||||
fileExists(fileName: string): boolean {
|
||||
return project.fileExists(fileName);
|
||||
},
|
||||
readFile(fileName: string): string |
|
||||
undefined {
|
||||
return project.readFile(fileName);
|
||||
},
|
||||
directoryExists(directoryName: string): boolean {
|
||||
return project.directoryExists(directoryName);
|
||||
},
|
||||
getCurrentDirectory(): string {
|
||||
return project.getCurrentDirectory();
|
||||
},
|
||||
getDirectories(path: string): string[] {
|
||||
return project.getDirectories(path);
|
||||
},
|
||||
getSourceFile(
|
||||
fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void,
|
||||
shouldCreateNewSourceFile?: boolean): ts.SourceFile |
|
||||
undefined {
|
||||
const path = project.projectService.toPath(fileName);
|
||||
return project.getSourceFile(path);
|
||||
},
|
||||
getSourceFileByPath(
|
||||
fileName: string, path: ts.Path, languageVersion: ts.ScriptTarget,
|
||||
onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile |
|
||||
undefined {
|
||||
return project.getSourceFile(path);
|
||||
},
|
||||
getCancellationToken(): ts.CancellationToken {
|
||||
return {
|
||||
isCancellationRequested() {
|
||||
return project.getCancellationToken().isCancellationRequested();
|
||||
},
|
||||
throwIfCancellationRequested() {
|
||||
if (this.isCancellationRequested()) {
|
||||
throw new ts.OperationCanceledException();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return project.getDefaultLibFileName();
|
||||
},
|
||||
writeFile(
|
||||
fileName: string, data: string, writeByteOrderMark: boolean,
|
||||
onError?: (message: string) => void, sourceFiles?: readonly ts.SourceFile[]) {
|
||||
return project.writeFile(fileName, data);
|
||||
},
|
||||
getCanonicalFileName(fileName: string): string {
|
||||
return project.projectService.toCanonicalFileName(fileName);
|
||||
},
|
||||
useCaseSensitiveFileNames(): boolean {
|
||||
return project.useCaseSensitiveFileNames();
|
||||
},
|
||||
getNewLine(): string {
|
||||
return project.getNewLine();
|
||||
},
|
||||
readDirectory(
|
||||
rootDir: string, extensions: readonly string[], excludes: readonly string[]|undefined,
|
||||
includes: readonly string[], depth?: number): string[] {
|
||||
return project.readDirectory(rootDir, extensions, excludes, includes, depth);
|
||||
},
|
||||
resolveModuleNames(
|
||||
moduleNames: string[], containingFile: string, reusedNames: string[]|undefined,
|
||||
redirectedReference: ts.ResolvedProjectReference|undefined, options: ts.CompilerOptions):
|
||||
(ts.ResolvedModule | undefined)[] {
|
||||
return project.resolveModuleNames(
|
||||
moduleNames, containingFile, reusedNames, redirectedReference);
|
||||
},
|
||||
resolveTypeReferenceDirectives(
|
||||
typeReferenceDirectiveNames: string[], containingFile: string,
|
||||
redirectedReference: ts.ResolvedProjectReference|undefined, options: ts.CompilerOptions):
|
||||
(ts.ResolvedTypeReferenceDirective | undefined)[] {
|
||||
return project.resolveTypeReferenceDirectives(
|
||||
typeReferenceDirectiveNames, containingFile, redirectedReference);
|
||||
},
|
||||
};
|
||||
|
||||
if (project.trace) {
|
||||
compilerHost.trace = function trace(s: string) {
|
||||
project.trace!(s);
|
||||
};
|
||||
}
|
||||
if (project.realpath) {
|
||||
compilerHost.realpath = function realpath(path: string): string {
|
||||
return project.realpath!(path);
|
||||
};
|
||||
}
|
||||
return compilerHost;
|
||||
}
|
@ -7,30 +7,53 @@
|
||||
*/
|
||||
|
||||
import {CompilerOptions, createNgCompilerOptions} from '@angular/compiler-cli';
|
||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||
import {NgCompilerAdapter} from '@angular/compiler-cli/src/ngtsc/core/api';
|
||||
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
|
||||
import {isShim} from '@angular/compiler-cli/src/ngtsc/shims';
|
||||
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
||||
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
import {Compiler} from './compiler/compiler';
|
||||
|
||||
export class LanguageService {
|
||||
private options: CompilerOptions;
|
||||
private readonly compiler: Compiler;
|
||||
private lastKnownProgram: ts.Program|null = null;
|
||||
private readonly strategy: TypeCheckingProgramStrategy;
|
||||
private readonly adapter: NgCompilerAdapter;
|
||||
|
||||
constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
|
||||
this.options = parseNgCompilerOptions(project);
|
||||
this.strategy = createTypeCheckingProgramStrategy(project);
|
||||
this.adapter = createNgCompilerAdapter(project);
|
||||
this.watchConfigFile(project);
|
||||
this.compiler = new Compiler(project, this.options);
|
||||
}
|
||||
|
||||
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
|
||||
const result = this.compiler.analyze();
|
||||
if (!result) {
|
||||
return [];
|
||||
const program = this.strategy.getProgram();
|
||||
const compiler = this.createCompiler(program);
|
||||
if (fileName.endsWith('.ts')) {
|
||||
const sourceFile = program.getSourceFile(fileName);
|
||||
if (!sourceFile) {
|
||||
return [];
|
||||
}
|
||||
const ttc = compiler.getTemplateTypeChecker();
|
||||
const diagnostics = ttc.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile);
|
||||
this.lastKnownProgram = compiler.getNextProgram();
|
||||
return diagnostics;
|
||||
}
|
||||
const {compiler, program} = result;
|
||||
const sourceFile = program.getSourceFile(fileName);
|
||||
if (!sourceFile) {
|
||||
return [];
|
||||
}
|
||||
return compiler.getDiagnostics(sourceFile);
|
||||
throw new Error('Ivy LS currently does not support external template');
|
||||
}
|
||||
|
||||
private createCompiler(program: ts.Program): NgCompiler {
|
||||
return new NgCompiler(
|
||||
this.adapter,
|
||||
this.options,
|
||||
program,
|
||||
this.strategy,
|
||||
new PatchedProgramIncrementalBuildStrategy(),
|
||||
this.lastKnownProgram,
|
||||
);
|
||||
}
|
||||
|
||||
private watchConfigFile(project: ts.server.Project) {
|
||||
@ -47,7 +70,6 @@ export class LanguageService {
|
||||
project.log(`Config file changed: ${fileName}`);
|
||||
if (eventKind === ts.FileWatcherEventKind.Changed) {
|
||||
this.options = parseNgCompilerOptions(project);
|
||||
this.compiler.setCompilerOptions(this.options);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -66,3 +88,80 @@ export function parseNgCompilerOptions(project: ts.server.Project): CompilerOpti
|
||||
const basePath = project.getCurrentDirectory();
|
||||
return createNgCompilerOptions(basePath, config, project.getCompilationSettings());
|
||||
}
|
||||
|
||||
function createNgCompilerAdapter(project: ts.server.Project): NgCompilerAdapter {
|
||||
return {
|
||||
entryPoint: null, // entry point is only needed if code is emitted
|
||||
constructionDiagnostics: [],
|
||||
ignoreForEmit: new Set(),
|
||||
factoryTracker: null, // no .ngfactory shims
|
||||
unifiedModulesHost: null, // only used in Bazel
|
||||
rootDirs: project.getCompilationSettings().rootDirs?.map(absoluteFrom) || [],
|
||||
isShim,
|
||||
fileExists(fileName: string): boolean {
|
||||
return project.fileExists(fileName);
|
||||
},
|
||||
readFile(fileName: string): string |
|
||||
undefined {
|
||||
return project.readFile(fileName);
|
||||
},
|
||||
getCurrentDirectory(): string {
|
||||
return project.getCurrentDirectory();
|
||||
},
|
||||
getCanonicalFileName(fileName: string): string {
|
||||
return project.projectService.toCanonicalFileName(fileName);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTypeCheckingProgramStrategy(project: ts.server.Project):
|
||||
TypeCheckingProgramStrategy {
|
||||
return {
|
||||
supportsInlineOperations: false,
|
||||
shimPathForComponent(component: ts.ClassDeclaration): AbsoluteFsPath {
|
||||
return TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(component.getSourceFile()));
|
||||
},
|
||||
getProgram(): ts.Program {
|
||||
const program = project.getLanguageService().getProgram();
|
||||
if (!program) {
|
||||
throw new Error('Language service does not have a program!');
|
||||
}
|
||||
return program;
|
||||
},
|
||||
updateFiles(contents: Map<AbsoluteFsPath, string>) {
|
||||
for (const [fileName, newText] of contents) {
|
||||
const scriptInfo = getOrCreateTypeCheckScriptInfo(project, fileName);
|
||||
const snapshot = scriptInfo.getSnapshot();
|
||||
const length = snapshot.getLength();
|
||||
scriptInfo.editContent(0, length, newText);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getOrCreateTypeCheckScriptInfo(
|
||||
project: ts.server.Project, tcf: string): ts.server.ScriptInfo {
|
||||
// First check if there is already a ScriptInfo for the tcf
|
||||
const {projectService} = project;
|
||||
let scriptInfo = projectService.getScriptInfo(tcf);
|
||||
if (!scriptInfo) {
|
||||
// ScriptInfo needs to be opened by client to be able to set its user-defined
|
||||
// content. We must also provide file content, otherwise the service will
|
||||
// attempt to fetch the content from disk and fail.
|
||||
scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(
|
||||
ts.server.toNormalizedPath(tcf),
|
||||
true, // openedByClient
|
||||
'', // fileContent
|
||||
ts.ScriptKind.TS, // scriptKind
|
||||
);
|
||||
if (!scriptInfo) {
|
||||
throw new Error(`Failed to create script info for ${tcf}`);
|
||||
}
|
||||
}
|
||||
// Add ScriptInfo to project if it's missing. A ScriptInfo needs to be part of
|
||||
// the project so that it becomes part of the program.
|
||||
if (!project.containsScriptInfo(scriptInfo)) {
|
||||
project.addRoot(scriptInfo);
|
||||
}
|
||||
return scriptInfo;
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ jasmine_node_test(
|
||||
],
|
||||
tags = [
|
||||
"ivy-only",
|
||||
"manual", # do not run this on CI since compiler APIs are not yet stable
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
|
@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import {LocationStrategy} from '@angular/common';
|
||||
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, isDevMode, OnChanges, OnDestroy, Renderer2} from '@angular/core';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, isDevMode, OnChanges, OnDestroy, Renderer2, SimpleChanges} from '@angular/core';
|
||||
import {Subject, Subscription} from 'rxjs';
|
||||
|
||||
import {QueryParamsHandling} from '../config';
|
||||
import {Event, NavigationEnd} from '../events';
|
||||
@ -115,7 +115,7 @@ import {UrlTree} from '../url_tree';
|
||||
* @publicApi
|
||||
*/
|
||||
@Directive({selector: ':not(a):not(area)[routerLink]'})
|
||||
export class RouterLink {
|
||||
export class RouterLink implements OnChanges {
|
||||
/**
|
||||
* Passed to {@link Router#createUrlTree Router#createUrlTree} as part of the `NavigationExtras`.
|
||||
* @see {@link NavigationExtras#queryParams NavigationExtras#queryParams}
|
||||
@ -167,6 +167,9 @@ export class RouterLink {
|
||||
private commands: any[] = [];
|
||||
private preserve!: boolean;
|
||||
|
||||
/** @internal */
|
||||
onChanges = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private router: Router, private route: ActivatedRoute,
|
||||
@Attribute('tabindex') tabIndex: string, renderer: Renderer2, el: ElementRef) {
|
||||
@ -175,6 +178,13 @@ export class RouterLink {
|
||||
}
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
// This is subscribed to by `RouterLinkActive` so that it knows to update when there are changes
|
||||
// to the RouterLinks it's tracking.
|
||||
this.onChanges.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commands to pass to {@link Router#createUrlTree Router#createUrlTree}.
|
||||
* - **array**: commands to pass to {@link Router#createUrlTree Router#createUrlTree}.
|
||||
@ -202,6 +212,7 @@ export class RouterLink {
|
||||
this.preserve = value;
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
@HostListener('click')
|
||||
onClick(): boolean {
|
||||
const extras = {
|
||||
@ -297,6 +308,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@HostBinding() href!: string;
|
||||
|
||||
/** @internal */
|
||||
onChanges = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private router: Router, private route: ActivatedRoute,
|
||||
private locationStrategy: LocationStrategy) {
|
||||
@ -334,13 +348,17 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
this.preserve = value;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {}): any {
|
||||
/** @nodoc */
|
||||
ngOnChanges(changes: SimpleChanges): any {
|
||||
this.updateTargetUrlAndHref();
|
||||
this.onChanges.next();
|
||||
}
|
||||
/** @nodoc */
|
||||
ngOnDestroy(): any {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
@HostListener('click', ['$event.button', '$event.ctrlKey', '$event.metaKey', '$event.shiftKey'])
|
||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean {
|
||||
if (button !== 0 || ctrlKey || metaKey || shiftKey) {
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
|
||||
import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, Optional, QueryList, Renderer2, SimpleChanges} from '@angular/core';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {from, of, Subscription} from 'rxjs';
|
||||
import {mergeAll} from 'rxjs/operators';
|
||||
|
||||
import {Event, NavigationEnd} from '../events';
|
||||
import {Router} from '../router';
|
||||
@ -79,14 +80,13 @@ import {RouterLink, RouterLinkWithHref} from './router_link';
|
||||
exportAs: 'routerLinkActive',
|
||||
})
|
||||
export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@ContentChildren(RouterLink, {descendants: true}) links!: QueryList<RouterLink>;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@ContentChildren(RouterLinkWithHref, {descendants: true})
|
||||
linksWithHrefs!: QueryList<RouterLinkWithHref>;
|
||||
|
||||
private classes: string[] = [];
|
||||
private subscription: Subscription;
|
||||
private routerEventsSubscription: Subscription;
|
||||
private linkInputChangesSubscription?: Subscription;
|
||||
public readonly isActive: boolean = false;
|
||||
|
||||
@Input() routerLinkActiveOptions: {exact: boolean} = {exact: false};
|
||||
@ -95,18 +95,32 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
||||
private router: Router, private element: ElementRef, private renderer: Renderer2,
|
||||
private readonly cdr: ChangeDetectorRef, @Optional() private link?: RouterLink,
|
||||
@Optional() private linkWithHref?: RouterLinkWithHref) {
|
||||
this.subscription = router.events.subscribe((s: Event) => {
|
||||
this.routerEventsSubscription = router.events.subscribe((s: Event) => {
|
||||
if (s instanceof NavigationEnd) {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** @nodoc */
|
||||
ngAfterContentInit(): void {
|
||||
this.links.changes.subscribe(_ => this.update());
|
||||
this.linksWithHrefs.changes.subscribe(_ => this.update());
|
||||
this.update();
|
||||
// `of(null)` is used to force subscribe body to execute once immediately (like `startWith`).
|
||||
from([this.links.changes, this.linksWithHrefs.changes, of(null)])
|
||||
.pipe(mergeAll())
|
||||
.subscribe(_ => {
|
||||
this.update();
|
||||
this.subscribeToEachLinkOnChanges();
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeToEachLinkOnChanges() {
|
||||
this.linkInputChangesSubscription?.unsubscribe();
|
||||
const allLinkChanges =
|
||||
[...this.links.toArray(), ...this.linksWithHrefs.toArray(), this.link, this.linkWithHref]
|
||||
.filter((link): link is RouterLink|RouterLinkWithHref => !!link)
|
||||
.map(link => link.onChanges);
|
||||
this.linkInputChangesSubscription =
|
||||
from(allLinkChanges).pipe(mergeAll()).subscribe(() => this.update());
|
||||
}
|
||||
|
||||
@Input()
|
||||
@ -115,11 +129,14 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit
|
||||
this.classes = classes.filter(c => !!c);
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.update();
|
||||
}
|
||||
/** @nodoc */
|
||||
ngOnDestroy(): void {
|
||||
this.subscription.unsubscribe();
|
||||
this.routerEventsSubscription.unsubscribe();
|
||||
this.linkInputChangesSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
|
@ -76,10 +76,12 @@ export class RouterOutlet implements OnDestroy, OnInit {
|
||||
parentContexts.onChildOutletCreated(this.name, this);
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
ngOnDestroy(): void {
|
||||
this.parentContexts.onChildOutletDestroyed(this.name);
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
ngOnInit(): void {
|
||||
if (!this.activated) {
|
||||
// If the outlet was not instantiated at the time the route got activated we need to populate
|
||||
|
@ -948,7 +948,7 @@ export class Router {
|
||||
this.lastSuccessfulId = -1;
|
||||
}
|
||||
|
||||
/** @docsNotRequired */
|
||||
/** @nodoc */
|
||||
ngOnDestroy(): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ export class RouterPreloader implements OnDestroy {
|
||||
return this.processRoutes(ngModule, this.router.config);
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
|
@ -87,6 +87,7 @@ export class RouterScroller implements OnDestroy {
|
||||
routerEvent, this.lastSource === 'popstate' ? this.store[this.restoredId] : null, anchor));
|
||||
}
|
||||
|
||||
/** @nodoc */
|
||||
ngOnDestroy() {
|
||||
if (this.routerEventsSubscription) {
|
||||
this.routerEventsSubscription.unsubscribe();
|
||||
|
@ -14,6 +14,54 @@ import {RouterTestingModule} from '@angular/router/testing';
|
||||
|
||||
describe('Integration', () => {
|
||||
describe('routerLinkActive', () => {
|
||||
it('should update when the associated routerLinks change - #18469', fakeAsync(() => {
|
||||
@Component({
|
||||
template: `
|
||||
<a id="first-link" [routerLink]="[firstLink]" routerLinkActive="active">{{firstLink}}</a>
|
||||
<div id="second-link" routerLinkActive="active">
|
||||
<a [routerLink]="[secondLink]">{{secondLink}}</a>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class LinkComponent {
|
||||
firstLink = 'link-a';
|
||||
secondLink = 'link-b';
|
||||
|
||||
changeLinks(): void {
|
||||
const temp = this.secondLink;
|
||||
this.secondLink = this.firstLink;
|
||||
this.firstLink = temp;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({template: 'simple'})
|
||||
class SimpleCmp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule.withRoutes(
|
||||
[{path: 'link-a', component: SimpleCmp}, {path: 'link-b', component: SimpleCmp}])],
|
||||
declarations: [LinkComponent, SimpleCmp]
|
||||
});
|
||||
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, LinkComponent);
|
||||
const firstLink = fixture.debugElement.query(p => p.nativeElement.id === 'first-link');
|
||||
const secondLink = fixture.debugElement.query(p => p.nativeElement.id === 'second-link');
|
||||
router.navigateByUrl('/link-a');
|
||||
advance(fixture);
|
||||
|
||||
expect(firstLink.nativeElement.classList).toContain('active');
|
||||
expect(secondLink.nativeElement.classList).not.toContain('active');
|
||||
|
||||
fixture.componentInstance.changeLinks();
|
||||
fixture.detectChanges();
|
||||
advance(fixture);
|
||||
|
||||
expect(firstLink.nativeElement.classList).not.toContain('active');
|
||||
expect(secondLink.nativeElement.classList).toContain('active');
|
||||
}));
|
||||
|
||||
it('should not cause infinite loops in the change detection - #15825', fakeAsync(() => {
|
||||
@Component({selector: 'simple', template: 'simple'})
|
||||
class SimpleCmp {
|
||||
|
@ -245,7 +245,10 @@ def karma_web_test_suite(name, **kwargs):
|
||||
runtime_deps = runtime_deps,
|
||||
bootstrap = bootstrap,
|
||||
deps = deps,
|
||||
browsers = ["//dev-infra/browsers/firefox:firefox"],
|
||||
browsers = [
|
||||
"//dev-infra/browsers/chromium:chromium",
|
||||
"//dev-infra/browsers/firefox:firefox",
|
||||
],
|
||||
data = data,
|
||||
tags = tags,
|
||||
**kwargs
|
||||
|
Reference in New Issue
Block a user