Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
b6bd8d7572 | |||
b08168bb90 | |||
407fa42679 | |||
aef432384a | |||
fb70083339 | |||
c9c2408176 | |||
e066bddfe9 | |||
447a600477 | |||
70f9bfff43 | |||
57c02b044c | |||
6defe962c8 | |||
267bcb3e9c | |||
b0b66881b4 | |||
9ff8d78bcd | |||
563b707497 | |||
5357e643b3 | |||
f71d132f7c | |||
ba3edda230 | |||
0767d37c07 | |||
8ba24578bc | |||
133a97ad67 | |||
4e67a3ab3f | |||
377f0010fc | |||
6e09129e4c | |||
d80e51a6b1 | |||
feb66b00da | |||
cb19eac105 | |||
6e0564ade6 | |||
05eeb7d279 | |||
2ce5fa3cce | |||
e140cdcb34 | |||
14b2db1d43 | |||
2afc7e982e |
@ -280,7 +280,7 @@ jobs:
|
||||
|
||||
- run: yarn lint
|
||||
- run: yarn ts-circular-deps:check
|
||||
- run: node tools/pullapprove/verify.js
|
||||
- run: yarn -s ng-dev pullapprove:verify
|
||||
|
||||
test:
|
||||
executor:
|
||||
|
47
.dev-infra.json
Normal file
47
.dev-infra.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"commitMessage": {
|
||||
"maxLength": 120,
|
||||
"minBodyLength": 0,
|
||||
"types": [
|
||||
"build",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"release",
|
||||
"style",
|
||||
"test"
|
||||
],
|
||||
"scopes": [
|
||||
"animations",
|
||||
"bazel",
|
||||
"benchpress",
|
||||
"changelog",
|
||||
"common",
|
||||
"compiler",
|
||||
"compiler-cli",
|
||||
"core",
|
||||
"dev-infra",
|
||||
"docs-infra",
|
||||
"elements",
|
||||
"forms",
|
||||
"http",
|
||||
"language-service",
|
||||
"localize",
|
||||
"ngcc",
|
||||
"packaging",
|
||||
"platform-browser",
|
||||
"platform-browser-dynamic",
|
||||
"platform-server",
|
||||
"platform-webworker",
|
||||
"platform-webworker-dynamic",
|
||||
"router",
|
||||
"service-worker",
|
||||
"upgrade",
|
||||
"ve",
|
||||
"zone.js"
|
||||
]
|
||||
}
|
||||
}
|
@ -958,7 +958,6 @@ groups:
|
||||
'tools/ngcontainer/**',
|
||||
'tools/npm/**',
|
||||
'tools/npm_integration_test/**',
|
||||
'tools/pullapprove/**',
|
||||
'tools/rxjs/**',
|
||||
'tools/saucelabs/**',
|
||||
'tools/size-tracking/**',
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,3 +1,40 @@
|
||||
<a name="9.1.0"></a>
|
||||
# [9.1.0](https://github.com/angular/angular/compare/9.1.0-rc.2...9.1.0) (2020-03-25)
|
||||
|
||||
Promoted `9.1.0-rc.2` to `9.1.0`.
|
||||
|
||||
<a name="9.1.0-rc.2"></a>
|
||||
# [9.1.0-rc.2](https://github.com/angular/angular/compare/9.1.0-rc.1...9.1.0-rc.2) (2020-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **common:** let `KeyValuePipe` accept type unions with `null` ([#36093](https://github.com/angular/angular/issues/36093)) ([407fa42](https://github.com/angular/angular/commit/407fa42)), closes [#35743](https://github.com/angular/angular/issues/35743)
|
||||
* **elements:** correctly handle setting inputs to `undefined` ([#36140](https://github.com/angular/angular/issues/36140)) ([e066bdd](https://github.com/angular/angular/commit/e066bdd))
|
||||
* **elements:** correctly set `SimpleChange#firstChange` for pre-existing inputs ([#36140](https://github.com/angular/angular/issues/36140)) ([447a600](https://github.com/angular/angular/commit/447a600)), closes [#36130](https://github.com/angular/angular/issues/36130)
|
||||
* **ngcc:** use path-mappings from tsconfig in dependency resolution ([#36180](https://github.com/angular/angular/issues/36180)) ([6defe96](https://github.com/angular/angular/commit/6defe96)), closes [#36119](https://github.com/angular/angular/issues/36119)
|
||||
* **ngcc:** use preserve whitespaces from tsconfig if provided ([#36189](https://github.com/angular/angular/issues/36189)) ([aef4323](https://github.com/angular/angular/commit/aef4323)), closes [#35871](https://github.com/angular/angular/issues/35871)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** add dependency info and ng-content selectors to metadata ([#35695](https://github.com/angular/angular/issues/35695)) ([fb70083](https://github.com/angular/angular/commit/fb70083))
|
||||
|
||||
|
||||
|
||||
<a name="9.1.0-rc.1"></a>
|
||||
# [9.1.0-rc.1](https://github.com/angular/angular/compare/9.1.0-rc.0...9.1.0-rc.1) (2020-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** record correct end of expression ([#34690](https://github.com/angular/angular/issues/34690)) ([df890d7](https://github.com/angular/angular/commit/df890d7)), closes [#33477](https://github.com/angular/angular/issues/33477)
|
||||
* **core:** workaround Terser inlining bug ([#36200](https://github.com/angular/angular/issues/36200)) ([f71d132](https://github.com/angular/angular/commit/f71d132))
|
||||
* **localize:** allow ICU expansion case to start with any character except `}` ([#36123](https://github.com/angular/angular/issues/36123)) ([0767d37](https://github.com/angular/angular/commit/0767d37)), closes [#31586](https://github.com/angular/angular/issues/31586)
|
||||
* **compiler:** Propagate value span of ExpressionBinding to ParsedProperty ([#36133](https://github.com/angular/angular/issues/36133)) ([2ce5fa3](https://github.com/angular/angular/commit/2ce5fa3))
|
||||
|
||||
|
||||
|
||||
<a name="9.1.0-rc.0"></a>
|
||||
# [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>
|
||||
|
@ -1288,7 +1288,7 @@ In this example, we have a new macro task (nested setTimeout), by default, when
|
||||
region="fake-async-test-tick-new-macro-task-async">
|
||||
</code-example>
|
||||
|
||||
And in some case, we don't want to trigger the new maco task when ticking, we can use `tick(milliseconds, {processNewMacroTasksSynchronously: false})` to not invoke new maco task.
|
||||
And in some case, we don't want to trigger the new macro task when ticking, we can use `tick(milliseconds, {processNewMacroTasksSynchronously: false})` to not invoke new maco task.
|
||||
|
||||
#### Comparing dates inside fakeAsync()
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 60 KiB |
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"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 526c3cc37",
|
||||
"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; }
|
||||
|
7
goldens/public-api/common/common.d.ts
vendored
7
goldens/public-api/common/common.d.ts
vendored
@ -139,10 +139,17 @@ export declare class KeyValuePipe implements PipeTransform {
|
||||
transform<V>(input: {
|
||||
[key: string]: V;
|
||||
} | Map<string, V>, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number): Array<KeyValue<string, V>>;
|
||||
transform<V>(input: {
|
||||
[key: string]: V;
|
||||
} | Map<string, V> | null, compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number): Array<KeyValue<string, V>> | null;
|
||||
transform<V>(input: {
|
||||
[key: number]: V;
|
||||
} | Map<number, V>, compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number): Array<KeyValue<number, V>>;
|
||||
transform<V>(input: {
|
||||
[key: number]: V;
|
||||
} | Map<number, V> | null, compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number): Array<KeyValue<number, V>> | null;
|
||||
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>;
|
||||
transform<K, V>(input: Map<K, V> | null, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>> | null;
|
||||
}
|
||||
|
||||
export declare class Location {
|
||||
|
4
goldens/public-api/core/core.d.ts
vendored
4
goldens/public-api/core/core.d.ts
vendored
@ -715,7 +715,7 @@ export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportA
|
||||
[key: string]: string;
|
||||
}, OutputMap extends {
|
||||
[key: string]: string;
|
||||
}, QueryFields extends string[]> = ɵComponentDef<T>;
|
||||
}, QueryFields extends string[], NgContentSelectors extends string[]> = ɵComponentDef<T>;
|
||||
|
||||
export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener;
|
||||
|
||||
@ -834,7 +834,7 @@ export declare function ɵɵembeddedViewStart(viewBlockId: number, decls: number
|
||||
|
||||
export declare function ɵɵenableBindings(): void;
|
||||
|
||||
export declare type ɵɵFactoryDef<T> = () => T;
|
||||
export declare type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;
|
||||
|
||||
export declare function ɵɵgetCurrentView(): OpaqueViewState;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "9.1.0-rc.0",
|
||||
"version": "9.1.0",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -147,6 +147,7 @@
|
||||
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
|
||||
"devDependencies": {
|
||||
"@angular/cli": "9.0.3",
|
||||
"@angular/dev-infra-private": "angular/dev-infra-private-builds#3724a71",
|
||||
"@bazel/bazelisk": "^1.3.0",
|
||||
"@bazel/buildifier": "^0.29.0",
|
||||
"@bazel/ibazel": "^0.12.3",
|
||||
|
@ -1,2 +0,0 @@
|
||||
*.xpi
|
||||
addon-sdk*
|
@ -1,38 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
declare var exportFunction: any;
|
||||
declare var unsafeWindow: any;
|
||||
|
||||
exportFunction(function() {
|
||||
const curTime = unsafeWindow.performance.now();
|
||||
(<any>self).port.emit('startProfiler', curTime);
|
||||
}, unsafeWindow, {defineAs: 'startProfiler'});
|
||||
|
||||
exportFunction(function() {
|
||||
(<any>self).port.emit('stopProfiler');
|
||||
}, unsafeWindow, {defineAs: 'stopProfiler'});
|
||||
|
||||
exportFunction(function(cb: Function) {
|
||||
(<any>self).port.once('perfProfile', cb);
|
||||
(<any>self).port.emit('getProfile');
|
||||
}, unsafeWindow, {defineAs: 'getProfile'});
|
||||
|
||||
exportFunction(function() {
|
||||
(<any>self).port.emit('forceGC');
|
||||
}, unsafeWindow, {defineAs: 'forceGC'});
|
||||
|
||||
exportFunction(function(name: string) {
|
||||
const curTime = unsafeWindow.performance.now();
|
||||
(<any>self).port.emit('markStart', name, curTime);
|
||||
}, unsafeWindow, {defineAs: 'markStart'});
|
||||
|
||||
exportFunction(function(name: string) {
|
||||
const curTime = unsafeWindow.performance.now();
|
||||
(<any>self).port.emit('markEnd', name, curTime);
|
||||
}, unsafeWindow, {defineAs: 'markEnd'});
|
@ -1,80 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const {Cc, Ci, Cu} = require('chrome');
|
||||
const os = Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
|
||||
const ParserUtil = require('./parser_util');
|
||||
|
||||
class Profiler {
|
||||
private _profiler: any;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private _markerEvents !: any[];
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private _profilerStartTime !: number;
|
||||
|
||||
constructor() { this._profiler = Cc['@mozilla.org/tools/profiler;1'].getService(Ci.nsIProfiler); }
|
||||
|
||||
start(entries: any, interval: any, features: any, timeStarted: any) {
|
||||
this._profiler.StartProfiler(entries, interval, features, features.length);
|
||||
this._profilerStartTime = timeStarted;
|
||||
this._markerEvents = [];
|
||||
}
|
||||
|
||||
stop() { this._profiler.StopProfiler(); }
|
||||
|
||||
getProfilePerfEvents() {
|
||||
const profileData = this._profiler.getProfileData();
|
||||
let perfEvents = ParserUtil.convertPerfProfileToEvents(profileData);
|
||||
perfEvents = this._mergeMarkerEvents(perfEvents);
|
||||
perfEvents.sort(function(event1: any, event2: any) {
|
||||
return event1.ts - event2.ts;
|
||||
}); // Sort by ts
|
||||
return perfEvents;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _mergeMarkerEvents(perfEvents: any[]): any[] {
|
||||
this._markerEvents.forEach(function(markerEvent) { perfEvents.push(markerEvent); });
|
||||
return perfEvents;
|
||||
}
|
||||
|
||||
addStartEvent(name: string, timeStarted: number) {
|
||||
this._markerEvents.push({ph: 'B', ts: timeStarted - this._profilerStartTime, name: name});
|
||||
}
|
||||
|
||||
addEndEvent(name: string, timeEnded: number) {
|
||||
this._markerEvents.push({ph: 'E', ts: timeEnded - this._profilerStartTime, name: name});
|
||||
}
|
||||
}
|
||||
|
||||
function forceGC() {
|
||||
Cu.forceGC();
|
||||
os.notifyObservers(null, 'child-gc-request', null);
|
||||
}
|
||||
|
||||
const mod = require('sdk/page-mod');
|
||||
const data = require('sdk/self').data;
|
||||
const profiler = new Profiler();
|
||||
mod.PageMod({
|
||||
include: ['*'],
|
||||
contentScriptFile: data.url('installed_script.js'),
|
||||
onAttach: (worker: any) => {
|
||||
worker.port.on(
|
||||
'startProfiler',
|
||||
(timeStarted: any) => profiler.start(
|
||||
/* = profiler memory */ 3000000, 0.1, ['leaf', 'js', 'stackwalk', 'gc'], timeStarted));
|
||||
worker.port.on('stopProfiler', () => profiler.stop());
|
||||
worker.port.on(
|
||||
'getProfile', () => worker.port.emit('perfProfile', profiler.getProfilePerfEvents()));
|
||||
worker.port.on('forceGC', forceGC);
|
||||
worker.port.on(
|
||||
'markStart', (name: string, timeStarted: any) => profiler.addStartEvent(name, timeStarted));
|
||||
worker.port.on(
|
||||
'markEnd', (name: string, timeEnded: any) => profiler.addEndEvent(name, timeEnded));
|
||||
}
|
||||
});
|
@ -1,92 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} perfProfile The perf profile JSON object.
|
||||
* @return {Object[]} An array of recognized events that are captured
|
||||
* within the perf profile.
|
||||
*/
|
||||
export function convertPerfProfileToEvents(perfProfile: any): any[] {
|
||||
const inProgressEvents = new Map(); // map from event name to start time
|
||||
const finishedEvents: {[key: string]: any}[] = []; // Event[] finished events
|
||||
const addFinishedEvent = function(eventName: string, startTime: number, endTime: number) {
|
||||
const categorizedEventName = categorizeEvent(eventName);
|
||||
let args: {[key: string]: any}|undefined = undefined;
|
||||
if (categorizedEventName == 'gc') {
|
||||
// TODO: We cannot measure heap size at the moment
|
||||
args = {usedHeapSize: 0};
|
||||
}
|
||||
if (startTime == endTime) {
|
||||
// Finished instantly
|
||||
finishedEvents.push({ph: 'X', ts: startTime, name: categorizedEventName, args: args});
|
||||
} else {
|
||||
// Has duration
|
||||
finishedEvents.push({ph: 'B', ts: startTime, name: categorizedEventName, args: args});
|
||||
finishedEvents.push({ph: 'E', ts: endTime, name: categorizedEventName, args: args});
|
||||
}
|
||||
};
|
||||
|
||||
const samples = perfProfile.threads[0].samples;
|
||||
// In perf profile, firefox samples all the frames in set time intervals. Here
|
||||
// we go through all the samples and construct the start and end time for each
|
||||
// event.
|
||||
for (let i = 0; i < samples.length; ++i) {
|
||||
const sample = samples[i];
|
||||
const sampleTime = sample.time;
|
||||
|
||||
// Add all the frames into a set so it's easier/faster to find the set
|
||||
// differences
|
||||
const sampleFrames = new Set();
|
||||
sample.frames.forEach(function(frame: {[key: string]: any}) {
|
||||
sampleFrames.add(frame['location']);
|
||||
});
|
||||
|
||||
// If an event is in the inProgressEvents map, but not in the current sample,
|
||||
// then it must have just finished. We add this event to the finishedEvents
|
||||
// array and remove it from the inProgressEvents map.
|
||||
const previousSampleTime = (i == 0 ? /* not used */ -1 : samples[i - 1].time);
|
||||
inProgressEvents.forEach(function(startTime, eventName) {
|
||||
if (!(sampleFrames.has(eventName))) {
|
||||
addFinishedEvent(eventName, startTime, previousSampleTime);
|
||||
inProgressEvents.delete(eventName);
|
||||
}
|
||||
});
|
||||
|
||||
// If an event is in the current sample, but not in the inProgressEvents map,
|
||||
// then it must have just started. We add this event to the inProgressEvents
|
||||
// map.
|
||||
sampleFrames.forEach(function(eventName) {
|
||||
if (!(inProgressEvents.has(eventName))) {
|
||||
inProgressEvents.set(eventName, sampleTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If anything is still in progress, we need to included it as a finished event
|
||||
// since recording ended.
|
||||
const lastSampleTime = samples[samples.length - 1].time;
|
||||
inProgressEvents.forEach(function(startTime, eventName) {
|
||||
addFinishedEvent(eventName, startTime, lastSampleTime);
|
||||
});
|
||||
|
||||
// Remove all the unknown categories.
|
||||
return finishedEvents.filter(function(event) { return event['name'] != 'unknown'; });
|
||||
}
|
||||
|
||||
// TODO: this is most likely not exhaustive.
|
||||
export function categorizeEvent(eventName: string): string {
|
||||
if (eventName.indexOf('PresShell::Paint') > -1) {
|
||||
return 'render';
|
||||
} else if (eventName.indexOf('FirefoxDriver.prototype.executeScript') > -1) {
|
||||
return 'script';
|
||||
} else if (eventName.indexOf('forceGC') > -1) {
|
||||
return 'gc';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const q = require('q');
|
||||
const FirefoxProfile = require('firefox-profile');
|
||||
const jpm = require('jpm/lib/xpi');
|
||||
const pathUtil = require('path');
|
||||
|
||||
const PERF_ADDON_PACKAGE_JSON_DIR = '..';
|
||||
|
||||
exports.getAbsolutePath = function(path: string) {
|
||||
const normalizedPath = pathUtil.normalize(path);
|
||||
if (pathUtil.resolve(normalizedPath) == normalizedPath) {
|
||||
// Already absolute path
|
||||
return normalizedPath;
|
||||
} else {
|
||||
return pathUtil.join(__dirname, normalizedPath);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getFirefoxProfile = function(extensionPath: string) {
|
||||
const deferred = q.defer();
|
||||
|
||||
const firefoxProfile = new FirefoxProfile();
|
||||
firefoxProfile.addExtensions([extensionPath], () => {
|
||||
firefoxProfile.encoded((err: any, encodedProfile: string) => {
|
||||
const multiCapabilities = [{browserName: 'firefox', firefox_profile: encodedProfile}];
|
||||
deferred.resolve(multiCapabilities);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
exports.getFirefoxProfileWithExtension = function() {
|
||||
const absPackageJsonDir = pathUtil.join(__dirname, PERF_ADDON_PACKAGE_JSON_DIR);
|
||||
const packageJson = require(pathUtil.join(absPackageJsonDir, 'package.json'));
|
||||
|
||||
const savedCwd = process.cwd();
|
||||
process.chdir(absPackageJsonDir);
|
||||
|
||||
return jpm(packageJson).then((xpiPath: string) => {
|
||||
process.chdir(savedCwd);
|
||||
return exports.getFirefoxProfile(xpiPath);
|
||||
});
|
||||
};
|
@ -1 +0,0 @@
|
||||
{ "version" : "0.0.1", "main" : "lib/main.js", "name" : "ffperf-addon" }
|
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
require('core-js');
|
||||
require('reflect-metadata');
|
||||
const testHelper = require('../../src/firefox_extension/lib/test_helper.js');
|
||||
|
||||
exports.config = {
|
||||
specs: ['spec.js', 'sample_benchmark.js'],
|
||||
|
||||
framework: 'jasmine2',
|
||||
|
||||
jasmineNodeOpts: {showColors: true, defaultTimeoutInterval: 1200000},
|
||||
|
||||
getMultiCapabilities: function() { return testHelper.getFirefoxProfileWithExtension(); }
|
||||
};
|
@ -1,100 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {convertPerfProfileToEvents} from '../../src/firefox_extension/lib/parser_util';
|
||||
|
||||
function assertEventsEqual(actualEvents: any[], expectedEvents: any[]) {
|
||||
expect(actualEvents.length == expectedEvents.length);
|
||||
for (let i = 0; i < actualEvents.length; ++i) {
|
||||
const actualEvent = actualEvents[i];
|
||||
const expectedEvent = expectedEvents[i];
|
||||
for (const key in actualEvent) {
|
||||
expect(actualEvent[key]).toEqual(expectedEvent[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
describe('convertPerfProfileToEvents', function() {
|
||||
it('should convert single instantaneous event', function() {
|
||||
const profileData = {
|
||||
threads: [
|
||||
{samples: [{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]}]}
|
||||
]
|
||||
};
|
||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
||||
assertEventsEqual(perfEvents, [{ph: 'X', ts: 1, name: 'script'}]);
|
||||
});
|
||||
|
||||
it('should convert single non-instantaneous event', function() {
|
||||
const profileData = {
|
||||
threads: [{
|
||||
samples: [
|
||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
||||
{time: 2, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
||||
{time: 100, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]}
|
||||
]
|
||||
}]
|
||||
};
|
||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
||||
assertEventsEqual(
|
||||
perfEvents, [{ph: 'B', ts: 1, name: 'script'}, {ph: 'E', ts: 100, name: 'script'}]);
|
||||
});
|
||||
|
||||
it('should convert multiple instantaneous events', function() {
|
||||
const profileData = {
|
||||
threads: [{
|
||||
samples: [
|
||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
||||
{time: 2, frames: [{location: 'PresShell::Paint'}]}
|
||||
]
|
||||
}]
|
||||
};
|
||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
||||
assertEventsEqual(
|
||||
perfEvents, [{ph: 'X', ts: 1, name: 'script'}, {ph: 'X', ts: 2, name: 'render'}]);
|
||||
});
|
||||
|
||||
it('should convert multiple mixed events', function() {
|
||||
const profileData = {
|
||||
threads: [{
|
||||
samples: [
|
||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
||||
{time: 2, frames: [{location: 'PresShell::Paint'}]},
|
||||
{time: 5, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
||||
{time: 10, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]}
|
||||
]
|
||||
}]
|
||||
};
|
||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
||||
assertEventsEqual(perfEvents, [
|
||||
{ph: 'X', ts: 1, name: 'script'}, {ph: 'X', ts: 2, name: 'render'},
|
||||
{ph: 'B', ts: 5, name: 'script'}, {ph: 'E', ts: 10, name: 'script'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add args to gc events', function() {
|
||||
const profileData = {threads: [{samples: [{time: 1, frames: [{location: 'forceGC'}]}]}]};
|
||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
||||
assertEventsEqual(perfEvents, [{ph: 'X', ts: 1, name: 'gc', args: {usedHeapSize: 0}}]);
|
||||
});
|
||||
|
||||
it('should skip unknown events', function() {
|
||||
const profileData = {
|
||||
threads: [{
|
||||
samples: [
|
||||
{time: 1, frames: [{location: 'FirefoxDriver.prototype.executeScript'}]},
|
||||
{time: 2, frames: [{location: 'foo'}]}
|
||||
]
|
||||
}]
|
||||
};
|
||||
const perfEvents = convertPerfProfileToEvents(profileData);
|
||||
assertEventsEqual(perfEvents, [{ph: 'X', ts: 1, name: 'script'}]);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {$, browser} from 'protractor';
|
||||
|
||||
const benchpress = require('../../index.js');
|
||||
|
||||
// TODO: this test is currnetly failing. it seems that it didn't run on the ci for a while
|
||||
xdescribe('deep tree baseline', function() {
|
||||
const runner = new benchpress.Runner([
|
||||
// use protractor as Webdriver client
|
||||
benchpress.SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS,
|
||||
// use RegressionSlopeValidator to validate samples
|
||||
benchpress.Validator.bind(benchpress.RegressionSlopeValidator),
|
||||
// use 10 samples to calculate slope regression
|
||||
benchpress.bind(benchpress.RegressionSlopeValidator.SAMPLE_SIZE).toValue(20),
|
||||
// use the script metric to calculate slope regression
|
||||
benchpress.bind(benchpress.RegressionSlopeValidator.METRIC).toValue('scriptTime'),
|
||||
benchpress.bind(benchpress.Options.FORCE_GC).toValue(true)
|
||||
]);
|
||||
|
||||
|
||||
it('should be fast!', function(done) {
|
||||
browser.ignoreSynchronization = true;
|
||||
browser.get('http://localhost:8001/playground/src/benchpress/');
|
||||
|
||||
/*
|
||||
* Tell benchpress to click the buttons to destroy and re-create the tree for each sample.
|
||||
* Benchpress will log the collected metrics after each sample is collected, and will stop
|
||||
* sampling as soon as the calculated regression slope for last 20 samples is stable.
|
||||
*/
|
||||
runner
|
||||
.sample({
|
||||
id: 'baseline',
|
||||
execute: function() { $('button').click(); },
|
||||
providers: [benchpress.bind(benchpress.Options.SAMPLE_DESCRIPTION).toValue({depth: 9})]
|
||||
})
|
||||
.then(done, done.fail);
|
||||
});
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/* tslint:disable:no-console */
|
||||
import {browser} from 'protractor';
|
||||
|
||||
const assertEventsContainsName = function(events: any[], eventName: string) {
|
||||
let found = false;
|
||||
for (let i = 0; i < events.length; ++i) {
|
||||
if (events[i].name == eventName) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(found).toBeTruthy();
|
||||
};
|
||||
|
||||
// TODO: this test is currnetly failing. it seems that it didn't run on the ci for a while
|
||||
xdescribe('firefox extension', function() {
|
||||
const TEST_URL = 'http://localhost:8001/playground/src/hello_world/index.html';
|
||||
|
||||
it('should measure performance', function() {
|
||||
browser.sleep(3000); // wait for extension to load
|
||||
|
||||
browser.driver.get(TEST_URL);
|
||||
|
||||
browser.executeScript('window.startProfiler()').then(function() {
|
||||
console.log('started measuring perf');
|
||||
});
|
||||
|
||||
browser.executeAsyncScript('setTimeout(arguments[0], 1000);');
|
||||
browser.executeScript('window.forceGC()');
|
||||
|
||||
browser.executeAsyncScript('var cb = arguments[0]; window.getProfile(cb);')
|
||||
.then(function(profile: any) {
|
||||
assertEventsContainsName(profile, 'gc');
|
||||
assertEventsContainsName(profile, 'script');
|
||||
});
|
||||
});
|
||||
});
|
@ -85,6 +85,7 @@ export class NgForOfContext<T, U extends NgIterable<T> = NgIterable<T>> {
|
||||
* more complex then a property access, for example when using the async pipe (`userStreams |
|
||||
* async`).
|
||||
* - `index: number`: The index of the current item in the iterable.
|
||||
* - `count: number`: The length of the iterable.
|
||||
* - `first: boolean`: True when the item is the first item in the iterable.
|
||||
* - `last: boolean`: True when the item is the last item in the iterable.
|
||||
* - `even: boolean`: True when the item has an even index in the iterable.
|
||||
|
@ -55,12 +55,23 @@ export class KeyValuePipe implements PipeTransform {
|
||||
input: {[key: string]: V}|Map<string, V>,
|
||||
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
|
||||
Array<KeyValue<string, V>>;
|
||||
transform<V>(
|
||||
input: {[key: string]: V}|Map<string, V>|null,
|
||||
compareFn?: (a: KeyValue<string, V>, b: KeyValue<string, V>) => number):
|
||||
Array<KeyValue<string, V>>|null;
|
||||
transform<V>(
|
||||
input: {[key: number]: V}|Map<number, V>,
|
||||
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
|
||||
Array<KeyValue<number, V>>;
|
||||
transform<V>(
|
||||
input: {[key: number]: V}|Map<number, V>|null,
|
||||
compareFn?: (a: KeyValue<number, V>, b: KeyValue<number, V>) => number):
|
||||
Array<KeyValue<number, V>>|null;
|
||||
transform<K, V>(input: Map<K, V>, compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number):
|
||||
Array<KeyValue<K, V>>;
|
||||
transform<K, V>(
|
||||
input: Map<K, V>|null,
|
||||
compareFn?: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number): Array<KeyValue<K, V>>|null;
|
||||
transform<K, V>(
|
||||
input: null|{[key: string]: V, [key: number]: V}|Map<K, V>,
|
||||
compareFn: (a: KeyValue<K, V>, b: KeyValue<K, V>) => number = defaultComparator):
|
||||
|
@ -203,6 +203,17 @@ let thisArg: any;
|
||||
detectChangesAndExpectText('0123456789');
|
||||
}));
|
||||
|
||||
it('should display count correctly', async(() => {
|
||||
const template = '<span *ngFor="let item of items; let len=count">{{len}}</span>';
|
||||
fixture = createTestComponent(template);
|
||||
|
||||
getComponent().items = [0, 1, 2];
|
||||
detectChangesAndExpectText('333');
|
||||
|
||||
getComponent().items = [4, 3, 2, 1, 0, -1];
|
||||
detectChangesAndExpectText('666666');
|
||||
}));
|
||||
|
||||
it('should display first item correctly', async(() => {
|
||||
const template =
|
||||
'<span *ngFor="let item of items; let isFirst=first">{{isFirst.toString()}}</span>';
|
||||
|
@ -63,6 +63,16 @@ describe('KeyValuePipe', () => {
|
||||
const transform2 = pipe.transform({1: 3});
|
||||
expect(transform1 !== transform2).toEqual(true);
|
||||
});
|
||||
it('should accept a type union of an object with string keys and null', () => {
|
||||
let value !: {[key: string]: string} | null;
|
||||
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
|
||||
expect(pipe.transform(value)).toEqual(null);
|
||||
});
|
||||
it('should accept a type union of an object with number keys and null', () => {
|
||||
let value !: {[key: number]: string} | null;
|
||||
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
|
||||
expect(pipe.transform(value)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Map', () => {
|
||||
@ -115,6 +125,11 @@ describe('KeyValuePipe', () => {
|
||||
const transform2 = pipe.transform(new Map([[1, 3]]));
|
||||
expect(transform1 !== transform2).toEqual(true);
|
||||
});
|
||||
it('should accept a type union of a Map and null', () => {
|
||||
let value !: Map<number, number>| null;
|
||||
const pipe = new KeyValuePipe(defaultKeyValueDiffers);
|
||||
expect(pipe.transform(value)).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -12,6 +12,7 @@ ts_library(
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli",
|
||||
"//packages/compiler-cli/src/ngtsc/annotations",
|
||||
"//packages/compiler-cli/src/ngtsc/cycles",
|
||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||
|
@ -92,6 +92,13 @@ if (require.main === module) {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('tsconfig', {
|
||||
describe:
|
||||
'A path to a tsconfig.json file that will be used to configure the Angular compiler and module resolution used by ngcc.\n' +
|
||||
'If not provided, ngcc will attempt to read a `tsconfig.json` file from the folder above that given by the `-s` option.\n' +
|
||||
'Set to false (via `--no-tsconfig`) if you do not want ngcc to use any `tsconfig.json` file.',
|
||||
type: 'string',
|
||||
})
|
||||
.strict()
|
||||
.help()
|
||||
.parse(args);
|
||||
@ -113,6 +120,10 @@ if (require.main === module) {
|
||||
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
||||
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
||||
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
||||
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
|
||||
// string "false" to capture the `tsconfig=false` option.
|
||||
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
|
||||
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
|
||||
|
||||
(async() => {
|
||||
try {
|
||||
@ -126,7 +137,7 @@ if (require.main === module) {
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
async: options['async'], invalidateEntryPointManifest, errorOnFailedEntryPoint,
|
||||
async: options['async'], invalidateEntryPointManifest, errorOnFailedEntryPoint, tsConfigPath
|
||||
});
|
||||
|
||||
if (logger) {
|
||||
|
@ -8,6 +8,7 @@
|
||||
import {ConstantPool} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ParsedConfiguration} from '../../..';
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
||||
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
||||
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
||||
@ -55,6 +56,7 @@ export class DecorationAnalyzer {
|
||||
private rootDirs = this.bundle.rootDirs;
|
||||
private packagePath = this.bundle.entryPoint.package;
|
||||
private isCore = this.bundle.isCore;
|
||||
private compilerOptions = this.tsConfig !== null? this.tsConfig.options: {};
|
||||
|
||||
moduleResolver =
|
||||
new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null);
|
||||
@ -87,7 +89,7 @@ export class DecorationAnalyzer {
|
||||
new ComponentDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
|
||||
this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
|
||||
/* defaultPreserveWhitespaces */ false,
|
||||
!!this.compilerOptions.preserveWhitespaces,
|
||||
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
|
||||
this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||
NOOP_DEPENDENCY_TRACKER, this.injectableRegistry, /* annotateForClosureCompiler */ false),
|
||||
@ -123,7 +125,8 @@ export class DecorationAnalyzer {
|
||||
constructor(
|
||||
private fs: FileSystem, private bundle: EntryPointBundle,
|
||||
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
|
||||
private diagnosticHandler: (error: ts.Diagnostic) => void = () => {}) {}
|
||||
private diagnosticHandler: (error: ts.Diagnostic) => void = () => {},
|
||||
private tsConfig: ParsedConfiguration|null = null) {}
|
||||
|
||||
/**
|
||||
* Analyze a program to find all the decorated files should be transformed.
|
||||
|
@ -12,6 +12,7 @@ import {DepGraph} from 'dependency-graph';
|
||||
import * as os from 'os';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {readConfiguration} from '../..';
|
||||
import {replaceTsWithNgInErrors} from '../../src/ngtsc/diagnostics';
|
||||
import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, getFileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||
|
||||
@ -94,6 +95,9 @@ export interface SyncNgccOptions {
|
||||
/**
|
||||
* Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`.
|
||||
* These are used to resolve paths to locally built Angular libraries.
|
||||
*
|
||||
* Note that `pathMappings` specified here take precedence over any `pathMappings` loaded from a
|
||||
* TS config file.
|
||||
*/
|
||||
pathMappings?: PathMappings;
|
||||
|
||||
@ -143,6 +147,18 @@ export interface SyncNgccOptions {
|
||||
* Default: `false` (i.e. the manifest will be used if available)
|
||||
*/
|
||||
invalidateEntryPointManifest?: boolean;
|
||||
|
||||
/**
|
||||
* An absolute path to a TS config file (e.g. `tsconfig.json`) or a directory containing one, that
|
||||
* will be used to configure module resolution with things like path mappings, if not specified
|
||||
* explicitly via the `pathMappings` property to `mainNgcc`.
|
||||
*
|
||||
* If `undefined`, ngcc will attempt to load a `tsconfig.json` file from the directory above the
|
||||
* `basePath`.
|
||||
*
|
||||
* If `null`, ngcc will not attempt to load any TS config file at all.
|
||||
*/
|
||||
tsConfigPath?: string|null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,12 +181,12 @@ export type NgccOptions = AsyncNgccOptions | SyncNgccOptions;
|
||||
*/
|
||||
export function mainNgcc(options: AsyncNgccOptions): Promise<void>;
|
||||
export function mainNgcc(options: SyncNgccOptions): void;
|
||||
export function mainNgcc({basePath, targetEntryPointPath,
|
||||
propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||
compileAllFormats = true, createNewEntryPointFormats = false,
|
||||
logger = new ConsoleLogger(LogLevel.info), pathMappings, async = false,
|
||||
errorOnFailedEntryPoint = false, enableI18nLegacyMessageIdFormat = true,
|
||||
invalidateEntryPointManifest = false}: NgccOptions): void|Promise<void> {
|
||||
export function mainNgcc(
|
||||
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||
compileAllFormats = true, createNewEntryPointFormats = false,
|
||||
logger = new ConsoleLogger(LogLevel.info), pathMappings, async = false,
|
||||
errorOnFailedEntryPoint = false, enableI18nLegacyMessageIdFormat = true,
|
||||
invalidateEntryPointManifest = false, tsConfigPath}: NgccOptions): void|Promise<void> {
|
||||
if (!!targetEntryPointPath) {
|
||||
// targetEntryPointPath forces us to error if an entry-point fails.
|
||||
errorOnFailedEntryPoint = true;
|
||||
@ -184,7 +200,19 @@ export function mainNgcc({basePath, targetEntryPointPath,
|
||||
// master/worker process.
|
||||
const fileSystem = getFileSystem();
|
||||
const absBasePath = absoluteFrom(basePath);
|
||||
const config = new NgccConfiguration(fileSystem, dirname(absBasePath));
|
||||
const projectPath = dirname(absBasePath);
|
||||
const config = new NgccConfiguration(fileSystem, projectPath);
|
||||
const tsConfig = tsConfigPath !== null ? readConfiguration(tsConfigPath || projectPath) : null;
|
||||
|
||||
// If `pathMappings` is not provided directly, then try getting it from `tsConfig`, if available.
|
||||
if (tsConfig !== null && pathMappings === undefined && tsConfig.options.baseUrl !== undefined &&
|
||||
tsConfig.options.paths) {
|
||||
pathMappings = {
|
||||
baseUrl: resolve(projectPath, tsConfig.options.baseUrl),
|
||||
paths: tsConfig.options.paths,
|
||||
};
|
||||
}
|
||||
|
||||
const dependencyResolver = getDependencyResolver(fileSystem, logger, config, pathMappings);
|
||||
const entryPointManifest = invalidateEntryPointManifest ?
|
||||
new InvalidatingEntryPointManifest(fileSystem, config, logger) :
|
||||
@ -279,7 +307,7 @@ export function mainNgcc({basePath, targetEntryPointPath,
|
||||
const createCompileFn: CreateCompileFn = onTaskCompleted => {
|
||||
const fileWriter = getFileWriter(
|
||||
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint);
|
||||
const transformer = new Transformer(fileSystem, logger);
|
||||
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
||||
|
||||
return (task: Task) => {
|
||||
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
||||
|
@ -7,6 +7,7 @@
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ParsedConfiguration} from '../../..';
|
||||
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {DecorationAnalyzer} from '../analysis/decoration_analyzer';
|
||||
@ -63,7 +64,9 @@ export type TransformResult = {
|
||||
* - Some formats may contain multiple "modules" in a single file.
|
||||
*/
|
||||
export class Transformer {
|
||||
constructor(private fs: FileSystem, private logger: Logger) {}
|
||||
constructor(
|
||||
private fs: FileSystem, private logger: Logger,
|
||||
private tsConfig: ParsedConfiguration|null = null) {}
|
||||
|
||||
/**
|
||||
* Transform the source (and typings) files of a bundle.
|
||||
@ -146,7 +149,7 @@ export class Transformer {
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
const decorationAnalyzer = new DecorationAnalyzer(
|
||||
this.fs, bundle, reflectionHost, referencesRegistry,
|
||||
diagnostic => diagnostics.push(diagnostic));
|
||||
diagnostic => diagnostics.push(diagnostic), this.tsConfig);
|
||||
const decorationAnalyses = decorationAnalyzer.analyzeProgram();
|
||||
|
||||
const moduleWithProvidersAnalyzer =
|
||||
|
@ -91,6 +91,7 @@ export class DtsRenderer {
|
||||
const endOfClass = dtsClass.dtsDeclaration.getEnd();
|
||||
dtsClass.compilation.forEach(declaration => {
|
||||
const type = translateType(declaration.type, importManager);
|
||||
markForEmitAsSingleLine(type);
|
||||
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
|
||||
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
|
||||
outputText.appendRight(endOfClass - 1, newStatement);
|
||||
@ -176,3 +177,8 @@ export class DtsRenderer {
|
||||
return dtsMap;
|
||||
}
|
||||
}
|
||||
|
||||
function markForEmitAsSingleLine(node: ts.Node) {
|
||||
ts.setEmitFlags(node, ts.EmitFlags.SingleLine);
|
||||
ts.forEachChild(node, markForEmitAsSingleLine);
|
||||
}
|
||||
|
@ -399,9 +399,22 @@ runInEachFileSystem(() => {
|
||||
expect(dtsContents)
|
||||
.toContain(`export declare class ${exportedName} extends PlatformLocation`);
|
||||
// And that ngcc's modifications to that class use the correct (exported) name
|
||||
expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}, never>`);
|
||||
});
|
||||
|
||||
it('should include constructor metadata in factory definitions', () => {
|
||||
mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
targetEntryPointPath: '@angular/common',
|
||||
propertiesToConsider: ['esm2015']
|
||||
});
|
||||
|
||||
const dtsContents = fs.readFile(_('/node_modules/@angular/common/common.d.ts'));
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
`static ɵfac: ɵngcc0.ɵɵFactoryDef<NgPluralCase, [{ attribute: "ngPluralCase"; }, null, null, { host: true; }]>`);
|
||||
});
|
||||
|
||||
it('should add generic type for ModuleWithProviders and generate exports for private modules',
|
||||
() => {
|
||||
compileIntoApf('test-package', {
|
||||
@ -1230,21 +1243,171 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
|
||||
describe('with pathMappings', () => {
|
||||
it('should find and compile packages accessible via the pathMappings', () => {
|
||||
mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
propertiesToConsider: ['es2015'],
|
||||
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
|
||||
});
|
||||
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
||||
it('should infer the @app pathMapping from a local tsconfig.json path', () => {
|
||||
fs.writeFile(
|
||||
_('/tsconfig.json'),
|
||||
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
|
||||
const logger = new MockLogger();
|
||||
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015'], logger});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
fesm2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
// The local-package-3 and local-package-4 will not be processed because there is no path
|
||||
// mappings for `@x` and plain local imports.
|
||||
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-3')}.`,
|
||||
'It is missing required dependencies:\n - @x/local-package'
|
||||
]);
|
||||
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-4')}.`,
|
||||
'It is missing required dependencies:\n - local-package'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should read the @x pathMapping from a specified tsconfig.json path', () => {
|
||||
fs.writeFile(
|
||||
_('/tsconfig.app.json'),
|
||||
JSON.stringify({compilerOptions: {paths: {'@x/*': ['dist/*']}, baseUrl: './'}}));
|
||||
const logger = new MockLogger();
|
||||
mainNgcc({
|
||||
basePath: '/dist',
|
||||
propertiesToConsider: ['es2015'],
|
||||
tsConfigPath: _('/tsconfig.app.json'), logger
|
||||
});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
// The local-package-2 and local-package-4 will not be processed because there is no path
|
||||
// mappings for `@app` and plain local imports.
|
||||
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-2')}.`,
|
||||
'It is missing required dependencies:\n - @app/local-package'
|
||||
]);
|
||||
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-4')}.`,
|
||||
'It is missing required dependencies:\n - local-package'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the explicit `pathMappings`, ignoring the local tsconfig.json settings',
|
||||
() => {
|
||||
const logger = new MockLogger();
|
||||
fs.writeFile(
|
||||
_('/tsconfig.json'),
|
||||
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
|
||||
mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
propertiesToConsider: ['es2015'],
|
||||
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'}, logger
|
||||
});
|
||||
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
fesm2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
// The local-package-2 and local-package-3 will not be processed because there is no path
|
||||
// mappings for `@app` and `@x` local imports.
|
||||
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-2')}.`,
|
||||
'It is missing required dependencies:\n - @app/local-package'
|
||||
]);
|
||||
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-3')}.`,
|
||||
'It is missing required dependencies:\n - @x/local-package'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not use pathMappings from a local tsconfig.json path if tsConfigPath is null',
|
||||
() => {
|
||||
const logger = new MockLogger();
|
||||
fs.writeFile(
|
||||
_('/tsconfig.json'),
|
||||
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
|
||||
mainNgcc({
|
||||
basePath: '/dist',
|
||||
propertiesToConsider: ['es2015'],
|
||||
tsConfigPath: null, logger,
|
||||
});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
// Since the tsconfig is not loaded, the `@app/local-package` import in `local-package-2`
|
||||
// is not path-mapped correctly, and so it fails to be processed.
|
||||
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
|
||||
.toBeUndefined();
|
||||
expect(logger.logs.debug).toContain([
|
||||
`Invalid entry-point ${_('/dist/local-package-2')}.`,
|
||||
'It is missing required dependencies:\n - @app/local-package'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespace preservation', () => {
|
||||
it('should default not to preserve whitespace', () => {
|
||||
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(fs.readFile(_('/dist/local-package/index.js')))
|
||||
.toMatch(/ɵɵtext\(\d+, " Hello\\n"\);/);
|
||||
});
|
||||
|
||||
it('should preserve whitespace if set in a loaded tsconfig.json', () => {
|
||||
fs.writeFile(
|
||||
_('/tsconfig.json'),
|
||||
JSON.stringify({angularCompilerOptions: {preserveWhitespaces: true}}));
|
||||
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(fs.readFile(_('/dist/local-package/index.js')))
|
||||
.toMatch(/ɵɵtext\(\d+, "\\n Hello\\n"\);/);
|
||||
});
|
||||
|
||||
it('should not preserve whitespace if set to false in a loaded tsconfig.json', () => {
|
||||
fs.writeFile(
|
||||
_('/tsconfig.json'),
|
||||
JSON.stringify({angularCompilerOptions: {preserveWhitespaces: false}}));
|
||||
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
|
||||
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
|
||||
es2015: '0.0.0-PLACEHOLDER',
|
||||
typings: '0.0.0-PLACEHOLDER',
|
||||
});
|
||||
expect(fs.readFile(_('/dist/local-package/index.js')))
|
||||
.toMatch(/ɵɵtext\(\d+, " Hello\\n"\);/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1477,7 +1640,7 @@ runInEachFileSystem(() => {
|
||||
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta<DerivedCmp, "[base]", never, {}, {}, never>;');
|
||||
'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta<DerivedCmp, "[base]", never, {}, {}, never, never>;');
|
||||
});
|
||||
|
||||
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',
|
||||
@ -1748,7 +1911,7 @@ runInEachFileSystem(() => {
|
||||
},
|
||||
]);
|
||||
|
||||
// An Angular package that has been built locally and stored in the `dist` directory.
|
||||
// Angular packages that have been built locally and stored in the `dist` directory.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/dist/local-package/package.json'),
|
||||
@ -1758,12 +1921,60 @@ runInEachFileSystem(() => {
|
||||
{
|
||||
name: _('/dist/local-package/index.js'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '<h2>Hello</h2>'}] }\n];`
|
||||
`import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '<h2>\\n Hello\\n</h2>'}] }\n];`
|
||||
},
|
||||
{
|
||||
name: _('/dist/local-package/index.d.ts'),
|
||||
contents: `export declare class AppComponent {};`
|
||||
},
|
||||
// local-package-2 depends upon local-package, via an `@app` aliased import.
|
||||
{
|
||||
name: _('/dist/local-package-2/package.json'),
|
||||
contents: '{"name": "local-package-2", "es2015": "./index.js", "typings": "./index.d.ts"}'
|
||||
},
|
||||
{name: _('/dist/local-package-2/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||
{
|
||||
name: _('/dist/local-package-2/index.js'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport {AppComponent} from '@app/local-package';`
|
||||
},
|
||||
{
|
||||
name: _('/dist/local-package-2/index.d.ts'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport {AppComponent} from '@app/local-package';`
|
||||
},
|
||||
// local-package-3 depends upon local-package, via an `@x` aliased import.
|
||||
{
|
||||
name: _('/dist/local-package-3/package.json'),
|
||||
contents: '{"name": "local-package-3", "es2015": "./index.js", "typings": "./index.d.ts"}'
|
||||
},
|
||||
{name: _('/dist/local-package-3/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||
{
|
||||
name: _('/dist/local-package-3/index.js'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport {AppComponent} from '@x/local-package';`
|
||||
},
|
||||
{
|
||||
name: _('/dist/local-package-3/index.d.ts'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport {AppComponent} from '@x/local-package';`
|
||||
},
|
||||
// local-package-4 depends upon local-package, via a plain import.
|
||||
{
|
||||
name: _('/dist/local-package-4/package.json'),
|
||||
contents: '{"name": "local-package-", "es2015": "./index.js", "typings": "./index.d.ts"}'
|
||||
},
|
||||
{name: _('/dist/local-package-4/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||
{
|
||||
name: _('/dist/local-package-4/index.js'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport {AppComponent} from 'local-package';`
|
||||
},
|
||||
{
|
||||
name: _('/dist/local-package-4/index.d.ts'),
|
||||
contents:
|
||||
`import {Component} from '@angular/core';\nexport {AppComponent} from 'local-package';`
|
||||
},
|
||||
]);
|
||||
|
||||
// An Angular package that has a missing dependency
|
||||
|
@ -130,7 +130,7 @@ runInEachFileSystem(() => {
|
||||
result.find(f => f.path === _('/node_modules/test-package/typings/file.d.ts')) !;
|
||||
expect(typingsFile.contents)
|
||||
.toContain(
|
||||
'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef<A>;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta');
|
||||
'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef<A, never>;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta');
|
||||
});
|
||||
|
||||
it('should render imports into typings files', () => {
|
||||
|
@ -313,6 +313,7 @@ export class ComponentDecoratorHandler implements
|
||||
...metadata,
|
||||
template: {
|
||||
nodes: template.emitNodes,
|
||||
ngContentSelectors: template.ngContentSelectors,
|
||||
},
|
||||
encapsulation,
|
||||
interpolation: template.interpolation,
|
||||
@ -770,12 +771,13 @@ export class ComponentDecoratorHandler implements
|
||||
interpolation = InterpolationConfig.fromArray(value as[string, string]);
|
||||
}
|
||||
|
||||
const {errors, nodes: emitNodes, styleUrls, styles} = parseTemplate(templateStr, templateUrl, {
|
||||
preserveWhitespaces,
|
||||
interpolationConfig: interpolation,
|
||||
range: templateRange, escapedString,
|
||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||
});
|
||||
const {errors, nodes: emitNodes, styleUrls, styles, ngContentSelectors} =
|
||||
parseTemplate(templateStr, templateUrl, {
|
||||
preserveWhitespaces,
|
||||
interpolationConfig: interpolation,
|
||||
range: templateRange, escapedString,
|
||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||
});
|
||||
|
||||
// Unfortunately, the primary parse of the template above may not contain accurate source map
|
||||
// information. If used directly, it would result in incorrect code locations in template
|
||||
@ -804,6 +806,7 @@ export class ComponentDecoratorHandler implements
|
||||
diagNodes,
|
||||
styleUrls,
|
||||
styles,
|
||||
ngContentSelectors,
|
||||
errors,
|
||||
template: templateStr, templateUrl,
|
||||
isInline: component.has('template'),
|
||||
@ -923,6 +926,11 @@ export interface ParsedTemplate {
|
||||
*/
|
||||
styles: string[];
|
||||
|
||||
/**
|
||||
* Any ng-content selectors extracted from the template.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
|
||||
/**
|
||||
* Whether the template was inline.
|
||||
*/
|
||||
|
@ -274,6 +274,7 @@ function extractInjectableCtorDeps(
|
||||
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
|
||||
const meta: R3DependencyMetadata = {
|
||||
token: new WrappedNodeExpr(dep),
|
||||
attribute: null,
|
||||
host: false,
|
||||
resolved: R3ResolvedDependencyType.Token,
|
||||
optional: false,
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
|
||||
import {Expression, ExternalExpr, LiteralExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
|
||||
@ -48,6 +48,7 @@ export function getConstructorDependencies(
|
||||
}
|
||||
ctorParams.forEach((param, idx) => {
|
||||
let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder);
|
||||
let attribute: Expression|null = null;
|
||||
let optional = false, self = false, skipSelf = false, host = false;
|
||||
let resolved = R3ResolvedDependencyType.Token;
|
||||
|
||||
@ -74,7 +75,13 @@ export function getConstructorDependencies(
|
||||
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec),
|
||||
`Unexpected number of arguments to @Attribute().`);
|
||||
}
|
||||
token = new WrappedNodeExpr(dec.args[0]);
|
||||
const attributeName = dec.args[0];
|
||||
token = new WrappedNodeExpr(attributeName);
|
||||
if (ts.isStringLiteralLike(attributeName)) {
|
||||
attribute = new LiteralExpr(attributeName.text);
|
||||
} else {
|
||||
attribute = new WrappedNodeExpr(ts.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
|
||||
}
|
||||
resolved = R3ResolvedDependencyType.Attribute;
|
||||
} else {
|
||||
throw new FatalDiagnosticError(
|
||||
@ -93,7 +100,7 @@ export function getConstructorDependencies(
|
||||
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param,
|
||||
});
|
||||
} else {
|
||||
deps.push({token, optional, self, skipSelf, host, resolved});
|
||||
deps.push({token, attribute, optional, self, skipSelf, host, resolved});
|
||||
}
|
||||
});
|
||||
if (errors.length === 0) {
|
||||
@ -369,7 +376,8 @@ const parensWrapperTransformerFactory: ts.TransformerFactory<ts.Expression> =
|
||||
/**
|
||||
* Wraps all functions in a given expression in parentheses. This is needed to avoid problems
|
||||
* where Tsickle annotations added between analyse and transform phases in Angular may trigger
|
||||
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement. More
|
||||
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement.
|
||||
* More
|
||||
* info can be found in Tsickle source code here:
|
||||
* https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021
|
||||
*
|
||||
|
@ -205,7 +205,7 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
|
||||
const newMembers = fields.map(decl => {
|
||||
const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)];
|
||||
const typeRef = translateType(decl.type, imports);
|
||||
emitAsSingleLine(typeRef);
|
||||
markForEmitAsSingleLine(typeRef);
|
||||
return ts.createProperty(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ modifiers,
|
||||
@ -226,9 +226,9 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
|
||||
}
|
||||
}
|
||||
|
||||
function emitAsSingleLine(node: ts.Node) {
|
||||
function markForEmitAsSingleLine(node: ts.Node) {
|
||||
ts.setEmitFlags(node, ts.EmitFlags.SingleLine);
|
||||
ts.forEachChild(node, emitAsSingleLine);
|
||||
ts.forEachChild(node, markForEmitAsSingleLine);
|
||||
}
|
||||
|
||||
export class ReturnTypeTransform implements DtsTransform {
|
||||
|
@ -447,12 +447,12 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
`An ExpressionType with type arguments cannot have multiple levels of type arguments`);
|
||||
}
|
||||
|
||||
const typeArgs = type.typeParams.map(param => param.visitType(this, context));
|
||||
const typeArgs = type.typeParams.map(param => this.translateType(param, context));
|
||||
return ts.createTypeReferenceNode(typeNode.typeName, typeArgs);
|
||||
}
|
||||
|
||||
visitArrayType(type: ArrayType, context: Context): ts.ArrayTypeNode {
|
||||
return ts.createArrayTypeNode(this.translateType(type, context));
|
||||
return ts.createArrayTypeNode(this.translateType(type.of, context));
|
||||
}
|
||||
|
||||
visitMapType(type: MapType, context: Context): ts.TypeLiteralNode {
|
||||
@ -497,8 +497,18 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.LiteralTypeNode {
|
||||
return ts.createLiteralTypeNode(ts.createLiteral(ast.value as string));
|
||||
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.TypeNode {
|
||||
if (ast.value === null) {
|
||||
return ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword);
|
||||
} else if (ast.value === undefined) {
|
||||
return ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
|
||||
} else if (typeof ast.value === 'boolean') {
|
||||
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
|
||||
} else if (typeof ast.value === 'number') {
|
||||
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
|
||||
} else {
|
||||
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
|
||||
}
|
||||
}
|
||||
|
||||
visitLocalizedString(ast: LocalizedString, context: Context): never {
|
||||
@ -578,6 +588,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
return ts.createTypeReferenceNode(node, /* typeArguments */ undefined);
|
||||
} else if (ts.isTypeNode(node)) {
|
||||
return node;
|
||||
} else if (ts.isLiteralExpression(node)) {
|
||||
return ts.createLiteralTypeNode(node);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`);
|
||||
@ -590,8 +602,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
return ts.createTypeQueryNode(expr as ts.Identifier);
|
||||
}
|
||||
|
||||
private translateType(expr: Type, context: Context): ts.TypeNode {
|
||||
const typeNode = expr.visitType(this, context);
|
||||
private translateType(type: Type, context: Context): ts.TypeNode {
|
||||
const typeNode = type.visitType(this, context);
|
||||
if (!ts.isTypeNode(typeNode)) {
|
||||
throw new Error(
|
||||
`A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`);
|
||||
|
@ -34,6 +34,7 @@ export const Inject = callableParamDecorator();
|
||||
export const Self = callableParamDecorator();
|
||||
export const SkipSelf = callableParamDecorator();
|
||||
export const Optional = callableParamDecorator();
|
||||
export const Host = callableParamDecorator();
|
||||
|
||||
export const ContentChild = callablePropDecorator();
|
||||
export const ContentChildren = callablePropDecorator();
|
||||
@ -68,7 +69,8 @@ export function forwardRef<T>(fn: () => T): T {
|
||||
export interface SimpleChanges { [propName: string]: any; }
|
||||
|
||||
export type ɵɵNgModuleDefWithMeta<ModuleT, DeclarationsT, ImportsT, ExportsT> = any;
|
||||
export type ɵɵDirectiveDefWithMeta<DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT> = any;
|
||||
export type ɵɵDirectiveDefWithMeta<
|
||||
DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT, NgContentSelectorsT> = any;
|
||||
export type ɵɵPipeDefWithMeta<PipeT, NameT> = any;
|
||||
|
||||
export enum ViewEncapsulation {
|
||||
|
@ -68,8 +68,8 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep, never>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||
});
|
||||
|
||||
it('should compile Injectables with a generic service', () => {
|
||||
@ -86,7 +86,7 @@ runInEachFileSystem(os => {
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('Store.ɵprov =');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Store<any>>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Store<any>, never>;');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Store<any>>;');
|
||||
});
|
||||
|
||||
@ -117,8 +117,8 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep, never>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||
});
|
||||
|
||||
it('should compile Injectables with providedIn and factory without errors', () => {
|
||||
@ -143,7 +143,7 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).not.toContain('__decorate');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||
});
|
||||
|
||||
it('should compile Injectables with providedIn and factory with deps without errors', () => {
|
||||
@ -172,7 +172,7 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).not.toContain('__decorate');
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
|
||||
});
|
||||
|
||||
it('should compile @Injectable with an @Optional dependency', () => {
|
||||
@ -237,7 +237,7 @@ runInEachFileSystem(os => {
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[dir]", never, {}, {}, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir, never>');
|
||||
});
|
||||
|
||||
it('should compile abstract Directives without errors', () => {
|
||||
@ -259,7 +259,7 @@ runInEachFileSystem(os => {
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, never, never, {}, {}, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir, never>');
|
||||
});
|
||||
|
||||
it('should compile Components (inline template) without errors', () => {
|
||||
@ -283,8 +283,8 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
|
||||
});
|
||||
|
||||
it('should compile Components (dynamic inline template) without errors', () => {
|
||||
@ -309,8 +309,9 @@ runInEachFileSystem(os => {
|
||||
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta' +
|
||||
'<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
|
||||
});
|
||||
|
||||
it('should compile Components (function call inline template) without errors', () => {
|
||||
@ -337,8 +338,8 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
|
||||
});
|
||||
|
||||
it('should compile Components (external template) without errors', () => {
|
||||
@ -935,7 +936,7 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵmod: i0.ɵɵNgModuleDefWithMeta<TestModule, [typeof TestCmp], never, never>');
|
||||
@ -1327,7 +1328,7 @@ runInEachFileSystem(os => {
|
||||
.toContain(
|
||||
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
|
||||
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe, never>;');
|
||||
});
|
||||
|
||||
it('should compile pure Pipes without errors', () => {
|
||||
@ -1352,7 +1353,7 @@ runInEachFileSystem(os => {
|
||||
.toContain(
|
||||
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
|
||||
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe, never>;');
|
||||
});
|
||||
|
||||
it('should compile Pipes with dependencies', () => {
|
||||
@ -1393,7 +1394,7 @@ runInEachFileSystem(os => {
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents)
|
||||
.toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe<any>, "test-pipe">;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe<any>>;');
|
||||
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe<any>, never>;');
|
||||
});
|
||||
|
||||
it('should include @Pipes in @NgModule scopes', () => {
|
||||
@ -2574,6 +2575,141 @@ runInEachFileSystem(os => {
|
||||
`FooCmp.ɵfac = function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`);
|
||||
});
|
||||
|
||||
it('should include constructor dependency metadata for directives/components/pipes', () => {
|
||||
env.write(`test.ts`, `
|
||||
import {Attribute, Component, Directive, Pipe, Self, SkipSelf, Host, Optional} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
export function dynamic() {};
|
||||
|
||||
@Directive()
|
||||
export class WithDecorators {
|
||||
constructor(
|
||||
@Self() withSelf: MyService,
|
||||
@SkipSelf() withSkipSelf: MyService,
|
||||
@Host() withHost: MyService,
|
||||
@Optional() withOptional: MyService,
|
||||
@Attribute("attr") withAttribute: string,
|
||||
@Attribute(dynamic()) withAttributeDynamic: string,
|
||||
@Optional() @SkipSelf() @Host() withMany: MyService,
|
||||
noDecorators: MyService) {}
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class NoCtor {}
|
||||
|
||||
@Directive()
|
||||
export class EmptyCtor {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class WithoutDecorators {
|
||||
constructor(noDecorators: MyService) {}
|
||||
}
|
||||
|
||||
@Component({ template: 'test' })
|
||||
export class MyCmp {
|
||||
constructor(@Host() withHost: MyService) {}
|
||||
}
|
||||
|
||||
@Pipe({ name: 'test' })
|
||||
export class MyPipe {
|
||||
constructor(@Host() withHost: MyService) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵfac: i0.ɵɵFactoryDef<WithDecorators, [' +
|
||||
'{ self: true; }, { skipSelf: true; }, { host: true; }, ' +
|
||||
'{ optional: true; }, { attribute: "attr"; }, { attribute: unknown; }, ' +
|
||||
'{ optional: true; host: true; skipSelf: true; }, null]>');
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<NoCtor, never>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<EmptyCtor, never>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<WithoutDecorators, never>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<MyCmp, [{ host: true; }]>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<MyPipe, [{ host: true; }]>`);
|
||||
});
|
||||
|
||||
it('should include constructor dependency metadata for @Injectable', () => {
|
||||
env.write(`test.ts`, `
|
||||
import {Injectable, Self, Host} from '@angular/core';
|
||||
|
||||
export class MyService {}
|
||||
|
||||
@Injectable()
|
||||
export class Inj {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
|
||||
@Injectable({ useExisting: MyService })
|
||||
export class InjUseExisting {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
|
||||
@Injectable({ useClass: MyService })
|
||||
export class InjUseClass {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
|
||||
@Injectable({ useClass: MyService, deps: [[new Host(), MyService]] })
|
||||
export class InjUseClassWithDeps {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
|
||||
@Injectable({ useFactory: () => new Injectable(new MyService()) })
|
||||
export class InjUseFactory {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
|
||||
@Injectable({ useFactory: (service: MyService) => new Injectable(service), deps: [[new Host(), MyService]] })
|
||||
export class InjUseFactoryWithDeps {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
|
||||
@Injectable({ useValue: new Injectable(new MyService()) })
|
||||
export class InjUseValue {
|
||||
constructor(@Self() service: MyService) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<Inj, [{ self: true; }]>`);
|
||||
expect(dtsContents)
|
||||
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseExisting, [{ self: true; }]>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseClass, [{ self: true; }]>`);
|
||||
expect(dtsContents)
|
||||
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseClassWithDeps, [{ self: true; }]>`);
|
||||
expect(dtsContents)
|
||||
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseFactory, [{ self: true; }]>`);
|
||||
expect(dtsContents)
|
||||
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseFactoryWithDeps, [{ self: true; }]>`);
|
||||
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseValue, [{ self: true; }]>`);
|
||||
});
|
||||
|
||||
it('should include ng-content selectors in the metadata', () => {
|
||||
env.write(`test.ts`, `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'test',
|
||||
template: '<ng-content></ng-content> <ng-content select=".foo"></ng-content>',
|
||||
})
|
||||
export class TestCmp {
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const dtsContents = env.getContents('test.d.ts');
|
||||
expect(dtsContents)
|
||||
.toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test", never, {}, {}, never, ["*", ".foo"]>');
|
||||
});
|
||||
|
||||
it('should generate queries for components', () => {
|
||||
env.write(`test.ts`, `
|
||||
import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core';
|
||||
@ -6520,7 +6656,7 @@ export const Foo = Foo__PRE_R3__;
|
||||
export declare class NgZone {}
|
||||
|
||||
export declare class Testability {
|
||||
static ɵfac: i0.ɵɵFactoryDef<Testability>;
|
||||
static ɵfac: i0.ɵɵFactoryDef<Testability, never>;
|
||||
constructor(ngZone: NgZone) {}
|
||||
}
|
||||
`);
|
||||
|
@ -296,11 +296,12 @@ function convertR3DependencyMetadata(facade: R3DependencyMetadataFacade): R3Depe
|
||||
}
|
||||
return {
|
||||
token: tokenExpr,
|
||||
attribute: null,
|
||||
resolved: facade.resolved,
|
||||
host: facade.host,
|
||||
optional: facade.optional,
|
||||
self: facade.self,
|
||||
skipSelf: facade.skipSelf
|
||||
skipSelf: facade.skipSelf,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -141,6 +141,13 @@ export interface R3DependencyMetadata {
|
||||
*/
|
||||
token: o.Expression;
|
||||
|
||||
/**
|
||||
* If an @Attribute decorator is present, this is the literal type of the attribute name, or
|
||||
* the unknown type if no literal type is available (e.g. the attribute name is an expression).
|
||||
* Will be null otherwise.
|
||||
*/
|
||||
attribute: o.Expression|null;
|
||||
|
||||
/**
|
||||
* An enum indicating whether this dependency has special meaning to Angular and needs to be
|
||||
* injected specially.
|
||||
@ -180,6 +187,7 @@ export interface R3FactoryFn {
|
||||
export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
||||
const t = o.variable('t');
|
||||
const statements: o.Statement[] = [];
|
||||
let ctorDepsType: o.Type = o.NONE_TYPE;
|
||||
|
||||
// The type to instantiate via constructor invocation. If there is no delegated factory, meaning
|
||||
// this type is always created by constructor invocation, then this is the type-to-create
|
||||
@ -197,6 +205,8 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
||||
ctorExpr = new o.InstantiateExpr(
|
||||
typeForCtor,
|
||||
injectDependencies(meta.deps, meta.injectFn, meta.target === R3FactoryTarget.Pipe));
|
||||
|
||||
ctorDepsType = createCtorDepsType(meta.deps);
|
||||
}
|
||||
} else {
|
||||
const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`);
|
||||
@ -269,8 +279,9 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn {
|
||||
[new o.FnParam('t', o.DYNAMIC_TYPE)], body, o.INFERRED_TYPE, undefined,
|
||||
`${meta.name}_Factory`),
|
||||
statements,
|
||||
type: o.expressionType(
|
||||
o.importExpr(R3.FactoryDef, [typeWithParameters(meta.type.type, meta.typeArgumentCount)]))
|
||||
type: o.expressionType(o.importExpr(
|
||||
R3.FactoryDef,
|
||||
[typeWithParameters(meta.type.type, meta.typeArgumentCount), ctorDepsType]))
|
||||
};
|
||||
}
|
||||
|
||||
@ -319,6 +330,49 @@ function compileInjectDependency(
|
||||
}
|
||||
}
|
||||
|
||||
function createCtorDepsType(deps: R3DependencyMetadata[]): o.Type {
|
||||
let hasTypes = false;
|
||||
const attributeTypes = deps.map(dep => {
|
||||
const type = createCtorDepType(dep);
|
||||
if (type !== null) {
|
||||
hasTypes = true;
|
||||
return type;
|
||||
} else {
|
||||
return o.literal(null);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasTypes) {
|
||||
return o.expressionType(o.literalArr(attributeTypes));
|
||||
} else {
|
||||
return o.NONE_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
function createCtorDepType(dep: R3DependencyMetadata): o.LiteralMapExpr|null {
|
||||
const entries: {key: string, quoted: boolean, value: o.Expression}[] = [];
|
||||
|
||||
if (dep.resolved === R3ResolvedDependencyType.Attribute) {
|
||||
if (dep.attribute !== null) {
|
||||
entries.push({key: 'attribute', value: dep.attribute, quoted: false});
|
||||
}
|
||||
}
|
||||
if (dep.optional) {
|
||||
entries.push({key: 'optional', value: o.literal(true), quoted: false});
|
||||
}
|
||||
if (dep.host) {
|
||||
entries.push({key: 'host', value: o.literal(true), quoted: false});
|
||||
}
|
||||
if (dep.self) {
|
||||
entries.push({key: 'self', value: o.literal(true), quoted: false});
|
||||
}
|
||||
if (dep.skipSelf) {
|
||||
entries.push({key: 'skipSelf', value: o.literal(true), quoted: false});
|
||||
}
|
||||
|
||||
return entries.length > 0 ? o.literalMap(entries) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function useful for extracting `R3DependencyMetadata` from a Render2
|
||||
* `CompileTypeMetadata` instance.
|
||||
@ -348,7 +402,7 @@ export function dependenciesFromGlobalMetadata(
|
||||
// Construct the dependency.
|
||||
deps.push({
|
||||
token,
|
||||
resolved,
|
||||
attribute: null, resolved,
|
||||
host: !!dependency.isHost,
|
||||
optional: !!dependency.isOptional,
|
||||
self: !!dependency.isSelf,
|
||||
|
@ -52,6 +52,7 @@ export interface Render3ParseResult {
|
||||
errors: ParseError[];
|
||||
styles: string[];
|
||||
styleUrls: string[];
|
||||
ngContentSelectors: string[];
|
||||
}
|
||||
|
||||
export function htmlAstToRender3Ast(
|
||||
@ -73,6 +74,7 @@ export function htmlAstToRender3Ast(
|
||||
errors: allErrors,
|
||||
styleUrls: transformer.styleUrls,
|
||||
styles: transformer.styles,
|
||||
ngContentSelectors: transformer.ngContentSelectors,
|
||||
};
|
||||
}
|
||||
|
||||
@ -80,6 +82,7 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||
errors: ParseError[] = [];
|
||||
styles: string[] = [];
|
||||
styleUrls: string[] = [];
|
||||
ngContentSelectors: string[] = [];
|
||||
private inI18nBlock: boolean = false;
|
||||
|
||||
constructor(private bindingParser: BindingParser) {}
|
||||
@ -189,6 +192,8 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||
const selector = preparsedElement.selectAttr;
|
||||
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
|
||||
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);
|
||||
|
||||
this.ngContentSelectors.push(selector);
|
||||
} else if (isTemplateElement) {
|
||||
// `<ng-template>`
|
||||
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
||||
|
@ -129,6 +129,12 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||
* Parsed nodes of the template.
|
||||
*/
|
||||
nodes: t.Node[];
|
||||
|
||||
/**
|
||||
* Any ng-content selectors extracted from the template. Contains `null` when an ng-content
|
||||
* element without selector is present.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -22,7 +22,7 @@ import {CONTENT_ATTR, HOST_ATTR} from '../../style_compiler';
|
||||
import {BindingParser} from '../../template_parser/binding_parser';
|
||||
import {OutputContext, error} from '../../util';
|
||||
import {BoundEvent} from '../r3_ast';
|
||||
import {R3FactoryTarget, compileFactoryFunction} from '../r3_factory';
|
||||
import {R3DependencyMetadata, R3FactoryTarget, R3ResolvedDependencyType, compileFactoryFunction} from '../r3_factory';
|
||||
import {Identifiers as R3} from '../r3_identifiers';
|
||||
import {Render3ParseResult} from '../r3_template_transform';
|
||||
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
|
||||
@ -124,7 +124,9 @@ export function compileDirectiveFromMetadata(
|
||||
addFeatures(definitionMap, meta);
|
||||
const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]);
|
||||
|
||||
const type = createTypeForDef(meta, R3.DirectiveDefWithMeta);
|
||||
const typeParams = createDirectiveTypeParams(meta);
|
||||
const type = o.expressionType(o.importExpr(R3.DirectiveDefWithMeta, typeParams));
|
||||
|
||||
return {expression, type};
|
||||
}
|
||||
|
||||
@ -252,7 +254,11 @@ export function compileComponentFromMetadata(
|
||||
}
|
||||
|
||||
const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]);
|
||||
const type = createTypeForDef(meta, R3.ComponentDefWithMeta);
|
||||
|
||||
|
||||
const typeParams = createDirectiveTypeParams(meta);
|
||||
typeParams.push(stringArrayAsType(meta.template.ngContentSelectors));
|
||||
const type = o.expressionType(o.importExpr(R3.ComponentDefWithMeta, typeParams));
|
||||
|
||||
return {expression, type};
|
||||
}
|
||||
@ -311,7 +317,7 @@ export function compileComponentFromRender2(
|
||||
const meta: R3ComponentMetadata = {
|
||||
...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector),
|
||||
selector: component.selector,
|
||||
template: {nodes: render3Ast.nodes},
|
||||
template: {nodes: render3Ast.nodes, ngContentSelectors: render3Ast.ngContentSelectors},
|
||||
directives: [],
|
||||
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
|
||||
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
|
||||
@ -470,24 +476,24 @@ function stringMapAsType(map: {[key: string]: string | string[]}): o.Type {
|
||||
return o.expressionType(o.literalMap(mapValues));
|
||||
}
|
||||
|
||||
function stringArrayAsType(arr: string[]): o.Type {
|
||||
function stringArrayAsType(arr: ReadonlyArray<string|null>): o.Type {
|
||||
return arr.length > 0 ? o.expressionType(o.literalArr(arr.map(value => o.literal(value)))) :
|
||||
o.NONE_TYPE;
|
||||
}
|
||||
|
||||
function createTypeForDef(meta: R3DirectiveMetadata, typeBase: o.ExternalReference): o.Type {
|
||||
function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] {
|
||||
// On the type side, remove newlines from the selector as it will need to fit into a TypeScript
|
||||
// string literal, which must be on one line.
|
||||
const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null;
|
||||
|
||||
return o.expressionType(o.importExpr(typeBase, [
|
||||
return [
|
||||
typeWithParameters(meta.type.type, meta.typeArgumentCount),
|
||||
selectorForType !== null ? stringAsType(selectorForType) : o.NONE_TYPE,
|
||||
meta.exportAs !== null ? stringArrayAsType(meta.exportAs) : o.NONE_TYPE,
|
||||
stringMapAsType(meta.inputs),
|
||||
stringMapAsType(meta.outputs),
|
||||
stringArrayAsType(meta.queries.map(q => q.propertyName)),
|
||||
]));
|
||||
];
|
||||
}
|
||||
|
||||
// Define and update any view queries
|
||||
|
@ -1983,8 +1983,13 @@ export interface ParseTemplateOptions {
|
||||
* @param options options to modify how the template is parsed
|
||||
*/
|
||||
export function parseTemplate(
|
||||
template: string, templateUrl: string, options: ParseTemplateOptions = {}):
|
||||
{errors?: ParseError[], nodes: t.Node[], styleUrls: string[], styles: string[]} {
|
||||
template: string, templateUrl: string, options: ParseTemplateOptions = {}): {
|
||||
errors?: ParseError[],
|
||||
nodes: t.Node[],
|
||||
styleUrls: string[],
|
||||
styles: string[],
|
||||
ngContentSelectors: string[]
|
||||
} {
|
||||
const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options;
|
||||
const bindingParser = makeBindingParser(interpolationConfig);
|
||||
const htmlParser = new HtmlParser();
|
||||
@ -1993,7 +1998,13 @@ export function parseTemplate(
|
||||
{leadingTriviaChars: LEADING_TRIVIA_CHARS, ...options, tokenizeExpansionForms: true});
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
return {errors: parseResult.errors, nodes: [], styleUrls: [], styles: []};
|
||||
return {
|
||||
errors: parseResult.errors,
|
||||
nodes: [],
|
||||
styleUrls: [],
|
||||
styles: [],
|
||||
ngContentSelectors: []
|
||||
};
|
||||
}
|
||||
|
||||
let rootNodes: html.Node[] = parseResult.rootNodes;
|
||||
@ -2020,12 +2031,13 @@ export function parseTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
const {nodes, errors, styleUrls, styles} = htmlAstToRender3Ast(rootNodes, bindingParser);
|
||||
const {nodes, errors, styleUrls, styles, ngContentSelectors} =
|
||||
htmlAstToRender3Ast(rootNodes, bindingParser);
|
||||
if (errors && errors.length > 0) {
|
||||
return {errors, nodes: [], styleUrls: [], styles: []};
|
||||
return {errors, nodes: [], styleUrls: [], styles: [], ngContentSelectors: []};
|
||||
}
|
||||
|
||||
return {nodes, styleUrls, styles};
|
||||
return {nodes, styleUrls, styles, ngContentSelectors};
|
||||
}
|
||||
|
||||
const elementRegistry = new DomElementSchemaRegistry();
|
||||
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
|
@ -89,6 +89,39 @@ export interface DirectiveType<T> extends Type<T> {
|
||||
*/
|
||||
export interface PipeType<T> extends Type<T> { ɵpipe: never; }
|
||||
|
||||
/**
|
||||
* An object literal of this type is used to represent the metadata of a constructor dependency.
|
||||
* The type itself is never referred to from generated code.
|
||||
*/
|
||||
export type CtorDependency = {
|
||||
/**
|
||||
* If an `@Attribute` decorator is used, this represents the injected attribute's name. If the
|
||||
* attribute name is a dynamic expression instead of a string literal, this will be the unknown
|
||||
* type.
|
||||
*/
|
||||
attribute?: string | unknown;
|
||||
|
||||
/**
|
||||
* If `@Optional()` is used, this key is set to true.
|
||||
*/
|
||||
optional?: true;
|
||||
|
||||
/**
|
||||
* If `@Host` is used, this key is set to true.
|
||||
*/
|
||||
host?: true;
|
||||
|
||||
/**
|
||||
* If `@Self` is used, this key is set to true.
|
||||
*/
|
||||
self?: true;
|
||||
|
||||
/**
|
||||
* If `@SkipSelf` is used, this key is set to true.
|
||||
*/
|
||||
skipSelf?: true;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* @codeGenApi
|
||||
*/
|
||||
@ -236,12 +269,13 @@ export interface DirectiveDef<T> {
|
||||
*/
|
||||
export type ɵɵComponentDefWithMeta<
|
||||
T, Selector extends String, ExportAs extends string[], InputMap extends{[key: string]: string},
|
||||
OutputMap extends{[key: string]: string}, QueryFields extends string[]> = ComponentDef<T>;
|
||||
OutputMap extends{[key: string]: string}, QueryFields extends string[],
|
||||
NgContentSelectors extends string[]> = ComponentDef<T>;
|
||||
|
||||
/**
|
||||
* @codeGenApi
|
||||
*/
|
||||
export type ɵɵFactoryDef<T> = () => T;
|
||||
export type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;
|
||||
|
||||
/**
|
||||
* Runtime link information for Components.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -87,15 +87,6 @@ jasmine_node_test(
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_web",
|
||||
tags = [
|
||||
# FIXME: fix on saucelabs
|
||||
# IE 11.0.0 (Windows 8.1.0.0) ivy NgModule providers should throw when the aliased provider does not exist FAILED
|
||||
# Error: Expected function to throw an exception with message 'R3InjectorError(SomeModule)[car -> SportsCar]:
|
||||
# NullInjectorError: No provider for Car!', but it threw an exception with message 'R3InjectorError(SomeModule)[car -> Car]:
|
||||
# NullInjectorError: No provider for Car!'.
|
||||
# at <Jasmine>
|
||||
"fixme-saucelabs-ivy",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
|
@ -794,7 +794,7 @@ function declareTests(config?: {useJit: boolean}) {
|
||||
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
|
||||
let errorMsg = `NullInjectorError: No provider for ${stringify(SportsCar)}!`;
|
||||
if (ivyEnabled) {
|
||||
errorMsg = `R3InjectorError(SomeModule)[car -> SportsCar]: \n ` + errorMsg;
|
||||
errorMsg = `R3InjectorError(SomeModule)[car -> ${stringify(SportsCar)}]: \n ` + errorMsg;
|
||||
}
|
||||
expect(() => injector.get('car')).toThrowError(errorMsg);
|
||||
});
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {ɵɵComponentDefWithMeta, ɵɵPipeDefWithMeta as PipeDefWithMeta} from '@angular/core';
|
||||
|
||||
declare class SuperComponent {
|
||||
static ɵcmp: ɵɵComponentDefWithMeta<SuperComponent, '[super]', never, {}, {}, never>;
|
||||
static ɵcmp: ɵɵComponentDefWithMeta<SuperComponent, '[super]', never, {}, {}, never, never>;
|
||||
}
|
||||
|
||||
declare class SubComponent extends SuperComponent {
|
||||
@ -18,7 +18,7 @@ declare class SubComponent extends SuperComponent {
|
||||
// would produce type errors when the "strictFunctionTypes" option is enabled.
|
||||
onlyInSubtype: string;
|
||||
|
||||
static ɵcmp: ɵɵComponentDefWithMeta<SubComponent, '[sub]', never, {}, {}, never>;
|
||||
static ɵcmp: ɵɵComponentDefWithMeta<SubComponent, '[sub]', never, {}, {}, never, never>;
|
||||
}
|
||||
|
||||
declare class SuperPipe { static ɵpipe: PipeDefWithMeta<SuperPipe, 'super'>; }
|
||||
|
@ -66,8 +66,11 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
/** Initial input values that were set before the component was created. */
|
||||
private readonly initialInputValues = new Map<string, any>();
|
||||
|
||||
/** Set of inputs that were not initially set when the component was created. */
|
||||
private readonly uninitializedInputs = new Set<string>();
|
||||
/**
|
||||
* Set of component inputs that have not yet changed, i.e. for which `ngOnChanges()` has not
|
||||
* fired. (This is used to determine the value of `fistChange` in `SimpleChange` instances.)
|
||||
*/
|
||||
private readonly unchangedInputs = new Set<string>();
|
||||
|
||||
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
||||
|
||||
@ -130,7 +133,11 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strictEquals(value, this.getInputValue(property))) {
|
||||
// Ignore the value if it is strictly equal to the current value, except if it is `undefined`
|
||||
// and this is the first change to the value (because an explicit `undefined` _is_ strictly
|
||||
// equal to not having a value set at all, but we still need to record this as a change).
|
||||
if (strictEquals(value, this.getInputValue(property)) &&
|
||||
!((value === undefined) && this.unchangedInputs.has(property))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -164,12 +171,16 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
/** Set any stored initial inputs on the component's properties. */
|
||||
protected initializeInputs(): void {
|
||||
this.componentFactory.inputs.forEach(({propName}) => {
|
||||
if (this.implementsOnChanges) {
|
||||
// If the component implements `ngOnChanges()`, keep track of which inputs have never
|
||||
// changed so far.
|
||||
this.unchangedInputs.add(propName);
|
||||
}
|
||||
|
||||
if (this.initialInputValues.has(propName)) {
|
||||
// Call `setInputValue()` now that the component has been instantiated to update its
|
||||
// properties and fire `ngOnChanges()`.
|
||||
this.setInputValue(propName, this.initialInputValues.get(propName));
|
||||
} else {
|
||||
// Keep track of inputs that were not initialized in case we need to know this for
|
||||
// calling ngOnChanges with SimpleChanges
|
||||
this.uninitializedInputs.add(propName);
|
||||
}
|
||||
});
|
||||
|
||||
@ -235,8 +246,8 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstChange = this.uninitializedInputs.has(property);
|
||||
this.uninitializedInputs.delete(property);
|
||||
const isFirstChange = this.unchangedInputs.has(property);
|
||||
this.unchangedInputs.delete(property);
|
||||
|
||||
const previousValue = isFirstChange ? undefined : this.getInputValue(property);
|
||||
this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange);
|
||||
|
@ -93,22 +93,25 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||
|
||||
it('should call ngOnChanges with the change', () => {
|
||||
expectSimpleChanges(componentRef.instance.simpleChanges[0], {
|
||||
fooFoo: new SimpleChange(undefined, 'fooFoo-1', false),
|
||||
falsyNull: new SimpleChange(undefined, null, false),
|
||||
falsyEmpty: new SimpleChange(undefined, '', false),
|
||||
falsyFalse: new SimpleChange(undefined, false, false),
|
||||
falsyZero: new SimpleChange(undefined, 0, false),
|
||||
fooFoo: new SimpleChange(undefined, 'fooFoo-1', true),
|
||||
falsyUndefined: new SimpleChange(undefined, undefined, true),
|
||||
falsyNull: new SimpleChange(undefined, null, true),
|
||||
falsyEmpty: new SimpleChange(undefined, '', true),
|
||||
falsyFalse: new SimpleChange(undefined, false, true),
|
||||
falsyZero: new SimpleChange(undefined, 0, true),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call ngOnChanges with proper firstChange value', fakeAsync(() => {
|
||||
strategy.setInputValue('falsyUndefined', 'notanymore');
|
||||
strategy.setInputValue('fooFoo', 'fooFoo-2');
|
||||
strategy.setInputValue('barBar', 'barBar-1');
|
||||
strategy.setInputValue('falsyUndefined', 'notanymore');
|
||||
tick(16); // scheduler waits 16ms if RAF is unavailable
|
||||
(strategy as any).detectChanges();
|
||||
expectSimpleChanges(componentRef.instance.simpleChanges[1], {
|
||||
falsyUndefined: new SimpleChange(undefined, 'notanymore', false),
|
||||
fooFoo: new SimpleChange('fooFoo-1', 'fooFoo-2', false),
|
||||
barBar: new SimpleChange(undefined, 'barBar-1', true),
|
||||
falsyUndefined: new SimpleChange(undefined, 'notanymore', false),
|
||||
});
|
||||
}));
|
||||
});
|
||||
@ -296,9 +299,9 @@ function expectSimpleChanges(actual: SimpleChanges, expected: SimpleChanges) {
|
||||
Object.keys(expected).forEach(key => {
|
||||
expect(actual[key]).toBeTruthy(`Change should have included key ${key}`);
|
||||
if (actual[key]) {
|
||||
expect(actual[key].previousValue).toBe(expected[key].previousValue);
|
||||
expect(actual[key].currentValue).toBe(expected[key].currentValue);
|
||||
expect(actual[key].firstChange).toBe(expected[key].firstChange);
|
||||
expect(actual[key].previousValue).toBe(expected[key].previousValue, `${key}.previousValue`);
|
||||
expect(actual[key].currentValue).toBe(expected[key].currentValue, `${key}.currentValue`);
|
||||
expect(actual[key].firstChange).toBe(expected[key].firstChange, `${key}.firstChange`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,206 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
/* tslint:disable:no-console */
|
||||
const parseYaml = require('yaml').parse;
|
||||
const readFileSync = require('fs').readFileSync;
|
||||
const Minimatch = require('minimatch').Minimatch;
|
||||
const {exec, set, cd} = require('shelljs');
|
||||
const path = require('path');
|
||||
|
||||
// Exit early on shelljs errors
|
||||
set('-e');
|
||||
|
||||
// Regex Matcher for contains_any_globs conditions
|
||||
const CONTAINS_ANY_GLOBS_REGEX = /^'([^']+)',?$/;
|
||||
|
||||
// Full path of the angular project directory
|
||||
const ANGULAR_PROJECT_DIR = path.resolve(__dirname, '../..');
|
||||
// Change to the Angular project directory
|
||||
cd(ANGULAR_PROJECT_DIR);
|
||||
|
||||
// Whether to log verbosely
|
||||
const VERBOSE_MODE = process.argv.includes('-v');
|
||||
// Full path to PullApprove config file
|
||||
const PULL_APPROVE_YAML_PATH = path.resolve(ANGULAR_PROJECT_DIR, '.pullapprove.yml');
|
||||
// All relative path file names in the git repo, this is retrieved using git rather
|
||||
// that a glob so that we only get files that are checked in, ignoring things like
|
||||
// node_modules, .bazelrc.user, etc
|
||||
const ALL_FILES = exec('git ls-tree --full-tree -r --name-only HEAD', {silent: true})
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(_ => _);
|
||||
if (!ALL_FILES.length) {
|
||||
console.error(
|
||||
`No files were found to be in the git tree, did you run this command from \n` +
|
||||
`inside the angular repository?`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/** Gets the glob matching information from each group's condition. */
|
||||
function getGlobMatchersFromCondition(groupName, condition) {
|
||||
const trimmedCondition = condition.trim();
|
||||
const globMatchers = [];
|
||||
const badConditionLines = [];
|
||||
|
||||
// If the condition starts with contains_any_globs, evaluate all of the globs
|
||||
if (trimmedCondition.startsWith('contains_any_globs')) {
|
||||
trimmedCondition.split('\n')
|
||||
.slice(1, -1)
|
||||
.map(glob => {
|
||||
const trimmedGlob = glob.trim();
|
||||
const match = trimmedGlob.match(CONTAINS_ANY_GLOBS_REGEX);
|
||||
if (!match) {
|
||||
badConditionLines.push(trimmedGlob);
|
||||
return;
|
||||
}
|
||||
return match[1];
|
||||
})
|
||||
.filter(globString => !!globString)
|
||||
.forEach(globString => globMatchers.push({
|
||||
group: groupName,
|
||||
glob: globString,
|
||||
matcher: new Minimatch(globString, {dot: true}),
|
||||
matchCount: 0,
|
||||
}));
|
||||
}
|
||||
return [globMatchers, badConditionLines];
|
||||
}
|
||||
|
||||
/** Create logs for each review group. */
|
||||
function logGroups(groups) {
|
||||
Array.from(groups.entries()).sort().forEach(([groupName, globs]) => {
|
||||
console.groupCollapsed(groupName);
|
||||
Array.from(globs.values())
|
||||
.sort((a, b) => b.matchCount - a.matchCount)
|
||||
.forEach(glob => console.log(`${glob.glob} - ${glob.matchCount}`));
|
||||
console.groupEnd();
|
||||
});
|
||||
}
|
||||
|
||||
/** Logs a header within a text drawn box. */
|
||||
function logHeader(...params) {
|
||||
const totalWidth = 80;
|
||||
const fillWidth = totalWidth - 2;
|
||||
const headerText = params.join(' ').substr(0, fillWidth);
|
||||
const leftSpace = Math.ceil((fillWidth - headerText.length) / 2);
|
||||
const rightSpace = fillWidth - leftSpace - headerText.length;
|
||||
const fill = (count, content) => content.repeat(count);
|
||||
|
||||
console.log(`┌${fill(fillWidth, '─')}┐`);
|
||||
console.log(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`);
|
||||
console.log(`└${fill(fillWidth, '─')}┘`);
|
||||
}
|
||||
|
||||
/** Runs the pull approve verification check on provided files. */
|
||||
function runVerification(files) {
|
||||
// All of the globs created for each group's conditions.
|
||||
const allGlobs = [];
|
||||
// The pull approve config file.
|
||||
const pullApprove = readFileSync(PULL_APPROVE_YAML_PATH, {encoding: 'utf8'});
|
||||
// All of the PullApprove groups, parsed from the PullApprove yaml file.
|
||||
const parsedPullApproveGroups = parseYaml(pullApprove).groups;
|
||||
// All files which were found to match a condition in PullApprove.
|
||||
const matchedFiles = new Set();
|
||||
// All files which were not found to match a condition in PullApprove.
|
||||
const unmatchedFiles = new Set();
|
||||
// All PullApprove groups which matched at least one file.
|
||||
const matchedGroups = new Map();
|
||||
// All PullApprove groups which did not match at least one file.
|
||||
const unmatchedGroups = new Map();
|
||||
// All condition lines which were not able to be correctly parsed, by group.
|
||||
const badConditionLinesByGroup = new Map();
|
||||
// Total number of condition lines which were not able to be correctly parsed.
|
||||
let badConditionLineCount = 0;
|
||||
|
||||
// Get all of the globs from the PullApprove group conditions.
|
||||
Object.entries(parsedPullApproveGroups).map(([groupName, group]) => {
|
||||
for (const condition of group.conditions) {
|
||||
const [matchers, badConditions] = getGlobMatchersFromCondition(groupName, condition);
|
||||
if (badConditions.length) {
|
||||
badConditionLinesByGroup.set(groupName, badConditions);
|
||||
badConditionLineCount += badConditions.length;
|
||||
}
|
||||
allGlobs.push(...matchers);
|
||||
}
|
||||
});
|
||||
|
||||
if (badConditionLineCount) {
|
||||
console.log(`Discovered ${badConditionLineCount} parsing errors in PullApprove conditions`);
|
||||
console.log(`Attempted parsing using: ${CONTAINS_ANY_GLOBS_REGEX}`);
|
||||
console.log();
|
||||
console.log(`Unable to properly parse the following line(s) by group:`);
|
||||
for (const [groupName, badConditionLines] of badConditionLinesByGroup.entries()) {
|
||||
console.log(`- ${groupName}:`);
|
||||
badConditionLines.forEach(line => console.log(` ${line}`));
|
||||
}
|
||||
console.log();
|
||||
console.log(
|
||||
`Please correct the invalid conditions, before PullApprove verification can be completed`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check each file for if it is matched by a PullApprove condition.
|
||||
for (let file of files) {
|
||||
const matched = allGlobs.filter(glob => glob.matcher.match(file));
|
||||
matched.length ? matchedFiles.add(file) : unmatchedFiles.add(file);
|
||||
matched.forEach(glob => glob.matchCount++);
|
||||
}
|
||||
|
||||
// Add each glob for each group to a map either matched or unmatched.
|
||||
allGlobs.forEach(glob => {
|
||||
const groups = glob.matchCount ? matchedGroups : unmatchedGroups;
|
||||
const globs = groups.get(glob.group) || new Map();
|
||||
// Set the globs map in the groups map
|
||||
groups.set(glob.group, globs);
|
||||
// Set the glob in the globs map
|
||||
globs.set(glob.glob, glob);
|
||||
});
|
||||
|
||||
// PullApprove is considered verified if no files or groups are found to be unsed.
|
||||
const verificationSucceeded = !(unmatchedFiles.size || unmatchedGroups.size);
|
||||
|
||||
/**
|
||||
* Overall result
|
||||
*/
|
||||
logHeader('Result');
|
||||
if (verificationSucceeded) {
|
||||
console.log('PullApprove verification succeeded!');
|
||||
} else {
|
||||
console.log(`PullApprove verification failed.\n`);
|
||||
console.log(`Please update '.pullapprove.yml' to ensure that all necessary`);
|
||||
console.log(`files/directories have owners and all patterns that appear in`);
|
||||
console.log(`the file correspond to actual files/directories in the repo.`);
|
||||
}
|
||||
/**
|
||||
* File by file Summary
|
||||
*/
|
||||
logHeader('PullApprove file match results');
|
||||
console.groupCollapsed(`Matched Files (${matchedFiles.size} files)`);
|
||||
VERBOSE_MODE && matchedFiles.forEach(file => console.log(file));
|
||||
console.groupEnd();
|
||||
console.groupCollapsed(`Unmatched Files (${unmatchedFiles.size} files)`);
|
||||
unmatchedFiles.forEach(file => console.log(file));
|
||||
console.groupEnd();
|
||||
|
||||
/**
|
||||
* Group by group Summary
|
||||
*/
|
||||
logHeader('PullApprove group matches');
|
||||
console.groupCollapsed(`Matched Groups (${matchedGroups.size} groups)`);
|
||||
VERBOSE_MODE && logGroups(matchedGroups);
|
||||
console.groupEnd();
|
||||
console.groupCollapsed(`Unmatched Groups (${unmatchedGroups.size} groups)`);
|
||||
logGroups(unmatchedGroups);
|
||||
console.groupEnd();
|
||||
|
||||
// Provide correct exit code based on verification success.
|
||||
process.exit(verificationSucceeded ? 0 : 1);
|
||||
}
|
||||
|
||||
runVerification(ALL_FILES);
|
@ -153,6 +153,10 @@
|
||||
universal-analytics "^0.4.20"
|
||||
uuid "^3.3.2"
|
||||
|
||||
"@angular/dev-infra-private@angular/dev-infra-private-builds#3724a71":
|
||||
version "0.0.0"
|
||||
resolved "https://codeload.github.com/angular/dev-infra-private-builds/tar.gz/3724a71047361d85f4131d990f00a5aecdbc3ddc"
|
||||
|
||||
"@babel/code-frame@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
|
||||
|
Reference in New Issue
Block a user