Compare commits

...

103 Commits

Author SHA1 Message Date
fbcb66e70c release: cut the v10.1.2 release 2020-09-16 14:38:02 -07:00
3d94919800 refactor(dev-infra): refactor commit-message files (#38845)
Refactor the commit-message files to be consistent with how other ng-dev tooling
is structured.

PR Close #38845
2020-09-15 16:05:46 -07:00
af3b401e15 docs: add ngc to glossary (#36781)
ngc angular compiler was not mentioned in the glossary.
Glossary should contain the relevant terms in angular
which are hard to get. So, added a small defination of
ngc to the glossary

PR Close #36781
2020-09-15 11:29:56 -07:00
e4c12c8f9e test(ngcc): load standard files only once (#38840)
In the integration test suite of ngcc, we load a set of files from
`node_modules` into memory. This includes the `typescript` package and
`@angular` scoped packages, which account for a large number of large
files that needs to be loaded from disk. This commit moves this work
to the top-level, such that it doesn't have to be repeated in all tests.

PR Close #38840
2020-09-15 11:23:28 -07:00
ea36466060 perf(ngcc): reduce maximum worker count (#38840)
Recent optimizations to ngcc have significantly reduced the total time
it takes to process `node_modules`, to such extend that sharding across
multiple processes has become less effective. Previously, running
ngcc asynchronously would allow for up to 8 workers to be allocated,
however these workers have to repeat work that could otherwise be shared.
Because ngcc is now able to reuse more shared computations, the overhead
of multiple workers is increased and therefore becomes less effective.
As an additional benefit, having fewer workers requires less memory and
less startup time.

To give an idea, using the following test setup:

```bash
npx @angular/cli new perf-test
cd perf-test
yarn ng add @angular/material
./node_modules/.bin/ngcc --properties es2015 module main \
  --first-only --create-ivy-entry-points
```

We observe the following figures on CI:

|                   | 10.1.1    | PR #38840 |
| ----------------- | --------- | --------- |
| Sync              | 85s       | 25s       |
| Async (8 workers) | 22s       | 16s       |
| Async (4 workers) | -         | 11s       |

In addition to changing the default number of workers, ngcc will now
use the environment variable `NGCC_MAX_WORKERS` that may be configured
to either reduce or increase the number of workers.

PR Close #38840
2020-09-15 11:23:24 -07:00
58411e7ad9 perf(ngcc): introduce cache for sharing data across entry-points (#38840)
ngcc creates typically two `ts.Program` instances for each entry-point,
one for processing sources and another one for processing the typings.
The creation of these programs is somewhat expensive, as it concerns
module resolution and parsing of source files.

This commit implements several layers of caching to optimize the
creation of programs:

1. A shared module resolution cache across all entry-points within a
   single invocation of ngcc. Both the sources and typings program
   benefit from this cache.
2. Sharing the parsed `ts.SourceFile` for a single entry-point between
   the sources and typings program.
3. Sharing parsed `ts.SourceFile`s of TypeScript's default libraries
   across all entry-points within a single invocation. Some of these
   default library typings are large and therefore expensive to parse,
   so sharing the parsed source files across all entry-points offers
   a significant performance improvement.

Using a bare CLI app created using `ng new` + `ng add @angular/material`,
the above changes offer a 3-4x improvement in ngcc's processing time
when running synchronously and ~2x improvement for asynchronous runs.

PR Close #38840
2020-09-15 11:23:18 -07:00
84bd1a233d feat(dev-infra): include CI status check in the caretaker check (#38779)
Add a CI status check in the ng-dev caretaker check command.

PR Close #38779
2020-09-15 08:45:06 -07:00
ef13d8f33a fix(dev-infra): remove ANSI escape codes from log file outputs (#38792)
Remove the ANSI codes from the log file outputs to make the ng-dev log files
more readable.

PR Close #38792
2020-09-15 08:44:04 -07:00
dc4f85888e build: create temporary script for symbol extractor tests (#38819)
Creates a temporary script to running all symbol extractor tests.

PR Close #38819
2020-09-14 16:54:45 -07:00
2bdfb14be0 build: add tag to symbol-extractor tests (#38819)
Add a tag to symbol-extractor tests to allow for bazel querying.

PR Close #38819
2020-09-14 16:54:42 -07:00
26deef2d3e feat(dev-infra): allow local ng-dev configuration to error on invalid commit messages (#38784)
As part of the commit message conformance check, local commit message checks are
made to be warnings rather than failures. An additional local option is also in
place to allow for the commit message validation failures to be considered errors
instead.

PR Close #38784
2020-09-14 14:34:55 -07:00
f52a494248 docs(elements): convert the ng add command to code-example (#38834)
the command was hardly visible currently
changing it to shell for visibility

PR Close #38834
2020-09-14 14:31:10 -07:00
ebede67433 perf(compiler-cli): optimize computation of type-check scope information (#38843)
When type-checking a component, the declaring NgModule scope is used
to create a directive matcher that contains flattened directive metadata,
i.e. the metadata of a directive and its base classes. This computation
is done for all components, whereas the type-check scope is constant per
NgModule. Additionally, the flattening of metadata is constant per
directive instance so doesn't necessarily have to be recomputed for
each component.

This commit introduces a `TypeCheckScopes` class that is responsible
for flattening directives and computing the scope per NgModule. It
caches the computed results as appropriate to avoid repeated computation.

PR Close #38843
2020-09-14 13:07:08 -07:00
565840515c perf(compiler-cli): only emit directive/pipe references that are used (#38843)
For the compilation of a component, the compiler has to prepare some
information about the directives and pipes that are used in the template.
This information includes an expression for directives/pipes, for usage
within the compilation output. For large NgModule compilation scopes
this has shown to introduce a performance hotspot, as the generation of
expressions is quite expensive. This commit reduces the performance
overhead by only generating expressions for the directives/pipes that
are actually used within the template, significantly cutting down on
the compiler's resolve phase.

PR Close #38843
2020-09-14 13:07:03 -07:00
c62d1cb80a refactor(dev-infra): do not validate config file multiple times (#38808)
Currently we validate the configuration file on each `getConfig`
invocation. We can only validate once since the configuration
is cached.

Also while being at it, renames the cache variables to lower-case as those
do not represent constants (which are convention-wise upper case).

PR Close #38808
2020-09-14 08:34:19 -07:00
aa43cbf8c5 fix(dev-infra): correct build setup for dev-infra (#38815)
Correct's the missing dependencies and incorrect build dependencies for the
dev-infra bazel targets.

PR Close #38815
2020-09-11 13:59:35 -07:00
b05d79d14a build(router): update symbols for routing app (#38817)
This commit updates the golden symbol files for the routing app.

PR Close #38817
2020-09-11 13:20:39 -07:00
04c2bb9580 build: create sample router app (#38714)
This commit creates a sample router test application to introduce the
symbol tests. It serves as a guard to ensure that any future work on the
router package does not unintentionally increase the payload size.

PR Close #38714
2020-09-11 12:10:49 -07:00
ec2dbe7fb4 fix(compiler): detect pipes in ICUs in template binder (#38810)
Recent work on compiler internals in #38539 led to an unexpected failure,
where a pipe used exclusively inside of an ICU would no longer be
emitted into the compilation output. This caused runtime errors due to
missing pipes.

The issue occurred because the change in #38539 would determine the set
of used pipes up-front, independent from the template compilation using
the `R3TargetBinder`. However, `R3TargetBinder` did not consider
expressions within ICUs, so any pipe usages within those expressions
would not be detected. This fix unblocks #38539 and also concerns
upcoming linker work, given that prelink compilations would not go
through full template compilation but only `R3TargetBinder`.

PR Close #38810
2020-09-11 12:07:43 -07:00
8096c63c66 docs: clarify what fixes are merged to LTS versions (#38788)
PR Close #38788
2020-09-11 08:45:46 -07:00
6ff9e6e2bd refactor(dev-infra): add default params to runBenchmark (#38748)
* Use '' as the default for 'url'
* Use [] as the default for 'params'
* Use true as the default for 'ignoreBrowserSynchronization'

PR Close #38748
2020-09-11 08:44:38 -07:00
31d0ee4cbf docs: Move Displaying data topic and ToH tutorial (#38774)
Move the "Displaying data topic" into the Tutorials section.
Move the ToH tutorial to the top of the tutorials section.

PR Close #38774
2020-09-10 14:32:28 -07:00
a47383d1e8 fix(localize): ensure that formatOptions is optional (#38787)
Some lower level APIs are used by CLI, and requiring
the `formatOpions` argument at that level is a
breaking change. This commit makes it optional
at every level to avoid the breaking change.

PR Close #38787
2020-09-10 10:55:08 -07:00
9078187378 docs: add note about only one label target being allowed to be applied to a PR (#38793)
To clarify the use of GitHub labels for targetting PRs, a not about only being
allowed to apply one label is added.

PR Close #38793
2020-09-10 09:41:06 -07:00
dcb473db34 docs: update docs to reflect new PR targeting methods for release trains (#38401) (#38793)
As part of the migration to a common strategy/method for branching and releasing across
the main angular repositories, updates need to be made to the documentation. These changes
reflect the updates made and is based on the following document which describes the
merging label expectations: https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU

PR Close #38401

PR Close #38793
2020-09-10 09:41:00 -07:00
edb7f90363 fix(core): clear the RefreshTransplantedView when detached (#38768)
The `RefreshTransplantedView` flag is used to indicate that the view or one of its children
is transplanted and dirty, so it should still be refreshed as part of change detection.
This flag is set on the transplanted view itself as well setting a
counter on as its parents.
When a transplanted view is detached and still has this flag, it means
it got detached before it was refreshed. This can happen for "backwards
references" or transplanted views that are inserted at a location that
was already checked. In this case, we should decrement the parent
counters _and_ clear the flag on the detached view so it's not seen as
"transplanted" anymore (it is detached and has no parent counters to
adjust).

fixes #38619

PR Close #38768
2020-09-10 09:11:41 -07:00
9c51ba321e fix(router): Ensure routes are processed in priority order and only if needed (#38780)
There is a slight difference between `map`...`concatAll` and `concatMap`
in that the latter (`concatMap`) will ensure that the computations are
executed in-order and only if needed while the former may execute the
`map` body of all items if they do not emit immediately. That is, if the stream
is
`from([a, b, c]).pipe(map(v => of(v).pipe(delay(1))), concatAll(), first())`
the `map` body will execute for all of `a`, `b`, and `c`.
However, the following will only execute the `concatMap` body for `a`
`from([a, b, c]).pipe(concatMap(v => of(v).pipe(delay(1))), first())`

See https://stackblitz.com/edit/rxjs-cvwxyx

fixes #38691

PR Close #38780
2020-09-10 08:54:39 -07:00
d8714d045d refactor(core): _reset() remove nextRecord (#38752)
The nextRecord is not neccessary, so remove it and use record._nextMoved to iterate

PR Close #38752
2020-09-10 08:52:54 -07:00
5de2ac3e1b fix(upgrade): add try/catch when downgrading injectables (#38671)
This commit improves the error thrown by the downgrade module with a more
descriptive message on why the downgrade is failing.

Closes #37579

PR Close #38671
2020-09-10 08:50:18 -07:00
7669bd856f feat(dev-infra): Allow local user ng-dev configuration to disable commit message wizard (#38701)
As not all users, particularly contributors consistently contributing with a deep
understanding of our commit message guidelines, will not want to rely on the
commit message wizard, we allow a user to opt out of using this wizard during
commit message creation.

PR Close #38701
2020-09-09 16:31:17 -07:00
18d911d807 feat(dev-infra): Add support for local user ng-dev configuration (#38701)
Create a utility for loading a local user configuration object to describe
local configuration values, such as skipping the commit message wizard.

PR Close #38701
2020-09-09 16:31:17 -07:00
38ff66dc32 docs: put docs style guide back in left nav (#38683)
PR Close #38683
2020-09-09 16:22:58 -07:00
5672aba2f9 refactor(dev-infra): update commit message validation to return validation result (#38703)
Previously, the validateCommitMessage function ran validation and logged the results.
The validateCommitMessage function now returns an object containing the validation
results and the cli action functions are instead responsible for logging the results.

This is being done as a prefactor for a change which allows for commit message
hook validation to be either a blocking error or a warning.

PR Close #38703
2020-09-09 16:22:36 -07:00
5567bdc48e test(language-service): [ivy] remove all markers from test (#38777)
In the test project there are no longer reference markers and location
markers, so there's no need to "pre-process" the source files to remove
them. This will make the Ivy tests cleaner and faster.

PR Close #38777
2020-09-09 16:21:56 -07:00
1c156eb304 release: cut the v10.1.1 release 2020-09-09 13:12:30 -07:00
396548442e Revert "perf(compiler-cli): only emit directive/pipe references that are used (#38749)" (#38767)
This reverts commit 775c305771.
internal failure:
https://test.corp.google.com/ui#id=OCL:329948619:BASE:329967516:1599160428139:d63165ae

PR Close #38767
2020-09-09 12:24:08 -07:00
c54161098d Revert "perf(compiler-cli): optimize computation of type-check scope information (#38749)" (#38767)
This reverts commit e156e29edd.
internal failure:
https://test.corp.google.com/ui#id=OCL:329948619:BASE:329967516:1599160428139:d63165ae

PR Close #38767
2020-09-09 12:24:08 -07:00
f1b355b54f docs: Restructure table of contents to provide a more streamlined experience (#38763)
This PR is to make the `10.1.x` branch navigation the same as `master`.

PR Close #38763
2020-09-09 10:46:09 -07:00
e40ffb95c8 docs: add Andrew Grekov to GDE resources (#36690)
This commit adds Andrew Grekov to the GDE
resources page and describes his work as a software
engineer using angular and .NET.

PR Close #36690
2020-09-09 09:44:48 -07:00
20564f997f docs: add Sam Vloeberghs to GDE list (#38761)
PR Close #38761
2020-09-09 09:43:37 -07:00
b1398d1771 docs(docs-infra): add The Deep Dive podcast, update Angular inDepth URL (#37621)
Add the new podcast called The Deep Dive to the list of Podcast resources.

Also replace the name and URL for Angular inDepth as the old URL is deprecated.

PR Close #37621
2020-09-09 09:11:11 -07:00
7e9134aae8 docs(router): fixed PreloadAllModules comment typo (#38758)
PR Close #38758
2020-09-09 09:07:50 -07:00
0a55058440 docs: mark the entryComponents array as deprecated (#38616)
The `entryComponents` array is now deprecated (per https://angular.io/api/core/NgModule#entryComponents
and https://angular.io/guide/deprecations#entryComponents).

PR Close #38616
2020-09-09 09:07:19 -07:00
bb1122d087 docs: update v9 support status to LTS (#38744)
PR Close #38744
2020-09-09 09:06:56 -07:00
2ea49c7add fix(dev-infra): set build commit message type to allow an optional scope (#38745)
Allow, optionally, a scope to be used with the build type commit message.

PR Close #38745
2020-09-09 09:06:33 -07:00
83d69978fd docs: Fix component decorator closing brackets (#38754)
PR Close #38754
2020-09-09 09:06:10 -07:00
62de2131e1 docs: add missing space (#38759)
This commit adds a missing space after the comma in listing the rxjs
operators section.

PR Close #38759
2020-09-09 09:05:47 -07:00
e156e29edd perf(compiler-cli): optimize computation of type-check scope information (#38749)
When type-checking a component, the declaring NgModule scope is used
to create a directive matcher that contains flattened directive metadata,
i.e. the metadata of a directive and its base classes. This computation
is done for all components, whereas the type-check scope is constant per
NgModule. Additionally, the flattening of metadata is constant per
directive instance so doesn't necessarily have to be recomputed for
each component.

This commit introduces a `TypeCheckScopes` class that is responsible
for flattening directives and computing the scope per NgModule. It
caches the computed results as appropriate to avoid repeated computation.

PR Close #38749
2020-09-08 15:35:32 -07:00
775c305771 perf(compiler-cli): only emit directive/pipe references that are used (#38749)
For the compilation of a component, the compiler has to prepare some
information about the directives and pipes that are used in the template.
This information includes an expression for directives/pipes, for usage
within the compilation output. For large NgModule compilation scopes
this has shown to introduce a performance hotspot, as the generation of
expressions is quite expensive. This commit reduces the performance
overhead by only generating expressions for the directives/pipes that
are actually used within the template, significantly cutting down on
the compiler's resolve phase.

PR Close #38749
2020-09-08 15:35:32 -07:00
190dca0fdc fix(localize): enable whitespace preservation marker in XLIFF files (#38737)
Whitespace can be relevant in extracted XLIFF translation files.
Some i18n tools - e.g. CAT tool (OmegaT) - will reformat
the file to collapse whitespace if there is no indication to tell it
not to.

This commit adds the ability to specify "format options" that are passed
to the translation file serializer. The XLIFF 1.2 and 2.0 seralizers have
been updated to accept `{"xml:space":"preserve"}` format option which will
by added to the `<file>` element in the serialized translation file during
extraction.

Fixes #38679

PR Close #38737
2020-09-08 14:24:52 -07:00
309709d4b2 fix(router): If users are using the Alt key when clicking the router links, prioritize browser’s default behavior (#38375)
In most browsers, clicking links with the Alt key has a special behavior, for example, Chrome
downloads the target resource. As with other modifier keys, the router should stop the original
navigation to avoid preventing the browser’s default behavior.

When users click a link while holding the Alt key together, the browsers behave as follows.

Windows 10:

| Browser    | Behavior                                    |
|:-----------|:--------------------------------------------|
| Chrome 84  | Download the target resource                |
| Firefox 79 | Prevent navigation and therefore do nothing |
| Edge 84    | Download the target resource                |
| IE 11      | No impact                                   |

macOS Catalina:

| Browser    | Behavior                                    |
|:-----------|:--------------------------------------------|
| Chrome 84  | Download the target resource                |
| Firefox 79 | Prevent navigation and therefore do nothing |
| Safari 13  | Download the target resource                |

PR Close #38375
2020-09-08 14:07:11 -07:00
028ef30b34 docs: Describe a scenario in which ngOnChanges is not called before ngOnInit. (#38625)
Closes #38613

PR Close #38625
2020-09-08 14:06:48 -07:00
56d5ff2a89 fix(compiler-cli): ensure that a declaration is available in type-to-value conversion (#38684)
The type-to-value conversion could previously crash if a symbol was
resolved that does not have any declarations, e.g. because it's imported
from a missing module. This would typically result in a semantic
TypeScript diagnostic and halt further compilation, therefore not
reaching the type-to-value conversion logic. In Bazel however, it turns
out that Angular semantic diagnostics are requested even if there are
semantic TypeScript errors in the program, so it would then reach the
type-to-value conversation and crash.

This commit fixes the unsafe access and adds a test that ignores the
TypeScript semantic error, effectively replicating the situation as
experienced under Bazel.

Fixes #38670

PR Close #38684
2020-09-08 14:06:25 -07:00
b4eb016e56 fix(compiler-cli): compute source-mappings for localized strings (#38747)
Previously, localized strings had very limited or incorrect source-mapping
information available.

Now the i18n AST nodes and related output AST nodes include source-span
information about message-parts and placeholders - including closing tag
placeholders.

This information is then used when generating the final localized string
ASTs to ensure that the correct source-mapping is rendered.

See #38588 (comment)

PR Close #38747
2020-09-08 14:00:35 -07:00
6b0dba48b1 refactor(compiler): move the MessagePiece classes into output_ast.ts (#38747)
The `MessagePiece` and derived classes, `LiteralPiece` and `PlaceholderPiece`
need to be referenced in the `LocalizedString` output AST class, so that we
can render the source-spans of each piece.

PR Close #38747
2020-09-08 14:00:35 -07:00
cfd4c0b4dc refactor(compiler): track the closing source-span of TagPlaceholders (#38747)
The `TagPlaceholder` can contain children, in which case there are two source
spans of interest: the opening tag and the closing tag. This commit now allows
the closing tag source-span to be tracked, so that it can be used later in
source-mapping.

PR Close #38747
2020-09-08 14:00:34 -07:00
38762020d3 refactor(compiler): capture interpolation source-spans in expression parser (#38747)
The expression parser will split the expression up at the interpolation markers
into expressions and static strings. This commit also captures the positions of
these strings in the expression to be used in source-mapping later.

PR Close #38747
2020-09-08 14:00:34 -07:00
a1c34c6f0a fix(compiler): correct confusion between field and property names (#38685)
The `R3TargetBinder` accepts an interface for directive metadata which
declares types for `input` and `output` objects. These types convey the
mapping between the property names for an input or output and the
corresponding property name on the component class. Due to
`R3TargetBinder`'s requirements, this mapping was specified with property
names as keys and field names as values.

However, because of duck typing, this interface was accidentally satisifed
by the opposite mapping, of field names to property names, that was produced
in other parts of the compiler. This form more naturally represents the data
model for inputs.

Rather than accept the field -> property mapping and invert it, this commit
introduces a new abstraction for such mappings which is bidirectional,
eliminating the ambiguous plain object type. This mapping uses new,
unambiguous terminology ("class property name" and "binding property name")
and can be used to satisfy both the needs of the binder as well as those of
the template type-checker (field -> property).

A new test ensures that the input/output metadata produced by the compiler
during analysis is directly compatible with the binder via this unambiguous
new interface.

PR Close #38685
2020-09-08 11:43:03 -07:00
b084bffb64 perf(core): use ngDevMode to tree-shake error messages (#38612)
This commit adds `ngDevMode` guard to throw some errors only in dev mode
(similar to how things work in other parts of Ivy runtime code). The
`ngDevMode` flag helps to tree-shake these error messages from production
builds (in dev mode everything will work as it works right now) to decrease
production bundle size.

PR Close #38612
2020-09-08 11:41:44 -07:00
6a28675a5e fix(ngcc): use aliased exported types correctly (#38666)
If a type has been renamed when it was exported, we need to
reference the external public alias name rather than the internal
original name for the type. Otherwise we will try to import the
type by its internal name, which is not publicly accessible.

Fixes #38238

PR Close #38666
2020-09-08 11:41:21 -07:00
4de8dc3554 fix(localize): do not expose NodeJS typings in $localize runtime code (#38700)
A recent change to `@angular/localize` brought in the `AbsoluteFsPath` type
from the `@angular/compiler-cli`. But this brought along with it a reference
to NodeJS typings - specifically the `FileSystem` interface refers to the
`Buffer` type from NodeJS.

This affects compilation of `@angular/localize` code that will be run in
the browser - for example projects that reference `loadTranslations()`.
The compilation breaks if the NodeJS typings are not included in the build.
Clearly it is not desirable to have these typings included when the project
is not targeting NodeJS.

This commit replaces references to the NodeJS `Buffer` type with `Uint8Array`,
which is available across all platforms and is actually the super-class of
`Buffer`.

Fixes #38692

PR Close #38700
2020-09-08 11:40:58 -07:00
ab4f953c78 fix(localize): render location in XLIFF 2 even if there is no metadata (#38713)
Previously, the location of a translation message, in XLIFF 2, was only
rendered if there were also notes for meaning or description. Now the
location will be rendered even if the other metadata is not provided.

Fixes #38705

PR Close #38713
2020-09-08 11:40:35 -07:00
ee432aaab8 refactor(core): remove deprecated ɵɵselect instruction (#38733)
This instruction was deprecated in 664e0015d4
and is no longer referenced in any meaningful
way, so it can be removed.

PR Close #38733
2020-09-08 11:40:12 -07:00
5863537575 build: upgrade all preview-server JS dependencies to latest versions (#38736)
This commit upgrades all dependencies in `aio/aio-builds-setup/scripts-js/`
to latest versions and also includes all necessary code changes to
ensure the tests are passing with the new dependency versions.

In particular:
- We ensure `nock`'s `Scope#done()` is not called before receiving a
  response to account for a breaking change introduced in
  nock/nock#1960.
- The use of `nock`'s `Scope#log()` method was removed, because the
  method is no longer available since nock/nock#1966. See
  https://github.com/nock/nock#debugging for more info on debugging
  failed matches.

See also
e23ba31b13/migration_guides/migrating_to_13.md
for more info on migrating from `nock` v12 to v13.

PR Close #38736
2020-09-08 10:07:26 -07:00
fcd2eb2ffb fix(dev-infra): change logging of commit message restoration to debug (#38704)
Use debug level of logging for messages in commit message restoration.

PR Close #38704
2020-09-08 10:07:03 -07:00
251a28cb15 docs: remove duplicate trans-unit element closing tag (#38715)
PR Close #38715
2020-09-08 10:06:03 -07:00
54bb1c3d6a docs(zone.js): fix table formatting in markdown (#38723)
PR Close #38723
2020-09-08 10:05:40 -07:00
6c6dd5f38c docs: fix result of sanitization example (#38724)
This is same as #36059 which lost in #36954.
PR Close #38724
2020-09-08 10:04:54 -07:00
9794f20674 docs: fix typos in library guide (#38726)
This PR fixes minor typos in the Creating libraries guide.

PR Close #38726
2020-09-08 10:04:31 -07:00
027b041cfd docs: fix typos in deployment guide (#38727)
This PR fixes some typos regarding the .browserslistrc file in the Deployent guide

PR Close #38727
2020-09-08 10:03:57 -07:00
4886cf5965 docs: word correction (#38729)
PR Close #38729
2020-09-08 10:03:22 -07:00
f21d50d2e6 docs(core): update CONSTS to DECLS (#38731)
This terminology was changed in d5b87d32b0
but a few instances were missed.

PR Close #38731
2020-09-08 10:02:50 -07:00
0ef985368e docs: fix typo in lightweight injection guide (#38741)
PR Close #38741
2020-09-08 10:02:20 -07:00
0a277c6c40 docs: remove reverted bug fix from 10.1 change log (#38718)
PR Close #38718
2020-09-08 09:08:30 -07:00
9bf32c4dcb refactor(compiler-cli): make template parsing errors into diagnostics (#38576)
Previously, the compiler was not able to display template parsing errors as
true `ts.Diagnostic`s that point inside the template. Instead, it would
throw an actual `Error`, and "crash" with a stack trace containing the
template errors.

Not only is this a poor user experience, but it causes the Language Service
to also crash as the user is editing a template (in actuality the LS has to
work around this bug).

With this commit, such parsing errors are converted to true template
diagnostics with appropriate span information to be displayed contextually
along with all other diagnostics. This majorly improves the user experience
and unblocks the Language Service from having to deal with the compiler
"crashing" to report errors.

PR Close #38576
2020-09-03 14:02:44 -07:00
1c2ccfed4d refactor(compiler-cli): split out template diagnostics package (#38576)
The template type-checking engine includes utilities for creating
`ts.Diagnostic`s for component templates. Previously only the template type-
checker itself created such diagnostics. However, the template parser also
produces errors which should be represented as template diagnostics.

This commit prepares for that conversion by extracting the machinery for
producing template diagnostics into its own sub-package, so that other parts
of the compiler can depend on it without depending on the entire template
type-checker.

PR Close #38576
2020-09-03 14:02:39 -07:00
25afbcc459 docs: add dayjs date adapter to resources page (#38031)
PR Close #38031
2020-09-03 12:00:18 -07:00
29c89c9297 docs(dev-infra): fix typo in comment (arguements --> arguments) (#38653)
PR Close #38653
2020-09-03 09:45:03 -07:00
efc76064d9 fix(core): reset tView between tests in Ivy TestBed (#38659)
`tView` that is stored on a component def contains information about directives and pipes
that are available in the scope of this component. Patching component scope causes `tView` to be
updated. Prior to this commit, the `tView` information was not restored/reset in case component
class is not declared in the `declarations` field while calling `TestBed.configureTestingModule`,
thus causing `tView` to be reused between tests (thus preserving scopes information between tests).
This commit updates TestBed logic to preserve `tView` value before applying scope changes and
reset it back to the previous state between tests.

Closes #38600.

PR Close #38659
2020-09-03 09:44:22 -07:00
dbab74429f fix(localize): install @angular/localize in devDependencies by default (#38680)
Previously this package was installed in the default `dependencies` section
of `package.json`, but this meant that its own dependencies are treated as
dependencies of the main project: Babel, for example.

Generally, $localize` is not used at runtime - it is compiled out by the
translation tooling, so there is no need for it to be a full dependency.
In fact, even if it is used at runtime, the package itself is only used
at dev-time since the runtime bits will be bundled into a distributable.
So putting this package in `devDependencies` would only prevent libraries
from bringing the package into application projects that used them. This
is probably good in itself, since it should be up to the downstream project
to decide if it wants to include `@angular/localize` at runtime.

This commit changes the default location of the package to be the
`devDependencies` section, but gives an option `useAtRuntime` to choose
otherwise.

Fixes #38329

PR Close #38680
2020-09-03 09:41:39 -07:00
6aac499ee7 docs: Restructure table of contents to provide a more streamlined experience (#38689)
PR Close #38689
2020-09-02 15:39:25 -07:00
32f33f095f fix(localize): render context of translation file parse errors (#38673)
Previously the position of the error in a translation file when parsing
it was not displayed. Just the error message.

Now the position (line and column) and some context is displayed
along with the error messages.

Fixes #38377

PR Close #38673
2020-09-02 14:46:17 -07:00
b0bd777ba9 docs: correct link to chrome status in component style guide (#38682)
Corrects the link to the chromestatus page which errantly linked to features
rather than feature (singular).

Fixes #38676

PR Close #38682
2020-09-02 14:45:23 -07:00
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
247 changed files with 8662 additions and 2866 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"

3
.gitignore vendored
View File

@ -40,6 +40,9 @@ yarn-error.log
# User specific bazel settings
.bazelrc.user
# User specific ng-dev settings
.ng-dev.user*
.notes.md
baseline.json

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,11 +1,120 @@
<a name="10.1.0-rc.0"></a>
# 10.1.0-rc.0 (2020-08-26)
<a name="10.1.2"></a>
## 10.1.2 (2020-09-16)
### Bug Fixes
* **compiler:** detect pipes in ICUs in template binder ([#38810](https://github.com/angular/angular/issues/38810)) ([ec2dbe7](https://github.com/angular/angular/commit/ec2dbe7)), closes [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539) [#38539](https://github.com/angular/angular/issues/38539)
* **core:** clear the `RefreshTransplantedView` when detached ([#38768](https://github.com/angular/angular/issues/38768)) ([edb7f90](https://github.com/angular/angular/commit/edb7f90)), closes [#38619](https://github.com/angular/angular/issues/38619)
* **localize:** ensure that `formatOptions` is optional ([#38787](https://github.com/angular/angular/issues/38787)) ([a47383d](https://github.com/angular/angular/commit/a47383d))
* **router:** Ensure routes are processed in priority order and only if needed ([#38780](https://github.com/angular/angular/issues/38780)) ([9c51ba3](https://github.com/angular/angular/commit/9c51ba3)), closes [#38691](https://github.com/angular/angular/issues/38691)
* **upgrade:** add try/catch when downgrading injectables ([#38671](https://github.com/angular/angular/issues/38671)) ([5de2ac3](https://github.com/angular/angular/commit/5de2ac3)), closes [#37579](https://github.com/angular/angular/issues/37579)
### Performance Improvements
* **compiler-cli:** only emit directive/pipe references that are used ([#38843](https://github.com/angular/angular/issues/38843)) ([5658405](https://github.com/angular/angular/commit/5658405))
* **compiler-cli:** optimize computation of type-check scope information ([#38843](https://github.com/angular/angular/issues/38843)) ([ebede67](https://github.com/angular/angular/commit/ebede67))
* **ngcc:** introduce cache for sharing data across entry-points ([#38840](https://github.com/angular/angular/issues/38840)) ([58411e7](https://github.com/angular/angular/commit/58411e7))
* **ngcc:** reduce maximum worker count ([#38840](https://github.com/angular/angular/issues/38840)) ([ea36466](https://github.com/angular/angular/commit/ea36466))
<a name="10.1.1"></a>
## 10.1.1 (2020-09-09)
### Bug Fixes
* **compiler:** correct confusion between field and property names ([#38685](https://github.com/angular/angular/issues/38685)) ([a1c34c6](https://github.com/angular/angular/commit/a1c34c6))
* **compiler-cli:** compute source-mappings for localized strings ([#38747](https://github.com/angular/angular/issues/38747)) ([b4eb016](https://github.com/angular/angular/commit/b4eb016)), closes [#38588](https://github.com/angular/angular/issues/38588)
* **compiler-cli:** ensure that a declaration is available in type-to-value conversion ([#38684](https://github.com/angular/angular/issues/38684)) ([56d5ff2](https://github.com/angular/angular/commit/56d5ff2)), closes [#38670](https://github.com/angular/angular/issues/38670)
* **core:** reset `tView` between tests in Ivy TestBed ([#38659](https://github.com/angular/angular/issues/38659)) ([efc7606](https://github.com/angular/angular/commit/efc7606)), closes [#38600](https://github.com/angular/angular/issues/38600)
* **localize:** do not expose NodeJS typings in $localize runtime code ([#38700](https://github.com/angular/angular/issues/38700)) ([4de8dc3](https://github.com/angular/angular/commit/4de8dc3)), closes [#38692](https://github.com/angular/angular/issues/38692)
* **localize:** enable whitespace preservation marker in XLIFF files ([#38737](https://github.com/angular/angular/issues/38737)) ([190dca0](https://github.com/angular/angular/commit/190dca0)), closes [#38679](https://github.com/angular/angular/issues/38679)
* **localize:** install `[@angular](https://github.com/angular)/localize` in `devDependencies` by default ([#38680](https://github.com/angular/angular/issues/38680)) ([dbab744](https://github.com/angular/angular/commit/dbab744)), closes [#38329](https://github.com/angular/angular/issues/38329)
* **localize:** render context of translation file parse errors ([#38673](https://github.com/angular/angular/issues/38673)) ([32f33f0](https://github.com/angular/angular/commit/32f33f0)), closes [#38377](https://github.com/angular/angular/issues/38377)
* **localize:** render location in XLIFF 2 even if there is no metadata ([#38713](https://github.com/angular/angular/issues/38713)) ([ab4f953](https://github.com/angular/angular/commit/ab4f953)), closes [#38705](https://github.com/angular/angular/issues/38705)
* **ngcc:** use aliased exported types correctly ([#38666](https://github.com/angular/angular/issues/38666)) ([6a28675](https://github.com/angular/angular/commit/6a28675)), closes [#38238](https://github.com/angular/angular/issues/38238)
* **router:** If users are using the Alt key when clicking the router links, prioritize browsers default behavior ([#38375](https://github.com/angular/angular/issues/38375)) ([309709d](https://github.com/angular/angular/commit/309709d))
### Performance Improvements
* **core:** use `ngDevMode` to tree-shake error messages ([#38612](https://github.com/angular/angular/issues/38612)) ([b084bff](https://github.com/angular/angular/commit/b084bff))
<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)
* **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))
### 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))
@ -13,29 +122,6 @@
## 10.0.14 (2020-08-26)
<a name="10.1.0-next.8"></a>
# 10.1.0-next.8 (2020-08-24)
### Bug Fixes
* **localize:** extract the correct message ids ([#38498](https://github.com/angular/angular/issues/38498)) ([ac461e1](https://github.com/angular/angular/commit/ac461e1))
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([7ad3264](https://github.com/angular/angular/commit/7ad3264)), closes [#12842](https://github.com/angular/angular/issues/12842)
### Features
* **compiler:** support unary operators for more accurate type checking ([#37918](https://github.com/angular/angular/issues/37918)) ([874792d](https://github.com/angular/angular/commit/874792d)), closes [#20845](https://github.com/angular/angular/issues/20845) [#36178](https://github.com/angular/angular/issues/36178)
* **compiler-cli:** add support for TypeScript 4.0 ([#38076](https://github.com/angular/angular/issues/38076)) ([0fc44e0](https://github.com/angular/angular/commit/0fc44e0))
### Performance Improvements
* **forms:** use internal `ngDevMode` flag to tree-shake error messages in prod builds ([#37821](https://github.com/angular/angular/issues/37821)) ([201a546](https://github.com/angular/angular/commit/201a546)), closes [#37697](https://github.com/angular/angular/issues/37697)
<a name="10.0.12"></a>
## 10.0.12 (2020-08-24)
@ -53,13 +139,6 @@
* **language-service:** introduce hybrid visitor to locate AST node ([#38540](https://github.com/angular/angular/issues/38540)) ([66d8c22](https://github.com/angular/angular/commit/66d8c22))
<a name="10.1.0-next.7"></a>
# 10.1.0-next.7 (2020-08-19)
This release contains various API docs improvements.
<a name="10.0.11"></a>
## 10.0.11 (2020-08-19)
@ -70,29 +149,6 @@ This release contains various API docs improvements.
<a name="10.1.0-next.6"></a>
# 10.1.0-next.6 (2020-08-17)
### Bug Fixes
* **core:** detect DI parameters in JIT mode for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([ca07da4](https://github.com/angular/angular/commit/ca07da4)), closes [#38453](https://github.com/angular/angular/issues/38453)
* **core:** move generated i18n statements to the `consts` field of ComponentDef ([#38404](https://github.com/angular/angular/issues/38404)) ([cb05c01](https://github.com/angular/angular/commit/cb05c01))
* **localize:** render ICU placeholders in extracted translation files ([#38484](https://github.com/angular/angular/issues/38484)) ([81c3e80](https://github.com/angular/angular/commit/81c3e80))
* **ngcc:** detect synthesized delegate constructors for downleveled ES2015 classes ([#38463](https://github.com/angular/angular/issues/38463)) ([3b9c802](https://github.com/angular/angular/commit/3b9c802)), closes [#38453](https://github.com/angular/angular/issues/38453) [#38453](https://github.com/angular/angular/issues/38453)
### Performance Improvements
* **compiler-cli:** don't emit template guards when child scope is empty ([#38418](https://github.com/angular/angular/issues/38418)) ([1388c17](https://github.com/angular/angular/commit/1388c17))
* **compiler-cli:** only generate directive declarations when used ([#38418](https://github.com/angular/angular/issues/38418)) ([fb8f4b4](https://github.com/angular/angular/commit/fb8f4b4))
* **compiler-cli:** only generate type-check code for referenced DOM elements ([#38418](https://github.com/angular/angular/issues/38418)) ([f42e6ce](https://github.com/angular/angular/commit/f42e6ce))
### Code Refactoring
* **router:** export DefaultRouteReuseStrategy to Router public_api ([#31575](https://github.com/angular/angular/issues/31575)) ([ca79880](https://github.com/angular/angular/commit/ca79880))
<a name="10.0.10"></a>
## 10.0.10 (2020-08-17)
@ -108,25 +164,6 @@ This release contains various API docs improvements.
<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)
@ -148,24 +185,6 @@ This release contains various API docs improvements.
* **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)
@ -187,16 +206,6 @@ This release contains various API docs improvements.
<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)
@ -210,23 +219,6 @@ This release contains various API docs improvements.
<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)
@ -261,62 +253,6 @@ This release contains various API docs improvements.
* **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)
@ -598,12 +534,12 @@ https://github.com/microsoft/TypeScript/issues/38374 for more
information and updates.
If you used Closure Compiler with Angular in the past, you will likely
be better off consuming Angular packages built from sources directly
be better off consuming Angular packages built from sources directly
rather than consuming the version we publish on npm,
which is primarily optimized for Webpack/Rollup + Terser build pipeline.
As a temporary workaround, you might consider using your current build
pipeline with Closure flag `--compilation_level=SIMPLE`. This flag
pipeline with Closure flag `--compilation_level=SIMPLE`. This flag
will ensure that your build pipeline produces buildable and
runnable artifacts, at the cost of increased payload size due to
advanced optimizations being disabled.
@ -611,17 +547,17 @@ advanced optimizations being disabled.
If you were affected by this change, please help us understand your
needs by leaving a comment on https://github.com/angular/angular/issues/37234.
* **core:** make generic mandatory for ModuleWithProviders
* **core:** make generic mandatory for ModuleWithProviders
A generic type parameter has always been required for the `ModuleWithProviders` pattern to work with Ivy, but prior to this commit, View Engine allowed the generic type to be omitted (though support was officially deprecated).
If you're using `ModuleWithProviders` without a generic type in your application code, a v10 migration will update your code for you.
If you're using `ModuleWithProviders` without a generic type in your application code, a v10 migration will update your code for you.
However, if you are using View Engine and also depending on a library that omits the generic type, you will now get a build time error similar to:
```
error TS2314: Generic type 'ModuleWithProviders<T>' requires 1 type argument(s).
```
In this case, ngcc won't help you (because it's Ivy-only) and the migration only covers application code.
You should contact the library author to fix their library to provide a type parameter when they use this class.
@ -1949,7 +1885,7 @@ API surface going forward.
* **core:** Injector.get now accepts abstract classes to return
type-safe values. Previous implementation returned `any` through the
deprecated implementation.
* Angular now compiles with Ivy by default ([#32219](https://github.com/angular/angular/issues/32219)) ([ec4381d](https://github.com/angular/angular/commit/ec4381d)).
* Angular now compiles with Ivy by default ([#32219](https://github.com/angular/angular/issues/32219)) ([ec4381d](https://github.com/angular/angular/commit/ec4381d)).
If you aren't familiar with Ivy, read our [blog post about the Ivy preview](https://blog.angular.io/its-time-for-the-compatibility-opt-in-preview-of-ivy-38f3542a282f?gi=8bfeb44b05c) and see the list of changes [here](https://docs.google.com/document/d/1Dije0AsJ0PxL3NaeNPxpYDeapj30b_QC0xfeIvIIzgg/preview).

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

@ -16,13 +16,6 @@ import {BuildNums, PrNums, SHA} from './constants';
const logger = new Logger('mock-external-apis');
const log = (...args: any[]) => {
// Filter out non-matching URL checks
if (!/^matching.+: false$/.test(args[0])) {
logger.log(...args);
}
};
const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
@ -91,8 +84,8 @@ const createArchive = (buildNum: number, prNum: number, sha: string) => {
};
// Create request scopes
const circleCiApi = nock(CIRCLE_CI_API_HOST).log(log).persist();
const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
const circleCiApi = nock(CIRCLE_CI_API_HOST).persist();
const githubApi = nock(GITHUB_API_HOST).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
//////////////////////////////

View File

@ -27,28 +27,28 @@
"body-parser": "^1.19.0",
"delete-empty": "^3.0.0",
"express": "^4.17.1",
"jasmine": "^3.5.0",
"nock": "^12.0.3",
"node-fetch": "^2.6.0",
"jasmine": "^3.6.1",
"nock": "^13.0.4",
"node-fetch": "^2.6.1",
"shelljs": "^0.8.4",
"source-map-support": "^0.5.19",
"tar-stream": "^2.1.2",
"tslib": "^1.11.1"
"tar-stream": "^2.1.3",
"tslib": "^2.0.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/express": "^4.17.6",
"@types/jasmine": "^3.5.10",
"@types/express": "^4.17.8",
"@types/jasmine": "^3.5.14",
"@types/nock": "^11.1.0",
"@types/node": "^13.13.2",
"@types/node": "^14.6.4",
"@types/node-fetch": "^2.5.7",
"@types/shelljs": "^0.8.7",
"@types/supertest": "^2.0.8",
"nodemon": "^2.0.3",
"@types/shelljs": "^0.8.8",
"@types/supertest": "^2.0.10",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"supertest": "^4.0.2",
"tslint": "^6.1.1",
"tslint": "^6.1.3",
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
"typescript": "^3.8.3"
"typescript": "^4.0.2"
}
}

View File

@ -214,23 +214,24 @@ describe('GithubApi', () => {
});
it('should call \'https.request()\' with the correct options', () => {
it('should call \'https.request()\' with the correct options', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method')
.reply(200);
(api as any).request('method', '/path');
await (api as any).request('method', '/path');
requestHandler.done();
});
it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
it('should add the \'Authorization\' header containing the \'githubToken\'', async () => {
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method', undefined, {
reqheaders: {Authorization: 'token 12345'},
})
.reply(200);
(api as any).request('method', '/path');
await (api as any).request('method', '/path');
requestHandler.done();
});
@ -244,12 +245,13 @@ describe('GithubApi', () => {
});
it('should \'JSON.stringify\' and send the data along with the request', () => {
it('should \'JSON.stringify\' and send the data along with the request', async () => {
const data = {key: 'value'};
const requestHandler = nock('https://api.github.com')
.intercept('/path', 'method', JSON.stringify(data))
.reply(200);
(api as any).request('method', '/path', data);
await (api as any).request('method', '/path', data);
requestHandler.done();
});

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,6 @@
<!-- #enddocregion translated-plural -->
<!-- #docregion translated-select -->
<!-- #docregion translate-select-1 -->
</trans-unit>
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>

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

@ -62,7 +62,7 @@ In the following example, the `@Component()` metadata object and the class const
@Component({
selector: 'app-typical',
template: '<div>A typical component for {{data.name}}</div>'
)}
})
export class TypicalComponent {
@Input() data: TypicalData;
constructor(private someService: SomeService) { ... }

View File

@ -125,7 +125,7 @@ Emulated is the default and most commonly used view encapsulation. For more info
<div class="alert is-important">
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/features/6750456638341120) and tools.
The shadow-piercing descendant combinator is deprecated and [support is being removed from major browsers](https://www.chromestatus.com/feature/6750456638341120) and tools.
As such we plan to drop support in Angular (for all 3 of `/deep/`, `>>>` and `::ng-deep`).
Until then `::ng-deep` should be preferred for a broader compatibility with the tools.

View File

@ -26,7 +26,7 @@ The `ng generate` command creates the `projects/my-lib` folder in your workspace
</div>
When you generate a new library, the workspace configuration file, `angular.json`, is updated with a project of type 'library'.
When you generate a new library, the workspace configuration file, `angular.json`, is updated with a project of type `library`.
<code-example format="json">
"projects": {
@ -109,7 +109,7 @@ If you want a dropdown that would contain different passed-in values each time,
Suppose you want to read a configuration file and then generate a form based on that configuration.
If that form will need additional customization by the developer who is using your library, it might work best as a schematic.
However, if the forms will always be the same and not need much customization by developers, then you could create a dynamic component that takes the configuration and generates the form.
However, if the form will always be the same and not need much customization by developers, then you could create a dynamic component that takes the configuration and generates the form.
In general, the more complex the customization, the more useful the schematic approach.
To learn more, see [Schematics Overview](guide/schematics) and [Schematicsfor Libraries](guide/schematics-for-libraries).

View File

@ -511,9 +511,9 @@ Each script tag has a `type="module"` or `nomodule` attribute. Browsers with nat
To include differential loading in your application builds, you must configure the Browserslist and TypeScript configuration files in your application project.
The following examples show a `browserlistrc` and `tsconfig.json` file for a newly created Angular application. In this configuration, legacy browsers such as IE 9-11 are ignored, and the compilation target is ES2015.
The following examples show a `.browserslistrc` and `tsconfig.json` file for a newly created Angular application. In this configuration, legacy browsers such as IE 9-11 are ignored, and the compilation target is ES2015.
<code-example language="none" header="browserslistrc">
<code-example language="none" header=".browserslistrc">
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
@ -527,7 +527,7 @@ The following examples show a `browserlistrc` and `tsconfig.json` file for a new
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@ -119,7 +119,14 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
In browsers that support Custom Elements natively, the specification requires developers use ES2015 classes to define Custom Elements - developers can opt-in to this by setting the `target: "es2015"` property in their project's [TypeScript configuration file](/guide/typescript-configuration). As Custom Element and ES2015 support may not be available in all browsers, developers can instead choose to use a polyfill to support older browsers and ES5 code.
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --project=*your_project_name*`.
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill:
<code-example language="sh">
ng add @angular/elements --project=*your_project_name*
</code-example>
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
- For more information about Angular browser support, see [Browser Support](guide/browser-support).

View File

@ -76,6 +76,12 @@ All router components must be entry components. Because this would require you t
## The `entryComponents` array
<div class="alert is-helpful">
Since 9.0.0 with Ivy, the `entryComponents` property is no longer necessary. See [deprecations guide](guide/deprecations#entryComponents).
</div>
Though the `@NgModule` decorator has an `entryComponents` array, most of the time
you won't have to explicitly set any entry components because Angular adds components listed in `@NgModule.bootstrap` and those in route definitions to entry components automatically. Though these two mechanisms account for most entry components, if your app happens to bootstrap or dynamically load a component by type imperatively,
you must add it to `entryComponents` explicitly.

View File

@ -627,6 +627,11 @@ The [npm package manager](https://docs.npmjs.com/getting-started/what-is-npm) is
Learn more about how Angular uses [Npm Packages](guide/npm-packages).
{@ ngc}
## ngc
`ngc` is a Typescript-to-Javascript transpiler that processes Angular decorators, metadata, and templates, and emits JavaScript code.
The most recent implementation is internally refered to as `ngtsc` because it's a minimalistic wrapper around the TypeScript compiler `tsc` that adds a transform for processing Angular code.
{@a O}
{@a observable}

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

@ -62,6 +62,8 @@ Angular executes hook methods in the following sequence. You can use them to per
Called before `ngOnInit()` and whenever one or more data-bound input properties change.
Note that if your component has no inputs or you use it without providing any inputs, the framework will not call `ngOnChanges()`.
</td>
</tr>
<tr style='vertical-align:top'>

View File

@ -141,7 +141,7 @@ Because the token is now an abstract class, and the injectable component impleme
The implementation of the method (with all of its code overhead) resides in the injectable component that can be tree-shaken.
This allows the parent to communicate with the child (if it is present) in a type-safe manner.
For example, the `LibCardComponent` now queries`LibHeaderToken` rather than `LibHeaderComponent`.
For example, the `LibCardComponent` now queries `LibHeaderToken` rather than `LibHeaderComponent`.
The following example shows how the pattern allows `LibCardComponent` to communicate with the `LibHeaderComponent` without actually referring to `LibHeaderComponent`.
```

View File

@ -223,6 +223,6 @@ content harmlessly. The following is the browser output
of the `evilTitle` examples.
<code-example language="bash">
"Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated evil title.
"Template alert("evil never sleeps")Syntax" is the property bound evil title.
"Template &lt;script&gt;alert("evil never sleeps")&lt;/script&gt; Syntax" is the interpolated evil title.
"Template Syntax" is the property bound evil title.
</code-example>

View File

@ -94,7 +94,7 @@ All of our major releases are supported for 18 months.
* 6 months of *active support*, during which regularly-scheduled updates and patches are released.
* 12 months of *long-term support (LTS)*, during which only critical fixes and security patches are released.
* 12 months of *long-term support (LTS)*, during which only [critical fixes and security patches](#lts-fixes) are released.
The following table provides the status for Angular versions under support.
@ -102,11 +102,18 @@ The following table provides the status for Angular versions under support.
Version | Status | Released | Active Ends | LTS Ends
------- | ------ | ------------ | ------------ | ------------
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
^9.0.0 | LTS | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
### LTS fixes
As a general rule, a fix is considered for an LTS version if it resolves one of:
* a newly identified security vulnerability,
* a regression, since the start of LTS, caused by a 3rd party change, such as a new browser version.
{@a deprecation}
## Deprecation practices

View File

@ -53,7 +53,7 @@ RxJS provides many operators, but only a handful are used frequently. For a list
| Area | Operators |
| :------------| :----------|
| Creation | `from`,`fromEvent`, `of` |
| Creation | `from`, `fromEvent`, `of` |
| Combination | `combineLatest`, `concat`, `merge`, `startWith` , `withLatestFrom`, `zip` |
| Filtering | `debounceTime`, `distinctUntilChanged`, `filter`, `take`, `takeUntil` |
| Transformation | `bufferTime`, `concatMap`, `map`, `mergeMap`, `scan`, `switchMap` |

View File

@ -107,7 +107,7 @@ Notice that all of the files the browser needs to render this application are ca
<div class="alert is-helpful">
Pay attention to two key points:
1. The generated `ngsw-config.json` includes a limited list of cacheable fonts and images extentions. In some cases, you might want to modify the glob pattern to suit your needs.
1. The generated `ngsw-config.json` includes a limited list of cacheable fonts and images extensions. In some cases, you might want to modify the glob pattern to suit your needs.
1. If `resourcesOutputPath` or `assets` paths are modified after the generation of configuration file, you need to change the paths manually in `ngsw-config.json`.
</div>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -810,5 +810,21 @@
"website": "kreuzercode.com",
"bio": "Kevin is a passionate freelance front-end engineer and Google Developer Expert based in Switzerland. He is a JavaScript enthusiast and fascinated by Angular. Kevin always tries to learn new things, expand his knowledge, and share it with others in the form of blog posts, workshops, podcasts, or presentations. He is a writer for various publications and the most active writer on Angular in-depth in 2019. Contributing to multiple projects and maintaining 7 npm packages, Kevin is also a big believer in open source. Furthermore, Kevin is a big football fan. Since his childhood, he has supported Real Madrid, which you might notice in a lot of his blog posts and tutorials.",
"groups": ["GDE"]
},
"samvloeberghs": {
"name": "Sam Vloeberghs",
"picture": "samvloeberghs.jpg",
"groups": ["GDE"],
"twitter": "samvloeberghs",
"website": "https://samvloeberghs.be",
"bio": "Sam is a freelance software architect and Internet entrepreneur, currently focusing on frontend technologies. Co-organiser of the Belgian Angular conference NG-BE and Angular Belgium Meetup group."
},
"thekiba": {
"name": "Andrew Grekov",
"picture": "thekiba.jpg",
"twitter": "thekiba_io",
"website": "https://thekiba.io",
"bio": "Andrew is a software engineer using Angular and .NET. He spends most of his spare time staying up-to-date, helping other people, and experimenting with web tech.",
"groups": ["GDE"]
}
}

View File

@ -30,10 +30,10 @@
"url": "https://dev.to/t/angular",
"title": "DEV Community"
},
"angular-in-depth": {
"desc": "The place where advanced Angular concepts are explained",
"url": "https://blog.angularindepth.com",
"title": "Angular In Depth"
"indepth-dev": {
"desc": "Peer-reviewed Angular articles and tutorials.",
"url": "https://indepth.dev/angular/",
"title": "Angular inDepth"
}
}
},
@ -63,6 +63,12 @@
"logo": "",
"title": "NgRuAir",
"url": "https://github.com/ngRuAir/ngruair"
},
"the-deep-dive": {
"desc": "The advanced web development podcast about Angular, RxJS, TypeScript and other technologies. English, audio only.",
"logo": "https://i.imgur.com/mmE5Feq.jpg",
"title": "The Deep Dive",
"url": "https://thedeepdive.simplecast.com"
}
}
}
@ -429,6 +435,12 @@
"desc": "Jigsaw provides a set of web components based on Angular. It is supporting the development of all applications of Big Data Product of ZTE (https://www.zte.com.cn).",
"title": "Awade Jigsaw (Chinese)",
"url": "https://jigsaw-zte.gitee.io"
},
"material-dayjs-adapter": {
"desc": "A DayJS implementation of @angular/material's DateAdapter that results in smaller bundle sizes than its MomentJS counterpart.",
"rev": true,
"title": "material-dayjs-adapter",
"url": "https://www.npmjs.com/package/@tabuckner/material-dayjs-adapter"
}
}
}

File diff suppressed because it is too large Load Diff

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").
@ -236,7 +236,7 @@ For more information about services, see [Introduction to Services and Dependenc
<!-- Accessing data with the HTTP client -->
Servers often return data in the form of a stream.
Streams are useful because they make it easy to transform the returned data and make modifications to the way you request that data.
Streams are useful because they make it easy to transform the returned data and make modifications to the way you request that data.
The Angular HTTP client, `HttpClient`, is a built-in way to fetch data from external APIs and provide them to your app as a stream.
This section shows you how to use the HTTP client to retrieve shipping prices from an external file.

View File

@ -99,10 +99,11 @@ describe('site App', function() {
describe('scrolling to the top', () => {
it('should scroll to the top when navigating to another page', () => {
page.navigateTo('guide/security');
page.scrollTo('bottom');
expect(page.getScrollTop()).toBeGreaterThan(0);
// Navigate to Reference section, then check
// Find the navigation item that has the text "api"
page.click(page.getNavItem(/reference/i));
page.click(page.getNavItem(/api/i));
expect(page.locationPath()).toBe('/api');
expect(page.getScrollTop()).toBe(0);

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

@ -23,27 +23,36 @@ const globalOptions = {
const runner = createBenchpressRunner();
export async function runBenchmark(config: {
export async function runBenchmark({
id,
url = '',
params = [],
ignoreBrowserSynchronization = true,
microMetrics,
work,
prepare,
setup,
}: {
id: string,
url: string,
params: {name: string, value: any}[],
ignoreBrowserSynchronization?: boolean,
microMetrics?: {[key: string]: string},
work?: () => void,
prepare?: () => void,
setup?: () => void
work?: (() => void)|(() => Promise<unknown>),
prepare?: (() => void)|(() => Promise<unknown>),
setup?: (() => void)|(() => Promise<unknown>),
}): Promise<any> {
openBrowser(config);
if (config.setup) {
await config.setup();
openBrowser({url, params, ignoreBrowserSynchronization});
if (setup) {
await setup();
}
const description: {[key: string]: any} = {};
config.params.forEach((param) => description[param.name] = param.value);
params.forEach((param) => description[param.name] = param.value);
return runner.sample({
id: config.id,
execute: config.work,
prepare: config.prepare,
microMetrics: config.microMetrics,
id,
execute: work,
prepare,
microMetrics,
providers: [{provide: Options.SAMPLE_DESCRIPTION, useValue: {}}]
});
}

View File

@ -0,0 +1,21 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "caretaker",
srcs = glob([
"**/*.ts",
]),
module_name = "@angular/dev-infra-private/caretaker",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",
"@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,30 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {GitClient} from '../../utils/git';
import {getCaretakerConfig} from '../config';
import {printCiStatus} from './ci';
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);
// TODO(josephperrott): Allow these checks to be loaded in parallel.
await printServiceStatuses();
await printGithubTasks(git, config.caretaker);
await printG3Comparison(git);
await printCiStatus(git);
}

View File

@ -0,0 +1,59 @@
/**
* @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';
import {GitClient} from '../../utils/git';
/** The results of checking the status of CI. */
interface StatusCheckResult {
status: 'success'|'failed'|'canceled'|'infrastructure_fail'|'timedout'|'failed'|'no_tests';
timestamp: Date;
buildUrl: string;
}
/** Retrieve and log status of CI for the project. */
export async function printCiStatus(git: GitClient) {
info.group(bold(`CI`));
// TODO(josephperrott): Expand list of branches checked to all active branches.
await printStatus(git, 'master');
info.groupEnd();
info();
}
/** Log the status of CI for a given branch to the console. */
async function printStatus(git: GitClient, branch: string) {
const result = await getStatusOfBranch(git, branch);
const branchName = branch.padEnd(10);
if (result === null) {
info(`${branchName} was not found on CircleCI`);
} else if (result.status === 'success') {
info(`${branchName}`);
} else {
info(`${branchName} ❌ (Ran at: ${result.timestamp.toLocaleString()})`);
}
}
/** Get the CI status of a given branch from CircleCI. */
async function getStatusOfBranch(git: GitClient, branch: string): Promise<StatusCheckResult|null> {
const {owner, name} = git.remoteConfig;
const url = `https://circleci.com/api/v1.1/project/gh/${owner}/${name}/tree/${
branch}?limit=1&filter=completed&shallow=true`;
const result = (await fetch(url).then(result => result.json()))?.[0];
if (result) {
return {
status: result.outcome,
timestamp: new Date(result.stop_time),
buildUrl: result.build_url
};
}
return null;
}

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

@ -3,18 +3,10 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "commit-message",
srcs = [
"builder.ts",
"cli.ts",
"commit-message-draft.ts",
"config.ts",
"parse.ts",
"restore-commit-message.ts",
"validate.ts",
"validate-file.ts",
"validate-range.ts",
"wizard.ts",
],
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/dev-infra-private/commit-message",
visibility = ["//dev-infra:__subpackages__"],
deps = [
@ -32,11 +24,7 @@ ts_library(
ts_library(
name = "test_lib",
testonly = True,
srcs = [
"builder.spec.ts",
"parse.spec.ts",
"validate.spec.ts",
],
srcs = glob(["**/*.spec.ts"]),
deps = [
":commit-message",
"//dev-infra/utils",

View File

@ -7,104 +7,19 @@
*/
import * as yargs from 'yargs';
import {info} from '../utils/console';
import {restoreCommitMessage} from './restore-commit-message';
import {validateFile} from './validate-file';
import {validateCommitRange} from './validate-range';
import {runWizard} from './wizard';
import {RestoreCommitMessageModule} from './restore-commit-message/cli';
import {ValidateFileModule} from './validate-file/cli';
import {ValidateRangeModule} from './validate-range/cli';
import {WizardModule} from './wizard/cli';
/** Build the parser for the commit-message commands. */
export function buildCommitMessageParser(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.command(
'restore-commit-message-draft', false,
args => {
return args.option('file-env-variable', {
type: 'string',
array: true,
conflicts: ['file'],
required: true,
description:
'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
coerce: arg => {
const [file, source] = (process.env[arg] || '').split(' ');
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return [file, source];
},
});
},
args => {
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
})
.command(
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
return args
.positional(
'filePath',
{description: 'The file path to write the generated commit message into'})
.positional('source', {
choices: ['message', 'template', 'merge', 'squash', 'commit'],
description: 'The source of the commit message as described here: ' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
})
.positional(
'commitSha', {description: 'The commit sha if source is set to `commit`'});
}),
async (args: any) => {
await runWizard(args);
})
.command(
'pre-commit-validate', 'Validate the most recent commit message', {
'file': {
type: 'string',
conflicts: ['file-env-variable'],
description: 'The path of the commit message file.',
},
'file-env-variable': {
type: 'string',
conflicts: ['file'],
description:
'The key of the environment variable for the path of the commit message file.',
coerce: arg => {
const file = process.env[arg];
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return file;
},
}
},
args => {
const file = args.file || args['file-env-variable'] || '.git/COMMIT_EDITMSG';
validateFile(file);
})
.command(
'validate-range', 'Validate a range of commit messages', {
'range': {
description: 'The range of commits to check, e.g. --range abc123..xyz456',
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
type: 'string',
requiresArg: true,
},
},
argv => {
// If on CI, and not pull request number is provided, assume the branch
// being run on is an upstream branch.
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
info(`need to validate commit messages on CI runs on upstream branches.`);
info();
info(`Skipping check of provided commit range`);
return;
}
validateCommitRange(argv.range);
});
.command(RestoreCommitMessageModule)
.command(WizardModule)
.command(ValidateFileModule)
.command(ValidateRangeModule);
}
if (require.main == module) {

View File

@ -0,0 +1,13 @@
/**
* @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
*/
/**
* The source triggering the git commit message creation.
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export type CommitMsgSource = 'message'|'template'|'merge'|'squash'|'commit';

View File

@ -8,6 +8,7 @@
import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
/** Configuration for commit-message comands. */
export interface CommitMessageConfig {
maxLineLength: number;
minBodyLength: number;
@ -49,7 +50,7 @@ export const COMMIT_TYPES: {[key: string]: CommitType} = {
build: {
name: 'build',
description: 'Changes to local repository build system and tooling',
scope: ScopeRequirement.Forbidden,
scope: ScopeRequirement.Optional,
},
ci: {
name: 'ci',

View File

@ -0,0 +1,51 @@
/**
* @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 {CommitMsgSource} from '../commit-message-source';
import {restoreCommitMessage} from './restore-commit-message';
export interface RestoreCommitMessageOptions {
fileEnvVariable: string[];
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs.option('file-env-variable' as 'fileEnvVariable', {
type: 'string',
array: true,
demandOption: true,
description: 'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
'https://git-scm.com/docs/githooks#_prepare_commit_msg',
coerce: arg => {
const [file, source] = (process.env[arg] || '').split(' ');
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return [file, source];
},
});
}
/** Handles the command. */
async function handler({fileEnvVariable}: Arguments<RestoreCommitMessageOptions>) {
restoreCommitMessage(fileEnvVariable[0], fileEnvVariable[1] as CommitMsgSource);
}
/** yargs command module describing the command. */
export const RestoreCommitMessageModule: CommandModule<{}, RestoreCommitMessageOptions> = {
handler,
builder,
command: 'restore-commit-message-draft',
// Description: Restore a commit message draft if one has been saved from a failed commit attempt.
// No describe is defiend to hide the command from the --help.
describe: false,
};

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {info} from 'console';
import {writeFileSync} from 'fs';
import {loadCommitMessageDraft} from './commit-message-draft';
import {debug, log} from '../../utils/console';
import {loadCommitMessageDraft} from '../commit-message-draft';
import {CommitMsgSource} from '../commit-message-source';
/**
* Restore the commit message draft to the git to be used as the default commit message.
@ -17,22 +19,21 @@ import {loadCommitMessageDraft} from './commit-message-draft';
* The source provided may be one of the sources described in
* https://git-scm.com/docs/githooks#_prepare_commit_msg
*/
export function restoreCommitMessage(
filePath: string, source?: 'message'|'template'|'squash'|'commit') {
export function restoreCommitMessage(filePath: string, source?: CommitMsgSource) {
if (!!source) {
info('Skipping commit message restoration attempt');
log('Skipping commit message restoration attempt');
if (source === 'message') {
info('A commit message was already provided via the command with a -m or -F flag');
debug('A commit message was already provided via the command with a -m or -F flag');
}
if (source === 'template') {
info('A commit message was already provided via the -t flag or config.template setting');
debug('A commit message was already provided via the -t flag or config.template setting');
}
if (source === 'squash') {
info('A commit message was already provided as a merge action or via .git/MERGE_MSG');
debug('A commit message was already provided as a merge action or via .git/MERGE_MSG');
}
if (source === 'commit') {
info('A commit message was already provided through a revision specified via --fixup, -c,');
info('-C or --amend flag');
debug('A commit message was already provided through a revision specified via --fixup, -c,');
debug('-C or --amend flag');
}
process.exit(0);
}

View File

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

View File

@ -0,0 +1,62 @@
/**
* @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 {getUserConfig} from '../../utils/config';
import {validateFile} from './validate-file';
export interface ValidateFileOptions {
file?: string;
fileEnvVariable?: string;
error: boolean;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.option('file', {
type: 'string',
conflicts: ['file-env-variable'],
description: 'The path of the commit message file.',
})
.option('file-env-variable' as 'fileEnvVariable', {
type: 'string',
conflicts: ['file'],
description: 'The key of the environment variable for the path of the commit message file.',
coerce: (arg: string) => {
const file = process.env[arg];
if (!file) {
throw new Error(`Provided environment variable "${arg}" was not found.`);
}
return file;
},
})
.option('error', {
type: 'boolean',
description:
'Whether invalid commit messages should be treated as failures rather than a warning',
default: !!getUserConfig().commitMessage?.errorOnInvalidMessage || !!process.env['CI']
});
}
/** Handles the command. */
async function handler({error, file, fileEnvVariable}: Arguments<ValidateFileOptions>) {
const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG';
validateFile(filePath, error);
}
/** yargs command module describing the command. */
export const ValidateFileModule: CommandModule<{}, ValidateFileOptions> = {
handler,
builder,
command: 'pre-commit-validate',
describe: 'Validate the most recent commit message',
};

View File

@ -0,0 +1,47 @@
/**
* @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 {readFileSync} from 'fs';
import {resolve} from 'path';
import {getRepoBaseDir} from '../../utils/config';
import {error, green, info, log, red, yellow} from '../../utils/console';
import {deleteCommitMessageDraft, saveCommitMessageDraft} from '../commit-message-draft';
import {printValidationErrors, validateCommitMessage} from '../validate';
/** Validate commit message at the provided file path. */
export function validateFile(filePath: string, isErrorMode: boolean) {
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
const {valid, errors} = validateCommitMessage(commitMessage);
if (valid) {
info(`${green('√')} Valid commit message`);
deleteCommitMessageDraft(filePath);
process.exitCode = 0;
return;
}
/** Function used to print to the console log. */
let printFn = isErrorMode ? error : log;
printFn(`${isErrorMode ? red('✘') : yellow('!')} Invalid commit message`);
printValidationErrors(errors, printFn);
if (isErrorMode) {
printFn(red('Aborting commit attempt due to invalid commit message.'));
printFn(
red('Commit message aborted as failure rather than warning due to local configuration.'));
} else {
printFn(yellow('Before this commit can be merged into the upstream repository, it must be'));
printFn(yellow('amended to follow commit message guidelines.'));
}
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft(filePath, commitMessage);
// Set the correct exit code based on if invalid commit message is an error.
process.exitCode = isErrorMode ? 1 : 0;
}

View File

@ -0,0 +1,50 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Arguments, Argv, CommandModule} from 'yargs';
import {info} from '../../utils/console';
import {validateCommitRange} from './validate-range';
export interface ValidateRangeOptions {
range: string;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs.option('range', {
description: 'The range of commits to check, e.g. --range abc123..xyz456',
demandOption: ' A range must be provided, e.g. --range abc123..xyz456',
type: 'string',
requiresArg: true,
});
}
/** Handles the command. */
async function handler({range}: Arguments<ValidateRangeOptions>) {
// If on CI, and no pull request number is provided, assume the branch
// being run on is an upstream branch.
if (process.env['CI'] && process.env['CI_PULL_REQUEST'] === 'false') {
info(`Since valid commit messages are enforced by PR linting on CI, we do not`);
info(`need to validate commit messages on CI runs on upstream branches.`);
info();
info(`Skipping check of provided commit range`);
return;
}
validateCommitRange(range);
}
/** yargs command module describing the command. */
export const ValidateRangeModule: CommandModule<{}, ValidateRangeOptions> = {
handler,
builder,
command: 'validate-range',
describe: 'Validate a range of commit messages',
};

View File

@ -5,11 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {info} from '../utils/console';
import {exec} from '../utils/shelljs';
import {error, info} from '../../utils/console';
import {exec} from '../../utils/shelljs';
import {parseCommitMessage} from './parse';
import {validateCommitMessage, ValidateCommitMessageOptions} from './validate';
import {parseCommitMessage} from '../parse';
import {printValidationErrors, validateCommitMessage, ValidateCommitMessageOptions} from '../validate';
// Whether the provided commit is a fixup commit.
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
@ -19,11 +19,20 @@ const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
/** Validate all commits in a provided git commit range. */
export function validateCommitRange(range: string) {
// A random value is used as a string to allow for a definite split point in the git log result.
/**
* A random value is used as a string to allow for a definite split point in the git log result.
*/
const randomValueSeparator = `${Math.random()}`;
// Custom git log format that provides the commit header and body, separated as expected with
// the custom separator as the trailing value.
/**
* Custom git log format that provides the commit header and body, separated as expected with the
* custom separator as the trailing value.
*/
const gitLogFormat = `%s%n%n%b${randomValueSeparator}`;
/**
* A list of tuples containing a commit header string and the list of error messages for the
* commit.
*/
const errors: [commitHeader: string, errors: string[]][] = [];
// Retrieve the commits in the provided range.
const result = exec(`git log --reverse --format=${gitLogFormat} ${range}`);
@ -45,12 +54,22 @@ export function validateCommitRange(range: string) {
undefined :
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
};
return validateCommitMessage(m, options);
const {valid, errors: localErrors, commit} = validateCommitMessage(m, options);
if (localErrors.length) {
errors.push([commit.header, localErrors]);
}
return valid;
});
if (allCommitsInRangeValid) {
info('√ All commit messages in range valid.');
} else {
error('✘ Invalid commit message');
errors.forEach(([header, validationErrors]) => {
error.group(header);
printValidationErrors(validationErrors);
error.groupEnd();
});
// Exit with a non-zero exit code if invalid commit messages have
// been discovered.
process.exit(1);

View File

@ -8,7 +8,7 @@
// Imports
import * as validateConfig from './config';
import {validateCommitMessage} from './validate';
import {validateCommitMessage, ValidateCommitMessageResult} from './validate';
type CommitMessageConfig = validateConfig.CommitMessageConfig;
@ -31,44 +31,35 @@ const SCOPES = config.commitMessage.scopes.join(', ');
const INVALID = false;
const VALID = true;
function expectValidationResult(
validationResult: ValidateCommitMessageResult, valid: boolean, errors: string[] = []) {
expect(validationResult).toEqual(jasmine.objectContaining({valid, errors}));
}
// TODO(josephperrott): Clean up tests to test script rather than for
// specific commit messages we want to use.
describe('validate-commit-message.js', () => {
let lastError: string = '';
beforeEach(() => {
lastError = '';
spyOn(console, 'error').and.callFake((msg: string) => lastError = msg);
spyOn(validateConfig, 'getCommitMessageConfig')
.and.returnValue(config as ReturnType<typeof validateConfig.getCommitMessageConfig>);
});
describe('validateMessage()', () => {
it('should be valid', () => {
expect(validateCommitMessage('feat(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('fixup! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('squash! fix(packaging): something')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "fix(packaging): something"')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('feat(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('squash! fix(packaging): something'), VALID);
expectValidationResult(validateCommitMessage('Revert: "fix(packaging): something"'), VALID);
});
it('should validate max length', () => {
const msg =
'fix(compiler): something super mega extra giga tera long, maybe even longer and longer and longer and longer and longer and longer...';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`The commit message header is longer than ${
config.commitMessage.maxLineLength} characters`);
expectValidationResult(validateCommitMessage(msg), INVALID, [
`The commit message header is longer than ${config.commitMessage.maxLineLength} characters`
]);
});
it('should skip max length limit for URLs', () => {
@ -77,49 +68,56 @@ describe('validate-commit-message.js', () => {
'limit. For more details see the following super long URL:\n\n' +
'https://github.com/angular/components/commit/e2ace018ddfad10608e0e32932c43dcfef4095d7#diff-9879d6db96fd29134fc802214163b95a';
expect(validateCommitMessage(msg)).toBe(VALID);
expectValidationResult(validateCommitMessage(msg), VALID);
});
it('should validate "<type>(<scope>): <subject>" format', () => {
const msg = 'not correct format';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`The commit message header does not match the expected format.`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`The commit message header does not match the expected format.`]);
});
it('should fail when type is invalid', () => {
const msg = 'weird(core): something';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'weird' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'weird' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
it('should fail when scope is invalid', () => {
const errorMessageFor = (scope: string, header: string) =>
`'${scope}' is not an allowed scope.\n => SCOPES: ${SCOPES}`;
expect(validateCommitMessage('fix(Compiler): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('Compiler', 'fix(Compiler): something'));
expectValidationResult(
validateCommitMessage('fix(Compiler): something'), INVALID,
[errorMessageFor('Compiler', 'fix(Compiler): something')]);
expect(validateCommitMessage('feat(bah): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('bah', 'feat(bah): something'));
expectValidationResult(
validateCommitMessage('feat(bah): something'), INVALID,
[errorMessageFor('bah', 'feat(bah): something')]);
expect(validateCommitMessage('fix(webworker): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('webworker', 'fix(webworker): something'));
expectValidationResult(
validateCommitMessage('fix(webworker): something'), INVALID,
[errorMessageFor('webworker', 'fix(webworker): something')]);
expect(validateCommitMessage('refactor(security): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('security', 'refactor(security): something'));
expectValidationResult(
validateCommitMessage('refactor(security): something'), INVALID,
[errorMessageFor('security', 'refactor(security): something')]);
expect(validateCommitMessage('refactor(docs): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('docs', 'refactor(docs): something'));
expectValidationResult(
validateCommitMessage('refactor(docs): something'), INVALID,
[errorMessageFor('docs', 'refactor(docs): something')]);
expect(validateCommitMessage('feat(angular): something')).toBe(INVALID);
expect(lastError).toContain(errorMessageFor('angular', 'feat(angular): something'));
expectValidationResult(
validateCommitMessage('feat(angular): something'), INVALID,
[errorMessageFor('angular', 'feat(angular): something')]);
});
it('should allow empty scope', () => {
expect(validateCommitMessage('build: blablabla')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('build: blablabla'), VALID);
});
// We do not want to allow WIP. It is OK to fail the PR build in this case to show that there is
@ -127,30 +125,25 @@ describe('validate-commit-message.js', () => {
it('should not allow "WIP: ..." syntax', () => {
const msg = 'WIP: fix: something';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'WIP' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
describe('(revert)', () => {
it('should allow valid "revert: ..." syntaxes', () => {
expect(validateCommitMessage('revert: anything')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('Revert: "anything"')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('revert anything')).toBe(VALID);
expect(lastError).toBe('');
expect(validateCommitMessage('rEvErT anything')).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage('revert: anything'), VALID);
expectValidationResult(validateCommitMessage('Revert: "anything"'), VALID);
expectValidationResult(validateCommitMessage('revert anything'), VALID);
expectValidationResult(validateCommitMessage('rEvErT anything'), VALID);
});
it('should not allow "revert(scope): ..." syntax', () => {
const msg = 'revert(compiler): reduce generated code payload size by 65%';
expect(validateCommitMessage(msg)).toBe(INVALID);
expect(lastError).toContain(`'revert' is not an allowed type.\n => TYPES: ${TYPES}`);
expectValidationResult(
validateCommitMessage(msg), INVALID,
[`'revert' is not an allowed type.\n => TYPES: ${TYPES}`]);
});
// https://github.com/angular/angular/issues/23479
@ -158,28 +151,26 @@ describe('validate-commit-message.js', () => {
const msg =
'Revert "fix(compiler): Pretty print object instead of [Object object] (#22689)" (#23442)';
expect(validateCommitMessage(msg)).toBe(VALID);
expect(lastError).toBe('');
expectValidationResult(validateCommitMessage(msg), VALID);
});
});
describe('(squash)', () => {
describe('without `disallowSquash`', () => {
it('should return commits as valid', () => {
expect(validateCommitMessage('squash! feat(core): add feature')).toBe(VALID);
expect(validateCommitMessage('squash! fix: a bug')).toBe(VALID);
expect(validateCommitMessage('squash! fix a typo')).toBe(VALID);
expectValidationResult(validateCommitMessage('squash! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('squash! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('squash! fix a typo'), VALID);
});
});
describe('with `disallowSquash`', () => {
it('should fail', () => {
expect(validateCommitMessage('fix(core): something', {disallowSquash: true})).toBe(VALID);
expect(validateCommitMessage('squash! fix(core): something', {
disallowSquash: true
})).toBe(INVALID);
expect(lastError).toContain(
'The commit must be manually squashed into the target commit');
expectValidationResult(
validateCommitMessage('fix(core): something', {disallowSquash: true}), VALID);
expectValidationResult(
validateCommitMessage('squash! fix(core): something', {disallowSquash: true}),
INVALID, ['The commit must be manually squashed into the target commit']);
});
});
});
@ -187,9 +178,9 @@ describe('validate-commit-message.js', () => {
describe('(fixup)', () => {
describe('without `nonFixupCommitHeaders`', () => {
it('should return commits as valid', () => {
expect(validateCommitMessage('fixup! feat(core): add feature')).toBe(VALID);
expect(validateCommitMessage('fixup! fix: a bug')).toBe(VALID);
expect(validateCommitMessage('fixup! fixup! fix: a bug')).toBe(VALID);
expectValidationResult(validateCommitMessage('fixup! feat(core): add feature'), VALID);
expectValidationResult(validateCommitMessage('fixup! fix: a bug'), VALID);
expectValidationResult(validateCommitMessage('fixup! fixup! fix: a bug'), VALID);
});
});
@ -197,36 +188,39 @@ describe('validate-commit-message.js', () => {
it('should check that the fixup commit matches a non-fixup one', () => {
const msg = 'fixup! foo';
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}))
.toBe(VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}))
.toBe(VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}))
.toBe(VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['foo', 'bar', 'baz']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['bar', 'baz', 'foo']}),
VALID);
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['baz', 'foo', 'bar']}),
VALID);
expect(validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}))
.toBe(INVALID);
expect(lastError).toContain(
'Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux');
expectValidationResult(
validateCommitMessage(
msg, {disallowSquash: false, nonFixupCommitHeaders: ['qux', 'quux', 'quuux']}),
INVALID,
['Unable to find match for fixup commit among prior commits: \n' +
' qux\n' +
' quux\n' +
' quuux']);
});
it('should fail if `nonFixupCommitHeaders` is empty', () => {
expect(validateCommitMessage('refactor(core): make reactive', {
disallowSquash: false,
nonFixupCommitHeaders: []
})).toBe(VALID);
expect(validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}))
.toBe(INVALID);
expect(lastError).toContain(
`Unable to find match for fixup commit among prior commits: -`);
expectValidationResult(
validateCommitMessage(
'refactor(core): make reactive',
{disallowSquash: false, nonFixupCommitHeaders: []}),
VALID);
expectValidationResult(
validateCommitMessage(
'fixup! foo', {disallowSquash: false, nonFixupCommitHeaders: []}),
INVALID, [`Unable to find match for fixup commit among prior commits: -`]);
});
});
});
@ -246,24 +240,27 @@ describe('validate-commit-message.js', () => {
});
it('should fail validation if the body is shorter than `minBodyLength`', () => {
expect(validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'))
.toBe(VALID);
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expectValidationResult(
validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'),
VALID);
expectValidationResult(
validateCommitMessage('fix(core): something\n\n too short'), INVALID,
['The commit message body does not meet the minimum length of 30 characters']);
expectValidationResult(validateCommitMessage('fix(core): something'), INVALID, [
'The commit message body does not meet the minimum length of 30 characters'
]);
});
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
() => {
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
expect(validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
.toBe(VALID);
expectValidationResult(validateCommitMessage('docs: just fixing a typo'), VALID);
expectValidationResult(validateCommitMessage('docs(core): just fixing a typo'), VALID);
expectValidationResult(
validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'),
VALID);
});
});
});

View File

@ -8,7 +8,7 @@
import {error} from '../utils/console';
import {COMMIT_TYPES, getCommitMessageConfig, ScopeRequirement} from './config';
import {parseCommitMessage} from './parse';
import {parseCommitMessage, ParsedCommitMessage} from './parse';
/** Options for commit message validation. */
export interface ValidateCommitMessageOptions {
@ -16,133 +16,147 @@ export interface ValidateCommitMessageOptions {
nonFixupCommitHeaders?: string[];
}
/** The result of a commit message validation check. */
export interface ValidateCommitMessageResult {
valid: boolean;
errors: string[];
commit: ParsedCommitMessage;
}
/** Regex matching a URL for an entire commit body line. */
const COMMIT_BODY_URL_LINE_RE = /^https?:\/\/.*$/;
/** Validate a commit message against using the local repo's config. */
export function validateCommitMessage(
commitMsg: string, options: ValidateCommitMessageOptions = {}) {
function printError(errorMessage: string) {
error(
`INVALID COMMIT MSG: \n` +
`${'─'.repeat(40)}\n` +
`${commitMsg}\n` +
`${'─'.repeat(40)}\n` +
`ERROR: \n` +
` ${errorMessage}` +
`\n\n` +
`The expected format for a commit is: \n` +
`<type>(<scope>): <subject>\n\n<body>`);
}
commitMsg: string, options: ValidateCommitMessageOptions = {}): ValidateCommitMessageResult {
const config = getCommitMessageConfig().commitMessage;
const commit = parseCommitMessage(commitMsg);
const errors: string[] = [];
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
/** Perform the validation checks against the parsed commit. */
function validateCommitAndCollectErrors() {
// TODO(josephperrott): Remove early return calls when commit message errors are found
// All revert commits are considered valid.
if (commit.isRevert) {
return true;
}
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
// All squashes are considered valid, as the commit will be squashed into another in
// the git history anyway, unless the options provided to not allow squash commits.
if (commit.isSquash) {
if (options.disallowSquash) {
printError('The commit must be manually squashed into the target commit');
// All revert commits are considered valid.
if (commit.isRevert) {
return true;
}
// All squashes are considered valid, as the commit will be squashed into another in
// the git history anyway, unless the options provided to not allow squash commits.
if (commit.isSquash) {
if (options.disallowSquash) {
errors.push('The commit must be manually squashed into the target commit');
return false;
}
return true;
}
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
// check.
if (commit.isFixup) {
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
errors.push(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
return false;
}
return true;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
errors.push(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
return true;
}
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
// check.
if (commit.isFixup) {
if (options.nonFixupCommitHeaders && !options.nonFixupCommitHeaders.includes(commit.header)) {
printError(
'Unable to find match for fixup commit among prior commits: ' +
(options.nonFixupCommitHeaders.map(x => `\n ${x}`).join('') || '-'));
if (!commit.type) {
errors.push(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
errors.push(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
errors.push(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
errors.push(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
errors.push(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
errors.push(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
const bodyByLine = commit.body.split('\n');
const lineExceedsMaxLength = bodyByLine.some(line => {
// Check if any line exceeds the max line length limit. The limit is ignored for
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
});
if (lineExceedsMaxLength) {
errors.push(
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}
return true;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if (commit.header.length > config.maxLineLength) {
printError(`The commit message header is longer than ${config.maxLineLength} characters`);
return false;
}
if (!commit.type) {
printError(`The commit message header does not match the expected format.`);
return false;
}
if (COMMIT_TYPES[commit.type] === undefined) {
printError(`'${commit.type}' is not an allowed type.\n => TYPES: ${
Object.keys(COMMIT_TYPES).join(', ')}`);
return false;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT_TYPES[commit.type].scope;
if (scopeRequirementForType === ScopeRequirement.Forbidden && commit.scope) {
printError(`Scopes are forbidden for commits with type '${commit.type}', but a scope of '${
commit.scope}' was provided.`);
return false;
}
if (scopeRequirementForType === ScopeRequirement.Required && !commit.scope) {
printError(
`Scopes are required for commits with type '${commit.type}', but no scope was provided.`);
return false;
}
if (commit.scope && !config.scopes.includes(commit.scope)) {
printError(
`'${commit.scope}' is not an allowed scope.\n => SCOPES: ${config.scopes.join(', ')}`);
return false;
}
// Commits with the type of `release` do not require a commit body.
if (commit.type === 'release') {
return true;
}
//////////////////////////
// Checking commit body //
//////////////////////////
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
printError(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
}
const bodyByLine = commit.body.split('\n');
const lineExceedsMaxLength = bodyByLine.some(line => {
// Check if any line exceeds the max line length limit. The limit is ignored for
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
return line.length > config.maxLineLength && !COMMIT_BODY_URL_LINE_RE.test(line);
});
if (lineExceedsMaxLength) {
printError(
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}
return true;
return {valid: validateCommitAndCollectErrors(), errors, commit};
}
/** Print the error messages from the commit message validation to the console. */
export function printValidationErrors(errors: string[], print = error) {
print.group(`Error${errors.length === 1 ? '' : 's'}:`);
errors.forEach(line => print(line));
print.groupEnd();
print();
print('The expected format for a commit is: ');
print('<type>(<scope>): <summary>');
print();
print('<body>');
print();
}

View File

@ -0,0 +1,54 @@
/**
* @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 {CommitMsgSource} from '../commit-message-source';
import {runWizard} from './wizard';
export interface WizardOptions {
filePath: string;
commitSha: string|undefined;
source: CommitMsgSource|undefined;
}
/** Builds the command. */
function builder(yargs: Argv) {
return yargs
.positional('filePath', {
description: 'The file path to write the generated commit message into',
type: 'string',
demandOption: true,
})
.positional('source', {
choices: ['message', 'template', 'merge', 'squash', 'commit'] as const,
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`',
type: 'string',
});
}
/** Handles the command. */
async function handler(args: Arguments<WizardOptions>) {
await runWizard(args);
}
/** yargs command module describing the command. */
export const WizardModule: CommandModule<{}, WizardOptions> = {
handler,
builder,
command: 'wizard <filePath> [source] [commitSha]',
// Description: Run the wizard to build a base commit message before opening to complete.
// No describe is defiend to hide the command from the --help.
describe: false,
};

View File

@ -7,15 +7,12 @@
*/
import {writeFileSync} from 'fs';
import {info} from '../utils/console';
import {getUserConfig} from '../../utils/config';
import {debug, info} from '../../utils/console';
import {buildCommitMessage} from './builder';
import {buildCommitMessage} from '../builder';
import {CommitMsgSource} from '../commit-message-source';
/**
* 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>
@ -24,11 +21,16 @@ const defaultCommitMessage = `<type>(<scope>): <summary>
# 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
args: {filePath: string, source?: CommitMsgSource, commitSha?: string}) {
if (getUserConfig().commitMessage?.disableWizard) {
debug('Skipping commit message wizard due to enabled `commitMessage.disableWizard` option in');
debug('user config.');
process.exitCode = 0;
return;
}
if (args.source !== undefined) {
info(`Skipping commit message wizard due because the commit was created via '${
info(`Skipping commit message wizard because the commit was created via '${
args.source}' source`);
process.exitCode = 0;
return;

View File

@ -8,35 +8,21 @@
import {Arguments, Argv, CommandModule} from 'yargs';
import {error} from '../../utils/console';
import {addGithubTokenFlag} from '../../utils/yargs';
import {checkOutPullRequestLocally} from '../common/checkout-pr';
export interface CheckoutOptions {
prNumber: number;
'github-token'?: string;
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 checkout pull request command. */
function builder(yargs: Argv) {
return yargs.positional('prNumber', {type: 'number', demandOption: true}).option('github-token', {
type: 'string',
description: 'Github token. If not set, token is retrieved from the environment variables.'
});
return addGithubTokenFlag(yargs).positional('prNumber', {type: 'number', demandOption: true});
}
/** Handles the checkout pull request command. */
async function handler({prNumber, 'github-token': token}: Arguments<CheckoutOptions>) {
const githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
if (!githubToken) {
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
error('Alternatively, pass the `--github-token` command line flag.');
error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
process.exitCode = 1;
return;
}
async function handler({prNumber, githubToken}: Arguments<CheckoutOptions>) {
const prCheckoutOptions = {allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}`};
await checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions);
}

View File

@ -3,6 +3,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "common",
srcs = glob(["*.ts"]),
module_name = "@angular/dev-infra-private/pr/common",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/utils",

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

@ -12,11 +12,13 @@
"@angular/benchpress": "0.2.1",
"@octokit/graphql": "<from-root>",
"@octokit/types": "<from-root>",
"@octokit/rest": "<from-root>",
"brotli": "<from-root>",
"chalk": "<from-root>",
"cli-progress": "<from-root>",
"glob": "<from-root>",
"inquirer": "<from-root>",
"inquirer-autocomplete-prompt": "<from-root>",
"minimatch": "<from-root>",
"multimatch": "<from-root>",
"node-fetch": "<from-root>",
@ -26,9 +28,7 @@
"tslib": "<from-root>",
"typed-graphqlify": "<from-root>",
"yaml": "<from-root>",
"yargs": "<from-root>"
},
"peerDependencies": {
"yargs": "<from-root>",
"@bazel/buildifier": "<from-root>",
"clang-format": "<from-root>",
"protractor": "<from-root>",

View File

@ -15,11 +15,13 @@ ts_library(
"@npm//@types/inquirer",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/yargs",
"@npm//chalk",
"@npm//inquirer",
"@npm//inquirer-autocomplete-prompt",
"@npm//shelljs",
"@npm//tslib",
"@npm//typed-graphqlify",
"@npm//yargs",
],
)

View File

@ -9,7 +9,7 @@
import {existsSync} from 'fs';
import {dirname, join} from 'path';
import {error} from './console';
import {debug, error} from './console';
import {exec} from './shelljs';
import {isTsNodeAvailable} from './ts-node';
@ -49,7 +49,16 @@ export type NgDevConfig<T = {}> = CommonConfig&T;
const CONFIG_FILE_PATH = '.ng-dev/config';
/** The configuration for ng-dev. */
let CONFIG: {}|null = null;
let cachedConfig: NgDevConfig|null = null;
/**
* The filename expected for local user config, without the file extension to allow a typescript,
* javascript or json file to be used.
*/
const USER_CONFIG_FILE_PATH = '.ng-dev.user';
/** The local user configuration for ng-dev. */
let userConfig: {[key: string]: any}|null = null;
/**
* Get the configuration from the file system, returning the already loaded
@ -57,15 +66,15 @@ let CONFIG: {}|null = null;
*/
export function getConfig(): NgDevConfig {
// If the global config is not defined, load it from the file system.
if (CONFIG === null) {
if (cachedConfig === null) {
// The full path to the configuration file.
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
// Set the global config object.
CONFIG = readConfigFile(configPath);
// Read the configuration and validate it before caching it for the future.
cachedConfig = validateCommonConfig(readConfigFile(configPath));
}
// Return a clone of the global config to ensure that a new instance of the config is returned
// each time, preventing unexpected effects of modifications to the config object.
return validateCommonConfig({...CONFIG});
// Return a clone of the cached global config to ensure that a new instance of the config
// is returned each time, preventing unexpected effects of modifications to the config object.
return {...cachedConfig};
}
/** Validate the common configuration has been met for the ng-dev command. */
@ -86,8 +95,11 @@ function validateCommonConfig(config: Partial<NgDevConfig>) {
return config as NgDevConfig;
}
/** Resolves and reads the specified configuration file. */
function readConfigFile(configPath: string): object {
/**
* Resolves and reads the specified configuration file, optionally returning an empty object if the
* configuration file cannot be read.
*/
function readConfigFile(configPath: string, returnEmptyObjectOnError = false): object {
// If the the `.ts` extension has not been set up already, and a TypeScript based
// version of the given configuration seems to exist, set up `ts-node` if available.
if (require.extensions['.ts'] === undefined && existsSync(`${configPath}.ts`) &&
@ -103,7 +115,12 @@ function readConfigFile(configPath: string): object {
try {
return require(configPath);
} catch (e) {
error('Could not read configuration file.');
if (returnEmptyObjectOnError) {
debug(`Could not read configuration file at ${configPath}, returning empty object instead.`);
debug(e);
return {};
}
error(`Could not read configuration file at ${configPath}.`);
error(e);
process.exit(1);
}
@ -135,3 +152,23 @@ export function getRepoBaseDir() {
}
return baseRepoDir.trim();
}
/**
* Get the local user configuration from the file system, returning the already loaded copy if it is
* defined.
*
* @returns The user configuration object, or an empty object if no user configuration file is
* present. The object is an untyped object as there are no required user configurations.
*/
export function getUserConfig() {
// If the global config is not defined, load it from the file system.
if (userConfig === null) {
// The full path to the configuration file.
const configPath = join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
// Set the global config object.
userConfig = readConfigFile(configPath, true);
}
// Return a clone of the user config to ensure that a new instance of the config is returned
// each time, preventing unexpected effects of modifications to the config object.
return {...userConfig};
}

View File

@ -7,14 +7,19 @@
*/
import chalk from 'chalk';
import {writeFileSync} from 'fs';
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> {
@ -140,6 +145,7 @@ function runConsoleCommand(loadCommand: () => Function, logLevel: LOG_LEVELS, ..
if (getLogLevel() >= logLevel) {
loadCommand()(...text);
}
printToLogFile(logLevel, ...text);
}
/**
@ -155,3 +161,59 @@ 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');
// Strip ANSI escape codes from log outputs.
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
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

@ -34,7 +34,7 @@ export class GitCommandError extends Error {
/**
* Common client for performing Git interactions.
*
* Takes in two optional arguements:
* Takes in two optional arguments:
* _githubToken: the token used for authentifation in github interactions, by default empty
* allowing readonly actions.
* _config: The dev-infra configuration containing GitClientConfig information, by default
@ -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(' ')));

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

@ -4,71 +4,23 @@ Caretaker is responsible for merging PRs into the individual branches and intern
## Responsibilities
- Draining the queue of PRs ready to be merged. (PRs with [`PR action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22PR+action%3A+merge%22) label)
- Draining the queue of PRs ready to be merged. (PRs with [`action: merge`](https://github.com/angular/angular/pulls?q=is%3Aopen+is%3Apr+label%3A%22action%3A+merge%22) label)
- Assigning [new issues](https://github.com/angular/angular/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) to individual component authors.
## Merging the PR
A PR needs to have `PR action: merge` and `PR target: *` labels to be considered
ready to merge. Merging is performed by running `merge-pr` with a PR number to merge.
A PR needs to have `action: merge` and `target: *` labels to be considered
ready to merge. Merging is performed by running `ng-dev pr merge` with a PR number to merge.
The tooling automatically verifies the given PR is ready for merge. If the PR passes the tests, the
tool will automatically merge it based on the applied target label.
To merge a PR run:
```
$ ./scripts/github/merge-pr 1234
$ yarn ng-dev pr merge <pr number>
```
The `merge-pr` script will:
- Ensure that all appropriate labels are on the PR.
- Fetches the latest PR code from the `angular/angular` repo.
- It will `cherry-pick` all of the SHAs from the PR into the current corresponding branches `master` and or `?.?.x` (patch).
- It will rewrite commit history by automatically adding `Close #1234` and `(#1234)` into the commit message.
NOTE: The `merge-pr` will land the PR on `master` and or `?.?.x` (patch) as described by `PR target: *` label.
### Recovering from failed `merge-pr` due to conflicts
When running `merge-pr` the script will output the commands which it is about to run.
```
$ ./scripts/github/merge-pr 1234
======================
GitHub Merge PR Steps
======================
git cherry-pick angular/pr/1234~1..angular/pr/1234
git filter-branch -f --msg-filter "/home/misko/angular/scripts/github/utils/github.closes 1234" HEAD~1..HEAD
```
If the `cherry-pick` command fails than resolve conflicts and use `git cherry-pick --continue` once ready. After the `cherry-pick` is done cut&paste and run the `filter-branch` command to properly rewrite the messages
## Cherry-picking PRs into patch branch
In addition to merging PRs into the master branch, many PRs need to be also merged into a patch branch.
Follow these steps to get patch branch up to date.
1. Check out the most recent patch branch: `git checkout 4.3.x`
2. Get a list of PRs merged into master: `git log master --oneline -n10`
3. For each PR number in the commit message run: `./scripts/github/merge-pr 1234`
- The PR will only merge if the `PR target:` matches the branch.
Once all of the PRs are in patch branch, push the all branches and tags to github using `push-upstream` script.
## Pushing merged PRs into github
Use `push-upstream` script to push all of the branch and tags to github.
```
$ ./scripts/github/push-upstream
git push git@github.com:angular/angular.git master:master 4.3.x:4.3.x
Counting objects: 25, done.
Delta compression using up to 6 threads.
Compressing objects: 100% (17/17), done.
Writing objects: 100% (25/25), 2.22 KiB | 284.00 KiB/s, done.
Total 25 (delta 22), reused 8 (delta 7)
remote: Resolving deltas: 100% (22/22), completed with 18 local objects.
To github.com:angular/angular.git
079d884b6..d1c4a94bb master -> master
git push --tags -f git@github.com:angular/angular.git patch_sync:patch_sync
Everything up-to-date
```
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.

View File

@ -12,7 +12,7 @@ Change approvals in our monorepo are managed via [PullApprove](https://docs.pull
# Merging
Once a change has all of the required approvals, either the last approver or the PR author (if PR author has the project collaborator status)
should mark the PR with the `PR action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
should mark the PR with the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target).
This signals to the caretaker that the PR should be merged. See [merge instructions](CARETAKER.md).
# Who is the Caretaker?

View File

@ -1,6 +1,6 @@
# Triage Process and GitHub Labels for Angular
This document describes how the Angular team uses labels and milestones to triage issues on github.
This document describes how the Angular team uses labels and milestones to triage issues on GitHub.
The basic idea of the process is that caretaker only assigns a component (`comp: *`) label.
The owner of the component is then responsible for the secondary / component-level triage.
@ -125,32 +125,32 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency:
PRs also have additional label categories that should be used to signal their state.
Every triaged PR must have a `PR action` label assigned to it:
Every triaged PR must have a `action: *` label assigned to it:
* `PR action: discuss`: Discussion is needed, to be led by the author.
* `action: discuss`: Discussion is needed, to be led by the author.
* _**Who adds it:** Typically the PR author._
* _**Who removes it:** Whoever added it._
* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
* `action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface.
* _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._
* _**Who removes it:** Whoever added it or the reviewer adding the last missing review._
* `PR action: cleanup`: More work is needed from the author.
* `action: cleanup`: More work is needed from the author.
* _**Who adds it:** The reviewer requesting changes to the PR._
* _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._
* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
* `action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready".
* _**Who adds it:** Typically the PR author._
* _**Who removes it:** Whoever added it._
In addition, PRs can have the following states:
* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
* _**Who adds it:** The PR author._
* _**Who removes it:** Whoever added it._
* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
* _**Who adds it:** Any team member._
* _**Who removes it:** Any team member._
When a PR is ready for review, a review should be requested using the Reviewers interface in Github.
When a PR is ready for review, a review should be requested using the Reviewers interface in GitHub.
## PR Target
@ -160,15 +160,29 @@ In our git workflow, we merge changes either to the `master` branch, the active
The decision about the target must be done by the PR author and/or reviewer.
This decision is then honored when the PR is being merged by the caretaker.
To communicate the target we use the following labels:
To communicate the target we use GitHub labels and only one target label may be applied to a PR.
* `PR target: master & patch`: the PR should me merged into the master branch and cherry-picked into the most recent patch branch. All PRs with fixes, docs and refactorings should use this target.
* `PR target: master-only`: the PR should be merged only into the `master` branch. All PRs with new features, API changes or high-risk changes should use this target.
* `PR target: patch-only`: the PR should be merged only into the most recent patch branch (e.g. 5.0.x). This target is useful if a `master & patch` PR can't be cleanly cherry-picked into the stable branch and a new PR is needed.
* `PR target: LTS-only`: the PR should be merged only into the active LTS branch(es). Only security and critical fixes are allowed in these branches. Always send a new PR targeting just the LTS branch and request review approval from @IgorMinar.
* `PR target: TBD`: the target is yet to be determined.
Targeting an active release train:
If a PR is missing the `PR target: *` label, or if the label is set to "TBD" when the PR is sent to the caretaker, the caretaker should reject the PR and request the appropriate target label to be applied before the PR is merged.
* `target: major`: Any breaking change
* `target: minor`: Any new feature
* `target: patch`: Bug fixes, refactorings, documentation changes, etc. that pose no or very low risk of adversely
affecting existing applications.
Special Cases:
* `target: rc`: A critical fix for an active release-train while it is in a feature freeze or RC phase
* `target: lts`: A criticial fix for a specific release-train that is still within the long term support phase
Notes:
- To land a change only in a patch/RC branch, without landing it in any other active release-train branch (such
as `master`), the patch/RC branch can be targeted in the GitHub UI with the appropriate
`target: patch`/`target: rc` label.
- `target: lts` PRs must target the specific LTS branch they would need to merge into in the GitHub UI, in
cases which a change is desired in multiple LTS branches, individual PRs for each LTS branch must be created
If a PR is missing the `target:*` label, it will be marked as pending by the angular robot status checks.
## PR Approvals
@ -182,7 +196,7 @@ In any case, the reviewer should actually look through the code and provide feed
Note that approved state does not mean a PR is ready to be merged.
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
Only the `PR action: merge` label means that the PR is ready for merging.
Only the `action: merge` label means that the PR is ready for merging.
## Special Labels
@ -201,7 +215,7 @@ Only issues with `cla:yes` should be merged into master.
Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md)
### `PR action: merge-assistance`
### `action: merge-assistance`
* _**Who adds it:** Any team member._
* _**Who removes it:** Any team member._
@ -211,7 +225,7 @@ The comment should be formatted like this: `merge-assistance: <explain what kind
For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged.
### `PR action: rerun CI at HEAD`
### `action: rerun CI at HEAD`
* _**Who adds it:** Any team member._
* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._

View File

@ -19,6 +19,7 @@ export declare enum ErrorCode {
CONFIG_FLAT_MODULE_NO_INDEX = 4001,
CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK = 4002,
HOST_BINDING_PARSE_ERROR = 5001,
TEMPLATE_PARSE_ERROR = 5002,
NGMODULE_INVALID_DECLARATION = 6001,
NGMODULE_INVALID_IMPORT = 6002,
NGMODULE_INVALID_EXPORT = 6003,

View File

@ -432,7 +432,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
ngOnChanges(changes: SimpleChanges): any;
ngOnDestroy(): any;
onClick(button: number, ctrlKey: boolean, metaKey: boolean, shiftKey: boolean): boolean;
onClick(button: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean): boolean;
}
export declare class RouterModule {

View File

@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 147573,
"main-es2015": 146989,
"polyfills-es2015": 36571
}
}

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

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.1.0-rc.0",
"version": "10.1.2",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -35,6 +35,8 @@
"tslint": "tsc -p tools/tsconfig.json && tslint -c tslint.json \"+(dev-infra|packages|modules|scripts|tools)/**/*.+(js|ts)\"",
"public-api:check": "node goldens/public-api/manage.js test",
"public-api:update": "node goldens/public-api/manage.js accept",
"symbol-extractor:check": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js test",
"symbol-extractor:update": "node tools/symbol-extractor/run_all_symbols_extractor_tests.js accept",
"ts-circular-deps": "ts-node --transpile-only -- dev-infra/ts-circular-dependencies/index.ts --config ./packages/circular-deps-test.conf.js",
"ts-circular-deps:check": "yarn -s ts-circular-deps check",
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve",
@ -81,13 +83,14 @@
"@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",
@ -152,7 +155,7 @@
"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.",

View File

@ -14,6 +14,7 @@ import {Logger} from '../../../src/ngtsc/logging';
import {ParsedConfiguration} from '../../../src/perform_compile';
import {getEntryPointFormat} from '../packages/entry_point';
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache';
import {PathMappings} from '../path_mappings';
import {FileWriter} from '../writing/file_writer';
@ -30,6 +31,8 @@ export function getCreateCompileFn(
return (beforeWritingFiles, onTaskCompleted) => {
const {Transformer} = require('../packages/transformer');
const transformer = new Transformer(fileSystem, logger, tsConfig);
const sharedFileCache = new SharedFileCache(fileSystem);
const moduleResolutionCache = createModuleResolutionCache(fileSystem);
return (task: Task) => {
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
@ -54,8 +57,8 @@ export function getCreateCompileFn(
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
const bundle = makeEntryPointBundle(
fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true,
enableI18nLegacyMessageIdFormat);
fileSystem, entryPoint, sharedFileCache, moduleResolutionCache, formatPath, isCore,
format, processDts, pathMappings, true, enableI18nLegacyMessageIdFormat);
const result = transformer.transform(bundle);
if (result.success) {

View File

@ -7,8 +7,8 @@
*/
import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util';
@ -1593,35 +1593,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
constructorParamInfo[index] :
{decorators: null, typeExpression: null};
const nameNode = node.name;
let typeValueReference: TypeValueReference;
if (typeExpression !== null) {
// `typeExpression` is an expression in a "type" context. Resolve it to a declared value.
// Either it's a reference to an imported type, or a type declared locally. Distinguish the
// two cases with `getDeclarationOfExpression`.
const decl = this.getDeclarationOfExpression(typeExpression);
if (decl !== null && decl.node !== null && decl.viaModule !== null &&
isNamedDeclaration(decl.node)) {
typeValueReference = {
kind: TypeValueReferenceKind.IMPORTED,
valueDeclaration: decl.node,
moduleName: decl.viaModule,
importedName: decl.node.name.text,
nestedPath: null,
};
} else {
typeValueReference = {
kind: TypeValueReferenceKind.LOCAL,
expression: typeExpression,
defaultImportStatement: null,
};
}
} else {
typeValueReference = {
kind: TypeValueReferenceKind.UNAVAILABLE,
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
};
}
const typeValueReference = this.typeToValue(typeExpression);
return {
name: getNameText(nameNode),
@ -1633,6 +1605,29 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
});
}
/**
* Compute the `TypeValueReference` for the given `typeExpression`.
*
* In ngcc, all the `typeExpression` are guaranteed to be "values" because it is working in JS and
* not TS. This means that the TS compiler is not going to remove the "type" import and so we can
* always use a LOCAL `TypeValueReference` kind, rather than trying to force an additional import
* for non-local expressions.
*/
private typeToValue(typeExpression: ts.Expression|null): TypeValueReference {
if (typeExpression === null) {
return {
kind: TypeValueReferenceKind.UNAVAILABLE,
reason: {kind: ValueUnavailableKind.MISSING_TYPE},
};
}
return {
kind: TypeValueReferenceKind.LOCAL,
expression: typeExpression,
defaultImportStatement: null,
};
}
/**
* Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static property of the class.

View File

@ -8,8 +8,6 @@
/// <reference types="node" />
import * as os from 'os';
import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system';
import {Logger} from '../../src/ngtsc/logging';
import {ParsedConfiguration} from '../../src/perform_compile';
@ -35,7 +33,7 @@ import {composeTaskCompletedCallbacks, createLogErrorHandler, createMarkAsProces
import {AsyncLocker} from './locking/async_locker';
import {LockFileWithChildProcess} from './locking/lock_file_with_child_process';
import {SyncLocker} from './locking/sync_locker';
import {AsyncNgccOptions, getSharedSetup, SyncNgccOptions} from './ngcc_options';
import {AsyncNgccOptions, getMaxNumberOfWorkers, getSharedSetup, SyncNgccOptions} from './ngcc_options';
import {NgccConfiguration} from './packages/configuration';
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
@ -92,10 +90,9 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
return;
}
// Execute in parallel, if async execution is acceptable and there are more than 2 CPU cores.
// (One CPU core is always reserved for the master process and we need at least 2 worker processes
// in order to run tasks in parallel.)
const inParallel = async && (os.cpus().length > 2);
// Determine the number of workers to use and whether ngcc should run in parallel.
const workerCount = async ? getMaxNumberOfWorkers() : 1;
const inParallel = workerCount > 1;
const analyzeEntryPoints = getAnalyzeEntryPointsFn(
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
@ -113,7 +110,7 @@ export function mainNgcc(options: AsyncNgccOptions|SyncNgccOptions): void|Promis
const createTaskCompletedCallback =
getCreateTaskCompletedCallback(pkgJsonUpdater, errorOnFailedEntryPoint, logger, fileSystem);
const executor = getExecutor(
async, inParallel, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
async, workerCount, logger, fileWriter, pkgJsonUpdater, fileSystem, config,
createTaskCompletedCallback);
return executor.execute(analyzeEntryPoints, createCompileFn);
@ -153,7 +150,7 @@ function getCreateTaskCompletedCallback(
}
function getExecutor(
async: boolean, inParallel: boolean, logger: Logger, fileWriter: FileWriter,
async: boolean, workerCount: number, logger: Logger, fileWriter: FileWriter,
pkgJsonUpdater: PackageJsonUpdater, fileSystem: FileSystem, config: NgccConfiguration,
createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
@ -161,9 +158,8 @@ function getExecutor(
// Execute asynchronously (either serially or in parallel)
const {retryAttempts, retryDelay} = config.getLockingConfig();
const locker = new AsyncLocker(lockFile, logger, retryDelay, retryAttempts);
if (inParallel) {
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
const workerCount = Math.min(8, os.cpus().length - 1);
if (workerCount > 1) {
// Execute in parallel.
return new ClusterExecutor(
workerCount, fileSystem, logger, fileWriter, pkgJsonUpdater, locker,
createTaskCompletedCallback);

View File

@ -5,6 +5,8 @@
* 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 os from 'os';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
import {ConsoleLogger, Logger, LogLevel} from '../../src/ngtsc/logging';
import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile';
@ -254,3 +256,26 @@ function checkForSolutionStyleTsConfig(
` ngcc ... --tsconfig "${fileSystem.relative(projectPath, tsConfig.project)}"`);
}
}
/**
* Determines the maximum number of workers to use for parallel execution. This can be set using the
* NGCC_MAX_WORKERS environment variable, or is computed based on the number of available CPUs. One
* CPU core is always reserved for the master process, so we take the number of CPUs minus one, with
* a maximum of 4 workers. We don't scale the number of workers beyond 4 by default, as it takes
* considerably more memory and CPU cycles while not offering a substantial improvement in time.
*/
export function getMaxNumberOfWorkers(): number {
const maxWorkers = process.env.NGCC_MAX_WORKERS;
if (maxWorkers === undefined) {
// Use up to 4 CPU cores for workers, always reserving one for master.
return Math.max(1, Math.min(4, os.cpus().length - 1));
}
const numericMaxWorkers = +maxWorkers.trim();
if (!Number.isInteger(numericMaxWorkers)) {
throw new Error('NGCC_MAX_WORKERS should be an integer.');
} else if (numericMaxWorkers < 1) {
throw new Error('NGCC_MAX_WORKERS should be at least 1.');
}
return numericMaxWorkers;
}

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
import {PathMappings} from '../path_mappings';
import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPoint, EntryPointFormat} from './entry_point';
import {NgccSourcesCompilerHost} from './ngcc_compiler_host';
import {NgccDtsCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host';
import {EntryPointFileCache, SharedFileCache} from './source_file_cache';
/**
* A bundle of files and paths (and TS programs) that correspond to a particular
@ -31,6 +32,8 @@ export interface EntryPointBundle {
* Get an object that describes a formatted bundle for an entry-point.
* @param fs The current file-system being used.
* @param entryPoint The entry-point that contains the bundle.
* @param sharedFileCache The cache to use for source files that are shared across all entry-points.
* @param moduleResolutionCache The module resolution cache to use.
* @param formatPath The path to the source files for this bundle.
* @param isCore This entry point is the Angular core package.
* @param format The underlying format of the bundle.
@ -42,7 +45,8 @@ export interface EntryPointBundle {
* component templates.
*/
export function makeEntryPointBundle(
fs: FileSystem, entryPoint: EntryPoint, formatPath: string, isCore: boolean,
fs: FileSystem, entryPoint: EntryPoint, sharedFileCache: SharedFileCache,
moduleResolutionCache: ts.ModuleResolutionCache, formatPath: string, isCore: boolean,
format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings,
mirrorDtsFromSrc: boolean = false,
enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle {
@ -50,8 +54,10 @@ export function makeEntryPointBundle(
const rootDir = entryPoint.packagePath;
const options: ts
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
const dtsHost = new NgtscCompilerHost(fs, options);
const entryPointCache = new EntryPointFileCache(fs, sharedFileCache);
const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache, moduleResolutionCache);
const srcHost = new NgccSourcesCompilerHost(
fs, options, entryPointCache, moduleResolutionCache, entryPoint.packagePath);
// Create the bundle programs, as necessary.
const absFormatPath = fs.resolve(entryPoint.path, formatPath);

View File

@ -10,6 +10,7 @@ import * as ts from 'typescript';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {isWithinPackage} from '../analysis/util';
import {isRelativePath} from '../utils';
import {EntryPointFileCache} from './source_file_cache';
/**
* Represents a compiler host that resolves a module import as a JavaScript source file if
@ -18,19 +19,24 @@ import {isRelativePath} from '../utils';
* would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file.
*/
export class NgccSourcesCompilerHost extends NgtscCompilerHost {
private cache = ts.createModuleResolutionCache(
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
constructor(
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
private moduleResolutionCache: ts.ModuleResolutionCache,
protected packagePath: AbsoluteFsPath) {
super(fs, options);
}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
return this.cache.getCachedSourceFile(fileName, languageVersion);
}
resolveModuleNames(
moduleNames: string[], containingFile: string, reusedNames?: string[],
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
return moduleNames.map(moduleName => {
const {resolvedModule} = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.cache, redirectedReference);
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
redirectedReference);
// If the module request originated from a relative import in a JavaScript source file,
// TypeScript may have resolved the module to its .d.ts declaration file if the .js source
@ -59,3 +65,31 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
});
}
}
/**
* A compiler host implementation that is used for the typings program. It leverages the entry-point
* cache for source files and module resolution, as these results can be reused across the sources
* program.
*/
export class NgccDtsCompilerHost extends NgtscCompilerHost {
constructor(
fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache,
private moduleResolutionCache: ts.ModuleResolutionCache) {
super(fs, options);
}
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
return this.cache.getCachedSourceFile(fileName, languageVersion);
}
resolveModuleNames(
moduleNames: string[], containingFile: string, reusedNames?: string[],
redirectedReference?: ts.ResolvedProjectReference): Array<ts.ResolvedModule|undefined> {
return moduleNames.map(moduleName => {
const {resolvedModule} = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.moduleResolutionCache,
redirectedReference);
return resolvedModule;
});
}
}

View File

@ -0,0 +1,197 @@
/**
* @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 ts from 'typescript';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
/**
* A cache that holds on to source files that can be shared for processing all entry-points in a
* single invocation of ngcc. In particular, the following files are shared across all entry-points
* through this cache:
*
* 1. Default library files such as `lib.dom.d.ts` and `lib.es5.d.ts`. These files don't change
* and some are very large, so parsing is expensive. Therefore, the parsed `ts.SourceFile`s for
* the default library files are cached.
* 2. The typings of @angular scoped packages. The typing files for @angular packages are typically
* used in the entry-points that ngcc processes, so benefit from a single source file cache.
* Especially `@angular/core/core.d.ts` is large and expensive to parse repeatedly. In contrast
* to default library files, we have to account for these files to be invalidated during a single
* invocation of ngcc, as ngcc will overwrite the .d.ts files during its processing.
*
* The lifecycle of this cache corresponds with a single invocation of ngcc. Separate invocations,
* e.g. the CLI's synchronous module resolution fallback will therefore all have their own cache.
* This allows for the source file cache to be garbage collected once ngcc processing has completed.
*/
export class SharedFileCache {
private sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
constructor(private fs: FileSystem) {}
/**
* Loads a `ts.SourceFile` if the provided `fileName` is deemed appropriate to be cached. To
* optimize for memory usage, only files that are generally used in all entry-points are cached.
* If `fileName` is not considered to benefit from caching or the requested file does not exist,
* then `undefined` is returned.
*/
getCachedSourceFile(fileName: string): ts.SourceFile|undefined {
const absPath = this.fs.resolve(fileName);
if (isDefaultLibrary(absPath, this.fs)) {
return this.getStableCachedFile(absPath);
} else if (isAngularDts(absPath, this.fs)) {
return this.getVolatileCachedFile(absPath);
} else {
return undefined;
}
}
/**
* Attempts to load the source file from the cache, or parses the file into a `ts.SourceFile` if
* it's not yet cached. This method assumes that the file will not be modified for the duration
* that this cache is valid for. If that assumption does not hold, the `getVolatileCachedFile`
* method is to be used instead.
*/
private getStableCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
if (!this.sfCache.has(absPath)) {
const content = readFile(absPath, this.fs);
if (content === undefined) {
return undefined;
}
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
this.sfCache.set(absPath, sf);
}
return this.sfCache.get(absPath)!;
}
/**
* In contrast to `getStableCachedFile`, this method always verifies that the cached source file
* is the same as what's stored on disk. This is done for files that are expected to change during
* ngcc's processing, such as @angular scoped packages for which the .d.ts files are overwritten
* by ngcc. If the contents on disk have changed compared to a previously cached source file, the
* content from disk is re-parsed and the cache entry is replaced.
*/
private getVolatileCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined {
const content = readFile(absPath, this.fs);
if (content === undefined) {
return undefined;
}
if (!this.sfCache.has(absPath) || this.sfCache.get(absPath)!.text !== content) {
const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015);
this.sfCache.set(absPath, sf);
}
return this.sfCache.get(absPath)!;
}
}
const DEFAULT_LIB_PATTERN = ['node_modules', 'typescript', 'lib', /^lib\..+\.d\.ts$/];
/**
* Determines whether the provided path corresponds with a default library file inside of the
* typescript package.
*
* @param absPath The path for which to determine if it corresponds with a default library file.
* @param fs The filesystem to use for inspecting the path.
*/
export function isDefaultLibrary(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
return isFile(absPath, DEFAULT_LIB_PATTERN, fs);
}
const ANGULAR_DTS_PATTERN = ['node_modules', '@angular', /./, /\.d\.ts$/];
/**
* Determines whether the provided path corresponds with a .d.ts file inside of an @angular
* scoped package. This logic only accounts for the .d.ts files in the root, which is sufficient
* to find the large, flattened entry-point files that benefit from caching.
*
* @param absPath The path for which to determine if it corresponds with an @angular .d.ts file.
* @param fs The filesystem to use for inspecting the path.
*/
export function isAngularDts(absPath: AbsoluteFsPath, fs: FileSystem): boolean {
return isFile(absPath, ANGULAR_DTS_PATTERN, fs);
}
/**
* Helper function to determine whether a file corresponds with a given pattern of segments.
*
* @param path The path for which to determine if it corresponds with the provided segments.
* @param segments Array of segments; the `path` must have ending segments that match the
* patterns in this array.
* @param fs The filesystem to use for inspecting the path.
*/
function isFile(
path: AbsoluteFsPath, segments: ReadonlyArray<string|RegExp>, fs: FileSystem): boolean {
for (let i = segments.length - 1; i >= 0; i--) {
const pattern = segments[i];
const segment = fs.basename(path);
if (typeof pattern === 'string') {
if (pattern !== segment) {
return false;
}
} else {
if (!pattern.test(segment)) {
return false;
}
}
path = fs.dirname(path);
}
return true;
}
/**
* A cache for processing a single entry-point. This exists to share `ts.SourceFile`s between the
* source and typing programs that are created for a single program.
*/
export class EntryPointFileCache {
private readonly sfCache = new Map<AbsoluteFsPath, ts.SourceFile>();
constructor(private fs: FileSystem, private sharedFileCache: SharedFileCache) {}
/**
* Returns and caches a parsed `ts.SourceFile` for the provided `fileName`. If the `fileName` is
* cached in the shared file cache, that result is used. Otherwise, the source file is cached
* internally. This method returns `undefined` if the requested file does not exist.
*
* @param fileName The path of the file to retrieve a source file for.
* @param languageVersion The language version to use for parsing the file.
*/
getCachedSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined {
const staticSf = this.sharedFileCache.getCachedSourceFile(fileName);
if (staticSf !== undefined) {
return staticSf;
}
const absPath = this.fs.resolve(fileName);
if (this.sfCache.has(absPath)) {
return this.sfCache.get(absPath);
}
const content = readFile(absPath, this.fs);
if (content === undefined) {
return undefined;
}
const sf = ts.createSourceFile(fileName, content, languageVersion);
this.sfCache.set(absPath, sf);
return sf;
}
}
function readFile(absPath: AbsoluteFsPath, fs: FileSystem): string|undefined {
if (!fs.exists(absPath) || !fs.stat(absPath).isFile()) {
return undefined;
}
return fs.readFile(absPath);
}
/**
* Creates a `ts.ModuleResolutionCache` that uses the provided filesystem for path operations.
*
* @param fs The filesystem to use for path operations.
*/
export function createModuleResolutionCache(fs: FileSystem): ts.ModuleResolutionCache {
return ts.createModuleResolutionCache(fs.pwd(), fileName => {
return fs.isCaseSensitive() ? fileName : fileName.toLowerCase();
});
}

View File

@ -14,6 +14,7 @@ import {NgccEntryPointConfig} from '../../src/packages/configuration';
import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host';
import {createModuleResolutionCache, EntryPointFileCache, SharedFileCache} from '../../src/packages/source_file_cache';
export type TestConfig = Pick<NgccEntryPointConfig, 'generateDeepReexports'>;
@ -68,7 +69,10 @@ export function makeTestBundleProgram(
const rootDir = fs.dirname(entryPointPath);
const options: ts.CompilerOptions =
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
const moduleResolutionCache = createModuleResolutionCache(fs);
const entryPointFileCache = new EntryPointFileCache(fs, new SharedFileCache(fs));
const host =
new NgccSourcesCompilerHost(fs, options, entryPointFileCache, moduleResolutionCache, rootDir);
return makeBundleProgram(
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
}

View File

@ -1211,6 +1211,69 @@ exports.MissingClass2 = MissingClass2;
});
describe('getConstructorParameters', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types',
() => {
const files = [
{
name: _('/node_modules/shared-lib/foo.d.ts'),
contents: `
declare class Foo {}
export {Foo as Bar};
`,
},
{
name: _('/node_modules/shared-lib/index.d.ts'),
contents: `
export {Bar as Baz} from './foo';
`,
},
{
name: _('/local.js'),
contents: `
var Internal = (function() {
function Internal() {
}
return Internal;
}());
exports.External = Internal;
`
},
{
name: _('/main.js'),
contents: `
var shared = require('shared-lib');
var local = require('./local');
var SameFile = (function() {
function SameFile() {
}
return SameFile;
}());
exports.SameFile = SameFile;
var SomeClass = (function() {
function SomeClass(arg1, arg2, arg3) {}
return SomeClass;
}());
SomeClass.ctorParameters = function() { return [{ type: shared.Baz }, { type: local.External }, { type: SameFile }]; };
exports.SomeClass = SomeClass;
`,
},
];
loadTestFiles(files);
const bundle = makeTestBundleProgram(_('/main.js'));
const host =
createHost(bundle, new CommonJsReflectionHost(new MockLogger(), false, bundle));
const classNode = getDeclaration(
bundle.program, _('/main.js'), 'SomeClass', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(
parameters, ['shared.Baz', 'local.External', 'SameFile']);
});
it('should find the decorated constructor parameters', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]);
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);

View File

@ -1140,6 +1140,57 @@ runInEachFileSystem(() => {
});
describe('getConstructorParameters()', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types',
() => {
const files = [
{
name: _('/node_modules/shared-lib/foo.d.ts'),
contents: `
declare class Foo {}
export {Foo as Bar};
`,
},
{
name: _('/node_modules/shared-lib/index.d.ts'),
contents: `
export {Bar as Baz} from './foo';
`,
},
{
name: _('/local.js'),
contents: `
class Internal {}
export {Internal as External};
`
},
{
name: _('/main.js'),
contents: `
import {Baz} from 'shared-lib';
import {External} from './local';
export class SameFile {}
export class SomeClass {
constructor(arg1, arg2, arg3) {}
}
SomeClass.ctorParameters = [{ type: Baz }, { type: External }, { type: SameFile }];
`,
},
];
loadTestFiles(files);
const bundle = makeTestBundleProgram(_('/main.js'));
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const classNode =
getDeclaration(bundle.program, _('/main.js'), 'SomeClass', isNamedClassDeclaration);
const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']);
});
it('should find the decorated constructor parameters', () => {
loadFakeCore(getFileSystem());
loadTestFiles([SOME_DIRECTIVE_FILE]);
@ -1154,7 +1205,7 @@ runInEachFileSystem(() => {
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(
parameters, ['ViewContainerRef', 'TemplateRef', null], '@angular/core');
parameters, ['ViewContainerRef', 'TemplateRef', null]);
});
it('should accept `ctorParameters` as an array', () => {

View File

@ -1252,6 +1252,67 @@ runInEachFileSystem(() => {
});
describe('getConstructorParameters()', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types',
() => {
const files = [
{
name: _('/node_modules/shared-lib/foo.d.ts'),
contents: `
declare class Foo {}
export {Foo as Bar};
`,
},
{
name: _('/node_modules/shared-lib/index.d.ts'),
contents: `
export {Bar as Baz} from './foo';
`,
},
{
name: _('/local.js'),
contents: `
var Internal = (function() {
function Internal() {
}
return Internal;
}());
export {Internal as External};
`
},
{
name: _('/main.js'),
contents: `
import {Baz} from 'shared-lib';
import {External} from './local';
var SameFile = (function() {
function SameFile() {
}
return SameFile;
}());
export SameFile;
var SomeClass = (function() {
function SomeClass(arg1, arg2, arg3) {}
return SomeClass;
}());
SomeClass.ctorParameters = function() { return [{ type: Baz }, { type: External }, { type: SameFile }]; };
export SomeClass;
`,
},
];
loadTestFiles(files);
const bundle = makeTestBundleProgram(_('/main.js'));
const host = createHost(bundle, new Esm5ReflectionHost(new MockLogger(), false, bundle));
const classNode = getDeclaration(
bundle.program, _('/main.js'), 'SomeClass', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']);
});
it('should find the decorated constructor parameters', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]);
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);

View File

@ -1332,6 +1332,78 @@ runInEachFileSystem(() => {
});
describe('getConstructorParameters', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types',
() => {
const files = [
{
name: _('/node_modules/shared-lib/foo.d.ts'),
contents: `
declare class Foo {}
export {Foo as Bar};
`,
},
{
name: _('/node_modules/shared-lib/index.d.ts'),
contents: `
export {Bar as Baz} from './foo';
`,
},
{
name: _('/local.js'),
contents: `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define('local', ['exports'], factory) :
(factory(global.local));
}(this, (function (exports) { 'use strict';
var Internal = (function() {
function Internal() {
}
return Internal;
}());
exports.External = Internal;
})));
`
},
{
name: _('/main.js'),
contents: `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('shared-lib), require('./local')) :
typeof define === 'function' && define.amd ? define('main', ['exports', 'shared-lib', './local'], factory) :
(factory(global.main, global.shared, global.local));
}(this, (function (exports, shared, local) { 'use strict';
var SameFile = (function() {
function SameFile() {
}
return SameFile;
}());
exports.SameFile = SameFile;
var SomeClass = (function() {
function SomeClass(arg1, arg2, arg3) {}
return SomeClass;
}());
SomeClass.ctorParameters = function() { return [{ type: shared.Baz }, { type: local.External }, { type: SameFile }]; };
exports.SomeClass = SomeClass;
})));
`,
},
];
loadTestFiles(files);
const bundle = makeTestBundleProgram(_('/main.js'));
const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle));
const classNode = getDeclaration(
bundle.program, _('/main.js'), 'SomeClass', isNamedVariableDeclaration);
const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(
parameters, ['shared.Baz', 'local.External', 'SameFile']);
});
it('should find the decorated constructor parameters', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]);
const bundle = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
@ -1591,7 +1663,7 @@ runInEachFileSystem(() => {
`;
break;
case 'inlined_with_suffix':
fileHeaderWithUmd = `
fileHeaderWithUmd = `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports)) :
typeof define === 'function' && define.amd ? define('test', ['exports'], factory) :

View File

@ -13,26 +13,36 @@ import {CtorParameter, TypeValueReferenceKind} from '../../../src/ngtsc/reflecti
* names.
*/
export function expectTypeValueReferencesForParameters(
parameters: CtorParameter[], expectedParams: (string|null)[], fromModule: string|null = null) {
parameters: CtorParameter[], expectedParams: (string|null)[],
fromModule: (string|null)[] = []) {
parameters!.forEach((param, idx) => {
const expected = expectedParams[idx];
if (expected !== null) {
if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) {
fail(`Incorrect typeValueReference generated, expected ${expected}`);
fail(`Incorrect typeValueReference generated for ${param.name}, expected "${
expected}" because "${param.typeValueReference.reason}"`);
} else if (
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL && fromModule !== null) {
fail(`Incorrect typeValueReference generated, expected non-local`);
param.typeValueReference.kind === TypeValueReferenceKind.LOCAL &&
fromModule[idx] != null) {
fail(`Incorrect typeValueReference generated for ${param.name}, expected non-LOCAL (from ${
fromModule[idx]}) but was marked LOCAL`);
} else if (
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL && fromModule === null) {
fail(`Incorrect typeValueReference generated, expected local`);
param.typeValueReference.kind !== TypeValueReferenceKind.LOCAL &&
fromModule[idx] == null) {
fail(`Incorrect typeValueReference generated for ${
param.name}, expected LOCAL but was imported from ${
param.typeValueReference.moduleName}`);
} else if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL) {
if (!ts.isIdentifier(param.typeValueReference.expression)) {
fail(`Incorrect typeValueReference generated, expected identifier`);
if (!ts.isIdentifier(param.typeValueReference.expression) &&
!ts.isPropertyAccessExpression(param.typeValueReference.expression)) {
fail(`Incorrect typeValueReference generated for ${
param.name}, expected an identifier but got "${
param.typeValueReference.expression.getText()}"`);
} else {
expect(param.typeValueReference.expression.text).toEqual(expected);
expect(param.typeValueReference.expression.getText()).toEqual(expected);
}
} else if (param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED) {
expect(param.typeValueReference.moduleName).toBe(fromModule!);
expect(param.typeValueReference.moduleName).toBe(fromModule[idx]!);
expect(param.typeValueReference.importedName).toBe(expected);
}
}

View File

@ -213,13 +213,15 @@ export function compileIntoApf(
fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2));
}
const stdFiles = loadStandardTestFiles({fakeCore: false});
/**
* Prepares a mock filesystem that contains all provided source files, which can be used to emit
* compiled code into.
*/
function setupCompileFs(sources: PackageSources): {rootNames: string[], compileFs: FileSystem} {
const compileFs = new MockFileSystemPosix(true);
compileFs.init(loadStandardTestFiles({fakeCore: false}));
compileFs.init(stdFiles);
const rootNames = Object.keys(sources);

View File

@ -5,12 +5,13 @@
* 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 os from 'os';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../src/ngtsc/logging/testing';
import {clearTsConfigCache, getSharedSetup, NgccOptions} from '../src/ngcc_options';
import {clearTsConfigCache, getMaxNumberOfWorkers, getSharedSetup, NgccOptions} from '../src/ngcc_options';
@ -100,6 +101,67 @@ runInEachFileSystem(() => {
});
});
describe('getMaxNumberOfWorkers', () => {
let processEnv: NodeJS.ProcessEnv;
let cpuSpy: jasmine.Spy;
beforeEach(() => {
processEnv = process.env;
process.env = {...process.env};
cpuSpy = spyOn(os, 'cpus');
});
afterEach(() => {
process.env = processEnv;
});
it('should use NGCC_MAX_WORKERS environment variable if set', () => {
process.env.NGCC_MAX_WORKERS = '16';
expect(getMaxNumberOfWorkers()).toBe(16);
process.env.NGCC_MAX_WORKERS = '8';
expect(getMaxNumberOfWorkers()).toBe(8);
process.env.NGCC_MAX_WORKERS = ' 8 ';
expect(getMaxNumberOfWorkers()).toBe(8);
});
it('should throw an error if NGCC_MAX_WORKERS is less than 1', () => {
process.env.NGCC_MAX_WORKERS = '0';
expect(() => getMaxNumberOfWorkers())
.toThrow(new Error('NGCC_MAX_WORKERS should be at least 1.'));
process.env.NGCC_MAX_WORKERS = '-1';
expect(() => getMaxNumberOfWorkers())
.toThrow(new Error('NGCC_MAX_WORKERS should be at least 1.'));
});
it('should throw an error if NGCC_MAX_WORKERS is not an integer', () => {
process.env.NGCC_MAX_WORKERS = 'a';
expect(() => getMaxNumberOfWorkers())
.toThrow(new Error('NGCC_MAX_WORKERS should be an integer.'));
process.env.NGCC_MAX_WORKERS = '1.5';
expect(() => getMaxNumberOfWorkers())
.toThrow(new Error('NGCC_MAX_WORKERS should be an integer.'));
process.env.NGCC_MAX_WORKERS = '-';
expect(() => getMaxNumberOfWorkers())
.toThrow(new Error('NGCC_MAX_WORKERS should be an integer.'));
});
it('should fallback to the number of cpus, minus one (for the master process), with a maximum of 4 workers',
() => {
simulateNumberOfCpus(1);
expect(getMaxNumberOfWorkers()).toBe(1);
simulateNumberOfCpus(2);
expect(getMaxNumberOfWorkers()).toBe(1);
simulateNumberOfCpus(4);
expect(getMaxNumberOfWorkers()).toBe(3);
simulateNumberOfCpus(6);
expect(getMaxNumberOfWorkers()).toBe(4);
simulateNumberOfCpus(8);
expect(getMaxNumberOfWorkers()).toBe(4);
});
function simulateNumberOfCpus(cpus: number): void {
cpuSpy.and.returnValue(new Array(cpus).fill({model: 'Mock CPU'} as any));
}
});
/**
* This function creates an object that contains the minimal required properties for NgccOptions.
*/

View File

@ -10,6 +10,7 @@ import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {EntryPoint} from '../../src/packages/entry_point';
import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache';
runInEachFileSystem(() => {
describe('entry point bundle', () => {
@ -180,7 +181,10 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const esm5bundle = makeEntryPointBundle(fs, entryPoint, './index.js', false, 'esm5', true);
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5', true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toEqual(jasmine.arrayWithExactContents([
@ -291,8 +295,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
@ -328,8 +335,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toContain(absoluteFrom('/node_modules/test/internal.js'));
@ -351,8 +361,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './esm2015/index.js', false, 'esm2015', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache,
'./esm2015/index.js', false, 'esm2015',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toContain(absoluteFrom('/node_modules/internal/esm2015/src/internal.js'));
@ -374,8 +387,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm5',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ false);
expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName))
.toContain(absoluteFrom('/node_modules/test/internal.js'));
@ -398,8 +414,11 @@ runInEachFileSystem(() => {
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const moduleResolutionCache = createModuleResolutionCache(fs);
const bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm2015', /* transformDts */ true,
fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false,
'esm2015',
/* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]);
});

View File

@ -0,0 +1,223 @@
/**
* @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 ts from 'typescript';
import {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {EntryPointFileCache, isAngularDts, isDefaultLibrary, SharedFileCache} from '../../src/packages/source_file_cache';
runInEachFileSystem(() => {
describe('caching', () => {
let _: typeof absoluteFrom;
let fs: FileSystem;
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
loadTestFiles([
{
name: _('/node_modules/typescript/lib/lib.es5.d.ts'),
contents: `export declare interface Array {}`,
},
{
name: _('/node_modules/typescript/lib/lib.dom.d.ts'),
contents: `export declare interface Window {}`,
},
{
name: _('/node_modules/@angular/core/core.d.ts'),
contents: `export declare interface Component {}`,
},
{
name: _('/node_modules/@angular/common/common.d.ts'),
contents: `export declare interface NgIf {}`,
},
{
name: _('/index.ts'),
contents: `export const index = true;`,
},
{
name: _('/main.ts'),
contents: `export const main = true;`,
},
]);
});
describe('SharedFileCache', () => {
it('should cache a parsed source file for default libraries', () => {
const cache = new SharedFileCache(fs);
const libEs5 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
expect(libEs5).not.toBeUndefined();
expect(libEs5.text).toContain('Array');
const libDom = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
expect(libDom).not.toBeUndefined();
expect(libDom.text).toContain('Window');
const libEs5_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!;
expect(libEs5_2).toBe(libEs5);
const libDom_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!;
expect(libDom_2).toBe(libDom);
});
it('should cache a parsed source file for @angular scoped packages', () => {
const cache = new SharedFileCache(fs);
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core).not.toBeUndefined();
expect(core.text).toContain('Component');
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common).not.toBeUndefined();
expect(common.text).toContain('NgIf');
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core_2).toBe(core);
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common_2).toBe(common);
});
it('should reparse @angular d.ts files when they change', () => {
const cache = new SharedFileCache(fs);
const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core).not.toBeUndefined();
expect(core.text).toContain('Component');
const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common).not.toBeUndefined();
expect(common.text).toContain('NgIf');
fs.writeFile(
_('/node_modules/@angular/core/core.d.ts'), `export declare interface Directive {}`);
const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core_2).not.toBe(core);
expect(core_2.text).toContain('Directive');
const core_3 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!;
expect(core_3).toBe(core_2);
const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!;
expect(common_2).toBe(common);
});
it('should not cache files that are not default library files inside of the typescript package',
() => {
const cache = new SharedFileCache(fs);
expect(cache.getCachedSourceFile('/node_modules/typescript/lib/typescript.d.ts'))
.toBeUndefined();
expect(cache.getCachedSourceFile('/typescript/lib.es5.d.ts')).toBeUndefined();
});
});
describe('isDefaultLibrary()', () => {
it('should accept lib files inside of the typescript package', () => {
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.d.ts'), fs)).toBe(true);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.dom.d.ts'), fs)).toBe(true);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es2015.core.d.ts'), fs))
.toBe(true);
expect(isDefaultLibrary(_('/root/node_modules/typescript/lib/lib.es5.d.ts'), fs))
.toBe(true);
});
it('should reject non lib files inside of the typescript package', () => {
expect(isDefaultLibrary(_('/node_modules/typescript/lib/typescript.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/typescript/lib.es5.d.ts'), fs)).toBe(false);
});
it('should reject lib files outside of the typescript package', () => {
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false);
expect(isDefaultLibrary(_('/typescript/lib/lib.es5.d.ts'), fs)).toBe(false);
});
});
describe('isAngularDts()', () => {
it('should accept .d.ts files inside of the @angular scope', () => {
expect(isAngularDts(_('/node_modules/@angular/core/core.d.ts'), fs)).toBe(true);
expect(isAngularDts(_('/node_modules/@angular/common/common.d.ts'), fs)).toBe(true);
});
it('should reject non-.d.ts files inside @angular scoped packages', () => {
expect(isAngularDts(_('/node_modules/@angular/common/src/common.ts'), fs)).toBe(false);
});
it('should reject .d.ts files nested deeply inside @angular scoped packages', () => {
expect(isAngularDts(_('/node_modules/@angular/common/src/common.d.ts'), fs)).toBe(false);
});
it('should reject .d.ts files directly inside the @angular scope', () => {
expect(isAngularDts(_('/node_modules/@angular/common.d.ts'), fs)).toBe(false);
});
it('should reject files that are not inside node_modules', () => {
expect(isAngularDts(_('/@angular/core/core.d.ts'), fs)).toBe(false);
});
});
describe('EntryPointFileCache', () => {
let sharedFileCache: SharedFileCache;
beforeEach(() => {
sharedFileCache = new SharedFileCache(fs);
});
it('should prefer source files cached in SharedFileCache', () => {
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
const libEs5_1 = cache1.getCachedSourceFile(
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
expect(libEs5_1).not.toBeUndefined();
expect(libEs5_1.text).toContain('Array');
expect(libEs5_1.languageVersion).toBe(ts.ScriptTarget.ES2015);
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
const libEs5_2 = cache2.getCachedSourceFile(
'/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!;
expect(libEs5_1).toBe(libEs5_2);
});
it('should cache source files that are not default library files', () => {
const cache = new EntryPointFileCache(fs, sharedFileCache);
const index = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
expect(index).not.toBeUndefined();
expect(index.text).toContain('index');
expect(index.languageVersion).toBe(ts.ScriptTarget.ESNext);
const main = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
expect(main).not.toBeUndefined();
expect(main.text).toContain('main');
expect(main.languageVersion).toBe(ts.ScriptTarget.ESNext);
const index_2 = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
expect(index_2).toBe(index);
const main_2 = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!;
expect(main_2).toBe(main);
});
it('should not share non-library files across multiple cache instances', () => {
const cache1 = new EntryPointFileCache(fs, sharedFileCache);
const cache2 = new EntryPointFileCache(fs, sharedFileCache);
const index1 = cache1.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
const index2 = cache2.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!;
expect(index1).not.toBe(index2);
});
it('should return undefined if the file does not exist', () => {
const cache = new EntryPointFileCache(fs, sharedFileCache);
expect(cache.getCachedSourceFile('/nonexistent.ts', ts.ScriptTarget.ESNext))
.toBeUndefined();
});
it('should return undefined if the path is a directory', () => {
const cache = new EntryPointFileCache(fs, sharedFileCache);
expect(cache.getCachedSourceFile('/node_modules', ts.ScriptTarget.ESNext)).toBeUndefined();
});
});
});
});

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