Compare commits

..

22 Commits

Author SHA1 Message Date
3e80f0e526 release: cut the v10.0.10 release 2020-08-17 13:10:36 -07:00
f3dd6c224c fix(ngcc): detect synthesized delegate constructors for downleveled ES2015 classes (#38500)
Similarly to the change we landed in the `@angular/core` reflection
capabilities, we need to make sure that ngcc can detect pass-through
delegate constructors for classes using downleveled ES2015 output.

More details can be found in the preceding commit, and in the issue
outlining the problem: #38453.

Fixes #38453.

PR Close #38500
2020-08-17 12:50:19 -07:00
863acb6c21 fix(core): detect DI parameters in JIT mode for downleveled ES2015 classes (#38500)
In the Angular Package Format, we always shipped UMD bundles and previously even ES5 module output.
With V10, we removed the ES5 module output but kept the UMD ES5 output.

For this, we were able to remove our second TypeScript transpilation. Instead we started only
building ES2015 output and then downleveled it to ES5 UMD for the NPM packages. This worked
as expected but unveiled an issue in the `@angular/core` reflection capabilities.

In JIT mode, Angular determines constructor parameters (for DI) using the `ReflectionCapabilities`. The
reflection capabilities basically read runtime metadata of classes to determine the DI parameters. Such
metadata can be either stored in static class properties like `ctorParameters` or within TypeScript's `design:params`.

If Angular comes across a class that does not have any parameter metadata, it tries to detect if the
given class is actually delegating to an inherited class. It does this naively in JIT by checking if the
stringified class (function in ES5) matches a certain pattern. e.g.

```js
function MatTable() {
  var _this = _super.apply(this, arguments) || this;
```

These patterns are reluctant to changes of the class output. If a class is not recognized properly, the
DI parameters will be assumed empty and the class is **incorrectly** constructed without arguments.

This actually happened as part of v10 now. Since we downlevel ES2015 to ES5 (instead of previously
compiling sources directly to ES5), the class output changed slightly so that Angular no longer detects
it. e.g.

```js
var _this = _super.apply(this, __spread(arguments)) || this;
```

This happens because the ES2015 output will receive an auto-generated constructor if the class
defines class properties. This constructor is then already containing an explicit `super` call.

```js
export class MatTable extends CdkTable {
    constructor() {
        super(...arguments);
        this.disabled = true;
    }
}
```

If we then downlevel this file to ES5 with `--downlevelIteration`, TypeScript adjusts the `super` call so that
the spread operator is no longer used (not supported in ES5). The resulting super call is different to the
super call that would have been emitted if we would directly transpile to ES5. Ultimately, Angular no
longer detects such classes as having an delegate constructor -> and DI breaks.

We fix this by expanding the rather naive RegExp patterns used for the reflection capabilities
so that downleveled pass-through/delegate constructors are properly detected. There is a risk
of a false-positive as we cannot detect whether `__spread` is actually the TypeScript spread
helper, but given the reflection patterns already make lots of assumptions (e.g. that `super` is
actually the superclass, we should be fine making this assumption too. The false-positive would
not result in a broken app, but rather in unnecessary providers being injected (as a noop).

Fixes #38453

PR Close #38500
2020-08-17 12:50:16 -07:00
989e8a1f99 fix(router): ensure routerLinkActive updates when associated routerLinks change (#38349)
This commit introduces a new subscription in the `routerLinkActive` directive which triggers an update
when any of its associated routerLinks have changes. `RouterLinkActive` not only needs to know when
links are added or removed, but it also needs to know about if a link it already knows about
changes in some way.

Quick note that `from...mergeAll` is used instead of just a simple
`merge` (or `scheduled...mergeAll`) to avoid introducing new rxjs
operators in order to keep bundle size down.

Fixes #18469

PR Close #38349
2020-08-17 12:34:02 -07:00
84d1ba792b refactor(language-service): [Ivy] remove temporary compiler (#38310)
Now that Ivy compiler has a proper `TemplateTypeChecker` interface
(see https://github.com/angular/angular/pull/38105) we no longer need to
keep the temporary compiler implementation.

The temporary compiler was created to enable testing infrastructure to
be developed for the Ivy language service.

This commit removes the whole `ivy/compiler` directory and moves two
functions `createTypeCheckingProgramStrategy` and
`getOrCreateTypeCheckScriptInfo` to the `LanguageService` class.

Also re-enable the Ivy LS test since it's no longer blocking development.

PR Close #38310
2020-08-17 11:30:36 -07:00
b32126c335 fix(common): Allow scrolling when browser supports scrollTo (#38468)
This commit fixes a regression from "fix(common): ensure
scrollRestoration is writable (#30630)" that caused scrolling to not
happen at all in browsers that do not support scroll restoration. The
issue was that `supportScrollRestoration` was updated to return `false`
if a browser did not have a writable `scrollRestoration`. However, the
previous behavior was that the function would return `true` if
`window.scrollTo` was defined. Every scrolling function in the
`ViewportScroller` used `supportScrollRestoration` and, with the update
in bb88c9fa3d, no scrolling would be
performed if a browser did not have writable `scrollRestoration` but
_did_ have `window.scrollTo`.

Note, that this failure was detected in the saucelabs tests. IE does not
support scroll restoration so IE tests were failing.

PR Close #38468
2020-08-14 11:41:26 -07:00
d5e09f4d62 fix(core): fix multiple nested views removal from ViewContainerRef (#38317)
When removal of one view causes removal of another one from the same
ViewContainerRef it triggers an error with views length calculation. This commit
fixes this bug by removing a view from the list of available views before invoking
actual view removal (which might be recursive and relies on the length of the list
of available views).

Fixes #38201.

PR Close #38317
2020-08-13 13:35:56 -07:00
Ahn
c25c57c3a3 style(compiler-cli): remove unused constant (#38441)
Remove unused constant allDiagnostics

PR Close #38441
2020-08-13 13:32:44 -07:00
692e34d4a2 test(docs-infra): remove deprecated ReflectiveInjector (#38408)
This commit replaces the old and slow `ReflectiveInjector` that was
deprecated in v5 with the new `Injector`. Note: This change was only
done in the spec files inside the `aio` folder.

While changing this, it was not possible to directly use `Injector.get`
to get the correct typing for the mocked classes. For example:

```typescript
locationService = injector.get<TestLocationService>(LocationService);
```

Fails with:

> Argument of type 'typeof LocationService' is not assignable to parameter
of type 'Type<TestLocationService> | InjectionToken<TestLocationService> |
AbstractType<TestLocationService>'.
  Type 'typeof LocationService' is not assignable to type 'Type<TestLocationService>'.
    Property 'searchResult' is missing in type 'LocationService' but required in type
    'TestLocationService'.

Therefore, it was necessary to first convert to `unknown` and then to
`TestLocationService`.

```typescript
locationService = injector.get(LocationService) as unknown as TestLocationService;
```

PR Close #38408
2020-08-13 12:56:17 -07:00
071348eb72 build: run browsers tests on chromium locally (#38435) (#38450)
Previously we added a browser target for `firefox` into the
dev-infra package. It looks like as part of this change, we
accidentally switched the local web testing target to `firefox`.

Web tests are not commonly run locally as we use Domino and
NodeJS tests for primary development. Sometimes though we intend
to run tests in a browser. This would currently work with Firefox
but not on Windows (as Firefox is a noop there in Bazel).

This commit switches the primary browser back to `chromium`. Also
Firefox has been added as a second browser to web testing targets.

This allows us to reduce browsers in the legacy Saucelabs job. i.e.
not running Chrome and Firefox there. This should increase stability
and speed up the legacy job (+ reduced rate limit for Saucelabs).

PR Close #38435

PR Close #38450
2020-08-13 12:55:16 -07:00
8803f9f4dc ci: disable saucelabs tests on Firefox ESR while investigating failures (#37647) (#38450)
Firefox ESR tests fail running the acceptance tests on saucelabs.  These tests are being
disabled while investigating the failure as it is not entirely clear whether this is
saucelabs failure or a something like a memory pressure error in the test itself.

PR Close #37647

PR Close #38450
2020-08-13 12:55:13 -07:00
aa816d3887 ci: disable closure size tracking test (#38449)
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.

PR Close #38449
2020-08-13 11:41:16 -07:00
a5ba40a78b refactor(router): Add annotations to correct Router documentation (#38448)
The `@HostListener` functions and lifecycle hooks aren't intended to be public API but
do need to appear in the `.d.ts` files or type checking will break. Adding the
nodoc annotation will correctly hide this function on the docs site.

Again, note that `@internal` cannot be used because the result would be
that the functions then do not appear in the `.d.ts` files. This would
break lifecycle hooks because the class would be seen as not
implementing the interface correctly. This would also break
`HostListener` because the compiled templates would attempt to call the
`onClick` functions, but those would also not appear in the `d.ts` and
would produce errors like "Property 'onClick' does not exist on type 'RouterLinkWithHref'".

PR Close #38448
2020-08-13 11:36:13 -07:00
cb83b8a887 fix(core): error if CSS custom property in host binding has number in name (#38432)
Fixes an error if a CSS custom property, used inside a host binding, has a
number in its name. The error is thrown because the styling parser only
expects characters from A to Z,dashes, underscores and a handful of other
characters.

Fixes #37292.

PR Close #38432
2020-08-13 10:35:11 -07:00
8bb726e899 build: update ng-dev config file for new commit message configuration (#38430)
Removes the commit message types from the config as they are now staticly
defined in the dev-infra code.

PR Close #38430
2020-08-13 09:11:25 -07:00
88662a540d feat(dev-infra): migrate to unified commit message types in commit message linting (#38430)
Previously commit message types were provided as part of the ng-dev config in the repository
using the ng-dev toolset.  This change removes this configuration expectation and instead
predefines the valid types for commit messages.

Additionally, with this new unified set of types requirements around providing a scope have
been put in place.  Scopes are either required, optional or forbidden for a given commit
type.

PR Close #38430
2020-08-13 09:11:23 -07:00
9b32a5917c feat(dev-infra): update to latest benchpress version (#38440)
We recently updated the benchpress package to have a more loose
Angular core peer dependency, and less other unused dependencies.

We should make sure to use that in the dev-infra package so that
peer dependencies can be satisified in consumer projects, and so
that less unused dependencies are brought into projects.

PR Close #38440
2020-08-13 09:09:34 -07:00
ffd1691ba9 feat(dev-infra): save invalid commit message attempts to be restored on next commit attempt (#38304)
When a commit message fails validation, rather than throwing out the commit message entirely
the commit message is saved into a draft file and restored on the next commit attempt.

PR Close #38304
2020-08-13 08:45:28 -07:00
afd4417a7b refactor(dev-infra): extract the commit message parsing function into its own file (#38429)
Extracts the commit message parsing function into its own file.

PR Close #38429
2020-08-12 16:10:08 -07:00
2f53bbba20 docs: remove unused Input decorator (#38306)
In the part "5. Add In-app Navigation" of the tutorial it was already removed
PR Close #38306
2020-08-12 11:26:11 -07:00
a4f99f4de9 refactor(dev-infra): use promptConfirm util in ng-dev's formatter (#38419)
Use the promptConfirm util instead of manually creating a confirm prompt with
inquirer.

PR Close #38419
2020-08-12 11:25:12 -07:00
0711128d28 docs: update web-worker CLI commands to bash style (#38421)
With this change we update the CLI generate commands to be in bash style.

PR Close #38421
2020-08-12 11:24:36 -07:00
68 changed files with 1807 additions and 685 deletions

View File

@ -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

View File

@ -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',

View File

@ -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)

View File

@ -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,

View File

@ -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.

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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);
});

View File

@ -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: [] }
]});
}

View File

@ -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);
});

View File

@ -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();

View File

@ -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);
});

View File

@ -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', () => {

View File

@ -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);
});

View File

@ -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');
});

View File

@ -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>.');
});

View File

@ -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();

View File

@ -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.

View File

@ -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",
],
)

View File

@ -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': {

View 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);
}

View File

@ -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,
},
};

View 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);
});
});

View 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),
};
}

View 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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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']
}
};

View File

@ -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;
}

View File

@ -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",

View File

@ -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) {

View File

@ -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';

View File

@ -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>",

View File

@ -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;
}

View File

@ -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": {

View File

@ -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"
}
}
}

View File

@ -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 {

View File

@ -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', () => {

View File

@ -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;

View File

@ -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()', () => {

View File

@ -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]);

View 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()', () => {

View File

@ -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) {

View File

@ -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 {

View File

@ -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).

View File

@ -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;

View File

@ -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 {

View File

@ -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"

View File

@ -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",
],

View File

@ -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')

View File

@ -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({

View 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;
}

View File

@ -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{}';

View File

@ -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",
],
)

View File

@ -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",
],
)

View File

@ -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.

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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",

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -948,7 +948,7 @@ export class Router {
this.lastSuccessfulId = -1;
}
/** @docsNotRequired */
/** @nodoc */
ngOnDestroy(): void {
this.dispose();
}

View File

@ -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();

View File

@ -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();

View File

@ -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 {

View File

@ -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