Compare commits

...

93 Commits

Author SHA1 Message Date
Misko Hevery
d36828a7a1 release: cut the v10.1.0-next.8 release 2020-08-24 15:38:13 -07:00
Misko Hevery
f18e2d5898 docs: release notes for the v10.0.12 release 2020-08-24 15:31:03 -07:00
Alan Agius
375f0a6f67 build: update tslint to 6.1.3 (#38076)
This version supports TypeScript 4.0

PR Close #38076
2020-08-24 13:07:05 -07:00
Alan Agius
281b647f15 refactor(compiler-cli): remove usage of ts.updateIdentifier (#38076)
With Typescript 4, `ts.updateIdentifier` is no longer available.
Calling `ts.updateIdentifier` used to return the same node when
`typeArguments` was `undefined` because `node.typeArguments`
was also `undefined`.

Relevant TS code:
```js
function updateIdentifier(node, typeArguments) {
  return node.typeArguments !== typeArguments
      ? updateNode(createIdentifier(ts.idText(node), typeArguments), node)
      : node;
}
```

PR Close #38076
2020-08-24 13:07:02 -07:00
Alan Agius
0fc44e0436 feat(compiler-cli): add support for TypeScript 4.0 (#38076)
With this change we add support for TypeScript 4.0

PR Close #38076
2020-08-24 13:06:59 -07:00
Sonu Kapoor
201a546af8 perf(forms): use internal ngDevMode flag to tree-shake error messages in prod builds (#37821)
This commit adds a guard before throwing any forms errors. This will tree-shake
error messages which cannot be minified. It should also help to reduce the
bundle size of the `forms` package in production by ~20%.

Closes #37697

PR Close #37821
2020-08-24 09:26:28 -07:00
windmichael
6e643d9874 docs(localize): fix angular.json syntax (#38553)
In chapter internationalization (i18n) at section "Deploy multiple locales" the syntax for angular.json is wrong.
This commit fixes the angular.json, when specifying the translation file and the baseHref for a locale.

PR Close #38553
2020-08-24 09:25:35 -07:00
Keen Yee Liau
4985267211 test(language-service): [Ivy] return cursor position in overwritten template (#38552)
In many testing scenarios, there is a common pattern:

1. Overwrite template (inline or external)
2. Find cursor position
3. Call one of language service APIs
4. Inspect spans in result

In order to faciliate this pattern, this commit refactors
`MockHost.overwrite()` and `MockHost.overwriteInlineTemplate()` to
allow a faux cursor symbol `¦` to be injected into the template, and
the methods will automatically remove it before updating the script snapshot.
Both methods will return the cursor position and the new text without
the cursor symbol.

This makes testing very convenient. Here's a typical example:

```ts
const {position, text} = mockHost.overwrite('template.html', `{{ ti¦tle }}`);
const quickInfo = ngLS.getQuickInfoAtPosition('template.html', position);
const {start, length} = quickInfo!.textSpan;
expect(text.substring(start, start + length)).toBe('title');
```

PR Close #38552
2020-08-24 09:25:04 -07:00
Keen Yee Liau
b48cc6ead5 feat(language-service): introduce hybrid visitor to locate AST node (#38540)
This commit introduces two visitors, one for Template AST and the other
for Expression AST to allow us to easily find the node that most closely
corresponds to a given cursor position.

This is crucial because many language service APIs take in a `position`
parameter, and the information returned depends on how well we can find
a good candidate node.

In View Engine implementation of language service, the search for the node
and the processing of information to return the result are strongly coupled.
This makes the code hard to understand and hard to debug because the stack
trace is often littered with layers of visitor calls.

With this new feature, we could test the "searching" part separately and
colocate all the logic (aka hacks) that's required to retrieve an accurate
span for a given node.

Right now, only the most "narrow" node is returned by the main exported
function `findNodeAtPosition`. If needed, we could expose the entire AST
path, or expose other methods to provide more context for a node.

Note that due to limitations in the template AST interface, there are
a few known cases where microsyntax spans are not recorded properly.
This will be dealt with in a follow-up PR.

PR Close #38540
2020-08-24 09:24:18 -07:00
JoostK
874792dc43 feat(compiler): support unary operators for more accurate type checking (#37918)
Prior to this change, the unary + and - operators would be parsed as `x - 0`
and `0 - x` respectively. The runtime semantics of these expressions are
equivalent, however they may introduce inaccurate template type checking
errors as the literal type is lost, for example:

```ts
@Component({
  template: `<button [disabled]="isAdjacent(-1)"></button>`
})
export class Example {
  isAdjacent(direction: -1 | 1): boolean { return false; }
}
```

would incorrectly report a type-check error:

> error TS2345: Argument of type 'number' is not assignable to parameter
  of type '-1 | 1'.

Additionally, the translated expression for the unary + operator would be
considered as arithmetic expression with an incompatible left-hand side:

> error TS2362: The left-hand side of an arithmetic operation must be of
  type 'any', 'number', 'bigint' or an enum type.

To resolve this issues, the implicit transformation should be avoided.
This commit adds a new unary AST node to represent these expressions,
allowing for more accurate type-checking.

Fixes #20845
Fixes #36178

PR Close #37918
2020-08-21 12:25:53 -07:00
crisbeto
e7da4040d6 fix(compiler-cli): adding references to const enums in runtime code (#38542)
We had a couple of places where we were assuming that if a particular
symbol has a value, then it will exist at runtime. This is true in most cases,
but it breaks down for `const` enums.

Fixes #38513.

PR Close #38542
2020-08-21 12:23:21 -07:00
Ajit Singh
2a643e1ab6 docs: change function name from async -> waitForAsync (#38548)
async function name was changed to waitForAsync but it was left in testing-utility-api
file.

PR Close #38548
2020-08-21 12:17:51 -07:00
George Kalpakas
364284b0dc fix(dev-infra): ignore comments when validating commit messages (#38438)
When creating a commit with the git cli, git pre-populates the editor
used to enter the commit message with some comments (i.e. lines starting
with `#`). These comments contain helpful instructions or information
regarding the changes that are part of the commit. As happens with all
commit message comments, they are removed by git and do not end up in
the final commit message.

However, the file that is passed to the `commit-msg` to be validated
still contains these comments. This may affect the outcome of the commit
message validation. In such cases, the author will not realize that the
commit message is not in the desired format until the linting checks
fail on CI (which validates the final commit messages and is not
affected by this issue), usually several minutes later.

Possible ways in which the commit message validation outcome can be
affected:
- The minimum body length check may pass incorrectly, even if there is
  no actual body, because the comments are counted as part of the body.
- The maximum line length check may fail incorrectly due to a very long
  line in the comments.

This commit fixes the problem by removing comment lines before
validating a commit message.

Fixes #37865

PR Close #38438
2020-08-21 12:17:14 -07:00
Aristeidis Bampakos
956b25a100 docs: apply code styling in template reference variables guide (#38522)
PR Close #38522
2020-08-20 13:01:33 -07:00
Ajit Singh
8017ca4db3 fix(docs-infra): fix vertical alignment of external link icons (#38410)
At some places external link icons appear as a subscript. For example
8366effeec/aio/content/guide/roadmap.md\#L37
this commit places external link icons in the middle to improve there
positioning in a line.

PR Close #38410
2020-08-20 09:40:01 -07:00
Santosh Yadav
22f1ac3e37 docs: udpate the details (#37967)
updating my twitter handle and bio as it is changed from
Angular and Web Tech to Angular also the
twitter handle is changed to SantoshYadavDev

PR Close #37967
2020-08-20 09:38:58 -07:00
George Kalpakas
4ee5e730ab build: upgrade cli command docs sources to ef770f1cb (#38546)
Updating [angular#master](https://github.com/angular/angular/tree/master) from
[cli-builds#master](https://github.com/angular/cli-builds/tree/master).

##
Relevant changes in
[commit range](b0b27361d...ef770f1cb):

**Modified**
- help/build.json
- help/generate.json
- help/test.json
- help/xi18n.json

PR Close #38546
2020-08-20 09:32:11 -07:00
Leon Yu
6442875c99 docs(core): Fix typo in JSDoc for AbstractType<T> (#38541)
PR Close #38541
2020-08-20 09:30:17 -07:00
Misko Hevery
8f24bc9443 Revert "fix(router): support lazy loading for empty path named outlets (#38379)"
This reverts commit 7ad32649c0d0004fcc3604c62cf0c1ae159a825b.
2020-08-19 21:05:31 -07:00
Pete Bacon Darwin
ac461e1efd fix(localize): extract the correct message ids (#38498)
Previously, if `useLegacyIds` was enabled, the message extractor
was always rendering the legacy message ids in translation
files even if an explicit "custom message id" had been provided
in the original message.

PR Close #38498
2020-08-19 14:19:41 -07:00
Bjarki
f245c6bb15 fix(core): remove closing body tag from inert DOM builder (#38454)
Fix a bug in the HTML sanitizer where an unclosed iframe tag would
result in an escaped closing body tag as the output:

_sanitizeHtml(document, '<iframe>') => '&lt;/body&gt;'

This closing body tag comes from the DOMParserHelper where the HTML to be
sanitized is wrapped with surrounding body tags. When an opening iframe
tag is parsed by DOMParser, which DOMParserHelper uses, everything up
until its matching closing tag is consumed as a text node. In the above
example this includes the appended closing body tag.

By removing the explicit closing body tag from the DOMParserHelper and
relying on the body tag being closed implicitly at the end, the above
example is sanitized as expected:

_sanitizeHtml(document, '<iframe>') => ''

PR Close #38454
2020-08-19 14:18:44 -07:00
Pete Bacon Darwin
68a9a01a64 fix(localize): parse all parts of a translation with nested HTML (#38452)
Previously nested container placeholders (i.e. HTML elements) were
not being fully parsed from translation files. This resulted in bad
translation of messages that contain these placeholders.

Note that this causes the canonical message ID to change for
such messages. Currently all messages generated from
templates use "legacy" message ids that are not affected by
this change, so this fix should not be seen as a breaking change.

Fixes #38422

PR Close #38452
2020-08-19 14:16:41 -07:00
Pete Bacon Darwin
8cd4099db9 fix(localize): include the last placeholder in parsed translation text (#38452)
When creating a `ParsedTranslation` from a set of message parts and
placeholder names a textual representation of the message is computed.
Previously the last placeholder and text segment were missing from this
computed message string.

PR Close #38452
2020-08-19 14:16:38 -07:00
Alex Rickabaugh
0b54c0c6b4 refactor(compiler-cli): add getTemplateOfComponent to TemplateTypeChecker (#38355)
This commit adds a `getTemplateOfComponent` method to the
`TemplateTypeChecker` API, which retrieves the actual nodes parsed and used
by the compiler for template type-checking. This is advantageous for the
language service, which may need to query other APIs in
`TemplateTypeChecker` that require the same nodes used to bind the template
while generating the TCB.

Fixes #38352

PR Close #38355
2020-08-19 14:07:03 -07:00
Aristeidis Bampakos
1ec609946f docs: Typos fixes in the binding syntax guide (#38519)
PR Close #38519
2020-08-19 14:05:48 -07:00
Andrew Scott
7ad32649c0 fix(router): support lazy loading for empty path named outlets (#38379)
In general, the router only matches and loads a single Route config tree. However,
named outlets with empty paths are a special case where the router can
and should actually match two different `Route`s and ensure that the
modules are loaded for each match.

This change updates the "ApplyRedirects" stage to ensure that named
outlets with empty paths finish loading their configs before proceeding
to the next stage in the routing pipe. This is necessary because if the
named outlet has `loadChildren` but the associated lazy config is not loaded
before following stages attempt to match and activate relevant `Route`s,
an error will occur.

fixes #12842

PR Close #38379
2020-08-19 11:36:06 -07:00
Misko Hevery
9ad69c1503 release: cut the zone.js-0.11.1 release (#38537)
PR Close #38537
2020-08-19 10:50:46 -07:00
atscott
9af2de821c release: cut the v10.1.0-next.7 release 2020-08-19 09:35:47 -07:00
atscott
0270020ac2 docs: release notes for the v10.0.11 release 2020-08-19 09:16:16 -07:00
JiaLiPassion
6b662d10c1 fix(zone.js): zone.js package.json should not include files/directories field (#38528)
Close #38526, #38516, #38513

After update to `APF`, the `directories` and `files` options are not compatible,
so we need to remove those fileds to make sure everything work as expected.

PR Close #38528
2020-08-19 09:06:28 -07:00
Aristeidis Bampakos
55fd725e74 docs: Fix typo in the inputs and outputs guide (#38524)
PR Close #38524
2020-08-19 08:27:44 -07:00
Joey Perrott
f77fd5e02a feat(dev-infra): create a wizard for building commit messages (#38457)
Creates a wizard to walk through creating a commit message in the correct
template for commit messages in Angular repositories.

PR Close #38457
2020-08-18 17:01:14 -07:00
Joey Perrott
63ba74fe4e feat(dev-infra): tooling to check out pending PR (#38474)
Creates a tool within ng-dev to checkout a pending PR from the upstream repository.  This automates
an action that many developers on the Angular team need to do periodically in the process of testing
and reviewing incoming PRs.

Example usage:
  ng-dev pr checkout <pr-number>

PR Close #38474
2020-08-18 16:22:47 -07:00
JiaLiPassion
aaa1d8e2fe release: cut the zone.js-0.11.0 release (#38473)
PR Close #38473
2020-08-18 11:47:23 -07:00
Andrew Scott
dbfb50e9f4 fix(router): ensure routerLinkActive updates when associated routerLinks change (#38511)
This commit introduces a new subscription in the `routerLinkActive` directive which triggers an update
when any of its associated routerLinks have changes. `RouterLinkActive` not only needs to know when
links are added or removed, but it also needs to know about if a link it already knows about
changes in some way.

Quick note that `from...mergeAll` is used instead of just a simple
`merge` (or `scheduled...mergeAll`) to avoid introducing new rxjs
operators in order to keep bundle size down.

Fixes #18469

PR Close #38511
2020-08-18 10:21:49 -07:00
Andrew Scott
bee44b3359 Revert "fix(router): ensure routerLinkActive updates when associated routerLinks change (#38349)" (#38511)
This reverts commit e0e5c9f195460c7626c30248e7a79de9a2ebe377.
Failures in Google tests were detected.

PR Close #38511
2020-08-18 10:21:47 -07:00
Andrea Balducci
723a9ff095 docs(common): Wrong parameter description on TrackBy (#38495)
Track By Function receive the T[index] data, not the node id.
TrackByFunction reference description has the same issue.
PR Close #38495
2020-08-18 10:08:44 -07:00
Joey Perrott
e472f5f688 refactor(ngcc): update yargs and typings for yargs (#38470)
Updating yargs and typings for the updated yargs module.

PR Close #38470
2020-08-17 15:30:33 -07:00
Joey Perrott
8373b720f3 refactor(localize): update yargs and typings for yargs (#38470)
Updating yargs and typings for the updated yargs module.

PR Close #38470
2020-08-17 15:30:32 -07:00
Joey Perrott
301513311e refactor(dev-infra): update yargs and typings for yargs (#38470)
Updating yargs and typings for the updated yargs module.

PR Close #38470
2020-08-17 15:30:32 -07:00
atscott
64cf087ae5 release: cut the v10.1.0-next.6 release 2020-08-17 13:23:09 -07:00
atscott
fec9dcbeb0 docs: release notes for the v10.0.10 release 2020-08-17 13:19:03 -07:00
Andrew Scott
e0e5c9f195 fix(router): ensure routerLinkActive updates when associated routerLinks change (#38349)
This commit introduces a new subscription in the `routerLinkActive` directive which triggers an update
when any of its associated routerLinks have changes. `RouterLinkActive` not only needs to know when
links are added or removed, but it also needs to know about if a link it already knows about
changes in some way.

Quick note that `from...mergeAll` is used instead of just a simple
`merge` (or `scheduled...mergeAll`) to avoid introducing new rxjs
operators in order to keep bundle size down.

Fixes #18469

PR Close #38349
2020-08-17 12:33:59 -07:00
Keen Yee Liau
cfe424e875 refactor(language-service): [Ivy] remove temporary compiler (#38310)
Now that Ivy compiler has a proper `TemplateTypeChecker` interface
(see https://github.com/angular/angular/pull/38105) we no longer need to
keep the temporary compiler implementation.

The temporary compiler was created to enable testing infrastructure to
be developed for the Ivy language service.

This commit removes the whole `ivy/compiler` directory and moves two
functions `createTypeCheckingProgramStrategy` and
`getOrCreateTypeCheckScriptInfo` to the `LanguageService` class.

Also re-enable the Ivy LS test since it's no longer blocking development.

PR Close #38310
2020-08-17 11:30:33 -07:00
Paul Gschwendtner
3b9c802dee fix(ngcc): detect synthesized delegate constructors for downleveled ES2015 classes (#38463)
Similarly to the change we landed in the `@angular/core` reflection
capabilities, we need to make sure that ngcc can detect pass-through
delegate constructors for classes using downleveled ES2015 output.

More details can be found in the preceding commit, and in the issue
outlining the problem: #38453.

Fixes #38453.

PR Close #38463
2020-08-17 10:55:40 -07:00
Paul Gschwendtner
ca07da4563 fix(core): detect DI parameters in JIT mode for downleveled ES2015 classes (#38463)
In the Angular Package Format, we always shipped UMD bundles and previously even ES5 module output.
With V10, we removed the ES5 module output but kept the UMD ES5 output.

For this, we were able to remove our second TypeScript transpilation. Instead we started only
building ES2015 output and then downleveled it to ES5 UMD for the NPM packages. This worked
as expected but unveiled an issue in the `@angular/core` reflection capabilities.

In JIT mode, Angular determines constructor parameters (for DI) using the `ReflectionCapabilities`. The
reflection capabilities basically read runtime metadata of classes to determine the DI parameters. Such
metadata can be either stored in static class properties like `ctorParameters` or within TypeScript's `design:params`.

If Angular comes across a class that does not have any parameter metadata, it tries to detect if the
given class is actually delegating to an inherited class. It does this naively in JIT by checking if the
stringified class (function in ES5) matches a certain pattern. e.g.

```js
function MatTable() {
  var _this = _super.apply(this, arguments) || this;
```

These patterns are reluctant to changes of the class output. If a class is not recognized properly, the
DI parameters will be assumed empty and the class is **incorrectly** constructed without arguments.

This actually happened as part of v10 now. Since we downlevel ES2015 to ES5 (instead of previously
compiling sources directly to ES5), the class output changed slightly so that Angular no longer detects
it. e.g.

```js
var _this = _super.apply(this, __spread(arguments)) || this;
```

This happens because the ES2015 output will receive an auto-generated constructor if the class
defines class properties. This constructor is then already containing an explicit `super` call.

```js
export class MatTable extends CdkTable {
    constructor() {
        super(...arguments);
        this.disabled = true;
    }
}
```

If we then downlevel this file to ES5 with `--downlevelIteration`, TypeScript adjusts the `super` call so that
the spread operator is no longer used (not supported in ES5). The resulting super call is different to the
super call that would have been emitted if we would directly transpile to ES5. Ultimately, Angular no
longer detects such classes as having an delegate constructor -> and DI breaks.

We fix this by expanding the rather naive RegExp patterns used for the reflection capabilities
so that downleveled pass-through/delegate constructors are properly detected. There is a risk
of a false-positive as we cannot detect whether `__spread` is actually the TypeScript spread
helper, but given the reflection patterns already make lots of assumptions (e.g. that `super` is
actually the superclass, we should be fine making this assumption too. The false-positive would
not result in a broken app, but rather in unnecessary providers being injected (as a noop).

Fixes #38453

PR Close #38463
2020-08-17 10:55:37 -07:00
Pete Bacon Darwin
81c3e809aa fix(localize): render ICU placeholders in extracted translation files (#38484)
Previously placeholders were only rendered for dynamic interpolation
expressons in `$localize` tagged strings. But there are also potentially
dynamic values in ICU expressions too, so we need to render these as
placeholders when extracting i18n messages into translation files.

PR Close #38484
2020-08-17 10:44:26 -07:00
Pete Bacon Darwin
be96510ce9 test(compiler): add additional i18n serialization tests (#38484)
The addiational tests check that ICUs containing interpolations
are serialized correctly.

PR Close #38484
2020-08-17 10:44:24 -07:00
Andrew Kushnir
cb05c0102f fix(core): move generated i18n statements to the consts field of ComponentDef (#38404)
This commit updates the code to move generated i18n statements into the `consts` field of
ComponentDef to avoid invoking `$localize` function before component initialization (to better
support runtime translations) and also avoid problems with lazy-loading when i18n defs may not
be present in a chunk where it's referenced.

Prior to this change the i18n statements were generated at the top leve:

```
var I18N_0;
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
    var MSG_X = goog.getMsg(“…”);
    I18N_0 = MSG_X;
} else {
    I18N_0 = $localize('...');
}

defineComponent({
    // ...
    template: function App_Template(rf, ctx) {
        i0.ɵɵi18n(2, I18N_0);
    }
});
```

This commit updates the logic to generate the following code instead:

```
defineComponent({
    // ...
    consts: function() {
        var I18N_0;
        if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
            var MSG_X = goog.getMsg(“…”);
            I18N_0 = MSG_X;
        } else {
            I18N_0 = $localize('...');
        }
        return [
            I18N_0
        ];
    },
    template: function App_Template(rf, ctx) {
        i0.ɵɵi18n(2, 0);
    }
});
```

Also note that i18n template instructions now refer to the `consts` array using an index
(similar to other template instructions).

PR Close #38404
2020-08-17 10:13:57 -07:00
Andrew Kushnir
5f90b64328 refactor(compiler): i18n compiler tests refactoring (#38404)
This commit refactors i18n compiler tests to avoid code duplication and simplify further maintenance and updates.

PR Close #38404
2020-08-17 10:13:55 -07:00
Andrew Scott
71079ce47e fix(common): Allow scrolling when browser supports scrollTo (#38468)
This commit fixes a regression from "fix(common): ensure
scrollRestoration is writable (#30630)" that caused scrolling to not
happen at all in browsers that do not support scroll restoration. The
issue was that `supportScrollRestoration` was updated to return `false`
if a browser did not have a writable `scrollRestoration`. However, the
previous behavior was that the function would return `true` if
`window.scrollTo` was defined. Every scrolling function in the
`ViewportScroller` used `supportScrollRestoration` and, with the update
in bb88c9fa3daac80086efbda951d81c159e3840f4, no scrolling would be
performed if a browser did not have writable `scrollRestoration` but
_did_ have `window.scrollTo`.

Note, that this failure was detected in the saucelabs tests. IE does not
support scroll restoration so IE tests were failing.

PR Close #38468
2020-08-14 11:41:22 -07:00
Anas Barghoud
ca798804b2 fix(router): export DefaultRouteReuseStrategy to Router public_api (#31575)
export DefaultRouteStrategy class that was used internally and exposed, and add documentation for each one of methods

PR Close #31575
2020-08-13 16:02:41 -07:00
waterplea
b071495f92 fix(core): fix multiple nested views removal from ViewContainerRef (#38317)
When removal of one view causes removal of another one from the same
ViewContainerRef it triggers an error with views length calculation. This commit
fixes this bug by removing a view from the list of available views before invoking
actual view removal (which might be recursive and relies on the length of the list
of available views).

Fixes #38201.

PR Close #38317
2020-08-13 13:35:53 -07:00
Ahn
d5f819ebc1 style(compiler-cli): remove unused constant (#38441)
Remove unused constant allDiagnostics

PR Close #38441
2020-08-13 13:32:41 -07:00
JoostK
1388c1761f perf(compiler-cli): don't emit template guards when child scope is empty (#38418)
For a template that contains for example `<span *ngIf="first"></span>`
there's no need to render the `NgIf` guard expression, as the child
scope does not have any type-checking statements, so any narrowing
effect of the guard is not applicable.

This seems like a minor improvement, however it reduces the number of
flow-node antecedents that TypeScript needs to keep into account for
such cases, resulting in an overall reduction of type-checking time.

PR Close #38418
2020-08-13 13:28:46 -07:00
JoostK
fb8f4b4d72 perf(compiler-cli): only generate directive declarations when used (#38418)
The template type-checker would always generate a directive declaration
even if its type was never used. For example, directives without any
input nor output bindings nor exportAs references don't need the
directive to be declared, as its type would never be used.

This commit makes the `TcbOp`s that are responsible for declaring a
directive as optional, such that they are only executed when requested
from another operation.

PR Close #38418
2020-08-13 13:28:44 -07:00
JoostK
f42e6ce917 perf(compiler-cli): only generate type-check code for referenced DOM elements (#38418)
The template type-checker would generate a statement with a call
expression for all DOM elements in a template of the form:

```
const _t1 = document.createElement("div");
```

Profiling has shown that this is a particularly expensive call to
perform type inference on, as TypeScript needs to perform signature
selection of `Document.createElement` and resolve the exact type from
the `HTMLElementTagNameMap`. However, it can be observed that the
statement by itself does not contribute anything to the type-checking
result if `_t1` is not actually used anywhere, which is only rarely the
case---it requires that the element is referenced by its name from
somewhere else in the template. Consequently, the type-checker can skip
generating this statement altogether for most DOM elements.

The effect of this optimization is significant in several phases:
1. Less type-check code to generate
2. Less type-check code to emit and parse again
3. No expensive type inference to perform for the call expression

The effect on phase 3 is the most significant here, as type-checking is
not currently incremental in the sense that only phases 1 and 2 can
be reused from a prior compilation. The actual type-checking of all
templates in phase 3 needs to be repeated on each incremental
compilation, so any performance gains we achieve here are very
beneficial.

PR Close #38418
2020-08-13 13:28:42 -07:00
Sonu Kapoor
175c79d1d8 test(docs-infra): remove deprecated ReflectiveInjector (#38408)
This commit replaces the old and slow `ReflectiveInjector` that was
deprecated in v5 with the new `Injector`. Note: This change was only
done in the spec files inside the `aio` folder.

While changing this, it was not possible to directly use `Injector.get`
to get the correct typing for the mocked classes. For example:

```typescript
locationService = injector.get<TestLocationService>(LocationService);
```

Fails with:

> Argument of type 'typeof LocationService' is not assignable to parameter
of type 'Type<TestLocationService> | InjectionToken<TestLocationService> |
AbstractType<TestLocationService>'.
  Type 'typeof LocationService' is not assignable to type 'Type<TestLocationService>'.
    Property 'searchResult' is missing in type 'LocationService' but required in type
    'TestLocationService'.

Therefore, it was necessary to first convert to `unknown` and then to
`TestLocationService`.

```typescript
locationService = injector.get(LocationService) as unknown as TestLocationService;
```

PR Close #38408
2020-08-13 12:56:13 -07:00
Andrew Scott
945751e2e8 ci: disable closure size tracking test (#38449)
We should define ngDevMode to false in Closure, but --define only works in the global scope.
With ngDevMode not being set to false, this size tracking test provides little value but a lot of
headache to continue updating the size.

PR Close #38449
2020-08-13 11:41:13 -07:00
Andrew Scott
b769771d60 refactor(router): Add annotations to correct Router documentation (#38448)
The `@HostListener` functions and lifecycle hooks aren't intended to be public API but
do need to appear in the `.d.ts` files or type checking will break. Adding the
nodoc annotation will correctly hide this function on the docs site.

Again, note that `@internal` cannot be used because the result would be
that the functions then do not appear in the `.d.ts` files. This would
break lifecycle hooks because the class would be seen as not
implementing the interface correctly. This would also break
`HostListener` because the compiled templates would attempt to call the
`onClick` functions, but those would also not appear in the `d.ts` and
would produce errors like "Property 'onClick' does not exist on type 'RouterLinkWithHref'".

PR Close #38448
2020-08-13 11:36:10 -07:00
crisbeto
a80f654af9 fix(core): error if CSS custom property in host binding has number in name (#38432)
Fixes an error if a CSS custom property, used inside a host binding, has a
number in its name. The error is thrown because the styling parser only
expects characters from A to Z,dashes, underscores and a handful of other
characters.

Fixes #37292.

PR Close #38432
2020-08-13 10:35:07 -07:00
Paul Gschwendtner
aa847cb014 build: run browsers tests on chromium locally (#38435)
Previously we added a browser target for `firefox` into the
dev-infra package. It looks like as part of this change, we
accidentally switched the local web testing target to `firefox`.

Web tests are not commonly run locally as we use Domino and
NodeJS tests for primary development. Sometimes though we intend
to run tests in a browser. This would currently work with Firefox
but not on Windows (as Firefox is a noop there in Bazel).

This commit switches the primary browser back to `chromium`. Also
Firefox has been added as a second browser to web testing targets.

This allows us to reduce browsers in the legacy Saucelabs job. i.e.
not running Chrome and Firefox there. This should increase stability
and speed up the legacy job (+ reduced rate limit for Saucelabs).

PR Close #38435
2020-08-13 09:37:02 -07:00
Joey Perrott
8763d8201c build: update ng-dev config file for new commit message configuration (#38430)
Removes the commit message types from the config as they are now staticly
defined in the dev-infra code.

PR Close #38430
2020-08-13 09:11:19 -07:00
Joey Perrott
9f7a37b4e9 feat(dev-infra): migrate to unified commit message types in commit message linting (#38430)
Previously commit message types were provided as part of the ng-dev config in the repository
using the ng-dev toolset.  This change removes this configuration expectation and instead
predefines the valid types for commit messages.

Additionally, with this new unified set of types requirements around providing a scope have
been put in place.  Scopes are either required, optional or forbidden for a given commit
type.

PR Close #38430
2020-08-13 09:11:17 -07:00
Paul Gschwendtner
773f7908c0 feat(dev-infra): update to latest benchpress version (#38440)
We recently updated the benchpress package to have a more loose
Angular core peer dependency, and less other unused dependencies.

We should make sure to use that in the dev-infra package so that
peer dependencies can be satisified in consumer projects, and so
that less unused dependencies are brought into projects.

PR Close #38440
2020-08-13 09:09:31 -07:00
Joey Perrott
f4ced74e3a feat(dev-infra): save invalid commit message attempts to be restored on next commit attempt (#38304)
When a commit message fails validation, rather than throwing out the commit message entirely
the commit message is saved into a draft file and restored on the next commit attempt.

PR Close #38304
2020-08-13 08:45:25 -07:00
Joey Perrott
8366effeec refactor(dev-infra): extract the commit message parsing function into its own file (#38429)
Extracts the commit message parsing function into its own file.

PR Close #38429
2020-08-12 16:10:07 -07:00
Sergey Falinsky
5f2e475abf docs: remove unused Input decorator (#38306)
In the part "5. Add In-app Navigation" of the tutorial it was already removed
PR Close #38306
2020-08-12 11:26:10 -07:00
Joey Perrott
aa3520eb7d refactor(dev-infra): use promptConfirm util in ng-dev's formatter (#38419)
Use the promptConfirm util instead of manually creating a confirm prompt with
inquirer.

PR Close #38419
2020-08-12 11:25:10 -07:00
Alan Agius
823dd5b341 docs: update web-worker CLI commands to bash style (#38421)
With this change we update the CLI generate commands to be in bash style.

PR Close #38421
2020-08-12 11:24:35 -07:00
Andrew Kushnir
d6d7caa2a8 release: cut the v10.1.0-next.5 release 2020-08-12 09:57:20 -07:00
Andrew Kushnir
dcf7baf3d1 docs: release notes for the v10.0.9 release 2020-08-12 09:49:28 -07:00
Vlad GURDIGA
4d17418569 docs: delete one superfluous sentence (#38339)
PR Close #38339
2020-08-12 08:23:18 -07:00
Joey Perrott
a2e069fdda build: run formatting automatically on pre-commit hook (#38402)
Runs the `ng-dev format changed` command whenever the `git commit` command is
run.  As all changes which are checked by CI will require this check passing, this
change can prevent needless roundtrips to correct lint/formatting errors. This
automatic formatting can be bypassed with the `--no-verify` flag on the `git commit`
command.

PR Close #38402
2020-08-11 16:32:54 -07:00
Joey Perrott
28534d83ee feat(dev-infra): Add support for formatting all staged files (#38402)
Adds an ng-dev formatter option to format all of the staged files. This will can
be used to format only the staged files during the pre-commit hook.

PR Close #38402
2020-08-11 16:32:54 -07:00
Alan Agius
a6292faa97 docs: remove solution style tsconfig (#38394)
Following the issues highlighted in
https://docs.google.com/document/d/1eB6cGCG_2ircfS5GzpDC9dBgikeYYcMxghVH5sDESHw/edit?usp=sharing
and discussions held with the TypeScript team.
Together with the TypeScript team it was decided that the best course of action is to rollback this feature.

In future, it is not excluded that solution style tsconfigs are re-introduced.

See: https://github.com/angular/angular-cli/pull/18478

PR Close #38394
2020-08-11 16:32:20 -07:00
Andrew Kushnir
e2e5f83869 ci: update payload size limits for Closure tests (#38411)
Currently the Closure-related tests are not tree-shaking the dev-mode-only content, thus payload
size checks are failing even if dev-mode-only content is added.
The 2e9fdbde9e commit
added some logic to JIT compiler, which is likely triggered the payload size increase. This commit
updates the payload size limits for Closure-related test to get master and patch branches back to
the "green" state.

PR Close #38411
2020-08-11 10:59:39 -07:00
Andrew Scott
71138f6004 feat(compiler-cli): Add compiler option to report errors when assigning to restricted input fields (#38249)
The compiler does not currently report errors when there's an `@Input()`
for a `private`, `protected`, or `readonly` directive/component class member.
This change adds an option to enable reporting errors when a template
attempts to bind to one of these restricted input fields.

PR Close #38249
2020-08-11 09:55:48 -07:00
JoostK
fa0104017a refactor(compiler-cli): only use type constructors for directives with generic types (#38249)
Prior to this change, the template type checker would always use a
type-constructor to instantiate a directive. This type-constructor call
serves two purposes:

1. Infer any generic types for the directive instance from the inputs
   that are passed in.
2. Type check the inputs that are passed into the directive's inputs.

The first purpose is only relevant when the directive actually has any
generic types and using a type-constructor for these cases inhibits
a type-check performance penalty, as a type-constructor's signature is
quite complex and needs to be generated for each directive.

This commit refactors the generated type-check blocks to only generate
a type-constructor call for directives that have generic types. Type
checking of inputs is achieved by generating individual statements for
all inputs, using assignments into the directive's fields.

Even if a type-constructor is used for type-inference of generic types
will the input checking also be achieved using the individual assignment
statements. This is done to support the rework of the language service,
which will start to extract symbol information from the type-check
blocks.

As a future optimization, it may be possible to reduce the number of
inputs passed into a type-constructor to only those inputs that
contribute the the type-inference of the generics. As this is not a
necessity at the moment this is left as follow-up work.

Closes #38185

PR Close #38249
2020-08-11 09:55:48 -07:00
JoostK
80b67e02b7 fix(compiler-cli): infer quote expressions as any type in type checker (#37917)
"Quote expressions" are expressions that start with an identifier followed by a
comma, allowing arbitrary syntax to follow. These kinds of expressions would
throw a an error in the template type checker, which would make them hard to
track down. As quote expressions are not generally used at all, the error would
typically occur for URLs that would inadvertently occur in a binding:

```html
<a [href]="https://example.com"></a>
```

This commit lets such bindings be inferred as the `any` type.

Fixes #36568
Resolves FW-2051

PR Close #37917
2020-08-11 09:54:53 -07:00
JoostK
18098d38b8 fix(compiler-cli): avoid creating value expressions for symbols from type-only imports (#37912)
In TypeScript 3.8 support was added for type-only imports, which only brings in
the symbol as a type, not their value. The Angular compiler did not yet take
the type-only keyword into account when representing symbols in type positions
as value expressions. The class metadata that the compiler emits would include
the value expression for its parameter types, generating actual imports as
necessary. For type-only imports this should not be done, as it introduces an
actual import of the module that was originally just a type-only import.

This commit lets the compiler deal with type-only imports specially, preventing
a value expression from being created.

Fixes #37900

PR Close #37912
2020-08-11 09:53:25 -07:00
JoostK
9514fd9080 fix(compiler): evaluate safe navigation expressions in correct binding order (#37911)
When using the safe navigation operator in a binding expression, a temporary
variable may be used for storing the result of a side-effectful call.
For example, the following template uses a pipe and a safe property access:

```html
<app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view>
```

The result of the pipe evaluation is stored in a temporary to be able to check
whether it is present. The temporary variable needs to be declared in a separate
statement and this would also cause the full expression itself to be pulled out
into a separate statement. This would compile into the following
pseudo-code instructions:

```js
var temp = null;
var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name;
property('enabled', ctx.enabled)('firstName', firstName);
```

Notice that the pipe evaluation happens before evaluating the `enabled` binding,
such that the runtime's internal binding index would correspond with `enabled`,
not `firstName`. This introduces a problem when the pipe uses `WrappedValue` to
force a change to be detected, as the runtime would then mark the binding slot
corresponding with `enabled` as dirty, instead of `firstName`. This results
in the `enabled` binding to be updated, triggering setters and affecting how
`OnChanges` is called.

In the pseudo-code above, the intermediate `firstName` variable is not strictly
necessary---it only improved readability a bit---and emitting it inline with
the binding itself avoids the out-of-order execution of the pipe:

```js
var temp = null;
property('enabled', ctx.enabled)
  ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name);
```

This commit introduces a new `BindingForm` that results in the above code to be
generated and adds compiler and acceptance tests to verify the proper behavior.

Fixes #37194

PR Close #37911
2020-08-11 09:51:10 -07:00
JoostK
2e9fdbde9e fix(core): prevent NgModule scope being overwritten in JIT compiler (#37795)
In JIT compiled apps, component definitions are compiled upon first
access. For a component class `A` that extends component class `B`, the
`B` component is also compiled when the `InheritDefinitionFeature` runs
during the compilation of `A` before it has finalized. A problem arises
when the compilation of `B` would flush the NgModule scoping queue,
where the NgModule declaring `A` is still pending. The scope information
would be applied to the definition of `A`, but its compilation is still
in progress so requesting the component definition would compile `A`
again from scratch. This "inner compilation" is correctly assigned the
NgModule scope, but once the "outer compilation" of `A` finishes it
would overwrite the inner compilation's definition, losing the NgModule
scope information.

In summary, flushing the NgModule scope queue could trigger a reentrant
compilation, where JIT compilation is non-reentrant. To avoid the
reentrant compilation, a compilation depth counter is introduced to
avoid flushing the NgModule scope during nested compilations.

Fixes #37105

PR Close #37795
2020-08-11 09:50:27 -07:00
Alexander Vologin
df76a2048b fix(router): restore 'history.state' object for navigations coming from Angular router (#28108) (#28176)
When navigations coming from Angular router we may have a payload stored in state property. When this
 exists, set extras's state to the payload.

PR Close #28176
2020-08-11 08:36:13 -07:00
Andrew Kushnir
3d156162af fix(dev-infra): update i18n-related file locations in PullApprove config (#38403)
The changes in https://github.com/angular/angular/pull/38368 split `render3/i18n.ts` files into
smaller scripts, but the PullApprove config was not updated to reflect that. This commit updates
the PullApprove config to reflect the recent changes in i18n-related files.

PR Close #38403
2020-08-10 17:29:51 -07:00
crisbeto
5dc8d287aa fix(core): queries not matching string injection tokens (#38321)
Queries weren't matching directives that provide themselves via string
injection tokens, because the assumption was that any string passed to
a query decorator refers to a template reference.

These changes make it so we match both template references and
providers while giving precedence to the template references.

Fixes #38313.
Fixes #38315.

PR Close #38321
2020-08-10 15:27:24 -07:00
crisbeto
6da9e5851a fix(compiler-cli): preserve quotes in class member names (#38387)
When we were outputting class members for `setClassMetadata` calls,
we were using the string representation of the member name. This can
lead to us generating invalid code when the name contains dashes and
is quoted (e.g. `@Output() 'has-dashes' = new EventEmitter()`), because
the quotes will be stripped for the string representation.

These changes fix the issue by using the original name AST node that was
used for the declaration and which knows whether it's supposed to be
quoted or not.

Fixes #38311.

PR Close #38387
2020-08-10 15:26:45 -07:00
Misko Hevery
250e299dc3 refactor(core): break i18n.ts into smaller files (#38368)
This commit contains no changes to code. It only breaks `i18n.ts` file
into `i18n.ts` + `i18n_apply.ts` + `i18n_parse.ts` +
`i18n_postprocess.ts` for easier maintenance.

PR Close #38368
2020-08-10 15:07:42 -07:00
Zach Pomerantz
8f708b561c fix(router): defer loading of wildcard module until needed (#38348)
Defer loading the wildcard module so that it is not loaded until
subscribed to. This fixes an issue where it was being eagerly loaded.
As an example, wildcard module loading should only occur after all other potential
matches have been exhausted. A test case for this was also added to
demonstrate the fix.

Fixes #25494

PR Close #38348
2020-08-10 13:20:08 -07:00
Joey Perrott
e34c33cd46 fix(platform-server): remove styles added by ServerStylesHost on destruction (#38367)
When a ServerStylesHost instance is destroyed, all of the shared styles added to the DOM
head element by that instance should be removed.  Without this removal, over time a large
number of style rules will build up and cause extra memory pressure.  This brings the
ServerStylesHost in line with the DomStylesHost used by the platform browser, which
performs this same cleanup.

PR Close #38367
2020-08-10 13:12:23 -07:00
Misko Hevery
6d8c73a4d6 fix(core): Store the currently selected ICU in LView (#38345)
The currently selected ICU was incorrectly being stored it `TNode`
rather than in `LView`.

Remove: `TIcuContainerNode.activeCaseIndex`
Add: `LView[TIcu.currentCaseIndex]`

PR Close #38345
2020-08-10 12:41:17 -07:00
Gillan Martindale
6ff28ac944 docs: Remove redundant sentence from Router (#38398)
PR Close #38398
2020-08-10 09:52:30 -07:00
cindygk
0de93fd402 docs: update team contributors page (#38384)
Removing Kara, Denny, Judy, Tony, Matias as they are no longer actively working on the project
PR Close #38384
2020-08-10 09:51:50 -07:00
301 changed files with 11497 additions and 5113 deletions

View File

@ -656,6 +656,18 @@ jobs:
- run: yarn tsc -p packages
- run: yarn tsc -p modules
- run: yarn bazel build //packages/zone.js:npm_package
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
# test in all supported browsers on Saucelabs anyway until this job can be removed.
- run:
name: Preparing Bazel-generated fixtures required in legacy tests
command: |
yarn bazel build //packages/core/test:downleveled_es5_fixture
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
dist/all/@angular/core/test/reflection/es5_downleveled_inheritance_fixture.js
- run:
# Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready.
name: Waiting for Saucelabs tunnel to connect

View File

@ -7,18 +7,6 @@ export const commitMessage: CommitMessageConfig = {
maxLineLength: 120,
minBodyLength: 20,
minBodyLengthTypeExcludes: ['docs'],
types: [
'build',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'release',
'style',
'test',
],
scopes: [
'animations',
'bazel',

View File

@ -509,8 +509,8 @@ groups:
- >
contains_any_globs(files, [
'packages/core/src/i18n/**',
'packages/core/src/render3/i18n.ts',
'packages/core/src/render3/i18n.md',
'packages/core/src/render3/i18n/**',
'packages/core/src/render3/instructions/i18n.ts',
'packages/core/src/render3/interfaces/i18n.ts',
'packages/common/locales/**',
'packages/common/src/i18n/**',

View File

@ -1,3 +1,143 @@
<a name="10.1.0-next.8"></a>
# 10.1.0-next.8 (2020-08-24)
<a name="10.1.0-next.8"></a>
# 10.1.0-next.8 (2020-08-24)
### Bug Fixes
* **localize:** extract the correct message ids ([#38498](https://github.com/angular/angular/issues/38498)) ([ac461e1](https://github.com/angular/angular/commit/ac461e1))
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([7ad3264](https://github.com/angular/angular/commit/7ad3264)), closes [#12842](https://github.com/angular/angular/issues/12842)
### Features
* **compiler:** support unary operators for more accurate type checking ([#37918](https://github.com/angular/angular/issues/37918)) ([874792d](https://github.com/angular/angular/commit/874792d)), closes [#20845](https://github.com/angular/angular/issues/20845) [#36178](https://github.com/angular/angular/issues/36178)
* **compiler-cli:** add support for TypeScript 4.0 ([#38076](https://github.com/angular/angular/issues/38076)) ([0fc44e0](https://github.com/angular/angular/commit/0fc44e0))
### Performance Improvements
* **forms:** use internal `ngDevMode` flag to tree-shake error messages in prod builds ([#37821](https://github.com/angular/angular/issues/37821)) ([201a546](https://github.com/angular/angular/commit/201a546)), closes [#37697](https://github.com/angular/angular/issues/37697)
<a name="10.0.12"></a>
## 10.0.12 (2020-08-24)
### Bug Fixes
* **compiler-cli:** adding references to const enums in runtime code ([#38542](https://github.com/angular/angular/issues/38542)) ([814b436](https://github.com/angular/angular/commit/814b436)), closes [#38513](https://github.com/angular/angular/issues/38513)
* **core:** remove closing body tag from inert DOM builder ([#38454](https://github.com/angular/angular/issues/38454)) ([5528536](https://github.com/angular/angular/commit/5528536))
* **localize:** include the last placeholder in parsed translation text ([#38452](https://github.com/angular/angular/issues/38452)) ([57d1a48](https://github.com/angular/angular/commit/57d1a48))
* **localize:** parse all parts of a translation with nested HTML ([#38452](https://github.com/angular/angular/issues/38452)) ([07b99f5](https://github.com/angular/angular/commit/07b99f5)), closes [#38422](https://github.com/angular/angular/issues/38422)
### Features
* **language-service:** introduce hybrid visitor to locate AST node ([#38540](https://github.com/angular/angular/issues/38540)) ([66d8c22](https://github.com/angular/angular/commit/66d8c22))
<a name="10.1.0-next.7"></a>
# 10.1.0-next.7 (2020-08-19)
This release contains various API docs improvements.
<a name="10.0.11"></a>
## 10.0.11 (2020-08-19)
### Bug Fixes
* **router:** ensure routerLinkActive updates when associated routerLinks change (resubmit of [#38349](https://github.com/angular/angular/issues/38349)) ([#38511](https://github.com/angular/angular/issues/38511)) ([0af9533](https://github.com/angular/angular/commit/0af9533)), closes [#18469](https://github.com/angular/angular/issues/18469)
<a name="10.1.0-next.6"></a>
# 10.1.0-next.6 (2020-08-17)
### Bug Fixes
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([ca07da4](https://github.com/angular/angular/commit/ca07da4)), closes [#38453](https://github.com/angular/angular/issues/38453)
* **core:** move generated i18n statements to the `consts` field of ComponentDef ([#38404](https://github.com/angular/angular/issues/38404)) ([cb05c01](https://github.com/angular/angular/commit/cb05c01))
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([3b9c802](https://github.com/angular/angular/commit/3b9c802)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
### Performance Improvements
* **compiler-cli:** don't emit template guards when child scope is empty ([#38418](https://github.com/angular/angular/issues/38418)) ([1388c17](https://github.com/angular/angular/commit/1388c17))
* **compiler-cli:** only generate directive declarations when used ([#38418](https://github.com/angular/angular/issues/38418)) ([fb8f4b4](https://github.com/angular/angular/commit/fb8f4b4))
* **compiler-cli:** only generate type-check code for referenced DOM elements ([#38418](https://github.com/angular/angular/issues/38418)) ([f42e6ce](https://github.com/angular/angular/commit/f42e6ce))
### Code Refactoring
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
<a name="10.0.10"></a>
## 10.0.10 (2020-08-17)
### Bug Fixes
* **common:** Allow scrolling when browser supports scrollTo ([#38468](https://github.com/angular/angular/issues/38468)) ([b32126c](https://github.com/angular/angular/commit/b32126c)), closes [#30630](https://github.com/angular/angular/issues/30630)
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([863acb6](https://github.com/angular/angular/commit/863acb6)), closes [#38453](https://github.com/angular/angular/issues/38453)
* **core:** error if CSS custom property in host binding has number in name ([#38432](https://github.com/angular/angular/issues/38432)) ([cb83b8a](https://github.com/angular/angular/commit/cb83b8a)), closes [#37292](https://github.com/angular/angular/issues/37292)
* **core:** fix multiple nested views removal from ViewContainerRef ([#38317](https://github.com/angular/angular/issues/38317)) ([d5e09f4](https://github.com/angular/angular/commit/d5e09f4)), closes [#38201](https://github.com/angular/angular/issues/38201)
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38500](https://github.com/angular/angular/issues/38500)) ([f3dd6c2](https://github.com/angular/angular/commit/f3dd6c2)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
* **router:** ensure routerLinkActive updates when associated routerLinks change ([#38349](https://github.com/angular/angular/issues/38349)) ([989e8a1](https://github.com/angular/angular/commit/989e8a1)), closes [#18469](https://github.com/angular/angular/issues/18469)
<a name="10.1.0-next.5"></a>
# 10.1.0-next.5 (2020-08-12)
### Bug Fixes
* **compiler-cli:** avoid creating value expressions for symbols from type-only imports ([#37912](https://github.com/angular/angular/issues/37912)) ([18098d3](https://github.com/angular/angular/commit/18098d3)), closes [#37900](https://github.com/angular/angular/issues/37900)
* **compiler-cli:** type-check inputs that include undefined when there's coercion members ([#38273](https://github.com/angular/angular/issues/38273)) ([7525f3a](https://github.com/angular/angular/commit/7525f3a))
* **router:** defer loading of wildcard module until needed ([#38348](https://github.com/angular/angular/issues/38348)) ([8f708b5](https://github.com/angular/angular/commit/8f708b5)), closes [#25494](https://github.com/angular/angular/issues/25494)
* **router:** restore 'history.state' object for navigations coming from Angular router ([#28108](https://github.com/angular/angular/issues/28108)) ([#28176](https://github.com/angular/angular/issues/28176)) ([df76a20](https://github.com/angular/angular/commit/df76a20))
### Features
* **compiler-cli:** Add compiler option to report errors when assigning to restricted input fields ([#38249](https://github.com/angular/angular/issues/38249)) ([71138f6](https://github.com/angular/angular/commit/71138f6))
* **router:** better warning message when a router outlet has not been instantiated ([#30246](https://github.com/angular/angular/issues/30246)) ([1609815](https://github.com/angular/angular/commit/1609815))
<a name="10.0.9"></a>
## 10.0.9 (2020-08-12)
### Bug Fixes
* **common:** ensure scrollRestoration is writable ([#30630](https://github.com/angular/angular/issues/30630)) ([#38357](https://github.com/angular/angular/issues/38357)) ([58f4b3a](https://github.com/angular/angular/commit/58f4b3a)), closes [#30629](https://github.com/angular/angular/issues/30629)
* **compiler:** evaluate safe navigation expressions in correct binding order ([#37911](https://github.com/angular/angular/issues/37911)) ([f5b9d87](https://github.com/angular/angular/commit/f5b9d87)), closes [#37194](https://github.com/angular/angular/issues/37194)
* **compiler-cli:** avoid creating value expressions for symbols from type-only imports ([#38415](https://github.com/angular/angular/issues/38415)) ([ca2b4bc](https://github.com/angular/angular/commit/ca2b4bc)), closes [#37912](https://github.com/angular/angular/issues/37912)
* **compiler-cli:** infer quote expressions as any type in type checker ([#37917](https://github.com/angular/angular/issues/37917)) ([5b87c67](https://github.com/angular/angular/commit/5b87c67)), closes [#36568](https://github.com/angular/angular/issues/36568)
* **compiler-cli:** mark eager `NgModuleFactory` construction as not side effectful ([#38320](https://github.com/angular/angular/issues/38320)) ([016a41b](https://github.com/angular/angular/commit/016a41b)), closes [#38147](https://github.com/angular/angular/issues/38147)
* **compiler-cli:** match wrapHost parameter types within plugin interface ([#38004](https://github.com/angular/angular/issues/38004)) ([df01a82](https://github.com/angular/angular/commit/df01a82))
* **compiler-cli:** preserve quotes in class member names ([#38387](https://github.com/angular/angular/issues/38387)) ([c9acb7b](https://github.com/angular/angular/commit/c9acb7b)), closes [#38311](https://github.com/angular/angular/issues/38311)
* **core:** prevent NgModule scope being overwritten in JIT compiler ([#37795](https://github.com/angular/angular/issues/37795)) ([3acebdc](https://github.com/angular/angular/commit/3acebdc)), closes [#37105](https://github.com/angular/angular/issues/37105)
* **core:** queries not matching string injection tokens ([#38321](https://github.com/angular/angular/issues/38321)) ([32109dc](https://github.com/angular/angular/commit/32109dc)), closes [#38313](https://github.com/angular/angular/issues/38313) [#38315](https://github.com/angular/angular/issues/38315)
* **core:** Store the currently selected ICU in `LView` ([#38345](https://github.com/angular/angular/issues/38345)) ([ee5123f](https://github.com/angular/angular/commit/ee5123f))
* **platform-server:** remove styles added by ServerStylesHost on destruction ([#38367](https://github.com/angular/angular/issues/38367)) ([7f11149](https://github.com/angular/angular/commit/7f11149))
* **router:** prevent calling unsubscribe on undefined subscription in RouterPreloader ([#38344](https://github.com/angular/angular/issues/38344)) ([4151314](https://github.com/angular/angular/commit/4151314))
* **service-worker:** fix the chrome debugger syntax highlighter ([#38332](https://github.com/angular/angular/issues/38332)) ([f5d5bac](https://github.com/angular/angular/commit/f5d5bac))
<a name="10.1.0-next.4"></a>
# 10.1.0-next.4 (2020-08-04)

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
@ -11,7 +11,7 @@ import { HeroService } from '../hero.service';
styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
@Input() hero: Hero;
hero: Hero;
constructor(
private route: ActivatedRoute,

View File

@ -31,7 +31,7 @@ For example:
```json
{
"extends": "../tsconfig.base.json",
"extends": "../tsconfig.json",
"compilerOptions": {
"experimentalDecorators": true,
...

View File

@ -154,7 +154,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
</div>
For more information, see the [MDN Interfaces documentation](https://developer.mozilla.org/en-US/docs/Web/API#Interfaces) which has API docs for all the standard DOM elements and their properties.
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) attributes to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
In particular, you can navigate from the attributes page to the properties via "DOM interface" link, and navigate the inheritance hierarchy up to `HTMLTableCellElement`.
@ -195,7 +195,7 @@ To control the state of the button, set the `disabled` *property*,
<div class="alert is-helpful">
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to be a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
```html
<input [disabled]="condition ? true : false">

View File

@ -40,8 +40,7 @@ The top level of the workspace contains workspace-wide configuration files, conf
| `package-lock.json` | Provides version information for all packages installed into `node_modules` by the npm client. See [npm documentation](https://docs.npmjs.com/files/package-lock.json) for details. If you use the yarn client, this file will be [yarn.lock](https://yarnpkg.com/lang/en/docs/yarn-lock/) instead. |
| `src/` | Source files for the root-level application project. |
| `node_modules/` | Provides [npm packages](guide/npm-packages) to the entire workspace. Workspace-wide `node_modules` dependencies are visible to all projects. |
| `tsconfig.json` | The `tsconfig.json` file is a ["Solution Style"](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#support-for-solution-style-tsconfigjson-files) TypeScript configuration file. Code editors and TypeScripts language server use this file to improve development experience. Compilers do not use this file. |
| `tsconfig.base.json` | The base [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. All other configuration files inherit from this base file. For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.|
| `tsconfig.json` | The base [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. All other configuration files inherit from this base file. For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.|
| `tslint.json` | Default [TSLint](https://palantir.github.io/tslint/) configuration for projects in the workspace. |
@ -103,7 +102,7 @@ Angular components, templates, and styles go here.
The application-specific configuration files for the root application reside at the workspace root level.
For a multi-project workspace, project-specific configuration files are in the project root, under `projects/project-name/`.
Project-specific [TypeScript](https://www.typescriptlang.org/) configuration files inherit from the workspace-wide `tsconfig.base.json`, and project-specific [TSLint](https://palantir.github.io/tslint/) configuration files inherit from the workspace-wide `tslint.json`.
Project-specific [TypeScript](https://www.typescriptlang.org/) configuration files inherit from the workspace-wide `tsconfig.json`, and project-specific [TSLint](https://palantir.github.io/tslint/) configuration files inherit from the workspace-wide `tslint.json`.
| APPLICATION-SPECIFIC CONFIG FILES | PURPOSE |
| :--------------------- | :------------------------------------------|

View File

@ -766,8 +766,10 @@ The HTML `base` tag with the `href` attribute specifies the base URI, or URL, fo
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": "src/locale/messages.fr.xlf"
"baseHref": ""
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": ""
}
}
},
"architect": {

View File

@ -208,7 +208,7 @@ about the event and gives that data to the parent.
The child's template has two controls. The first is an HTML `<input>` with a
[template reference variable](guide/template-reference-variables) , `#newItem`,
where the user types in an item name. Whatever the user types
into the `<input>` gets stored in the `#newItem` variable.
into the `<input>` gets stored in the `value` property of the `#newItem` variable.
<code-example path="inputs-outputs/src/app/item-output/item-output.component.html" region="child-output" header="src/app/item-output/item-output.component.html"></code-example>
@ -218,7 +218,7 @@ an event binding because the part to the left of the equal
sign is in parentheses, `(click)`.
The `(click)` event is bound to the `addNewItem()` method in the child component class which
takes as its argument whatever the value of `#newItem` is.
takes as its argument whatever the value of `#newItem.value` property is.
Now the child component has an `@Output()`
for sending data to the parent and a method for raising an event.

View File

@ -11,11 +11,11 @@ That said, some applications will likely need to apply some manual updates.
In version 10, [a few deprecated APIs have been removed](guide/updating-to-version-10#removals) and there are a [few breaking changes](guide/updating-to-version-10#breaking-changes) unrelated to Ivy.
If you're seeing errors after updating to version 9, you'll first want to rule those changes out.
To do so, temporarily [turn off Ivy](guide/ivy#opting-out-of-angular-ivy) in your `tsconfig.base.json` and re-start your app.
To do so, temporarily [turn off Ivy](guide/ivy#opting-out-of-angular-ivy) in your `tsconfig.json` and re-start your app.
If you're still seeing the errors, they are not specific to Ivy. In this case, you may want to consult the [general version 10 guide](guide/updating-to-version-10). If you've opted into any of the new, stricter type-checking settings, you may also want to check out the [template type-checking guide](guide/template-typecheck).
If the errors are gone, switch back to Ivy by removing the changes to the `tsconfig.base.json` and review the list of expected changes below.
If the errors are gone, switch back to Ivy by removing the changes to the `tsconfig.json` and review the list of expected changes below.
{@a payload-size-debugging}
### Payload size debugging

View File

@ -1,55 +0,0 @@
# Solution-style `tsconfig.json` migration
## What does this migration do?
This migration adds support to existing projects for TypeScript's new ["solution-style" tsconfig feature](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig).
Support is added by making two changes:
1. Renaming the workspace-level `tsconfig.json` to `tsconfig.base.json`.
All project [TypeScript configuration files](guide/typescript-configuration) will extend from this base which contains the common options used throughout the workspace.
2. Adding the solution `tsconfig.json` file at the root of the workspace.
This `tsconfig.json` file will only contain references to project-level TypeScript configuration files and is only used by editors/IDEs.
As an example, the solution `tsconfig.json` for a new project is as follows:
```json
// This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScripts language server to improve development experience.
// It is not intended to be used to perform a compilation.
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./e2e/tsconfig.json"
}
]
}
```
## Why is this migration necessary?
Solution-style `tsconfig.json` files provide an improved editing experience and fix several long-standing defects when editing files in an IDE.
IDEs that leverage the TypeScript language service (for example, [Visual Studio Code](https://code.visualstudio.com)), will only use TypeScript configuration files that are named `tsconfig.json`.
In complex projects, there may be more than one compilation unit and each of these units may have different settings and options.
With the Angular CLI, a project will have application code that will target a browser.
It will also have unit tests that should not be included within the built application and that also need additional type information present (`jasmine` in this case).
Both parts of the project also share some but not all of the code within the project.
As a result, two separate TypeScript configuration files (`tsconfig.app.json` and `tsconfig.spec.json`) are needed to ensure that each part of the application is configured properly and that the right types are used for each part.
Also if web workers are used within a project, an additional tsconfig (`tsconfig.worker.json`) is needed.
Web workers use similar but incompatible types to the main browser application.
This requires the additional configuration file to ensure that the web worker files use the appropriate types and will build successfully.
While the Angular build system knows about all of these TypeScript configuration files, an IDE using TypeScript's language service does not.
Because of this, an IDE will not be able to properly analyze the code from each part of the project and may generate false errors or make suggestions that are incorrect for certain files.
By leveraging the new solution-style tsconfig, the IDE can now be aware of the configuration of each part of a project.
This allows each file to be treated appropriately based on its tsconfig.
IDE features such as error/warning reporting and auto-suggestion will operate more effectively as well.
The TypeScript 3.9 release [blog post](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig) also contains some additional information regarding this new feature.

View File

@ -9,7 +9,7 @@ This process helps ensure that intentional changes to the options are kept in pl
TypeScript Configuration File(s) | Changed Property | Existing Value | New Value
------------- | ------------- | ------------- | ------------- | -------------
`<workspace base>/tsconfig.base.json` | `"module"` | `"esnext"` | `"es2020"`
`<workspace base>/tsconfig.json` | `"module"` | `"esnext"` | `"es2020"`
Used in `browser` builder options (`ng build` for applications) | `"module"` | `"esnext"` | `"es2020"`
Used in `ng-packgr` builder options (`ng build` for libraries) | `"module"` | `"esnext"` | `"es2020"`
Used in `karma` builder options (`ng test` for applications) | `"module"` | `"esnext"` | `"es2020"`

View File

@ -2,10 +2,9 @@
In a single-page app, you change what the user sees by showing or hiding portions of the display that correspond to particular components, rather than going out to the server to get a new page.
As users perform application tasks, they need to move between the different [views](guide/glossary#view "Definition of view") that you have defined.
To implement this kind of navigation within the single page of your app, you use the Angular **`Router`**.
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular _router_.
The router enables navigation by interpreting a browser URL as an instruction to change the view.
To handle the navigation from one [view](guide/glossary#view) to the next, you use the Angular **`Router`**.
The **`Router`** enables navigation by interpreting a browser URL as an instruction to change the view.
To explore a sample app featuring the router's primary features, see the <live-example></live-example>.

View File

@ -37,9 +37,9 @@ by HTML.
<code-example path="template-reference-variables/src/app/app.component.html" region="ngForm" header="src/app/hero-form.component.html"></code-example>
The reference value of itemForm, without the ngForm attribute value, would be
The reference value of `itemForm`, without the `ngForm` attribute value, would be
the [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement).
There is, however, a difference between a Component and a Directive in that a `Component`
There is, however, a difference between a `Component` and a `Directive` in that a `Component`
will be referenced without specifying the attribute value, and a `Directive` will not
change the implicit reference (that is, the element).

View File

@ -114,6 +114,7 @@ In case of a false positive like these, there are a few options:
|Strictness flag|Effect|
|-|-|
|`strictInputTypes`|Whether the assignability of a binding expression to the `@Input()` field is checked. Also affects the inference of directive generic types. |
|`strictInputAccessModifiers`|Whether access modifiers such as `private`/`protected`/`readonly` are honored when assigning a binding expression to an `@Input()`. If disabled, the access modifiers of the `@Input` are ignored; only the type is checked.|
|`strictNullInputTypes`|Whether `strictNullChecks` is honored when checking `@Input()` bindings (per `strictInputTypes`). Turning this off can be useful when using a library that was not built with `strictNullChecks` in mind.|
|`strictAttributeTypes`|Whether to check `@Input()` bindings that are made using text attributes (for example, `<mat-tab label="Step 1">` vs `<mat-tab [label]="'Step 1'">`).
|`strictSafeNavigationTypes`|Whether the return type of safe navigation operations (for example, `user?.name`) will be correctly inferred based on the type of `user`). If disabled, `user?.name` will be of type `any`.

View File

@ -19,7 +19,7 @@ Here's a summary of the stand-alone functions, in order of likely utility:
<tr>
<td style="vertical-align: top">
<code>async</code>
<code>waitForAsync</code>
</td>
<td>

View File

@ -18,32 +18,7 @@ that are important to Angular developers, including details about the following
## Configuration files
A given Angular workspace contains several TypeScript configuration files.
At the root level, there are two main TypeScript configuration files: a `tsconfig.json` file and a `tsconfig.base.json` file.
The `tsconfig.json` file is a ["Solution Style"](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#support-for-solution-style-tsconfigjson-files) TypeScript configuration file.
Code editors and TypeScripts language server use this file to improve development experience.
Compilers do not use this file.
The `tsconfig.json` file contains a list of paths to the other TypeScript configuration files used in the workspace.
<code-example lang="json" header="tsconfig.json" linenums="false">
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./projects/my-lib/tsconfig.lib.json"
}
]
}
</code-example>
The `tsconfig.base.json` file specifies the base TypeScript and Angular compiler options that all projects in the workspace inherit.
At the root `tsconfig.json` file specifies the base TypeScript and Angular compiler options that all projects in the workspace inherit.
The TypeScript and Angular have a wide range of options which can be used to configure type-checking features and generated output.
For more information, see the [Configuration inheritance with extends](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#configuration-inheritance-with-extends) section of the TypeScript documentation.
@ -55,9 +30,9 @@ For details about configuration inheritance, see the [Configuration inheritance
</div>
The initial `tsconfig.base.json` for an Angular workspace typically looks like the following example.
The initial `tsconfig.json` for an Angular workspace typically looks like the following example.
<code-example lang="json" header="tsconfig.base.json" linenums="false">
<code-example lang="json" header="tsconfig.json" linenums="false">
{
"compileOnSave": false,
"compilerOptions": {

View File

@ -48,8 +48,7 @@ src/
app/ ... <i>application code</i>
app.server.module.ts <i>* server-side application module</i>
server.ts <i>* express web server</i>
tsconfig.json <i>TypeScript solution style configuration</i>
tsconfig.base.json <i>TypeScript base configuration</i>
tsconfig.json <i>TypeScript base configuration</i>
tsconfig.app.json <i>TypeScript browser application configuration</i>
tsconfig.server.json <i>TypeScript server application configuration</i>
tsconfig.spec.json <i>TypeScript tests configuration</i>

View File

@ -77,6 +77,5 @@ Read about the migrations the CLI handles for you automatically:
* [Migrating missing `@Directive()`/`@Component()` decorators](guide/migration-undecorated-classes)
* [Migrating `ModuleWithProviders`](guide/migration-module-with-providers)
* [Solution-style `tsconfig.json` migration](guide/migration-solution-style-tsconfig)
* [`tslib` direct dependency migration](guide/migration-update-libraries-tslib)
* [Update `module` and `target` compiler options migration](guide/migration-update-module-and-target-compiler-options)

View File

@ -14,12 +14,16 @@ The CLI does not support running Angular itself in a web worker.
To add a web worker to an existing project, use the Angular CLI `ng generate` command.
`ng generate web-worker` *location*
```bash
ng generate web-worker <location>
```
You can add a web worker anywhere in your application.
For example, to add a web worker to the root component, `src/app/app.component.ts`, run the following command.
`ng generate web-worker app`
```bash
ng generate web-worker app
```
The command performs the following actions.

View File

@ -86,24 +86,6 @@
"groups": ["Angular"],
"lead": "kara"
},
"matsko": {
"name": "Matias Niemela",
"picture": "matias.jpg",
"twitter": "yearofmoo",
"website": "http://yearofmoo.com",
"bio": "Matias Niemela is a fullstack web developer who has been programming & building websites for over 10 years, and a core team member of AngularJS for two years. In the spring of 2015 Matias joined Angular full time at Google. In his free time Matias loves to build complex things and is always up for public speaking, travelling and tweaking his current Vim setup.",
"groups": ["Angular"],
"lead": "kara"
},
"kara": {
"name": "Kara Erickson",
"picture": "kara-erickson.jpg",
"twitter": "karaforthewin",
"website": "https://github.com/kara",
"bio": "Kara is a software engineer on the Angular team at Google and a co-organizer of the Angular-SF Meetup. Prior to Google, she helped build UI components in Angular for guest management systems at OpenTable. She enjoys snacking indiscriminately and probably other things too.",
"groups": ["Angular"],
"lead": "igorminar"
},
"pkozlowski-opensource": {
"name": "Pawel Kozlowski",
"picture": "pawel.jpg",
@ -618,26 +600,6 @@
"bio": "Justin (aka Schwarty) is a Google Developer Expert in Web Technologies and Angular, the host and maintainer of the weekly AngularAir live video broadcast, educator, writer and content creator. He has Angular courses available on LinkedIn Learning and Pluralsight and loves passing on years of full stack development knowledge to help empower others to find their inner awesomeness!",
"groups": ["GDE"]
},
"dennispbrown": {
"name": "Denny Brown",
"picture": "denny.jpg",
"bio": "Denny is founder of Expert Support, a professional services firm specializing in technical communication, and leads the Angular technical writing team. His lifelong passion has been to reduce the time and effort required to understand complex technical information. Early on, he was Associate Chairman of the Computer Science Department at Stanford, where he taught introductory courses in programming. He also plays old-timers baseball in local leagues and national tournaments.",
"groups": ["Angular"],
"lead": "aikidave"
},
"jbogarthyde": {
"name": "Judy Bogart",
"picture": "judy.png",
"groups": ["Angular"],
"lead": "dennispbrown"
},
"rockument69": {
"name": "Tony Bove",
"picture": "rockument69.jpg",
"bio": "Tony is a technical writer with Expert Support. His lifelong passions are helping people use technology, writing fiction, and playing music. When he's not working or playing the harmonica with friends in a bluegrass band, he's swimming and snorkeling on a Kauai beach and playing ball with his Irish Wolfhound. He's worked at home for decades before it became a thing.",
"groups": ["Angular"],
"lead": "aikidave"
},
"kapunahelewong": {
"name": "Kapunahele Wong",
"picture": "kapunahele.jpg",
@ -751,9 +713,9 @@
"santosh": {
"name": "Santosh Yadav",
"picture": "santoshyadav.jpg",
"twitter": "Santosh19742211",
"twitter": "SantoshYadavDev",
"website": "https://www.santoshyadav.dev",
"bio": "Santosh is a GDE for Angular and Web Technologies and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
"bio": "Santosh is a GDE for Angular and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
"groups": ["GDE"]
},
"josephperrott": {

View File

@ -858,11 +858,6 @@
"title": "Missing @Injectable() Decorators",
"tooltip": "Migration to add missing @Injectable() decorators and incomplete provider definitions."
},
{
"url": "guide/migration-solution-style-tsconfig",
"title": "Solution-style `tsconfig.json`",
"tooltip": "Migration to create a solution-style `tsconfig.json`."
},
{
"url": "guide/migration-update-libraries-tslib",
"title": "`tslib` direct dependency",

View File

@ -385,7 +385,6 @@ next section on [Routing](tutorial/toh-pt5).
path="toh-pt4/src/app/heroes/heroes.component.ts">
</code-example>
The browser refreshes and the page displays the list of heroes.
Refresh the browser to see the list of heroes, and scroll to the bottom to see the
messages from the HeroService. Each time you click a hero, a new message appears to record
the selection. Use the "clear" button to clear the message history.

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 b0b27361d",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js ef770f1cb",
"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

@ -1,4 +1,4 @@
import { ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { of } from 'rxjs';
@ -12,20 +12,22 @@ import { LocationService } from 'app/shared/location.service';
describe('ContributorListComponent', () => {
let component: ContributorListComponent;
let injector: ReflectiveInjector;
let injector: Injector;
let contributorService: TestContributorService;
let locationService: TestLocationService;
let contributorGroups: ContributorGroup[];
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
ContributorListComponent,
{provide: ContributorService, useClass: TestContributorService },
{provide: LocationService, useClass: TestLocationService }
]);
injector = Injector.create({
providers: [
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
{provide: ContributorService, useClass: TestContributorService, deps: [] },
{provide: LocationService, useClass: TestLocationService, deps: [] }
]
});
locationService = injector.get(LocationService);
contributorService = injector.get(ContributorService);
locationService = injector.get(LocationService) as unknown as TestLocationService;
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
contributorGroups = contributorService.testContributors;
});

View File

@ -1,4 +1,4 @@
import { ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { of } from 'rxjs';
@ -12,20 +12,22 @@ import { Category } from './resource.model';
describe('ResourceListComponent', () => {
let component: ResourceListComponent;
let injector: ReflectiveInjector;
let injector: Injector;
let resourceService: TestResourceService;
let locationService: TestLocationService;
let categories: Category[];
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
ResourceListComponent,
{provide: ResourceService, useClass: TestResourceService },
{provide: LocationService, useClass: TestLocationService }
]);
injector = Injector.create({
providers: [
{provide: ResourceListComponent, deps: [ResourceService, LocationService] },
{provide: ResourceService, useClass: TestResourceService, deps: [] },
{provide: LocationService, useClass: TestLocationService, deps: [] }
]
});
locationService = injector.get(LocationService);
resourceService = injector.get(ResourceService);
locationService = injector.get(LocationService) as unknown as TestLocationService;
resourceService = injector.get(ResourceService) as unknown as TestResourceService;
categories = resourceService.testCategories;
});

View File

@ -1,4 +1,4 @@
import { ReflectiveInjector, NgZone } from '@angular/core';
import { Injector, NgZone } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { of } from 'rxjs';
import { SearchService } from './search.service';
@ -6,7 +6,7 @@ import { WebWorkerClient } from 'app/shared/web-worker';
describe('SearchService', () => {
let injector: ReflectiveInjector;
let injector: Injector;
let service: SearchService;
let sendMessageSpy: jasmine.Spy;
let mockWorker: WebWorkerClient;
@ -16,10 +16,13 @@ describe('SearchService', () => {
mockWorker = { sendMessage: sendMessageSpy } as any;
spyOn(WebWorkerClient, 'create').and.returnValue(mockWorker);
injector = ReflectiveInjector.resolveAndCreate([
SearchService,
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }) }
]);
injector = Injector.create({
providers: [
{ provide: SearchService, deps: [NgZone]},
{ provide: NgZone, useFactory: () => new NgZone({ enableLongStackTrace: false }), deps: [] }
]
});
service = injector.get(SearchService);
});

View File

@ -1,4 +1,4 @@
import { ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { environment } from 'environments/environment';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
@ -15,7 +15,7 @@ describe('Deployment service', () => {
it('should get the mode from the `mode` query parameter if available', () => {
const injector = getInjector();
const locationService: MockLocationService = injector.get(LocationService);
const locationService = injector.get(LocationService) as unknown as MockLocationService;
locationService.search.and.returnValue({ mode: 'bar' });
const deployment = injector.get(Deployment);
@ -25,8 +25,8 @@ describe('Deployment service', () => {
});
function getInjector() {
return ReflectiveInjector.resolveAndCreate([
Deployment,
{ provide: LocationService, useFactory: () => new MockLocationService('') }
]);
return Injector.create({providers: [
{ provide: Deployment, deps: [LocationService] },
{ provide: LocationService, useFactory: () => new MockLocationService(''), deps: [] }
]});
}

View File

@ -1,18 +1,23 @@
import { ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { GaService } from 'app/shared/ga.service';
import { WindowToken } from 'app/shared/window';
describe('GaService', () => {
let gaService: GaService;
let injector: ReflectiveInjector;
let injector: Injector;
let gaSpy: jasmine.Spy;
let mockWindow: any;
beforeEach(() => {
gaSpy = jasmine.createSpy('ga');
mockWindow = { ga: gaSpy };
injector = ReflectiveInjector.resolveAndCreate([GaService, { provide: WindowToken, useFactory: () => mockWindow }]);
injector = Injector.create({
providers: [
{ provide: GaService, deps: [WindowToken] },
{ provide: WindowToken, useFactory: () => mockWindow, deps: [] }
]});
gaService = injector.get(GaService);
});

View File

@ -1,4 +1,4 @@
import { ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { Location, LocationStrategy, PlatformLocation } from '@angular/common';
import { MockLocationStrategy } from '@angular/common/testing';
import { Subject } from 'rxjs';
@ -9,26 +9,28 @@ import { LocationService } from './location.service';
import { ScrollService } from './scroll.service';
describe('LocationService', () => {
let injector: ReflectiveInjector;
let injector: Injector;
let location: MockLocationStrategy;
let service: LocationService;
let swUpdates: MockSwUpdatesService;
let scrollService: MockScrollService;
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
LocationService,
Location,
{ provide: GaService, useClass: TestGaService },
{ provide: LocationStrategy, useClass: MockLocationStrategy },
{ provide: PlatformLocation, useClass: MockPlatformLocation },
{ provide: SwUpdatesService, useClass: MockSwUpdatesService },
{ provide: ScrollService, useClass: MockScrollService }
]);
injector = Injector.create({
providers: [
{ provide: LocationService, deps: [GaService, Location, ScrollService, PlatformLocation, SwUpdatesService] },
{ provide: Location, deps: [LocationStrategy, PlatformLocation] },
{ provide: GaService, useClass: TestGaService, deps: [] },
{ provide: LocationStrategy, useClass: MockLocationStrategy, deps: [] },
{ provide: PlatformLocation, useClass: MockPlatformLocation, deps: [] },
{ provide: SwUpdatesService, useClass: MockSwUpdatesService, deps: [] },
{ provide: ScrollService, useClass: MockScrollService, deps: [] }
]
});
location = injector.get(LocationStrategy);
service = injector.get(LocationService);
swUpdates = injector.get(SwUpdatesService);
location = injector.get(LocationStrategy) as unknown as MockLocationStrategy;
service = injector.get(LocationService);
swUpdates = injector.get(SwUpdatesService) as unknown as MockSwUpdatesService;
scrollService = injector.get(ScrollService);
});
@ -380,7 +382,7 @@ describe('LocationService', () => {
let platformLocation: MockPlatformLocation;
beforeEach(() => {
platformLocation = injector.get(PlatformLocation);
platformLocation = injector.get(PlatformLocation) as unknown as MockPlatformLocation;
});
it('should call replaceState on PlatformLocation', () => {
@ -577,7 +579,7 @@ describe('LocationService', () => {
let gaLocationChanged: jasmine.Spy;
beforeEach(() => {
const gaService = injector.get(GaService);
const gaService = injector.get(GaService) as unknown as TestGaService;
gaLocationChanged = gaService.locationChanged;
// execute currentPath observable so that gaLocationChanged is called
service.currentPath.subscribe();

View File

@ -1,4 +1,4 @@
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
import { ErrorHandler, Injector } from '@angular/core';
import { Logger } from './logger.service';
describe('logger service', () => {
@ -10,10 +10,10 @@ describe('logger service', () => {
beforeEach(() => {
logSpy = spyOn(console, 'log');
warnSpy = spyOn(console, 'warn');
const injector = ReflectiveInjector.resolveAndCreate([
Logger,
{ provide: ErrorHandler, useClass: MockErrorHandler }
]);
const injector = Injector.create({providers: [
{ provide: Logger, deps: [ErrorHandler] },
{ provide: ErrorHandler, useClass: MockErrorHandler, deps: [] }
]});
logger = injector.get(Logger);
errorHandler = injector.get(ErrorHandler);
});

View File

@ -1,4 +1,4 @@
import { ErrorHandler, ReflectiveInjector } from '@angular/core';
import { ErrorHandler, Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { WindowToken } from 'app/shared/window';
import { AppModule } from 'app/app.module';
@ -14,11 +14,12 @@ describe('ReportingErrorHandler service', () => {
onerrorSpy = jasmine.createSpy('onerror');
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
const injector = ReflectiveInjector.resolveAndCreate([
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }) }
]);
handler = injector.get(ErrorHandler);
const injector = Injector.create({providers: [
{ provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [WindowToken] },
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }), deps: [] }
]});
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
});
it('should be registered on the AppModule', () => {

View File

@ -1,4 +1,4 @@
import { Injector, ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { DOCUMENT } from '@angular/common';
@ -151,11 +151,11 @@ describe('ScrollSpyService', () => {
let scrollSpyService: ScrollSpyService;
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
injector = Injector.create({providers: [
{ provide: DOCUMENT, useValue: { body: {} } },
{ provide: ScrollService, useValue: { topOffset: 50 } },
ScrollSpyService
]);
{ provide: ScrollSpyService, deps: [DOCUMENT, ScrollService] }
]});
scrollSpyService = injector.get(ScrollSpyService);
});

View File

@ -1,7 +1,7 @@
import {Location, LocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
import {DOCUMENT} from '@angular/common';
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
import {ReflectiveInjector} from '@angular/core';
import {Injector} from '@angular/core';
import {fakeAsync, tick} from '@angular/core/testing';
import {ScrollService, topMargin} from './scroll.service';
@ -15,7 +15,7 @@ describe('ScrollService', () => {
};
const topOfPageElem = {} as Element;
let injector: ReflectiveInjector;
let injector: Injector;
let document: MockDocument;
let platformLocation: MockPlatformLocation;
let scrollService: ScrollService;
@ -41,21 +41,25 @@ describe('ScrollService', () => {
jasmine.createSpyObj('viewportScroller', ['getScrollPosition', 'scrollToPosition']);
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
{
provide: ScrollService,
useFactory: createScrollService,
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
},
{provide: Location, useClass: SpyLocation}, {provide: DOCUMENT, useClass: MockDocument},
{provide: PlatformLocation, useClass: MockPlatformLocation},
{provide: ViewportScroller, useValue: viewportScrollerStub},
{provide: LocationStrategy, useClass: MockLocationStrategy}
]);
injector = Injector.create( {
providers: [
{
provide: ScrollService,
useFactory: createScrollService,
deps: [DOCUMENT, PlatformLocation, ViewportScroller, Location],
},
{provide: Location, useClass: SpyLocation, deps: [] },
{provide: DOCUMENT, useClass: MockDocument, deps: []},
{provide: PlatformLocation, useClass: MockPlatformLocation, deps: []},
{provide: ViewportScroller, useValue: viewportScrollerStub},
{provide: LocationStrategy, useClass: MockLocationStrategy, deps: []}
]
});
platformLocation = injector.get(PlatformLocation);
document = injector.get(DOCUMENT);
document = injector.get(DOCUMENT) as unknown as MockDocument;
scrollService = injector.get(ScrollService);
location = injector.get(Location);
location = injector.get(Location) as unknown as SpyLocation;
spyOn(window, 'scrollBy');
});

View File

@ -1,5 +1,5 @@
import { DOCUMENT } from '@angular/common';
import { ReflectiveInjector } from '@angular/core';
import { Injector } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subject } from 'rxjs';
@ -7,7 +7,7 @@ import { ScrollItem, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-s
import { TocItem, TocService } from './toc.service';
describe('TocService', () => {
let injector: ReflectiveInjector;
let injector: Injector;
let scrollSpyService: MockScrollSpyService;
let tocService: TocService;
let lastTocList: TocItem[];
@ -21,13 +21,14 @@ describe('TocService', () => {
}
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
{ provide: DomSanitizer, useClass: TestDomSanitizer },
injector = Injector.create({providers: [
{ provide: DomSanitizer, useClass: TestDomSanitizer, deps: [] },
{ provide: DOCUMENT, useValue: document },
{ provide: ScrollSpyService, useClass: MockScrollSpyService },
TocService,
]);
scrollSpyService = injector.get(ScrollSpyService);
{ provide: ScrollSpyService, useClass: MockScrollSpyService, deps: [] },
{ provide: TocService, deps: [DOCUMENT, DomSanitizer, ScrollSpyService] },
]});
scrollSpyService = injector.get(ScrollSpyService) as unknown as MockScrollSpyService;
tocService = injector.get(TocService);
tocService.tocList.subscribe(tocList => lastTocList = tocList);
});
@ -330,7 +331,7 @@ describe('TocService', () => {
});
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer) as unknown as TestDomSanitizer;
expect(domSanitizer.bypassSecurityTrustHtml)
.toHaveBeenCalledWith('Setup to develop <i>locally</i>.');
});

View File

@ -1,4 +1,4 @@
import { ApplicationRef, ReflectiveInjector } from '@angular/core';
import { ApplicationRef, Injector } from '@angular/core';
import { discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing';
import { SwUpdate } from '@angular/service-worker';
import { Subject } from 'rxjs';
@ -8,7 +8,7 @@ import { SwUpdatesService } from './sw-updates.service';
describe('SwUpdatesService', () => {
let injector: ReflectiveInjector;
let injector: Injector;
let appRef: MockApplicationRef;
let service: SwUpdatesService;
let swu: MockSwUpdate;
@ -21,16 +21,16 @@ describe('SwUpdatesService', () => {
// run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper
// to call them inside each test's zone.
const setup = (isSwUpdateEnabled: boolean) => {
injector = ReflectiveInjector.resolveAndCreate([
{ provide: ApplicationRef, useClass: MockApplicationRef },
{ provide: Logger, useClass: MockLogger },
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled) },
SwUpdatesService
]);
injector = Injector.create({providers: [
{ provide: ApplicationRef, useClass: MockApplicationRef, deps: [] },
{ provide: Logger, useClass: MockLogger, deps: [] },
{ provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled), deps: [] },
{ provide: SwUpdatesService, deps: [ApplicationRef, Logger, SwUpdate] }
]});
appRef = injector.get(ApplicationRef);
appRef = injector.get(ApplicationRef) as unknown as MockApplicationRef;
service = injector.get(SwUpdatesService);
swu = injector.get(SwUpdate);
swu = injector.get(SwUpdate) as unknown as MockSwUpdate;
checkInterval = (service as any).checkInterval;
};
const tearDown = () => service.ngOnDestroy();

View File

@ -214,7 +214,7 @@ code {
margin-left: 2px;
position: relative;
@include line-height(24);
vertical-align: bottom;
vertical-align: middle;
}
}

View File

@ -12,8 +12,10 @@
// If a category becomes empty (e.g. BS and required), then the corresponding job must be commented
// out in the CI configuration.
var CIconfiguration = {
'Chrome': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
'Firefox': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}},
// Chrome and Firefox run as part of the Bazel browser tests, so we do not run them as
// part of the legacy Saucelabs tests.
'Chrome': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
'Firefox': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
// Set ESR as a not required browser as it fails for Ivy acceptance tests.
'FirefoxESR': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}},
// Disabled because using the "beta" channel of Chrome can cause non-deterministic CI results.

View File

@ -4,28 +4,39 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "commit-message",
srcs = [
"builder.ts",
"cli.ts",
"commit-message-draft.ts",
"config.ts",
"parse.ts",
"restore-commit-message.ts",
"validate.ts",
"validate-file.ts",
"validate-range.ts",
"wizard.ts",
],
module_name = "@angular/dev-infra-private/commit-message",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//inquirer",
"@npm//shelljs",
"@npm//yargs",
],
)
ts_library(
name = "validate-test",
name = "test_lib",
testonly = True,
srcs = ["validate.spec.ts"],
srcs = [
"builder.spec.ts",
"parse.spec.ts",
"validate.spec.ts",
],
deps = [
":commit-message",
"//dev-infra/utils",
@ -40,7 +51,6 @@ jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
deps = [
":commit-message",
":validate-test",
"test_lib",
],
)

View File

@ -0,0 +1,46 @@
/**
* @license
* Copyright Google LLC 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 * as config from '../utils/config';
import * as console from '../utils/console';
import {buildCommitMessage} from './builder';
describe('commit message building:', () => {
beforeEach(() => {
// stub logging calls to prevent noise in test log
spyOn(console, 'info').and.stub();
// provide a configuration for DevInfra when loaded
spyOn(config, 'getConfig').and.returnValue({
commitMessage: {
scopes: ['core'],
}
} as any);
});
it('creates a commit message with a scope', async () => {
buildPromptResponseSpies('fix', 'core', 'This is a summary');
expect(await buildCommitMessage()).toMatch(/^fix\(core\): This is a summary/);
});
it('creates a commit message without a scope', async () => {
buildPromptResponseSpies('build', false, 'This is a summary');
expect(await buildCommitMessage()).toMatch(/^build: This is a summary/);
});
});
/** Create spies to return the mocked selections from prompts. */
function buildPromptResponseSpies(type: string, scope: string|false, summary: string) {
spyOn(console, 'promptAutocomplete')
.and.returnValues(Promise.resolve(type), Promise.resolve(scope));
spyOn(console, 'promptInput').and.returnValue(Promise.resolve(summary));
}

View File

@ -0,0 +1,70 @@
/**
* @license
* Copyright Google LLC 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 {ListChoiceOptions} from 'inquirer';
import {info, promptAutocomplete, promptInput} from '../utils/console';
import {COMMIT_TYPES, CommitType, getCommitMessageConfig, ScopeRequirement} from './config';
/** Validate commit message at the provided file path. */
export async function buildCommitMessage() {
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
// TODO(josephperrott): Add default commit message information/commenting into generated messages
info('Just a few questions to start building the commit message!');
/** The commit message type. */
const type = await promptForCommitMessageType();
/** The commit message scope. */
const scope = await promptForCommitMessageScopeForType(type);
/** The commit message summary. */
const summary = await promptForCommitMessageSummary();
return `${type.name}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n`;
}
/** Prompts in the terminal for the commit message's type. */
async function promptForCommitMessageType(): Promise<CommitType> {
info('The type of change in the commit. Allows a reader to know the effect of the change,');
info('whether it brings a new feature, adds additional testing, documents the `project, etc.');
/** List of commit type options for the autocomplete prompt. */
const typeOptions: ListChoiceOptions[] =
Object.values(COMMIT_TYPES).map(({description, name}) => {
return {
name: `${name} - ${description}`,
value: name,
short: name,
};
});
/** The key of a commit message type, selected by the user via prompt. */
const typeName = await promptAutocomplete('Select a type for the commit:', typeOptions);
return COMMIT_TYPES[typeName];
}
/** Prompts in the terminal for the commit message's scope. */
async function promptForCommitMessageScopeForType(type: CommitType): Promise<string|false> {
// If the commit type's scope requirement is forbidden, return early.
if (type.scope === ScopeRequirement.Forbidden) {
info(`Skipping scope selection as the '${type.name}' type does not allow scopes`);
return false;
}
/** Commit message configuration */
const config = getCommitMessageConfig();
info('The area of the repository the changes in this commit most affects.');
return await promptAutocomplete(
'Select a scope for the commit:', config.commitMessage.scopes,
type.scope === ScopeRequirement.Optional ? '<no scope>' : '');
}
/** Prompts in the terminal for the commit message's summary. */
async function promptForCommitMessageSummary(): Promise<string> {
info('Provide a short summary of what the changes in the commit do');
return await promptInput('Provide a short summary of the commit');
}

View File

@ -9,13 +9,56 @@ import * as yargs from 'yargs';
import {info} from '../utils/console';
import {restoreCommitMessage} from './restore-commit-message';
import {validateFile} from './validate-file';
import {validateCommitRange} from './validate-range';
import {runWizard} from './wizard';
/** Build the parser for the commit-message commands. */
export function buildCommitMessageParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.command(
'restore-commit-message-draft', false,
args => {
return args.option('file-env-variable', {
type: 'string',
array: true,
conflicts: ['file'],
required: true,
description:
'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
coerce: arg => {
const [file, source] = (process.env[arg] || '').split(' ');
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return [file, source];
},
});
},
args => {
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
})
.command(
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
return args
.positional(
'filePath',
{description: 'The file path to write the generated commit message into'})
.positional('source', {
choices: ['message', 'template', 'merge', 'squash', 'commit'],
description: 'The source of the commit message as described here: ' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
})
.positional(
'commitSha', {description: 'The commit sha if source is set to `commit`'});
}),
async (args: any) => {
await runWizard(args);
})
.command(
'pre-commit-validate', 'Validate the most recent commit message', {
'file': {
@ -38,7 +81,7 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
}
},
args => {
const file = args.file || args.fileEnvVariable || '.git/COMMIT_EDITMSG';
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
validateFile(file);
})
.command(

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google LLC 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 {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs';
/** Load the commit message draft from the file system if it exists. */
export function loadCommitMessageDraft(basePath: string) {
const commitMessageDraftPath = `${basePath}.ngDevSave`;
if (existsSync(commitMessageDraftPath)) {
return readFileSync(commitMessageDraftPath).toString();
}
return '';
}
/** Remove the commit message draft from the file system. */
export function deleteCommitMessageDraft(basePath: string) {
const commitMessageDraftPath = `${basePath}.ngDevSave`;
if (existsSync(commitMessageDraftPath)) {
unlinkSync(commitMessageDraftPath);
}
}
/** Save the commit message draft to the file system for later retrieval. */
export function saveCommitMessageDraft(basePath: string, commitMessage: string) {
writeFileSync(`${basePath}.ngDevSave`, commitMessage);
}

View File

@ -12,7 +12,6 @@ export interface CommitMessageConfig {
maxLineLength: number;
minBodyLength: number;
minBodyLengthTypeExcludes?: string[];
types: string[];
scopes: string[];
}
@ -30,3 +29,66 @@ export function getCommitMessageConfig() {
assertNoErrors(errors);
return config as Required<typeof config>;
}
/** Scope requirement level to be set for each commit type. */
export enum ScopeRequirement {
Required,
Optional,
Forbidden,
}
/** A commit type */
export interface CommitType {
description: string;
name: string;
scope: ScopeRequirement;
}
/** The valid commit types for Angular commit messages. */
export const COMMIT_TYPES: {[key: string]: CommitType} = {
build: {
name: 'build',
description: 'Changes to local repository build system and tooling',
scope: ScopeRequirement.Forbidden,
},
ci: {
name: 'ci',
description: 'Changes to CI configuration and CI specific tooling',
scope: ScopeRequirement.Forbidden,
},
docs: {
name: 'docs',
description: 'Changes which exclusively affects documentation.',
scope: ScopeRequirement.Optional,
},
feat: {
name: 'feat',
description: 'Creates a new feature',
scope: ScopeRequirement.Required,
},
fix: {
name: 'fix',
description: 'Fixes a previously discovered failure/bug',
scope: ScopeRequirement.Required,
},
perf: {
name: 'perf',
description: 'Improves performance without any change in functionality or API',
scope: ScopeRequirement.Required,
},
refactor: {
name: 'refactor',
description: 'Refactor without any change in functionality or API (includes style changes)',
scope: ScopeRequirement.Required,
},
release: {
name: 'release',
description: 'A release point in the repository',
scope: ScopeRequirement.Forbidden,
},
test: {
name: 'test',
description: 'Improvements or corrections made to the project\'s test suite',
scope: ScopeRequirement.Required,
},
};

View File

@ -0,0 +1,105 @@
/**
* @license
* Copyright Google LLC 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 {parseCommitMessage, ParsedCommitMessage} from './parse';
const commitValues = {
prefix: '',
type: 'fix',
scope: 'changed-area',
summary: 'This is a short summary of the change',
body: 'This is a longer description of the change Closes #1',
};
function buildCommitMessage(params = {}) {
const {prefix, type, scope, summary, body} = {...commitValues, ...params};
return `${prefix}${type}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n${body}`;
}
describe('commit message parsing:', () => {
it('parses the scope', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).scope).toBe(commitValues.scope);
});
it('parses the type', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).type).toBe(commitValues.type);
});
it('parses the header', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).header)
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
});
it('parses the body', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).body).toBe(commitValues.body);
});
it('parses the body without Github linking', () => {
const body = 'This has linking\nCloses #1';
const message = buildCommitMessage({body});
expect(parseCommitMessage(message).bodyWithoutLinking).toBe('This has linking\n');
});
it('parses the subject', () => {
const message = buildCommitMessage();
expect(parseCommitMessage(message).subject).toBe(commitValues.summary);
});
it('identifies if a commit is a fixup', () => {
const message1 = buildCommitMessage();
expect(parseCommitMessage(message1).isFixup).toBe(false);
const message2 = buildCommitMessage({prefix: 'fixup! '});
expect(parseCommitMessage(message2).isFixup).toBe(true);
});
it('identifies if a commit is a revert', () => {
const message1 = buildCommitMessage();
expect(parseCommitMessage(message1).isRevert).toBe(false);
const message2 = buildCommitMessage({prefix: 'revert: '});
expect(parseCommitMessage(message2).isRevert).toBe(true);
const message3 = buildCommitMessage({prefix: 'revert '});
expect(parseCommitMessage(message3).isRevert).toBe(true);
});
it('identifies if a commit is a squash', () => {
const message1 = buildCommitMessage();
expect(parseCommitMessage(message1).isSquash).toBe(false);
const message2 = buildCommitMessage({prefix: 'squash! '});
expect(parseCommitMessage(message2).isSquash).toBe(true);
});
it('ignores comment lines', () => {
const message = buildCommitMessage({
prefix: '# This is a comment line before the header.\n' +
'## This is another comment line before the headers.\n',
body: '# This is a comment line befor the body.\n' +
'This is line 1 of the actual body.\n' +
'## This is another comment line inside the body.\n' +
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n' +
'### This is yet another comment line after the body.\n',
});
const parsedMessage = parseCommitMessage(message);
expect(parsedMessage.header)
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
expect(parsedMessage.body)
.toBe(
'This is line 1 of the actual body.\n' +
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n');
});
});

View File

@ -0,0 +1,77 @@
/**
* @license
* Copyright Google LLC 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
*/
/** A parsed commit message. */
export interface ParsedCommitMessage {
header: string;
body: string;
bodyWithoutLinking: string;
type: string;
scope: string;
subject: string;
isFixup: boolean;
isSquash: boolean;
isRevert: boolean;
}
/** Regex determining if a commit is a fixup. */
const FIXUP_PREFIX_RE = /^fixup! /i;
/** Regex finding all github keyword links. */
const GITHUB_LINKING_RE = /((closed?s?)|(fix(es)?(ed)?)|(resolved?s?))\s\#(\d+)/ig;
/** Regex determining if a commit is a squash. */
const SQUASH_PREFIX_RE = /^squash! /i;
/** Regex determining if a commit is a revert. */
const REVERT_PREFIX_RE = /^revert:? /i;
/** Regex determining the scope of a commit if provided. */
const TYPE_SCOPE_RE = /^(\w+)(?:\(([^)]+)\))?\:\s(.+)$/;
/** Regex determining the entire header line of the commit. */
const COMMIT_HEADER_RE = /^(.*)/i;
/** Regex determining the body of the commit. */
const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
/** Parse a full commit message into its composite parts. */
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
// Ignore comments (i.e. lines starting with `#`). Comments are automatically removed by git and
// should not be considered part of the final commit message.
commitMsg = commitMsg.split('\n').filter(line => !line.startsWith('#')).join('\n');
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),
};
}

View File

@ -0,0 +1,48 @@
/**
* @license
* Copyright Google LLC 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 {info} from 'console';
import {writeFileSync} from 'fs';
import {loadCommitMessageDraft} from './commit-message-draft';
/**
* Restore the commit message draft to the git to be used as the default commit message.
*
* The source provided may be one of the sources described in
* https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export function restoreCommitMessage(
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
if (!!source) {
info('Skipping commit message restoration attempt');
if (source === 'message') {
info('A commit message was already provided via the command with a -m or -F flag');
}
if (source === 'template') {
info('A commit message was already provided via the -t flag or config.template setting');
}
if (source === 'squash') {
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
}
if (source === 'commit') {
info('A commit message was already provided through a revision specified via --fixup, -c,');
info('-C or --amend flag');
}
process.exit(0);
}
/** A draft of a commit message. */
const commitMessage = loadCommitMessageDraft(filePath);
// If the commit message draft has content, restore it into the provided filepath.
if (commitMessage) {
writeFileSync(filePath, commitMessage);
}
// Exit the process
process.exit(0);
}

View File

@ -11,6 +11,7 @@ import {resolve} from 'path';
import {getRepoBaseDir} from '../utils/config';
import {info} from '../utils/console';
import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft';
import {validateCommitMessage} from './validate';
/** Validate commit message at the provided file path. */
@ -18,8 +19,12 @@ export function validateFile(filePath: string) {
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
if (validateCommitMessage(commitMessage)) {
info('√ Valid commit message');
deleteCommitMessageDraft(filePath);
return;
}
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft(filePath, commitMessage);
// If the validation did not return true, exit as a failure.
process.exit(1);
}

View File

@ -8,7 +8,8 @@
import {info} from '../utils/console';
import {exec} from '../utils/shelljs';
import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions} from './validate';
import {parseCommitMessage} from './parse';
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
// Whether the provided commit is a fixup commit.
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;

View File

@ -18,13 +18,6 @@ const config: {commitMessage: CommitMessageConfig} = {
commitMessage: {
maxLineLength: 120,
minBodyLength: 0,
types: [
'feat',
'fix',
'refactor',
'release',
'style',
],
scopes: [
'common',
'compiler',
@ -33,7 +26,7 @@ const config: {commitMessage: CommitMessageConfig} = {
]
}
};
const TYPES = config.commitMessage.types.join(', ');
const TYPES = Object.keys(validateConfig.COMMIT_TYPES).join(', ');
const SCOPES = config.commitMessage.scopes.join(', ');
const INVALID = false;
const VALID = true;
@ -47,7 +40,8 @@ describe('validate-commit-message.js', () => {
lastError = '';
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
spyOn(validateConfig, 'getCommitMessageConfig').and.returnValue(config);
spyOn(validateConfig, 'getCommitMessageConfig')
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
});
describe('validateMessage()', () => {
@ -55,16 +49,16 @@ describe('validate-commit-message.js', () => {
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('release(packaging): something')).toBe(VALID);
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fixup! release(packaging): something')).toBe(VALID);
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('squash! release(packaging): something')).toBe(VALID);
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "release(packaging): something"')).toBe(VALID);
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
expect(lastError).toBe('');
});
@ -110,8 +104,8 @@ describe('validate-commit-message.js', () => {
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('fix(webworker): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
@ -119,12 +113,12 @@ describe('validate-commit-message.js', () => {
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'));
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
});
it('should allow empty scope', () => {
expect(validateCommitMessage('fix: blablabla')).toBe(VALID);
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
expect(lastError).toBe('');
});
@ -243,7 +237,6 @@ describe('validate-commit-message.js', () => {
maxLineLength: 120,
minBodyLength: 30,
minBodyLengthTypeExcludes: ['docs'],
types: ['fix', 'docs'],
scopes: ['core']
}
};

View File

@ -7,7 +7,8 @@
*/
import {error} from '../utils/console';
import {getCommitMessageConfig} from './config';
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
import {parseCommitMessage} from './parse';
/** Options for commit message validation. */
export interface ValidateCommitMessageOptions {
@ -15,53 +16,9 @@ export interface ValidateCommitMessageOptions {
nonFixupCommitHeaders?: string[];
}
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([\s\S]*)$/;
/** Regex matching a URL for an entire commit body line. */
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
/** 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, options: ValidateCommitMessageOptions = {}) {
@ -129,8 +86,26 @@ export function validateCommitMessage(
return false;
}
if (!config.types.includes(commit.type)) {
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${config.types.join(', ')}`);
if (COMMIT_TYPES[commit.type] === undefined) {
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
printError(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}

View File

@ -0,0 +1,43 @@
/**
* @license
* Copyright Google LLC 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 {writeFileSync} from 'fs';
import {info} from '../utils/console';
import {buildCommitMessage} from './builder';
/**
* The source triggering the git commit message creation.
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
/** The default commit message used if the wizard does not procude a commit message. */
const defaultCommitMessage = `<type>(<scope>): <summary>
# <Describe the motivation behind this change - explain WHY you are making this change. Wrap all
# lines at 100 characters.>\n\n`;
export async function runWizard(
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
if (args.source !== undefined) {
info(`Skipping commit message wizard due because the commit was created via '${
args.source}' source`);
process.exitCode = 0;
return;
}
// Set the default commit message to be updated if the user cancels out of the wizard in progress
writeFileSync(args.filePath, defaultCommitMessage);
/** The generated commit message. */
const commitMessage = await buildCommitMessage();
writeFileSync(args.filePath, commitMessage);
}

View File

@ -10,12 +10,10 @@ ts_library(
deps = [
"//dev-infra/utils",
"@npm//@types/cli-progress",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//cli-progress",
"@npm//inquirer",
"@npm//multimatch",
"@npm//shelljs",
"@npm//yargs",

View File

@ -7,7 +7,7 @@
*/
import * as yargs from 'yargs';
import {allChangedFilesSince, allFiles} from '../utils/repo-files';
import {allChangedFilesSince, allFiles, allStagedFiles} from '../utils/repo-files';
import {checkFiles, formatFiles} from './format';
@ -22,22 +22,31 @@ export function buildFormatParser(localYargs: yargs.Argv) {
description: 'Run the formatter to check formatting rather than updating code format'
})
.command(
'all', 'Run the formatter on all files in the repository', {},
'all', 'Run the formatter on all files in the repository', args => args,
({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allFiles());
})
.command(
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref', {},
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref',
args => args.positional('shaOrRef', {type: 'string'}),
({shaOrRef, check}) => {
const sha = shaOrRef || 'master';
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allChangedFilesSince(sha));
})
.command('files <files..>', 'Run the formatter on provided files', {}, ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files);
});
.command(
'staged', 'Run the formatter on all staged files', args => args,
({check}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(allStagedFiles());
})
.command(
'files <files..>', 'Run the formatter on provided files',
args => args.positional('files', {array: true, type: 'string'}), ({check, files}) => {
const executionCmd = check ? checkFiles : formatFiles;
executionCmd(files!);
});
}
if (require.main === module) {

View File

@ -6,9 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {prompt} from 'inquirer';
import {error, info} from '../utils/console';
import {error, info, promptConfirm} from '../utils/console';
import {runFormatterInParallel} from './run-commands-parallel';
@ -57,11 +55,7 @@ export async function checkFiles(files: string[]) {
// If the command is run in a non-CI environment, prompt to format the files immediately.
let runFormatter = false;
if (!process.env['CI']) {
runFormatter = (await prompt({
type: 'confirm',
name: 'runFormatter',
message: 'Format the files now?',
})).runFormatter;
runFormatter = await promptConfirm('Format the files now?', true);
}
if (runFormatter) {

View File

@ -6,6 +6,7 @@ ts_library(
module_name = "@angular/dev-infra-private/pr",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/pr/checkout",
"//dev-infra/pr/discover-new-conflicts",
"//dev-infra/pr/merge",
"//dev-infra/pr/rebase",

View File

@ -0,0 +1,13 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "checkout",
srcs = glob(["*.ts"]),
module_name = "@angular/dev-infra-private/pr/checkout",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/pr/common",
"//dev-infra/utils",
"@npm//@types/yargs",
],
)

View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright Google LLC 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 {Arguments, Argv, CommandModule} from 'yargs';
import {error} from '../../utils/console';
import {checkOutPullRequestLocally} from '../common/checkout-pr';
export interface CheckoutOptions {
prNumber: number;
'github-token'?: string;
}
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/** Builds the checkout pull request command. */
function builder(yargs: Argv) {
return yargs.positional('prNumber', {type: 'number', demandOption: true}).option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
}
/** Handles the checkout pull request command. */
async function handler({prNumber, 'github-token': token}: Arguments<CheckoutOptions>) {
const githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
error('Alternatively, pass the `--github-token` command line flag.');
error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
process.exitCode = 1;
return;
}
const prCheckoutOptions = {allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}`};
await checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions);
}
/** yargs command module for checking out a PR */
export const CheckoutCommandModule: CommandModule<{}, CheckoutOptions> = {
handler,
builder,
command: 'checkout <pr-number>',
describe: 'Checkout a PR from the upstream repo',
};

View File

@ -8,6 +8,7 @@
import * as yargs from 'yargs';
import {CheckoutCommandModule} from './checkout/cli';
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
@ -24,7 +25,8 @@ export function buildPrParser(localYargs: yargs.Argv) {
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
.command(
'rebase <pr-number>', 'Rebase a pending PR and push the rebased commits back to Github',
buildRebaseCommand, handleRebaseCommand);
buildRebaseCommand, handleRebaseCommand)
.command(CheckoutCommandModule);
}
if (require.main === module) {

View File

@ -0,0 +1,12 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "common",
srcs = glob(["*.ts"]),
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@npm//@types/node",
"@npm//typed-graphqlify",
],
)

View File

@ -0,0 +1,135 @@
/**
* @license
* Copyright Google LLC 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 {types as graphQLTypes} from 'typed-graphqlify';
import {URL} from 'url';
import {info} from '../../utils/console';
import {GitClient} from '../../utils/git';
import {getPr} from '../../utils/github';
/* GraphQL schema for the response body for a pending PR. */
const PR_SCHEMA = {
state: graphQLTypes.string,
maintainerCanModify: graphQLTypes.boolean,
viewerDidAuthor: graphQLTypes.boolean,
headRefOid: graphQLTypes.string,
headRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
baseRef: {
name: graphQLTypes.string,
repository: {
url: graphQLTypes.string,
nameWithOwner: graphQLTypes.string,
},
},
};
export class UnexpectedLocalChangesError extends Error {
constructor(m: string) {
super(m);
Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype);
}
}
export class MaintainerModifyAccessError extends Error {
constructor(m: string) {
super(m);
Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype);
}
}
/** Options for checking out a PR */
export interface PullRequestCheckoutOptions {
/** Whether the PR should be checked out if the maintainer cannot modify. */
allowIfMaintainerCannotModify?: boolean;
}
/**
* Rebase the provided PR onto its merge target branch, and push up the resulting
* commit to the PRs repository.
*/
export async function checkOutPullRequestLocally(
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
/** Authenticated Git client for git and Github interactions. */
const git = new GitClient(githubToken);
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
// git environment. Checked before retrieving the PR to fail fast.
if (git.hasLocalChanges()) {
throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.');
}
/**
* The branch or revision originally checked out before this method performed
* any Git operations that may change the working branch.
*/
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* The PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, git);
/** The branch name of the PR from the repository the PR came from. */
const headRefName = pr.headRef.name;
/** The full ref for the repository and branch the PR came from. */
const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
/** The full URL path of the repository the PR came from with github token as authentication. */
const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken);
// Note: Since we use a detached head for rebasing the PR and therefore do not have
// remote-tracking branches configured, we need to set our expected ref and SHA. This
// allows us to use `--force-with-lease` for the detached head while ensuring that we
// never accidentally override upstream changes that have been pushed in the meanwhile.
// See:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
/** Flag for a force push with leage back to upstream. */
const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
// be pushed up.
if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) {
throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR');
}
try {
// Fetch the branch at the commit of the PR, and check it out in a detached state.
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
git.run(['fetch', headRefUrl, headRefName]);
git.run(['checkout', '--detach', 'FETCH_HEAD']);
} catch (e) {
git.checkout(previousBranchOrRevision, true);
throw e;
}
return {
/**
* Pushes the current local branch to the PR on the upstream repository.
*
* @returns true If the command did not fail causing a GitCommandError to be thrown.
* @throws GitCommandError Thrown when the push back to upstream fails.
*/
pushToUpstream: (): true => {
git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
return true;
},
/** Restores the state of the local repository to before the PR checkout occured. */
resetGitState: (): boolean => {
return git.checkout(previousBranchOrRevision, true);
}
};
}
/** Adds the provided token as username to the provided url. */
function addAuthenticationToUrl(urlString: string, token: string) {
const url = new URL(urlString);
url.username = token;
return url.toString();
}

View File

@ -12,18 +12,28 @@ import {error} from '../../utils/console';
import {discoverNewConflictsForPr} from './index';
/** The options available to the discover-new-conflicts command via CLI. */
export interface DiscoverNewConflictsCommandOptions {
date: number;
'pr-number': number;
}
/** Builds the discover-new-conflicts pull request command. */
export function buildDiscoverNewConflictsCommand(yargs: Argv) {
return yargs.option('date', {
description: 'Only consider PRs updated since provided date',
defaultDescription: '30 days ago',
coerce: Date.parse,
default: getThirtyDaysAgoDate,
});
export function buildDiscoverNewConflictsCommand(yargs: Argv):
Argv<DiscoverNewConflictsCommandOptions> {
return yargs
.option('date', {
description: 'Only consider PRs updated since provided date',
defaultDescription: '30 days ago',
coerce: (date) => typeof date === 'number' ? date : Date.parse(date),
default: getThirtyDaysAgoDate(),
})
.positional('pr-number', {demandOption: true, type: 'number'});
}
/** Handles the discover-new-conflicts pull request command. */
export async function handleDiscoverNewConflictsCommand({prNumber, date}: Arguments) {
export async function handleDiscoverNewConflictsCommand(
{'pr-number': prNumber, date}: Arguments<DiscoverNewConflictsCommandOptions>) {
// If a provided date is not able to be parsed, yargs provides it as NaN.
if (isNaN(date)) {
error('Unable to parse the value provided via --date flag');
@ -33,11 +43,11 @@ export async function handleDiscoverNewConflictsCommand({prNumber, date}: Argume
}
/** Gets a date object 30 days ago from today. */
function getThirtyDaysAgoDate(): Date {
function getThirtyDaysAgoDate() {
const date = new Date();
// Set the hours, minutes and seconds to 0 to only consider date.
date.setHours(0, 0, 0, 0);
// Set the date to 30 days in the past.
date.setDate(date.getDate() - 30);
return date;
return date.getTime();
}

View File

@ -72,7 +72,7 @@ export async function discoverNewConflictsForPr(
info(`Requesting pending PRs from Github`);
/** List of PRs from github currently known as mergable. */
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, config.github)).map(processPr);
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, git)).map(processPr);
/** The PR which is being checked against. */
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
if (requestedPr === undefined) {

View File

@ -12,17 +12,26 @@ import {error, red, yellow} from '../../utils/console';
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
/** The options available to the merge command via CLI. */
export interface MergeCommandOptions {
'github-token'?: string;
'pr-number': number;
}
/** Builds the options for the merge command. */
export function buildMergeCommand(yargs: Argv) {
return yargs.help().strict().option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
export function buildMergeCommand(yargs: Argv): Argv<MergeCommandOptions> {
return yargs.help()
.strict()
.positional('pr-number', {demandOption: true, type: 'number'})
.option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
}
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
export async function handleMergeCommand(args: Arguments) {
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
export async function handleMergeCommand(args: Arguments<MergeCommandOptions>) {
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
error(red('Alternatively, pass the `--github-token` command line flag.'));
@ -30,5 +39,5 @@ export async function handleMergeCommand(args: Arguments) {
process.exit(1);
}
await mergePullRequest(args.prNumber, githubToken);
await mergePullRequest(args['pr-number'], githubToken);
}

View File

@ -9,7 +9,7 @@
import {PullsListCommitsResponse, PullsMergeParams} from '@octokit/rest';
import {prompt} from 'inquirer';
import {parseCommitMessage} from '../../../commit-message/validate';
import {parseCommitMessage} from '../../../commit-message/parse';
import {GitClient} from '../../../utils/git';
import {GithubApiMergeMethod} from '../config';
import {PullRequestFailure} from '../failures';

View File

@ -15,17 +15,26 @@ import {rebasePr} from './index';
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/** Builds the rebase pull request command. */
export function buildRebaseCommand(yargs: Argv) {
return yargs.option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
/** The options available to the rebase command via CLI. */
export interface RebaseCommandOptions {
'github-token'?: string;
prNumber: number;
}
/** Builds the rebase pull request command. */
export function buildRebaseCommand(yargs: Argv): Argv<RebaseCommandOptions> {
return yargs
.option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
})
.positional('prNumber', {type: 'number', demandOption: true});
}
/** Handles the rebase pull request command. */
export async function handleRebaseCommand(args: Arguments) {
const githubToken = args.githubToken || process.env.GITHUB_TOKEN || process.env.TOKEN;
export async function handleRebaseCommand(args: Arguments<RebaseCommandOptions>) {
const githubToken = args['github-token'] || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
error('Alternatively, pass the `--github-token` command line flag.');

View File

@ -55,7 +55,7 @@ export async function rebasePr(
*/
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* Get the PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
const pr = await getPr(PR_SCHEMA, prNumber, git);
const headRefName = pr.headRef.name;
const baseRefName = pr.baseRef.name;

View File

@ -9,7 +9,7 @@
"ts-circular-deps": "./ts-circular-dependencies/index.js"
},
"dependencies": {
"@angular/benchpress": "0.2.0",
"@angular/benchpress": "0.2.1",
"@octokit/graphql": "<from-root>",
"@octokit/types": "<from-root>",
"brotli": "<from-root>",

View File

@ -30,20 +30,19 @@ export function tsCircularDependenciesBuilder(localYargs: yargs.Argv) {
{type: 'string', demandOption: true, description: 'Path to the configuration file.'})
.option('warnings', {type: 'boolean', description: 'Prints all warnings.'})
.command(
'check', 'Checks if the circular dependencies have changed.', {},
(argv: yargs.Arguments) => {
'check', 'Checks if the circular dependencies have changed.', args => args,
argv => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(false, config, warnings));
process.exit(main(false, config, !!warnings));
})
.command(
'approve', 'Approves the current circular dependencies.', {}, (argv: yargs.Arguments) => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(true, config, warnings));
});
.command('approve', 'Approves the current circular dependencies.', args => args, argv => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(true, config, !!warnings));
});
}
/**

View File

@ -17,6 +17,7 @@ ts_library(
"@npm//@types/shelljs",
"@npm//chalk",
"@npm//inquirer",
"@npm//inquirer-autocomplete-prompt",
"@npm//shelljs",
"@npm//tslib",
"@npm//typed-graphqlify",

View File

@ -7,7 +7,8 @@
*/
import chalk from 'chalk';
import {prompt} from 'inquirer';
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
/** Reexport of chalk colors for convenient access. */
@ -26,6 +27,52 @@ export async function promptConfirm(message: string, defaultValue = false): Prom
.result;
}
/** Prompts the user to select an option from a filterable autocomplete list. */
export async function promptAutocomplete(
message: string, choices: (string|ListChoiceOptions)[]): Promise<string>;
/**
* Prompts the user to select an option from a filterable autocomplete list, with an option to
* choose no value.
*/
export async function promptAutocomplete(
message: string, choices: (string|ListChoiceOptions)[],
noChoiceText?: string): Promise<string|false>;
export async function promptAutocomplete(
message: string, choices: (string|ListChoiceOptions)[],
noChoiceText?: string): Promise<string|false> {
// Creates a local prompt module with an autocomplete prompt type.
const prompt = createPromptModule({}).registerPrompt('autocomplete', inquirerAutocomplete);
if (noChoiceText) {
choices = [noChoiceText, ...choices];
}
// `prompt` must be cast as `any` as the autocomplete typings are not available.
const result = (await (prompt as any)({
type: 'autocomplete',
name: 'result',
message,
source: (_: any, input: string) => {
if (!input) {
return Promise.resolve(choices);
}
return Promise.resolve(choices.filter(choice => {
if (typeof choice === 'string') {
return choice.includes(input);
}
return choice.name!.includes(input);
}));
}
})).result;
if (result === noChoiceText) {
return false;
}
return result;
}
/** Prompts the user for one line of input. */
export async function promptInput(message: string): Promise<string> {
return (await prompt<{result: string}>({type: 'input', name: 'result', message})).result;
}
/**
* Supported levels for logging functions.
*

View File

@ -26,7 +26,7 @@ export class GithubApiRequestError extends Error {
**/
export class GithubClient extends Octokit {
/** The Github GraphQL (v4) API. */
graqhql: GithubGraphqlClient;
graphql: GithubGraphqlClient;
/** The current user based on checking against the Github API. */
private _currentUser: string|null = null;
@ -42,7 +42,7 @@ export class GithubClient extends Octokit {
});
// Create authenticated graphql client.
this.graqhql = new GithubGraphqlClient(token);
this.graphql = new GithubGraphqlClient(token);
}
/** Retrieve the login of the current user from Github. */
@ -51,7 +51,7 @@ export class GithubClient extends Octokit {
if (this._currentUser !== null) {
return this._currentUser;
}
const result = await this.graqhql.query({
const result = await this.graphql.query({
viewer: {
login: types.string,
}
@ -80,7 +80,7 @@ class GithubGraphqlClient {
// Set the default headers to include authorization with the provided token for all
// graphQL calls.
if (token) {
this.graqhql.defaults({headers: {authorization: `token ${token}`}});
this.graqhql = this.graqhql.defaults({headers: {authorization: `token ${token}`}});
}
}

View File

@ -150,6 +150,25 @@ export class GitClient {
return value.replace(this._githubTokenRegex, '<TOKEN>');
}
/**
* Checks out a requested branch or revision, optionally cleaning the state of the repository
* before attempting the checking. Returns a boolean indicating whether the branch or revision
* was cleanly checked out.
*/
checkout(branchOrRevision: string, cleanState: boolean): boolean {
if (cleanState) {
// Abort any outstanding ams.
this.runGraceful(['am', '--abort'], {stdio: 'ignore'});
// Abort any outstanding cherry-picks.
this.runGraceful(['cherry-pick', '--abort'], {stdio: 'ignore'});
// Abort any outstanding rebases.
this.runGraceful(['rebase', '--abort'], {stdio: 'ignore'});
// Clear any changes in the current repo.
this.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
}
return this.runGraceful(['checkout', branchOrRevision], {stdio: 'ignore'}).status === 0;
}
/**
* Assert the GitClient instance is using a token with permissions for the all of the
* provided OAuth scopes.

View File

@ -6,29 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {graphql as unauthenticatedGraphql} from '@octokit/graphql';
import {params, types} from 'typed-graphqlify';
import {params, query as graphqlQuery, types} from 'typed-graphqlify';
import {NgDevConfig} from './config';
/** The configuration required for github interactions. */
type GithubConfig = NgDevConfig['github'];
/**
* Authenticated instance of Github GraphQl API service, relies on a
* personal access token being available in the TOKEN environment variable.
*/
const graphql = unauthenticatedGraphql.defaults({
headers: {
// TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
// effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`,
}
});
import {GitClient} from './git';
/** Get a PR from github */
export async function getPr<PrSchema>(
prSchema: PrSchema, prNumber: number, {owner, name}: GithubConfig) {
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient) {
/** The owner and name of the repository */
const {owner, name} = git.remoteConfig;
/** The GraphQL query object to get a the PR */
const PR_QUERY = params(
{
$number: 'Int!', // The PR number
@ -41,14 +27,15 @@ export async function getPr<PrSchema>(
})
});
const result =
await graphql(graphqlQuery(PR_QUERY), {number: prNumber, owner, name}) as typeof PR_QUERY;
const result = (await git.github.graphql.query(PR_QUERY, {number: prNumber, owner, name}));
return result.repository.pullRequest;
}
/** Get all pending PRs from github */
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}: GithubConfig) {
// The GraphQL query object to get a page of pending PRs
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient) {
/** The owner and name of the repository */
const {owner, name} = git.remoteConfig;
/** The GraphQL query object to get a page of pending PRs */
const PRS_QUERY = params(
{
$first: 'Int', // How many entries to get with each request
@ -73,36 +60,22 @@ export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}:
}),
})
});
const query = graphqlQuery('members', PRS_QUERY);
/**
* Gets the query and queryParams for a specific page of entries.
*/
const queryBuilder = (count: number, cursor?: string) => {
return {
query,
params: {
after: cursor || null,
first: count,
owner,
name,
},
};
};
// The current cursor
/** The current cursor */
let cursor: string|undefined;
// If an additional page of members is expected
/** If an additional page of members is expected */
let hasNextPage = true;
// Array of pending PRs
/** Array of pending PRs */
const prs: Array<PrSchema> = [];
// For each page of the response, get the page and add it to the
// list of PRs
// For each page of the response, get the page and add it to the list of PRs
while (hasNextPage) {
const {query, params} = queryBuilder(100, cursor);
const results = await graphql(query, params) as typeof PRS_QUERY;
const params = {
after: cursor || null,
first: 100,
owner,
name,
};
const results = await git.github.graphql.query(PRS_QUERY, params) as typeof PRS_QUERY;
prs.push(...results.repository.pullRequests.nodes);
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
cursor = results.repository.pullRequests.pageInfo.endCursor;

View File

@ -0,0 +1,17 @@
/**
* @license
* Copyright Google LLC 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
*/
// inquirer-autocomplete-prompt doesn't provide types and no types are made available via
// DefinitelyTyped.
declare module "inquirer-autocomplete-prompt" {
import {registerPrompt} from 'inquirer';
let AutocompletePrompt: Parameters<typeof registerPrompt>[1];
export = AutocompletePrompt;
}

View File

@ -27,6 +27,18 @@ export function allChangedFilesSince(sha = 'HEAD') {
return Array.from(new Set([...diffFiles, ...untrackedFiles]));
}
/**
* A list of all staged files which have been modified.
*
* Only added, created and modified files are listed as others (deleted, renamed, etc) aren't
* changed or available as content to act upon.
*/
export function allStagedFiles() {
return gitOutputAsArray(`git diff --staged --name-only --diff-filter=ACM`);
}
export function allFiles() {
return gitOutputAsArray(`git ls-files`);
}

View File

@ -1758,9 +1758,10 @@
"packages/core/src/render3/index.ts"
],
[
"packages/core/src/render3/i18n.ts",
"packages/core/src/render3/i18n/i18n_apply.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts"
"packages/core/src/render3/index.ts",
"packages/core/src/render3/instructions/i18n.ts"
],
[
"packages/core/src/render3/interfaces/container.ts",

View File

@ -216,7 +216,7 @@ export declare class NgComponentOutlet implements OnChanges, OnDestroy {
}
export declare class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCheck {
set ngForOf(ngForOf: (U & NgIterable<T>) | undefined | null);
set ngForOf(ngForOf: U & NgIterable<T> | undefined | null);
set ngForTemplate(value: TemplateRef<NgForOfContext<T, U>>);
set ngForTrackBy(fn: TrackByFunction<T>);
get ngForTrackBy(): TrackByFunction<T>;

View File

@ -35,6 +35,7 @@ export interface StrictTemplateOptions {
strictContextGenerics?: boolean;
strictDomEventTypes?: boolean;
strictDomLocalRefTypes?: boolean;
strictInputAccessModifiers?: boolean;
strictInputTypes?: boolean;
strictLiteralTypes?: boolean;
strictNullInputTypes?: boolean;

View File

@ -2,7 +2,7 @@ export declare function createCustomElement<P>(component: Type<any>, config: NgE
export declare abstract class NgElement extends HTMLElement {
protected ngElementEventsSubscription: Subscription | null;
protected ngElementStrategy: NgElementStrategy;
protected abstract ngElementStrategy: NgElementStrategy;
abstract attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string, namespace?: string): void;
abstract connectedCallback(): void;
abstract disconnectedCallback(): void;

View File

@ -51,6 +51,14 @@ export declare class ActivationStart {
toString(): string;
}
export declare abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
shouldAttach(route: ActivatedRouteSnapshot): boolean;
shouldDetach(route: ActivatedRouteSnapshot): boolean;
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void;
}
export declare interface CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
}
@ -370,7 +378,7 @@ export declare class RouterEvent {
url: string);
}
export declare class RouterLink {
export declare class RouterLink implements OnChanges {
fragment: string;
preserveFragment: boolean;
/** @deprecated */ set preserveQueryParams(value: boolean);
@ -386,6 +394,7 @@ export declare class RouterLink {
};
get urlTree(): UrlTree;
constructor(router: Router, route: ActivatedRoute, tabIndex: string, renderer: Renderer2, el: ElementRef);
ngOnChanges(changes: SimpleChanges): void;
onClick(): boolean;
}
@ -421,7 +430,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
target: string;
get urlTree(): UrlTree;
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
ngOnChanges(changes: {}): any;
ngOnChanges(changes: SimpleChanges): any;
ngOnDestroy(): any;
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
}

View File

@ -62,7 +62,7 @@
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
"bundle": 1213130
"bundle": 1214857
}
}
}

View File

@ -53,7 +53,10 @@ INTEGRATION_TESTS = {
},
"dynamic-compiler": {"tags": ["no-ivy-aot"]},
"hello_world__closure": {
"commands": "payload_size_tracking",
# TODO: Re-enable the payload_size_tracking command:
# We should define ngDevMode to false in Closure, but --define only works in the global scope.
# With ngDevMode not being set to false, this size tracking test provides little value but a lot of
# headache to continue updating the size.
"tags": ["no-ivy-aot"],
},
"hello_world__systemjs_umd": {
@ -82,6 +85,12 @@ INTEGRATION_TESTS = {
# root @npm//typescript package.
"pinned_npm_packages": ["typescript"],
},
"typings_test_ts40": {
# Special case for `typings_test_ts40` test as we want to pin
# `typescript` at version 4.0.x for that test and not link to the
# root @npm//typescript package.
"pinned_npm_packages": ["typescript"],
},
}
[

View File

@ -0,0 +1,71 @@
/**
* @license
* Copyright Google LLC 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 * as animations from '@angular/animations';
import * as animationsBrowser from '@angular/animations/browser';
import * as animationsBrowserTesting from '@angular/animations/browser/testing';
import * as common from '@angular/common';
import * as commonHttp from '@angular/common/http';
import * as commonTesting from '@angular/common/testing';
import * as commonHttpTesting from '@angular/common/testing';
import * as compiler from '@angular/compiler';
import * as compilerTesting from '@angular/compiler/testing';
import * as core from '@angular/core';
import * as coreTesting from '@angular/core/testing';
import * as elements from '@angular/elements';
import * as forms from '@angular/forms';
import * as platformBrowser from '@angular/platform-browser';
import * as platformBrowserDynamic from '@angular/platform-browser-dynamic';
import * as platformBrowserDynamicTesting from '@angular/platform-browser-dynamic/testing';
import * as platformBrowserAnimations from '@angular/platform-browser/animations';
import * as platformBrowserTesting from '@angular/platform-browser/testing';
import * as platformServer from '@angular/platform-server';
import * as platformServerTesting from '@angular/platform-server/testing';
import * as platformWebworker from '@angular/platform-webworker';
import * as platformWebworkerDynamic from '@angular/platform-webworker-dynamic';
import * as router from '@angular/router';
import * as routerTesting from '@angular/router/testing';
import * as routerUpgrade from '@angular/router/upgrade';
import * as serviceWorker from '@angular/service-worker';
import * as upgrade from '@angular/upgrade';
import * as upgradeStatic from '@angular/upgrade/static';
import * as upgradeTesting from '@angular/upgrade/static/testing';
export default {
animations,
animationsBrowser,
animationsBrowserTesting,
common,
commonTesting,
commonHttp,
commonHttpTesting,
compiler,
compilerTesting,
core,
coreTesting,
elements,
forms,
platformBrowser,
platformBrowserTesting,
platformBrowserDynamic,
platformBrowserDynamicTesting,
platformBrowserAnimations,
platformServer,
platformServerTesting,
platformWebworker,
platformWebworkerDynamic,
router,
routerTesting,
routerUpgrade,
serviceWorker,
upgrade,
upgradeStatic,
upgradeTesting,
};

View File

@ -0,0 +1,30 @@
{
"name": "angular-integration",
"description": "Assert that users with TypeScript 4.0 can type-check an Angular application",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/elements": "file:../../dist/packages-dist/elements",
"@angular/forms": "file:../../dist/packages-dist/forms",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/platform-webworker": "file:../../dist/packages-dist/platform-webworker",
"@angular/platform-webworker-dynamic": "file:../../dist/packages-dist/platform-webworker-dynamic",
"@angular/router": "file:../../dist/packages-dist/router",
"@angular/service-worker": "file:../../dist/packages-dist/service-worker",
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
"@types/jasmine": "file:../../node_modules/@types/jasmine",
"rxjs": "file:../../node_modules/rxjs",
"typescript": "4.0.2",
"zone.js": "file:../../dist/zone.js-dist/zone.js"
},
"scripts": {
"test": "tsc"
}
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist/out-tsc",
"rootDir": ".",
"target": "es5",
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.iterable",
"es2015.promise"
],
"types": [],
},
"files": [
"include-all.ts",
"node_modules/@types/jasmine/index.d.ts"
]
}

View File

@ -37,6 +37,9 @@ module.exports = function(config) {
'node_modules/core-js/client/core.js',
'node_modules/jasmine-ajax/lib/mock-ajax.js',
// Dependencies built by Bazel. See `config.yml` for steps running before
// the legacy Saucelabs tests run.
'dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js',
'dist/bin/packages/zone.js/npm_package/bundles/zone-testing.umd.js',
'dist/bin/packages/zone.js/npm_package/bundles/task-tracking.umd.js',

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.1.0-next.4",
"version": "10.1.0-next.8",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -76,7 +76,7 @@
"@types/diff": "^3.5.1",
"@types/fs-extra": "4.0.2",
"@types/hammerjs": "2.0.35",
"@types/inquirer": "^6.5.0",
"@types/inquirer": "^7.3.0",
"@types/jasmine": "3.5.10",
"@types/jasmine-ajax": "^3.3.1",
"@types/jasminewd2": "^2.0.8",
@ -88,7 +88,7 @@
"@types/shelljs": "^0.8.6",
"@types/systemjs": "0.19.32",
"@types/yaml": "^1.2.0",
"@types/yargs": "^11.1.1",
"@types/yargs": "^15.0.5",
"@webcomponents/custom-elements": "^1.1.0",
"angular": "npm:angular@1.7",
"angular-1.5": "npm:angular@1.5",
@ -149,11 +149,11 @@
"terser": "^4.4.0",
"tsickle": "0.38.1",
"tslib": "^2.0.0",
"tslint": "6.0.0",
"typescript": "~3.9.5",
"tslint": "6.1.3",
"typescript": "~4.0.2",
"xhr2": "0.2.0",
"yaml": "^1.7.2",
"yargs": "15.3.0"
"yargs": "^15.4.1"
},
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
"devDependencies": {
@ -179,8 +179,9 @@
"glob": "7.1.2",
"gulp": "3.9.1",
"gulp-conventional-changelog": "^2.0.3",
"husky": "^4.2.3",
"inquirer": "^7.1.0",
"husky": "^4.2.5",
"inquirer": "^7.3.3",
"inquirer-autocomplete-prompt": "^1.0.2",
"jpm": "1.3.1",
"karma-browserstack-launcher": "^1.3.0",
"karma-sauce-launcher": "^2.0.2",
@ -209,7 +210,9 @@
"cldr-data-coverage": "full",
"husky": {
"hooks": {
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS"
"pre-commit": "yarn -s ng-dev format staged",
"commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS",
"prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS"
}
}
}

View File

@ -34,7 +34,7 @@
"@angular/compiler-cli": "0.0.0-PLACEHOLDER",
"@bazel/typescript": ">=1.0.0",
"terser": "^4.3.1",
"typescript": ">=3.9 <4.0",
"typescript": ">=3.9 <4.1",
"rollup": ">=1.20.0",
"rollup-plugin-commonjs": ">=9.0.0",
"rollup-plugin-node-resolve": ">=4.2.0",

View File

@ -155,7 +155,7 @@ export class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCh
* rather than the identity of the object itself.
*
* The function receives two inputs,
* the iteration index and the node object ID.
* the iteration index and the associated node data.
*/
@Input()
set ngForTrackBy(fn: TrackByFunction<T>) {

View File

@ -88,7 +88,7 @@ export class BrowserViewportScroller implements ViewportScroller {
* @returns The position in screen coordinates.
*/
getScrollPosition(): [number, number] {
if (this.supportScrollRestoration()) {
if (this.supportsScrolling()) {
return [this.window.scrollX, this.window.scrollY];
} else {
return [0, 0];
@ -100,7 +100,7 @@ export class BrowserViewportScroller implements ViewportScroller {
* @param position The new position in screen coordinates.
*/
scrollToPosition(position: [number, number]): void {
if (this.supportScrollRestoration()) {
if (this.supportsScrolling()) {
this.window.scrollTo(position[0], position[1]);
}
}
@ -110,7 +110,7 @@ export class BrowserViewportScroller implements ViewportScroller {
* @param anchor The ID of the anchor element.
*/
scrollToAnchor(anchor: string): void {
if (this.supportScrollRestoration()) {
if (this.supportsScrolling()) {
const elSelected =
this.document.getElementById(anchor) || this.document.getElementsByName(anchor)[0];
if (elSelected) {
@ -163,6 +163,14 @@ export class BrowserViewportScroller implements ViewportScroller {
return false;
}
}
private supportsScrolling(): boolean {
try {
return !!this.window.scrollTo;
} catch {
return false;
}
}
}
function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {

View File

@ -15,21 +15,30 @@ describe('BrowserViewportScroller', () => {
let windowSpy: any;
beforeEach(() => {
windowSpy = jasmine.createSpyObj('window', ['history']);
windowSpy.scrollTo = 1;
windowSpy = jasmine.createSpyObj('window', ['history', 'scrollTo']);
windowSpy.history.scrollRestoration = 'auto';
documentSpy = jasmine.createSpyObj('document', ['getElementById', 'getElementsByName']);
scroller = new BrowserViewportScroller(documentSpy, windowSpy, null!);
});
describe('setHistoryScrollRestoration', () => {
it('should not crash when scrollRestoration is not writable', () => {
function createNonWritableScrollRestoration() {
Object.defineProperty(windowSpy.history, 'scrollRestoration', {
value: 'auto',
configurable: true,
});
}
it('should not crash when scrollRestoration is not writable', () => {
createNonWritableScrollRestoration();
expect(() => scroller.setHistoryScrollRestoration('manual')).not.toThrow();
});
it('should still allow scrolling if scrollRestoration is not writable', () => {
createNonWritableScrollRestoration();
scroller.scrollToPosition([10, 10]);
expect(windowSpy.scrollTo as jasmine.Spy).toHaveBeenCalledWith(10, 10);
});
});
describe('scrollToAnchor', () => {

View File

@ -19,9 +19,10 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
alias: 'source',
describe:
'A path (relative to the working directory) of the `node_modules` folder to process.',
default: './node_modules'
default: './node_modules',
type: 'string',
})
.option('f', {alias: 'formats', hidden: true, array: true})
.option('f', {alias: 'formats', hidden: true, array: true, type: 'string'})
.option('p', {
alias: 'properties',
array: true,
@ -29,7 +30,8 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
'An array of names of properties in package.json to compile (e.g. `module` or `main`)\n' +
'Each of these properties should hold the path to a bundle-format.\n' +
'If provided, only the specified properties are considered for processing.\n' +
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.'
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.',
type: 'string',
})
.option('t', {
alias: 'target',
@ -37,6 +39,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
'If this property is provided then `error-on-failed-entry-point` is forced to true.\n' +
'This option overrides the `--use-program-dependencies` option.',
type: 'string',
})
.option('use-program-dependencies', {
type: 'boolean',
@ -47,7 +50,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
.option('first-only', {
describe:
'If specified then only the first matching package.json property will be compiled.',
type: 'boolean'
type: 'boolean',
})
.option('create-ivy-entry-points', {
describe:
@ -78,6 +81,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
alias: 'loglevel',
describe: 'The lowest severity logging message that should be output.',
choices: ['debug', 'info', 'warn', 'error'],
type: 'string',
})
.option('invalidate-entry-point-manifest', {
describe:
@ -105,7 +109,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
.help()
.parse(args);
if (options['f'] && options['f'].length) {
if (options.f?.length) {
console.error(
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
process.exit(1);
@ -113,12 +117,12 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
setFileSystem(new NodeJSFileSystem());
const baseSourcePath = resolve(options['s'] || './node_modules');
const propertiesToConsider: string[] = options['p'];
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
const baseSourcePath = resolve(options.s || './node_modules');
const propertiesToConsider = options.p;
const targetEntryPointPath = options.t;
const compileAllFormats = !options['first-only'];
const createNewEntryPointFormats = options['create-ivy-entry-points'];
const logLevel = options['l'] as keyof typeof LogLevel | undefined;
const logLevel = options.l as keyof typeof LogLevel | undefined;
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
@ -126,7 +130,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
// 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'];
const tsConfigPath = `${options.tsconfig}` === 'false' ? null : options.tsconfig;
const logger = logLevel && new ConsoleLogger(LogLevel[logLevel]);
@ -138,7 +142,7 @@ export function parseCommandLineOptions(args: string[]): NgccOptions {
createNewEntryPointFormats,
logger,
enableI18nLegacyMessageIdFormat,
async: options['async'],
async: options.async,
invalidateEntryPointManifest,
errorOnFailedEntryPoint,
tsConfigPath,

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
@ -1594,7 +1594,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
{decorators: null, typeExpression: null};
const nameNode = node.name;
let typeValueReference: TypeValueReference|null = null;
let typeValueReference: TypeValueReference;
if (typeExpression !== null) {
// `typeExpression` is an expression in a "type" context. Resolve it to a declared value.
// Either it's a reference to an imported type, or a type declared locally. Distinguish the
@ -1603,7 +1603,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (decl !== null && decl.node !== null && decl.viaModule !== null &&
isNamedDeclaration(decl.node)) {
typeValueReference = {
local: false,
kind: TypeValueReferenceKind.IMPORTED,
valueDeclaration: decl.node,
moduleName: decl.viaModule,
importedName: decl.node.name.text,
@ -1611,11 +1611,16 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
};
} else {
typeValueReference = {
local: true,
kind: TypeValueReferenceKind.LOCAL,
expression: typeExpression,
defaultImportStatement: null,
};
}
} else {
typeValueReference = {
kind: TypeValueReferenceKind.UNAVAILABLE,
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
};
}
return {

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, KnownDeclaration, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, getClassDeclarationFromInnerDeclaration, getPropertyValueFromSymbol, isAssignmentStatement, ParamInfo} from './esm2015_host';
@ -219,7 +219,7 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return Array.from(constructor.parameters);
}
if (isSynthesizedConstructor(constructor)) {
if (this.isSynthesizedConstructor(constructor)) {
return null;
}
@ -352,6 +352,219 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
const classDeclarationParent = classSymbol.implementation.valueDeclaration.parent;
return ts.isBlock(classDeclarationParent) ? Array.from(classDeclarationParent.statements) : [];
}
///////////// Host Private Helpers /////////////
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
* compiler generates a synthetic constructor.
*
* We need to identify such constructors as ngcc needs to be able to tell if a class did
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
* empty constructor apart from a synthesized constructor, but fortunately that does not
* matter for the code generated by ngtsc.
*
* When a class has a superclass however, a synthesized constructor must not be considered
* as a user-defined constructor as that prevents a base factory call from being created by
* ngtsc, resulting in a factory function that does not inject the dependencies of the
* superclass. Hence, we identify a default synthesized super call in the constructor body,
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
*
* Additionally, we handle synthetic delegate constructors that are emitted when TypeScript
* downlevel's ES2015 synthetically generated to ES5. These vary slightly from the default
* structure mentioned above because the ES2015 output uses a spread operator, for delegating
* to the parent constructor, that is preserved through a TypeScript helper in ES5. e.g.
*
* ```
* return _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* Such constructs can be still considered as synthetic delegate constructors as they are
* the product of a common TypeScript to ES5 synthetic constructor, just being downleveled
* to ES5 using `tsc`. See: https://github.com/angular/angular/issues/38453.
*
*
* @param constructor a constructor function to test
* @returns true if the constructor appears to have been synthesized
*/
private isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
if (!constructor.body) return false;
const firstStatement = constructor.body.statements[0];
if (!firstStatement) return false;
return this.isSynthesizedSuperThisAssignment(firstStatement) ||
this.isSynthesizedSuperReturnStatement(firstStatement);
}
/**
* Identifies synthesized super calls which pass-through function arguments directly and are
* being assigned to a common `_this` variable. The following patterns we intend to match:
*
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
* ```
* var _this = _super !== null && _super.apply(this, arguments) || this;
* ```
*
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
* ```
* var _this = _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
private isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
if (!ts.isVariableStatement(statement)) return false;
const variableDeclarations = statement.declarationList.declarations;
if (variableDeclarations.length !== 1) return false;
const variableDeclaration = variableDeclarations[0];
if (!ts.isIdentifier(variableDeclaration.name) ||
!variableDeclaration.name.text.startsWith('_this'))
return false;
const initializer = variableDeclaration.initializer;
if (!initializer) return false;
return this.isSynthesizedDefaultSuperCall(initializer);
}
/**
* Identifies synthesized super calls which pass-through function arguments directly and
* are being returned. The following patterns correspond to synthetic super return calls:
*
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
* ```
* return _super !== null && _super.apply(this, arguments) || this;
* ```
*
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
* ```
* return _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
private isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
if (!ts.isReturnStatement(statement)) return false;
const expression = statement.expression;
if (!expression) return false;
return this.isSynthesizedDefaultSuperCall(expression);
}
/**
* Identifies synthesized super calls which pass-through function arguments directly. The
* synthetic delegate super call match the following patterns we intend to match:
*
* 1. Delegate call emitted by TypeScript when it emits ES5 directly.
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* 2. Delegate call emitted by TypeScript when it downlevel's ES2015 to ES5.
* ```
* _super.apply(this, tslib.__spread(arguments)) || this;
* ```
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
private isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
const left = expression.left;
if (isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) {
return isSuperNotNull(left.left) && this.isSuperApplyCall(left.right);
} else {
return this.isSuperApplyCall(left);
}
}
/**
* Tests whether the expression corresponds to a `super` call passing through
* function arguments without any modification. e.g.
*
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
*
* Additionally, we also handle cases where `arguments` are wrapped by a TypeScript spread helper.
* This can happen if ES2015 class output contain auto-generated constructors due to class
* members. The ES2015 output will be using `super(...arguments)` to delegate to the superclass,
* but once downleveled to ES5, the spread operator will be persisted through a TypeScript spread
* helper. For example:
*
* ```
* _super.apply(this, __spread(arguments)) || this;
* ```
*
* More details can be found in: https://github.com/angular/angular/issues/38453.
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
private isSuperApplyCall(expression: ts.Expression): boolean {
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
const targetFn = expression.expression;
if (!ts.isPropertyAccessExpression(targetFn)) return false;
if (!isSuperIdentifier(targetFn.expression)) return false;
if (targetFn.name.text !== 'apply') return false;
const thisArgument = expression.arguments[0];
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
const argumentsExpr = expression.arguments[1];
// If the super is directly invoked with `arguments`, return `true`. This represents the
// common TypeScript output where the delegate constructor super call matches the following
// pattern: `super.apply(this, arguments)`.
if (isArgumentsIdentifier(argumentsExpr)) {
return true;
}
// The other scenario we intend to detect: The `arguments` variable might be wrapped with the
// TypeScript spread helper (either through tslib or inlined). This can happen if an explicit
// delegate constructor uses `super(...arguments)` in ES2015 and is downleveled to ES5 using
// `--downlevelIteration`. The output in such cases would not directly pass the function
// `arguments` to the `super` call, but wrap it in a TS spread helper. The output would match
// the following pattern: `super.apply(this, tslib.__spread(arguments))`. We check for such
// constructs below, but perform the detection of the call expression definition as last as
// that is the most expensive operation here.
if (!ts.isCallExpression(argumentsExpr) || argumentsExpr.arguments.length !== 1 ||
!isArgumentsIdentifier(argumentsExpr.arguments[0])) {
return false;
}
const argumentsCallExpr = argumentsExpr.expression;
let argumentsCallDeclaration: Declaration|null = null;
// The `__spread` helper could be globally available, or accessed through a namespaced
// import. Hence we support a property access here as long as it resolves to the actual
// known TypeScript spread helper.
if (ts.isIdentifier(argumentsCallExpr)) {
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr);
} else if (
ts.isPropertyAccessExpression(argumentsCallExpr) &&
ts.isIdentifier(argumentsCallExpr.name)) {
argumentsCallDeclaration = this.getDeclarationOfIdentifier(argumentsCallExpr.name);
}
return argumentsCallDeclaration !== null &&
argumentsCallDeclaration.known === KnownDeclaration.TsHelperSpread;
}
}
///////////// Internal Helpers /////////////
@ -422,103 +635,8 @@ function reflectArrayElement(element: ts.Expression) {
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
}
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
* compiler generates a synthetic constructor.
*
* We need to identify such constructors as ngcc needs to be able to tell if a class did
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
* empty constructor apart from a synthesized constructor, but fortunately that does not
* matter for the code generated by ngtsc.
*
* When a class has a superclass however, a synthesized constructor must not be considered
* as a user-defined constructor as that prevents a base factory call from being created by
* ngtsc, resulting in a factory function that does not inject the dependencies of the
* superclass. Hence, we identify a default synthesized super call in the constructor body,
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
*
* @param constructor a constructor function to test
* @returns true if the constructor appears to have been synthesized
*/
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
if (!constructor.body) return false;
const firstStatement = constructor.body.statements[0];
if (!firstStatement) return false;
return isSynthesizedSuperThisAssignment(firstStatement) ||
isSynthesizedSuperReturnStatement(firstStatement);
}
/**
* Identifies a synthesized super call of the form:
*
* ```
* var _this = _super !== null && _super.apply(this, arguments) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
if (!ts.isVariableStatement(statement)) return false;
const variableDeclarations = statement.declarationList.declarations;
if (variableDeclarations.length !== 1) return false;
const variableDeclaration = variableDeclarations[0];
if (!ts.isIdentifier(variableDeclaration.name) ||
!variableDeclaration.name.text.startsWith('_this'))
return false;
const initializer = variableDeclaration.initializer;
if (!initializer) return false;
return isSynthesizedDefaultSuperCall(initializer);
}
/**
* Identifies a synthesized super call of the form:
*
* ```
* return _super !== null && _super.apply(this, arguments) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
if (!ts.isReturnStatement(statement)) return false;
const expression = statement.expression;
if (!expression) return false;
return isSynthesizedDefaultSuperCall(expression);
}
/**
* Tests whether the expression is of the form:
*
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
const left = expression.left;
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
function isArgumentsIdentifier(expression: ts.Expression): boolean {
return ts.isIdentifier(expression) && expression.text === 'arguments';
}
function isSuperNotNull(expression: ts.Expression): boolean {
@ -526,31 +644,6 @@ function isSuperNotNull(expression: ts.Expression): boolean {
isSuperIdentifier(expression.left);
}
/**
* Tests whether the expression is of the form
*
* ```
* _super.apply(this, arguments)
* ```
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
function isSuperApplyCall(expression: ts.Expression): boolean {
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
const targetFn = expression.expression;
if (!ts.isPropertyAccessExpression(targetFn)) return false;
if (!isSuperIdentifier(targetFn.expression)) return false;
if (targetFn.name.text !== 'apply') return false;
const thisArgument = expression.arguments[0];
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
const argumentsArgument = expression.arguments[1];
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
}
function isBinaryExpr(
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {CommonJsReflectionHost} from '../../src/host/commonjs_host';
@ -1456,6 +1456,210 @@ exports.MissingClass2 = MissingClass2;
expect(decorators[0].args).toEqual([]);
});
});
function getConstructorParameters(
constructor: string, mode?: 'inlined'|'inlined_with_suffix'|'imported') {
let fileHeader = '';
switch (mode) {
case 'imported':
fileHeader = `const tslib = require('tslib');`;
break;
case 'inlined':
fileHeader =
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
break;
case 'inlined_with_suffix':
fileHeader =
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
break;
}
const file = {
name: _('/synthesized_constructors.js'),
contents: `
${fileHeader}
var TestClass = /** @class */ (function (_super) {
__extends(TestClass, _super);
${constructor}
return TestClass;
}(null));
exports.TestClass = TestClass;`,
};
loadTestFiles([file]);
const bundle = makeTestBundleProgram(file.name);
const host =
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const classNode =
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
return host.getConstructorParameters(classNode);
}
describe('TS -> ES5: synthesized constructors', () => {
it('recognizes _this assignment from super call', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.synthesizedProperty = null;
return _this;
}
`);
expect(parameters).toBeNull();
});
it('recognizes super call as return statement', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters).toBeNull();
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}
`);
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass(arg) {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters!.length).toBe(1);
});
it('does not consider manual super calls as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super.call(this) || this;
}
`);
expect(parameters!.length).toBe(0);
});
it('does not consider empty constructors as synthesized', () => {
const parameters = getConstructorParameters(`function TestClass() {}`);
expect(parameters!.length).toBe(0);
});
});
// See: https://github.com/angular/angular/issues/38453.
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread$1(arguments)) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib.__spread(arguments)) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread$1(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported');
expect(parameters).toBeNull();
});
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(
`
function TestClass(arg) {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters!.length).toBe(1);
});
});
});
describe('getDefinitionOfFunction()', () => {
@ -1599,7 +1803,7 @@ exports.MissingClass2 = MissingClass2;
isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode)!;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
local: true,
kind: TypeValueReferenceKind.LOCAL,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassMemberKind, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
import {ClassMemberKind, isNamedVariableDeclaration, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
@ -484,7 +484,7 @@ runInEachFileSystem(() => {
isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode)!;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
local: true,
kind: TypeValueReferenceKind.LOCAL,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection';
import {ClassMemberKind, isNamedFunctionDeclaration, isNamedVariableDeclaration, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
import {getIifeBody} from '../../src/host/esm2015_host';
@ -544,7 +544,7 @@ export { AliasedDirective$1 };
isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode)!;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
local: true,
kind: TypeValueReferenceKind.LOCAL,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
import {ClassMemberKind, ConcreteDeclaration, CtorParameter, Decorator, DownleveledEnum, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {DelegatingReflectionHost} from '../../src/host/delegating_host';
@ -1417,86 +1417,236 @@ runInEachFileSystem(() => {
});
});
describe('synthesized constructors', () => {
function getConstructorParameters(constructor: string) {
const file = {
name: _('/synthesized_constructors.js'),
contents: `
function getConstructorParameters(
constructor: string,
mode?: 'inlined'|'inlined_with_suffix'|'imported'|'imported_namespace') {
let fileHeader = '';
switch (mode) {
case 'imported':
fileHeader = `import {__spread} from 'tslib';`;
break;
case 'imported_namespace':
fileHeader = `import * as tslib from 'tslib';`;
break;
case 'inlined':
fileHeader =
`var __spread = (this && this.__spread) || function (...args) { /* ... */ }`;
break;
case 'inlined_with_suffix':
fileHeader =
`var __spread$1 = (this && this.__spread$1) || function (...args) { /* ... */ }`;
break;
}
const file = {
name: _('/synthesized_constructors.js'),
contents: `
${fileHeader}
var TestClass = /** @class */ (function (_super) {
__extends(TestClass, _super);
${constructor}
return TestClass;
}(null));
`,
};
};
loadTestFiles([file]);
const bundle = makeTestBundleProgram(file.name);
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const classNode =
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
return host.getConstructorParameters(classNode);
}
loadTestFiles([file]);
const bundle = makeTestBundleProgram(file.name);
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const classNode =
getDeclaration(bundle.program, file.name, 'TestClass', isNamedVariableDeclaration);
return host.getConstructorParameters(classNode);
}
describe('TS -> ES5: synthesized constructors', () => {
it('recognizes _this assignment from super call', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.synthesizedProperty = null;
return _this;
}`);
function TestClass() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.synthesizedProperty = null;
return _this;
}
`);
expect(parameters).toBeNull();
});
it('recognizes super call as return statement', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super !== null && _super.apply(this, arguments) || this;
}`);
function TestClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters).toBeNull();
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(`
function TestClass() {
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`);
function TestClass() {
var _this_1 = _super_1 !== null && _super_1.apply(this, arguments) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}
`);
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass(arg) {
return _super !== null && _super.apply(this, arguments) || this;
}`);
function TestClass(arg) {
return _super !== null && _super.apply(this, arguments) || this;
}
`);
expect(parameters!.length).toBe(1);
});
it('does not consider manual super calls as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass() {
return _super.call(this) || this;
}`);
function TestClass() {
return _super.call(this) || this;
}
`);
expect(parameters!.length).toBe(0);
});
it('does not consider empty constructors as synthesized', () => {
const parameters = getConstructorParameters(`
function TestClass() {
}`);
const parameters = getConstructorParameters(`function TestClass() {}`);
expect(parameters!.length).toBe(0);
});
});
// See: https://github.com/angular/angular/issues/38453.
describe('ES2015 -> ES5: synthesized constructors through TSC downleveling', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread$1(arguments)) || this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, __spread(arguments)) || this;
}`,
'imported');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
return _super.apply(this, tslib.__spread(arguments)) || this;
}`,
'imported_namespace');
expect(parameters).toBeNull();
});
describe('with class member assignment', () => {
it('recognizes delegate super call using inline spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using inline spread helper with suffix', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread$1(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'inlined_with_suffix');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, __spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported');
expect(parameters).toBeNull();
});
it('recognizes delegate super call using namespace imported spread helper', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this = _super.apply(this, tslib.__spread(arguments)) || this;
_this.synthesizedProperty = null;
return _this;
}`,
'imported_namespace');
expect(parameters).toBeNull();
});
});
it('handles the case where a unique name was generated for _super or _this', () => {
const parameters = getConstructorParameters(
`
function TestClass() {
var _this_1 = _super_1.apply(this, __spread(arguments)) || this;
_this_1._this = null;
_this_1._super = null;
return _this_1;
}`,
'inlined');
expect(parameters).toBeNull();
});
it('does not consider constructors with parameters as synthesized', () => {
const parameters = getConstructorParameters(
`
function TestClass(arg) {
return _super.apply(this, __spread(arguments)) || this;
}`,
'inlined');
expect(parameters!.length).toBe(1);
});
});
describe('(returned parameters `decorators.args`)', () => {
it('should be an empty array if param decorator has no `args` property', () => {
loadTestFiles([INVALID_CTOR_DECORATOR_ARGS_FILE]);
@ -1670,7 +1820,7 @@ runInEachFileSystem(() => {
bundle.program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode)!;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference! as {
local: true,
kind: TypeValueReferenceKind.LOCAL,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;

Some files were not shown because too many files have changed in this diff Show More