Compare commits

...

68 Commits

Author SHA1 Message Date
c01bd0fe8e release: cut the v10.1.0 release 2020-09-02 12:51:31 -07:00
5588324802 build: add configuration for the caretaker command (#38601)
Add configuration information for the new caretaker command

PR Close #38601
2020-09-01 13:05:43 -07:00
437ecc8090 feat(dev-infra): check services/status information of the repository for caretaker (#38601)
The angular team relies on a number of services for hosting code, running CI, etc. This
tool allows for checking the operational status of all services at once as well as the current
state of the repository with respect to merge and triage ready issues and prs.

PR Close #38601
2020-09-01 13:05:40 -07:00
0dda97ea66 fix(compiler): incorrectly inferring namespace for HTML nodes inside SVG (#38477)
The HTML parser gets an element's namespace either from the tag name
(e.g. `<svg:rect>`) or from its parent element `<svg><rect></svg>`) which
breaks down when an element is inside of an SVG `foreignElement`,
because foreign elements allow nodes from a different namespace to be
inserted into an SVG.

These changes add another flag to the tag definitions which tells child
nodes whether to try to inherit their namespaces from their parents.
It also adds a definition for `foreignObject` with the new flag,
allowing elements placed inside it to infer their namespaces instead.

Fixes #37218.

PR Close #38477
2020-08-31 13:25:43 -07:00
5e4aeaa348 refactor(dev-infra): use a mixin to require a github-token for an ng-dev command (#38630)
Creates a mixin for requiring a github token to be provided to a command.  This mixin
allows for a centralized management of the requirement and handling of the github-token.

PR Close #38630
2020-08-31 12:32:32 -07:00
cbbf8b542f docs: Remove confusion between do/avoid templates (#38647)
PR Close #38647
2020-08-31 10:25:21 -07:00
91dfb18840 refactor(forms): remove extra space in error message (#38637)
Remove extra whitespace at package/forms/model.ts error messages

PR Close #38637
2020-08-31 09:32:00 -07:00
e44ddf5baa refactor(dev-infra): improve error message for unexpected version branches (#38622)
Currently the merge script default branch configuration throws an error
if an unexpected version branch is discovered. The error right now
assumes to much knowledge of the logic and the document outlining
the release trains conceptually.

We change it to something more easy to understand that doesn't require
full understanding of the versioning/labeling/branching document that
has been created for the Angular organization.

PR Close #38622
2020-08-31 09:30:03 -07:00
6b1a505566 feat(dev-infra): write outputs of command runs to ng-dev log file (#38599)
Creates infrastructure to write outputs of command runs to ng-dev log file.
Additionally, on commands which fail with an exit code greater than 1, an
error log file is created with the posix timestamp of the commands run time
as an identifier.

PR Close #38599
2020-08-31 08:47:20 -07:00
659705ad78 docs: ng generate module command doc change (#38480)
PR Close #38480
2020-08-31 08:43:24 -07:00
8864b0ed69 docs: remove first person and space in CircleCI in the testing guide. (#38631)
PR Close #38631
2020-08-31 08:42:09 -07:00
4e596b672f docs: remove double space in start-data. (#38642)
PR Close #38642
2020-08-31 08:41:35 -07:00
83866827c3 docs: fix broken markdown in start/start-data (#38644)
PR Close #38644
2020-08-31 08:41:02 -07:00
7006cac50a refactor(dev-infra): remove style type from commit style guide (#38639)
The `style` commit type is not part of the commit parser config,
it should be removed from the documentation.

PR Close #38639
2020-08-31 08:40:19 -07:00
dd82f2fefd fix(bazel): fix integration test for bazel building (#38629)
Update the API used to request a timestamp.  The previous API we relied on for this
test application, worldclockapi.com no longer serves times and simply 403s on all
requests.  This caused our test to timeout as the HTTP request did not handle a failure
case.  By moving to a new api, the HTTP request responds as expected and timeouts
are corrected as there is not longer a pending microtask in the queue.

PR Close #38629
2020-08-28 11:16:45 -07:00
bf003340ab build: update ng-dev merge config to reflect new label updates (#38620)
Update the ng-dev merge configuration to reflect the new label updates

PR Close #38620
2020-08-28 08:03:31 -07:00
5e35edd724 ci: update angular robot to be based on new label updates (#38620)
Update the angular robot configuration to reflect the new label updates

PR Close #38620
2020-08-28 08:03:28 -07:00
c132dcd0ae ci: update github robot to reflect new target labels (#38428)
Updates the Github robot to reflect the updated target
labels that are used as part of the canonical versioning
and labeling for the Angular organization.

PR Close #38428
2020-08-28 08:02:28 -07:00
bbe331569b build: use new labeling and branching in merge script (#38428)
We introduced a new shared configuration for merge script
labels that follow the proposal of:
https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU

These label semantics and the branching are set up for the Angular
framework with this commit. The goal is that labeling and merging
is consistent between all Angular projects and that clear rules
are defined for branching. This was previously not the case.

PR Close #38428
2020-08-28 08:02:21 -07:00
21e9a0032c docs(forms): exclude internal-only methods and properties from docs (#38583)
Prior to this commit, a lot of internal-only class properties and methods (such as `ngOnChanges`)
of the Forms package directives were exposed on angular.io website. These fields are not expected
to be called externally (they are used/invoked by framework only), since they are part of internal
implementations of the following interfaces:

* Angular lifecycle hook interfaces
* ControlValueAccessor interface
* Validator interface

Having these internal-only fields in docs creates unnecessary noise on directive detail pages.
This commit adds the `@nodoc` annotation to these properties and methods to keep fields in the
golden files, but hide them in docs.

PR Close #38583
2020-08-27 16:39:43 -07:00
fb06903237 release: cut the v10.1.0-rc.0 release 2020-08-26 11:57:57 -07:00
ecc6fd0d28 docs: release notes for the v10.0.14 release 2020-08-26 11:51:32 -07:00
80cab26023 docs: Minor fixes in NgModules section (#36177)
Apply minor fixes to various guides of the NgModules section

PR Close #36177
2020-08-25 15:24:02 -07:00
841dfa68f9 docs: typo fixes in the component testing scenarios guide (#38574)
Fixed some typos and removed a warning about limitation of the `fakeAsync` that is already mentioned in a helpful alert

PR Close #38574
2020-08-25 15:16:53 -07:00
f0af387f6c fix(localize): ensure required XLIFF parameters are serialized (#38575)
When extracting i18n messages from source code, the XLIFF
serializers were missing some required attributes on the `<file>`
element.

This commit re-introduces the `original` property to each of XLIFF 1.2
and 2.0 serializers. Also it adds in the required `id` property for the
XLIFF 2.0 seralizer.

Fixes #38570

PR Close #38575
2020-08-25 15:14:19 -07:00
14e90bef58 fix(localize): render text of extracted placeholders (#38536)
Formats like XLIFF allow the text of the original source to
be included as metadata. This commit fixes the message
extractor to also render this text when available.

PR Close #38536
2020-08-25 15:13:23 -07:00
db3a21b382 refactor(localize): add placeholder locations in extracted messages (#38536)
Some translation file formats would like to be able to render the
text of placeholders taken from the original source files. This commit
adds this information to the extracted messages so that it can be
used in translation file serializers.

PR Close #38536
2020-08-25 15:13:20 -07:00
b8351f3b10 refactor(localize): ensure that translate plugin exceptions are not swallowed (#38536)
Previously, exceptions that were not `BabelParseError`s were just ignored.
Such exceptions are most likely programming errors in the package.
They are now re-thrown to ensure that the error is not hidden.

PR Close #38536
2020-08-25 15:13:17 -07:00
81053d3160 refactor(localize): run the translate plugin tests in mock FileSystems (#38536)
This commit is a tidy up of the translate plugin unit tests, but also ensures
that the tests are run in the context of a mock FileSystem. This ensures
that the tests are resilient to future refactors of the plugins that will
require a FileSystem to be initialized.

PR Close #38536
2020-08-25 15:13:15 -07:00
bdba1a062d refactor(localize): include text in original location of extracted messages (#38536)
When extracting messages, source-mapping information is used to find
the original location of the message being extracted. This commit will
now include the text from the original source in the message location
so that it can be serialized into the translation file.

PR Close #38536
2020-08-25 15:13:12 -07:00
23f855b300 refactor(localize): allow ParsedMessage to hold additional location data (#38536)
In preparation for supporting `equiv-text` placeholder information in
extracted translation files, this commit adds these optional properties
to the `ParsedMessage` interface and updates `parseMessage()` to
be able to store them.

PR Close #38536
2020-08-25 15:13:09 -07:00
94a3e0e81d docs: add correct path for karma.conf.js file (#38571)
In the latest Angular CLI versions, the `karma.conf.js` file resides in the root folder of the Angular CLI project.

PR Close #38571
2020-08-25 09:56:32 -07:00
9cbde86534 docs: fix typo in the testing component basics guide (#38573)
The guide ends with a sentence that implies there are more tests following the end of the guide.

PR Close #38573
2020-08-25 09:55:59 -07:00
18e474f522 fix(zone.js): zone.js toString patch should check typeof Promise is function (#38350)
Close #38361

zone.js monkey patch toString, and check the instance is `Promise` or not by using `instanceof Promise`,
sometimes when Promise is not available, the `instanceof` operation fails
and throw `TypeError: Right-hand side of 'instanceof' is not an object`
this PR check `typeof Promise` equals to function or not to prevent the error.

PR Close #38350
2020-08-25 09:51:50 -07:00
cb3db0d31b release: clean up changelog 2020-08-24 15:53:55 -07:00
d36828a7a1 release: cut the v10.1.0-next.8 release 2020-08-24 15:38:13 -07:00
f18e2d5898 docs: release notes for the v10.0.12 release 2020-08-24 15:31:03 -07:00
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
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
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
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
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
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
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
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
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
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
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
956b25a100 docs: apply code styling in template reference variables guide (#38522)
PR Close #38522
2020-08-20 13:01:33 -07:00
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
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
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
6442875c99 docs(core): Fix typo in JSDoc for AbstractType<T> (#38541)
PR Close #38541
2020-08-20 09:30:17 -07:00
8f24bc9443 Revert "fix(router): support lazy loading for empty path named outlets (#38379)"
This reverts commit 7ad32649c0.
2020-08-19 21:05:31 -07:00
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
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
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
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
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
1ec609946f docs: Typos fixes in the binding syntax guide (#38519)
PR Close #38519
2020-08-19 14:05:48 -07:00
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
9ad69c1503 release: cut the zone.js-0.11.1 release (#38537)
PR Close #38537
2020-08-19 10:50:46 -07:00
9af2de821c release: cut the v10.1.0-next.7 release 2020-08-19 09:35:47 -07:00
0270020ac2 docs: release notes for the v10.0.11 release 2020-08-19 09:16:16 -07:00
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
55fd725e74 docs: Fix typo in the inputs and outputs guide (#38524)
PR Close #38524
2020-08-19 08:27:44 -07:00
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
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
199 changed files with 4941 additions and 1811 deletions

View File

@ -73,15 +73,14 @@ merge:
- "packages/zone.js/scripts/**"
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
\nPlease help to unblock it by resolving these conflicts. Thanks!"
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.\nPlease help to unblock it by resolving these conflicts. Thanks!"
# label to monitor
mergeLabel: "PR action: merge"
mergeLabel: "action: merge"
# adding any of these labels will also add the merge label
mergeLinkedLabels:
- "PR action: merge-assistance"
- "action: merge-assistance"
# list of checks that will determine if the merge label can be added
checks:
@ -94,17 +93,17 @@ merge:
# whether the PR shouldn't have a conflict with the base branch
noConflict: true
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
# list of labels that a PR needs to have, checked with a regexp (e.g. "target:" will work for the label "target: master")
requiredLabels:
- "PR target: *"
- "target: *"
- "cla: yes"
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
forbiddenLabels:
- "PR target: TBD"
- "PR action: cleanup"
- "PR action: review"
- "PR state: blocked"
- "target: TBD"
- "action: cleanup"
- "action: review"
- "state: blocked"
- "cla: no"
# list of PR statuses that need to be successful
@ -121,12 +120,7 @@ merge:
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
# {{PLACEHOLDER}} will be replaced by the list of failing checks
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:
\n{{PLACEHOLDER}}
\n
\n**If you want your PR to be merged, it has to pass all the CI checks.**
\n
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:\n{{PLACEHOLDER}}\n\n**If you want your PR to be merged, it has to pass all the CI checks.**\n\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
# options for the triage plugin
triage:
@ -186,4 +180,4 @@ rerunCircleCI:
# set to true to disable
disabled: false
# the label which when added triggers a rerun of the default CircleCI workflow
triggerRerunLabel: "PR action: rerun CI at HEAD"
triggerRerunLabel: "action: rerun CI at HEAD"

19
.ng-dev/caretaker.ts Normal file
View File

@ -0,0 +1,19 @@
import {CaretakerConfig} from '../dev-infra/caretaker/config';
/** The configuration for `ng-dev caretaker` commands. */
export const caretaker: CaretakerConfig = {
githubQueries: [
{
name: 'Merge Queue',
query: `is:pr is:open status:success label:"action: merge"`,
},
{
name: 'Merge Assistance Queue',
query: `is:pr is:open status:success label:"action: merge-assistance"`,
},
{
name: 'Primary Triage Queue',
query: `is:open is:issue no:milestone`,
}
]
};

View File

@ -1,3 +1,4 @@
import {caretaker} from './caretaker';
import {commitMessage} from './commit-message';
import {format} from './format';
import {github} from './github';
@ -8,4 +9,5 @@ module.exports = {
format,
github,
merge,
caretaker,
};

View File

@ -1,38 +1,25 @@
import {MergeConfig} from '../dev-infra/pr/merge/config';
import {DevInfraMergeConfig} from '../dev-infra/pr/merge/config';
import {getDefaultTargetLabelConfiguration} from '../dev-infra/pr/merge/defaults';
import {github} from './github';
/**
* Configuration for the merge tool in `ng-dev`. This sets up the labels which
* are respected by the merge script (e.g. the target labels).
*/
export const merge = (): MergeConfig => {
// TODO: resume dynamically determining patch branch
const patch = '10.0.x';
export const merge: DevInfraMergeConfig['merge'] = async api => {
return {
githubApiMerge: false,
claSignedLabel: 'cla: yes',
mergeReadyLabel: /^PR action: merge(-assistance)?/,
caretakerNoteLabel: 'PR action: merge-assistance',
mergeReadyLabel: /^action: merge(-assistance)?/,
caretakerNoteLabel: 'action: merge-assistance',
commitMessageFixupLabel: 'commit message fixup',
labels: [
{
pattern: 'PR target: master-only',
branches: ['master'],
},
{
pattern: 'PR target: patch-only',
branches: [patch],
},
{
pattern: 'PR target: master & patch',
branches: ['master', patch],
},
],
labels: await getDefaultTargetLabelConfiguration(api, github, '@angular/core'),
requiredBaseCommits: {
// PRs that target either `master` or the patch branch, need to be rebased
// on top of the latest commit message validation fix.
// These SHAs are the commits that update the required license text in the header.
'master': '5aeb9a4124922d8ac08eb73b8f322905a32b0b3a',
[patch]: '27b95ba64a5d99757f4042073fd1860e20e3ed24'
'10.0.x': '27b95ba64a5d99757f4042073fd1860e20e3ed24',
},
};
};

View File

@ -1,26 +1,108 @@
<a name="10.1.0-next.6"></a>
# 10.1.0-next.6 (2020-08-17)
<a name="10.1.0"></a>
# 10.1.0 (2020-09-02)
### Features
* **bazel:** provide LinkablePackageInfo from ng_module ([#37623](https://github.com/angular/angular/issues/37623)) ([6898eab](https://github.com/angular/angular/commit/6898eab))
* **common:** add ReadonlyMap in place of Map in keyValuePipe ([#37311](https://github.com/angular/angular/issues/37311)) ([3373453](https://github.com/angular/angular/commit/3373453)), closes [#37308](https://github.com/angular/angular/issues/37308)
* **compiler-cli:** add `SourceFile.getOriginalLocation()` to sourcemaps package ([#32912](https://github.com/angular/angular/issues/32912)) ([6abb8d0](https://github.com/angular/angular/commit/6abb8d0))
* **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))
* **compiler-cli:** add support for TypeScript 4.0 ([#38076](https://github.com/angular/angular/issues/38076)) ([0fc44e0](https://github.com/angular/angular/commit/0fc44e0))
* **compiler-cli:** explain why an expression cannot be used in AOT compilations ([#37587](https://github.com/angular/angular/issues/37587)) ([712f1bd](https://github.com/angular/angular/commit/712f1bd))
* **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)
* **core:** rename async to waitForAsync to avoid confusing ([#37583](https://github.com/angular/angular/issues/37583)) ([8f07429](https://github.com/angular/angular/commit/8f07429))
* **core:** support injection token as predicate in queries ([#37506](https://github.com/angular/angular/issues/37506)) ([97dc85b](https://github.com/angular/angular/commit/97dc85b)), closes [#21152](https://github.com/angular/angular/issues/21152) [#36144](https://github.com/angular/angular/issues/36144)
* **core:** update reference and doc to change `async` to `waitAsync`. ([#37583](https://github.com/angular/angular/issues/37583)) ([8fbf40b](https://github.com/angular/angular/commit/8fbf40b))
* **forms:** AbstractControl to store raw validators in addition to combined validators function ([#37881](https://github.com/angular/angular/issues/37881)) ([ad7046b](https://github.com/angular/angular/commit/ad7046b))
* **localize:** allow duplicate messages to be handled during extraction ([#38082](https://github.com/angular/angular/issues/38082)) ([cf9a47b](https://github.com/angular/angular/commit/cf9a47b)), closes [#38077](https://github.com/angular/angular/issues/38077)
* **localize:** expose `canParse()` diagnostics ([#37909](https://github.com/angular/angular/issues/37909)) ([ec32eba](https://github.com/angular/angular/commit/ec32eba)), closes [#37901](https://github.com/angular/angular/issues/37901)
* **localize:** implement message extraction tool ([#32912](https://github.com/angular/angular/issues/32912)) ([190561d](https://github.com/angular/angular/commit/190561d))
* **platform-browser:** Allow `sms`-URLs ([#31463](https://github.com/angular/angular/issues/31463)) ([fc5c34d](https://github.com/angular/angular/commit/fc5c34d)), closes [#31462](https://github.com/angular/angular/issues/31462)
* **platform-server:** add option for absolute URL HTTP support ([#37539](https://github.com/angular/angular/issues/37539)) ([d37049a](https://github.com/angular/angular/commit/d37049a)), closes [#37071](https://github.com/angular/angular/issues/37071)
* **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))
### Bug Fixes
* **bazel:** fix integration test for bazel building ([#38629](https://github.com/angular/angular/issues/38629)) ([dd82f2f](https://github.com/angular/angular/commit/dd82f2f))
* **common:** date pipe gives wrong week number ([#37632](https://github.com/angular/angular/issues/37632)) ([ef1fb6d](https://github.com/angular/angular/commit/ef1fb6d)), closes [#33961](https://github.com/angular/angular/issues/33961)
* **common:** narrow `NgIf` context variables in template type checker ([#36627](https://github.com/angular/angular/issues/36627)) ([9c8bc4a](https://github.com/angular/angular/commit/9c8bc4a))
* **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:** ensure source-maps can handle webpack:// protocol ([#32912](https://github.com/angular/angular/issues/32912)) ([decd95e](https://github.com/angular/angular/commit/decd95e))
* **compiler-cli:** only read source-map comment from last line ([#32912](https://github.com/angular/angular/issues/32912)) ([07a07e3](https://github.com/angular/angular/commit/07a07e3))
* **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))
* **compiler:** incorrectly inferring namespace for HTML nodes inside SVG ([#38477](https://github.com/angular/angular/issues/38477)) ([0dda97e](https://github.com/angular/angular/commit/0dda97e)), closes [#37218](https://github.com/angular/angular/issues/37218)
* **compiler:** mark `NgModuleFactory` construction as not side effectful ([#38147](https://github.com/angular/angular/issues/38147)) ([7f8c222](https://github.com/angular/angular/commit/7f8c222))
* **core:** Allow modification of lifecycle hooks any time before bootstrap ([#35464](https://github.com/angular/angular/issues/35464)) ([737506e](https://github.com/angular/angular/commit/737506e)), closes [#30497](https://github.com/angular/angular/issues/30497)
* **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:** determine required DOMParser feature availability ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([c509243](https://github.com/angular/angular/commit/c509243))
* **core:** do not trigger CSP alert/report in Firefox and Chrome ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([b950d46](https://github.com/angular/angular/commit/b950d46)), closes [#25214](https://github.com/angular/angular/issues/25214)
* **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))
* **elements:** run strategy methods in correct zone ([#37814](https://github.com/angular/angular/issues/37814)) ([8df888d](https://github.com/angular/angular/commit/8df888d)), closes [#24181](https://github.com/angular/angular/issues/24181)
* **forms:** handle form groups/arrays own pending async validation ([#22575](https://github.com/angular/angular/issues/22575)) ([77b62a5](https://github.com/angular/angular/commit/77b62a5)), closes [#10064](https://github.com/angular/angular/issues/10064)
* **language-service:** non-existent module format in package output ([#37623](https://github.com/angular/angular/issues/37623)) ([413a0fb](https://github.com/angular/angular/commit/413a0fb))
* **localize:** ensure required XLIFF parameters are serialized ([#38575](https://github.com/angular/angular/issues/38575)) ([f0af387](https://github.com/angular/angular/commit/f0af387)), closes [#38570](https://github.com/angular/angular/issues/38570)
* **localize:** extract the correct message ids ([#38498](https://github.com/angular/angular/issues/38498)) ([ac461e1](https://github.com/angular/angular/commit/ac461e1))
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
* **localize:** render text of extracted placeholders ([#38536](https://github.com/angular/angular/issues/38536)) ([14e90be](https://github.com/angular/angular/commit/14e90be))
* **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))
* **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:** fix navigation ignoring logic to compare to the browser url ([#37716](https://github.com/angular/angular/issues/37716)) ([a5ffca0](https://github.com/angular/angular/commit/a5ffca0)), closes [#16710](https://github.com/angular/angular/issues/16710) [#13586](https://github.com/angular/angular/issues/13586)
* **router:** properly compare array queryParams for equality ([#37709](https://github.com/angular/angular/issues/37709)) ([#37860](https://github.com/angular/angular/issues/37860)) ([1801d0c](https://github.com/angular/angular/commit/1801d0c))
* **router:** remove parenthesis for primary outlet segment after removing auxiliary outlet segment ([#24656](https://github.com/angular/angular/issues/24656)) ([#37163](https://github.com/angular/angular/issues/37163)) ([71f008f](https://github.com/angular/angular/commit/71f008f))
* **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))
* **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)
### 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))
### 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:** fix regressions in incremental program reuse ([#37641](https://github.com/angular/angular/issues/37641)) ([5103d90](https://github.com/angular/angular/commit/5103d90))
* **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))
* **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)
* **ngcc:** shortcircuit tokenizing in ESM dependency host ([#37639](https://github.com/angular/angular/issues/37639)) ([bd7f440](https://github.com/angular/angular/commit/bd7f440))
* **ngcc:** use `EntryPointManifest` to speed up noop `ProgramBaseEntryPointFinder` ([#37665](https://github.com/angular/angular/issues/37665)) ([9318e23](https://github.com/angular/angular/commit/9318e23))
* **router:** apply prioritizedGuardValue operator to optimize CanLoad guards ([#37523](https://github.com/angular/angular/issues/37523)) ([d7dd295](https://github.com/angular/angular/commit/d7dd295))
<a name="10.0.14"></a>
## 10.0.14 (2020-08-26)
<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.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.0.10"></a>
## 10.0.10 (2020-08-17)
@ -36,25 +118,6 @@
<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)
@ -76,24 +139,6 @@
* **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)
### Bug Fixes
* **common:** narrow `NgIf` context variables in template type checker ([#36627](https://github.com/angular/angular/issues/36627)) ([9c8bc4a](https://github.com/angular/angular/commit/9c8bc4a))
* **compiler:** mark `NgModuleFactory` construction as not side effectful ([#38147](https://github.com/angular/angular/issues/38147)) ([7f8c222](https://github.com/angular/angular/commit/7f8c222))
### Features
* **core:** rename async to waitForAsync to avoid confusing ([#37583](https://github.com/angular/angular/issues/37583)) ([8f07429](https://github.com/angular/angular/commit/8f07429))
* **core:** update reference and doc to change `async` to `waitAsync`. ([#37583](https://github.com/angular/angular/issues/37583)) ([8fbf40b](https://github.com/angular/angular/commit/8fbf40b))
<a name="10.0.8"></a>
## 10.0.8 (2020-08-04)
@ -115,16 +160,6 @@
<a name="10.1.0-next.3"></a>
# 10.1.0-next.3 (2020-07-28)
### Bug Fixes
* **elements:** run strategy methods in correct zone ([#37814](https://github.com/angular/angular/issues/37814)) ([8df888d](https://github.com/angular/angular/commit/8df888d)), closes [#24181](https://github.com/angular/angular/issues/24181)
<a name="10.0.6"></a>
## 10.0.6 (2020-07-28)
@ -138,23 +173,6 @@
<a name="10.1.0-next.2"></a>
# 10.1.0-next.2 (2020-07-22)
### Bug Fixes
* **core:** Allow modification of lifecycle hooks any time before bootstrap ([#35464](https://github.com/angular/angular/issues/35464)) ([737506e](https://github.com/angular/angular/commit/737506e)), closes [#30497](https://github.com/angular/angular/issues/30497)
### Features
* **common:** add ReadonlyMap in place of Map in keyValuePipe ([#37311](https://github.com/angular/angular/issues/37311)) ([3373453](https://github.com/angular/angular/commit/3373453)), closes [#37308](https://github.com/angular/angular/issues/37308)
* **forms:** AbstractControl to store raw validators in addition to combined validators function ([#37881](https://github.com/angular/angular/issues/37881)) ([ad7046b](https://github.com/angular/angular/commit/ad7046b))
* **localize:** allow duplicate messages to be handled during extraction ([#38082](https://github.com/angular/angular/issues/38082)) ([cf9a47b](https://github.com/angular/angular/commit/cf9a47b)), closes [#38077](https://github.com/angular/angular/issues/38077)
<a name="10.0.5"></a>
## 10.0.5 (2020-07-22)
@ -189,62 +207,6 @@
* **bazel:** provide LinkablePackageInfo from ng_module ([#37778](https://github.com/angular/angular/issues/37778)) ([6cd10a1](https://github.com/angular/angular/commit/6cd10a1)), closes [/github.com/bazelbuild/rules_nodejs/blob/9a5de3728b05bf1647bbb87ad99f54e626604705/internal/linker/link_node_modules.bzl#L144-L146](https://github.com//github.com/bazelbuild/rules_nodejs/blob/9a5de3728b05bf1647bbb87ad99f54e626604705/internal/linker/link_node_modules.bzl/issues/L144-L146)
<a name="10.1.0-next.1"></a>
# 10.1.0-next.1 (2020-07-15)
### Bug Fixes
* **bazel:** ng_module rule does not expose flat module information in Ivy ([#36971](https://github.com/angular/angular/issues/36971)) ([1550663](https://github.com/angular/angular/commit/1550663))
* **compiler:** check more cases for pipe usage inside host bindings ([#37883](https://github.com/angular/angular/issues/37883)) ([9322b9a](https://github.com/angular/angular/commit/9322b9a)), closes [#34655](https://github.com/angular/angular/issues/34655) [#37610](https://github.com/angular/angular/issues/37610)
* **compiler-cli:** ensure file_system handles mixed Windows drives ([#37959](https://github.com/angular/angular/issues/37959)) ([6b31155](https://github.com/angular/angular/commit/6b31155)), closes [#36777](https://github.com/angular/angular/issues/36777)
* **language-service:** remove completion for string ([#37983](https://github.com/angular/angular/issues/37983)) ([10aba15](https://github.com/angular/angular/commit/10aba15))
* **ngcc:** report a warning if ngcc tries to use a solution-style tsconfig ([#38003](https://github.com/angular/angular/issues/38003)) ([b358495](https://github.com/angular/angular/commit/b358495)), closes [#36386](https://github.com/angular/angular/issues/36386)
* **router:** ensure duplicate popstate/hashchange events are handled correctly ([#37674](https://github.com/angular/angular/issues/37674)) ([9185c6e](https://github.com/angular/angular/commit/9185c6e)), closes [/github.com/angular/angular/issues/16710#issuecomment-646919529](https://github.com//github.com/angular/angular/issues/16710/issues/issuecomment-646919529) [#16710](https://github.com/angular/angular/issues/16710)
* **service-worker:** correctly handle relative base href ([#37922](https://github.com/angular/angular/issues/37922)) ([d19ef65](https://github.com/angular/angular/commit/d19ef65)), closes [#25055](https://github.com/angular/angular/issues/25055) [#25055](https://github.com/angular/angular/issues/25055)
* **service-worker:** correctly serve `ngsw/state` with a non-root SW scope ([#37922](https://github.com/angular/angular/issues/37922)) ([2156bee](https://github.com/angular/angular/commit/2156bee)), closes [#30505](https://github.com/angular/angular/issues/30505)
<a name="10.1.0-next.0"></a>
# 10.1.0-next.0 (2020-07-08)
### Bug Fixes
* **common:** date pipe gives wrong week number ([#37632](https://github.com/angular/angular/issues/37632)) ([ef1fb6d](https://github.com/angular/angular/commit/ef1fb6d)), closes [#33961](https://github.com/angular/angular/issues/33961)
* **compiler-cli:** ensure source-maps can handle webpack:// protocol ([#32912](https://github.com/angular/angular/issues/32912)) ([decd95e](https://github.com/angular/angular/commit/decd95e))
* **compiler-cli:** only read source-map comment from last line ([#32912](https://github.com/angular/angular/issues/32912)) ([07a07e3](https://github.com/angular/angular/commit/07a07e3))
* **core:** determine required DOMParser feature availability ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([c509243](https://github.com/angular/angular/commit/c509243))
* **core:** do not trigger CSP alert/report in Firefox and Chrome ([#36578](https://github.com/angular/angular/issues/36578)) ([#36578](https://github.com/angular/angular/issues/36578)) ([b950d46](https://github.com/angular/angular/commit/b950d46)), closes [#25214](https://github.com/angular/angular/issues/25214)
* **forms:** handle form groups/arrays own pending async validation ([#22575](https://github.com/angular/angular/issues/22575)) ([77b62a5](https://github.com/angular/angular/commit/77b62a5)), closes [#10064](https://github.com/angular/angular/issues/10064)
* **language-service:** non-existent module format in package output ([#37623](https://github.com/angular/angular/issues/37623)) ([413a0fb](https://github.com/angular/angular/commit/413a0fb))
* **router:** fix navigation ignoring logic to compare to the browser url ([#37716](https://github.com/angular/angular/issues/37716)) ([a5ffca0](https://github.com/angular/angular/commit/a5ffca0)), closes [#16710](https://github.com/angular/angular/issues/16710) [#13586](https://github.com/angular/angular/issues/13586)
* **router:** properly compare array queryParams for equality ([#37709](https://github.com/angular/angular/issues/37709)) ([#37860](https://github.com/angular/angular/issues/37860)) ([1801d0c](https://github.com/angular/angular/commit/1801d0c))
* **router:** remove parenthesis for primary outlet segment after removing auxiliary outlet segment ([#24656](https://github.com/angular/angular/issues/24656)) ([#37163](https://github.com/angular/angular/issues/37163)) ([71f008f](https://github.com/angular/angular/commit/71f008f))
### Features
* **bazel:** provide LinkablePackageInfo from ng_module ([#37623](https://github.com/angular/angular/issues/37623)) ([6898eab](https://github.com/angular/angular/commit/6898eab))
* **compiler-cli:** add `SourceFile.getOriginalLocation()` to sourcemaps package ([#32912](https://github.com/angular/angular/issues/32912)) ([6abb8d0](https://github.com/angular/angular/commit/6abb8d0))
* **compiler-cli:** explain why an expression cannot be used in AOT compilations ([#37587](https://github.com/angular/angular/issues/37587)) ([712f1bd](https://github.com/angular/angular/commit/712f1bd))
* **core:** support injection token as predicate in queries ([#37506](https://github.com/angular/angular/issues/37506)) ([97dc85b](https://github.com/angular/angular/commit/97dc85b)), closes [#21152](https://github.com/angular/angular/issues/21152) [#36144](https://github.com/angular/angular/issues/36144)
* **localize:** expose `canParse()` diagnostics ([#37909](https://github.com/angular/angular/issues/37909)) ([ec32eba](https://github.com/angular/angular/commit/ec32eba)), closes [#37901](https://github.com/angular/angular/issues/37901)
* **localize:** implement message extraction tool ([#32912](https://github.com/angular/angular/issues/32912)) ([190561d](https://github.com/angular/angular/commit/190561d))
* **platform-browser:** Allow `sms`-URLs ([#31463](https://github.com/angular/angular/issues/31463)) ([fc5c34d](https://github.com/angular/angular/commit/fc5c34d)), closes [#31462](https://github.com/angular/angular/issues/31462)
* **platform-server:** add option for absolute URL HTTP support ([#37539](https://github.com/angular/angular/issues/37539)) ([d37049a](https://github.com/angular/angular/commit/d37049a)), closes [#37071](https://github.com/angular/angular/issues/37071)
### Performance Improvements
* **compiler-cli:** fix regressions in incremental program reuse ([#37641](https://github.com/angular/angular/issues/37641)) ([5103d90](https://github.com/angular/angular/commit/5103d90))
* **ngcc:** shortcircuit tokenizing in ESM dependency host ([#37639](https://github.com/angular/angular/issues/37639)) ([bd7f440](https://github.com/angular/angular/commit/bd7f440))
* **ngcc:** use `EntryPointManifest` to speed up noop `ProgramBaseEntryPointFinder` ([#37665](https://github.com/angular/angular/issues/37665)) ([9318e23](https://github.com/angular/angular/commit/9318e23))
* **router:** apply prioritizedGuardValue operator to optimize CanLoad guards ([#37523](https://github.com/angular/angular/issues/37523)) ([d7dd295](https://github.com/angular/angular/commit/d7dd295))
<a name="10.0.3"></a>
## 10.0.3 (2020-07-08)

View File

@ -230,7 +230,6 @@ Must be one of the following:
* **fix**: A bug fix
* **perf**: A code change that improves performance
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
* **test**: Adding missing tests or correcting existing tests

View File

@ -10,8 +10,8 @@ import { Hero } from '../shared/hero.model';
template: `
<section>
Our list of heroes:
<hero-profile *ngFor="let hero of heroes" [hero]="hero">
</hero-profile>
<toh-hero *ngFor="let hero of heroes" [hero]="hero">
</toh-hero>
Total powers: {{totalPowers}}<br>
Average power: {{totalPowers / heroes.length}}
</section>

View File

@ -17,7 +17,7 @@ An NgModule is defined by a class decorated with `@NgModule()`. The `@NgModule()
* `imports`: Other modules whose exported classes are needed by component templates declared in *this* NgModule.
* `providers`: Creators of [services](guide/architecture-services) that this NgModule contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level, which is often preferred.)
* `providers`: Creators of [services](guide/architecture-services) that this NgModule contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level.)
* `bootstrap`: The main application view, called the *root component*, which hosts all other app views. Only the *root NgModule* should set the `bootstrap` property.

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

@ -12,7 +12,7 @@ Every application has at least one Angular module, the _root_ module,
which must be present for bootstrapping the application on launch.
By convention and by default, this NgModule is named `AppModule`.
When you use the [Angular CLI](cli) command `ng new` to generate an app, the default `AppModule` is as follows.
When you use the [Angular CLI](cli) command `ng new` to generate an app, the default `AppModule` looks like the following:
```typescript
/* JavaScript imports */
@ -90,8 +90,6 @@ A declarable can only belong to one module, so only declare it in
one `@NgModule`. When you need it elsewhere,
import the module that has the declarable you need in it.
**Only `@NgModule` references** go in the `imports` array.
### Using directives with `@NgModule`
@ -133,7 +131,7 @@ The module's `imports` array appears exclusively in the `@NgModule` metadata obj
It tells Angular about other NgModules that this particular module needs to function properly.
This list of modules are those that export components, directives, or pipes
that the component templates in this module reference. In this case, the component is
that component templates in this module reference. In this case, the component is
`AppComponent`, which references components, directives, or pipes in `BrowserModule`,
`FormsModule`, or `HttpClientModule`.
A component template can reference another component, directive,

View File

@ -79,7 +79,7 @@ To incorporate the feature module into your app, you have to let the root module
<code-example path="feature-modules/src/app/app.module.ts" region="app-module" header="src/app/app.module.ts"></code-example>
Now the `AppModule` knows about the feature module. If you were to add any service providers to the feature module, `AppModule` would know about those too, as would any other feature modules. However, NgModules dont expose their components.
Now the `AppModule` knows about the feature module. If you were to add any service providers to the feature module, `AppModule` would know about those too, as would any other feature modules. However, NgModules dont expose their components by default.
## Rendering a feature modules component template

View File

@ -766,9 +766,11 @@ 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"
"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

@ -87,7 +87,7 @@ To make one, enter the following command in the terminal, where `customers` is t
ng generate module customers --route customers --module app.module
</code-example>
This creates a `customers` folder with the new lazy-loadable module `CustomersModule` defined in the `customers.module.ts` file. The command automatically declares the `CustomersComponent` inside the new feature module.
This creates a `customers` folder having the new lazy-loadable feature module `CustomersModule` defined in the `customers.module.ts` file and the routing module `CustomersRoutingModule` defined in the `customers-routing.module.ts` file. The command automatically declares the `CustomersComponent` and imports `CustomersRoutingModule` inside the new feature module.
Because the new module is meant to be lazy-loaded, the command does NOT add a reference to the new feature module in the application's root module file, `app.module.ts`.
Instead, it adds the declared route, `customers` to the `routes` array declared in the module provided as the `--module` option.

View File

@ -101,7 +101,7 @@ should import `BrowserModule` from `@angular/platform-browser`.
`BrowserModule` provides services that are essential to launch and run a browser app.
`BrowserModule` also re-exports `CommonModule` from `@angular/common`,
which means that components in the `AppModule` module also have access to
which means that components in the `AppModule` also have access to
the Angular directives every app needs, such as `NgIf` and `NgFor`.
Do not import `BrowserModule` in any other module.
@ -140,7 +140,7 @@ declared in this NgModule.
You _can_ export any declarable class&mdash;components, directives, and pipes&mdash;whether
it's declared in this NgModule or in an imported NgModule.
You _can_ re-export entire imported NgModules, which effectively re-exports all of their exported classes.
You _can_ re-export entire imported NgModules, which effectively re-export all of their exported classes.
An NgModule can even export a module that it doesn't import.
<hr/>
@ -192,7 +192,7 @@ Its only purpose is to add http service providers to the application as a whole.
The `forRoot()` static method is a convention that makes it easy for developers to configure services and providers that are intended to be singletons. A good example of `forRoot()` is the `RouterModule.forRoot()` method.
Apps pass a `Routes` object to `RouterModule.forRoot()` in order to configure the app-wide `Router` service with routes.
Apps pass a `Routes` array to `RouterModule.forRoot()` in order to configure the app-wide `Router` service with routes.
`RouterModule.forRoot()` returns a [ModuleWithProviders](api/core/ModuleWithProviders).
You add that result to the `imports` list of the root `AppModule`.

View File

@ -36,7 +36,7 @@ NgModule metadata does the following:
* Declares which components, directives, and pipes belong to the module.
* Makes some of those components, directives, and pipes public so that other module's component templates can use them.
* Imports other modules with the components, directives, and pipes that components in the current module need.
* Provides services that the other application components can use.
* Provides services that other application components can use.
Every Angular app has at least one module, the root module.
You [bootstrap](guide/bootstrapping) that module to launch the application.

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

@ -375,6 +375,5 @@ Some noteworthy observations:
When you're filtering by CSS selector and only testing properties of a browser's _native element_, the `By.css` approach may be overkill.
It's often easier and more clear to filter with a standard `HTMLElement` method
such as `querySelector()` or `querySelectorAll()`,
as you'll see in the next set of tests.
such as `querySelector()` or `querySelectorAll()`.

View File

@ -190,7 +190,7 @@ It knows who the user is based on a property of the injected `UserService`:
<code-example path="testing/src/app/welcome/welcome.component.ts" header="app/welcome/welcome.component.ts"></code-example>
The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing.
Here's the testing module configuration for the spec file, `app/welcome/welcome.component.spec.ts`:
Here's the testing module configuration for the spec file:
<code-example path="testing/src/app/welcome/welcome.component.spec.ts" region="config-test-module" header="app/welcome/welcome.component.spec.ts"></code-example>
@ -415,7 +415,7 @@ You do have to call [tick()](api/core/testing/tick) to advance the (virtual) clo
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
In this case, it waits for the error handler's `setTimeout()`.
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) represents whether to invoke new generated macro tasks when ticking.
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) that represents whether to invoke new generated macro tasks when ticking.
<code-example
path="testing/src/app/demo/async-helper.spec.ts"
@ -594,11 +594,6 @@ Then you can assert that the quote element displays the expected text.
To use `waitForAsync()` functionality, you must import `zone.js/dist/zone-testing` in your test setup file.
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
The `fakeAsync()` utility function has a few limitations.
In particular, it won't work if the test body makes an `XMLHttpRequest` (XHR) call.
XHR calls within a test are rare so you can generally stick with [`fakeAsync()`](#fake-async).
But if you ever do need to call `XMLHttpRequest`, you'll want to know about `waitForAsync()`.
<div class="alert is-helpful">
The `TestBed.compileComponents()` method (see [below](#compile-components)) calls `XHR`
@ -1231,7 +1226,7 @@ and provide for _all_ services injected in _any_ component in the tree.
That's too much effort just to answer a few simple questions about links.
This section describes two techniques for minimizing the setup.
Use them, alone or in combination, to stay focused on the testing the primary component.
Use them, alone or in combination, to stay focused on testing the primary component.
{@a stub-component}
@ -1340,7 +1335,7 @@ The `HostListener` wires the click event of the host element
Clicking the anchor should trigger the `onClick()` method,
which sets the stub's telltale `navigatedTo` property.
Tests inspect `navigatedTo` to confirm that clicking the anchor
set the expected route definition.
sets the expected route definition.
<div class="alert is-helpful">
@ -1573,7 +1568,7 @@ calls to other `TestBed` static methods such as `compileComponents()`.
In this example, the `BannerComponent` is the only component to compile.
Other examples configure the testing module with multiple components
and may import application modules that hold yet more components.
Any of them could be require external files.
Any of them could require external files.
The `TestBed.compileComponents` method asynchronously compiles all components configured in the testing module.

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

@ -75,7 +75,7 @@ The tests run again, the browser refreshes, and the new test results appear.
The CLI takes care of Jasmine and Karma configuration for you.
You can fine-tune many options by editing the `karma.conf.js` and
You can fine-tune many options by editing the `karma.conf.js` in the root folder of the project and
the `test.ts` files in the `src/` folder.
The `karma.conf.js` file is a partial Karma configuration file.
@ -217,7 +217,7 @@ script:
- npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js
```
This does the same things as the Circle CI configuration, except that Travis doesn't come with Chrome, so we use Chromium instead.
This does the same things as the CircleCI configuration, except that Travis doesn't come with Chrome, so use Chromium instead.
Step 2: Commit your changes and push them to your repository.

View File

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

@ -40,7 +40,7 @@ A later part of this tutorial, [Use forms for user input](start/start-forms "Try
<code-example header="src/app/cart.service.ts" path="getting-started/src/app/cart.service.1.ts"></code-example>
<div class="alert is-helpful>
<div class="alert is-helpful">
The StackBlitz generator might provide the cart service in `app.module.ts` by default. That differs from the example, which uses a bundle-optimization technique, an `@Injectable()` decorator with the `{ providedIn: 'root' }` statement.
For more information about services, see [Introduction to Services and Dependency Injection](guide/architecture-services "Concepts > Intro to Services and DI").

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

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

View File

@ -8,6 +8,7 @@ ts_library(
],
module_name = "@angular/dev-infra-private",
deps = [
"//dev-infra/caretaker",
"//dev-infra/commit-message",
"//dev-infra/format",
"//dev-infra/pr",

View File

@ -0,0 +1,26 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "caretaker",
srcs = [
"cli.ts",
],
module_name = "@angular/dev-infra-private/caretaker",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/caretaker/check",
"@npm//@types/yargs",
"@npm//yargs",
],
)
ts_library(
name = "config",
srcs = [
"config.ts",
],
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
],
)

View File

@ -0,0 +1,21 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "check",
srcs = glob(["*.ts"]),
module_name = "@angular/dev-infra-private/caretaker/service-statuses",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/caretaker:config",
"//dev-infra/utils",
"@npm//@types/fs-extra",
"@npm//@types/node",
"@npm//@types/node-fetch",
"@npm//@types/yargs",
"@npm//multimatch",
"@npm//node-fetch",
"@npm//typed-graphqlify",
"@npm//yaml",
"@npm//yargs",
],
)

View File

@ -0,0 +1,27 @@
/**
* @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 {GitClient} from '../../utils/git';
import {getCaretakerConfig} from '../config';
import {printG3Comparison} from './g3';
import {printGithubTasks} from './github';
import {printServiceStatuses} from './services';
/** Check the status of services which Angular caretakers need to monitor. */
export async function checkServiceStatuses(githubToken: string) {
/** The configuration for the caretaker commands. */
const config = getCaretakerConfig();
/** The GitClient for interacting with git and Github. */
const git = new GitClient(githubToken, config);
await printServiceStatuses();
await printGithubTasks(git, config.caretaker);
await printG3Comparison(git);
}

View File

@ -0,0 +1,39 @@
/**
* @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 {addGithubTokenFlag} from '../../utils/yargs';
import {checkServiceStatuses} from './check';
export interface CaretakerCheckOptions {
githubToken: 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 command. */
function builder(yargs: Argv) {
return addGithubTokenFlag(yargs);
}
/** Handles the command. */
async function handler({githubToken}: Arguments<CaretakerCheckOptions>) {
await checkServiceStatuses(githubToken);
}
/** yargs command module for checking status information for the repository */
export const CheckModule: CommandModule<{}, CaretakerCheckOptions> = {
handler,
builder,
command: 'check',
describe: 'Check the status of information the caretaker manages for the repository',
};

View File

@ -0,0 +1,123 @@
/**
* @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} from 'fs-extra';
import * as multimatch from 'multimatch';
import {join} from 'path';
import {parse as parseYaml} from 'yaml';
import {getRepoBaseDir} from '../../utils/config';
import {bold, debug, info} from '../../utils/console';
import {GitClient} from '../../utils/git';
/** Compare the upstream master to the upstream g3 branch, if it exists. */
export async function printG3Comparison(git: GitClient) {
const angularRobotFilePath = join(getRepoBaseDir(), '.github/angular-robot.yml');
if (!existsSync(angularRobotFilePath)) {
return debug('No angular robot configuration file exists, skipping.');
}
/** The configuration defined for the angular robot. */
const robotConfig = parseYaml(readFileSync(angularRobotFilePath).toString());
/** The files to be included in the g3 sync. */
const includeFiles = robotConfig?.merge?.g3Status?.include || [];
/** The files to be expected in the g3 sync. */
const excludeFiles = robotConfig?.merge?.g3Status?.exclude || [];
if (includeFiles.length === 0 && excludeFiles.length === 0) {
debug('No g3Status include or exclude lists are defined in the angular robot configuration,');
debug('skipping.');
return;
}
/** Random prefix to create unique branch names. */
const randomPrefix = `prefix${Math.floor(Math.random() * 1000000)}`;
/** Ref name of the temporary master branch. */
const masterRef = `${randomPrefix}-master`;
/** Ref name of the temporary g3 branch. */
const g3Ref = `${randomPrefix}-g3`;
/** Url of the ref for fetching master and g3 branches. */
const refUrl = `https://github.com/${git.remoteConfig.owner}/${git.remoteConfig.name}.git`;
/** The result fo the fetch command. */
const fetchResult = git.runGraceful(['fetch', refUrl, `master:${masterRef}`, `g3:${g3Ref}`]);
// If the upstream repository does not have a g3 branch to compare to, skip the comparison.
if (fetchResult.status !== 0) {
if (fetchResult.stderr.includes(`couldn't find remote ref g3`)) {
return debug('No g3 branch exists on upstream, skipping.');
}
throw Error('Fetch of master and g3 branches for comparison failed.');
}
/** The statistical information about the git diff between master and g3. */
const stats = getDiffStats(git);
// Delete the temporarily created mater and g3 branches.
git.runGraceful(['branch', '-D', masterRef, g3Ref]);
info.group(bold('g3 branch check'));
info(`${stats.commits} commits between g3 and master`);
if (stats.files === 0) {
info('✅ No sync is needed at this time');
} else {
info(`${stats.files} files changed, ${stats.insertions} insertions(+), ${
stats.deletions} deletions(-) will be included in the next sync`);
}
info.groupEnd();
info();
/**
* Get git diff stats between master and g3, for all files and filtered to only g3 affecting
* files.
*/
function getDiffStats(git: GitClient) {
/** The diff stats to be returned. */
const stats = {
insertions: 0,
deletions: 0,
files: 0,
commits: 0,
};
// Determine the number of commits between master and g3 refs. */
stats.commits = parseInt(git.run(['rev-list', '--count', `${g3Ref}..${masterRef}`]).stdout, 10);
// Get the numstat information between master and g3
git.run(['diff', `${g3Ref}...${masterRef}`, '--numstat'])
.stdout
// Remove the extra space after git's output.
.trim()
// Split each line of git output into array
.split('\n')
// Split each line from the git output into components parts: insertions,
// deletions and file name respectively
.map(line => line.split('\t'))
// Parse number value from the insertions and deletions values
// Example raw line input:
// 10\t5\tsrc/file/name.ts
.map(line => [Number(line[0]), Number(line[1]), line[2]] as [number, number, string])
// Add each line's value to the diff stats, and conditionally to the g3
// stats as well if the file name is included in the files synced to g3.
.forEach(([insertions, deletions, fileName]) => {
if (checkMatchAgainstIncludeAndExclude(fileName, includeFiles, excludeFiles)) {
stats.insertions += insertions;
stats.deletions += deletions;
stats.files += 1;
}
});
return stats;
}
/** Determine whether the file name passes both include and exclude checks. */
function checkMatchAgainstIncludeAndExclude(
file: string, includes: string[], excludes: string[]) {
return multimatch(multimatch(file, includes), excludes, {flipNegate: true}).length !== 0;
}
}

View File

@ -0,0 +1,56 @@
/**
* @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 {alias, params, types} from 'typed-graphqlify';
import {bold, debug, info} from '../../utils/console';
import {GitClient} from '../../utils/git';
import {CaretakerConfig} from '../config';
interface GithubInfoQuery {
[key: string]: {
issueCount: number,
};
}
/** Retrieve the number of matching issues for each github query. */
export async function printGithubTasks(git: GitClient, config: CaretakerConfig) {
if (!config.githubQueries?.length) {
debug('No github queries defined in the configuration, skipping.');
return;
}
info.group(bold('Github Tasks'));
await getGithubInfo(git, config);
info.groupEnd();
info();
}
/** Retrieve query match counts and log discovered counts to the console. */
async function getGithubInfo(git: GitClient, {githubQueries: queries = []}: CaretakerConfig) {
/** The query object for graphql. */
const graphQlQuery: {[key: string]: {issueCount: number}} = {};
/** The Github search filter for the configured repository. */
const repoFilter = `repo:${git.remoteConfig.owner}/${git.remoteConfig.name}`;
queries.forEach(({name, query}) => {
/** The name of the query, with spaces removed to match GraphQL requirements. */
const queryKey = alias(name.replace(/ /g, ''), 'search');
graphQlQuery[queryKey] = params(
{
type: 'ISSUE',
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
},
{issueCount: types.number},
);
});
/** The results of the generated github query. */
const results = await git.github.graphql.query(graphQlQuery);
Object.values(results).forEach((result, i) => {
info(`${queries[i]?.name.padEnd(25)} ${result.issueCount}`);
});
}

View File

@ -0,0 +1,79 @@
/**
* @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 fetch from 'node-fetch';
import {bold, green, info, red} from '../../utils/console';
/** The status levels for services. */
enum ServiceStatus {
GREEN,
RED
}
/** The results of checking the status of a service */
interface StatusCheckResult {
status: ServiceStatus;
description: string;
lastUpdated: Date;
}
/** Retrieve and log stasuses for all of the services of concern. */
export async function printServiceStatuses() {
info.group(bold(`Service Statuses (checked: ${new Date().toLocaleString()})`));
logStatus('CircleCI', await getCircleCiStatus());
logStatus('Github', await getGithubStatus());
logStatus('NPM', await getNpmStatus());
logStatus('Saucelabs', await getSaucelabsStatus());
info.groupEnd();
info();
}
/** Log the status of the service to the console. */
function logStatus(serviceName: string, status: StatusCheckResult) {
serviceName = serviceName.padEnd(15);
if (status.status === ServiceStatus.GREEN) {
info(`${serviceName} ${green('✅')}`);
} else if (status.status === ServiceStatus.RED) {
info.group(`${serviceName} ${red('❌')} (Updated: ${status.lastUpdated.toLocaleString()})`);
info(` Details: ${status.description}`);
info.groupEnd();
}
}
/** Gets the service status information for Saucelabs. */
async function getSaucelabsStatus(): Promise<StatusCheckResult> {
return getStatusFromStandardApi('https://status.us-west-1.saucelabs.com/api/v2/status.json');
}
/** Gets the service status information for NPM. */
async function getNpmStatus(): Promise<StatusCheckResult> {
return getStatusFromStandardApi('https://status.npmjs.org/api/v2/status.json');
}
/** Gets the service status information for CircleCI. */
async function getCircleCiStatus(): Promise<StatusCheckResult> {
return getStatusFromStandardApi('https://status.circleci.com/api/v2/status.json');
}
/** Gets the service status information for Github. */
async function getGithubStatus(): Promise<StatusCheckResult> {
return getStatusFromStandardApi('https://www.githubstatus.com/api/v2/status.json');
}
/** Retrieve the status information for a service which uses a standard API response. */
async function getStatusFromStandardApi(url: string) {
const result = await fetch(url).then(result => result.json());
const status = result.status.indicator === 'none' ? ServiceStatus.GREEN : ServiceStatus.RED;
return {
status,
description: result.status.description,
lastUpdated: new Date(result.page.updated_at)
};
}

View File

@ -0,0 +1,16 @@
/**
* @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 {Argv} from 'yargs';
import {CheckModule} from './check/cli';
/** Build the parser for the caretaker commands. */
export function buildCaretakerParser(yargs: Argv) {
return yargs.command(CheckModule);
}

View File

@ -0,0 +1,24 @@
/**
* @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 {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
export interface CaretakerConfig {
githubQueries?: {name: string; query: string;}[];
}
/** Retrieve and validate the config as `CaretakerConfig`. */
export function getCaretakerConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The non-validated config object.
const config: Partial<NgDevConfig<{caretaker: CaretakerConfig}>> = getConfig();
assertNoErrors(errors);
return config as Required<typeof config>;
}

View File

@ -13,8 +13,11 @@ import {buildCommitMessageParser} from './commit-message/cli';
import {buildFormatParser} from './format/cli';
import {buildReleaseParser} from './release/cli';
import {buildPrParser} from './pr/cli';
import {captureLogOutputForCommand} from './utils/console';
import {buildCaretakerParser} from './caretaker/cli';
yargs.scriptName('ng-dev')
.middleware(captureLogOutputForCommand)
.demandCommand()
.recommendCommands()
.command('commit-message <command>', '', buildCommitMessageParser)
@ -23,6 +26,7 @@ yargs.scriptName('ng-dev')
.command('pullapprove <command>', '', buildPullapproveParser)
.command('release <command>', '', buildReleaseParser)
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
.command('caretaker <command>', '', buildCaretakerParser)
.wrap(120)
.strict()
.parse();

View File

@ -4,6 +4,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "commit-message",
srcs = [
"builder.ts",
"cli.ts",
"commit-message-draft.ts",
"config.ts",
@ -12,14 +13,17 @@ ts_library(
"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",
],
@ -29,6 +33,7 @@ ts_library(
name = "test_lib",
testonly = True,
srcs = [
"builder.spec.ts",
"parse.spec.ts",
"validate.spec.ts",
],

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

@ -12,6 +12,7 @@ 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) {
@ -41,6 +42,23 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
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': {

View File

@ -39,36 +39,56 @@ export enum ScopeRequirement {
/** 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

@ -82,4 +82,24 @@ describe('commit message parsing:', () => {
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

@ -36,6 +36,10 @@ 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 = '';

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

@ -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,36 @@
/**
* @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 {addGithubTokenFlag} from '../../utils/yargs';
import {checkOutPullRequestLocally} from '../common/checkout-pr';
export interface CheckoutOptions {
prNumber: number;
githubToken: string;
}
/** Builds the checkout pull request command. */
function builder(yargs: Argv) {
return addGithubTokenFlag(yargs).positional('prNumber', {type: 'number', demandOption: true});
}
/** Handles the checkout pull request command. */
async function handler({prNumber, githubToken}: Arguments<CheckoutOptions>) {
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

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

@ -8,36 +8,24 @@
import {Arguments, Argv} from 'yargs';
import {error, red, yellow} from '../../utils/console';
import {addGithubTokenFlag} from '../../utils/yargs';
import {GITHUB_TOKEN_GENERATE_URL, mergePullRequest} from './index';
import {mergePullRequest} from './index';
/** The options available to the merge command via CLI. */
export interface MergeCommandOptions {
'github-token'?: string;
githubToken: string;
'pr-number': number;
}
/** Builds the options for the merge command. */
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.'
});
return addGithubTokenFlag(yargs).help().strict().positional(
'pr-number', {demandOption: true, type: 'number'});
}
/** Handles the merge command. i.e. performs the merge of a specified pull request. */
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.'));
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
process.exit(1);
}
await mergePullRequest(args['pr-number'], githubToken);
export async function handleMergeCommand(
{'pr-number': pr, githubToken}: Arguments<MergeCommandOptions>) {
await mergePullRequest(pr, githubToken);
}

View File

@ -165,6 +165,11 @@ export async function findActiveVersionBranches(
latestVersionBranch: string | null,
releaseCandidateBranch: string | null,
}> {
// Version representing the release-train currently in the next phase. Note that we ignore
// patch and pre-release segments in order to be able to compare the next release train to
// other release trains from version branches (which follow the `N.N.x` pattern).
const nextReleaseTrainVersion = semver.parse(`${nextVersion.major}.${nextVersion.minor}.0`)!;
let latestVersionBranch: string|null = null;
let releaseCandidateBranch: string|null = null;
@ -177,15 +182,21 @@ export async function findActiveVersionBranches(
// next version-branch as that one is supposed to be the latest active version-branch. If it
// is not, then an error will be thrown due to two FF/RC branches existing at the same time.
for (const {name, parsed} of branches) {
// It can happen that version branches that are more recent than the version in the next
// branch (i.e. `master`) have been created. We could ignore such branches silently, but
// it might actually be symptomatic for an outdated version in the `next` branch, or an
// It can happen that version branches have been accidentally created which are more recent
// than the release-train in the next branch (i.e. `master`). We could ignore such branches
// silently, but it might be symptomatic for an outdated version in the `next` branch, or an
// accidentally created branch by the caretaker. In either way we want to raise awareness.
if (semver.gte(parsed, nextVersion)) {
if (semver.gt(parsed, nextReleaseTrainVersion)) {
throw Error(
`Discovered unexpected version-branch that is representing a minor ` +
`version more recent than the one in the "${nextBranchName}" branch. Consider ` +
`deleting the branch, or check if the version in "${nextBranchName}" is outdated.`);
`Discovered unexpected version-branch "${name}" for a release-train that is ` +
`more recent than the release-train currently in the "${nextBranchName}" branch. ` +
`Please either delete the branch if created by accident, or update the outdated ` +
`version in the next branch (${nextBranchName}).`);
} else if (semver.eq(parsed, nextReleaseTrainVersion)) {
throw Error(
`Discovered unexpected version-branch "${name}" for a release-train that is already ` +
`active in the "${nextBranchName}" branch. Please either delete the branch if ` +
`created by accident, or update the version in the next branch (${nextBranchName}).`);
}
const version = await getVersionOfBranch(repo, name);

View File

@ -280,7 +280,21 @@ describe('default target labels', () => {
.toBeRejectedWithError('Invalid version detected in following branch: 11.1.x.');
});
it('should error if branch more recent than version in "next" branch is found', async () => {
it('should error if version-branch more recent than "next" is discovered', async () => {
interceptBranchVersionRequest('master', '11.2.0-next.0');
interceptBranchVersionRequest('11.3.x', '11.3.0-next.0');
interceptBranchVersionRequest('11.1.x', '11.1.5');
interceptBranchesListRequest(['11.1.x', '11.3.x']);
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
.toBeRejectedWithError(
'Discovered unexpected version-branch "11.3.x" for a release-train that is ' +
'more recent than the release-train currently in the "master" branch. Please ' +
'either delete the branch if created by accident, or update the outdated version ' +
'in the next branch (master).');
});
it('should error if branch is matching with release-train in the "next" branch', async () => {
interceptBranchVersionRequest('master', '11.2.0-next.0');
interceptBranchVersionRequest('11.2.x', '11.2.0-next.0');
interceptBranchVersionRequest('11.1.x', '11.1.5');
@ -288,9 +302,9 @@ describe('default target labels', () => {
await expectAsync(getBranchesForLabel('target: lts', '10.2.x'))
.toBeRejectedWithError(
'Discovered unexpected version-branch that is representing a minor version more ' +
'recent than the one in the "master" branch. Consider deleting the branch, or check ' +
'if the version in "master" is outdated.');
'Discovered unexpected version-branch "11.2.x" for a release-train that is already ' +
'active in the "master" branch. Please either delete the branch if created by ' +
'accident, or update the version in the next branch (master).');
});
it('should allow merging PR only into patch branch with "target: patch"', async () => {

View File

@ -11,13 +11,11 @@ import {getConfig, getRepoBaseDir} from '../../utils/config';
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
import {GitClient} from '../../utils/git';
import {GithubApiRequestError} from '../../utils/git/github';
import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/yargs';
import {loadAndValidateConfig, MergeConfig, MergeConfigWithRemote} from './config';
import {loadAndValidateConfig, MergeConfigWithRemote} from './config';
import {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/**
* Merges a given pull request based on labels configured in the given merge configuration.

View File

@ -8,39 +8,23 @@
import {Arguments, Argv} from 'yargs';
import {error} from '../../utils/console';
import {addGithubTokenFlag} from '../../utils/yargs';
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`;
/** The options available to the rebase command via CLI. */
export interface RebaseCommandOptions {
'github-token'?: string;
githubToken: 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});
return addGithubTokenFlag(yargs).positional('prNumber', {type: 'number', demandOption: true});
}
/** Handles the rebase pull request command. */
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.');
error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
process.exit(1);
}
await rebasePr(args.prNumber, githubToken);
export async function handleRebaseCommand(
{prNumber, githubToken}: Arguments<RebaseCommandOptions>) {
await rebasePr(prNumber, githubToken);
}

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

@ -12,13 +12,18 @@ ts_library(
"@npm//@octokit/graphql",
"@npm//@octokit/rest",
"@npm//@octokit/types",
"@npm//@types/fs-extra",
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//chalk",
"@npm//fs-extra",
"@npm//inquirer",
"@npm//inquirer-autocomplete-prompt",
"@npm//shelljs",
"@npm//tslib",
"@npm//typed-graphqlify",
"@npm//yargs",
],
)

View File

@ -7,13 +7,19 @@
*/
import chalk from 'chalk';
import {prompt} from 'inquirer';
import {writeFileSync} from 'fs-extra';
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
import {join} from 'path';
import {Arguments} from 'yargs';
import {getRepoBaseDir} from './config';
/** Reexport of chalk colors for convenient access. */
export const red: typeof chalk = chalk.red;
export const green: typeof chalk = chalk.green;
export const yellow: typeof chalk = chalk.yellow;
export const bold: typeof chalk = chalk.bold;
/** Prompts the user with a confirmation question and a specified message. */
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {
@ -26,6 +32,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.
*
@ -93,6 +145,7 @@ function runConsoleCommand(loadCommand: () => Function, logLevel: LOG_LEVELS, ..
if (getLogLevel() >= logLevel) {
loadCommand()(...text);
}
printToLogFile(logLevel, ...text);
}
/**
@ -108,3 +161,56 @@ function getLogLevel() {
}
return logLevel;
}
/** All text to write to the log file. */
let LOGGED_TEXT = '';
/** Whether file logging as been enabled. */
let FILE_LOGGING_ENABLED = false;
/**
* The number of columns used in the prepended log level information on each line of the logging
* output file.
*/
const LOG_LEVEL_COLUMNS = 7;
/**
* Enable writing the logged outputs to the log file on process exit, sets initial lines from the
* command execution, containing information about the timing and command parameters.
*
* This is expected to be called only once during a command run, and should be called by the
* middleware of yargs to enable the file logging before the rest of the command parsing and
* response is executed.
*/
export function captureLogOutputForCommand(argv: Arguments) {
if (FILE_LOGGING_ENABLED) {
throw Error('`captureLogOutputForCommand` cannot be called multiple times');
}
/** The date time used for timestamping when the command was invoked. */
const now = new Date();
/** Header line to separate command runs in log files. */
const headerLine = Array(100).fill('#').join('');
LOGGED_TEXT += `${headerLine}\nCommand: ${argv.$0} ${argv._.join(' ')}\nRan at: ${now}\n`;
// On process exit, write the logged output to the appropriate log files
process.on('exit', (code: number) => {
LOGGED_TEXT += `Command ran in ${new Date().getTime() - now.getTime()}ms`;
/** Path to the log file location. */
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
writeFileSync(logFilePath, LOGGED_TEXT);
// For failure codes greater than 1, the new logged lines should be written to a specific log
// file for the command run failure.
if (code > 1) {
writeFileSync(join(getRepoBaseDir(), `.ng-dev.err-${now.getTime()}.log`), LOGGED_TEXT);
}
});
// Mark file logging as enabled to prevent the function from executing multiple times.
FILE_LOGGING_ENABLED = true;
}
/** Write the provided text to the log file, prepending each line with the log level. */
function printToLogFile(logLevel: LOG_LEVELS, ...text: string[]) {
const logLevelText = `${LOG_LEVELS[logLevel]}:`.padEnd(LOG_LEVEL_COLUMNS);
LOGGED_TEXT += text.join(' ').split('\n').map(l => `${logLevelText} ${l}\n`).join('');
}

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

@ -86,10 +86,10 @@ export class GitClient {
/**
* Spawns a given Git command process. Does not throw if the command fails. Additionally,
* if there is any stderr output, the output will be printed. This makes it easier to
* debug failed commands.
* info failed commands.
*/
runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
// To improve the debugging experience in case something fails, we print all executed
// To improve the infoging experience in case something fails, we print all executed
// Git commands. Note that we do not want to print the token if is contained in the
// command. It's common to share errors with others if the tool failed.
info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
@ -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;
}

37
dev-infra/utils/yargs.ts Normal file
View File

@ -0,0 +1,37 @@
/**
* @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 {Argv} from 'yargs';
import {error, red, yellow} from './console';
export type ArgvWithGithubToken = Argv<{githubToken: string}>;
export function addGithubTokenFlag(yargs: Argv): ArgvWithGithubToken {
return yargs
// 'github-token' is casted to 'githubToken' to properly set up typings to reflect the key in
// the Argv object being camelCase rather than kebob case due to the `camel-case-expansion`
// config: https://github.com/yargs/yargs-parser#camel-case-expansion
.option('github-token' as 'githubToken', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.',
coerce: (token: string) => {
const githubToken = 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.'));
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
process.exit(1);
}
return githubToken;
},
})
.default('github-token' as 'githubToken', '', '<LOCAL TOKEN>');
}
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new';

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

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

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

@ -15,6 +15,6 @@ export class AppComponent {
constructor(private http: HttpClient) {}
time$: Observable<string> =
this.http.get('http://worldclockapi.com/api/json/pst/now')
.pipe(map((result: any) => result.currentDateTime), startWith(['...']));
this.http.get('http://worldtimeapi.org/api/timezone/America/Los_Angeles.json')
.pipe(map((result: any) => result.datetime), startWith(['...']));
}

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

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.1.0-next.6",
"version": "10.1.0",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -76,18 +76,19 @@
"@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",
"@types/minimist": "^1.2.0",
"@types/multimatch": "^4.0.0",
"@types/node": "^12.11.1",
"@types/node-fetch": "^2.5.7",
"@types/selenium-webdriver": "3.0.7",
"@types/semver": "^6.0.2",
"@types/shelljs": "^0.8.6",
"@types/systemjs": "0.19.32",
"@types/yaml": "^1.2.0",
"@types/yaml": "^1.9.7",
"@types/yargs": "^15.0.5",
"@webcomponents/custom-elements": "^1.1.0",
"angular": "npm:angular@1.7",
@ -149,10 +150,10 @@
"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",
"yaml": "^1.10.0",
"yargs": "^15.4.1"
},
"// 2": "devDependencies are not used under Bazel. Many can be removed after test.sh is deleted.",
@ -179,8 +180,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",

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

@ -27,7 +27,7 @@
},
"peerDependencies": {
"@angular/compiler": "0.0.0-PLACEHOLDER",
"typescript": ">=3.9 <4.0"
"typescript": ">=3.9 <4.1"
},
"engines": {
"node": ">=10.0"

View File

@ -476,12 +476,15 @@ export class Evaluator {
return recordEntry(typeReference, node);
case ts.SyntaxKind.UnionType:
const unionType = <ts.UnionTypeNode>node;
// Remove null and undefined from the list of unions.
const references = unionType.types
// TODO(alan-agius4): remove `n.kind !== ts.SyntaxKind.NullKeyword` when
// TS 3.9 support is dropped. In TS 4.0 NullKeyword is a child of LiteralType.
const references =
unionType.types
.filter(
n => n.kind != ts.SyntaxKind.NullKeyword &&
n.kind != ts.SyntaxKind.UndefinedKeyword)
n => n.kind !== ts.SyntaxKind.NullKeyword &&
n.kind !== ts.SyntaxKind.UndefinedKeyword &&
!(ts.isLiteralTypeNode(n) && n.literal.kind === ts.SyntaxKind.NullKeyword))
.map(n => this.evaluateNode(n));
// The remmaining reference must be the same. If two have type arguments consider them

View File

@ -62,8 +62,8 @@ export class Symbols {
// even if the `SourceFile` was not type checked (which looks for `SourceFile`
// in the parent chain). This doesn't damage the node as the binder unconditionally
// sets the parent.
externalReference.expression.parent = externalReference;
externalReference.parent = this.sourceFile as any;
(externalReference.expression.parent as ts.Node) = externalReference;
(externalReference.parent as ts.Node) = this.sourceFile;
}
const from = stripQuotes(externalReference.expression.getText());
symbols.set(
@ -83,8 +83,8 @@ export class Symbols {
}
if (!importDecl.moduleSpecifier.parent) {
// See note above in the `ImportEqualDeclaration` case.
importDecl.moduleSpecifier.parent = importDecl;
importDecl.parent = this.sourceFile;
(importDecl.moduleSpecifier.parent as ts.Node) = importDecl;
(importDecl.parent as ts.Node) = this.sourceFile;
}
const from = stripQuotes(importDecl.moduleSpecifier.getText());
if (importDecl.importClause.name) {

View File

@ -28,7 +28,7 @@ export function generateSetClassMetadataCall(
if (!reflection.isClass(clazz)) {
return null;
}
const id = ts.updateIdentifier(reflection.getAdjacentNameOfClass(clazz));
const id = reflection.getAdjacentNameOfClass(clazz);
// Reflect over the class decorators. If none are present, or those that are aren't from
// Angular, then return null. Otherwise, turn them into metadata.

View File

@ -69,7 +69,7 @@ runInEachFileSystem(() => {
module: ts.ModuleKind.CommonJS,
});
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
const fooId = ts.updateIdentifier(fooClause.name!);
const fooId = fooClause.name!;
const fooDecl = fooClause.parent;
const tracker = new DefaultImportTracker();

View File

@ -20,7 +20,11 @@ export function extractReferencesFromType(
if (!ts.isTupleTypeNode(def)) {
return [];
}
return def.elementTypes.map(element => {
// TODO(alan-agius4): remove `def.elementTypes` and casts when TS 3.9 support is dropped and G3 is
// using TS 4.0.
return (((def as any).elements || (def as any).elementTypes) as ts.NodeArray<ts.TypeNode>)
.map(element => {
if (!ts.isTypeQueryNode(element)) {
throw new Error(`Expected TypeQueryNode: ${nodeDebugInfo(element)}`);
}
@ -69,7 +73,10 @@ export function readStringArrayType(type: ts.TypeNode): string[] {
return [];
}
const res: string[] = [];
type.elementTypes.forEach(el => {
// TODO(alan-agius4): remove `def.elementTypes` and casts when TS 3.9 support is dropped and G3 is
// using TS 4.0.
(((type as any).elements || (type as any).elementTypes) as ts.NodeArray<ts.TypeNode>)
.forEach(el => {
if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) {
return;
}

View File

@ -35,8 +35,9 @@ export function typeToValue(
const {local, decl} = symbols;
// It's only valid to convert a type reference to a value reference if the type actually
// has a value declaration associated with it.
if (decl.valueDeclaration === undefined) {
// has a value declaration associated with it. Note that const enums are an exception,
// because while they do have a value declaration, they don't exist at runtime.
if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) {
return noValueDeclaration(typeNode, decl.declarations[0]);
}
@ -58,9 +59,7 @@ export function typeToValue(
return {
kind: TypeValueReferenceKind.LOCAL,
// Copying the name here ensures the generated references will be correctly transformed
// along with the import.
expression: ts.updateIdentifier(firstDecl.name),
expression: firstDecl.name,
defaultImportStatement: firstDecl.parent,
};
} else if (ts.isImportSpecifier(firstDecl)) {

View File

@ -64,7 +64,11 @@ export class TypeScriptReflectionHost implements ReflectionHost {
// optional tokes that don't have providers.
if (typeNode && ts.isUnionTypeNode(typeNode)) {
let childTypeNodes = typeNode.types.filter(
childTypeNode => childTypeNode.kind !== ts.SyntaxKind.NullKeyword);
// TODO(alan-agius4): remove `childTypeNode.kind !== ts.SyntaxKind.NullKeyword` when
// TS 3.9 support is dropped. In TS 4.0 NullKeyword is a child of LiteralType.
childTypeNode => childTypeNode.kind !== ts.SyntaxKind.NullKeyword &&
!(ts.isLiteralTypeNode(childTypeNode) &&
childTypeNode.literal.kind === ts.SyntaxKind.NullKeyword));
if (childTypeNodes.length === 1) {
typeNode = childTypeNodes[0];

View File

@ -128,8 +128,6 @@ function transformFactorySourceFile(
const {moduleSymbols, sourceFilePath} = factoryMap.get(file.fileName)!;
file = ts.getMutableClone(file);
// Not every exported factory statement is valid. They were generated before the program was
// analyzed, and before ngtsc knew which symbols were actually NgModules. factoryMap contains
// that knowledge now, so this transform filters the statement list and removes exported factories
@ -221,7 +219,8 @@ function transformFactorySourceFile(
// satisfy closure compiler.
transformedStatements.push(nonEmptyExport);
}
file.statements = ts.createNodeArray(transformedStatements);
file = ts.updateSourceFileNode(file, transformedStatements);
// If any imports to @angular/core were detected and rewritten (which happens when compiling
// @angular/core), go through the SourceFile and rewrite references to symbols imported from core.

View File

@ -42,8 +42,7 @@ function flipIvySwitchInFile(sf: ts.SourceFile): ts.SourceFile {
// Only update the statements in the SourceFile if any have changed.
if (newStatements !== undefined) {
sf = ts.getMutableClone(sf);
sf.statements = ts.createNodeArray(newStatements);
return ts.updateSourceFileNode(sf, newStatements);
}
return sf;
}
@ -105,16 +104,12 @@ function flipIvySwitchesInVariableStatement(
// Find the post-switch variable identifier. If one can't be found, it's an error. This is
// reported as a thrown error and not a diagnostic as transformers cannot output diagnostics.
let newIdentifier = findPostSwitchIdentifier(statements, postSwitchName);
const newIdentifier = findPostSwitchIdentifier(statements, postSwitchName);
if (newIdentifier === null) {
throw new Error(`Unable to find identifier ${postSwitchName} in ${
stmt.getSourceFile().fileName} for the Ivy switch.`);
}
// Copy the identifier with updateIdentifier(). This copies the internal information which
// allows TS to write a correct reference to the identifier.
newIdentifier = ts.updateIdentifier(newIdentifier);
newDeclarations[i] = ts.updateVariableDeclaration(
/* node */ decl,
/* name */ decl.name,

View File

@ -28,9 +28,7 @@ export function aliasTransformFactory(exportStatements: Map<string, Map<string,
statements.push(stmt);
});
file = ts.getMutableClone(file);
file.statements = ts.createNodeArray(statements);
return file;
return ts.updateSourceFileNode(file, statements);
};
};
}

View File

@ -239,17 +239,15 @@ export class ReturnTypeTransform implements DtsTransform {
}
transformClassElement(element: ts.ClassElement, imports: ImportManager): ts.ClassElement {
if (!ts.isMethodSignature(element)) {
return element;
}
// // TODO(alan-agius4): Remove when we no longer support TS 3.9
// TS <= 3.9
if (ts.isMethodSignature(element)) {
const original = ts.getOriginalNode(element) as ts.MethodDeclaration;
if (!this.typeReplacements.has(original)) {
return element;
}
const returnType = this.typeReplacements.get(original)!;
const tsReturnType = translateType(returnType, imports);
const methodSignature = ts.updateMethodSignature(
/* node */ element,
/* typeParameters */ element.typeParameters,
@ -259,7 +257,7 @@ export class ReturnTypeTransform implements DtsTransform {
/* questionToken */ element.questionToken);
// Copy over any modifiers, these cannot be set during the `ts.updateMethodSignature` call.
methodSignature.modifiers = element.modifiers;
(methodSignature.modifiers as ts.ModifiersArray | undefined) = element.modifiers;
// A bug in the TypeScript declaration causes `ts.MethodSignature` not to be assignable to
// `ts.ClassElement`. Since `element` was a `ts.MethodSignature` already, transforming it into
@ -267,6 +265,24 @@ export class ReturnTypeTransform implements DtsTransform {
return methodSignature as unknown as ts.ClassElement;
}
// TS 4.0 +
if (ts.isMethodDeclaration(element)) {
const original = ts.getOriginalNode(element, ts.isMethodDeclaration);
if (!this.typeReplacements.has(original)) {
return element;
}
const returnType = this.typeReplacements.get(original)!;
const tsReturnType = translateType(returnType, imports);
return ts.updateMethod(
element, element.decorators, element.modifiers, element.asteriskToken, element.name,
element.questionToken, element.typeParameters, element.parameters, tsReturnType,
element.body);
}
return element;
}
transformFunctionDeclaration(element: ts.FunctionDeclaration, imports: ImportManager):
ts.FunctionDeclaration {
const original = ts.getOriginalNode(element) as ts.FunctionDeclaration;

View File

@ -187,8 +187,8 @@ class IvyTransformationVisitor extends Visitor {
// Create a new `NodeArray` with the filtered decorators that sourcemaps back to the original.
const array = ts.createNodeArray(filtered);
array.pos = node.decorators.pos;
array.end = node.decorators.end;
(array.pos as number) = node.decorators.pos;
(array.end as number) = node.decorators.end;
return array;
}

View File

@ -40,8 +40,9 @@ export function addImports(
// for @fileoverview Closure annotation. If there is no @fileoverview annotations, this
// statement would be a noop.
const fileoverviewAnchorStmt = ts.createNotEmittedStatement(sf);
sf.statements = ts.createNodeArray(
[fileoverviewAnchorStmt, ...existingImports, ...addedImports, ...extraStatements, ...body]);
return ts.updateSourceFileNode(sf, ts.createNodeArray([
fileoverviewAnchorStmt, ...existingImports, ...addedImports, ...extraStatements, ...body
]));
}
return sf;

View File

@ -7,7 +7,7 @@
*/
import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript';
import {DefaultImportRecorder, ImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
@ -24,6 +24,11 @@ export class Context {
}
}
const UNARY_OPERATORS = new Map<UnaryOperator, ts.PrefixUnaryOperator>([
[UnaryOperator.Minus, ts.SyntaxKind.MinusToken],
[UnaryOperator.Plus, ts.SyntaxKind.PlusToken],
]);
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken],
@ -361,6 +366,14 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
}
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context): ts.Expression {
if (!UNARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown unary operator: ${UnaryOperator[ast.operator]}`);
}
return ts.createPrefix(
UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context));
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context): ts.Expression {
if (!BINARY_OPERATORS.has(ast.operator)) {
throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`);
@ -513,7 +526,11 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.TypeNode {
if (ast.value === null) {
return ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword);
// TODO(alan-agius4): Remove when we no longer support TS 3.9
// Use: return ts.createLiteralTypeNode(ts.createNull()) directly.
return ts.versionMajorMinor.charAt(0) === '4' ?
ts.createLiteralTypeNode(ts.createNull() as any) :
ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword as any);
} else if (ast.value === undefined) {
return ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
} else if (typeof ast.value === 'boolean') {
@ -567,6 +584,10 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
throw new Error('Method not implemented.');
}
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context) {
throw new Error('Method not implemented.');
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context) {
throw new Error('Method not implemented.');
}
@ -679,7 +700,7 @@ function createLocalizedStringTaggedTemplate(
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
node.kind = ts.SyntaxKind.TemplateMiddle;
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateMiddle;
return node as ts.TemplateMiddle;
}
@ -687,7 +708,7 @@ function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
function createTemplateTail(cooked: string, raw: string): ts.TemplateTail {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
node.kind = ts.SyntaxKind.TemplateTail;
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail;
return node as ts.TemplateTail;
}

View File

@ -28,6 +28,14 @@ export interface TemplateTypeChecker {
*/
resetOverrides(): void;
/**
* Retrieve the template in use for the given component.
*
* If the template has been overridden via `overrideComponentTemplate`, this will retrieve the
* overridden template nodes.
*/
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null;
/**
* Provide a new template string that will be used in place of the user-defined template when
* checking or operating on the given component.

View File

@ -48,6 +48,29 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
}
}
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
this.ensureShimForComponent(component);
const sf = component.getSourceFile();
const sfPath = absoluteFromSourceFile(sf);
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
const fileRecord = this.getFileData(sfPath);
if (!fileRecord.shimData.has(shimPath)) {
return [];
}
const templateId = fileRecord.sourceManager.getTemplateId(component);
const shimRecord = fileRecord.shimData.get(shimPath)!;
if (!shimRecord.templates.has(templateId)) {
return null;
}
return shimRecord.templates.get(templateId)!.template;
}
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
{nodes: TmplAstNode[], errors?: ParseError[]} {
const {nodes, errors} = parseTemplate(template, 'override.html', {

View File

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
import {BoundTarget, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager} from '../../translator';
import {ComponentToShimMappingStrategy, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {ComponentToShimMappingStrategy, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {TemplateDiagnostic} from './diagnostics';
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
@ -41,6 +41,28 @@ export interface ShimTypeCheckingData {
* Whether any inline operations for the input file were required to generate this shim.
*/
hasInlines: boolean;
/**
* Map of `TemplateId` to information collected about the template during the template
* type-checking process.
*/
templates: Map<TemplateId, TemplateData>;
}
/**
* Data tracked for each template processed by the template type-checking system.
*/
export interface TemplateData {
/**
* Template nodes for which the TCB was generated.
*/
template: TmplAstNode[];
/**
* `BoundTarget` which was used to generate the TCB, and contains bindings for the associated
* template nodes.
*/
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
}
/**
@ -79,6 +101,12 @@ export interface PendingShimData {
* Shim file in the process of being generated.
*/
file: TypeCheckFile;
/**
* Map of `TemplateId` to information collected about the template as it's ingested.
*/
templates: Map<TemplateId, TemplateData>;
}
/**
@ -195,6 +223,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
const fileData = this.dataForFile(ref.node.getSourceFile());
const shimData = this.pendingShimForComponent(ref.node);
const boundTarget = binder.bind({template});
// Get all of the directives used in the template and record type constructors for all of them.
for (const dir of boundTarget.getUsedDirectives()) {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
@ -221,6 +250,11 @@ export class TypeCheckContextImpl implements TypeCheckContext {
});
}
}
const templateId = fileData.sourceManager.getTemplateId(ref.node);
shimData.templates.set(templateId, {
template,
boundTarget,
});
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node);
@ -231,7 +265,6 @@ export class TypeCheckContextImpl implements TypeCheckContext {
// and inlining would be required.
// Record diagnostics to indicate the issues with this template.
const templateId = fileData.sourceManager.getTemplateId(ref.node);
if (tcbRequiresInline) {
shimData.oobRecorder.requiresInlineTcb(templateId, ref.node);
}
@ -348,6 +381,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
],
hasInlines: pendingFileData.hasInlines,
path: pendingShimData.file.fileName,
templates: pendingShimData.templates,
});
updates.set(pendingShimData.file.fileName, pendingShimData.file.render());
}
@ -380,6 +414,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
file: new TypeCheckFile(
shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost),
templates: new Map<TemplateId, TemplateData>(),
});
}
return fileData.shimData.get(shimPath)!;

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, Unary} from '@angular/compiler';
import * as ts from 'typescript';
import {TypeCheckingConfig} from '../api';
@ -17,7 +17,12 @@ export const NULL_AS_ANY =
ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
const UNDEFINED = ts.createIdentifier('undefined');
const BINARY_OPS = new Map<string, ts.SyntaxKind>([
const UNARY_OPS = new Map<string, ts.PrefixUnaryOperator>([
['+', ts.SyntaxKind.PlusToken],
['-', ts.SyntaxKind.MinusToken],
]);
const BINARY_OPS = new Map<string, ts.BinaryOperator>([
['+', ts.SyntaxKind.PlusToken],
['-', ts.SyntaxKind.MinusToken],
['<', ts.SyntaxKind.LessThanToken],
@ -74,6 +79,17 @@ class AstTranslator implements AstVisitor {
return ast.visit(this);
}
visitUnary(ast: Unary): ts.Expression {
const expr = this.translate(ast.expr);
const op = UNARY_OPS.get(ast.operator);
if (op === undefined) {
throw new Error(`Unsupported Unary.operator: ${ast.operator}`);
}
const node = wrapForDiagnostics(ts.createPrefix(op, expr));
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
visitBinary(ast: Binary): ts.Expression {
const lhs = wrapForDiagnostics(this.translate(ast.left));
const rhs = wrapForDiagnostics(this.translate(ast.right));
@ -81,7 +97,7 @@ class AstTranslator implements AstVisitor {
if (op === undefined) {
throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
}
const node = ts.createBinary(lhs, op as any, rhs);
const node = ts.createBinary(lhs, op, rhs);
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
@ -314,6 +330,9 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
return ast.receiver.visit(VeSafeLhsInferenceBugDetector.SINGLETON);
}
visitUnary(ast: Unary): boolean {
return ast.expr.visit(this);
}
visitBinary(ast: Binary): boolean {
return ast.left.visit(this) || ast.right.visit(this);
}

View File

@ -36,6 +36,7 @@ export function canEmitType(type: ts.TypeNode, resolver: TypeReferenceResolver):
visitTypeReferenceNode: type => canEmitTypeReference(type),
visitArrayTypeNode: type => canEmitTypeWorker(type.elementType),
visitKeywordType: () => true,
visitLiteralType: () => true,
visitOtherType: () => false,
});
}
@ -111,6 +112,7 @@ export class TypeEmitter {
visitTypeReferenceNode: type => this.emitTypeReference(type),
visitArrayTypeNode: type => ts.updateArrayTypeNode(type, this.emitType(type.elementType)),
visitKeywordType: type => type,
visitLiteralType: type => type,
visitOtherType: () => {
throw new Error('Unable to emit a complex type');
},
@ -159,6 +161,7 @@ interface TypeEmitterVisitor<R> {
visitTypeReferenceNode(type: ts.TypeReferenceNode): R;
visitArrayTypeNode(type: ts.ArrayTypeNode): R;
visitKeywordType(type: ts.KeywordTypeNode): R;
visitLiteralType(type: ts.LiteralTypeNode): R;
visitOtherType(type: ts.TypeNode): R;
}
@ -167,6 +170,8 @@ function visitTypeNode<R>(type: ts.TypeNode, visitor: TypeEmitterVisitor<R>): R
return visitor.visitTypeReferenceNode(type);
} else if (ts.isArrayTypeNode(type)) {
return visitor.visitArrayTypeNode(type);
} else if (ts.isLiteralTypeNode(type)) {
return visitor.visitLiteralType(type);
}
switch (type.kind) {

View File

@ -248,6 +248,17 @@ runInEachFileSystem(() => {
expect(messages).toEqual([]);
});
it('should treat unary operators as literal types', () => {
const messages = diagnose(`{{ test(-1) + test(+1) + test(-2) }}`, `
class TestComponent {
test(value: -1 | 1): number { return value; }
}`);
expect(messages).toEqual([
`TestComponent.html(1, 31): Argument of type '-2' is not assignable to parameter of type '1 | -1'.`,
]);
});
describe('outputs', () => {
it('should produce a diagnostic for directive outputs', () => {
const messages = diagnose(
@ -445,7 +456,7 @@ class TestComponent {
}`);
expect(messages).toEqual(
[`TestComponent.html(1, 15): Type '2' is not assignable to type 'string'.`]);
[`TestComponent.html(1, 15): Type 'number' is not assignable to type 'string'.`]);
});
});
});

View File

@ -10,6 +10,10 @@ import {tcb, TestDeclaration} from './test_utils';
describe('type check blocks diagnostics', () => {
describe('parse spans', () => {
it('should annotate unary ops', () => {
expect(tcbWithSpans('{{ -a }}')).toContain('(-((ctx).a /*4,5*/) /*4,5*/) /*3,5*/');
});
it('should annotate binary ops', () => {
expect(tcbWithSpans('{{ a + b }}'))
.toContain('(((ctx).a /*3,4*/) /*3,4*/) + (((ctx).b /*7,8*/) /*7,8*/) /*3,8*/');

View File

@ -31,6 +31,11 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE)).toContain('((((ctx).a))!);');
});
it('should handle unary - operator', () => {
const TEMPLATE = `{{-1}}`;
expect(tcb(TEMPLATE)).toContain('(-1);');
});
it('should handle keyed property access', () => {
const TEMPLATE = `{{a[b]}}`;
expect(tcb(TEMPLATE)).toContain('(((ctx).a))[((ctx).b)];');

View File

@ -353,5 +353,40 @@ runInEachFileSystem(os => {
expect(diags2[0].messageText).toContain('invalid-element-b');
expect(diags2[0].messageText).not.toContain('invalid-element-a');
});
describe('getTemplateOfComponent()', () => {
it('should provide access to a component\'s real template', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'Cmp': '<div>Template</div>',
},
}]);
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
const nodes = templateTypeChecker.getTemplate(cmp)!;
expect(nodes).not.toBeNull();
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Template</div>');
});
it('should provide access to an overridden template', () => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'Cmp': '<div>Template</div>',
},
}]);
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
templateTypeChecker.overrideComponentTemplate(cmp, '<div>Overridden</div>');
templateTypeChecker.getDiagnosticsForComponent(cmp);
const nodes = templateTypeChecker.getTemplate(cmp)!;
expect(nodes).not.toBeNull();
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Overridden</div>');
});
});
});
});

View File

@ -140,10 +140,16 @@ function createCtorParametersClassPropertyType(): ts.TypeNode {
undefined),
])),
undefined));
return ts.createFunctionTypeNode(
undefined, [],
ts.createArrayTypeNode(
ts.createUnionTypeNode([ts.createTypeLiteralNode(typeElements), ts.createNull()])));
// TODO(alan-agius4): Remove when we no longer support TS 3.9
const nullLiteral = ts.createNull() as any;
const nullType = ts.versionMajorMinor.charAt(0) === '4' ?
ts.createLiteralTypeNode(nullLiteral as any) :
nullLiteral;
return ts.createFunctionTypeNode(undefined, [], ts.createArrayTypeNode(ts.createUnionTypeNode([
ts.createTypeLiteralNode(typeElements),
nullType,
])));
}
/**
@ -287,8 +293,13 @@ function typeReferenceToExpression(
// Ignore any generic types, just return the base type.
return entityNameToExpression(typeRef.typeName);
case ts.SyntaxKind.UnionType:
// TODO(alan-agius4): remove `t.kind !== ts.SyntaxKind.NullKeyword` when
// TS 3.9 support is dropped. In TS 4.0 NullKeyword is a child of LiteralType.
const childTypeNodes =
(node as ts.UnionTypeNode).types.filter(t => t.kind !== ts.SyntaxKind.NullKeyword);
(node as ts.UnionTypeNode)
.types.filter(
t => t.kind !== ts.SyntaxKind.NullKeyword &&
!(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword));
return childTypeNodes.length === 1 ?
typeReferenceToExpression(entityNameToExpression, childTypeNodes[0]) :
undefined;
@ -298,15 +309,20 @@ function typeReferenceToExpression(
}
/**
* Returns true if the given symbol refers to a value (as distinct from a type).
* Checks whether a given symbol refers to a value that exists at runtime (as distinct from a type).
*
* Expands aliases, which is important for the case where
* import * as x from 'some-module';
* and x is now a value (the module object).
*/
function symbolIsValue(tc: ts.TypeChecker, sym: ts.Symbol): boolean {
if (sym.flags & ts.SymbolFlags.Alias) sym = tc.getAliasedSymbol(sym);
return (sym.flags & ts.SymbolFlags.Value) !== 0;
function symbolIsRuntimeValue(typeChecker: ts.TypeChecker, symbol: ts.Symbol): boolean {
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = typeChecker.getAliasedSymbol(symbol);
}
// Note that const enums are a special case, because
// while they have a value, they don't exist at runtime.
return (symbol.flags & ts.SymbolFlags.Value & ts.SymbolFlags.ConstEnumExcludes) !== 0;
}
/** ParameterDecorationInfo describes the information for a single constructor parameter. */
@ -351,7 +367,7 @@ export function getDownlevelDecoratorsTransform(
const symbol = typeChecker.getSymbolAtLocation(name);
// Check if the entity name references a symbol that is an actual value. If it is not, it
// cannot be referenced by an expression, so return undefined.
if (!symbol || !symbolIsValue(typeChecker, symbol) || !symbol.declarations ||
if (!symbol || !symbolIsRuntimeValue(typeChecker, symbol) || !symbol.declarations ||
symbol.declarations.length === 0) {
return undefined;
}
@ -429,7 +445,7 @@ export function getDownlevelDecoratorsTransform(
const name = (element.name as ts.Identifier).text;
const mutable = ts.getMutableClone(element);
mutable.decorators = decoratorsToKeep.length ?
(mutable as any).decorators = decoratorsToKeep.length ?
ts.setTextRange(ts.createNodeArray(decoratorsToKeep), mutable.decorators) :
undefined;
return [name, mutable, toLower];
@ -546,8 +562,6 @@ export function getDownlevelDecoratorsTransform(
}
}
const newClassDeclaration = ts.getMutableClone(classDecl);
if (decoratorsToLower.length) {
newMembers.push(createDecoratorClassProperty(decoratorsToLower));
}
@ -562,12 +576,13 @@ export function getDownlevelDecoratorsTransform(
if (decoratedProperties.size) {
newMembers.push(createPropDecoratorsClassProperty(diagnostics, decoratedProperties));
}
newClassDeclaration.members = ts.setTextRange(
ts.createNodeArray(newMembers, newClassDeclaration.members.hasTrailingComma),
classDecl.members);
newClassDeclaration.decorators =
decoratorsToKeep.length ? ts.createNodeArray(decoratorsToKeep) : undefined;
return newClassDeclaration;
const members = ts.setTextRange(
ts.createNodeArray(newMembers, classDecl.members.hasTrailingComma), classDecl.members);
return ts.updateClassDeclaration(
classDecl, decoratorsToKeep.length ? decoratorsToKeep : undefined, classDecl.modifiers,
classDecl.name, classDecl.typeParameters, classDecl.heritageClauses, members);
}
/**

View File

@ -167,14 +167,13 @@ function transformSourceFile(
newStatements = tmpStatements;
}
// Note: We cannot use ts.updateSourcefile here as
// it does not work well with decorators.
// See https://github.com/Microsoft/TypeScript/issues/17384
const newSf = ts.getMutableClone(sourceFile);
const newSf = ts.updateSourceFileNode(
sourceFile, ts.setTextRange(ts.createNodeArray(newStatements), sourceFile.statements));
if (!(sourceFile.flags & ts.NodeFlags.Synthesized)) {
newSf.flags &= ~ts.NodeFlags.Synthesized;
(newSf.flags as ts.NodeFlags) &= ~ts.NodeFlags.Synthesized;
}
newSf.statements = ts.setTextRange(ts.createNodeArray(newStatements), sourceFile.statements);
return newSf;
}
@ -209,11 +208,6 @@ export interface RequestsMap {
getRequests(sourceFile: ts.SourceFile): RequestLocationMap;
}
interface MetadataAndLoweringRequests {
metadata: ModuleMetadata|undefined;
requests: RequestLocationMap;
}
function isEligibleForLowering(node: ts.Node|undefined): boolean {
if (node) {
switch (node.kind) {

View File

@ -7,7 +7,7 @@
*/
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript';
import {error} from './util';
@ -622,6 +622,23 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
/* type */ undefined, this._visitStatements(expr.statements)));
}
visitUnaryOperatorExpr(expr: UnaryOperatorExpr):
RecordedNode<ts.UnaryExpression|ts.ParenthesizedExpression> {
let unaryOperator: ts.BinaryOperator;
switch (expr.operator) {
case UnaryOperator.Minus:
unaryOperator = ts.SyntaxKind.MinusToken;
break;
case UnaryOperator.Plus:
unaryOperator = ts.SyntaxKind.PlusToken;
break;
default:
throw new Error(`Unknown operator: ${expr.operator}`);
}
const binary = ts.createPrefix(unaryOperator, expr.expr.visitExpression(this, null));
return this.record(expr, expr.parens ? ts.createParen(binary) : binary);
}
visitBinaryOperatorExpr(expr: BinaryOperatorExpr):
RecordedNode<ts.BinaryExpression|ts.ParenthesizedExpression> {
let binaryOperator: ts.BinaryOperator;

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