Compare commits

...

33 Commits

Author SHA1 Message Date
b6bd8d7572 release: cut the v9.1.0 release 2020-03-25 09:23:48 -07:00
b08168bb90 release: cut the v9.1.0-rc.2 release 2020-03-24 15:50:38 -07:00
407fa42679 fix(common): let KeyValuePipe accept type unions with null (#36093)
`KeyValuePipe` currently accepts `null` values as well as `Map`s and a
few others. However, due to the way in which TS overloads work, a type
of `T|null` will not be accepted by `KeyValuePipe`'s signatures, even
though both `T` and `null` individually would be.

To make this work, each signature that accepts some type `T` has been
duplicated with a second one below it that accepts a `T|null` and
includes `null` in its return type.

Fixes #35743

PR Close #36093
2020-03-24 14:41:42 -07:00
aef432384a fix(ngcc): use preserve whitespaces from tsconfig if provided (#36189)
Previously ngcc never preserved whitespaces but this is at odds
with how the ViewEngine compiler works. In ViewEngine, library
templates are recompiled with the current application's tsconfig
settings, which meant that whitespace preservation could be set
in the application tsconfig file.

This commit allows ngcc to use the `preserveWhitespaces` setting
from tsconfig when compiling library templates. One should be aware
that this disallows different projects with different tsconfig settings
to share the same node_modules folder, with regard to whitespace
preservation. But this is already the case in the current ngcc since
this configuration is hard coded right now.

Fixes #35871

PR Close #36189
2020-03-24 14:25:06 -07:00
fb70083339 feat(compiler): add dependency info and ng-content selectors to metadata (#35695)
This commit augments the `FactoryDef` declaration of Angular decorated
classes to contain information about the parameter decorators used in
the constructor. If no constructor is present, or none of the parameters
have any Angular decorators, then this will be represented using the
`null` type. Otherwise, a tuple type is used where the entry at index `i`
corresponds with parameter `i`. Each tuple entry can be one of two types:

1. If the associated parameter does not have any Angular decorators,
   the tuple entry will be the `null` type.
2. Otherwise, a type literal is used that may declare at least one of
   the following properties:
   - "attribute": if `@Attribute` is present. The injected attribute's
   name is used as string literal type, or the `unknown` type if the
   attribute name is not a string literal.
   - "self": if `@Self` is present, always of type `true`.
   - "skipSelf": if `@SkipSelf` is present, always of type `true`.
   - "host": if `@Host` is present, always of type `true`.
   - "optional": if `@Optional` is present, always of type `true`.

   A property is only present if the corresponding decorator is used.

   Note that the `@Inject` decorator is currently not included, as it's
   non-trivial to properly convert the token's value expression to a
   type that is valid in a declaration file.

Additionally, the `ComponentDefWithMeta` declaration that is created for
Angular components has been extended to include all selectors on
`ng-content` elements within the component's template.

This additional metadata is useful for tooling such as the Angular
Language Service, as it provides the ability to offer suggestions for
directives/components defined in libraries. At the moment, such
tooling extracts the necessary information from the _metadata.json_
manifest file as generated by ngc, however this metadata representation
is being replaced by the information emitted into the declaration files.

Resolves FW-1870

PR Close #35695
2020-03-24 14:21:42 -07:00
c9c2408176 docs(elements): correct typo in custom elements image (#36090)
Fixes #36050

PR Close #36090
2020-03-24 10:36:11 -07:00
e066bddfe9 fix(elements): correctly handle setting inputs to undefined (#36140)
Previously, when an input property was initially set to `undefined` it
would not be correctly recognized as a change (and trigger
`ngOnChanges()`).

This commit ensures that explicitly setting an input to `undefined` is
correctly handled the same as setting the property to any other value.
This aligns the behavior of Angular custom elements with that of the
corresponding components when used directly (not as custom elements).

PR Close #36140
2020-03-24 10:29:33 -07:00
447a600477 fix(elements): correctly set SimpleChange#firstChange for pre-existing inputs (#36140)
Previously, when an input property was set on an `NgElement` before
instantiating the underlying component, the `SimpleChange` object passed
to `ngOnChanges()` would have `firstChange` set to false, even if this
was the first change (as far as the component instance was concerned).

This commit fixes this by ensuring `SimpleChange#firstChange` is set to
true on first change, regardless if the property was set before or after
instantiating the component. This alignthe behavior of Angular custom
elements with that of the corresponding components when used directly
(not as custom elements).

Jira issue: [FW-2007](https://angular-team.atlassian.net/browse/FW-2007)

Fixes #36130

PR Close #36140
2020-03-24 10:29:33 -07:00
70f9bfff43 docs: fix typo in testing component with dependencies (#36219)
Fixes #36210

PR Close #36219
2020-03-24 10:19:48 -07:00
57c02b044c build(docs-infra): upgrade cli command docs sources to 526c3cc37 (#36225)
Updating [angular#9.1.x](https://github.com/angular/angular/tree/9.1.x) from [cli-builds#9.1.x](https://github.com/angular/cli-builds/tree/9.1.x).

##
Relevant changes in [commit range](1bc653bac...526c3cc37):

**Modified**
- help/deploy.json
- help/doc.json
- help/e2e.json
- help/generate.json
- help/test.json
- help/update.json

PR Close #36225
2020-03-24 10:18:14 -07:00
6defe962c8 fix(ngcc): use path-mappings from tsconfig in dependency resolution (#36180)
When computing the dependencies between packages which are not in
node_modules, we may need to rely upon path-mappings to find the path
to the imported entry-point.

This commit allows ngcc to use the path-mappings from a tsconfig
file to find dependencies. By default any tsconfig.json file in the directory
above the `basePath` is loaded but it is possible to use a path to a
specific file by providing the `tsConfigPath` property to mainNgcc,
or to turn off loading any tsconfig file by setting `tsConfigPath` to `null`.
At the command line this is controlled via the `--tsconfig` option.

Fixes #36119

PR Close #36180
2020-03-24 10:16:13 -07:00
267bcb3e9c test(common): Add test for NgForOfContext.count (#36046)
`NgForOfContext.count` is the length of the iterable.

PR Close #36046
2020-03-24 10:15:12 -07:00
b0b66881b4 docs(common): Add missing entry for NgForOfContext.count (#36046)
`count` is available in `NgForOfContext` but it's missing in the docs.

PR Close #36046
2020-03-24 10:15:12 -07:00
9ff8d78bcd test(core): re-enable IE 10/11 test on SauceLabs (#35962)
I was not able to reproduce IE 10/11 failrue of the disabled
tests on SauceLabs any more. I did some cleanup of the test
in question but I doubt it was the root cause of the problem.

PR Close #35962
2020-03-24 10:14:48 -07:00
563b707497 fix(dev-infra): use @angular/dev-infra-private package for pullapprove verification (#35996)
Adds devDependency on @angular/dev-infra-private and removes the verify script
from tools, relying instead on the script from ng-dev.

PR Close #35996
2020-03-24 10:14:06 -07:00
5357e643b3 release: cut the v9.1.0-rc.1 release 2020-03-23 13:01:11 -07:00
f71d132f7c fix(core): workaround Terser inlining bug (#36200)
This variable name change works around https://github.com/terser/terser/issues/615, which was causing the JIT production tests to fail in the Angular CLI repository (https://github.com/angular/angular-cli/issues/17264).

PR Close #36200
2020-03-23 12:24:03 -07:00
ba3edda230 fix(docs-infra): change app-list-item to app-item-list (#35601)
The `app-list-item` component sounds like it is used for a single
item, however it renders a list of items. There were also
several changes in the documentation, where it was becoming
confusing if the `app-list-item` is using a single item or multiple
items. This commit fixes this issue. It renames the component and its
respective properties to make sure that the intention is very clear.

Closes #35598

PR Close #35601
2020-03-23 11:40:16 -07:00
0767d37c07 fix(localize): allow ICU expansion case to start with any character except } (#36123)
Previously, an expansion case could only start with an alpha numeric character.
This commit fixes this by allowing an expansion case to start with any character
except `}`.

The [ICU spec](http://userguide.icu-project.org/formatparse/messages) is pretty vague:

> Use a "select" argument to select sub-messages via a fixed set of keywords.

It does not specify what can be a "keyword" but from looking at the surrounding syntax it
appears that it can indeed be any string that does not contain a `}` character.

Closes #31586

PR Close #36123
2020-03-23 11:37:12 -07:00
8ba24578bc fix(dev-infra): change circular deps positional params to camelCase (#36165)
Changes the positional params for the circular deps tooling to
use camelCase as it requires being defined in camelCase while
in strict mode.  Additionally, remove the `version()` call as
the boolean arguement does not exist in current versions and
throws errors on execution.

PR Close #36165
2020-03-23 11:36:28 -07:00
133a97ad67 fix(dev-infra): prep ts-circular-deps to load via node_modules (#36165)
to run ts-circular-deps via installed node_modules, we needed to set
the hashbang of the script to be a node environment, and discover the
project directory based on where the script is run rather than the
scripts file location.

PR Close #36165
2020-03-23 11:36:28 -07:00
4e67a3ab3f docs: Add asterisk info in template syntax guide (#36176)
Add helpful alert for asterisk syntax in the `ngFor` section of template syntax guide

PR Close #36176
2020-03-23 11:36:09 -07:00
377f0010fc docs: Change important alert of ngFor (#36176)
Update the important alert of ngFor so that it has a unique format with that of ngIf

PR Close #36176
2020-03-23 11:36:09 -07:00
6e09129e4c docs(elements): Edge supports Web Components (#36182)
PR Close #36182
2020-03-23 11:35:49 -07:00
d80e51a6b1 build(docs-infra): upgrade cli command docs sources to 1bc653bac (#36160)
Updating [angular#9.1.x](https://github.com/angular/angular/tree/9.1.x) from [cli-builds#9.0.x](https://github.com/angular/cli-builds/tree/9.0.x).

##
Relevant changes in [commit range](6aa3c134c...1bc653bac):

**Modified**
- help/deploy.json
- help/doc.json
- help/generate.json
- help/test.json
- help/update.json

PR Close #36160
2020-03-20 13:58:57 -07:00
feb66b00da docs(zone.js): Typos on zone.md file and fixes on code examples. (#36138)
1. During reading the documentation I found some code examples that were refering to the class properties via methods, but without specifying the context `this`.
2. The 'onInvoke' hook was duplicated
3. A minor typo on `Zones and execution contexts` section
4. A minor typo on `Zones and async lifecycle hooks` section

PR Close #36138
2020-03-20 13:57:01 -07:00
cb19eac105 fix(docs-infra): include correct dependencies in StackBlitz examples (#36071)
Previously, all StackBlitz examples included the default dependencies
for `cli`-type projects. However, different example types may have
different `package.json` files with different dependencies.
For example, the [boilerplate `package.json`][1] for `elements` examples
includes an extra dependency on `@angular/elements`.

This commit changes `StackblitzBuilder` to use the dependencies that
correspond to each example type.
(NOTE: Manually verified the changes.)

Jira issue: [FW-2002][2]

[1]: https://github.com/angular/angular/blob/05d058622/aio/tools/examples/shared/boilerplate/elements/package.json
[2]: https://angular-team.atlassian.net/browse/FW-2002

PR Close #36071
2020-03-20 13:56:26 -07:00
6e0564ade6 refactor(docs-infra): clean up stackblitz-builder/builder.js script (#36071)
- Remove unused dependencies.
- Change `var` to `const/let`.
- Change regular functions as callbacks to arrow functions.
- Remove unnecessary intermediate variables.
- Switch from custom `_existsSync()` implementation to built-in
  `fs.existsSync()`.

PR Close #36071
2020-03-20 13:56:26 -07:00
05eeb7d279 test(compiler): remove whitespace in spans (#36169)
https://github.com/angular/angular/pull/36133 and https://github.com/angular/angular/pull/35986
caused a conflict in test after they both got merged to master.
This PR fixes the failed tests.

PR Close #36169
2020-03-20 12:52:31 -07:00
2ce5fa3cce feat(compiler): Propagate value span of ExpressionBinding to ParsedProperty (#36133)
This commit propagates the correct value span in an ExpressionBinding of
a microsyntax expression to ParsedProperty, which in turn porpagates the
span to the template ASTs (both VE and Ivy).

PR Close #36133
2020-03-20 10:21:11 -07:00
e140cdcb34 fix(docs-infra): fix image name in example (#36127)
Closes #35618

PR Close #36127
2020-03-20 10:20:36 -07:00
14b2db1d43 feat(dev-infra): create commit-message validation script/tooling (#36117)
PR Close #36117
2020-03-20 10:20:13 -07:00
2afc7e982e refactor(benchpress): delete broken code (#35922)
PR Close #35922
2020-03-20 10:19:49 -07:00
83 changed files with 1494 additions and 995 deletions

View File

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

View File

@ -958,7 +958,6 @@ groups:
'tools/ngcontainer/**',
'tools/npm/**',
'tools/npm_integration_test/**',
'tools/pullapprove/**',
'tools/rxjs/**',
'tools/saucelabs/**',
'tools/size-tracking/**',

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ export class AppComponent {
// #enddocregion parent-data-type
// #docregion pass-object
currentItem = [{
currentItems = [{
id: 21,
name: 'phone'
}];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ ts_library(
srcs = [
"config.ts",
],
module_name = "@angular/dev-infra-private/utils",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"@npm//@types/json5",

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
*.xpi
addon-sdk*

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
{ "version" : "0.0.1", "main" : "lib/main.js", "name" : "ffperf-addon" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[];
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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