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>
|
||||
# [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 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(lamp.getAttribute('src')).toContain('lamp');
|
||||
|
@ -12,7 +12,7 @@ export class AppComponent {
|
||||
|
||||
currentCustomer = 'Maria';
|
||||
title = 'Featured product:';
|
||||
itemImageUrl = '../assets/pottedPlant.png';
|
||||
itemImageUrl = '../assets/potted-plant.png';
|
||||
|
||||
recommended = 'You might also like:';
|
||||
itemImageUrl2 = '../assets/lamp.png';
|
||||
|
@ -46,7 +46,7 @@
|
||||
|
||||
<h3>Pass objects:</h3>
|
||||
<!-- #docregion pass-object -->
|
||||
<app-list-item [items]="currentItem"></app-list-item>
|
||||
<app-item-list [items]="currentItems"></app-item-list>
|
||||
<!-- #enddocregion pass-object -->
|
||||
|
||||
<hr />
|
||||
|
@ -15,7 +15,7 @@ export class AppComponent {
|
||||
// #enddocregion parent-data-type
|
||||
|
||||
// #docregion pass-object
|
||||
currentItem = [{
|
||||
currentItems = [{
|
||||
id: 21,
|
||||
name: 'phone'
|
||||
}];
|
||||
|
@ -4,7 +4,7 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.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';
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { StringInitComponent } from './string-init/string-init.component';
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ItemDetailComponent,
|
||||
ListItemComponent,
|
||||
ItemListComponent,
|
||||
StringInitComponent
|
||||
],
|
||||
imports: [
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ListItemComponent } from './list-item.component';
|
||||
import { ItemListComponent } from './item-list.component';
|
||||
|
||||
describe('ItemListComponent', () => {
|
||||
let component: ListItemComponent;
|
||||
let fixture: ComponentFixture<ListItemComponent>;
|
||||
let component: ItemListComponent;
|
||||
let fixture: ComponentFixture<ItemListComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ListItemComponent ]
|
||||
declarations: [ ItemListComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ListItemComponent);
|
||||
fixture = TestBed.createComponent(ItemListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@ -3,11 +3,11 @@ import { ITEMS } from '../mock-items';
|
||||
import { Item } from '../item';
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-item',
|
||||
templateUrl: './list-item.component.html',
|
||||
styleUrls: ['./list-item.component.css']
|
||||
selector: 'app-item-list',
|
||||
templateUrl: './item-list.component.html',
|
||||
styleUrls: ['./item-list.component.css']
|
||||
})
|
||||
export class ListItemComponent {
|
||||
export class ItemListComponent {
|
||||
listItems = ITEMS;
|
||||
// #docregion item-input
|
||||
@Input() items: Item[];
|
@ -153,7 +153,7 @@ It marks that `<li>` element (and its children) as the "repeater template":
|
||||
<div class="alert is-important">
|
||||
|
||||
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>
|
||||
|
||||
|
@ -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.
|
||||
|
||||
[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.
|
||||
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
|
||||
|
||||
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>
|
||||
<tr>
|
||||
@ -94,11 +94,7 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
||||
<td>Supported natively.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Opera</td>
|
||||
<td>Supported natively.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Safari</td>
|
||||
<td>Edge (Chromium-based)</td>
|
||||
<td>Supported natively.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -106,10 +102,12 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
|
||||
<td>Supported natively.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Edge</td>
|
||||
<td>Working on an implementation. <br>
|
||||
|
||||
</td>
|
||||
<td>Opera</td>
|
||||
<td>Supported natively.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Safari</td>
|
||||
<td>Supported natively.</td>
|
||||
</tr>
|
||||
</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 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>
|
||||
|
||||
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`.
|
||||
|
||||
@ -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>
|
||||
|
||||
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
|
||||
(`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
|
||||
`ListItemComponent` gets its definition of an `item`.
|
||||
(`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
|
||||
`ItemListComponent` gets its definition of an `item`.
|
||||
|
||||
### 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>
|
||||
|
||||
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
|
||||
|
||||
@ -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>
|
||||
|
||||
<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.
|
||||
|
||||
<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 {
|
||||
data = 'initial value';
|
||||
serverUrl = 'SERVER_URL';
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.httpClient.get(serverUrl).subscribe(response => {
|
||||
this.httpClient.get(this.serverUrl).subscribe(response => {
|
||||
// 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() {
|
||||
setTimeout(() => {
|
||||
// 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() {
|
||||
Promise.resolve(1).then(v => {
|
||||
// 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.
|
||||
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`.
|
||||
|
||||
```javascript
|
||||
@ -257,16 +258,14 @@ The Zone Task concept is very similar to the Javascript VM Task concept.
|
||||
- `microTask`: such as `Promise.then()`.
|
||||
- `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:
|
||||
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||
"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",
|
||||
"test": "yarn check-env && ng test",
|
||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||
|
@ -132,7 +132,7 @@ class ExampleZipper {
|
||||
return basePath + file;
|
||||
});
|
||||
|
||||
if (json.files[0].substr(0, 1) === '!') {
|
||||
if (json.files[0][0] === '!') {
|
||||
json.files = defaultIncludes.concat(json.files);
|
||||
}
|
||||
}
|
||||
@ -144,16 +144,16 @@ class ExampleZipper {
|
||||
|
||||
let gpaths = json.files.map((fileName) => {
|
||||
fileName = fileName.trim();
|
||||
if (fileName.substr(0, 1) === '!') {
|
||||
if (fileName[0] === '!') {
|
||||
return '!' + path.join(exampleDirName, fileName.substr(1));
|
||||
} else {
|
||||
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);
|
||||
fileNames.forEach((fileName) => {
|
||||
@ -165,7 +165,7 @@ class ExampleZipper {
|
||||
// zip.append(fs.createReadStream(fileName), { name: relativePath });
|
||||
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
|
||||
|
@ -1,41 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
// Canonical path provides a consistent path (i.e. always forward slashes) across different OSes
|
||||
var path = require('canonical-path');
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
var jsdom = require("jsdom");
|
||||
var fs = require("fs-extra");
|
||||
var globby = require('globby');
|
||||
const path = require('canonical-path');
|
||||
const fs = require('fs-extra');
|
||||
const globby = require('globby');
|
||||
const jsdom = require('jsdom');
|
||||
|
||||
var regionExtractor = require('../transforms/examples-package/services/region-parser');
|
||||
const regionExtractor = require('../transforms/examples-package/services/region-parser');
|
||||
|
||||
class StackblitzBuilder {
|
||||
constructor(basePath, destPath) {
|
||||
this.basePath = basePath;
|
||||
this.destPath = destPath;
|
||||
|
||||
// Extract npm package dependencies
|
||||
var packageJson = require(path.join(__dirname, '../examples/shared/boilerplate/cli/package.json'));
|
||||
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();
|
||||
this.copyrights = this._buildCopyrightStrings();
|
||||
this._boilerplatePackageJsons = {};
|
||||
}
|
||||
|
||||
build() {
|
||||
this._checkForOutdatedConfig();
|
||||
|
||||
// When testing it sometimes helps to look a just one example directory like so:
|
||||
// var stackblitzPaths = path.join(this.basePath, '**/testing/*stackblitz.json');
|
||||
var stackblitzPaths = path.join(this.basePath, '**/*stackblitz.json');
|
||||
var fileNames = globby.sync(stackblitzPaths, { ignore: ['**/node_modules/**'] });
|
||||
// const stackblitzPaths = path.join(this.basePath, '**/testing/*stackblitz.json');
|
||||
const stackblitzPaths = path.join(this.basePath, '**/*stackblitz.json');
|
||||
const fileNames = globby.sync(stackblitzPaths, { ignore: ['**/node_modules/**'] });
|
||||
fileNames.forEach((configFileName) => {
|
||||
try {
|
||||
// console.log('***'+configFileName)
|
||||
@ -46,17 +34,46 @@ class StackblitzBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
_addDependencies(postData) {
|
||||
postData['dependencies'] = JSON.stringify(this.examplePackageDependencies);
|
||||
_addDependencies(config, postData) {
|
||||
// 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() {
|
||||
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' +
|
||||
'can be found in the LICENSE file at http://angular.io/license';
|
||||
var pad = '\n\n';
|
||||
this.copyrights.jsCss = `${pad}/*\n${copyright}\n*/`;
|
||||
this.copyrights.html = `${pad}<!-- \n${copyright}\n-->`;
|
||||
const pad = '\n\n';
|
||||
|
||||
return {
|
||||
jsCss: `${pad}/*\n${copyright}\n*/`,
|
||||
html: `${pad}<!-- \n${copyright}\n-->`,
|
||||
};
|
||||
}
|
||||
|
||||
// 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"`)
|
||||
_buildStackblitzFrom(configFileName) {
|
||||
// replace ending 'stackblitz.json' with 'stackblitz.no-link.html' to create output file name;
|
||||
var outputFileName = `stackblitz.no-link.html`;
|
||||
outputFileName = configFileName.replace(/stackblitz\.json$/, outputFileName);
|
||||
var altFileName;
|
||||
const outputFileName = configFileName.replace(/stackblitz\.json$/, 'stackblitz.no-link.html');
|
||||
let altFileName;
|
||||
if (this.destPath && this.destPath.length > 0) {
|
||||
var partPath = path.dirname(path.relative(this.basePath, outputFileName));
|
||||
var altFileName = path.join(this.destPath, partPath, path.basename(outputFileName)).replace('.no-link.', '.');
|
||||
const partPath = path.dirname(path.relative(this.basePath, outputFileName));
|
||||
altFileName = path.join(this.destPath, partPath, path.basename(outputFileName)).replace('.no-link.', '.');
|
||||
}
|
||||
try {
|
||||
var config = this._initConfigAndCollectFileNames(configFileName);
|
||||
var postData = this._createPostData(config, configFileName);
|
||||
this._addDependencies(postData);
|
||||
var html = this._createStackblitzHtml(config, postData);
|
||||
const config = this._initConfigAndCollectFileNames(configFileName);
|
||||
const postData = this._createPostData(config, configFileName);
|
||||
this._addDependencies(config, postData);
|
||||
const html = this._createStackblitzHtml(config, postData);
|
||||
fs.writeFileSync(outputFileName, html, 'utf-8');
|
||||
if (altFileName) {
|
||||
var altDirName = path.dirname(altFileName);
|
||||
const altDirName = path.dirname(altFileName);
|
||||
fs.ensureDirSync(altDirName);
|
||||
fs.writeFileSync(altFileName, html, 'utf-8');
|
||||
}
|
||||
} catch (e) {
|
||||
// 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);
|
||||
}
|
||||
if (altFileName && this._existsSync(altFileName)) {
|
||||
if (altFileName && fs.existsSync(altFileName)) {
|
||||
fs.unlinkSync(altFileName);
|
||||
}
|
||||
throw e;
|
||||
@ -100,8 +116,8 @@ class StackblitzBuilder {
|
||||
|
||||
_checkForOutdatedConfig() {
|
||||
// Ensure that nobody is trying to use the old config filenames (i.e. `plnkr.json`).
|
||||
var plunkerPaths = path.join(this.basePath, '**/*plnkr.json');
|
||||
var fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] });
|
||||
const plunkerPaths = path.join(this.basePath, '**/*plnkr.json');
|
||||
const fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] });
|
||||
|
||||
if (fileNames.length) {
|
||||
const readmePath = path.join(__dirname, 'README.md');
|
||||
@ -118,85 +134,87 @@ class StackblitzBuilder {
|
||||
|
||||
_getPrimaryFile(config) {
|
||||
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}'.`);
|
||||
}
|
||||
return config.file;
|
||||
} else {
|
||||
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) {
|
||||
throw new Error(`None of the default primary files (${defaultPrimaryFiles.join(', ')}) exists in '${config.basePath}'.`);
|
||||
}
|
||||
|
||||
|
||||
return primaryFile;
|
||||
}
|
||||
}
|
||||
|
||||
_createBaseStackblitzHtml(config) {
|
||||
var file = `?file=${this._getPrimaryFile(config)}`;
|
||||
var 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;
|
||||
const file = `?file=${this._getPrimaryFile(config)}`;
|
||||
const action = `https://run.stackblitz.com/api/angular/v1${file}`;
|
||||
|
||||
if (isEmbedded) {
|
||||
var form = document.getElementById('mainForm');
|
||||
var action = form.action;
|
||||
var actionHasParams = action.indexOf('?') > -1;
|
||||
var symbol = actionHasParams ? '&' : '?'
|
||||
form.action = form.action + symbol + embedded;
|
||||
}
|
||||
document.getElementById("mainForm").submit();
|
||||
</script>
|
||||
</body></html>`;
|
||||
return html;
|
||||
return `
|
||||
<!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) {
|
||||
var form = document.getElementById('mainForm');
|
||||
var action = form.action;
|
||||
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) {
|
||||
var postData = {};
|
||||
const postData = {};
|
||||
|
||||
// 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.`);
|
||||
}
|
||||
|
||||
config.fileNames.forEach((fileName) => {
|
||||
var content;
|
||||
var extn = path.extname(fileName);
|
||||
if (extn == '.png') {
|
||||
let content;
|
||||
const extn = path.extname(fileName);
|
||||
if (extn === '.png') {
|
||||
content = this._encodeBase64(fileName);
|
||||
fileName = fileName.substr(0, fileName.length - 4) + '.base64.png'
|
||||
fileName = `${fileName.slice(0, -extn.length)}.base64${extn}`;
|
||||
} else {
|
||||
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;
|
||||
} else if (extn == '.html') {
|
||||
} else if (extn === '.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
|
||||
if (relativeFileName == config.main) {
|
||||
if (relativeFileName === config.main) {
|
||||
relativeFileName = 'src/index.html';
|
||||
}
|
||||
|
||||
// A custom main.ts file? Rename it
|
||||
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) {
|
||||
// set config.description to title from index.html
|
||||
var matches = /<title>(.*)<\/title>/.exec(content);
|
||||
const matches = /<title>(.*)<\/title>/.exec(content);
|
||||
if (matches) {
|
||||
config.description = matches[1];
|
||||
}
|
||||
@ -208,28 +226,26 @@ class StackblitzBuilder {
|
||||
postData[`files[${relativeFileName}]`] = content;
|
||||
});
|
||||
|
||||
var tags = ['angular', 'example'].concat(config.tags || []);
|
||||
tags.forEach(function(tag,ix) {
|
||||
postData['tags[' + ix + ']'] = tag;
|
||||
});
|
||||
const tags = ['angular', 'example', ...config.tags || []];
|
||||
tags.forEach((tag, ix) => postData[`tags[${ix}]`] = tag);
|
||||
|
||||
postData.description = "Angular Example - " + config.description;
|
||||
postData.description = `Angular Example - ${config.description}`;
|
||||
|
||||
return postData;
|
||||
}
|
||||
|
||||
_createStackblitzHtml(config, postData) {
|
||||
var baseHtml = this._createBaseStackblitzHtml(config);
|
||||
var doc = jsdom.jsdom(baseHtml);
|
||||
var 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;
|
||||
const baseHtml = this._createBaseStackblitzHtml(config);
|
||||
const doc = jsdom.jsdom(baseHtml);
|
||||
const form = doc.querySelector('form');
|
||||
|
||||
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) {
|
||||
@ -237,36 +253,20 @@ class StackblitzBuilder {
|
||||
return fs.readFileSync(file, { encoding: 'base64' });
|
||||
}
|
||||
|
||||
_existsSync(filename) {
|
||||
try {
|
||||
fs.accessSync(filename);
|
||||
return true;
|
||||
} catch(ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_htmlToElement(document, html) {
|
||||
var div = document.createElement('div');
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
_initConfigAndCollectFileNames(configFileName) {
|
||||
var configDir = path.dirname(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}`);
|
||||
}
|
||||
const config = this._parseConfig(configFileName);
|
||||
|
||||
var defaultIncludes = ['**/*.ts', '**/*.js', '**/*.css', '**/*.html', '**/*.md', '**/*.json', '**/*.png', '**/*.svg'];
|
||||
var boilerplateIncludes = ['src/environments/*.*', 'angular.json', 'src/polyfills.ts'];
|
||||
const defaultIncludes = ['**/*.ts', '**/*.js', '**/*.css', '**/*.html', '**/*.md', '**/*.json', '**/*.png', '**/*.svg'];
|
||||
const boilerplateIncludes = ['src/environments/*.*', 'angular.json', 'src/polyfills.ts'];
|
||||
if (config.files) {
|
||||
if (config.files.length > 0) {
|
||||
if (config.files[0].substr(0, 1) == '!') {
|
||||
if (config.files[0][0] === '!') {
|
||||
config.files = defaultIncludes.concat(config.files);
|
||||
}
|
||||
}
|
||||
@ -275,10 +275,10 @@ class StackblitzBuilder {
|
||||
}
|
||||
config.files = config.files.concat(boilerplateIncludes);
|
||||
|
||||
var includeSpec = false;
|
||||
var gpaths = config.files.map(function(fileName) {
|
||||
let includeSpec = false;
|
||||
const gpaths = config.files.map((fileName) => {
|
||||
fileName = fileName.trim();
|
||||
if (fileName.substr(0,1) == '!') {
|
||||
if (fileName[0] === '!') {
|
||||
return '!' + path.join(config.basePath, fileName.substr(1));
|
||||
} else {
|
||||
includeSpec = includeSpec || /\.spec\.(ts|js)$/.test(fileName);
|
||||
@ -286,7 +286,7 @@ class StackblitzBuilder {
|
||||
}
|
||||
});
|
||||
|
||||
var defaultExcludes = [
|
||||
const defaultExcludes = [
|
||||
'!**/e2e/**/*.*',
|
||||
'!**/tsconfig.json',
|
||||
'!**/package.json',
|
||||
@ -308,10 +308,21 @@ class StackblitzBuilder {
|
||||
|
||||
gpaths.push(...defaultExcludes);
|
||||
|
||||
config.fileNames = globby.sync(gpaths, { ignore: ["**/node_modules/**"] });
|
||||
config.fileNames = globby.sync(gpaths, { ignore: ['**/node_modules/**'] });
|
||||
|
||||
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;
|
||||
|
@ -8,7 +8,9 @@ ts_library(
|
||||
],
|
||||
module_name = "@angular/dev-infra-private",
|
||||
deps = [
|
||||
"//dev-infra/commit-message",
|
||||
"//dev-infra/pullapprove",
|
||||
"//dev-infra/utils:config",
|
||||
"@npm//@types/node",
|
||||
],
|
||||
)
|
||||
@ -33,6 +35,7 @@ pkg_npm(
|
||||
deps = [
|
||||
":cli",
|
||||
":package-json",
|
||||
"//dev-infra/commit-message",
|
||||
"//dev-infra/ts-circular-dependencies",
|
||||
],
|
||||
)
|
||||
|
@ -6,7 +6,11 @@
|
||||
* 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 {readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {verify} from './pullapprove/verify';
|
||||
import {validateCommitMessage} from './commit-message/validate';
|
||||
import {getRepoBaseDir} from './utils/config';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@ -16,6 +20,12 @@ switch (args[0]) {
|
||||
case 'pullapprove:verify':
|
||||
verify();
|
||||
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:
|
||||
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",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils:config",
|
||||
"@npm//@types/glob",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/yargs",
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
@ -17,7 +18,9 @@ import {Analyzer, ReferenceChain} from './analyzer';
|
||||
import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden';
|
||||
import {convertPathToForwardSlash} from './file_system';
|
||||
|
||||
const projectDir = join(__dirname, '../../');
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
|
||||
const projectDir = getRepoBaseDir();
|
||||
const packagesDir = join(projectDir, 'packages/');
|
||||
// The default glob does not capture deprecated packages such as http, or the webworker platform.
|
||||
const defaultGlob =
|
||||
@ -26,10 +29,9 @@ const defaultGlob =
|
||||
if (require.main === module) {
|
||||
const {_: command, goldenFile, glob, baseDir, warnings} =
|
||||
yargs.help()
|
||||
.version(false)
|
||||
.strict()
|
||||
.command('check <golden-file>', 'Checks if the circular dependencies have changed.')
|
||||
.command('approve <golden-file>', 'Approves the current circular dependencies.')
|
||||
.command('check <goldenFile>', 'Checks if the circular dependencies have changed.')
|
||||
.command('approve <goldenFile>', 'Approves the current circular dependencies.')
|
||||
.demandCommand()
|
||||
.option(
|
||||
'approve',
|
||||
|
@ -5,6 +5,7 @@ ts_library(
|
||||
srcs = [
|
||||
"config.ts",
|
||||
],
|
||||
module_name = "@angular/dev-infra-private/utils",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"@npm//@types/json5",
|
||||
|
@ -5,16 +5,15 @@
|
||||
* 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 {parse} from 'json5';
|
||||
import {readFileSync} from 'fs';
|
||||
import {parse} from 'json5';
|
||||
import {join} from 'path';
|
||||
import {exec} from 'shelljs';
|
||||
|
||||
|
||||
/**
|
||||
* 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});
|
||||
if (baseRepoDir.code) {
|
||||
throw Error(
|
||||
@ -28,7 +27,7 @@ function getRepoBaseDir() {
|
||||
/**
|
||||
* 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');
|
||||
let rawConfig = '';
|
||||
try {
|
||||
@ -41,5 +40,8 @@ export function getAngularDevConfig(): DevInfraConfig {
|
||||
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",
|
||||
"version": "9.1.0-rc.0",
|
||||
"version": "9.1.0-rc.1",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"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 {
|
||||
return peek === chars.$EQ || chars.isAsciiLetter(peek) || chars.isDigit(peek);
|
||||
return peek !== chars.$RBRACE;
|
||||
}
|
||||
|
||||
function compareCharCodeCaseInsensitive(code1: number, code2: number): boolean {
|
||||
|
@ -145,8 +145,9 @@ export class BindingParser {
|
||||
binding.value ? moveParseSourceSpan(sourceSpan, binding.value.span) : undefined;
|
||||
targetVars.push(new ParsedVariable(key, value, bindingSpan, keySpan, valueSpan));
|
||||
} else if (binding.value) {
|
||||
const valueSpan = moveParseSourceSpan(sourceSpan, binding.value.ast.sourceSpan);
|
||||
this._parsePropertyAst(
|
||||
key, binding.value, sourceSpan, undefined, targetMatchableAttrs, targetProps);
|
||||
key, binding.value, sourceSpan, valueSpan, targetMatchableAttrs, targetProps);
|
||||
} else {
|
||||
targetMatchableAttrs.push([key, '']);
|
||||
this.parseLiteralAttr(
|
||||
|
@ -313,6 +313,26 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
|
||||
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', () => {
|
||||
const p = parser.parse(
|
||||
`{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([
|
||||
['Template', '0:32', '0:32', '32:38'],
|
||||
['TextAttribute', '5:31', '<empty>'],
|
||||
['BoundAttribute', '5:31', '<empty>'],
|
||||
['Variable', '13:22', '<empty>'], // let item
|
||||
['BoundAttribute', '5:31', '25:30'], // *ngFor="let item of items" -> items
|
||||
['Variable', '13:22', '<empty>'], // let item
|
||||
['Element', '0:38', '0:32', '32:38'],
|
||||
]);
|
||||
|
||||
@ -245,8 +245,8 @@ describe('R3 AST source spans', () => {
|
||||
// </ng-template>
|
||||
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
|
||||
['Template', '0:28', '0:28', '28:34'],
|
||||
['BoundAttribute', '5:27', '<empty>'],
|
||||
['BoundAttribute', '5:27', '<empty>'],
|
||||
['BoundAttribute', '5:27', '13:17'], // ngFor="item of items" -> item
|
||||
['BoundAttribute', '5:27', '21:26'], // ngFor="item of items" -> items
|
||||
['Element', '0:34', '0:28', '28:34'],
|
||||
]);
|
||||
});
|
||||
@ -263,8 +263,8 @@ describe('R3 AST source spans', () => {
|
||||
it('is correct for variables via as ...', () => {
|
||||
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
|
||||
['Template', '0:27', '0:27', '27:33'],
|
||||
['BoundAttribute', '5:26', '<empty>'],
|
||||
['Variable', '6:25', '6:10'], // ngIf="expr as local -> ngIf
|
||||
['BoundAttribute', '5:26', '12:16'], // ngIf="expr as local" -> expr
|
||||
['Variable', '6:25', '6:10'], // ngIf="expr as local -> ngIf
|
||||
['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'));
|
||||
}
|
||||
|
||||
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;
|
||||
if (preserveWhitespaces === undefined) {
|
||||
if (jitOptions !== null && jitOptions.preserveWhitespaces !== undefined) {
|
||||
preserveWhitespaces = jitOptions.preserveWhitespaces;
|
||||
if (options !== null && options.preserveWhitespaces !== undefined) {
|
||||
preserveWhitespaces = options.preserveWhitespaces;
|
||||
} else {
|
||||
preserveWhitespaces = false;
|
||||
}
|
||||
}
|
||||
let encapsulation = metadata.encapsulation;
|
||||
if (encapsulation === undefined) {
|
||||
if (jitOptions !== null && jitOptions.defaultEncapsulation !== undefined) {
|
||||
encapsulation = jitOptions.defaultEncapsulation;
|
||||
if (options !== null && options.defaultEncapsulation !== undefined) {
|
||||
encapsulation = options.defaultEncapsulation;
|
||||
} else {
|
||||
encapsulation = ViewEncapsulation.Emulated;
|
||||
}
|
||||
|
Reference in New Issue
Block a user