Compare commits
18 Commits
fix-307
...
9.1.0-rc.1
Author | SHA1 | Date | |
---|---|---|---|
5357e643b3 | |||
f71d132f7c | |||
ba3edda230 | |||
0767d37c07 | |||
8ba24578bc | |||
133a97ad67 | |||
4e67a3ab3f | |||
377f0010fc | |||
6e09129e4c | |||
d80e51a6b1 | |||
feb66b00da | |||
cb19eac105 | |||
6e0564ade6 | |||
05eeb7d279 | |||
2ce5fa3cce | |||
e140cdcb34 | |||
14b2db1d43 | |||
2afc7e982e |
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,3 +1,16 @@
|
|||||||
|
<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>
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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 1bc653bac",
|
||||||
"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; }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "angular-srcs",
|
"name": "angular-srcs",
|
||||||
"version": "9.1.0-rc.0",
|
"version": "9.1.0-rc.1",
|
||||||
"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",
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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 {
|
||||||
|
@ -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'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user