Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
b6bd8d7572 | |||
b08168bb90 | |||
407fa42679 | |||
aef432384a | |||
fb70083339 | |||
c9c2408176 | |||
e066bddfe9 | |||
447a600477 | |||
70f9bfff43 | |||
57c02b044c | |||
6defe962c8 | |||
267bcb3e9c | |||
b0b66881b4 | |||
9ff8d78bcd | |||
563b707497 | |||
5357e643b3 | |||
f71d132f7c | |||
ba3edda230 | |||
0767d37c07 | |||
8ba24578bc | |||
133a97ad67 | |||
4e67a3ab3f | |||
377f0010fc | |||
6e09129e4c | |||
d80e51a6b1 | |||
feb66b00da | |||
cb19eac105 | |||
6e0564ade6 | |||
05eeb7d279 | |||
2ce5fa3cce | |||
e140cdcb34 | |||
14b2db1d43 | |||
2afc7e982e |
@ -280,7 +280,7 @@ jobs:
|
|||||||
|
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
- run: yarn ts-circular-deps:check
|
- run: yarn ts-circular-deps:check
|
||||||
- run: node tools/pullapprove/verify.js
|
- run: yarn -s ng-dev pullapprove:verify
|
||||||
|
|
||||||
test:
|
test:
|
||||||
executor:
|
executor:
|
||||||
|
47
.dev-infra.json
Normal file
47
.dev-infra.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"commitMessage": {
|
||||||
|
"maxLength": 120,
|
||||||
|
"minBodyLength": 0,
|
||||||
|
"types": [
|
||||||
|
"build",
|
||||||
|
"ci",
|
||||||
|
"docs",
|
||||||
|
"feat",
|
||||||
|
"fix",
|
||||||
|
"perf",
|
||||||
|
"refactor",
|
||||||
|
"release",
|
||||||
|
"style",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"scopes": [
|
||||||
|
"animations",
|
||||||
|
"bazel",
|
||||||
|
"benchpress",
|
||||||
|
"changelog",
|
||||||
|
"common",
|
||||||
|
"compiler",
|
||||||
|
"compiler-cli",
|
||||||
|
"core",
|
||||||
|
"dev-infra",
|
||||||
|
"docs-infra",
|
||||||
|
"elements",
|
||||||
|
"forms",
|
||||||
|
"http",
|
||||||
|
"language-service",
|
||||||
|
"localize",
|
||||||
|
"ngcc",
|
||||||
|
"packaging",
|
||||||
|
"platform-browser",
|
||||||
|
"platform-browser-dynamic",
|
||||||
|
"platform-server",
|
||||||
|
"platform-webworker",
|
||||||
|
"platform-webworker-dynamic",
|
||||||
|
"router",
|
||||||
|
"service-worker",
|
||||||
|
"upgrade",
|
||||||
|
"ve",
|
||||||
|
"zone.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -958,7 +958,6 @@ groups:
|
|||||||
'tools/ngcontainer/**',
|
'tools/ngcontainer/**',
|
||||||
'tools/npm/**',
|
'tools/npm/**',
|
||||||
'tools/npm_integration_test/**',
|
'tools/npm_integration_test/**',
|
||||||
'tools/pullapprove/**',
|
|
||||||
'tools/rxjs/**',
|
'tools/rxjs/**',
|
||||||
'tools/saucelabs/**',
|
'tools/saucelabs/**',
|
||||||
'tools/size-tracking/**',
|
'tools/size-tracking/**',
|
||||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,3 +1,40 @@
|
|||||||
|
<a name="9.1.0"></a>
|
||||||
|
# [9.1.0](https://github.com/angular/angular/compare/9.1.0-rc.2...9.1.0) (2020-03-25)
|
||||||
|
|
||||||
|
Promoted `9.1.0-rc.2` to `9.1.0`.
|
||||||
|
|
||||||
|
<a name="9.1.0-rc.2"></a>
|
||||||
|
# [9.1.0-rc.2](https://github.com/angular/angular/compare/9.1.0-rc.1...9.1.0-rc.2) (2020-03-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **common:** let `KeyValuePipe` accept type unions with `null` ([#36093](https://github.com/angular/angular/issues/36093)) ([407fa42](https://github.com/angular/angular/commit/407fa42)), closes [#35743](https://github.com/angular/angular/issues/35743)
|
||||||
|
* **elements:** correctly handle setting inputs to `undefined` ([#36140](https://github.com/angular/angular/issues/36140)) ([e066bdd](https://github.com/angular/angular/commit/e066bdd))
|
||||||
|
* **elements:** correctly set `SimpleChange#firstChange` for pre-existing inputs ([#36140](https://github.com/angular/angular/issues/36140)) ([447a600](https://github.com/angular/angular/commit/447a600)), closes [#36130](https://github.com/angular/angular/issues/36130)
|
||||||
|
* **ngcc:** use path-mappings from tsconfig in dependency resolution ([#36180](https://github.com/angular/angular/issues/36180)) ([6defe96](https://github.com/angular/angular/commit/6defe96)), closes [#36119](https://github.com/angular/angular/issues/36119)
|
||||||
|
* **ngcc:** use preserve whitespaces from tsconfig if provided ([#36189](https://github.com/angular/angular/issues/36189)) ([aef4323](https://github.com/angular/angular/commit/aef4323)), closes [#35871](https://github.com/angular/angular/issues/35871)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **compiler:** add dependency info and ng-content selectors to metadata ([#35695](https://github.com/angular/angular/issues/35695)) ([fb70083](https://github.com/angular/angular/commit/fb70083))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="9.1.0-rc.1"></a>
|
||||||
|
# [9.1.0-rc.1](https://github.com/angular/angular/compare/9.1.0-rc.0...9.1.0-rc.1) (2020-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compiler:** record correct end of expression ([#34690](https://github.com/angular/angular/issues/34690)) ([df890d7](https://github.com/angular/angular/commit/df890d7)), closes [#33477](https://github.com/angular/angular/issues/33477)
|
||||||
|
* **core:** workaround Terser inlining bug ([#36200](https://github.com/angular/angular/issues/36200)) ([f71d132](https://github.com/angular/angular/commit/f71d132))
|
||||||
|
* **localize:** allow ICU expansion case to start with any character except `}` ([#36123](https://github.com/angular/angular/issues/36123)) ([0767d37](https://github.com/angular/angular/commit/0767d37)), closes [#31586](https://github.com/angular/angular/issues/31586)
|
||||||
|
* **compiler:** Propagate value span of ExpressionBinding to ParsedProperty ([#36133](https://github.com/angular/angular/issues/36133)) ([2ce5fa3](https://github.com/angular/angular/commit/2ce5fa3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="9.1.0-rc.0"></a>
|
<a name="9.1.0-rc.0"></a>
|
||||||
# [9.1.0-rc.0](https://github.com/angular/angular/compare/9.1.0-next.5...9.1.0-rc.0) (2020-03-19)
|
# [9.1.0-rc.0](https://github.com/angular/angular/compare/9.1.0-next.5...9.1.0-rc.0) (2020-03-19)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ describe('Interpolation e2e tests', () => {
|
|||||||
let pottedPlant = element.all(by.css('img')).get(0);
|
let pottedPlant = element.all(by.css('img')).get(0);
|
||||||
let lamp = element.all(by.css('img')).get(1);
|
let lamp = element.all(by.css('img')).get(1);
|
||||||
|
|
||||||
expect(pottedPlant.getAttribute('src')).toContain('pottedPlant');
|
expect(pottedPlant.getAttribute('src')).toContain('potted-plant');
|
||||||
expect(pottedPlant.isDisplayed()).toBe(true);
|
expect(pottedPlant.isDisplayed()).toBe(true);
|
||||||
|
|
||||||
expect(lamp.getAttribute('src')).toContain('lamp');
|
expect(lamp.getAttribute('src')).toContain('lamp');
|
||||||
|
@ -12,7 +12,7 @@ export class AppComponent {
|
|||||||
|
|
||||||
currentCustomer = 'Maria';
|
currentCustomer = 'Maria';
|
||||||
title = 'Featured product:';
|
title = 'Featured product:';
|
||||||
itemImageUrl = '../assets/pottedPlant.png';
|
itemImageUrl = '../assets/potted-plant.png';
|
||||||
|
|
||||||
recommended = 'You might also like:';
|
recommended = 'You might also like:';
|
||||||
itemImageUrl2 = '../assets/lamp.png';
|
itemImageUrl2 = '../assets/lamp.png';
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
<h3>Pass objects:</h3>
|
<h3>Pass objects:</h3>
|
||||||
<!-- #docregion pass-object -->
|
<!-- #docregion pass-object -->
|
||||||
<app-list-item [items]="currentItem"></app-list-item>
|
<app-item-list [items]="currentItems"></app-item-list>
|
||||||
<!-- #enddocregion pass-object -->
|
<!-- #enddocregion pass-object -->
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -15,7 +15,7 @@ export class AppComponent {
|
|||||||
// #enddocregion parent-data-type
|
// #enddocregion parent-data-type
|
||||||
|
|
||||||
// #docregion pass-object
|
// #docregion pass-object
|
||||||
currentItem = [{
|
currentItems = [{
|
||||||
id: 21,
|
id: 21,
|
||||||
name: 'phone'
|
name: 'phone'
|
||||||
}];
|
}];
|
||||||
|
@ -4,7 +4,7 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { ItemDetailComponent } from './item-detail/item-detail.component';
|
import { ItemDetailComponent } from './item-detail/item-detail.component';
|
||||||
import { ListItemComponent } from './list-item/list-item.component';
|
import { ItemListComponent } from './item-list/item-list.component';
|
||||||
import { StringInitComponent } from './string-init/string-init.component';
|
import { StringInitComponent } from './string-init/string-init.component';
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import { StringInitComponent } from './string-init/string-init.component';
|
|||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
ItemDetailComponent,
|
ItemDetailComponent,
|
||||||
ListItemComponent,
|
ItemListComponent,
|
||||||
StringInitComponent
|
StringInitComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ListItemComponent } from './list-item.component';
|
import { ItemListComponent } from './item-list.component';
|
||||||
|
|
||||||
describe('ItemListComponent', () => {
|
describe('ItemListComponent', () => {
|
||||||
let component: ListItemComponent;
|
let component: ItemListComponent;
|
||||||
let fixture: ComponentFixture<ListItemComponent>;
|
let fixture: ComponentFixture<ItemListComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ ListItemComponent ]
|
declarations: [ ItemListComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ListItemComponent);
|
fixture = TestBed.createComponent(ItemListComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -3,11 +3,11 @@ import { ITEMS } from '../mock-items';
|
|||||||
import { Item } from '../item';
|
import { Item } from '../item';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list-item',
|
selector: 'app-item-list',
|
||||||
templateUrl: './list-item.component.html',
|
templateUrl: './item-list.component.html',
|
||||||
styleUrls: ['./list-item.component.css']
|
styleUrls: ['./item-list.component.css']
|
||||||
})
|
})
|
||||||
export class ListItemComponent {
|
export class ItemListComponent {
|
||||||
listItems = ITEMS;
|
listItems = ITEMS;
|
||||||
// #docregion item-input
|
// #docregion item-input
|
||||||
@Input() items: Item[];
|
@Input() items: Item[];
|
@ -153,7 +153,7 @@ It marks that `<li>` element (and its children) as the "repeater template":
|
|||||||
<div class="alert is-important">
|
<div class="alert is-important">
|
||||||
|
|
||||||
Don't forget the leading asterisk (\*) in `*ngFor`. It is an essential part of the syntax.
|
Don't forget the leading asterisk (\*) in `*ngFor`. It is an essential part of the syntax.
|
||||||
For more information, see the [Template Syntax](guide/template-syntax#ngFor) page.
|
Read more about `ngFor` and `*` in the [ngFor section](guide/template-syntax#ngfor) of the [Template Syntax](guide/template-syntax) page.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
_Angular elements_ are Angular components packaged as _custom elements_ (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.
|
_Angular elements_ are Angular components packaged as _custom elements_ (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.
|
||||||
|
|
||||||
[Custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) are a Web Platform feature currently supported by Chrome, Firefox, Opera, and Safari, and available in other browsers through polyfills (see [Browser Support](#browser-support)).
|
[Custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) are a Web Platform feature currently supported by Chrome, Edge (Chromium-based), Firefox, Opera, and Safari, and available in other browsers through polyfills (see [Browser Support](#browser-support)).
|
||||||
A custom element extends HTML by allowing you to define a tag whose content is created and controlled by JavaScript code.
|
A custom element extends HTML by allowing you to define a tag whose content is created and controlled by JavaScript code.
|
||||||
The browser maintains a `CustomElementRegistry` of defined custom elements, which maps an instantiable JavaScript class to an HTML tag.
|
The browser maintains a `CustomElementRegistry` of defined custom elements, which maps an instantiable JavaScript class to an HTML tag.
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ For more information, see Web Component documentation for [Creating custom event
|
|||||||
|
|
||||||
## Browser support for custom elements
|
## Browser support for custom elements
|
||||||
|
|
||||||
The recently-developed [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) Web Platform feature is currently supported natively in a number of browsers. Support is pending or planned in other browsers.
|
The recently-developed [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) Web Platform feature is currently supported natively in a number of browsers.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -94,11 +94,7 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
|||||||
<td>Supported natively.</td>
|
<td>Supported natively.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Opera</td>
|
<td>Edge (Chromium-based)</td>
|
||||||
<td>Supported natively.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Safari</td>
|
|
||||||
<td>Supported natively.</td>
|
<td>Supported natively.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -106,10 +102,12 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
|||||||
<td>Supported natively.</td>
|
<td>Supported natively.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Edge</td>
|
<td>Opera</td>
|
||||||
<td>Working on an implementation. <br>
|
<td>Supported natively.</td>
|
||||||
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
|
<td>Safari</td>
|
||||||
|
<td>Supported natively.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -730,13 +730,13 @@ As you can see here, the `parentItem` in `AppComponent` is a string, which the `
|
|||||||
The previous simple example showed passing in a string. To pass in an object,
|
The previous simple example showed passing in a string. To pass in an object,
|
||||||
the syntax and thinking are the same.
|
the syntax and thinking are the same.
|
||||||
|
|
||||||
In this scenario, `ListItemComponent` is nested within `AppComponent` and the `items` property expects an array of objects.
|
In this scenario, `ItemListComponent` is nested within `AppComponent` and the `items` property expects an array of objects.
|
||||||
|
|
||||||
<code-example path="property-binding/src/app/app.component.html" region="pass-object" header="src/app/app.component.html"></code-example>
|
<code-example path="property-binding/src/app/app.component.html" region="pass-object" header="src/app/app.component.html"></code-example>
|
||||||
|
|
||||||
The `items` property is declared in the `ListItemComponent` with a type of `Item` and decorated with `@Input()`:
|
The `items` property is declared in the `ItemListComponent` with a type of `Item` and decorated with `@Input()`:
|
||||||
|
|
||||||
<code-example path="property-binding/src/app/list-item/list-item.component.ts" region="item-input" header="src/app/list-item.component.ts"></code-example>
|
<code-example path="property-binding/src/app/item-list/item-list.component.ts" region="item-input" header="src/app/item-list.component.ts"></code-example>
|
||||||
|
|
||||||
In this sample app, an `Item` is an object that has two properties; an `id` and a `name`.
|
In this sample app, an `Item` is an object that has two properties; an `id` and a `name`.
|
||||||
|
|
||||||
@ -747,11 +747,11 @@ specify a different item in `app.component.ts` so that the new item will render:
|
|||||||
|
|
||||||
<code-example path="property-binding/src/app/app.component.ts" region="pass-object" header="src/app.component.ts"></code-example>
|
<code-example path="property-binding/src/app/app.component.ts" region="pass-object" header="src/app.component.ts"></code-example>
|
||||||
|
|
||||||
You just have to make sure, in this case, that you're supplying an array of objects because that's the type of `items` and is what the nested component, `ListItemComponent`, expects.
|
You just have to make sure, in this case, that you're supplying an array of objects because that's the type of `Item` and is what the nested component, `ItemListComponent`, expects.
|
||||||
|
|
||||||
In this example, `AppComponent` specifies a different `item` object
|
In this example, `AppComponent` specifies a different `item` object
|
||||||
(`currentItem`) and passes it to the nested `ListItemComponent`. `ListItemComponent` was able to use `currentItem` because it matches what an `Item` object is according to `item.ts`. The `item.ts` file is where
|
(`currentItems`) and passes it to the nested `ItemListComponent`. `ItemListComponent` was able to use `currentItems` because it matches what an `Item` object is according to `item.ts`. The `item.ts` file is where
|
||||||
`ListItemComponent` gets its definition of an `item`.
|
`ItemListComponent` gets its definition of an `item`.
|
||||||
|
|
||||||
### Remember the brackets
|
### Remember the brackets
|
||||||
|
|
||||||
@ -780,7 +780,7 @@ not a template expression. Angular sets it and forgets about it.
|
|||||||
|
|
||||||
<code-example path="property-binding/src/app/app.component.html" region="string-init" header="src/app/app.component.html"></code-example>
|
<code-example path="property-binding/src/app/app.component.html" region="string-init" header="src/app/app.component.html"></code-example>
|
||||||
|
|
||||||
The `[item]` binding, on the other hand, remains a live binding to the component's `currentItem` property.
|
The `[item]` binding, on the other hand, remains a live binding to the component's `currentItems` property.
|
||||||
|
|
||||||
### Property binding vs. interpolation
|
### Property binding vs. interpolation
|
||||||
|
|
||||||
@ -1600,6 +1600,14 @@ The following example shows `NgFor` applied to a simple `<div>`. (Don't forget t
|
|||||||
|
|
||||||
<code-example path="built-in-directives/src/app/app.component.html" region="NgFor-1" header="src/app/app.component.html"></code-example>
|
<code-example path="built-in-directives/src/app/app.component.html" region="NgFor-1" header="src/app/app.component.html"></code-example>
|
||||||
|
|
||||||
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
|
Don't forget the asterisk (`*`) in front of `ngFor`. For more information
|
||||||
|
on the asterisk, see the [asterisk (*) prefix](guide/structural-directives#the-asterisk--prefix) section of
|
||||||
|
[Structural Directives](guide/structural-directives).
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
You can also apply an `NgFor` to a component element, as in the following example.
|
You can also apply an `NgFor` to a component element, as in the following example.
|
||||||
|
|
||||||
<code-example path="built-in-directives/src/app/app.component.html" region="NgFor-2" header="src/app/app.component.html"></code-example>
|
<code-example path="built-in-directives/src/app/app.component.html" region="NgFor-2" header="src/app/app.component.html"></code-example>
|
||||||
|
@ -1288,7 +1288,7 @@ In this example, we have a new macro task (nested setTimeout), by default, when
|
|||||||
region="fake-async-test-tick-new-macro-task-async">
|
region="fake-async-test-tick-new-macro-task-async">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
And in some case, we don't want to trigger the new maco task when ticking, we can use `tick(milliseconds, {processNewMacroTasksSynchronously: false})` to not invoke new maco task.
|
And in some case, we don't want to trigger the new macro task when ticking, we can use `tick(milliseconds, {processNewMacroTasksSynchronously: false})` to not invoke new maco task.
|
||||||
|
|
||||||
#### Comparing dates inside fakeAsync()
|
#### Comparing dates inside fakeAsync()
|
||||||
|
|
||||||
|
@ -117,12 +117,13 @@ To understand how change detection works, first consider when the application ne
|
|||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
data = 'initial value';
|
data = 'initial value';
|
||||||
|
serverUrl = 'SERVER_URL';
|
||||||
constructor(private httpClient: HttpClient) {}
|
constructor(private httpClient: HttpClient) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.httpClient.get(serverUrl).subscribe(response => {
|
this.httpClient.get(this.serverUrl).subscribe(response => {
|
||||||
// user does not need to trigger change detection manually
|
// user does not need to trigger change detection manually
|
||||||
data = response.data;
|
this.data = response.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,7 +142,7 @@ export class AppComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// user does not need to trigger change detection manually
|
// user does not need to trigger change detection manually
|
||||||
data = 'value updated';
|
this.data = 'value updated';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,7 +161,7 @@ export class AppComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
Promise.resolve(1).then(v => {
|
Promise.resolve(1).then(v => {
|
||||||
// user does not need to trigger change detection manually
|
// user does not need to trigger change detection manually
|
||||||
data = v;
|
this.data = v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,7 +201,7 @@ func.apply(ctx2);
|
|||||||
The value of `this` in the callback of `setTimeout` might differ depending on when `setTimeout` is called.
|
The value of `this` in the callback of `setTimeout` might differ depending on when `setTimeout` is called.
|
||||||
Thus you can lose the context in asynchronous operations.
|
Thus you can lose the context in asynchronous operations.
|
||||||
|
|
||||||
A zone provides a new zone context other than `this`, the zone context persists across asynchronous operations.
|
A zone provides a new zone context other than `this`, the zone context that persists across asynchronous operations.
|
||||||
In the following example, the new zone context is called `zoneThis`.
|
In the following example, the new zone context is called `zoneThis`.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -257,16 +258,14 @@ The Zone Task concept is very similar to the Javascript VM Task concept.
|
|||||||
- `microTask`: such as `Promise.then()`.
|
- `microTask`: such as `Promise.then()`.
|
||||||
- `eventTask`: such as `element.addEventListener()`.
|
- `eventTask`: such as `element.addEventListener()`.
|
||||||
|
|
||||||
The `onInvoke` hook triggers when a synchronize function is executed in a Zone.
|
|
||||||
|
|
||||||
These hooks trigger under the following circumstances:
|
These hooks trigger under the following circumstances:
|
||||||
|
|
||||||
- `onScheduleTask`: triggers when a new asynchronous task is scheduled, such as when you call `setTimeout()`.
|
- `onScheduleTask`: triggers when a new asynchronous task is scheduled, such as when you call `setTimeout()`.
|
||||||
- `onInvokeTask`: triggers when an asynchronous task is about to execute, such as when the callback of `setTimeout()` is about to execute.
|
- `onInvokeTask`: triggers when an asynchronous task is about to execute, such as when the callback of `setTimeout()` is about to execute.
|
||||||
- `onHasTask`: triggers when the status of one kind of task inside a zone changes from stable to unstable or from unstable to stable. A status of stable means there are no tasks inside the Zone, while unstable means a new task is scheduled in the zone.
|
- `onHasTask`: triggers when the status of one kind of task inside a zone changes from stable to unstable or from unstable to stable. A status of stable means there are no tasks inside the Zone, while unstable means a new task is scheduled in the zone.
|
||||||
- `onInvoke`: triggers when a synchronize function is going to execute in the zone.
|
- `onInvoke`: triggers when a synchronous function is going to execute in the zone.
|
||||||
|
|
||||||
With these hooks, `Zone` can monitor the status of all synchronize and asynchronous operations inside a zone.
|
With these hooks, `Zone` can monitor the status of all synchronous and asynchronous operations inside a zone.
|
||||||
|
|
||||||
The above example returns the following output.
|
The above example returns the following output.
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 60 KiB |
@ -23,7 +23,7 @@
|
|||||||
"build-local-with-viewengine": "yarn ~~build",
|
"build-local-with-viewengine": "yarn ~~build",
|
||||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
||||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 6aa3c134c",
|
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 526c3cc37",
|
||||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||||
"test": "yarn check-env && ng test",
|
"test": "yarn check-env && ng test",
|
||||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||||
|
@ -132,7 +132,7 @@ class ExampleZipper {
|
|||||||
return basePath + file;
|
return basePath + file;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (json.files[0].substr(0, 1) === '!') {
|
if (json.files[0][0] === '!') {
|
||||||
json.files = defaultIncludes.concat(json.files);
|
json.files = defaultIncludes.concat(json.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,16 +144,16 @@ class ExampleZipper {
|
|||||||
|
|
||||||
let gpaths = json.files.map((fileName) => {
|
let gpaths = json.files.map((fileName) => {
|
||||||
fileName = fileName.trim();
|
fileName = fileName.trim();
|
||||||
if (fileName.substr(0, 1) === '!') {
|
if (fileName[0] === '!') {
|
||||||
return '!' + path.join(exampleDirName, fileName.substr(1));
|
return '!' + path.join(exampleDirName, fileName.substr(1));
|
||||||
} else {
|
} else {
|
||||||
return path.join(exampleDirName, fileName);
|
return path.join(exampleDirName, fileName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Array.prototype.push.apply(gpaths, alwaysExcludes);
|
gpaths.push(...alwaysExcludes);
|
||||||
|
|
||||||
let fileNames = globby.sync(gpaths, { ignore: ['**/node_modules/**']});
|
let fileNames = globby.sync(gpaths, { ignore: ['**/node_modules/**'] });
|
||||||
|
|
||||||
let zip = this._createZipArchive(outputFileName);
|
let zip = this._createZipArchive(outputFileName);
|
||||||
fileNames.forEach((fileName) => {
|
fileNames.forEach((fileName) => {
|
||||||
@ -165,7 +165,7 @@ class ExampleZipper {
|
|||||||
// zip.append(fs.createReadStream(fileName), { name: relativePath });
|
// zip.append(fs.createReadStream(fileName), { name: relativePath });
|
||||||
let output = regionExtractor()(content, extn).contents;
|
let output = regionExtractor()(content, extn).contents;
|
||||||
|
|
||||||
zip.append(output, { name: relativePath } )
|
zip.append(output, { name: relativePath } );
|
||||||
});
|
});
|
||||||
|
|
||||||
// we need the package.json from _examples root, not the _boilerplate one
|
// we need the package.json from _examples root, not the _boilerplate one
|
||||||
|
@ -1,41 +1,29 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Canonical path provides a consistent path (i.e. always forward slashes) across different OSes
|
// Canonical path provides a consistent path (i.e. always forward slashes) across different OSes
|
||||||
var path = require('canonical-path');
|
const path = require('canonical-path');
|
||||||
var Q = require('q');
|
const fs = require('fs-extra');
|
||||||
var _ = require('lodash');
|
const globby = require('globby');
|
||||||
var jsdom = require("jsdom");
|
const jsdom = require('jsdom');
|
||||||
var fs = require("fs-extra");
|
|
||||||
var globby = require('globby');
|
|
||||||
|
|
||||||
var regionExtractor = require('../transforms/examples-package/services/region-parser');
|
const regionExtractor = require('../transforms/examples-package/services/region-parser');
|
||||||
|
|
||||||
class StackblitzBuilder {
|
class StackblitzBuilder {
|
||||||
constructor(basePath, destPath) {
|
constructor(basePath, destPath) {
|
||||||
this.basePath = basePath;
|
this.basePath = basePath;
|
||||||
this.destPath = destPath;
|
this.destPath = destPath;
|
||||||
|
|
||||||
// Extract npm package dependencies
|
this.copyrights = this._buildCopyrightStrings();
|
||||||
var packageJson = require(path.join(__dirname, '../examples/shared/boilerplate/cli/package.json'));
|
this._boilerplatePackageJsons = {};
|
||||||
this.examplePackageDependencies = packageJson.dependencies;
|
|
||||||
|
|
||||||
// Add unit test packages from devDependency for unit test examples
|
|
||||||
var devDependencies = packageJson.devDependencies;
|
|
||||||
this.examplePackageDependencies['jasmine-core'] = devDependencies['jasmine-core'];
|
|
||||||
this.examplePackageDependencies['jasmine-marbles'] = devDependencies['jasmine-marbles'];
|
|
||||||
|
|
||||||
this.copyrights = {};
|
|
||||||
|
|
||||||
this._buildCopyrightStrings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
this._checkForOutdatedConfig();
|
this._checkForOutdatedConfig();
|
||||||
|
|
||||||
// When testing it sometimes helps to look a just one example directory like so:
|
// When testing it sometimes helps to look a just one example directory like so:
|
||||||
// var stackblitzPaths = path.join(this.basePath, '**/testing/*stackblitz.json');
|
// const stackblitzPaths = path.join(this.basePath, '**/testing/*stackblitz.json');
|
||||||
var stackblitzPaths = path.join(this.basePath, '**/*stackblitz.json');
|
const stackblitzPaths = path.join(this.basePath, '**/*stackblitz.json');
|
||||||
var fileNames = globby.sync(stackblitzPaths, { ignore: ['**/node_modules/**'] });
|
const fileNames = globby.sync(stackblitzPaths, { ignore: ['**/node_modules/**'] });
|
||||||
fileNames.forEach((configFileName) => {
|
fileNames.forEach((configFileName) => {
|
||||||
try {
|
try {
|
||||||
// console.log('***'+configFileName)
|
// console.log('***'+configFileName)
|
||||||
@ -46,17 +34,46 @@ class StackblitzBuilder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_addDependencies(postData) {
|
_addDependencies(config, postData) {
|
||||||
postData['dependencies'] = JSON.stringify(this.examplePackageDependencies);
|
// Extract npm package dependencies
|
||||||
|
const exampleType = this._getExampleType(config.basePath);
|
||||||
|
const packageJson = this._getBoilerplatePackageJson(exampleType) || this._getBoilerplatePackageJson('cli');
|
||||||
|
const exampleDependencies = packageJson.dependencies;
|
||||||
|
|
||||||
|
// Add unit test packages from devDependencies for unit test examples
|
||||||
|
const devDependencies = packageJson.devDependencies;
|
||||||
|
['jasmine-core', 'jasmine-marbles'].forEach(dep => exampleDependencies[dep] = devDependencies[dep]);
|
||||||
|
|
||||||
|
postData.dependencies = JSON.stringify(exampleDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getExampleType(exampleDir) {
|
||||||
|
const configPath = `${exampleDir}/example-config.json`;
|
||||||
|
const configSrc = fs.existsSync(configPath) && fs.readFileSync(configPath, 'utf-8').trim();
|
||||||
|
const config = configSrc ? JSON.parse(configSrc) : {};
|
||||||
|
|
||||||
|
return config.projectType || 'cli';
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBoilerplatePackageJson(exampleType) {
|
||||||
|
if (!this._boilerplatePackageJsons.hasOwnProperty(exampleType)) {
|
||||||
|
const pkgJsonPath = `${__dirname}/../examples/shared/boilerplate/${exampleType}/package.json`;
|
||||||
|
this._boilerplatePackageJsons[exampleType] = fs.existsSync(pkgJsonPath) ? require(pkgJsonPath) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._boilerplatePackageJsons[exampleType];
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildCopyrightStrings() {
|
_buildCopyrightStrings() {
|
||||||
var copyright = 'Copyright Google LLC. All Rights Reserved.\n' +
|
const copyright = 'Copyright Google LLC. All Rights Reserved.\n' +
|
||||||
'Use of this source code is governed by an MIT-style license that\n' +
|
'Use of this source code is governed by an MIT-style license that\n' +
|
||||||
'can be found in the LICENSE file at http://angular.io/license';
|
'can be found in the LICENSE file at http://angular.io/license';
|
||||||
var pad = '\n\n';
|
const pad = '\n\n';
|
||||||
this.copyrights.jsCss = `${pad}/*\n${copyright}\n*/`;
|
|
||||||
this.copyrights.html = `${pad}<!-- \n${copyright}\n-->`;
|
return {
|
||||||
|
jsCss: `${pad}/*\n${copyright}\n*/`,
|
||||||
|
html: `${pad}<!-- \n${copyright}\n-->`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build stackblitz from JSON configuration file (e.g., stackblitz.json):
|
// Build stackblitz from JSON configuration file (e.g., stackblitz.json):
|
||||||
@ -68,30 +85,29 @@ class StackblitzBuilder {
|
|||||||
// file: string - name of file to display within the stackblitz (e.g. `"file": "app/app.module.ts"`)
|
// file: string - name of file to display within the stackblitz (e.g. `"file": "app/app.module.ts"`)
|
||||||
_buildStackblitzFrom(configFileName) {
|
_buildStackblitzFrom(configFileName) {
|
||||||
// replace ending 'stackblitz.json' with 'stackblitz.no-link.html' to create output file name;
|
// replace ending 'stackblitz.json' with 'stackblitz.no-link.html' to create output file name;
|
||||||
var outputFileName = `stackblitz.no-link.html`;
|
const outputFileName = configFileName.replace(/stackblitz\.json$/, 'stackblitz.no-link.html');
|
||||||
outputFileName = configFileName.replace(/stackblitz\.json$/, outputFileName);
|
let altFileName;
|
||||||
var altFileName;
|
|
||||||
if (this.destPath && this.destPath.length > 0) {
|
if (this.destPath && this.destPath.length > 0) {
|
||||||
var partPath = path.dirname(path.relative(this.basePath, outputFileName));
|
const partPath = path.dirname(path.relative(this.basePath, outputFileName));
|
||||||
var altFileName = path.join(this.destPath, partPath, path.basename(outputFileName)).replace('.no-link.', '.');
|
altFileName = path.join(this.destPath, partPath, path.basename(outputFileName)).replace('.no-link.', '.');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var config = this._initConfigAndCollectFileNames(configFileName);
|
const config = this._initConfigAndCollectFileNames(configFileName);
|
||||||
var postData = this._createPostData(config, configFileName);
|
const postData = this._createPostData(config, configFileName);
|
||||||
this._addDependencies(postData);
|
this._addDependencies(config, postData);
|
||||||
var html = this._createStackblitzHtml(config, postData);
|
const html = this._createStackblitzHtml(config, postData);
|
||||||
fs.writeFileSync(outputFileName, html, 'utf-8');
|
fs.writeFileSync(outputFileName, html, 'utf-8');
|
||||||
if (altFileName) {
|
if (altFileName) {
|
||||||
var altDirName = path.dirname(altFileName);
|
const altDirName = path.dirname(altFileName);
|
||||||
fs.ensureDirSync(altDirName);
|
fs.ensureDirSync(altDirName);
|
||||||
fs.writeFileSync(altFileName, html, 'utf-8');
|
fs.writeFileSync(altFileName, html, 'utf-8');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if we fail delete the outputFile if it exists because it is an old one.
|
// if we fail delete the outputFile if it exists because it is an old one.
|
||||||
if (this._existsSync(outputFileName)) {
|
if (fs.existsSync(outputFileName)) {
|
||||||
fs.unlinkSync(outputFileName);
|
fs.unlinkSync(outputFileName);
|
||||||
}
|
}
|
||||||
if (altFileName && this._existsSync(altFileName)) {
|
if (altFileName && fs.existsSync(altFileName)) {
|
||||||
fs.unlinkSync(altFileName);
|
fs.unlinkSync(altFileName);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
@ -100,8 +116,8 @@ class StackblitzBuilder {
|
|||||||
|
|
||||||
_checkForOutdatedConfig() {
|
_checkForOutdatedConfig() {
|
||||||
// Ensure that nobody is trying to use the old config filenames (i.e. `plnkr.json`).
|
// Ensure that nobody is trying to use the old config filenames (i.e. `plnkr.json`).
|
||||||
var plunkerPaths = path.join(this.basePath, '**/*plnkr.json');
|
const plunkerPaths = path.join(this.basePath, '**/*plnkr.json');
|
||||||
var fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] });
|
const fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] });
|
||||||
|
|
||||||
if (fileNames.length) {
|
if (fileNames.length) {
|
||||||
const readmePath = path.join(__dirname, 'README.md');
|
const readmePath = path.join(__dirname, 'README.md');
|
||||||
@ -118,85 +134,87 @@ class StackblitzBuilder {
|
|||||||
|
|
||||||
_getPrimaryFile(config) {
|
_getPrimaryFile(config) {
|
||||||
if (config.file) {
|
if (config.file) {
|
||||||
if (!this._existsSync(path.join(config.basePath, config.file))) {
|
if (!fs.existsSync(path.join(config.basePath, config.file))) {
|
||||||
throw new Error(`The specified primary file (${config.file}) does not exist in '${config.basePath}'.`);
|
throw new Error(`The specified primary file (${config.file}) does not exist in '${config.basePath}'.`);
|
||||||
}
|
}
|
||||||
return config.file;
|
return config.file;
|
||||||
} else {
|
} else {
|
||||||
const defaultPrimaryFiles = ['src/app/app.component.html', 'src/app/app.component.ts', 'src/app/main.ts'];
|
const defaultPrimaryFiles = ['src/app/app.component.html', 'src/app/app.component.ts', 'src/app/main.ts'];
|
||||||
const primaryFile = defaultPrimaryFiles.find(fileName => this._existsSync(path.join(config.basePath, fileName)));
|
const primaryFile = defaultPrimaryFiles.find(fileName => fs.existsSync(path.join(config.basePath, fileName)));
|
||||||
|
|
||||||
if (!primaryFile) {
|
if (!primaryFile) {
|
||||||
throw new Error(`None of the default primary files (${defaultPrimaryFiles.join(', ')}) exists in '${config.basePath}'.`);
|
throw new Error(`None of the default primary files (${defaultPrimaryFiles.join(', ')}) exists in '${config.basePath}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return primaryFile;
|
return primaryFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_createBaseStackblitzHtml(config) {
|
_createBaseStackblitzHtml(config) {
|
||||||
var file = `?file=${this._getPrimaryFile(config)}`;
|
const file = `?file=${this._getPrimaryFile(config)}`;
|
||||||
var action = `https://run.stackblitz.com/api/angular/v1${file}`;
|
const action = `https://run.stackblitz.com/api/angular/v1${file}`;
|
||||||
var html = `<!DOCTYPE html><html lang="en"><body>
|
|
||||||
<form id="mainForm" method="post" action="${action}" target="_self"></form>
|
|
||||||
<script>
|
|
||||||
var embedded = 'ctl=1';
|
|
||||||
var isEmbedded = window.location.search.indexOf(embedded) > -1;
|
|
||||||
|
|
||||||
if (isEmbedded) {
|
return `
|
||||||
var form = document.getElementById('mainForm');
|
<!DOCTYPE html><html lang="en"><body>
|
||||||
var action = form.action;
|
<form id="mainForm" method="post" action="${action}" target="_self"></form>
|
||||||
var actionHasParams = action.indexOf('?') > -1;
|
<script>
|
||||||
var symbol = actionHasParams ? '&' : '?'
|
var embedded = 'ctl=1';
|
||||||
form.action = form.action + symbol + embedded;
|
var isEmbedded = window.location.search.indexOf(embedded) > -1;
|
||||||
}
|
|
||||||
document.getElementById("mainForm").submit();
|
if (isEmbedded) {
|
||||||
</script>
|
var form = document.getElementById('mainForm');
|
||||||
</body></html>`;
|
var action = form.action;
|
||||||
return html;
|
var actionHasParams = action.indexOf('?') > -1;
|
||||||
|
var symbol = actionHasParams ? '&' : '?'
|
||||||
|
form.action = form.action + symbol + embedded;
|
||||||
|
}
|
||||||
|
document.getElementById("mainForm").submit();
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
_createPostData(config, configFileName) {
|
_createPostData(config, configFileName) {
|
||||||
var postData = {};
|
const postData = {};
|
||||||
|
|
||||||
// If `config.main` is specified, ensure that it points to an existing file.
|
// If `config.main` is specified, ensure that it points to an existing file.
|
||||||
if (config.main && !this._existsSync(path.join(config.basePath, config.main))) {
|
if (config.main && !fs.existsSync(path.join(config.basePath, config.main))) {
|
||||||
throw Error(`The main file ('${config.main}') specified in '${configFileName}' does not exist.`);
|
throw Error(`The main file ('${config.main}') specified in '${configFileName}' does not exist.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.fileNames.forEach((fileName) => {
|
config.fileNames.forEach((fileName) => {
|
||||||
var content;
|
let content;
|
||||||
var extn = path.extname(fileName);
|
const extn = path.extname(fileName);
|
||||||
if (extn == '.png') {
|
if (extn === '.png') {
|
||||||
content = this._encodeBase64(fileName);
|
content = this._encodeBase64(fileName);
|
||||||
fileName = fileName.substr(0, fileName.length - 4) + '.base64.png'
|
fileName = `${fileName.slice(0, -extn.length)}.base64${extn}`;
|
||||||
} else {
|
} else {
|
||||||
content = fs.readFileSync(fileName, 'utf-8');
|
content = fs.readFileSync(fileName, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extn == '.js' || extn == '.ts' || extn == '.css') {
|
if (extn === '.js' || extn === '.ts' || extn === '.css') {
|
||||||
content = content + this.copyrights.jsCss;
|
content = content + this.copyrights.jsCss;
|
||||||
} else if (extn == '.html') {
|
} else if (extn === '.html') {
|
||||||
content = content + this.copyrights.html;
|
content = content + this.copyrights.html;
|
||||||
}
|
}
|
||||||
// var escapedValue = escapeHtml(content);
|
// const escapedValue = escapeHtml(content);
|
||||||
|
|
||||||
var relativeFileName = path.relative(config.basePath, fileName);
|
let relativeFileName = path.relative(config.basePath, fileName);
|
||||||
|
|
||||||
// Is the main a custom index-xxx.html file? Rename it
|
// Is the main a custom index-xxx.html file? Rename it
|
||||||
if (relativeFileName == config.main) {
|
if (relativeFileName === config.main) {
|
||||||
relativeFileName = 'src/index.html';
|
relativeFileName = 'src/index.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
// A custom main.ts file? Rename it
|
// A custom main.ts file? Rename it
|
||||||
if (/src\/main[-.]\w+\.ts$/.test(relativeFileName)) {
|
if (/src\/main[-.]\w+\.ts$/.test(relativeFileName)) {
|
||||||
relativeFileName = 'src/main.ts'
|
relativeFileName = 'src/main.ts';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relativeFileName == 'index.html') {
|
if (relativeFileName === 'index.html') {
|
||||||
if (config.description == null) {
|
if (config.description == null) {
|
||||||
// set config.description to title from index.html
|
// set config.description to title from index.html
|
||||||
var matches = /<title>(.*)<\/title>/.exec(content);
|
const matches = /<title>(.*)<\/title>/.exec(content);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
config.description = matches[1];
|
config.description = matches[1];
|
||||||
}
|
}
|
||||||
@ -208,28 +226,26 @@ class StackblitzBuilder {
|
|||||||
postData[`files[${relativeFileName}]`] = content;
|
postData[`files[${relativeFileName}]`] = content;
|
||||||
});
|
});
|
||||||
|
|
||||||
var tags = ['angular', 'example'].concat(config.tags || []);
|
const tags = ['angular', 'example', ...config.tags || []];
|
||||||
tags.forEach(function(tag,ix) {
|
tags.forEach((tag, ix) => postData[`tags[${ix}]`] = tag);
|
||||||
postData['tags[' + ix + ']'] = tag;
|
|
||||||
});
|
|
||||||
|
|
||||||
postData.description = "Angular Example - " + config.description;
|
postData.description = `Angular Example - ${config.description}`;
|
||||||
|
|
||||||
return postData;
|
return postData;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createStackblitzHtml(config, postData) {
|
_createStackblitzHtml(config, postData) {
|
||||||
var baseHtml = this._createBaseStackblitzHtml(config);
|
const baseHtml = this._createBaseStackblitzHtml(config);
|
||||||
var doc = jsdom.jsdom(baseHtml);
|
const doc = jsdom.jsdom(baseHtml);
|
||||||
var form = doc.querySelector('form');
|
const form = doc.querySelector('form');
|
||||||
_.forEach(postData, (value, key) => {
|
|
||||||
var ele = this._htmlToElement(doc, '<input type="hidden" name="' + key + '">');
|
|
||||||
ele.setAttribute('value', value);
|
|
||||||
form.appendChild(ele)
|
|
||||||
});
|
|
||||||
var html = doc.documentElement.outerHTML;
|
|
||||||
|
|
||||||
return html;
|
for(const [key, value] of Object.entries(postData)) {
|
||||||
|
const ele = this._htmlToElement(doc, `<input type="hidden" name="${key}">`);
|
||||||
|
ele.setAttribute('value', value);
|
||||||
|
form.appendChild(ele);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.documentElement.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
_encodeBase64(file) {
|
_encodeBase64(file) {
|
||||||
@ -237,36 +253,20 @@ class StackblitzBuilder {
|
|||||||
return fs.readFileSync(file, { encoding: 'base64' });
|
return fs.readFileSync(file, { encoding: 'base64' });
|
||||||
}
|
}
|
||||||
|
|
||||||
_existsSync(filename) {
|
|
||||||
try {
|
|
||||||
fs.accessSync(filename);
|
|
||||||
return true;
|
|
||||||
} catch(ex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_htmlToElement(document, html) {
|
_htmlToElement(document, html) {
|
||||||
var div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
return div.firstChild;
|
return div.firstChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initConfigAndCollectFileNames(configFileName) {
|
_initConfigAndCollectFileNames(configFileName) {
|
||||||
var configDir = path.dirname(configFileName);
|
const config = this._parseConfig(configFileName);
|
||||||
var configSrc = fs.readFileSync(configFileName, 'utf-8');
|
|
||||||
try {
|
|
||||||
var config = (configSrc && configSrc.trim().length) ? JSON.parse(configSrc) : {};
|
|
||||||
config.basePath = configDir; // assumes 'stackblitz.json' is at `/src` level.
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Stackblitz config - unable to parse json file: ${configFileName}\n${e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultIncludes = ['**/*.ts', '**/*.js', '**/*.css', '**/*.html', '**/*.md', '**/*.json', '**/*.png', '**/*.svg'];
|
const defaultIncludes = ['**/*.ts', '**/*.js', '**/*.css', '**/*.html', '**/*.md', '**/*.json', '**/*.png', '**/*.svg'];
|
||||||
var boilerplateIncludes = ['src/environments/*.*', 'angular.json', 'src/polyfills.ts'];
|
const boilerplateIncludes = ['src/environments/*.*', 'angular.json', 'src/polyfills.ts'];
|
||||||
if (config.files) {
|
if (config.files) {
|
||||||
if (config.files.length > 0) {
|
if (config.files.length > 0) {
|
||||||
if (config.files[0].substr(0, 1) == '!') {
|
if (config.files[0][0] === '!') {
|
||||||
config.files = defaultIncludes.concat(config.files);
|
config.files = defaultIncludes.concat(config.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,10 +275,10 @@ class StackblitzBuilder {
|
|||||||
}
|
}
|
||||||
config.files = config.files.concat(boilerplateIncludes);
|
config.files = config.files.concat(boilerplateIncludes);
|
||||||
|
|
||||||
var includeSpec = false;
|
let includeSpec = false;
|
||||||
var gpaths = config.files.map(function(fileName) {
|
const gpaths = config.files.map((fileName) => {
|
||||||
fileName = fileName.trim();
|
fileName = fileName.trim();
|
||||||
if (fileName.substr(0,1) == '!') {
|
if (fileName[0] === '!') {
|
||||||
return '!' + path.join(config.basePath, fileName.substr(1));
|
return '!' + path.join(config.basePath, fileName.substr(1));
|
||||||
} else {
|
} else {
|
||||||
includeSpec = includeSpec || /\.spec\.(ts|js)$/.test(fileName);
|
includeSpec = includeSpec || /\.spec\.(ts|js)$/.test(fileName);
|
||||||
@ -286,7 +286,7 @@ class StackblitzBuilder {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var defaultExcludes = [
|
const defaultExcludes = [
|
||||||
'!**/e2e/**/*.*',
|
'!**/e2e/**/*.*',
|
||||||
'!**/tsconfig.json',
|
'!**/tsconfig.json',
|
||||||
'!**/package.json',
|
'!**/package.json',
|
||||||
@ -308,10 +308,21 @@ class StackblitzBuilder {
|
|||||||
|
|
||||||
gpaths.push(...defaultExcludes);
|
gpaths.push(...defaultExcludes);
|
||||||
|
|
||||||
config.fileNames = globby.sync(gpaths, { ignore: ["**/node_modules/**"] });
|
config.fileNames = globby.sync(gpaths, { ignore: ['**/node_modules/**'] });
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_parseConfig(configFileName) {
|
||||||
|
try {
|
||||||
|
const configSrc = fs.readFileSync(configFileName, 'utf-8');
|
||||||
|
const config = (configSrc && configSrc.trim().length) ? JSON.parse(configSrc) : {};
|
||||||
|
config.basePath = path.dirname(configFileName); // assumes 'stackblitz.json' is at `/src` level.
|
||||||
|
return config;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Stackblitz config - unable to parse json file: ${configFileName}\n${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StackblitzBuilder;
|
module.exports = StackblitzBuilder;
|
||||||
|
@ -8,7 +8,9 @@ ts_library(
|
|||||||
],
|
],
|
||||||
module_name = "@angular/dev-infra-private",
|
module_name = "@angular/dev-infra-private",
|
||||||
deps = [
|
deps = [
|
||||||
|
"//dev-infra/commit-message",
|
||||||
"//dev-infra/pullapprove",
|
"//dev-infra/pullapprove",
|
||||||
|
"//dev-infra/utils:config",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -33,6 +35,7 @@ pkg_npm(
|
|||||||
deps = [
|
deps = [
|
||||||
":cli",
|
":cli",
|
||||||
":package-json",
|
":package-json",
|
||||||
|
"//dev-infra/commit-message",
|
||||||
"//dev-infra/ts-circular-dependencies",
|
"//dev-infra/ts-circular-dependencies",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,11 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
import {readFileSync} from 'fs';
|
||||||
|
import {join} from 'path';
|
||||||
import {verify} from './pullapprove/verify';
|
import {verify} from './pullapprove/verify';
|
||||||
|
import {validateCommitMessage} from './commit-message/validate';
|
||||||
|
import {getRepoBaseDir} from './utils/config';
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
@ -16,6 +20,12 @@ switch (args[0]) {
|
|||||||
case 'pullapprove:verify':
|
case 'pullapprove:verify':
|
||||||
verify();
|
verify();
|
||||||
break;
|
break;
|
||||||
|
case 'commit-message:pre-commit-validate':
|
||||||
|
const commitMessage = readFileSync(join(getRepoBaseDir(), '.git/COMMIT_EDITMSG'), 'utf8');
|
||||||
|
if (validateCommitMessage(commitMessage)) {
|
||||||
|
console.info('√ Valid commit message');
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.info('No commands were matched');
|
console.info('No commands were matched');
|
||||||
}
|
}
|
||||||
|
39
dev-infra/commit-message/BUILD.bazel
Normal file
39
dev-infra/commit-message/BUILD.bazel
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
load("//tools:defaults.bzl", "jasmine_node_test")
|
||||||
|
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "commit-message",
|
||||||
|
srcs = [
|
||||||
|
"config.ts",
|
||||||
|
"validate.ts",
|
||||||
|
],
|
||||||
|
module_name = "@angular/dev-infra-private/commit-message",
|
||||||
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//dev-infra/utils:config",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//tslib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "validate-test",
|
||||||
|
testonly = True,
|
||||||
|
srcs = ["validate.spec.ts"],
|
||||||
|
deps = [
|
||||||
|
":commit-message",
|
||||||
|
"//dev-infra/utils:config",
|
||||||
|
"@npm//@types/events",
|
||||||
|
"@npm//@types/jasmine",
|
||||||
|
"@npm//@types/node",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
jasmine_node_test(
|
||||||
|
name = "test",
|
||||||
|
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||||
|
deps = [
|
||||||
|
":commit-message",
|
||||||
|
":validate-test",
|
||||||
|
],
|
||||||
|
)
|
13
dev-infra/commit-message/config.ts
Normal file
13
dev-infra/commit-message/config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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
|
||||||
|
*/
|
||||||
|
export interface CommitMessageConfig {
|
||||||
|
maxLineLength: number;
|
||||||
|
minBodyLength: number;
|
||||||
|
types: string[];
|
||||||
|
scopes: string[];
|
||||||
|
}
|
248
dev-infra/commit-message/validate.spec.ts
Normal file
248
dev-infra/commit-message/validate.spec.ts
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Imports
|
||||||
|
import * as utilConfig from '../utils/config';
|
||||||
|
|
||||||
|
import {validateCommitMessage} from './validate';
|
||||||
|
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const config = {
|
||||||
|
'commitMessage': {
|
||||||
|
'maxLineLength': 120,
|
||||||
|
'minBodyLength': 0,
|
||||||
|
'types': [
|
||||||
|
'feat',
|
||||||
|
'fix',
|
||||||
|
'refactor',
|
||||||
|
'release',
|
||||||
|
'style',
|
||||||
|
],
|
||||||
|
'scopes': [
|
||||||
|
'common',
|
||||||
|
'compiler',
|
||||||
|
'core',
|
||||||
|
'packaging',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const TYPES = config.commitMessage.types.join(', ');
|
||||||
|
const SCOPES = config.commitMessage.scopes.join(', ');
|
||||||
|
const INVALID = false;
|
||||||
|
const VALID = true;
|
||||||
|
|
||||||
|
// TODO(josephperrott): Clean up tests to test script rather than for
|
||||||
|
// specific commit messages we want to use.
|
||||||
|
describe('validate-commit-message.js', () => {
|
||||||
|
let lastError: string = '';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
lastError = '';
|
||||||
|
|
||||||
|
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
|
||||||
|
spyOn(utilConfig, 'getAngularDevConfig').and.returnValue(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateMessage()', () => {
|
||||||
|
|
||||||
|
it('should be valid', () => {
|
||||||
|
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('release(packaging): something')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('fixup! release(packaging): something')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('squash! release(packaging): something')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('Revert: "release(packaging): something"')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate max length', () => {
|
||||||
|
const msg =
|
||||||
|
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(
|
||||||
|
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate "<type>(<scope>): <subject>" format', () => {
|
||||||
|
const msg = 'not correct format';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(`The commit message header does not match the expected format.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when type is invalid', () => {
|
||||||
|
const msg = 'weird(core): something';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when scope is invalid', () => {
|
||||||
|
const errorMessageFor = (scope: string, header: string) =>
|
||||||
|
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
|
||||||
|
|
||||||
|
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
|
||||||
|
|
||||||
|
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('refactor(security): something')).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
|
||||||
|
|
||||||
|
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'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow empty scope', () => {
|
||||||
|
expect(validateCommitMessage('fix: blablabla')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
|
||||||
|
// work still to be done (i.e. fixing the commit message).
|
||||||
|
it('should not allow "WIP: ..." syntax', () => {
|
||||||
|
const msg = 'WIP: fix: something';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('(revert)', () => {
|
||||||
|
|
||||||
|
it('should allow valid "revert: ..." syntaxes', () => {
|
||||||
|
expect(validateCommitMessage('revert: anything')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('revert anything')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
expect(validateCommitMessage('rEvErT anything')).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow "revert(scope): ..." syntax', () => {
|
||||||
|
const msg = 'revert(compiler): reduce generated code payload size by 65%';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/angular/angular/issues/23479
|
||||||
|
it('should allow typical Angular messages generated by git', () => {
|
||||||
|
const msg =
|
||||||
|
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg)).toBe(VALID);
|
||||||
|
expect(lastError).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('(squash)', () => {
|
||||||
|
|
||||||
|
it('should strip the `squash! ` prefix and validate the rest', () => {
|
||||||
|
const errorMessage = `The commit message header does not match the expected format.`;
|
||||||
|
|
||||||
|
// Valid messages.
|
||||||
|
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
|
||||||
|
expect(validateCommitMessage('squash! fix: a bug', false)).toBe(VALID);
|
||||||
|
|
||||||
|
// Invalid messages.
|
||||||
|
expect(validateCommitMessage('squash! fix a typo', false)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain('squash! fix a typo');
|
||||||
|
expect(lastError).toContain(errorMessage);
|
||||||
|
|
||||||
|
expect(validateCommitMessage('squash! squash! fix: a bug')).toBe(INVALID);
|
||||||
|
expect(lastError).toContain('squash! squash! fix: a bug');
|
||||||
|
expect(lastError).toContain(errorMessage);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with `disallowSquash`', () => {
|
||||||
|
|
||||||
|
it('should fail', () => {
|
||||||
|
expect(validateCommitMessage('fix(core): something', true)).toBe(VALID);
|
||||||
|
expect(validateCommitMessage('squash! fix(core): something', true)).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(
|
||||||
|
'The commit must be manually squashed into the target commit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('(fixup)', () => {
|
||||||
|
|
||||||
|
describe('without `nonFixupCommitHeaders`', () => {
|
||||||
|
|
||||||
|
it('should strip the `fixup! ` prefix and validate the rest', () => {
|
||||||
|
const errorMessage = `The commit message header does not match the expected format.`;
|
||||||
|
|
||||||
|
// Valid messages.
|
||||||
|
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
|
||||||
|
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
|
||||||
|
|
||||||
|
// Invalid messages.
|
||||||
|
expect(validateCommitMessage('fixup! fix a typo')).toBe(INVALID);
|
||||||
|
expect(lastError).toContain('fixup! fix a typo');
|
||||||
|
expect(lastError).toContain(errorMessage);
|
||||||
|
|
||||||
|
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(INVALID);
|
||||||
|
expect(lastError).toContain('fixup! fixup! fix: a bug');
|
||||||
|
expect(lastError).toContain(errorMessage);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with `nonFixupCommitHeaders`', () => {
|
||||||
|
|
||||||
|
it('should check that the fixup commit matches a non-fixup one', () => {
|
||||||
|
const msg = 'fixup! foo';
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg, false, ['foo', 'bar', 'baz'])).toBe(VALID);
|
||||||
|
expect(validateCommitMessage(msg, false, ['bar', 'baz', 'foo'])).toBe(VALID);
|
||||||
|
expect(validateCommitMessage(msg, false, ['baz', 'foo', 'bar'])).toBe(VALID);
|
||||||
|
|
||||||
|
expect(validateCommitMessage(msg, false, ['qux', 'quux', 'quuux'])).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(
|
||||||
|
'Unable to find match for fixup commit among prior commits: \n' +
|
||||||
|
' qux\n' +
|
||||||
|
' quux\n' +
|
||||||
|
' quuux');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if `nonFixupCommitHeaders` is empty', () => {
|
||||||
|
expect(validateCommitMessage('refactor(core): make reactive', false, [])).toBe(VALID);
|
||||||
|
expect(validateCommitMessage('fixup! foo', false, [])).toBe(INVALID);
|
||||||
|
expect(lastError).toContain(
|
||||||
|
`Unable to find match for fixup commit among prior commits: -`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
134
dev-infra/commit-message/validate.ts
Normal file
134
dev-infra/commit-message/validate.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 {getAngularDevConfig} from '../utils/config';
|
||||||
|
import {CommitMessageConfig} from './config';
|
||||||
|
|
||||||
|
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(.*)/i;
|
||||||
|
|
||||||
|
/** 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, disallowSquash: boolean = false, nonFixupCommitHeaders?: string[]) {
|
||||||
|
function error(errorMessage: string) {
|
||||||
|
console.error(
|
||||||
|
`INVALID COMMIT MSG: \n` +
|
||||||
|
`${'─'.repeat(40)}\n` +
|
||||||
|
`${commitMsg}\n` +
|
||||||
|
`${'─'.repeat(40)}\n` +
|
||||||
|
`ERROR: \n` +
|
||||||
|
` ${errorMessage}` +
|
||||||
|
`\n\n` +
|
||||||
|
`The expected format for a commit is: \n` +
|
||||||
|
`<type>(<scope>): <subject>\n\n<body>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getAngularDevConfig<'commitMessage', CommitMessageConfig>().commitMessage;
|
||||||
|
const commit = parseCommitMessage(commitMsg);
|
||||||
|
|
||||||
|
if (commit.isRevert) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commit.isSquash && disallowSquash) {
|
||||||
|
error('The commit must be manually squashed into the target commit');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a fixup commit and `nonFixupCommitHeaders` is not empty, we only care to check whether
|
||||||
|
// there is a corresponding non-fixup commit (i.e. a commit whose header is identical to this
|
||||||
|
// commit's header after stripping the `fixup! ` prefix).
|
||||||
|
if (commit.isFixup && nonFixupCommitHeaders) {
|
||||||
|
if (!nonFixupCommitHeaders.includes(commit.header)) {
|
||||||
|
error(
|
||||||
|
'Unable to find match for fixup commit among prior commits: ' +
|
||||||
|
(nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commit.header.length > config.maxLineLength) {
|
||||||
|
error(`The commit message header is longer than ${config.maxLineLength} characters`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commit.type) {
|
||||||
|
error(`The commit message header does not match the expected format.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.types.includes(commit.type)) {
|
||||||
|
error(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commit.scope && !config.scopes.includes(commit.scope)) {
|
||||||
|
error(`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
|
||||||
|
error(
|
||||||
|
`The commit message body does not meet the minimum length of ${config.minBodyLength} characters`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyByLine = commit.body.split('\n');
|
||||||
|
if (bodyByLine.some(line => line.length > config.maxLineLength)) {
|
||||||
|
error(
|
||||||
|
`The commit messsage body contains lines greater than ${config.maxLineLength} characters`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
@ -6,6 +6,7 @@ ts_library(
|
|||||||
module_name = "@angular/dev-infra-private/ts-circular-dependencies",
|
module_name = "@angular/dev-infra-private/ts-circular-dependencies",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//dev-infra/utils:config",
|
||||||
"@npm//@types/glob",
|
"@npm//@types/glob",
|
||||||
"@npm//@types/node",
|
"@npm//@types/node",
|
||||||
"@npm//@types/yargs",
|
"@npm//@types/yargs",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright Google Inc. All Rights Reserved.
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
@ -17,7 +18,9 @@ import {Analyzer, ReferenceChain} from './analyzer';
|
|||||||
import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden';
|
import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden';
|
||||||
import {convertPathToForwardSlash} from './file_system';
|
import {convertPathToForwardSlash} from './file_system';
|
||||||
|
|
||||||
const projectDir = join(__dirname, '../../');
|
import {getRepoBaseDir} from '../utils/config';
|
||||||
|
|
||||||
|
const projectDir = getRepoBaseDir();
|
||||||
const packagesDir = join(projectDir, 'packages/');
|
const packagesDir = join(projectDir, 'packages/');
|
||||||
// The default glob does not capture deprecated packages such as http, or the webworker platform.
|
// The default glob does not capture deprecated packages such as http, or the webworker platform.
|
||||||
const defaultGlob =
|
const defaultGlob =
|
||||||
@ -26,10 +29,9 @@ const defaultGlob =
|
|||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const {_: command, goldenFile, glob, baseDir, warnings} =
|
const {_: command, goldenFile, glob, baseDir, warnings} =
|
||||||
yargs.help()
|
yargs.help()
|
||||||
.version(false)
|
|
||||||
.strict()
|
.strict()
|
||||||
.command('check <golden-file>', 'Checks if the circular dependencies have changed.')
|
.command('check <goldenFile>', 'Checks if the circular dependencies have changed.')
|
||||||
.command('approve <golden-file>', 'Approves the current circular dependencies.')
|
.command('approve <goldenFile>', 'Approves the current circular dependencies.')
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.option(
|
.option(
|
||||||
'approve',
|
'approve',
|
||||||
|
@ -5,6 +5,7 @@ ts_library(
|
|||||||
srcs = [
|
srcs = [
|
||||||
"config.ts",
|
"config.ts",
|
||||||
],
|
],
|
||||||
|
module_name = "@angular/dev-infra-private/utils",
|
||||||
visibility = ["//dev-infra:__subpackages__"],
|
visibility = ["//dev-infra:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"@npm//@types/json5",
|
"@npm//@types/json5",
|
||||||
|
@ -5,16 +5,15 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {parse} from 'json5';
|
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
|
import {parse} from 'json5';
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
import {exec} from 'shelljs';
|
import {exec} from 'shelljs';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the path of the directory for the repository base.
|
* Gets the path of the directory for the repository base.
|
||||||
*/
|
*/
|
||||||
function getRepoBaseDir() {
|
export function getRepoBaseDir() {
|
||||||
const baseRepoDir = exec(`git rev-parse --show-toplevel`, {silent: true});
|
const baseRepoDir = exec(`git rev-parse --show-toplevel`, {silent: true});
|
||||||
if (baseRepoDir.code) {
|
if (baseRepoDir.code) {
|
||||||
throw Error(
|
throw Error(
|
||||||
@ -28,7 +27,7 @@ function getRepoBaseDir() {
|
|||||||
/**
|
/**
|
||||||
* Retrieve the configuration from the .dev-infra.json file.
|
* Retrieve the configuration from the .dev-infra.json file.
|
||||||
*/
|
*/
|
||||||
export function getAngularDevConfig(): DevInfraConfig {
|
export function getAngularDevConfig<K, T>(): DevInfraConfig<K, T> {
|
||||||
const configPath = join(getRepoBaseDir(), '.dev-infra.json');
|
const configPath = join(getRepoBaseDir(), '.dev-infra.json');
|
||||||
let rawConfig = '';
|
let rawConfig = '';
|
||||||
try {
|
try {
|
||||||
@ -41,5 +40,8 @@ export function getAngularDevConfig(): DevInfraConfig {
|
|||||||
return parse(rawConfig);
|
return parse(rawConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface exressing the expected structure of the DevInfraConfig.
|
/**
|
||||||
export interface DevInfraConfig {}
|
* Interface exressing the expected structure of the DevInfraConfig.
|
||||||
|
* Allows for providing a typing for a part of the config to read.
|
||||||
|
*/
|
||||||
|
export interface DevInfraConfig<K, T> { [K: string]: T; }
|
||||||
|
7
goldens/public-api/common/common.d.ts
vendored
7
goldens/public-api/common/common.d.ts
vendored
@ -139,10 +139,17 @@ export declare class KeyValuePipe implements PipeTransform {
|
|||||||
transform<V>(input: {
|
transform<V>(input: {
|
||||||
[key: string]: V;
|
[key: string]: V;
|
||||||
} | Map<string, V>, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number): Array<KeyValue<string, V>>;
|
} | Map<string, V>, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number): Array<KeyValue<string, V>>;
|
||||||
|
transform<V>(input: {
|
||||||
|
[key: string]: V;
|
||||||
|
} | Map<string, V> | null, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number): Array<KeyValue<string, V>> | null;
|
||||||
transform<V>(input: {
|
transform<V>(input: {
|
||||||
[key: number]: V;
|
[key: number]: V;
|
||||||
} | Map<number, V>, compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number): Array<KeyValue<number, V>>;
|
} | Map<number, V>, compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number): Array<KeyValue<number, V>>;
|
||||||
|
transform<V>(input: {
|
||||||
|
[key: number]: V;
|
||||||
|
} | Map<number, V> | null, compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number): Array<KeyValue<number, V>> | null;
|
||||||
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>;
|
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>;
|
||||||
|
transform<K, V>(input: Map<K, V> | null, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class Location {
|
export declare class Location {
|
||||||
|
4
goldens/public-api/core/core.d.ts
vendored
4
goldens/public-api/core/core.d.ts
vendored
@ -715,7 +715,7 @@ export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportA
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}, OutputMap extends {
|
}, OutputMap extends {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}, QueryFields extends string[]> = ɵComponentDef<T>;
|
}, QueryFields extends string[], NgContentSelectors extends string[]> = ɵComponentDef<T>;
|
||||||
|
|
||||||
export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener;
|
export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener;
|
||||||
|
|
||||||
@ -834,7 +834,7 @@ export declare function ɵɵembeddedViewStart(viewBlockId: number, decls: number
|
|||||||
|
|
||||||
export declare function ɵɵenableBindings(): void;
|
export declare function ɵɵenableBindings(): void;
|
||||||
|
|
||||||
export declare type ɵɵFactoryDef<T> = () => T;
|
export declare type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;
|
||||||
|
|
||||||
export declare function ɵɵgetCurrentView(): OpaqueViewState;
|
export declare function ɵɵgetCurrentView(): OpaqueViewState;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "angular-srcs",
|
"name": "angular-srcs",
|
||||||
"version": "9.1.0-rc.0",
|
"version": "9.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Angular - a web framework for modern web apps",
|
"description": "Angular - a web framework for modern web apps",
|
||||||
"homepage": "https://github.com/angular/angular",
|
"homepage": "https://github.com/angular/angular",
|
||||||
@ -147,6 +147,7 @@
|
|||||||
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
|
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/cli": "9.0.3",
|
"@angular/cli": "9.0.3",
|
||||||
|
"@angular/dev-infra-private": "angular/dev-infra-private-builds#3724a71",
|
||||||
"@bazel/bazelisk": "^1.3.0",
|
"@bazel/bazelisk": "^1.3.0",
|
||||||
"@bazel/buildifier": "^0.29.0",
|
"@bazel/buildifier": "^0.29.0",
|
||||||
"@bazel/ibazel": "^0.12.3",
|
"@bazel/ibazel": "^0.12.3",
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
*.xpi
|
|
||||||
addon-sdk*
|
|
@ -1,38 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare var exportFunction: any;
|
|
||||||
declare var unsafeWindow: any;
|
|
||||||
|
|
||||||
exportFunction(function() {
|
|
||||||
const curTime = unsafeWindow.performance.now();
|
|
||||||
(<any>self).port.emit('startProfiler', curTime);
|
|
||||||
}, unsafeWindow, {defineAs: 'startProfiler'});
|
|
||||||
|
|
||||||
exportFunction(function() {
|
|
||||||
(<any>self).port.emit('stopProfiler');
|
|
||||||
}, unsafeWindow, {defineAs: 'stopProfiler'});
|
|
||||||
|
|
||||||
exportFunction(function(cb: Function) {
|
|
||||||
(<any>self).port.once('perfProfile', cb);
|
|
||||||
(<any>self).port.emit('getProfile');
|
|
||||||
}, unsafeWindow, {defineAs: 'getProfile'});
|
|
||||||
|
|
||||||
exportFunction(function() {
|
|
||||||
(<any>self).port.emit('forceGC');
|
|
||||||
}, unsafeWindow, {defineAs: 'forceGC'});
|
|
||||||
|
|
||||||
exportFunction(function(name: string) {
|
|
||||||
const curTime = unsafeWindow.performance.now();
|
|
||||||
(<any>self).port.emit('markStart', name, curTime);
|
|
||||||
}, unsafeWindow, {defineAs: 'markStart'});
|
|
||||||
|
|
||||||
exportFunction(function(name: string) {
|
|
||||||
const curTime = unsafeWindow.performance.now();
|
|
||||||
(<any>self).port.emit('markEnd', name, curTime);
|
|
||||||
}, unsafeWindow, {defineAs: 'markEnd'});
|
|
@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
const {Cc, Ci, Cu} = require('chrome');
|
|
||||||
const os = Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
|
|
||||||
const ParserUtil = require('./parser_util');
|
|
||||||
|
|
||||||
class Profiler {
|
|
||||||
private _profiler: any;
|
|
||||||
// TODO(issue/24571): remove '!'.
|
|
||||||
private _markerEvents !: any[];
|
|
||||||
// TODO(issue/24571): remove '!'.
|
|
||||||
private _profilerStartTime !: number;
|
|
||||||
|
|
||||||
constructor() { this._profiler = Cc['@mozilla.org/tools/profiler;1'].getService(Ci.nsIProfiler); }
|
|
||||||
|
|
||||||
start(entries: any, interval: any, features: any, timeStarted: any) {
|
|
||||||
this._profiler.StartProfiler(entries, interval, features, features.length);
|
|
||||||
this._profilerStartTime = timeStarted;
|
|
||||||
this._markerEvents = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() { this._profiler.StopProfiler(); }
|
|
||||||
|
|
||||||
getProfilePerfEvents() {
|
|
||||||
const profileData = this._profiler.getProfileData();
|
|
||||||
let perfEvents = ParserUtil.convertPerfProfileToEvents(profileData);
|
|
||||||
perfEvents = this._mergeMarkerEvents(perfEvents);
|
|
||||||
perfEvents.sort(function(event1: any, event2: any) {
|
|
||||||
return event1.ts - event2.ts;
|
|
||||||
}); // Sort by ts
|
|
||||||
return perfEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
private _mergeMarkerEvents(perfEvents: any[]): any[] {
|
|
||||||
this._markerEvents.forEach(function(markerEvent) { perfEvents.push(markerEvent); });
|
|
||||||
return perfEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
addStartEvent(name: string, timeStarted: number) {
|
|
||||||
this._markerEvents.push({ph: 'B', ts: timeStarted - this._profilerStartTime, name: name});
|
|
||||||
}
|
|
||||||
|
|
||||||
addEndEvent(name: string, timeEnded: number) {
|
|
||||||
this._markerEvents.push({ph: 'E', ts: timeEnded - this._profilerStartTime, name: name});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function forceGC() {
|
|
||||||
Cu.forceGC();
|
|
||||||
os.notifyObservers(null, 'child-gc-request', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mod = require('sdk/page-mod');
|
|
||||||
const data = require('sdk/self').data;
|
|
||||||
const profiler = new Profiler();
|
|
||||||
mod.PageMod({
|
|
||||||
include: ['*'],
|
|
||||||
contentScriptFile: data.url('installed_script.js'),
|
|
||||||
onAttach: (worker: any) => {
|
|
||||||
worker.port.on(
|
|
||||||
'startProfiler',
|
|
||||||
(timeStarted: any) => profiler.start(
|
|
||||||
/* = profiler memory */ 3000000, 0.1, ['leaf', 'js', 'stackwalk', 'gc'], timeStarted));
|
|
||||||
worker.port.on('stopProfiler', () => profiler.stop());
|
|
||||||
worker.port.on(
|
|
||||||
'getProfile', () => worker.port.emit('perfProfile', profiler.getProfilePerfEvents()));
|
|
||||||
worker.port.on('forceGC', forceGC);
|
|
||||||
worker.port.on(
|
|
||||||
'markStart', (name: string, timeStarted: any) => profiler.addStartEvent(name, timeStarted));
|
|
||||||
worker.port.on(
|
|
||||||
'markEnd', (name: string, timeEnded: any) => profiler.addEndEvent(name, timeEnded));
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,92 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} perfProfile The perf profile JSON object.
|
|
||||||
* @return {Object[]} An array of recognized events that are captured
|
|
||||||
* within the perf profile.
|
|
||||||
*/
|
|
||||||
export function convertPerfProfileToEvents(perfProfile: any): any[] {
|
|
||||||
const inProgressEvents = new Map(); // map from event name to start time
|
|
||||||
const finishedEvents: {[key: string]: any}[] = []; // Event[] finished events
|
|
||||||
const addFinishedEvent = function(eventName: string, startTime: number, endTime: number) {
|
|
||||||
const categorizedEventName = categorizeEvent(eventName);
|
|
||||||
let args: {[key: string]: any}|undefined = undefined;
|
|
||||||
if (categorizedEventName == 'gc') {
|
|
||||||
// TODO: We cannot measure heap size at the moment
|
|
||||||
args = {usedHeapSize: 0};
|
|
||||||
}
|
|
||||||
if (startTime == endTime) {
|
|
||||||
// Finished instantly
|
|
||||||
finishedEvents.push({ph: 'X', ts: startTime, name: categorizedEventName, args: args});
|
|
||||||
} else {
|
|
||||||
// Has duration
|
|
||||||
finishedEvents.push({ph: 'B', ts: startTime, name: categorizedEventName, args: args});
|
|
||||||
finishedEvents.push({ph: 'E', ts: endTime, name: categorizedEventName, args: args});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const samples = perfProfile.threads[0].samples;
|
|
||||||
// In perf profile, firefox samples all the frames in set time intervals. Here
|
|
||||||
// we go through all the samples and construct the start and end time for each
|
|
||||||
// event.
|
|
||||||
for (let i = 0; i < samples.length; ++i) {
|
|
||||||
const sample = samples[i];
|
|
||||||
const sampleTime = sample.time;
|
|
||||||
|
|
||||||
// Add all the frames into a set so it's easier/faster to find the set
|
|
||||||
// differences
|
|
||||||
const sampleFrames = new Set();
|
|
||||||
sample.frames.forEach(function(frame: {[key: string]: any}) {
|
|
||||||
sampleFrames.add(frame['location']);
|
|
||||||
});
|
|
||||||
|
|
||||||
// If an event is in the inProgressEvents map, but not in the current sample,
|
|
||||||
// then it must have just finished. We add this event to the finishedEvents
|
|
||||||
// array and remove it from the inProgressEvents map.
|
|
||||||
const previousSampleTime = (i == 0 ? /* not used */ -1 : samples[i - 1].time);
|
|
||||||
inProgressEvents.forEach(function(startTime, eventName) {
|
|
||||||
if (!(sampleFrames.has(eventName))) {
|
|
||||||
addFinishedEvent(eventName, startTime, previousSampleTime);
|
|
||||||
inProgressEvents.delete(eventName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If an event is in the current sample, but not in the inProgressEvents map,
|
|
||||||
// then it must have just started. We add this event to the inProgressEvents
|
|
||||||
// map.
|
|
||||||
sampleFrames.forEach(function(eventName) {
|
|
||||||
if (!(inProgressEvents.has(eventName))) {
|
|
||||||
inProgressEvents.set(eventName, sampleTime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If anything is still in progress, we need to included it as a finished event
|
|
||||||
// since recording ended.
|
|
||||||
const lastSampleTime = samples[samples.length - 1].time;
|
|
||||||
inProgressEvents.forEach(function(startTime, eventName) {
|
|
||||||
addFinishedEvent(eventName, startTime, lastSampleTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove all the unknown categories.
|
|
||||||
return finishedEvents.filter(function(event) { return event['name'] != 'unknown'; });
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is most likely not exhaustive.
|
|
||||||
export function categorizeEvent(eventName: string): string {
|
|
||||||
if (eventName.indexOf('PresShell::Paint') > -1) {
|
|
||||||
return 'render';
|
|
||||||
} else if (eventName.indexOf('FirefoxDriver.prototype.executeScript') > -1) {
|
|
||||||
return 'script';
|
|
||||||
} else if (eventName.indexOf('forceGC') > -1) {
|
|
||||||
return 'gc';
|
|
||||||
} else {
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
const q = require('q');
|
|
||||||
const FirefoxProfile = require('firefox-profile');
|
|
||||||
const jpm = require('jpm/lib/xpi');
|
|
||||||
const pathUtil = require('path');
|
|
||||||
|
|
||||||
const PERF_ADDON_PACKAGE_JSON_DIR = '..';
|
|
||||||
|
|
||||||
exports.getAbsolutePath = function(path: string) {
|
|
||||||
const normalizedPath = pathUtil.normalize(path);
|
|
||||||
if (pathUtil.resolve(normalizedPath) == normalizedPath) {
|
|
||||||
// Already absolute path
|
|
||||||
return normalizedPath;
|
|
||||||
} else {
|
|
||||||
return pathUtil.join(__dirname, normalizedPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getFirefoxProfile = function(extensionPath: string) {
|
|
||||||
const deferred = q.defer();
|
|
||||||
|
|
||||||
const firefoxProfile = new FirefoxProfile();
|
|
||||||
firefoxProfile.addExtensions([extensionPath], () => {
|
|
||||||
firefoxProfile.encoded((err: any, encodedProfile: string) => {
|
|
||||||
const multiCapabilities = [{browserName: 'firefox', firefox_profile: encodedProfile}];
|
|
||||||
deferred.resolve(multiCapabilities);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getFirefoxProfileWithExtension = function() {
|
|
||||||
const absPackageJsonDir = pathUtil.join(__dirname, PERF_ADDON_PACKAGE_JSON_DIR);
|
|
||||||
const packageJson = require(pathUtil.join(absPackageJsonDir, 'package.json'));
|
|
||||||
|
|
||||||
const savedCwd = process.cwd();
|
|
||||||
process.chdir(absPackageJsonDir);
|
|
||||||
|
|
||||||
return jpm(packageJson).then((xpiPath: string) => {
|
|
||||||
process.chdir(savedCwd);
|
|
||||||
return exports.getFirefoxProfile(xpiPath);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
{ "version" : "0.0.1", "main" : "lib/main.js", "name" : "ffperf-addon" }
|
|
@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
require('core-js');
|
|
||||||
require('reflect-metadata');
|
|
||||||
const testHelper = require('../../src/firefox_extension/lib/test_helper.js');
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
specs: ['spec.js', 'sample_benchmark.js'],
|
|
||||||
|
|
||||||
framework: 'jasmine2',
|
|
||||||
|
|
||||||
jasmineNodeOpts: {showColors: true, defaultTimeoutInterval: 1200000},
|
|
||||||
|
|
||||||
getMultiCapabilities: function() { return testHelper.getFirefoxProfileWithExtension(); }
|
|
||||||
};
|
|
@ -1,100 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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 {convertPerfProfileToEvents} from '../../src/firefox_extension/lib/parser_util';
|
|
||||||
|
|
||||||
function assertEventsEqual(actualEvents: any[], expectedEvents: any[]) {
|
|
||||||
expect(actualEvents.length == expectedEvents.length);
|
|
||||||
for (let i = 0; i < actualEvents.length; ++i) {
|
|
||||||
const actualEvent = actualEvents[i];
|
|
||||||
const expectedEvent = expectedEvents[i];
|
|
||||||
for (const key in actualEvent) {
|
|
||||||
expect(actualEvent[key]).toEqual(expectedEvent[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
describe('convertPerfProfileToEvents', function() {
|
|
||||||
it('should convert single instantaneous event', function() {
|
|
||||||
const profileData = {
|
|
||||||
threads: [
|
|
||||||
{samples: [{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]}]}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
|
||||||
assertEventsEqual(perfEvents, [{ph: 'X', ts: 1, name: 'script'}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert single non-instantaneous event', function() {
|
|
||||||
const profileData = {
|
|
||||||
threads: [{
|
|
||||||
samples: [
|
|
||||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
|
||||||
{time: 2, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
|
||||||
{time: 100, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
|
||||||
assertEventsEqual(
|
|
||||||
perfEvents, [{ph: 'B', ts: 1, name: 'script'}, {ph: 'E', ts: 100, name: 'script'}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert multiple instantaneous events', function() {
|
|
||||||
const profileData = {
|
|
||||||
threads: [{
|
|
||||||
samples: [
|
|
||||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
|
||||||
{time: 2, frames: [{location: 'PresShell::Paint'}]}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
|
||||||
assertEventsEqual(
|
|
||||||
perfEvents, [{ph: 'X', ts: 1, name: 'script'}, {ph: 'X', ts: 2, name: 'render'}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert multiple mixed events', function() {
|
|
||||||
const profileData = {
|
|
||||||
threads: [{
|
|
||||||
samples: [
|
|
||||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
|
||||||
{time: 2, frames: [{location: 'PresShell::Paint'}]},
|
|
||||||
{time: 5, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
|
||||||
{time: 10, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
|
||||||
assertEventsEqual(perfEvents, [
|
|
||||||
{ph: 'X', ts: 1, name: 'script'}, {ph: 'X', ts: 2, name: 'render'},
|
|
||||||
{ph: 'B', ts: 5, name: 'script'}, {ph: 'E', ts: 10, name: 'script'}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add args to gc events', function() {
|
|
||||||
const profileData = {threads: [{samples: [{time: 1, frames: [{location: 'forceGC'}]}]}]};
|
|
||||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
|
||||||
assertEventsEqual(perfEvents, [{ph: 'X', ts: 1, name: 'gc', args: {usedHeapSize: 0}}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip unknown events', function() {
|
|
||||||
const profileData = {
|
|
||||||
threads: [{
|
|
||||||
samples: [
|
|
||||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
|
||||||
{time: 2, frames: [{location: 'foo'}]}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
|
||||||
assertEventsEqual(perfEvents, [{ph: 'X', ts: 1, name: 'script'}]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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 {$, browser} from 'protractor';
|
|
||||||
|
|
||||||
const benchpress = require('../../index.js');
|
|
||||||
|
|
||||||
// TODO: this test is currnetly failing. it seems that it didn't run on the ci for a while
|
|
||||||
xdescribe('deep tree baseline', function() {
|
|
||||||
const runner = new benchpress.Runner([
|
|
||||||
// use protractor as Webdriver client
|
|
||||||
benchpress.SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS,
|
|
||||||
// use RegressionSlopeValidator to validate samples
|
|
||||||
benchpress.Validator.bind(benchpress.RegressionSlopeValidator),
|
|
||||||
// use 10 samples to calculate slope regression
|
|
||||||
benchpress.bind(benchpress.RegressionSlopeValidator.SAMPLE_SIZE).toValue(20),
|
|
||||||
// use the script metric to calculate slope regression
|
|
||||||
benchpress.bind(benchpress.RegressionSlopeValidator.METRIC).toValue('scriptTime'),
|
|
||||||
benchpress.bind(benchpress.Options.FORCE_GC).toValue(true)
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
it('should be fast!', function(done) {
|
|
||||||
browser.ignoreSynchronization = true;
|
|
||||||
browser.get('http://localhost:8001/playground/src/benchpress/');
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Tell benchpress to click the buttons to destroy and re-create the tree for each sample.
|
|
||||||
* Benchpress will log the collected metrics after each sample is collected, and will stop
|
|
||||||
* sampling as soon as the calculated regression slope for last 20 samples is stable.
|
|
||||||
*/
|
|
||||||
runner
|
|
||||||
.sample({
|
|
||||||
id: 'baseline',
|
|
||||||
execute: function() { $('button').click(); },
|
|
||||||
providers: [benchpress.bind(benchpress.Options.SAMPLE_DESCRIPTION).toValue({depth: 9})]
|
|
||||||
})
|
|
||||||
.then(done, done.fail);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* tslint:disable:no-console */
|
|
||||||
import {browser} from 'protractor';
|
|
||||||
|
|
||||||
const assertEventsContainsName = function(events: any[], eventName: string) {
|
|
||||||
let found = false;
|
|
||||||
for (let i = 0; i < events.length; ++i) {
|
|
||||||
if (events[i].name == eventName) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(found).toBeTruthy();
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: this test is currnetly failing. it seems that it didn't run on the ci for a while
|
|
||||||
xdescribe('firefox extension', function() {
|
|
||||||
const TEST_URL = 'http://localhost:8001/playground/src/hello_world/index.html';
|
|
||||||
|
|
||||||
it('should measure performance', function() {
|
|
||||||
browser.sleep(3000); // wait for extension to load
|
|
||||||
|
|
||||||
browser.driver.get(TEST_URL);
|
|
||||||
|
|
||||||
browser.executeScript('window.startProfiler()').then(function() {
|
|
||||||
console.log('started measuring perf');
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.executeAsyncScript('setTimeout(arguments[0], 1000);');
|
|
||||||
browser.executeScript('window.forceGC()');
|
|
||||||
|
|
||||||
browser.executeAsyncScript('var cb = arguments[0]; window.getProfile(cb);')
|
|
||||||
.then(function(profile: any) {
|
|
||||||
assertEventsContainsName(profile, 'gc');
|
|
||||||
assertEventsContainsName(profile, 'script');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -85,6 +85,7 @@ export class NgForOfContext<T, U extends NgIterable<T> = NgIterable<T>> {
|
|||||||
* more complex then a property access, for example when using the async pipe (`userStreams |
|
* more complex then a property access, for example when using the async pipe (`userStreams |
|
||||||
* async`).
|
* async`).
|
||||||
* - `index: number`: The index of the current item in the iterable.
|
* - `index: number`: The index of the current item in the iterable.
|
||||||
|
* - `count: number`: The length of the iterable.
|
||||||
* - `first: boolean`: True when the item is the first item in the iterable.
|
* - `first: boolean`: True when the item is the first item in the iterable.
|
||||||
* - `last: boolean`: True when the item is the last item in the iterable.
|
* - `last: boolean`: True when the item is the last item in the iterable.
|
||||||
* - `even: boolean`: True when the item has an even index in the iterable.
|
* - `even: boolean`: True when the item has an even index in the iterable.
|
||||||
|
@ -55,12 +55,23 @@ export class KeyValuePipe implements PipeTransform {
|
|||||||
input: {[key: string]: V}|Map<string, V>,
|
input: {[key: string]: V}|Map<string, V>,
|
||||||
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
|
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
|
||||||
Array<KeyValue<string, V>>;
|
Array<KeyValue<string, V>>;
|
||||||
|
transform<V>(
|
||||||
|
input: {[key: string]: V}|Map<string, V>|null,
|
||||||
|
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
|
||||||
|
Array<KeyValue<string, V>>|null;
|
||||||
transform<V>(
|
transform<V>(
|
||||||
input: {[key: number]: V}|Map<number, V>,
|
input: {[key: number]: V}|Map<number, V>,
|
||||||
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
|
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
|
||||||
Array<KeyValue<number, V>>;
|
Array<KeyValue<number, V>>;
|
||||||
|
transform<V>(
|
||||||
|
input: {[key: number]: V}|Map<number, V>|null,
|
||||||
|
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
|
||||||
|
Array<KeyValue<number, V>>|null;
|
||||||
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number):
|
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number):
|
||||||
Array<KeyValue<K, V>>;
|
Array<KeyValue<K, V>>;
|
||||||
|
transform<K, V>(
|
||||||
|
input: Map<K, V>|null,
|
||||||
|
compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>|null;
|
||||||
transform<K, V>(
|
transform<K, V>(
|
||||||
input: null|{[key: string]: V, [key: number]: V}|Map<K, V>,
|
input: null|{[key: string]: V, [key: number]: V}|Map<K, V>,
|
||||||
compareFn: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number = defaultComparator):
|
compareFn: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number = defaultComparator):
|
||||||
|
@ -203,6 +203,17 @@ let thisArg: any;
|
|||||||
detectChangesAndExpectText('0123456789');
|
detectChangesAndExpectText('0123456789');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should display count correctly', async(() => {
|
||||||
|
const template = '<span *ngFor="let item of items; let len=count">{{len}}</span>';
|
||||||
|
fixture = createTestComponent(template);
|
||||||
|
|
||||||
|
getComponent().items = [0, 1, 2];
|
||||||
|
detectChangesAndExpectText('333');
|
||||||
|
|
||||||
|
getComponent().items = [4, 3, 2, 1, 0, -1];
|
||||||
|
detectChangesAndExpectText('666666');
|
||||||
|
}));
|
||||||
|
|
||||||
it('should display first item correctly', async(() => {
|
it('should display first item correctly', async(() => {
|
||||||
const template =
|
const template =
|
||||||
'<span *ngFor="let item of items; let isFirst=first">{{isFirst.toString()}}</span>';
|
'<span *ngFor="let item of items; let isFirst=first">{{isFirst.toString()}}</span>';
|
||||||
|
@ -63,6 +63,16 @@ describe('KeyValuePipe', () => {
|
|||||||
const transform2 = pipe.transform({1: 3});
|
const transform2 = pipe.transform({1: 3});
|
||||||
expect(transform1 !== transform2).toEqual(true);
|
expect(transform1 !== transform2).toEqual(true);
|
||||||
});
|
});
|
||||||
|
it('should accept a type union of an object with string keys and null', () => {
|
||||||
|
let value !: {[key: string]: string} | null;
|
||||||
|
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
|
||||||
|
expect(pipe.transform(value)).toEqual(null);
|
||||||
|
});
|
||||||
|
it('should accept a type union of an object with number keys and null', () => {
|
||||||
|
let value !: {[key: number]: string} | null;
|
||||||
|
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
|
||||||
|
expect(pipe.transform(value)).toEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Map', () => {
|
describe('Map', () => {
|
||||||
@ -115,6 +125,11 @@ describe('KeyValuePipe', () => {
|
|||||||
const transform2 = pipe.transform(new Map([[1, 3]]));
|
const transform2 = pipe.transform(new Map([[1, 3]]));
|
||||||
expect(transform1 !== transform2).toEqual(true);
|
expect(transform1 !== transform2).toEqual(true);
|
||||||
});
|
});
|
||||||
|
it('should accept a type union of a Map and null', () => {
|
||||||
|
let value !: Map<number, number>| null;
|
||||||
|
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
|
||||||
|
expect(pipe.transform(value)).toEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ ts_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//packages:types",
|
"//packages:types",
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli",
|
||||||
"//packages/compiler-cli/src/ngtsc/annotations",
|
"//packages/compiler-cli/src/ngtsc/annotations",
|
||||||
"//packages/compiler-cli/src/ngtsc/cycles",
|
"//packages/compiler-cli/src/ngtsc/cycles",
|
||||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
|
@ -92,6 +92,13 @@ if (require.main === module) {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
.option('tsconfig', {
|
||||||
|
describe:
|
||||||
|
'A path to a tsconfig.json file that will be used to configure the Angular compiler and module resolution used by ngcc.\n' +
|
||||||
|
'If not provided, ngcc will attempt to read a `tsconfig.json` file from the folder above that given by the `-s` option.\n' +
|
||||||
|
'Set to false (via `--no-tsconfig`) if you do not want ngcc to use any `tsconfig.json` file.',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.help()
|
.help()
|
||||||
.parse(args);
|
.parse(args);
|
||||||
@ -113,6 +120,10 @@ if (require.main === module) {
|
|||||||
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
||||||
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
||||||
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
||||||
|
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
|
||||||
|
// string "false" to capture the `tsconfig=false` option.
|
||||||
|
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
|
||||||
|
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
try {
|
try {
|
||||||
@ -126,7 +137,7 @@ if (require.main === module) {
|
|||||||
createNewEntryPointFormats,
|
createNewEntryPointFormats,
|
||||||
logger,
|
logger,
|
||||||
enableI18nLegacyMessageIdFormat,
|
enableI18nLegacyMessageIdFormat,
|
||||||
async: options['async'], invalidateEntryPointManifest, errorOnFailedEntryPoint,
|
async: options['async'], invalidateEntryPointManifest, errorOnFailedEntryPoint, tsConfigPath
|
||||||
});
|
});
|
||||||
|
|
||||||
if (logger) {
|
if (logger) {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import {ConstantPool} from '@angular/compiler';
|
import {ConstantPool} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {ParsedConfiguration} from '../../..';
|
||||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
||||||
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
||||||
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
||||||
@ -55,6 +56,7 @@ export class DecorationAnalyzer {
|
|||||||
private rootDirs = this.bundle.rootDirs;
|
private rootDirs = this.bundle.rootDirs;
|
||||||
private packagePath = this.bundle.entryPoint.package;
|
private packagePath = this.bundle.entryPoint.package;
|
||||||
private isCore = this.bundle.isCore;
|
private isCore = this.bundle.isCore;
|
||||||
|
private compilerOptions = this.tsConfig !== null? this.tsConfig.options: {};
|
||||||
|
|
||||||
moduleResolver =
|
moduleResolver =
|
||||||
new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null);
|
new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null);
|
||||||
@ -87,7 +89,7 @@ export class DecorationAnalyzer {
|
|||||||
new ComponentDecoratorHandler(
|
new ComponentDecoratorHandler(
|
||||||
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
|
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
|
||||||
this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
|
this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
|
||||||
/* defaultPreserveWhitespaces */ false,
|
!!this.compilerOptions.preserveWhitespaces,
|
||||||
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
|
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
|
||||||
this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
|
this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||||
NOOP_DEPENDENCY_TRACKER, this.injectableRegistry, /* annotateForClosureCompiler */ false),
|
NOOP_DEPENDENCY_TRACKER, this.injectableRegistry, /* annotateForClosureCompiler */ false),
|
||||||
@ -123,7 +125,8 @@ export class DecorationAnalyzer {
|
|||||||
constructor(
|
constructor(
|
||||||
private fs: FileSystem, private bundle: EntryPointBundle,
|
private fs: FileSystem, private bundle: EntryPointBundle,
|
||||||
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
|
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
|
||||||
private diagnosticHandler: (error: ts.Diagnostic) => void = () => {}) {}
|
private diagnosticHandler: (error: ts.Diagnostic) => void = () => {},
|
||||||
|
private tsConfig: ParsedConfiguration|null = null) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze a program to find all the decorated files should be transformed.
|
* Analyze a program to find all the decorated files should be transformed.
|
||||||
|
@ -12,6 +12,7 @@ import {DepGraph} from 'dependency-graph';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {readConfiguration} from '../..';
|
||||||
import {replaceTsWithNgInErrors} from '../../src/ngtsc/diagnostics';
|
import {replaceTsWithNgInErrors} from '../../src/ngtsc/diagnostics';
|
||||||
import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, getFileSystem, resolve} from '../../src/ngtsc/file_system';
|
import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, getFileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||||
|
|
||||||
@ -94,6 +95,9 @@ export interface SyncNgccOptions {
|
|||||||
/**
|
/**
|
||||||
* Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`.
|
* Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`.
|
||||||
* These are used to resolve paths to locally built Angular libraries.
|
* These are used to resolve paths to locally built Angular libraries.
|
||||||
|
*
|
||||||
|
* Note that `pathMappings` specified here take precedence over any `pathMappings` loaded from a
|
||||||
|
* TS config file.
|
||||||
*/
|
*/
|
||||||
pathMappings?: PathMappings;
|
pathMappings?: PathMappings;
|
||||||
|
|
||||||
@ -143,6 +147,18 @@ export interface SyncNgccOptions {
|
|||||||
* Default: `false` (i.e. the manifest will be used if available)
|
* Default: `false` (i.e. the manifest will be used if available)
|
||||||
*/
|
*/
|
||||||
invalidateEntryPointManifest?: boolean;
|
invalidateEntryPointManifest?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An absolute path to a TS config file (e.g. `tsconfig.json`) or a directory containing one, that
|
||||||
|
* will be used to configure module resolution with things like path mappings, if not specified
|
||||||
|
* explicitly via the `pathMappings` property to `mainNgcc`.
|
||||||
|
*
|
||||||
|
* If `undefined`, ngcc will attempt to load a `tsconfig.json` file from the directory above the
|
||||||
|
* `basePath`.
|
||||||
|
*
|
||||||
|
* If `null`, ngcc will not attempt to load any TS config file at all.
|
||||||
|
*/
|
||||||
|
tsConfigPath?: string|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,12 +181,12 @@ export type NgccOptions = AsyncNgccOptions | SyncNgccOptions;
|
|||||||
*/
|
*/
|
||||||
export function mainNgcc(options: AsyncNgccOptions): Promise<void>;
|
export function mainNgcc(options: AsyncNgccOptions): Promise<void>;
|
||||||
export function mainNgcc(options: SyncNgccOptions): void;
|
export function mainNgcc(options: SyncNgccOptions): void;
|
||||||
export function mainNgcc({basePath, targetEntryPointPath,
|
export function mainNgcc(
|
||||||
propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||||
compileAllFormats = true, createNewEntryPointFormats = false,
|
compileAllFormats = true, createNewEntryPointFormats = false,
|
||||||
logger = new ConsoleLogger(LogLevel.info), pathMappings, async = false,
|
logger = new ConsoleLogger(LogLevel.info), pathMappings, async = false,
|
||||||
errorOnFailedEntryPoint = false, enableI18nLegacyMessageIdFormat = true,
|
errorOnFailedEntryPoint = false, enableI18nLegacyMessageIdFormat = true,
|
||||||
invalidateEntryPointManifest = false}: NgccOptions): void|Promise<void> {
|
invalidateEntryPointManifest = false, tsConfigPath}: NgccOptions): void|Promise<void> {
|
||||||
if (!!targetEntryPointPath) {
|
if (!!targetEntryPointPath) {
|
||||||
// targetEntryPointPath forces us to error if an entry-point fails.
|
// targetEntryPointPath forces us to error if an entry-point fails.
|
||||||
errorOnFailedEntryPoint = true;
|
errorOnFailedEntryPoint = true;
|
||||||
@ -184,7 +200,19 @@ export function mainNgcc({basePath, targetEntryPointPath,
|
|||||||
// master/worker process.
|
// master/worker process.
|
||||||
const fileSystem = getFileSystem();
|
const fileSystem = getFileSystem();
|
||||||
const absBasePath = absoluteFrom(basePath);
|
const absBasePath = absoluteFrom(basePath);
|
||||||
const config = new NgccConfiguration(fileSystem, dirname(absBasePath));
|
const projectPath = dirname(absBasePath);
|
||||||
|
const config = new NgccConfiguration(fileSystem, projectPath);
|
||||||
|
const tsConfig = tsConfigPath !== null ? readConfiguration(tsConfigPath || projectPath) : null;
|
||||||
|
|
||||||
|
// If `pathMappings` is not provided directly, then try getting it from `tsConfig`, if available.
|
||||||
|
if (tsConfig !== null && pathMappings === undefined && tsConfig.options.baseUrl !== undefined &&
|
||||||
|
tsConfig.options.paths) {
|
||||||
|
pathMappings = {
|
||||||
|
baseUrl: resolve(projectPath, tsConfig.options.baseUrl),
|
||||||
|
paths: tsConfig.options.paths,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const dependencyResolver = getDependencyResolver(fileSystem, logger, config, pathMappings);
|
const dependencyResolver = getDependencyResolver(fileSystem, logger, config, pathMappings);
|
||||||
const entryPointManifest = invalidateEntryPointManifest ?
|
const entryPointManifest = invalidateEntryPointManifest ?
|
||||||
new InvalidatingEntryPointManifest(fileSystem, config, logger) :
|
new InvalidatingEntryPointManifest(fileSystem, config, logger) :
|
||||||
@ -279,7 +307,7 @@ export function mainNgcc({basePath, targetEntryPointPath,
|
|||||||
const createCompileFn: CreateCompileFn = onTaskCompleted => {
|
const createCompileFn: CreateCompileFn = onTaskCompleted => {
|
||||||
const fileWriter = getFileWriter(
|
const fileWriter = getFileWriter(
|
||||||
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint);
|
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint);
|
||||||
const transformer = new Transformer(fileSystem, logger);
|
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
||||||
|
|
||||||
return (task: Task) => {
|
return (task: Task) => {
|
||||||
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {ParsedConfiguration} from '../../..';
|
||||||
import {FileSystem} from '../../../src/ngtsc/file_system';
|
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||||
import {TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
import {TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||||
import {DecorationAnalyzer} from '../analysis/decoration_analyzer';
|
import {DecorationAnalyzer} from '../analysis/decoration_analyzer';
|
||||||
@ -63,7 +64,9 @@ export type TransformResult = {
|
|||||||
* - Some formats may contain multiple "modules" in a single file.
|
* - Some formats may contain multiple "modules" in a single file.
|
||||||
*/
|
*/
|
||||||
export class Transformer {
|
export class Transformer {
|
||||||
constructor(private fs: FileSystem, private logger: Logger) {}
|
constructor(
|
||||||
|
private fs: FileSystem, private logger: Logger,
|
||||||
|
private tsConfig: ParsedConfiguration|null = null) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the source (and typings) files of a bundle.
|
* Transform the source (and typings) files of a bundle.
|
||||||
@ -146,7 +149,7 @@ export class Transformer {
|
|||||||
const diagnostics: ts.Diagnostic[] = [];
|
const diagnostics: ts.Diagnostic[] = [];
|
||||||
const decorationAnalyzer = new DecorationAnalyzer(
|
const decorationAnalyzer = new DecorationAnalyzer(
|
||||||
this.fs, bundle, reflectionHost, referencesRegistry,
|
this.fs, bundle, reflectionHost, referencesRegistry,
|
||||||
diagnostic => diagnostics.push(diagnostic));
|
diagnostic => diagnostics.push(diagnostic), this.tsConfig);
|
||||||
const decorationAnalyses = decorationAnalyzer.analyzeProgram();
|
const decorationAnalyses = decorationAnalyzer.analyzeProgram();
|
||||||
|
|
||||||
const moduleWithProvidersAnalyzer =
|
const moduleWithProvidersAnalyzer =
|
||||||
|
@ -91,6 +91,7 @@ export class DtsRenderer {
|
|||||||
const endOfClass = dtsClass.dtsDeclaration.getEnd();
|
const endOfClass = dtsClass.dtsDeclaration.getEnd();
|
||||||
dtsClass.compilation.forEach(declaration => {
|
dtsClass.compilation.forEach(declaration => {
|
||||||
const type = translateType(declaration.type, importManager);
|
const type = translateType(declaration.type, importManager);
|
||||||
|
markForEmitAsSingleLine(type);
|
||||||
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
|
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
|
||||||
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
|
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
|
||||||
outputText.appendRight(endOfClass - 1, newStatement);
|
outputText.appendRight(endOfClass - 1, newStatement);
|
||||||
@ -176,3 +177,8 @@ export class DtsRenderer {
|
|||||||
return dtsMap;
|
return dtsMap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markForEmitAsSingleLine(node: ts.Node) {
|
||||||
|
ts.setEmitFlags(node, ts.EmitFlags.SingleLine);
|
||||||
|
ts.forEachChild(node, markForEmitAsSingleLine);
|
||||||
|
}
|
||||||
|
@ -399,9 +399,22 @@ runInEachFileSystem(() => {
|
|||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(`export declare class ${exportedName} extends PlatformLocation`);
|
.toContain(`export declare class ${exportedName} extends PlatformLocation`);
|
||||||
// And that ngcc's modifications to that class use the correct (exported) name
|
// And that ngcc's modifications to that class use the correct (exported) name
|
||||||
expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}>`);
|
expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}, never>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should include constructor metadata in factory definitions', () => {
|
||||||
|
mainNgcc({
|
||||||
|
basePath: '/node_modules',
|
||||||
|
targetEntryPointPath: '@angular/common',
|
||||||
|
propertiesToConsider: ['esm2015']
|
||||||
|
});
|
||||||
|
|
||||||
|
const dtsContents = fs.readFile(_('/node_modules/@angular/common/common.d.ts'));
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(
|
||||||
|
`static ɵfac: ɵngcc0.ɵɵFactoryDef<NgPluralCase, [{ attribute: "ngPluralCase"; }, null, null, { host: true; }]>`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should add generic type for ModuleWithProviders and generate exports for private modules',
|
it('should add generic type for ModuleWithProviders and generate exports for private modules',
|
||||||
() => {
|
() => {
|
||||||
compileIntoApf('test-package', {
|
compileIntoApf('test-package', {
|
||||||
@ -1230,21 +1243,171 @@ runInEachFileSystem(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('with pathMappings', () => {
|
describe('with pathMappings', () => {
|
||||||
it('should find and compile packages accessible via the pathMappings', () => {
|
it('should infer the @app pathMapping from a local tsconfig.json path', () => {
|
||||||
mainNgcc({
|
fs.writeFile(
|
||||||
basePath: '/node_modules',
|
_('/tsconfig.json'),
|
||||||
propertiesToConsider: ['es2015'],
|
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
|
||||||
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
|
const logger = new MockLogger();
|
||||||
});
|
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015'], logger});
|
||||||
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
es2015: '0.0.0-PLACEHOLDER',
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
fesm2015: '0.0.0-PLACEHOLDER',
|
|
||||||
typings: '0.0.0-PLACEHOLDER',
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
});
|
});
|
||||||
|
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
// The local-package-3 and local-package-4 will not be processed because there is no path
|
||||||
|
// mappings for `@x` and plain local imports.
|
||||||
|
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-3')}.`,
|
||||||
|
'It is missing required dependencies:\n - @x/local-package'
|
||||||
|
]);
|
||||||
|
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-4')}.`,
|
||||||
|
'It is missing required dependencies:\n - local-package'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the @x pathMapping from a specified tsconfig.json path', () => {
|
||||||
|
fs.writeFile(
|
||||||
|
_('/tsconfig.app.json'),
|
||||||
|
JSON.stringify({compilerOptions: {paths: {'@x/*': ['dist/*']}, baseUrl: './'}}));
|
||||||
|
const logger = new MockLogger();
|
||||||
|
mainNgcc({
|
||||||
|
basePath: '/dist',
|
||||||
|
propertiesToConsider: ['es2015'],
|
||||||
|
tsConfigPath: _('/tsconfig.app.json'), logger
|
||||||
|
});
|
||||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
es2015: '0.0.0-PLACEHOLDER',
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
typings: '0.0.0-PLACEHOLDER',
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
});
|
});
|
||||||
|
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
// The local-package-2 and local-package-4 will not be processed because there is no path
|
||||||
|
// mappings for `@app` and plain local imports.
|
||||||
|
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-2')}.`,
|
||||||
|
'It is missing required dependencies:\n - @app/local-package'
|
||||||
|
]);
|
||||||
|
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-4')}.`,
|
||||||
|
'It is missing required dependencies:\n - local-package'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the explicit `pathMappings`, ignoring the local tsconfig.json settings',
|
||||||
|
() => {
|
||||||
|
const logger = new MockLogger();
|
||||||
|
fs.writeFile(
|
||||||
|
_('/tsconfig.json'),
|
||||||
|
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
|
||||||
|
mainNgcc({
|
||||||
|
basePath: '/node_modules',
|
||||||
|
propertiesToConsider: ['es2015'],
|
||||||
|
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'}, logger
|
||||||
|
});
|
||||||
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
fesm2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
// The local-package-2 and local-package-3 will not be processed because there is no path
|
||||||
|
// mappings for `@app` and `@x` local imports.
|
||||||
|
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-2')}.`,
|
||||||
|
'It is missing required dependencies:\n - @app/local-package'
|
||||||
|
]);
|
||||||
|
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-3')}.`,
|
||||||
|
'It is missing required dependencies:\n - @x/local-package'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use pathMappings from a local tsconfig.json path if tsConfigPath is null',
|
||||||
|
() => {
|
||||||
|
const logger = new MockLogger();
|
||||||
|
fs.writeFile(
|
||||||
|
_('/tsconfig.json'),
|
||||||
|
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
|
||||||
|
mainNgcc({
|
||||||
|
basePath: '/dist',
|
||||||
|
propertiesToConsider: ['es2015'],
|
||||||
|
tsConfigPath: null, logger,
|
||||||
|
});
|
||||||
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
// Since the tsconfig is not loaded, the `@app/local-package` import in `local-package-2`
|
||||||
|
// is not path-mapped correctly, and so it fails to be processed.
|
||||||
|
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
|
||||||
|
.toBeUndefined();
|
||||||
|
expect(logger.logs.debug).toContain([
|
||||||
|
`Invalid entry-point ${_('/dist/local-package-2')}.`,
|
||||||
|
'It is missing required dependencies:\n - @app/local-package'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('whitespace preservation', () => {
|
||||||
|
it('should default not to preserve whitespace', () => {
|
||||||
|
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
|
||||||
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
expect(fs.readFile(_('/dist/local-package/index.js')))
|
||||||
|
.toMatch(/ɵɵtext\(\d+, " Hello\\n"\);/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve whitespace if set in a loaded tsconfig.json', () => {
|
||||||
|
fs.writeFile(
|
||||||
|
_('/tsconfig.json'),
|
||||||
|
JSON.stringify({angularCompilerOptions: {preserveWhitespaces: true}}));
|
||||||
|
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
|
||||||
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
expect(fs.readFile(_('/dist/local-package/index.js')))
|
||||||
|
.toMatch(/ɵɵtext\(\d+, "\\n Hello\\n"\);/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not preserve whitespace if set to false in a loaded tsconfig.json', () => {
|
||||||
|
fs.writeFile(
|
||||||
|
_('/tsconfig.json'),
|
||||||
|
JSON.stringify({angularCompilerOptions: {preserveWhitespaces: false}}));
|
||||||
|
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
|
||||||
|
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||||
|
es2015: '0.0.0-PLACEHOLDER',
|
||||||
|
typings: '0.0.0-PLACEHOLDER',
|
||||||
|
});
|
||||||
|
expect(fs.readFile(_('/dist/local-package/index.js')))
|
||||||
|
.toMatch(/ɵɵtext\(\d+, " Hello\\n"\);/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1477,7 +1640,7 @@ runInEachFileSystem(() => {
|
|||||||
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
|
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta<DerivedCmp, "[base]", never, {}, {}, never>;');
|
'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta<DerivedCmp, "[base]", never, {}, {}, never, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',
|
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',
|
||||||
@ -1748,7 +1911,7 @@ runInEachFileSystem(() => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// An Angular package that has been built locally and stored in the `dist` directory.
|
// Angular packages that have been built locally and stored in the `dist` directory.
|
||||||
loadTestFiles([
|
loadTestFiles([
|
||||||
{
|
{
|
||||||
name: _('/dist/local-package/package.json'),
|
name: _('/dist/local-package/package.json'),
|
||||||
@ -1758,12 +1921,60 @@ runInEachFileSystem(() => {
|
|||||||
{
|
{
|
||||||
name: _('/dist/local-package/index.js'),
|
name: _('/dist/local-package/index.js'),
|
||||||
contents:
|
contents:
|
||||||
`import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '<h2>Hello</h2>'}] }\n];`
|
`import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '<h2>\\n Hello\\n</h2>'}] }\n];`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: _('/dist/local-package/index.d.ts'),
|
name: _('/dist/local-package/index.d.ts'),
|
||||||
contents: `export declare class AppComponent {};`
|
contents: `export declare class AppComponent {};`
|
||||||
},
|
},
|
||||||
|
// local-package-2 depends upon local-package, via an `@app` aliased import.
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-2/package.json'),
|
||||||
|
contents: '{"name": "local-package-2", "es2015": "./index.js", "typings": "./index.d.ts"}'
|
||||||
|
},
|
||||||
|
{name: _('/dist/local-package-2/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-2/index.js'),
|
||||||
|
contents:
|
||||||
|
`import {Component} from '@angular/core';\nexport {AppComponent} from '@app/local-package';`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-2/index.d.ts'),
|
||||||
|
contents:
|
||||||
|
`import {Component} from '@angular/core';\nexport {AppComponent} from '@app/local-package';`
|
||||||
|
},
|
||||||
|
// local-package-3 depends upon local-package, via an `@x` aliased import.
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-3/package.json'),
|
||||||
|
contents: '{"name": "local-package-3", "es2015": "./index.js", "typings": "./index.d.ts"}'
|
||||||
|
},
|
||||||
|
{name: _('/dist/local-package-3/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-3/index.js'),
|
||||||
|
contents:
|
||||||
|
`import {Component} from '@angular/core';\nexport {AppComponent} from '@x/local-package';`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-3/index.d.ts'),
|
||||||
|
contents:
|
||||||
|
`import {Component} from '@angular/core';\nexport {AppComponent} from '@x/local-package';`
|
||||||
|
},
|
||||||
|
// local-package-4 depends upon local-package, via a plain import.
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-4/package.json'),
|
||||||
|
contents: '{"name": "local-package-", "es2015": "./index.js", "typings": "./index.d.ts"}'
|
||||||
|
},
|
||||||
|
{name: _('/dist/local-package-4/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-4/index.js'),
|
||||||
|
contents:
|
||||||
|
`import {Component} from '@angular/core';\nexport {AppComponent} from 'local-package';`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/dist/local-package-4/index.d.ts'),
|
||||||
|
contents:
|
||||||
|
`import {Component} from '@angular/core';\nexport {AppComponent} from 'local-package';`
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// An Angular package that has a missing dependency
|
// An Angular package that has a missing dependency
|
||||||
|
@ -130,7 +130,7 @@ runInEachFileSystem(() => {
|
|||||||
result.find(f => f.path === _('/node_modules/test-package/typings/file.d.ts')) !;
|
result.find(f => f.path === _('/node_modules/test-package/typings/file.d.ts')) !;
|
||||||
expect(typingsFile.contents)
|
expect(typingsFile.contents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef<A>;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta');
|
'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef<A, never>;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render imports into typings files', () => {
|
it('should render imports into typings files', () => {
|
||||||
|
@ -313,6 +313,7 @@ export class ComponentDecoratorHandler implements
|
|||||||
...metadata,
|
...metadata,
|
||||||
template: {
|
template: {
|
||||||
nodes: template.emitNodes,
|
nodes: template.emitNodes,
|
||||||
|
ngContentSelectors: template.ngContentSelectors,
|
||||||
},
|
},
|
||||||
encapsulation,
|
encapsulation,
|
||||||
interpolation: template.interpolation,
|
interpolation: template.interpolation,
|
||||||
@ -770,12 +771,13 @@ export class ComponentDecoratorHandler implements
|
|||||||
interpolation = InterpolationConfig.fromArray(value as[string, string]);
|
interpolation = InterpolationConfig.fromArray(value as[string, string]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {errors, nodes: emitNodes, styleUrls, styles} = parseTemplate(templateStr, templateUrl, {
|
const {errors, nodes: emitNodes, styleUrls, styles, ngContentSelectors} =
|
||||||
preserveWhitespaces,
|
parseTemplate(templateStr, templateUrl, {
|
||||||
interpolationConfig: interpolation,
|
preserveWhitespaces,
|
||||||
range: templateRange, escapedString,
|
interpolationConfig: interpolation,
|
||||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
range: templateRange, escapedString,
|
||||||
});
|
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||||
|
});
|
||||||
|
|
||||||
// Unfortunately, the primary parse of the template above may not contain accurate source map
|
// Unfortunately, the primary parse of the template above may not contain accurate source map
|
||||||
// information. If used directly, it would result in incorrect code locations in template
|
// information. If used directly, it would result in incorrect code locations in template
|
||||||
@ -804,6 +806,7 @@ export class ComponentDecoratorHandler implements
|
|||||||
diagNodes,
|
diagNodes,
|
||||||
styleUrls,
|
styleUrls,
|
||||||
styles,
|
styles,
|
||||||
|
ngContentSelectors,
|
||||||
errors,
|
errors,
|
||||||
template: templateStr, templateUrl,
|
template: templateStr, templateUrl,
|
||||||
isInline: component.has('template'),
|
isInline: component.has('template'),
|
||||||
@ -923,6 +926,11 @@ export interface ParsedTemplate {
|
|||||||
*/
|
*/
|
||||||
styles: string[];
|
styles: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any ng-content selectors extracted from the template.
|
||||||
|
*/
|
||||||
|
ngContentSelectors: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the template was inline.
|
* Whether the template was inline.
|
||||||
*/
|
*/
|
||||||
|
@ -274,6 +274,7 @@ function extractInjectableCtorDeps(
|
|||||||
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
|
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
|
||||||
const meta: R3DependencyMetadata = {
|
const meta: R3DependencyMetadata = {
|
||||||
token: new WrappedNodeExpr(dep),
|
token: new WrappedNodeExpr(dep),
|
||||||
|
attribute: null,
|
||||||
host: false,
|
host: false,
|
||||||
resolved: R3ResolvedDependencyType.Token,
|
resolved: R3ResolvedDependencyType.Token,
|
||||||
optional: false,
|
optional: false,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
|
import {Expression, ExternalExpr, LiteralExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
|
import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
|
||||||
@ -48,6 +48,7 @@ export function getConstructorDependencies(
|
|||||||
}
|
}
|
||||||
ctorParams.forEach((param, idx) => {
|
ctorParams.forEach((param, idx) => {
|
||||||
let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder);
|
let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder);
|
||||||
|
let attribute: Expression|null = null;
|
||||||
let optional = false, self = false, skipSelf = false, host = false;
|
let optional = false, self = false, skipSelf = false, host = false;
|
||||||
let resolved = R3ResolvedDependencyType.Token;
|
let resolved = R3ResolvedDependencyType.Token;
|
||||||
|
|
||||||
@ -74,7 +75,13 @@ export function getConstructorDependencies(
|
|||||||
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec),
|
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec),
|
||||||
`Unexpected number of arguments to @Attribute().`);
|
`Unexpected number of arguments to @Attribute().`);
|
||||||
}
|
}
|
||||||
token = new WrappedNodeExpr(dec.args[0]);
|
const attributeName = dec.args[0];
|
||||||
|
token = new WrappedNodeExpr(attributeName);
|
||||||
|
if (ts.isStringLiteralLike(attributeName)) {
|
||||||
|
attribute = new LiteralExpr(attributeName.text);
|
||||||
|
} else {
|
||||||
|
attribute = new WrappedNodeExpr(ts.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
|
||||||
|
}
|
||||||
resolved = R3ResolvedDependencyType.Attribute;
|
resolved = R3ResolvedDependencyType.Attribute;
|
||||||
} else {
|
} else {
|
||||||
throw new FatalDiagnosticError(
|
throw new FatalDiagnosticError(
|
||||||
@ -93,7 +100,7 @@ export function getConstructorDependencies(
|
|||||||
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param,
|
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
deps.push({token, optional, self, skipSelf, host, resolved});
|
deps.push({token, attribute, optional, self, skipSelf, host, resolved});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (errors.length === 0) {
|
if (errors.length === 0) {
|
||||||
@ -369,7 +376,8 @@ const parensWrapperTransformerFactory: ts.TransformerFactory<ts.Expression> =
|
|||||||
/**
|
/**
|
||||||
* Wraps all functions in a given expression in parentheses. This is needed to avoid problems
|
* Wraps all functions in a given expression in parentheses. This is needed to avoid problems
|
||||||
* where Tsickle annotations added between analyse and transform phases in Angular may trigger
|
* where Tsickle annotations added between analyse and transform phases in Angular may trigger
|
||||||
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement. More
|
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement.
|
||||||
|
* More
|
||||||
* info can be found in Tsickle source code here:
|
* info can be found in Tsickle source code here:
|
||||||
* https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021
|
* https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021
|
||||||
*
|
*
|
||||||
|
@ -205,7 +205,7 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
|
|||||||
const newMembers = fields.map(decl => {
|
const newMembers = fields.map(decl => {
|
||||||
const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)];
|
const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)];
|
||||||
const typeRef = translateType(decl.type, imports);
|
const typeRef = translateType(decl.type, imports);
|
||||||
emitAsSingleLine(typeRef);
|
markForEmitAsSingleLine(typeRef);
|
||||||
return ts.createProperty(
|
return ts.createProperty(
|
||||||
/* decorators */ undefined,
|
/* decorators */ undefined,
|
||||||
/* modifiers */ modifiers,
|
/* modifiers */ modifiers,
|
||||||
@ -226,9 +226,9 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitAsSingleLine(node: ts.Node) {
|
function markForEmitAsSingleLine(node: ts.Node) {
|
||||||
ts.setEmitFlags(node, ts.EmitFlags.SingleLine);
|
ts.setEmitFlags(node, ts.EmitFlags.SingleLine);
|
||||||
ts.forEachChild(node, emitAsSingleLine);
|
ts.forEachChild(node, markForEmitAsSingleLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReturnTypeTransform implements DtsTransform {
|
export class ReturnTypeTransform implements DtsTransform {
|
||||||
|
@ -447,12 +447,12 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
|||||||
`An ExpressionType with type arguments cannot have multiple levels of type arguments`);
|
`An ExpressionType with type arguments cannot have multiple levels of type arguments`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeArgs = type.typeParams.map(param => param.visitType(this, context));
|
const typeArgs = type.typeParams.map(param => this.translateType(param, context));
|
||||||
return ts.createTypeReferenceNode(typeNode.typeName, typeArgs);
|
return ts.createTypeReferenceNode(typeNode.typeName, typeArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitArrayType(type: ArrayType, context: Context): ts.ArrayTypeNode {
|
visitArrayType(type: ArrayType, context: Context): ts.ArrayTypeNode {
|
||||||
return ts.createArrayTypeNode(this.translateType(type, context));
|
return ts.createArrayTypeNode(this.translateType(type.of, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitMapType(type: MapType, context: Context): ts.TypeLiteralNode {
|
visitMapType(type: MapType, context: Context): ts.TypeLiteralNode {
|
||||||
@ -497,8 +497,18 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.LiteralTypeNode {
|
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.TypeNode {
|
||||||
return ts.createLiteralTypeNode(ts.createLiteral(ast.value as string));
|
if (ast.value === null) {
|
||||||
|
return ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword);
|
||||||
|
} else if (ast.value === undefined) {
|
||||||
|
return ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
|
||||||
|
} else if (typeof ast.value === 'boolean') {
|
||||||
|
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
|
||||||
|
} else if (typeof ast.value === 'number') {
|
||||||
|
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
|
||||||
|
} else {
|
||||||
|
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLocalizedString(ast: LocalizedString, context: Context): never {
|
visitLocalizedString(ast: LocalizedString, context: Context): never {
|
||||||
@ -578,6 +588,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
|||||||
return ts.createTypeReferenceNode(node, /* typeArguments */ undefined);
|
return ts.createTypeReferenceNode(node, /* typeArguments */ undefined);
|
||||||
} else if (ts.isTypeNode(node)) {
|
} else if (ts.isTypeNode(node)) {
|
||||||
return node;
|
return node;
|
||||||
|
} else if (ts.isLiteralExpression(node)) {
|
||||||
|
return ts.createLiteralTypeNode(node);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`);
|
`Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`);
|
||||||
@ -590,8 +602,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
|||||||
return ts.createTypeQueryNode(expr as ts.Identifier);
|
return ts.createTypeQueryNode(expr as ts.Identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
private translateType(expr: Type, context: Context): ts.TypeNode {
|
private translateType(type: Type, context: Context): ts.TypeNode {
|
||||||
const typeNode = expr.visitType(this, context);
|
const typeNode = type.visitType(this, context);
|
||||||
if (!ts.isTypeNode(typeNode)) {
|
if (!ts.isTypeNode(typeNode)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`);
|
`A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`);
|
||||||
|
@ -34,6 +34,7 @@ export const Inject = callableParamDecorator();
|
|||||||
export const Self = callableParamDecorator();
|
export const Self = callableParamDecorator();
|
||||||
export const SkipSelf = callableParamDecorator();
|
export const SkipSelf = callableParamDecorator();
|
||||||
export const Optional = callableParamDecorator();
|
export const Optional = callableParamDecorator();
|
||||||
|
export const Host = callableParamDecorator();
|
||||||
|
|
||||||
export const ContentChild = callablePropDecorator();
|
export const ContentChild = callablePropDecorator();
|
||||||
export const ContentChildren = callablePropDecorator();
|
export const ContentChildren = callablePropDecorator();
|
||||||
@ -68,7 +69,8 @@ export function forwardRef<T>(fn: () => T): T {
|
|||||||
export interface SimpleChanges { [propName: string]: any; }
|
export interface SimpleChanges { [propName: string]: any; }
|
||||||
|
|
||||||
export type ɵɵNgModuleDefWithMeta<ModuleT, DeclarationsT, ImportsT, ExportsT> = any;
|
export type ɵɵNgModuleDefWithMeta<ModuleT, DeclarationsT, ImportsT, ExportsT> = any;
|
||||||
export type ɵɵDirectiveDefWithMeta<DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT> = any;
|
export type ɵɵDirectiveDefWithMeta<
|
||||||
|
DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT, NgContentSelectorsT> = any;
|
||||||
export type ɵɵPipeDefWithMeta<PipeT, NameT> = any;
|
export type ɵɵPipeDefWithMeta<PipeT, NameT> = any;
|
||||||
|
|
||||||
export enum ViewEncapsulation {
|
export enum ViewEncapsulation {
|
||||||
|
@ -68,8 +68,8 @@ runInEachFileSystem(os => {
|
|||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep, never>;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Injectables with a generic service', () => {
|
it('should compile Injectables with a generic service', () => {
|
||||||
@ -86,7 +86,7 @@ runInEachFileSystem(os => {
|
|||||||
const jsContents = env.getContents('test.js');
|
const jsContents = env.getContents('test.js');
|
||||||
expect(jsContents).toContain('Store.ɵprov =');
|
expect(jsContents).toContain('Store.ɵprov =');
|
||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Store<any>>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Store<any>, never>;');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Store<any>>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Store<any>>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -117,8 +117,8 @@ runInEachFileSystem(os => {
|
|||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep, never>;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Injectables with providedIn and factory without errors', () => {
|
it('should compile Injectables with providedIn and factory without errors', () => {
|
||||||
@ -143,7 +143,7 @@ runInEachFileSystem(os => {
|
|||||||
expect(jsContents).not.toContain('__decorate');
|
expect(jsContents).not.toContain('__decorate');
|
||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Injectables with providedIn and factory with deps without errors', () => {
|
it('should compile Injectables with providedIn and factory with deps without errors', () => {
|
||||||
@ -172,7 +172,7 @@ runInEachFileSystem(os => {
|
|||||||
expect(jsContents).not.toContain('__decorate');
|
expect(jsContents).not.toContain('__decorate');
|
||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile @Injectable with an @Optional dependency', () => {
|
it('should compile @Injectable with an @Optional dependency', () => {
|
||||||
@ -237,7 +237,7 @@ runInEachFileSystem(os => {
|
|||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[dir]", never, {}, {}, never>');
|
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[dir]", never, {}, {}, never>');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir, never>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile abstract Directives without errors', () => {
|
it('should compile abstract Directives without errors', () => {
|
||||||
@ -259,7 +259,7 @@ runInEachFileSystem(os => {
|
|||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, never, never, {}, {}, never>');
|
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, never, never, {}, {}, never>');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir, never>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Components (inline template) without errors', () => {
|
it('should compile Components (inline template) without errors', () => {
|
||||||
@ -283,8 +283,8 @@ runInEachFileSystem(os => {
|
|||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Components (dynamic inline template) without errors', () => {
|
it('should compile Components (dynamic inline template) without errors', () => {
|
||||||
@ -309,8 +309,9 @@ runInEachFileSystem(os => {
|
|||||||
|
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
'static ɵcmp: i0.ɵɵComponentDefWithMeta' +
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
|
'<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||||
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Components (function call inline template) without errors', () => {
|
it('should compile Components (function call inline template) without errors', () => {
|
||||||
@ -337,8 +338,8 @@ runInEachFileSystem(os => {
|
|||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Components (external template) without errors', () => {
|
it('should compile Components (external template) without errors', () => {
|
||||||
@ -935,7 +936,7 @@ runInEachFileSystem(os => {
|
|||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain(
|
.toContain(
|
||||||
'static ɵmod: i0.ɵɵNgModuleDefWithMeta<TestModule, [typeof TestCmp], never, never>');
|
'static ɵmod: i0.ɵɵNgModuleDefWithMeta<TestModule, [typeof TestCmp], never, never>');
|
||||||
@ -1327,7 +1328,7 @@ runInEachFileSystem(os => {
|
|||||||
.toContain(
|
.toContain(
|
||||||
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
|
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
|
||||||
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
|
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile pure Pipes without errors', () => {
|
it('should compile pure Pipes without errors', () => {
|
||||||
@ -1352,7 +1353,7 @@ runInEachFileSystem(os => {
|
|||||||
.toContain(
|
.toContain(
|
||||||
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
|
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
|
||||||
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
|
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile Pipes with dependencies', () => {
|
it('should compile Pipes with dependencies', () => {
|
||||||
@ -1393,7 +1394,7 @@ runInEachFileSystem(os => {
|
|||||||
const dtsContents = env.getContents('test.d.ts');
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
expect(dtsContents)
|
expect(dtsContents)
|
||||||
.toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe<any>, "test-pipe">;');
|
.toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe<any>, "test-pipe">;');
|
||||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe<any>>;');
|
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe<any>, never>;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include @Pipes in @NgModule scopes', () => {
|
it('should include @Pipes in @NgModule scopes', () => {
|
||||||
@ -2574,6 +2575,141 @@ runInEachFileSystem(os => {
|
|||||||
`FooCmp.ɵfac = function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`);
|
`FooCmp.ɵfac = function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should include constructor dependency metadata for directives/components/pipes', () => {
|
||||||
|
env.write(`test.ts`, `
|
||||||
|
import {Attribute, Component, Directive, Pipe, Self, SkipSelf, Host, Optional} from '@angular/core';
|
||||||
|
|
||||||
|
export class MyService {}
|
||||||
|
export function dynamic() {};
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class WithDecorators {
|
||||||
|
constructor(
|
||||||
|
@Self() withSelf: MyService,
|
||||||
|
@SkipSelf() withSkipSelf: MyService,
|
||||||
|
@Host() withHost: MyService,
|
||||||
|
@Optional() withOptional: MyService,
|
||||||
|
@Attribute("attr") withAttribute: string,
|
||||||
|
@Attribute(dynamic()) withAttributeDynamic: string,
|
||||||
|
@Optional() @SkipSelf() @Host() withMany: MyService,
|
||||||
|
noDecorators: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class NoCtor {}
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class EmptyCtor {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class WithoutDecorators {
|
||||||
|
constructor(noDecorators: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({ template: 'test' })
|
||||||
|
export class MyCmp {
|
||||||
|
constructor(@Host() withHost: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({ name: 'test' })
|
||||||
|
export class MyPipe {
|
||||||
|
constructor(@Host() withHost: MyService) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(
|
||||||
|
'static ɵfac: i0.ɵɵFactoryDef<WithDecorators, [' +
|
||||||
|
'{ self: true; }, { skipSelf: true; }, { host: true; }, ' +
|
||||||
|
'{ optional: true; }, { attribute: "attr"; }, { attribute: unknown; }, ' +
|
||||||
|
'{ optional: true; host: true; skipSelf: true; }, null]>');
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<NoCtor, never>`);
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<EmptyCtor, never>`);
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<WithoutDecorators, never>`);
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<MyCmp, [{ host: true; }]>`);
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<MyPipe, [{ host: true; }]>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include constructor dependency metadata for @Injectable', () => {
|
||||||
|
env.write(`test.ts`, `
|
||||||
|
import {Injectable, Self, Host} from '@angular/core';
|
||||||
|
|
||||||
|
export class MyService {}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class Inj {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ useExisting: MyService })
|
||||||
|
export class InjUseExisting {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ useClass: MyService })
|
||||||
|
export class InjUseClass {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ useClass: MyService, deps: [[new Host(), MyService]] })
|
||||||
|
export class InjUseClassWithDeps {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ useFactory: () => new Injectable(new MyService()) })
|
||||||
|
export class InjUseFactory {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ useFactory: (service: MyService) => new Injectable(service), deps: [[new Host(), MyService]] })
|
||||||
|
export class InjUseFactoryWithDeps {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ useValue: new Injectable(new MyService()) })
|
||||||
|
export class InjUseValue {
|
||||||
|
constructor(@Self() service: MyService) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<Inj, [{ self: true; }]>`);
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseExisting, [{ self: true; }]>`);
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseClass, [{ self: true; }]>`);
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseClassWithDeps, [{ self: true; }]>`);
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseFactory, [{ self: true; }]>`);
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseFactoryWithDeps, [{ self: true; }]>`);
|
||||||
|
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseValue, [{ self: true; }]>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include ng-content selectors in the metadata', () => {
|
||||||
|
env.write(`test.ts`, `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test',
|
||||||
|
template: '<ng-content></ng-content> <ng-content select=".foo"></ng-content>',
|
||||||
|
})
|
||||||
|
export class TestCmp {
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
const dtsContents = env.getContents('test.d.ts');
|
||||||
|
expect(dtsContents)
|
||||||
|
.toContain(
|
||||||
|
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test", never, {}, {}, never, ["*", ".foo"]>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should generate queries for components', () => {
|
it('should generate queries for components', () => {
|
||||||
env.write(`test.ts`, `
|
env.write(`test.ts`, `
|
||||||
import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core';
|
import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core';
|
||||||
@ -6520,7 +6656,7 @@ export const Foo = Foo__PRE_R3__;
|
|||||||
export declare class NgZone {}
|
export declare class NgZone {}
|
||||||
|
|
||||||
export declare class Testability {
|
export declare class Testability {
|
||||||
static ɵfac: i0.ɵɵFactoryDef<Testability>;
|
static ɵfac: i0.ɵɵFactoryDef<Testability, never>;
|
||||||
constructor(ngZone: NgZone) {}
|
constructor(ngZone: NgZone) {}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -296,11 +296,12 @@ function convertR3DependencyMetadata(facade: R3DependencyMetadataFacade): R3Depe
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
token: tokenExpr,
|
token: tokenExpr,
|
||||||
|
attribute: null,
|
||||||
resolved: facade.resolved,
|
resolved: facade.resolved,
|
||||||
host: facade.host,
|
host: facade.host,
|
||||||
optional: facade.optional,
|
optional: facade.optional,
|
||||||
self: facade.self,
|
self: facade.self,
|
||||||
skipSelf: facade.skipSelf
|
skipSelf: facade.skipSelf,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -747,7 +747,7 @@ function isNamedEntityEnd(code: number): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isExpansionCaseStart(peek: number): boolean {
|
function isExpansionCaseStart(peek: number): boolean {
|
||||||
return peek === chars.$EQ || chars.isAsciiLetter(peek) || chars.isDigit(peek);
|
return peek !== chars.$RBRACE;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareCharCodeCaseInsensitive(code1: number, code2: number): boolean {
|
function compareCharCodeCaseInsensitive(code1: number, code2: number): boolean {
|
||||||
|
@ -141,6 +141,13 @@ export interface R3DependencyMetadata {
|
|||||||
*/
|
*/
|
||||||
token: o.Expression;
|
token: o.Expression;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an @Attribute decorator is present, this is the literal type of the attribute name, or
|
||||||
|
* the unknown type if no literal type is available (e.g. the attribute name is an expression).
|
||||||
|
* Will be null otherwise.
|
||||||
|
*/
|
||||||
|
attribute: o.Expression|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An enum indicating whether this dependency has special meaning to Angular and needs to be
|
* An enum indicating whether this dependency has special meaning to Angular and needs to be
|
||||||
* injected specially.
|
* injected specially.
|
||||||
@ -180,6 +187,7 @@ export interface R3FactoryFn {
|
|||||||
export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
||||||
const t = o.variable('t');
|
const t = o.variable('t');
|
||||||
const statements: o.Statement[] = [];
|
const statements: o.Statement[] = [];
|
||||||
|
let ctorDepsType: o.Type = o.NONE_TYPE;
|
||||||
|
|
||||||
// The type to instantiate via constructor invocation. If there is no delegated factory, meaning
|
// The type to instantiate via constructor invocation. If there is no delegated factory, meaning
|
||||||
// this type is always created by constructor invocation, then this is the type-to-create
|
// this type is always created by constructor invocation, then this is the type-to-create
|
||||||
@ -197,6 +205,8 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
|||||||
ctorExpr = new o.InstantiateExpr(
|
ctorExpr = new o.InstantiateExpr(
|
||||||
typeForCtor,
|
typeForCtor,
|
||||||
injectDependencies(meta.deps, meta.injectFn, meta.target === R3FactoryTarget.Pipe));
|
injectDependencies(meta.deps, meta.injectFn, meta.target === R3FactoryTarget.Pipe));
|
||||||
|
|
||||||
|
ctorDepsType = createCtorDepsType(meta.deps);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`);
|
const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`);
|
||||||
@ -269,8 +279,9 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
|||||||
[new o.FnParam('t', o.DYNAMIC_TYPE)], body, o.INFERRED_TYPE, undefined,
|
[new o.FnParam('t', o.DYNAMIC_TYPE)], body, o.INFERRED_TYPE, undefined,
|
||||||
`${meta.name}_Factory`),
|
`${meta.name}_Factory`),
|
||||||
statements,
|
statements,
|
||||||
type: o.expressionType(
|
type: o.expressionType(o.importExpr(
|
||||||
o.importExpr(R3.FactoryDef, [typeWithParameters(meta.type.type, meta.typeArgumentCount)]))
|
R3.FactoryDef,
|
||||||
|
[typeWithParameters(meta.type.type, meta.typeArgumentCount), ctorDepsType]))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +330,49 @@ function compileInjectDependency(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCtorDepsType(deps: R3DependencyMetadata[]): o.Type {
|
||||||
|
let hasTypes = false;
|
||||||
|
const attributeTypes = deps.map(dep => {
|
||||||
|
const type = createCtorDepType(dep);
|
||||||
|
if (type !== null) {
|
||||||
|
hasTypes = true;
|
||||||
|
return type;
|
||||||
|
} else {
|
||||||
|
return o.literal(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasTypes) {
|
||||||
|
return o.expressionType(o.literalArr(attributeTypes));
|
||||||
|
} else {
|
||||||
|
return o.NONE_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCtorDepType(dep: R3DependencyMetadata): o.LiteralMapExpr|null {
|
||||||
|
const entries: {key: string, quoted: boolean, value: o.Expression}[] = [];
|
||||||
|
|
||||||
|
if (dep.resolved === R3ResolvedDependencyType.Attribute) {
|
||||||
|
if (dep.attribute !== null) {
|
||||||
|
entries.push({key: 'attribute', value: dep.attribute, quoted: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dep.optional) {
|
||||||
|
entries.push({key: 'optional', value: o.literal(true), quoted: false});
|
||||||
|
}
|
||||||
|
if (dep.host) {
|
||||||
|
entries.push({key: 'host', value: o.literal(true), quoted: false});
|
||||||
|
}
|
||||||
|
if (dep.self) {
|
||||||
|
entries.push({key: 'self', value: o.literal(true), quoted: false});
|
||||||
|
}
|
||||||
|
if (dep.skipSelf) {
|
||||||
|
entries.push({key: 'skipSelf', value: o.literal(true), quoted: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.length > 0 ? o.literalMap(entries) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper function useful for extracting `R3DependencyMetadata` from a Render2
|
* A helper function useful for extracting `R3DependencyMetadata` from a Render2
|
||||||
* `CompileTypeMetadata` instance.
|
* `CompileTypeMetadata` instance.
|
||||||
@ -348,7 +402,7 @@ export function dependenciesFromGlobalMetadata(
|
|||||||
// Construct the dependency.
|
// Construct the dependency.
|
||||||
deps.push({
|
deps.push({
|
||||||
token,
|
token,
|
||||||
resolved,
|
attribute: null, resolved,
|
||||||
host: !!dependency.isHost,
|
host: !!dependency.isHost,
|
||||||
optional: !!dependency.isOptional,
|
optional: !!dependency.isOptional,
|
||||||
self: !!dependency.isSelf,
|
self: !!dependency.isSelf,
|
||||||
|
@ -52,6 +52,7 @@ export interface Render3ParseResult {
|
|||||||
errors: ParseError[];
|
errors: ParseError[];
|
||||||
styles: string[];
|
styles: string[];
|
||||||
styleUrls: string[];
|
styleUrls: string[];
|
||||||
|
ngContentSelectors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlAstToRender3Ast(
|
export function htmlAstToRender3Ast(
|
||||||
@ -73,6 +74,7 @@ export function htmlAstToRender3Ast(
|
|||||||
errors: allErrors,
|
errors: allErrors,
|
||||||
styleUrls: transformer.styleUrls,
|
styleUrls: transformer.styleUrls,
|
||||||
styles: transformer.styles,
|
styles: transformer.styles,
|
||||||
|
ngContentSelectors: transformer.ngContentSelectors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +82,7 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
errors: ParseError[] = [];
|
errors: ParseError[] = [];
|
||||||
styles: string[] = [];
|
styles: string[] = [];
|
||||||
styleUrls: string[] = [];
|
styleUrls: string[] = [];
|
||||||
|
ngContentSelectors: string[] = [];
|
||||||
private inI18nBlock: boolean = false;
|
private inI18nBlock: boolean = false;
|
||||||
|
|
||||||
constructor(private bindingParser: BindingParser) {}
|
constructor(private bindingParser: BindingParser) {}
|
||||||
@ -189,6 +192,8 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
const selector = preparsedElement.selectAttr;
|
const selector = preparsedElement.selectAttr;
|
||||||
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
|
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
|
||||||
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);
|
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);
|
||||||
|
|
||||||
|
this.ngContentSelectors.push(selector);
|
||||||
} else if (isTemplateElement) {
|
} else if (isTemplateElement) {
|
||||||
// `<ng-template>`
|
// `<ng-template>`
|
||||||
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
||||||
|
@ -129,6 +129,12 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
|||||||
* Parsed nodes of the template.
|
* Parsed nodes of the template.
|
||||||
*/
|
*/
|
||||||
nodes: t.Node[];
|
nodes: t.Node[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any ng-content selectors extracted from the template. Contains `null` when an ng-content
|
||||||
|
* element without selector is present.
|
||||||
|
*/
|
||||||
|
ngContentSelectors: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,7 @@ import {CONTENT_ATTR, HOST_ATTR} from '../../style_compiler';
|
|||||||
import {BindingParser} from '../../template_parser/binding_parser';
|
import {BindingParser} from '../../template_parser/binding_parser';
|
||||||
import {OutputContext, error} from '../../util';
|
import {OutputContext, error} from '../../util';
|
||||||
import {BoundEvent} from '../r3_ast';
|
import {BoundEvent} from '../r3_ast';
|
||||||
import {R3FactoryTarget, compileFactoryFunction} from '../r3_factory';
|
import {R3DependencyMetadata, R3FactoryTarget, R3ResolvedDependencyType, compileFactoryFunction} from '../r3_factory';
|
||||||
import {Identifiers as R3} from '../r3_identifiers';
|
import {Identifiers as R3} from '../r3_identifiers';
|
||||||
import {Render3ParseResult} from '../r3_template_transform';
|
import {Render3ParseResult} from '../r3_template_transform';
|
||||||
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
|
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
|
||||||
@ -124,7 +124,9 @@ export function compileDirectiveFromMetadata(
|
|||||||
addFeatures(definitionMap, meta);
|
addFeatures(definitionMap, meta);
|
||||||
const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]);
|
const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]);
|
||||||
|
|
||||||
const type = createTypeForDef(meta, R3.DirectiveDefWithMeta);
|
const typeParams = createDirectiveTypeParams(meta);
|
||||||
|
const type = o.expressionType(o.importExpr(R3.DirectiveDefWithMeta, typeParams));
|
||||||
|
|
||||||
return {expression, type};
|
return {expression, type};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +254,11 @@ export function compileComponentFromMetadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]);
|
const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]);
|
||||||
const type = createTypeForDef(meta, R3.ComponentDefWithMeta);
|
|
||||||
|
|
||||||
|
const typeParams = createDirectiveTypeParams(meta);
|
||||||
|
typeParams.push(stringArrayAsType(meta.template.ngContentSelectors));
|
||||||
|
const type = o.expressionType(o.importExpr(R3.ComponentDefWithMeta, typeParams));
|
||||||
|
|
||||||
return {expression, type};
|
return {expression, type};
|
||||||
}
|
}
|
||||||
@ -311,7 +317,7 @@ export function compileComponentFromRender2(
|
|||||||
const meta: R3ComponentMetadata = {
|
const meta: R3ComponentMetadata = {
|
||||||
...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector),
|
...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector),
|
||||||
selector: component.selector,
|
selector: component.selector,
|
||||||
template: {nodes: render3Ast.nodes},
|
template: {nodes: render3Ast.nodes, ngContentSelectors: render3Ast.ngContentSelectors},
|
||||||
directives: [],
|
directives: [],
|
||||||
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
|
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
|
||||||
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
|
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
|
||||||
@ -470,24 +476,24 @@ function stringMapAsType(map: {[key: string]: string | string[]}): o.Type {
|
|||||||
return o.expressionType(o.literalMap(mapValues));
|
return o.expressionType(o.literalMap(mapValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringArrayAsType(arr: string[]): o.Type {
|
function stringArrayAsType(arr: ReadonlyArray<string|null>): o.Type {
|
||||||
return arr.length > 0 ? o.expressionType(o.literalArr(arr.map(value => o.literal(value)))) :
|
return arr.length > 0 ? o.expressionType(o.literalArr(arr.map(value => o.literal(value)))) :
|
||||||
o.NONE_TYPE;
|
o.NONE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTypeForDef(meta: R3DirectiveMetadata, typeBase: o.ExternalReference): o.Type {
|
function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] {
|
||||||
// On the type side, remove newlines from the selector as it will need to fit into a TypeScript
|
// On the type side, remove newlines from the selector as it will need to fit into a TypeScript
|
||||||
// string literal, which must be on one line.
|
// string literal, which must be on one line.
|
||||||
const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null;
|
const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null;
|
||||||
|
|
||||||
return o.expressionType(o.importExpr(typeBase, [
|
return [
|
||||||
typeWithParameters(meta.type.type, meta.typeArgumentCount),
|
typeWithParameters(meta.type.type, meta.typeArgumentCount),
|
||||||
selectorForType !== null ? stringAsType(selectorForType) : o.NONE_TYPE,
|
selectorForType !== null ? stringAsType(selectorForType) : o.NONE_TYPE,
|
||||||
meta.exportAs !== null ? stringArrayAsType(meta.exportAs) : o.NONE_TYPE,
|
meta.exportAs !== null ? stringArrayAsType(meta.exportAs) : o.NONE_TYPE,
|
||||||
stringMapAsType(meta.inputs),
|
stringMapAsType(meta.inputs),
|
||||||
stringMapAsType(meta.outputs),
|
stringMapAsType(meta.outputs),
|
||||||
stringArrayAsType(meta.queries.map(q => q.propertyName)),
|
stringArrayAsType(meta.queries.map(q => q.propertyName)),
|
||||||
]));
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define and update any view queries
|
// Define and update any view queries
|
||||||
|
@ -1983,8 +1983,13 @@ export interface ParseTemplateOptions {
|
|||||||
* @param options options to modify how the template is parsed
|
* @param options options to modify how the template is parsed
|
||||||
*/
|
*/
|
||||||
export function parseTemplate(
|
export function parseTemplate(
|
||||||
template: string, templateUrl: string, options: ParseTemplateOptions = {}):
|
template: string, templateUrl: string, options: ParseTemplateOptions = {}): {
|
||||||
{errors?: ParseError[], nodes: t.Node[], styleUrls: string[], styles: string[]} {
|
errors?: ParseError[],
|
||||||
|
nodes: t.Node[],
|
||||||
|
styleUrls: string[],
|
||||||
|
styles: string[],
|
||||||
|
ngContentSelectors: string[]
|
||||||
|
} {
|
||||||
const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options;
|
const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options;
|
||||||
const bindingParser = makeBindingParser(interpolationConfig);
|
const bindingParser = makeBindingParser(interpolationConfig);
|
||||||
const htmlParser = new HtmlParser();
|
const htmlParser = new HtmlParser();
|
||||||
@ -1993,7 +1998,13 @@ export function parseTemplate(
|
|||||||
{leadingTriviaChars: LEADING_TRIVIA_CHARS, ...options, tokenizeExpansionForms: true});
|
{leadingTriviaChars: LEADING_TRIVIA_CHARS, ...options, tokenizeExpansionForms: true});
|
||||||
|
|
||||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||||
return {errors: parseResult.errors, nodes: [], styleUrls: [], styles: []};
|
return {
|
||||||
|
errors: parseResult.errors,
|
||||||
|
nodes: [],
|
||||||
|
styleUrls: [],
|
||||||
|
styles: [],
|
||||||
|
ngContentSelectors: []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootNodes: html.Node[] = parseResult.rootNodes;
|
let rootNodes: html.Node[] = parseResult.rootNodes;
|
||||||
@ -2020,12 +2031,13 @@ export function parseTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {nodes, errors, styleUrls, styles} = htmlAstToRender3Ast(rootNodes, bindingParser);
|
const {nodes, errors, styleUrls, styles, ngContentSelectors} =
|
||||||
|
htmlAstToRender3Ast(rootNodes, bindingParser);
|
||||||
if (errors && errors.length > 0) {
|
if (errors && errors.length > 0) {
|
||||||
return {errors, nodes: [], styleUrls: [], styles: []};
|
return {errors, nodes: [], styleUrls: [], styles: [], ngContentSelectors: []};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {nodes, styleUrls, styles};
|
return {nodes, styleUrls, styles, ngContentSelectors};
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementRegistry = new DomElementSchemaRegistry();
|
const elementRegistry = new DomElementSchemaRegistry();
|
||||||
|
@ -145,8 +145,9 @@ export class BindingParser {
|
|||||||
binding.value ? moveParseSourceSpan(sourceSpan, binding.value.span) : undefined;
|
binding.value ? moveParseSourceSpan(sourceSpan, binding.value.span) : undefined;
|
||||||
targetVars.push(new ParsedVariable(key, value, bindingSpan, keySpan, valueSpan));
|
targetVars.push(new ParsedVariable(key, value, bindingSpan, keySpan, valueSpan));
|
||||||
} else if (binding.value) {
|
} else if (binding.value) {
|
||||||
|
const valueSpan = moveParseSourceSpan(sourceSpan, binding.value.ast.sourceSpan);
|
||||||
this._parsePropertyAst(
|
this._parsePropertyAst(
|
||||||
key, binding.value, sourceSpan, undefined, targetMatchableAttrs, targetProps);
|
key, binding.value, sourceSpan, valueSpan, targetMatchableAttrs, targetProps);
|
||||||
} else {
|
} else {
|
||||||
targetMatchableAttrs.push([key, '']);
|
targetMatchableAttrs.push([key, '']);
|
||||||
this.parseLiteralAttr(
|
this.parseLiteralAttr(
|
||||||
|
@ -313,6 +313,26 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
|||||||
expect(p.errors.length).toEqual(0);
|
expect(p.errors.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should support ICU expressions with cases that contain any character except '}'`,
|
||||||
|
() => {
|
||||||
|
const p = parser.parse(
|
||||||
|
`{a, select, b {foo} % bar {% bar}}`, 'TestComp', {tokenizeExpansionForms: true});
|
||||||
|
expect(p.errors.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when expansion case is not properly closed', () => {
|
||||||
|
const p = parser.parse(
|
||||||
|
`{a, select, b {foo} % { bar {% bar}}`, 'TestComp', {tokenizeExpansionForms: true});
|
||||||
|
expect(humanizeErrors(p.errors)).toEqual([
|
||||||
|
[
|
||||||
|
6,
|
||||||
|
'Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ \'{\' }}") to escape it.)',
|
||||||
|
'0:36'
|
||||||
|
],
|
||||||
|
[null, 'Invalid ICU message. Missing \'}\'.', '0:22']
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should error when expansion case is not closed', () => {
|
it('should error when expansion case is not closed', () => {
|
||||||
const p = parser.parse(
|
const p = parser.parse(
|
||||||
`{messages.length, plural, =0 {one`, 'TestComp', {tokenizeExpansionForms: true});
|
`{messages.length, plural, =0 {one`, 'TestComp', {tokenizeExpansionForms: true});
|
||||||
|
@ -232,8 +232,8 @@ describe('R3 AST source spans', () => {
|
|||||||
expectFromHtml('<div *ngFor="let item of items"></div>').toEqual([
|
expectFromHtml('<div *ngFor="let item of items"></div>').toEqual([
|
||||||
['Template', '0:32', '0:32', '32:38'],
|
['Template', '0:32', '0:32', '32:38'],
|
||||||
['TextAttribute', '5:31', '<empty>'],
|
['TextAttribute', '5:31', '<empty>'],
|
||||||
['BoundAttribute', '5:31', '<empty>'],
|
['BoundAttribute', '5:31', '25:30'], // *ngFor="let item of items" -> items
|
||||||
['Variable', '13:22', '<empty>'], // let item
|
['Variable', '13:22', '<empty>'], // let item
|
||||||
['Element', '0:38', '0:32', '32:38'],
|
['Element', '0:38', '0:32', '32:38'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -245,8 +245,8 @@ describe('R3 AST source spans', () => {
|
|||||||
// </ng-template>
|
// </ng-template>
|
||||||
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
|
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
|
||||||
['Template', '0:28', '0:28', '28:34'],
|
['Template', '0:28', '0:28', '28:34'],
|
||||||
['BoundAttribute', '5:27', '<empty>'],
|
['BoundAttribute', '5:27', '13:17'], // ngFor="item of items" -> item
|
||||||
['BoundAttribute', '5:27', '<empty>'],
|
['BoundAttribute', '5:27', '21:26'], // ngFor="item of items" -> items
|
||||||
['Element', '0:34', '0:28', '28:34'],
|
['Element', '0:34', '0:28', '28:34'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -263,8 +263,8 @@ describe('R3 AST source spans', () => {
|
|||||||
it('is correct for variables via as ...', () => {
|
it('is correct for variables via as ...', () => {
|
||||||
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
|
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
|
||||||
['Template', '0:27', '0:27', '27:33'],
|
['Template', '0:27', '0:27', '27:33'],
|
||||||
['BoundAttribute', '5:26', '<empty>'],
|
['BoundAttribute', '5:26', '12:16'], // ngIf="expr as local" -> expr
|
||||||
['Variable', '6:25', '6:10'], // ngIf="expr as local -> ngIf
|
['Variable', '6:25', '6:10'], // ngIf="expr as local -> ngIf
|
||||||
['Element', '0:33', '0:27', '27:33'],
|
['Element', '0:33', '0:27', '27:33'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -89,6 +89,39 @@ export interface DirectiveType<T> extends Type<T> {
|
|||||||
*/
|
*/
|
||||||
export interface PipeType<T> extends Type<T> { ɵpipe: never; }
|
export interface PipeType<T> extends Type<T> { ɵpipe: never; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object literal of this type is used to represent the metadata of a constructor dependency.
|
||||||
|
* The type itself is never referred to from generated code.
|
||||||
|
*/
|
||||||
|
export type CtorDependency = {
|
||||||
|
/**
|
||||||
|
* If an `@Attribute` decorator is used, this represents the injected attribute's name. If the
|
||||||
|
* attribute name is a dynamic expression instead of a string literal, this will be the unknown
|
||||||
|
* type.
|
||||||
|
*/
|
||||||
|
attribute?: string | unknown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `@Optional()` is used, this key is set to true.
|
||||||
|
*/
|
||||||
|
optional?: true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `@Host` is used, this key is set to true.
|
||||||
|
*/
|
||||||
|
host?: true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `@Self` is used, this key is set to true.
|
||||||
|
*/
|
||||||
|
self?: true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `@SkipSelf` is used, this key is set to true.
|
||||||
|
*/
|
||||||
|
skipSelf?: true;
|
||||||
|
} | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @codeGenApi
|
* @codeGenApi
|
||||||
*/
|
*/
|
||||||
@ -236,12 +269,13 @@ export interface DirectiveDef<T> {
|
|||||||
*/
|
*/
|
||||||
export type ɵɵComponentDefWithMeta<
|
export type ɵɵComponentDefWithMeta<
|
||||||
T, Selector extends String, ExportAs extends string[], InputMap extends{[key: string]: string},
|
T, Selector extends String, ExportAs extends string[], InputMap extends{[key: string]: string},
|
||||||
OutputMap extends{[key: string]: string}, QueryFields extends string[]> = ComponentDef<T>;
|
OutputMap extends{[key: string]: string}, QueryFields extends string[],
|
||||||
|
NgContentSelectors extends string[]> = ComponentDef<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @codeGenApi
|
* @codeGenApi
|
||||||
*/
|
*/
|
||||||
export type ɵɵFactoryDef<T> = () => T;
|
export type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime link information for Components.
|
* Runtime link information for Components.
|
||||||
|
@ -69,19 +69,23 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
|
|||||||
throw new Error(error.join('\n'));
|
throw new Error(error.join('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const jitOptions = getJitOptions();
|
// This const was called `jitOptions` previously but had to be renamed to `options` because
|
||||||
|
// of a bug with Terser that caused optimized JIT builds to throw a `ReferenceError`.
|
||||||
|
// This bug was investigated in https://github.com/angular/angular-cli/issues/17264.
|
||||||
|
// We should not rename it back until https://github.com/terser/terser/issues/615 is fixed.
|
||||||
|
const options = getJitOptions();
|
||||||
let preserveWhitespaces = metadata.preserveWhitespaces;
|
let preserveWhitespaces = metadata.preserveWhitespaces;
|
||||||
if (preserveWhitespaces === undefined) {
|
if (preserveWhitespaces === undefined) {
|
||||||
if (jitOptions !== null && jitOptions.preserveWhitespaces !== undefined) {
|
if (options !== null && options.preserveWhitespaces !== undefined) {
|
||||||
preserveWhitespaces = jitOptions.preserveWhitespaces;
|
preserveWhitespaces = options.preserveWhitespaces;
|
||||||
} else {
|
} else {
|
||||||
preserveWhitespaces = false;
|
preserveWhitespaces = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let encapsulation = metadata.encapsulation;
|
let encapsulation = metadata.encapsulation;
|
||||||
if (encapsulation === undefined) {
|
if (encapsulation === undefined) {
|
||||||
if (jitOptions !== null && jitOptions.defaultEncapsulation !== undefined) {
|
if (options !== null && options.defaultEncapsulation !== undefined) {
|
||||||
encapsulation = jitOptions.defaultEncapsulation;
|
encapsulation = options.defaultEncapsulation;
|
||||||
} else {
|
} else {
|
||||||
encapsulation = ViewEncapsulation.Emulated;
|
encapsulation = ViewEncapsulation.Emulated;
|
||||||
}
|
}
|
||||||
|
@ -87,15 +87,6 @@ jasmine_node_test(
|
|||||||
|
|
||||||
karma_web_test_suite(
|
karma_web_test_suite(
|
||||||
name = "test_web",
|
name = "test_web",
|
||||||
tags = [
|
|
||||||
# FIXME: fix on saucelabs
|
|
||||||
# IE 11.0.0 (Windows 8.1.0.0) ivy NgModule providers should throw when the aliased provider does not exist FAILED
|
|
||||||
# Error: Expected function to throw an exception with message 'R3InjectorError(SomeModule)[car -> SportsCar]:
|
|
||||||
# NullInjectorError: No provider for Car!', but it threw an exception with message 'R3InjectorError(SomeModule)[car -> Car]:
|
|
||||||
# NullInjectorError: No provider for Car!'.
|
|
||||||
# at <Jasmine>
|
|
||||||
"fixme-saucelabs-ivy",
|
|
||||||
],
|
|
||||||
deps = [
|
deps = [
|
||||||
":test_lib",
|
":test_lib",
|
||||||
],
|
],
|
||||||
|
@ -794,7 +794,7 @@ function declareTests(config?: {useJit: boolean}) {
|
|||||||
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
|
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
|
||||||
let errorMsg = `NullInjectorError: No provider for ${stringify(SportsCar)}!`;
|
let errorMsg = `NullInjectorError: No provider for ${stringify(SportsCar)}!`;
|
||||||
if (ivyEnabled) {
|
if (ivyEnabled) {
|
||||||
errorMsg = `R3InjectorError(SomeModule)[car -> SportsCar]: \n ` + errorMsg;
|
errorMsg = `R3InjectorError(SomeModule)[car -> ${stringify(SportsCar)}]: \n ` + errorMsg;
|
||||||
}
|
}
|
||||||
expect(() => injector.get('car')).toThrowError(errorMsg);
|
expect(() => injector.get('car')).toThrowError(errorMsg);
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import {ɵɵComponentDefWithMeta, ɵɵPipeDefWithMeta as PipeDefWithMeta} from '@angular/core';
|
import {ɵɵComponentDefWithMeta, ɵɵPipeDefWithMeta as PipeDefWithMeta} from '@angular/core';
|
||||||
|
|
||||||
declare class SuperComponent {
|
declare class SuperComponent {
|
||||||
static ɵcmp: ɵɵComponentDefWithMeta<SuperComponent, '[super]', never, {}, {}, never>;
|
static ɵcmp: ɵɵComponentDefWithMeta<SuperComponent, '[super]', never, {}, {}, never, never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class SubComponent extends SuperComponent {
|
declare class SubComponent extends SuperComponent {
|
||||||
@ -18,7 +18,7 @@ declare class SubComponent extends SuperComponent {
|
|||||||
// would produce type errors when the "strictFunctionTypes" option is enabled.
|
// would produce type errors when the "strictFunctionTypes" option is enabled.
|
||||||
onlyInSubtype: string;
|
onlyInSubtype: string;
|
||||||
|
|
||||||
static ɵcmp: ɵɵComponentDefWithMeta<SubComponent, '[sub]', never, {}, {}, never>;
|
static ɵcmp: ɵɵComponentDefWithMeta<SubComponent, '[sub]', never, {}, {}, never, never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class SuperPipe { static ɵpipe: PipeDefWithMeta<SuperPipe, 'super'>; }
|
declare class SuperPipe { static ɵpipe: PipeDefWithMeta<SuperPipe, 'super'>; }
|
||||||
|
@ -66,8 +66,11 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
|||||||
/** Initial input values that were set before the component was created. */
|
/** Initial input values that were set before the component was created. */
|
||||||
private readonly initialInputValues = new Map<string, any>();
|
private readonly initialInputValues = new Map<string, any>();
|
||||||
|
|
||||||
/** Set of inputs that were not initially set when the component was created. */
|
/**
|
||||||
private readonly uninitializedInputs = new Set<string>();
|
* Set of component inputs that have not yet changed, i.e. for which `ngOnChanges()` has not
|
||||||
|
* fired. (This is used to determine the value of `fistChange` in `SimpleChange` instances.)
|
||||||
|
*/
|
||||||
|
private readonly unchangedInputs = new Set<string>();
|
||||||
|
|
||||||
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
||||||
|
|
||||||
@ -130,7 +133,11 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strictEquals(value, this.getInputValue(property))) {
|
// Ignore the value if it is strictly equal to the current value, except if it is `undefined`
|
||||||
|
// and this is the first change to the value (because an explicit `undefined` _is_ strictly
|
||||||
|
// equal to not having a value set at all, but we still need to record this as a change).
|
||||||
|
if (strictEquals(value, this.getInputValue(property)) &&
|
||||||
|
!((value === undefined) && this.unchangedInputs.has(property))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,12 +171,16 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
|||||||
/** Set any stored initial inputs on the component's properties. */
|
/** Set any stored initial inputs on the component's properties. */
|
||||||
protected initializeInputs(): void {
|
protected initializeInputs(): void {
|
||||||
this.componentFactory.inputs.forEach(({propName}) => {
|
this.componentFactory.inputs.forEach(({propName}) => {
|
||||||
|
if (this.implementsOnChanges) {
|
||||||
|
// If the component implements `ngOnChanges()`, keep track of which inputs have never
|
||||||
|
// changed so far.
|
||||||
|
this.unchangedInputs.add(propName);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.initialInputValues.has(propName)) {
|
if (this.initialInputValues.has(propName)) {
|
||||||
|
// Call `setInputValue()` now that the component has been instantiated to update its
|
||||||
|
// properties and fire `ngOnChanges()`.
|
||||||
this.setInputValue(propName, this.initialInputValues.get(propName));
|
this.setInputValue(propName, this.initialInputValues.get(propName));
|
||||||
} else {
|
|
||||||
// Keep track of inputs that were not initialized in case we need to know this for
|
|
||||||
// calling ngOnChanges with SimpleChanges
|
|
||||||
this.uninitializedInputs.add(propName);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -235,8 +246,8 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstChange = this.uninitializedInputs.has(property);
|
const isFirstChange = this.unchangedInputs.has(property);
|
||||||
this.uninitializedInputs.delete(property);
|
this.unchangedInputs.delete(property);
|
||||||
|
|
||||||
const previousValue = isFirstChange ? undefined : this.getInputValue(property);
|
const previousValue = isFirstChange ? undefined : this.getInputValue(property);
|
||||||
this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange);
|
this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange);
|
||||||
|
@ -93,22 +93,25 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
|||||||
|
|
||||||
it('should call ngOnChanges with the change', () => {
|
it('should call ngOnChanges with the change', () => {
|
||||||
expectSimpleChanges(componentRef.instance.simpleChanges[0], {
|
expectSimpleChanges(componentRef.instance.simpleChanges[0], {
|
||||||
fooFoo: new SimpleChange(undefined, 'fooFoo-1', false),
|
fooFoo: new SimpleChange(undefined, 'fooFoo-1', true),
|
||||||
falsyNull: new SimpleChange(undefined, null, false),
|
falsyUndefined: new SimpleChange(undefined, undefined, true),
|
||||||
falsyEmpty: new SimpleChange(undefined, '', false),
|
falsyNull: new SimpleChange(undefined, null, true),
|
||||||
falsyFalse: new SimpleChange(undefined, false, false),
|
falsyEmpty: new SimpleChange(undefined, '', true),
|
||||||
falsyZero: new SimpleChange(undefined, 0, false),
|
falsyFalse: new SimpleChange(undefined, false, true),
|
||||||
|
falsyZero: new SimpleChange(undefined, 0, true),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call ngOnChanges with proper firstChange value', fakeAsync(() => {
|
it('should call ngOnChanges with proper firstChange value', fakeAsync(() => {
|
||||||
strategy.setInputValue('falsyUndefined', 'notanymore');
|
strategy.setInputValue('fooFoo', 'fooFoo-2');
|
||||||
strategy.setInputValue('barBar', 'barBar-1');
|
strategy.setInputValue('barBar', 'barBar-1');
|
||||||
|
strategy.setInputValue('falsyUndefined', 'notanymore');
|
||||||
tick(16); // scheduler waits 16ms if RAF is unavailable
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
||||||
(strategy as any).detectChanges();
|
(strategy as any).detectChanges();
|
||||||
expectSimpleChanges(componentRef.instance.simpleChanges[1], {
|
expectSimpleChanges(componentRef.instance.simpleChanges[1], {
|
||||||
falsyUndefined: new SimpleChange(undefined, 'notanymore', false),
|
fooFoo: new SimpleChange('fooFoo-1', 'fooFoo-2', false),
|
||||||
barBar: new SimpleChange(undefined, 'barBar-1', true),
|
barBar: new SimpleChange(undefined, 'barBar-1', true),
|
||||||
|
falsyUndefined: new SimpleChange(undefined, 'notanymore', false),
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@ -296,9 +299,9 @@ function expectSimpleChanges(actual: SimpleChanges, expected: SimpleChanges) {
|
|||||||
Object.keys(expected).forEach(key => {
|
Object.keys(expected).forEach(key => {
|
||||||
expect(actual[key]).toBeTruthy(`Change should have included key ${key}`);
|
expect(actual[key]).toBeTruthy(`Change should have included key ${key}`);
|
||||||
if (actual[key]) {
|
if (actual[key]) {
|
||||||
expect(actual[key].previousValue).toBe(expected[key].previousValue);
|
expect(actual[key].previousValue).toBe(expected[key].previousValue, `${key}.previousValue`);
|
||||||
expect(actual[key].currentValue).toBe(expected[key].currentValue);
|
expect(actual[key].currentValue).toBe(expected[key].currentValue, `${key}.currentValue`);
|
||||||
expect(actual[key].firstChange).toBe(expected[key].firstChange);
|
expect(actual[key].firstChange).toBe(expected[key].firstChange, `${key}.firstChange`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,206 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google Inc. 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
|
|
||||||
*/
|
|
||||||
/* tslint:disable:no-console */
|
|
||||||
const parseYaml = require('yaml').parse;
|
|
||||||
const readFileSync = require('fs').readFileSync;
|
|
||||||
const Minimatch = require('minimatch').Minimatch;
|
|
||||||
const {exec, set, cd} = require('shelljs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Exit early on shelljs errors
|
|
||||||
set('-e');
|
|
||||||
|
|
||||||
// Regex Matcher for contains_any_globs conditions
|
|
||||||
const CONTAINS_ANY_GLOBS_REGEX = /^'([^']+)',?$/;
|
|
||||||
|
|
||||||
// Full path of the angular project directory
|
|
||||||
const ANGULAR_PROJECT_DIR = path.resolve(__dirname, '../..');
|
|
||||||
// Change to the Angular project directory
|
|
||||||
cd(ANGULAR_PROJECT_DIR);
|
|
||||||
|
|
||||||
// Whether to log verbosely
|
|
||||||
const VERBOSE_MODE = process.argv.includes('-v');
|
|
||||||
// Full path to PullApprove config file
|
|
||||||
const PULL_APPROVE_YAML_PATH = path.resolve(ANGULAR_PROJECT_DIR, '.pullapprove.yml');
|
|
||||||
// All relative path file names in the git repo, this is retrieved using git rather
|
|
||||||
// that a glob so that we only get files that are checked in, ignoring things like
|
|
||||||
// node_modules, .bazelrc.user, etc
|
|
||||||
const ALL_FILES = exec('git ls-tree --full-tree -r --name-only HEAD', {silent: true})
|
|
||||||
.trim()
|
|
||||||
.split('\n')
|
|
||||||
.filter(_ => _);
|
|
||||||
if (!ALL_FILES.length) {
|
|
||||||
console.error(
|
|
||||||
`No files were found to be in the git tree, did you run this command from \n` +
|
|
||||||
`inside the angular repository?`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets the glob matching information from each group's condition. */
|
|
||||||
function getGlobMatchersFromCondition(groupName, condition) {
|
|
||||||
const trimmedCondition = condition.trim();
|
|
||||||
const globMatchers = [];
|
|
||||||
const badConditionLines = [];
|
|
||||||
|
|
||||||
// If the condition starts with contains_any_globs, evaluate all of the globs
|
|
||||||
if (trimmedCondition.startsWith('contains_any_globs')) {
|
|
||||||
trimmedCondition.split('\n')
|
|
||||||
.slice(1, -1)
|
|
||||||
.map(glob => {
|
|
||||||
const trimmedGlob = glob.trim();
|
|
||||||
const match = trimmedGlob.match(CONTAINS_ANY_GLOBS_REGEX);
|
|
||||||
if (!match) {
|
|
||||||
badConditionLines.push(trimmedGlob);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return match[1];
|
|
||||||
})
|
|
||||||
.filter(globString => !!globString)
|
|
||||||
.forEach(globString => globMatchers.push({
|
|
||||||
group: groupName,
|
|
||||||
glob: globString,
|
|
||||||
matcher: new Minimatch(globString, {dot: true}),
|
|
||||||
matchCount: 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return [globMatchers, badConditionLines];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create logs for each review group. */
|
|
||||||
function logGroups(groups) {
|
|
||||||
Array.from(groups.entries()).sort().forEach(([groupName, globs]) => {
|
|
||||||
console.groupCollapsed(groupName);
|
|
||||||
Array.from(globs.values())
|
|
||||||
.sort((a, b) => b.matchCount - a.matchCount)
|
|
||||||
.forEach(glob => console.log(`${glob.glob} - ${glob.matchCount}`));
|
|
||||||
console.groupEnd();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Logs a header within a text drawn box. */
|
|
||||||
function logHeader(...params) {
|
|
||||||
const totalWidth = 80;
|
|
||||||
const fillWidth = totalWidth - 2;
|
|
||||||
const headerText = params.join(' ').substr(0, fillWidth);
|
|
||||||
const leftSpace = Math.ceil((fillWidth - headerText.length) / 2);
|
|
||||||
const rightSpace = fillWidth - leftSpace - headerText.length;
|
|
||||||
const fill = (count, content) => content.repeat(count);
|
|
||||||
|
|
||||||
console.log(`┌${fill(fillWidth, '─')}┐`);
|
|
||||||
console.log(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`);
|
|
||||||
console.log(`└${fill(fillWidth, '─')}┘`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Runs the pull approve verification check on provided files. */
|
|
||||||
function runVerification(files) {
|
|
||||||
// All of the globs created for each group's conditions.
|
|
||||||
const allGlobs = [];
|
|
||||||
// The pull approve config file.
|
|
||||||
const pullApprove = readFileSync(PULL_APPROVE_YAML_PATH, {encoding: 'utf8'});
|
|
||||||
// All of the PullApprove groups, parsed from the PullApprove yaml file.
|
|
||||||
const parsedPullApproveGroups = parseYaml(pullApprove).groups;
|
|
||||||
// All files which were found to match a condition in PullApprove.
|
|
||||||
const matchedFiles = new Set();
|
|
||||||
// All files which were not found to match a condition in PullApprove.
|
|
||||||
const unmatchedFiles = new Set();
|
|
||||||
// All PullApprove groups which matched at least one file.
|
|
||||||
const matchedGroups = new Map();
|
|
||||||
// All PullApprove groups which did not match at least one file.
|
|
||||||
const unmatchedGroups = new Map();
|
|
||||||
// All condition lines which were not able to be correctly parsed, by group.
|
|
||||||
const badConditionLinesByGroup = new Map();
|
|
||||||
// Total number of condition lines which were not able to be correctly parsed.
|
|
||||||
let badConditionLineCount = 0;
|
|
||||||
|
|
||||||
// Get all of the globs from the PullApprove group conditions.
|
|
||||||
Object.entries(parsedPullApproveGroups).map(([groupName, group]) => {
|
|
||||||
for (const condition of group.conditions) {
|
|
||||||
const [matchers, badConditions] = getGlobMatchersFromCondition(groupName, condition);
|
|
||||||
if (badConditions.length) {
|
|
||||||
badConditionLinesByGroup.set(groupName, badConditions);
|
|
||||||
badConditionLineCount += badConditions.length;
|
|
||||||
}
|
|
||||||
allGlobs.push(...matchers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (badConditionLineCount) {
|
|
||||||
console.log(`Discovered ${badConditionLineCount} parsing errors in PullApprove conditions`);
|
|
||||||
console.log(`Attempted parsing using: ${CONTAINS_ANY_GLOBS_REGEX}`);
|
|
||||||
console.log();
|
|
||||||
console.log(`Unable to properly parse the following line(s) by group:`);
|
|
||||||
for (const [groupName, badConditionLines] of badConditionLinesByGroup.entries()) {
|
|
||||||
console.log(`- ${groupName}:`);
|
|
||||||
badConditionLines.forEach(line => console.log(` ${line}`));
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
console.log(
|
|
||||||
`Please correct the invalid conditions, before PullApprove verification can be completed`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each file for if it is matched by a PullApprove condition.
|
|
||||||
for (let file of files) {
|
|
||||||
const matched = allGlobs.filter(glob => glob.matcher.match(file));
|
|
||||||
matched.length ? matchedFiles.add(file) : unmatchedFiles.add(file);
|
|
||||||
matched.forEach(glob => glob.matchCount++);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add each glob for each group to a map either matched or unmatched.
|
|
||||||
allGlobs.forEach(glob => {
|
|
||||||
const groups = glob.matchCount ? matchedGroups : unmatchedGroups;
|
|
||||||
const globs = groups.get(glob.group) || new Map();
|
|
||||||
// Set the globs map in the groups map
|
|
||||||
groups.set(glob.group, globs);
|
|
||||||
// Set the glob in the globs map
|
|
||||||
globs.set(glob.glob, glob);
|
|
||||||
});
|
|
||||||
|
|
||||||
// PullApprove is considered verified if no files or groups are found to be unsed.
|
|
||||||
const verificationSucceeded = !(unmatchedFiles.size || unmatchedGroups.size);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overall result
|
|
||||||
*/
|
|
||||||
logHeader('Result');
|
|
||||||
if (verificationSucceeded) {
|
|
||||||
console.log('PullApprove verification succeeded!');
|
|
||||||
} else {
|
|
||||||
console.log(`PullApprove verification failed.\n`);
|
|
||||||
console.log(`Please update '.pullapprove.yml' to ensure that all necessary`);
|
|
||||||
console.log(`files/directories have owners and all patterns that appear in`);
|
|
||||||
console.log(`the file correspond to actual files/directories in the repo.`);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* File by file Summary
|
|
||||||
*/
|
|
||||||
logHeader('PullApprove file match results');
|
|
||||||
console.groupCollapsed(`Matched Files (${matchedFiles.size} files)`);
|
|
||||||
VERBOSE_MODE && matchedFiles.forEach(file => console.log(file));
|
|
||||||
console.groupEnd();
|
|
||||||
console.groupCollapsed(`Unmatched Files (${unmatchedFiles.size} files)`);
|
|
||||||
unmatchedFiles.forEach(file => console.log(file));
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group by group Summary
|
|
||||||
*/
|
|
||||||
logHeader('PullApprove group matches');
|
|
||||||
console.groupCollapsed(`Matched Groups (${matchedGroups.size} groups)`);
|
|
||||||
VERBOSE_MODE && logGroups(matchedGroups);
|
|
||||||
console.groupEnd();
|
|
||||||
console.groupCollapsed(`Unmatched Groups (${unmatchedGroups.size} groups)`);
|
|
||||||
logGroups(unmatchedGroups);
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
// Provide correct exit code based on verification success.
|
|
||||||
process.exit(verificationSucceeded ? 0 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
runVerification(ALL_FILES);
|
|
@ -153,6 +153,10 @@
|
|||||||
universal-analytics "^0.4.20"
|
universal-analytics "^0.4.20"
|
||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
|
|
||||||
|
"@angular/dev-infra-private@angular/dev-infra-private-builds#3724a71":
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://codeload.github.com/angular/dev-infra-private-builds/tar.gz/3724a71047361d85f4131d990f00a5aecdbc3ddc"
|
||||||
|
|
||||||
"@babel/code-frame@^7.0.0":
|
"@babel/code-frame@^7.0.0":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
|
||||||
|
Reference in New Issue
Block a user