Compare commits

..

69 Commits

Author SHA1 Message Date
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
77 changed files with 589 additions and 1742 deletions

View File

@ -653,10 +653,8 @@ jobs:
name: Starting Saucelabs tunnel service
command: ./tools/saucelabs/sauce-service.sh run
background: true
# add module umd tsc compile option so the test can work
# properly in the legacy browsers
- run: yarn tsc -p packages --module UMD
- run: yarn tsc -p modules --module UMD
- run: yarn tsc -p packages
- run: yarn tsc -p modules
- run: yarn bazel build //packages/zone.js:npm_package
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
# specific tests which are reliant on such generated fixtures is not an option as SystemJS

View File

@ -1,5 +1,5 @@
---
name: "\U0001F41E Bug report"
name: "\U0001F41EBug report"
about: Report a bug in the Angular Framework
---
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅

View File

@ -1,5 +1,5 @@
---
name: "\U0001F680 Feature request"
name: "\U0001F680Feature request"
about: Suggest a feature for Angular Framework
---

View File

@ -1,5 +1,5 @@
---
name: "❓ Support request"
name: "❓Support request"
about: Questions and requests for support
---

View File

@ -1,5 +1,5 @@
---
name: "\U0001F6E0 Angular CLI"
name: "\U0001F6E0Angular CLI"
about: Issues and feature requests for Angular CLI
---

View File

@ -1,5 +1,5 @@
---
name: "\U0001F48E Angular Components"
name: "\U0001F48EAngular Components"
about: Issues and feature requests for Angular Components
---

View File

@ -10,6 +10,6 @@ jobs:
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/lock-closed@414834b2b24dd2df37c6ed00808387ee6fd91b66
- uses: angular/dev-infra/github-actions/lock-closed@66462f6
with:
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}

View File

@ -1,21 +1,3 @@
<a name="11.0.0-next.1"></a>
# 11.0.0-next.1 (2020-09-09)
### Bug Fixes
* **compiler-cli:** compute source-mappings for localized strings ([#38645](https://github.com/angular/angular/issues/38645)) ([7e0b3fd](https://github.com/angular/angular/commit/7e0b3fd)), closes [#38588](https://github.com/angular/angular/issues/38588)
* **core:** remove CollectionChangeRecord symbol ([#38668](https://github.com/angular/angular/issues/38668)) ([fdea180](https://github.com/angular/angular/commit/fdea180))
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([926ffcd](https://github.com/angular/angular/commit/926ffcd)), closes [#12842](https://github.com/angular/angular/issues/12842)
### BREAKING CHANGES
* **core:** CollectionChangeRecord has been removed, use IterableChangeRecord
instead
<a name="10.1.1"></a>
## 10.1.1 (2020-09-09)
@ -41,31 +23,6 @@ instead
<a name="11.0.0-next.0"></a>
# 11.0.0-next.0 (2020-09-02)
### Bug Fixes
* **forms:** ensure to emit `statusChanges` on subsequent value update/validations ([#38354](https://github.com/angular/angular/issues/38354)) ([d9fea85](https://github.com/angular/angular/commit/d9fea85)), closes [#20424](https://github.com/angular/angular/issues/20424) [#14542](https://github.com/angular/angular/issues/14542)
* **service-worker:** fix condition to check for a cache-busted request ([#36847](https://github.com/angular/angular/issues/36847)) ([5be4edf](https://github.com/angular/angular/commit/5be4edf))
### Features
* **service-worker:** add `UnrecoverableStateError` ([#36847](https://github.com/angular/angular/issues/36847)) ([036a2fa](https://github.com/angular/angular/commit/036a2fa)), closes [#36539](https://github.com/angular/angular/issues/36539)
### BREAKING CHANGES
* **forms:** Previously if FormControl, FormGroup and FormArray class instances had async validators
defined at initialization time, the status change event was not emitted once async validator
completed. After this change the status event is emitted into the `statusChanges` observable.
If your code relies on the old behavior, you can filter/ignore this additional status change
event.
<a name="10.1.0"></a>
# 10.1.0 (2020-09-02)

View File

@ -1,9 +0,0 @@
/*
* This example project is special in that it is not a cli app. To run tests appropriate for this
* project, the test command is overwritten in `aio/content/examples/observables/example-config.json`.
*
* This is an empty placeholder file to ensure that `aio/tools/examples/run-example-e2e.js` runs
* tests for this project.
*
* TODO: Fix our infrastructure/tooling, so that this hack is not necessary.
*/

View File

@ -1,12 +0,0 @@
{
"tests": [
{
"cmd": "yarn",
"args": ["tsc", "--project", "tsconfig.spec.json", "--module", "commonjs"]
},
{
"cmd": "yarn",
"args": ["jasmine", "out-tsc/**/*.spec.js"]
}
]
}

View File

@ -1,26 +0,0 @@
import { docRegionChain, docRegionObservable, docRegionUnsubscribe } from './observables';
describe('observables', () => {
it('should print 2', (doneFn: DoneFn) => {
const consoleLogSpy = spyOn(console, 'log');
const observable = docRegionObservable(console);
observable.subscribe(() => {
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
expect(consoleLogSpy).toHaveBeenCalledWith(2);
doneFn();
});
});
it('should close the subscription', () => {
const subscription = docRegionUnsubscribe();
expect(subscription.closed).toBeTruthy();
});
it('should chain an observable', (doneFn: DoneFn) => {
const observable = docRegionChain();
observable.subscribe(value => {
expect(value).toBe(4);
doneFn();
});
});
});

View File

@ -1,72 +1,40 @@
// #docplaster
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
export function docRegionObservable(console: Console) {
// #docregion observable
// #docregion observable
// declare a publishing operation
const observable = new Observable<number>(observer => {
// Subscriber fn...
// #enddocregion observable
// The below code is used for unit testing only
observer.next(2);
// #docregion observable
});
// declare a publishing operation
const observable = new Observable<number>(observer => {
// Subscriber fn...
});
// initiate execution
observable.subscribe(value => {
// observer handles notifications
// #enddocregion observable
// The below code is used for unit testing only
console.log(value);
// #docregion observable
});
// initiate execution
observable.subscribe(() => {
// observer handles notifications
});
// #enddocregion observable
return observable;
}
// #enddocregion observable
export function docRegionUnsubscribe() {
const observable = new Observable<number>(() => {
// Subscriber fn...
});
// #docregion unsubscribe
// #docregion unsubscribe
const subscription = observable.subscribe(() => {
// observer handles notifications
});
const subscription = observable.subscribe(() => {
// observer handles notifications
});
subscription.unsubscribe();
subscription.unsubscribe();
// #enddocregion unsubscribe
return subscription;
}
// #enddocregion unsubscribe
export function docRegionError() {
const observable = new Observable<number>(() => {
// Subscriber fn...
});
// #docregion error
// #docregion error
observable.subscribe(() => {
throw new Error('my error');
});
// #enddocregion error
}
observable.subscribe(() => {
throw Error('my error');
});
export function docRegionChain() {
let observable = new Observable<number>(observer => {
// Subscriber fn...
observer.next(2);
});
// #enddocregion error
observable =
// #docregion chain
// #docregion chain
observable.pipe(map(v => 2 * v));
observable.pipe(map(v => 2 * v));
// #enddocregion chain
return observable;
}
// #enddocregion chain

View File

@ -1,23 +0,0 @@
import { docRegionError, docRegionPromise } from './promises';
describe('promises', () => {
it('should print 2', (doneFn: DoneFn) => {
const consoleLogSpy = spyOn(console, 'log');
const pr = docRegionPromise(console, 2);
pr.then((value) => {
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
expect(consoleLogSpy).toHaveBeenCalledWith(2);
expect(value).toBe(4);
doneFn();
});
});
it('should throw an error', (doneFn: DoneFn) => {
const promise = docRegionError();
promise
.then(() => {
throw new Error('Promise should be rejected.');
},
() => doneFn());
});
});

View File

@ -1,44 +1,25 @@
// #docplaster
// #docregion promise
// initiate execution
const promise = new Promise<number>((resolve, reject) => {
// Executer fn...
});
export function docRegionPromise(console: Console, inputValue: number) {
// #docregion promise
// initiate execution
let promise = new Promise<number>((resolve, reject) => {
// Executer fn...
// #enddocregion promise
// The below is used in the unit tests.
resolve(inputValue);
// #docregion promise
});
// #enddocregion promise
promise =
// #docregion promise
promise.then(value => {
// handle result here
// #enddocregion promise
// The below is used in the unit tests.
console.log(value);
return value;
// #docregion promise
});
// #enddocregion promise
promise =
// #docregion chain
promise.then(v => 2 * v);
// #enddocregion chain
promise.then(value => {
// handle result here
});
return promise;
}
// #enddocregion promise
export function docRegionError() {
let promise = Promise.resolve();
promise =
// #docregion error
// #docregion chain
promise.then(() => {
throw new Error('my error');
});
promise.then(v => 2 * v);
// #enddocregion error
return promise;
}
// #enddocregion chain
// #docregion error
promise.then(() => {
throw Error('my error');
});
// #enddocregion error

View File

@ -1,17 +0,0 @@
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
function notifyUser(message: string): void { }
// #docregion sw-unrecoverable-state
@Injectable()
export class HandleUnrecoverableStateService {
constructor(updates: SwUpdate) {
updates.unrecoverable.subscribe(event => {
notifyUser(
`An error occurred that we cannot recover from:\n${event.reason}\n\n` +
'Please reload the page.');
});
}
}
// #enddocregion sw-unrecoverable-state

View File

@ -38,6 +38,7 @@ v9 - v12
| `@angular/bazel` | [`Bazel builder and schematics`](#bazelbuilder) | v10 |
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
| `@angular/core` | [`CollectionChangeRecord`](#core) | <!--v7--> v11 |
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
@ -88,6 +89,7 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i
| API | Replacement | Deprecation announced | Notes |
| --- | ----------- | --------------------- | ----- |
| [`CollectionChangeRecord`](api/core/CollectionChangeRecord) | [`IterableChangeRecord`](api/core/IterableChangeRecord) | v4 | none |
| [`DefaultIterableDiffer`](api/core/DefaultIterableDiffer) | n/a | v4 | Not part of public API. |
| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) |
| [`ReflectiveKey`](api/core/ReflectiveKey) | none | v5 | none |

View File

@ -67,33 +67,6 @@ Therefore, it is recommended to reload the page once the promise returned by `ac
</div>
### Handling an unrecoverable state
In some cases, the version of the app used by the service worker to serve a client might be in a broken state that cannot be recovered from without a full page reload.
For example, imagine the following scenario:
- A user opens the app for the first time and the service worker caches the latest version of the app.
Let's assume the app's cached assets include `index.html`, `main.<main-hash-1>.js` and `lazy-chunk.<lazy-hash-1>.js`.
- The user closes the app and does not open it for a while.
- After some time, a new version of the app is deployed to the server.
This newer version includes the files `index.html`, `main.<main-hash-2>.js` and `lazy-chunk.<lazy-hash-2>.js` (note that the hashes are different now, because the content of the files has changed).
The old version is no longer available on the server.
- In the meantime, the user's browser decides to evict `lazy-chunk.<lazy-hash-1>.js` from its cache.
Browsers may decide to evict specific (or all) resources from a cache in order to reclaim disk space.
- The user opens the app again.
The service worker serves the latest version known to it at this point, namely the old version (`index.html` and `main.<main-hash-1>.js`).
- At some later point, the app requests the lazy bundle, `lazy-chunk.<lazy-hash-1>.js`.
- The service worker is unable to find the asset in the cache (remember that the browser evicted it).
Nor is it able to retrieve it from the server (since the server now only has `lazy-chunk.<lazy-hash-2>.js` from the newer version).
In the above scenario, the service worker is not able to serve an asset that would normally be cached.
That particular app version is broken and there is no way to fix the state of the client without reloading the page.
In such cases, the service worker notifies the client by sending an `UnrecoverableStateEvent` event.
You can subscribe to `SwUpdate#unrecoverable` to be notified and handle these errors.
<code-example path="service-worker-getting-started/src/app/handle-unrecoverable-state.service.ts" header="handle-unrecoverable-state.service.ts" region="sw-unrecoverable-state"></code-example>
## More on Angular service workers
You may also be interested in the following:

View File

@ -53,9 +53,6 @@
},
"kyliau": {
"name": "Keen Yee Liau",
"twitter": "liauky",
"website": "https://github.com/kyliau",
"bio": "Keen works on language service and CLI. He also maintains Karma and Protractor.",
"groups": ["Angular"],
"lead": "igorminar",
"picture": "kyliau.jpg"

View File

@ -56,35 +56,35 @@
"tooltip": "Set up your environment and learn basic concepts",
"children": [
{
"title": "Try it",
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
"children": [
{
"url": "start",
"title": "A Sample App",
"tooltip": "Take a look at Angular's component model, template syntax, and component communication."
},
{
"url": "start/start-routing",
"title": "In-app Navigation",
"tooltip": "Navigate among different page views using the browser's URL."
},
{
"url": "start/start-data",
"title": "Manage Data",
"tooltip": "Use services and access external data via HTTP."
},
{
"url": "start/start-forms",
"title": "Forms for User Input",
"tooltip": "Learn about fetching and managing data from users with forms."
},
{
"url": "start/start-deployment",
"title": "Deployment",
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
}
]
"title": "Try it",
"tooltip": "Examine and work with a ready-made sample app, with no setup.",
"children": [
{
"url": "start",
"title": "A Sample App",
"tooltip": "Take a look at Angular's component model, template syntax, and component communication."
},
{
"url": "start/start-routing",
"title": "In-app Navigation",
"tooltip": "Navigate among different page views using the browser's URL."
},
{
"url": "start/start-data",
"title": "Manage Data",
"tooltip": "Use services and access external data via HTTP."
},
{
"url": "start/start-forms",
"title": "Forms for User Input",
"tooltip": "Learn about fetching and managing data from users with forms."
},
{
"url": "start/start-deployment",
"title": "Deployment",
"tooltip": "Move to local development, or deploy your application to Firebase or your own server."
}
]
},
{
"url": "guide/setup-local",

View File

@ -23,7 +23,7 @@
"build-local-with-viewengine": "yarn ~~build",
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 32391604b",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js ef770f1cb",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
"test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn update-webdriver",

View File

@ -3,7 +3,7 @@
{%- macro renderHeritage(exportDoc) -%}
{%- if exportDoc.extendsClauses.length %} extends {% for clause in exportDoc.extendsClauses -%}
{% if clause.doc.path %}<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% else %}{$ clause.text $}{% endif %}{% if not loop.last %}, {% endif -%}
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}
{% endfor %}{% endif %}
{%- if exportDoc.implementsClauses.length %} implements {% for clause in exportDoc.implementsClauses -%}
<a class="code-anchor" href="{$ clause.doc.path $}">{$ clause.text $}</a>{% if not loop.last %}, {% endif -%}

View File

@ -4,23 +4,71 @@ Caretaker is responsible for merging PRs into the individual branches and intern
## Responsibilities
- 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)
- 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)
- 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 `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.
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.
To merge a PR run:
```
$ yarn ng-dev pr merge <pr number>
$ ./scripts/github/merge-pr 1234
```
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
The `ng-dev pr merge` tool will automatically restore to the previous git state when a merge fails.
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
```

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 `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 `PR 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

@ -154,7 +154,9 @@ available as a long-term distribution mechanism, but they are guaranteed to be a
time of the build.
You can access the artifacts for a specific CI run by going to the workflow page, clicking on the
`publish_packages_as_artifacts` job and then switching to the "ARTIFACTS" tab.
`publish_packages_as_artifacts` job and then switching to the "Artifacts" tab.
(If you happen to know the build number of the job, the URL will be something like:
`https://circleci.com/gh/angular/angular/<build-number>#artifacts`)
#### Archives for each Package
On the "Artifacts" tab, there is a list of links to compressed archives for Angular packages. The

View File

@ -125,28 +125,28 @@ 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 `action: *` label assigned to it:
Every triaged PR must have a `PR action` label assigned to it:
* `action: discuss`: Discussion is needed, to be led by the author.
* `PR action: discuss`: Discussion is needed, to be led by the author.
* _**Who adds it:** Typically the PR author._
* _**Who removes it:** Whoever added it._
* `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.
* `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.
* _**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._
* `action: cleanup`: More work is needed from the author.
* `PR 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)._
* `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".
* `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".
* _**Who adds it:** Typically the PR author._
* _**Who removes it:** Whoever added it._
In addition, PRs can have the following states:
* `state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage.
* `PR 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._
* `state: blocked`: PR is blocked on an issue or other PR. Not ready for merge.
* `PR 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._
@ -162,27 +162,13 @@ This decision is then honored when the PR is being merged by the caretaker.
To communicate the target we use the following labels:
Targeting an active release train:
* `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.
* `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.
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.
## PR Approvals
@ -196,7 +182,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 `action: merge` label means that the PR is ready for merging.
Only the `PR action: merge` label means that the PR is ready for merging.
## Special Labels
@ -215,7 +201,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)
### `action: merge-assistance`
### `PR action: merge-assistance`
* _**Who adds it:** Any team member._
* _**Who removes it:** Any team member._
@ -225,7 +211,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.
### `action: rerun CI at HEAD`
### `PR 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

@ -85,6 +85,10 @@ export declare interface ClassSansProvider {
useClass: Type<any>;
}
/** @deprecated */
export declare interface CollectionChangeRecord<V> extends IterableChangeRecord<V> {
}
export declare class Compiler {
compileModuleAndAllComponentsAsync: <T>(moduleType: Type<T>) => Promise<ModuleWithComponentFactories<T>>;
compileModuleAndAllComponentsSync: <T>(moduleType: Type<T>) => ModuleWithComponentFactories<T>;

View File

@ -29,17 +29,11 @@ export declare class SwUpdate {
readonly activated: Observable<UpdateActivatedEvent>;
readonly available: Observable<UpdateAvailableEvent>;
get isEnabled(): boolean;
readonly unrecoverable: Observable<UnrecoverableStateEvent>;
constructor(sw: ɵangular_packages_service_worker_service_worker_a);
activateUpdate(): Promise<void>;
checkForUpdate(): Promise<void>;
}
export declare interface UnrecoverableStateEvent {
reason: string;
type: 'UNRECOVERABLE_STATE';
}
export declare interface UpdateActivatedEvent {
current: {
hash: string;

View File

@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 245303,
"main-es2015": 245351,
"polyfills-es2015": 36938,
"5-es2015": 751
}
@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 221887,
"main-es2015": 221939,
"polyfills-es2015": 36723,
"5-es2015": 781
}

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "11.0.0-next.1",
"version": "10.1.1",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParseError, ParseSourceFile, parseTemplate, ParseTemplateOptions, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles';
@ -31,7 +31,7 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
const EMPTY_MAP = new Map<string, Expression>();
const EMPTY_ARRAY: any[] = [];
@ -260,7 +260,7 @@ export class ComponentDecoratorHandler implements
let diagnostics: ts.Diagnostic[]|undefined = undefined;
if (template.errors !== null) {
if (template.errors !== undefined) {
// If there are any template parsing errors, convert them to `ts.Diagnostic`s for display.
const id = getTemplateId(node);
diagnostics = template.errors.map(error => {
@ -336,11 +336,11 @@ export class ComponentDecoratorHandler implements
meta: {
...metadata,
template: {
nodes: template.nodes,
nodes: template.emitNodes,
ngContentSelectors: template.ngContentSelectors,
},
encapsulation,
interpolation: template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG,
interpolation: template.interpolation,
styles: styles || [],
// These will be replaced during the compilation step, after all `NgModule`s have been
@ -772,7 +772,7 @@ export class ComponentDecoratorHandler implements
private _parseTemplate(
component: Map<string, ts.Expression>, templateStr: string, templateUrl: string,
templateRange: LexerRange|undefined, escapedString: boolean): ParsedComponentTemplate {
templateRange: LexerRange|undefined, escapedString: boolean): ParsedTemplate {
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
if (component.has('preserveWhitespaces')) {
const expr = component.get('preserveWhitespaces')!;
@ -783,7 +783,7 @@ export class ComponentDecoratorHandler implements
preserveWhitespaces = value;
}
let interpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
let interpolation: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
if (component.has('interpolation')) {
const expr = component.get('interpolation')!;
const value = this.evaluator.evaluate(expr);
@ -792,20 +792,18 @@ export class ComponentDecoratorHandler implements
throw createValueHasWrongTypeError(
expr, value, 'interpolation must be an array with 2 elements of string type');
}
interpolationConfig = InterpolationConfig.fromArray(value as [string, string]);
interpolation = InterpolationConfig.fromArray(value as [string, string]);
}
// We always normalize line endings if the template has been escaped (i.e. is inline).
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
const parsedTemplate = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig,
range: templateRange,
escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs,
});
const {errors, nodes: emitNodes, styleUrls, styles, ngContentSelectors} =
parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange,
escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs,
});
// Unfortunately, the primary parse of the template above may not contain accurate source map
// information. If used directly, it would result in incorrect code locations in template
@ -822,17 +820,22 @@ export class ComponentDecoratorHandler implements
const {nodes: diagNodes} = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces: true,
interpolationConfig,
interpolationConfig: interpolation,
range: templateRange,
escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs,
i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs,
leadingTriviaChars: [],
});
return {
...parsedTemplate,
interpolation,
emitNodes,
diagNodes,
styleUrls,
styles,
ngContentSelectors,
errors,
template: templateStr,
templateUrl,
isInline: component.has('template'),
@ -899,7 +902,12 @@ function sourceMapUrl(resourceUrl: string): string {
* This contains the actual parsed template as well as any metadata collected during its parsing,
* some of which might be useful for re-parsing the template with different options.
*/
export interface ParsedComponentTemplate extends ParsedTemplate {
export interface ParsedTemplate {
/**
* The `InterpolationConfig` specified by the user.
*/
interpolation: InterpolationConfig;
/**
* A full path to the file which contains the template.
*
@ -909,10 +917,22 @@ export interface ParsedComponentTemplate extends ParsedTemplate {
templateUrl: string;
/**
* True if the original template was stored inline;
* False if the template was in an external file.
* The string contents of the template.
*
* This is the "logical" template string, after expansion of any escaped characters (for inline
* templates). This may differ from the actual template bytes as they appear in the .ts file.
*/
isInline: boolean;
template: string;
/**
* Any errors from parsing the template the first time.
*/
errors?: ParseError[]|undefined;
/**
* The template AST, parsed according to the user's specifications.
*/
emitNodes: TmplAstNode[];
/**
* The template AST, parsed in a manner which preserves source map information for diagnostics.
@ -921,12 +941,36 @@ export interface ParsedComponentTemplate extends ParsedTemplate {
*/
diagNodes: TmplAstNode[];
/**
*
*/
/**
* Any styleUrls extracted from the metadata.
*/
styleUrls: string[];
/**
* Any inline styles extracted from the metadata.
*/
styles: string[];
/**
* Any ng-content selectors extracted from the template.
*/
ngContentSelectors: string[];
/**
* Whether the template was inline.
*/
isInline: boolean;
/**
* The `ParseSourceFile` for the template.
*/
file: ParseSourceFile;
}
export interface ParsedTemplateWithSource extends ParsedComponentTemplate {
export interface ParsedTemplateWithSource extends ParsedTemplate {
sourceMapping: TemplateSourceMapping;
}

View File

@ -246,7 +246,7 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
name = node.name;
kind = IdentifierKind.Element;
}
const sourceSpan = node.startSourceSpan;
const {sourceSpan} = node;
// An element's or template's source span can be of the form `<element>`, `<element />`, or
// `<element></element>`. Only the selector is interesting to the indexer, so the source is
// searched for the first occurrence of the element (selector) name.

View File

@ -79,7 +79,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
leadingTriviaChars: [],
});
if (errors !== null) {
if (errors !== undefined) {
return {nodes, errors};
}

View File

@ -94,7 +94,7 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker {
}
const diag = makeTemplateDiagnostic(
id, mapping, element.startSourceSpan, ts.DiagnosticCategory.Error,
id, mapping, element.sourceSpan, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.SCHEMA_INVALID_ELEMENT), errorMsg);
this._diagnostics.push(diag);
}

View File

@ -354,7 +354,7 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
const templateUrl = `${className}.html`;
const templateFile = new ParseSourceFile(template, templateUrl);
const {nodes, errors} = parseTemplate(template, templateUrl);
if (errors !== null) {
if (errors !== undefined) {
throw new Error('Template parse errors: \n' + errors.join('\n'));
}

View File

@ -42,7 +42,7 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<msg id="5811701742971715242" desc="with ICU and other things"><source>src/icu.html:4,6</source>
foo <ph name="ICU"><ex>{ count, plural, =1 {...} other {...}}</ex>{ count, plural, =1 {...} other {...}}</ph>
</msg>
<msg id="7254052530614200029" desc="with placeholders"><source>src/placeholders.html:1,3</source>Name: <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex>&lt;b&gt;</ph><ph name="NAME"><ex>{{
<msg id="7254052530614200029" desc="with placeholders"><source>src/placeholders.html:1</source>Name: <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex>&lt;b&gt;</ph><ph name="NAME"><ex>{{
name // i18n(ph=&quot;name&quot;)
}}</ex>{{
name // i18n(ph=&quot;name&quot;)
@ -182,7 +182,7 @@ const EXPECTED_XLIFF2 = `<?xml version="1.0" encoding="UTF-8" ?>
<unit id="7254052530614200029">
<notes>
<note category="description">with placeholders</note>
<note category="location">src/placeholders.html:1,3</note>
<note category="location">src/placeholders.html:1</note>
</notes>
<segment>
<source>Name: <pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="&lt;b&gt;" dispEnd="&lt;/b&gt;"><ph id="1" equiv="NAME" disp="{{

View File

@ -342,7 +342,7 @@ runInEachFileSystem((os) => {
expect(mappings).toContain(
{source: '<h3>', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '<ng-content select="title"></ng-content>',
source: '<ng-content select="title">',
generated: 'i0.ɵɵprojection(1)',
sourceUrl: '../test.ts'
});
@ -351,7 +351,7 @@ runInEachFileSystem((os) => {
expect(mappings).toContain(
{source: '<div>', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
expect(mappings).toContain({
source: '<ng-content></ng-content>',
source: '<ng-content>',
generated: 'i0.ɵɵprojection(3, 1)',
sourceUrl: '../test.ts'
});

View File

@ -99,7 +99,7 @@ export {Identifiers as R3Identifiers} from './render3/r3_identifiers';
export {R3DependencyMetadata, R3ResolvedDependencyType, compileFactoryFunction, R3FactoryMetadata, R3FactoryTarget} from './render3/r3_factory';
export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
export {makeBindingParser, ParsedTemplate, parseTemplate, ParseTemplateOptions} from './render3/view/template';
export {makeBindingParser, parseTemplate, ParseTemplateOptions} from './render3/view/template';
export {R3Reference} from './render3/util';
export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler';
export {publishFacade} from './jit_compiler_facade';

View File

@ -124,7 +124,7 @@ class _Visitor implements html.Visitor {
this._translations = translations;
// Construct a single fake root element
const wrapper = new html.Element('wrapper', [], nodes, undefined!, undefined!, undefined);
const wrapper = new html.Element('wrapper', [], nodes, undefined!, undefined, undefined);
const translatedNode = wrapper.visit(this, null);
@ -492,7 +492,7 @@ class _Visitor implements html.Visitor {
}
private _reportError(node: html.Node, msg: string): void {
this._errors.push(new I18nError(node.sourceSpan, msg));
this._errors.push(new I18nError(node.sourceSpan!, msg));
}
}

View File

@ -83,7 +83,7 @@ class _I18nVisitor implements html.Visitor {
const isVoid: boolean = getHtmlTagDefinition(el.name).isVoid;
const startPhName =
context.placeholderRegistry.getStartTagPlaceholderName(el.name, attrs, isVoid);
context.placeholderToContent[startPhName] = el.startSourceSpan.toString();
context.placeholderToContent[startPhName] = el.sourceSpan!.toString();
let closePhName = '';
@ -104,7 +104,7 @@ class _I18nVisitor implements html.Visitor {
}
visitText(text: html.Text, context: I18nMessageVisitorContext): i18n.Node {
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan, context);
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan!, context);
return context.visitNodeFn(text, node);
}

View File

@ -231,9 +231,9 @@ class XliffParser implements ml.Visitor {
break;
case _TARGET_TAG:
const innerTextStart = element.startSourceSpan.end.offset;
const innerTextStart = element.startSourceSpan!.end.offset;
const innerTextEnd = element.endSourceSpan!.start.offset;
const content = element.startSourceSpan.start.file.content;
const content = element.startSourceSpan!.start.file.content;
const innerText = content.slice(innerTextStart, innerTextEnd);
this._unitMlString = innerText;
break;
@ -264,7 +264,7 @@ class XliffParser implements ml.Visitor {
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
this._errors.push(new I18nError(node.sourceSpan!, message));
}
}
@ -288,14 +288,14 @@ class XmlToI18n implements ml.Visitor {
}
visitText(text: ml.Text, context: any) {
return new i18n.Text(text.value, text.sourceSpan);
return new i18n.Text(text.value, text.sourceSpan!);
}
visitElement(el: ml.Element, context: any): i18n.Placeholder|ml.Node[]|null {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan!);
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
@ -332,7 +332,7 @@ class XmlToI18n implements ml.Visitor {
visitAttribute(attribute: ml.Attribute, context: any) {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
this._errors.push(new I18nError(node.sourceSpan!, message));
}
}

View File

@ -246,9 +246,9 @@ class Xliff2Parser implements ml.Visitor {
break;
case _TARGET_TAG:
const innerTextStart = element.startSourceSpan.end.offset;
const innerTextStart = element.startSourceSpan!.end.offset;
const innerTextEnd = element.endSourceSpan!.start.offset;
const content = element.startSourceSpan.start.file.content;
const content = element.startSourceSpan!.start.file.content;
const innerText = content.slice(innerTextStart, innerTextEnd);
this._unitMlString = innerText;
break;

View File

@ -130,9 +130,9 @@ class XtbParser implements ml.Visitor {
if (this._msgIdToHtml.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
} else {
const innerTextStart = element.startSourceSpan.end.offset;
const innerTextStart = element.startSourceSpan!.end.offset;
const innerTextEnd = element.endSourceSpan!.start.offset;
const content = element.startSourceSpan.start.file.content;
const content = element.startSourceSpan!.start.file.content;
const innerText = content.slice(innerTextStart!, innerTextEnd!);
this._msgIdToHtml[id] = innerText;
}
@ -155,7 +155,7 @@ class XtbParser implements ml.Visitor {
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
this._errors.push(new I18nError(node.sourceSpan!, message));
}
}
@ -179,7 +179,7 @@ class XmlToI18n implements ml.Visitor {
}
visitText(text: ml.Text, context: any) {
return new i18n.Text(text.value, text.sourceSpan);
return new i18n.Text(text.value, text.sourceSpan!);
}
visitExpansion(icu: ml.Expansion, context: any) {
@ -203,7 +203,7 @@ class XmlToI18n implements ml.Visitor {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan!);
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
@ -218,6 +218,6 @@ class XmlToI18n implements ml.Visitor {
visitAttribute(attribute: ml.Attribute, context: any) {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
this._errors.push(new I18nError(node.sourceSpan!, message));
}
}

View File

@ -129,7 +129,7 @@ export class CompilerFacadeImpl implements CompilerFacade {
const template = parseTemplate(
facade.template, sourceMapUrl,
{preserveWhitespaces: facade.preserveWhitespaces, interpolationConfig});
if (template.errors !== null) {
if (template.errors !== undefined) {
const errors = template.errors.map(err => err.toString()).join(', ');
throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`);
}

View File

@ -64,7 +64,7 @@ export class Attribute extends NodeWithI18n {
export class Element extends NodeWithI18n {
constructor(
public name: string, public attrs: Attribute[], public children: Node[],
sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null = null,
public endSourceSpan: ParseSourceSpan|null = null, i18n?: I18nMeta) {
super(sourceSpan, i18n);
}

View File

@ -642,11 +642,13 @@ class _Tokenizer {
this._beginToken(TokenType.RAW_TEXT);
const condition = this._readUntil(chars.$COMMA);
const normalizedCondition = this._processCarriageReturns(condition);
if (this._i18nNormalizeLineEndingsInICUs) {
// We explicitly want to normalize line endings for this text.
if (this._escapedString || this._i18nNormalizeLineEndingsInICUs) {
// Either the template is inline or,
// we explicitly want to normalize line endings for this text.
this._endToken([normalizedCondition]);
} else {
// We are not normalizing line endings.
// The expression is in an external template and, for backward compatibility,
// we are not normalizing line endings.
const conditionToken = this._endToken([condition]);
if (normalizedCondition !== condition) {
this.nonNormalizedIcuExpressions.push(conditionToken);

View File

@ -258,9 +258,7 @@ class _TreeBuilder {
}
const end = this._peek.sourceSpan.start;
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
const startSpan = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
const el = new html.Element(fullName, attrs, [], span, startSpan, undefined);
const el = new html.Element(fullName, attrs, [], span, span, undefined);
this._pushElement(el);
if (selfClosing) {
// Elements that are self-closed have their `endSourceSpan` set to the full span, as the
@ -303,7 +301,6 @@ class _TreeBuilder {
// removed from the element stack at this point are closed implicitly, so they won't get
// an end source span (as there is no explicit closing element).
el.endSourceSpan = endSourceSpan;
el.sourceSpan.end = endSourceSpan.end || el.sourceSpan.end;
this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex);
return true;

View File

@ -79,8 +79,13 @@ export class Element implements Node {
constructor(
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {}
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {
// If the element is empty then the source span should include any closing tag
if (children.length === 0 && startSourceSpan && endSourceSpan) {
this.sourceSpan = new ParseSourceSpan(sourceSpan.start, endSourceSpan.end);
}
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitElement(this);
}
@ -91,7 +96,7 @@ export class Template implements Node {
public tagName: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public templateAttrs: (BoundAttribute|TextAttribute)[],
public children: Node[], public references: Reference[], public variables: Variable[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitTemplate(this);

View File

@ -544,7 +544,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) {
this._namespace = nsInstruction;
this.creationInstruction(element.startSourceSpan, nsInstruction);
this.creationInstruction(element.sourceSpan, nsInstruction);
}
/**
@ -671,16 +671,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
trimTrailingNulls(parameters));
} else {
this.creationInstruction(
element.startSourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
element.sourceSpan, isNgContainer ? R3.elementContainerStart : R3.elementStart,
trimTrailingNulls(parameters));
if (isNonBindableMode) {
this.creationInstruction(element.startSourceSpan, R3.disableBindings);
this.creationInstruction(element.sourceSpan, R3.disableBindings);
}
if (i18nAttrs.length > 0) {
this.i18nAttributesInstruction(
elementIndex, i18nAttrs, element.startSourceSpan ?? element.sourceSpan);
this.i18nAttributesInstruction(elementIndex, i18nAttrs, element.sourceSpan);
}
// Generate Listeners (outputs)
@ -696,7 +695,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Note: it's important to keep i18n/i18nStart instructions after i18nAttributes and
// listeners, to make sure i18nAttributes instruction targets current element at runtime.
if (isI18nRootElement) {
this.i18nStart(element.startSourceSpan, element.i18n!, createSelfClosingI18nInstruction);
this.i18nStart(element.sourceSpan, element.i18n!, createSelfClosingI18nInstruction);
}
}
@ -828,7 +827,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (!createSelfClosingInstruction) {
// Finish element construction mode.
const span = element.endSourceSpan ?? element.sourceSpan;
const span = element.endSourceSpan || element.sourceSpan;
if (isI18nRootElement) {
this.i18nEnd(span, createSelfClosingI18nInstruction);
}
@ -920,8 +919,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// elements, in case of inline templates, corresponding instructions will be generated in the
// nested template function.
if (i18nAttrs.length > 0) {
this.i18nAttributesInstruction(
templateIndex, i18nAttrs, template.startSourceSpan ?? template.sourceSpan);
this.i18nAttributesInstruction(templateIndex, i18nAttrs, template.sourceSpan);
}
// Add the input bindings
@ -2025,7 +2023,13 @@ export interface ParseTemplateOptions {
* @param options options to modify how the template is parsed
*/
export function parseTemplate(
template: string, templateUrl: string, options: ParseTemplateOptions = {}): ParsedTemplate {
template: string, templateUrl: string, options: ParseTemplateOptions = {}): {
errors?: ParseError[],
nodes: t.Node[],
styleUrls: string[],
styles: string[],
ngContentSelectors: string[]
} {
const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options;
const bindingParser = makeBindingParser(interpolationConfig);
const htmlParser = new HtmlParser();
@ -2035,9 +2039,6 @@ export function parseTemplate(
if (parseResult.errors && parseResult.errors.length > 0) {
return {
interpolationConfig,
preserveWhitespaces,
template,
errors: parseResult.errors,
nodes: [],
styleUrls: [],
@ -2073,28 +2074,10 @@ export function parseTemplate(
const {nodes, errors, styleUrls, styles, ngContentSelectors} =
htmlAstToRender3Ast(rootNodes, bindingParser);
if (errors && errors.length > 0) {
return {
interpolationConfig,
preserveWhitespaces,
template,
errors,
nodes: [],
styleUrls: [],
styles: [],
ngContentSelectors: []
};
return {errors, nodes: [], styleUrls: [], styles: [], ngContentSelectors: []};
}
return {
interpolationConfig,
preserveWhitespaces,
errors: null,
template,
nodes,
styleUrls,
styles,
ngContentSelectors
};
return {nodes, styleUrls, styles, ngContentSelectors};
}
const elementRegistry = new DomElementSchemaRegistry();
@ -2211,54 +2194,3 @@ function createClosureModeGuard(): o.BinaryOperatorExpr {
.notIdentical(o.literal('undefined', o.STRING_TYPE))
.and(o.variable(NG_I18N_CLOSURE_MODE));
}
/**
* Information about the template which was extracted during parsing.
*
* This contains the actual parsed template as well as any metadata collected during its parsing,
* some of which might be useful for re-parsing the template with different options.
*/
export interface ParsedTemplate {
/**
* Include whitespace nodes in the parsed output.
*/
preserveWhitespaces?: boolean;
/**
* How to parse interpolation markers.
*/
interpolationConfig?: InterpolationConfig;
/**
* The string contents of the template.
*
* This is the "logical" template string, after expansion of any escaped characters (for inline
* templates). This may differ from the actual template bytes as they appear in the .ts file.
*/
template: string;
/**
* Any errors from parsing the template the first time.
*/
errors: ParseError[]|null;
/**
* The template AST, parsed from the template.
*/
nodes: t.Node[];
/**
* Any styleUrls extracted from the metadata.
*/
styleUrls: string[];
/**
* Any inline styles extracted from the metadata.
*/
styles: string[];
/**
* Any ng-content selectors extracted from the template.
*/
ngContentSelectors: string[];
}

View File

@ -244,9 +244,9 @@ class TemplateParseVisitor implements html.Visitor {
visitText(text: html.Text, parent: ElementContext): any {
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR())!;
const valueNoNgsp = replaceNgsp(text.value);
const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan);
return expr ? new t.BoundTextAst(expr, ngContentIndex, text.sourceSpan) :
new t.TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan);
const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan!);
return expr ? new t.BoundTextAst(expr, ngContentIndex, text.sourceSpan!) :
new t.TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan!);
}
visitAttribute(attribute: html.Attribute, context: any): any {
@ -335,14 +335,14 @@ class TemplateParseVisitor implements html.Visitor {
const boundDirectivePropNames = new Set<string>();
const directiveAsts = this._createDirectiveAsts(
isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
elementOrDirectiveRefs, element.sourceSpan, references, boundDirectivePropNames);
elementOrDirectiveRefs, element.sourceSpan!, references, boundDirectivePropNames);
const elementProps: t.BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, elementOrDirectiveProps, boundDirectivePropNames);
const isViewRoot = parent.isTemplateElement || hasInlineTemplates;
const providerContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext!, isViewRoot, directiveAsts, attrs,
references, isTemplateElement, queryStartIndex, element.sourceSpan);
references, isTemplateElement, queryStartIndex, element.sourceSpan!);
const children: t.TemplateAst[] = html.visitAll(
preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
@ -360,26 +360,26 @@ class TemplateParseVisitor implements html.Visitor {
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
// `<ng-content>` element
if (element.children && !element.children.every(_isEmptyTextNode)) {
this._reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
this._reportError(`<ng-content> element cannot have content.`, element.sourceSpan!);
}
parsedElement = new t.NgContentAst(
this.ngContentCount++, hasInlineTemplates ? null! : ngContentIndex, element.sourceSpan);
this.ngContentCount++, hasInlineTemplates ? null! : ngContentIndex, element.sourceSpan!);
} else if (isTemplateElement) {
// `<ng-template>` element
this._assertAllEventsPublishedByDirectives(directiveAsts, events);
this._assertNoComponentsNorElementBindingsOnTemplate(
directiveAsts, elementProps, element.sourceSpan);
directiveAsts, elementProps, element.sourceSpan!);
parsedElement = new t.EmbeddedTemplateAst(
attrs, events, references, elementVars, providerContext.transformedDirectiveAsts,
providerContext.transformProviders, providerContext.transformedHasViewContainer,
providerContext.queryMatches, children, hasInlineTemplates ? null! : ngContentIndex,
element.sourceSpan);
element.sourceSpan!);
} else {
// element other than `<ng-content>` and `<ng-template>`
this._assertElementExists(matchElement, element);
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan);
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan!);
const ngContentIndex =
hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector);
@ -397,22 +397,22 @@ class TemplateParseVisitor implements html.Visitor {
const {directives} = this._parseDirectives(this.selectorMatcher, templateSelector);
const templateBoundDirectivePropNames = new Set<string>();
const templateDirectiveAsts = this._createDirectiveAsts(
true, elName, directives, templateElementOrDirectiveProps, [], element.sourceSpan, [],
true, elName, directives, templateElementOrDirectiveProps, [], element.sourceSpan!, [],
templateBoundDirectivePropNames);
const templateElementProps: t.BoundElementPropertyAst[] = this._createElementPropertyAsts(
elName, templateElementOrDirectiveProps, templateBoundDirectivePropNames);
this._assertNoComponentsNorElementBindingsOnTemplate(
templateDirectiveAsts, templateElementProps, element.sourceSpan);
templateDirectiveAsts, templateElementProps, element.sourceSpan!);
const templateProviderContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext!, parent.isTemplateElement,
templateDirectiveAsts, [], [], true, templateQueryStartIndex, element.sourceSpan);
templateDirectiveAsts, [], [], true, templateQueryStartIndex, element.sourceSpan!);
templateProviderContext.afterElement();
parsedElement = new t.EmbeddedTemplateAst(
[], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts,
templateProviderContext.transformProviders,
templateProviderContext.transformedHasViewContainer, templateProviderContext.queryMatches,
[parsedElement], ngContentIndex, element.sourceSpan);
[parsedElement], ngContentIndex, element.sourceSpan!);
}
return parsedElement;
@ -707,7 +707,7 @@ class TemplateParseVisitor implements html.Visitor {
errorMsg +=
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
}
this._reportError(errorMsg, element.sourceSpan);
this._reportError(errorMsg, element.sourceSpan!);
}
}
@ -815,7 +815,7 @@ class NonBindableVisitor implements html.Visitor {
visitText(text: html.Text, parent: ElementContext): t.TextAst {
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR())!;
return new t.TextAst(text.value, ngContentIndex, text.sourceSpan);
return new t.TextAst(text.value, ngContentIndex, text.sourceSpan!);
}
visitExpansion(expansion: html.Expansion, context: any): any {

View File

@ -322,28 +322,19 @@ import {serializeNodes as serializeHtmlNodes} from '../ml_parser/util/util';
describe('elements', () => {
it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n></b>'
],
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n></b>'
],
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n></b>'
],
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
});
@ -379,7 +370,7 @@ import {serializeNodes as serializeHtmlNodes} from '../ml_parser/util/util';
it('should report when start and end of a block are not at the same level', () => {
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<p><!-- /i18n --></p>'],
['Unclosed block', '<p>'],
]);
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([

View File

@ -42,7 +42,7 @@ class _Humanizer implements html.Visitor {
visitElement(element: html.Element, context: any): any {
const res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
if (this.includeSourceSpan) {
res.push(element.startSourceSpan.toString() ?? null);
res.push(element.startSourceSpan?.toString() ?? null);
res.push(element.endSourceSpan?.toString() ?? null);
}
this.result.push(res);
@ -82,7 +82,7 @@ class _Humanizer implements html.Visitor {
private _appendContext(ast: html.Node, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
input.push(ast.sourceSpan!.toString());
return input;
}
}

View File

@ -332,75 +332,40 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
]);
});
it('should normalize line-endings in expansion forms in inline templates if `i18nNormalizeLineEndingsInICUs` is true',
() => {
const parsed = parser.parse(
`<div>\r\n` +
` {\r\n` +
` messages.length,\r\n` +
` plural,\r\n` +
` =0 {You have \r\nno\r\n messages}\r\n` +
` =1 {One {{message}}}}\r\n` +
`</div>`,
'TestComp', {
tokenizeExpansionForms: true,
escapedString: true,
i18nNormalizeLineEndingsInICUs: true,
});
it('should normalize line-endings in expansion forms in inline templates', () => {
const parsed = parser.parse(
`<div>\r\n` +
` {\r\n` +
` messages.length,\r\n` +
` plural,\r\n` +
` =0 {You have \r\nno\r\n messages}\r\n` +
` =1 {One {{message}}}}\r\n` +
`</div>`,
'TestComp', {
tokenizeExpansionForms: true,
escapedString: true,
});
expect(humanizeDom(parsed)).toEqual([
[html.Element, 'div', 0],
[html.Text, '\n ', 1],
[html.Expansion, '\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1],
]);
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(parsed)).toEqual([
[html.Element, 'div', 0],
[html.Text, '\n ', 1],
[html.Expansion, '\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1],
]);
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0],
]);
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0]
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0]
]);
expect(parsed.errors).toEqual([]);
});
it('should not normalize line-endings in ICU expressions in external templates when `i18nNormalizeLineEndingsInICUs` is not set',
() => {
const parsed = parser.parse(
`<div>\r\n` +
` {\r\n` +
` messages.length,\r\n` +
` plural,\r\n` +
` =0 {You have \r\nno\r\n messages}\r\n` +
` =1 {One {{message}}}}\r\n` +
`</div>`,
'TestComp', {tokenizeExpansionForms: true, escapedString: true});
expect(humanizeDom(parsed)).toEqual([
[html.Element, 'div', 0],
[html.Text, '\n ', 1],
[html.Expansion, '\r\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1],
]);
const cases = (<any>parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0]
]);
expect(parsed.errors).toEqual([]);
});
expect(parsed.errors).toEqual([]);
});
it('should normalize line-endings in expansion forms in external templates if `i18nNormalizeLineEndingsInICUs` is true',
() => {
@ -503,67 +468,33 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
]);
});
it('should normalize line endings in nested expansion forms for inline templates, when `i18nNormalizeLineEndingsInICUs` is true',
() => {
const parsed = parser.parse(
`{\r\n` +
` messages.length, plural,\r\n` +
` =0 { zero \r\n` +
` {\r\n` +
` p.gender, select,\r\n` +
` male {m}\r\n` +
` }\r\n` +
` }\r\n` +
`}`,
'TestComp', {
tokenizeExpansionForms: true,
escapedString: true,
i18nNormalizeLineEndingsInICUs: true
});
expect(humanizeDom(parsed)).toEqual([
[html.Expansion, '\n messages.length', 'plural', 0],
[html.ExpansionCase, '=0', 1],
]);
it('should normalize line endings in nested expansion forms for inline templates', () => {
const parsed = parser.parse(
`{\r\n` +
` messages.length, plural,\r\n` +
` =0 { zero \r\n` +
` {\r\n` +
` p.gender, select,\r\n` +
` male {m}\r\n` +
` }\r\n` +
` }\r\n` +
`}`,
'TestComp', {tokenizeExpansionForms: true, escapedString: true});
expect(humanizeDom(parsed)).toEqual([
[html.Expansion, '\n messages.length', 'plural', 0],
[html.ExpansionCase, '=0', 1],
]);
const expansion = parsed.rootNodes[0] as html.Expansion;
expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([
[html.Text, 'zero \n ', 0],
[html.Expansion, '\n p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, '\n ', 0],
]);
const expansion = parsed.rootNodes[0] as html.Expansion;
expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([
[html.Text, 'zero \n ', 0],
[html.Expansion, '\n p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, '\n ', 0],
]);
expect(parsed.errors).toEqual([]);
});
it('should not normalize line endings in nested expansion forms for inline templates, when `i18nNormalizeLineEndingsInICUs` is not defined',
() => {
const parsed = parser.parse(
`{\r\n` +
` messages.length, plural,\r\n` +
` =0 { zero \r\n` +
` {\r\n` +
` p.gender, select,\r\n` +
` male {m}\r\n` +
` }\r\n` +
` }\r\n` +
`}`,
'TestComp', {tokenizeExpansionForms: true, escapedString: true});
expect(humanizeDom(parsed)).toEqual([
[html.Expansion, '\r\n messages.length', 'plural', 0],
[html.ExpansionCase, '=0', 1],
]);
const expansion = parsed.rootNodes[0] as html.Expansion;
expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([
[html.Text, 'zero \n ', 0],
[html.Expansion, '\r\n p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, '\n ', 0],
]);
expect(parsed.errors).toEqual([]);
});
expect(parsed.errors).toEqual([]);
});
it('should not normalize line endings in nested expansion forms for external templates, when `i18nNormalizeLineEndingsInICUs` is not set',
() => {
@ -653,8 +584,7 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
.toEqual([
[
html.Element, 'div', 0,
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>',
html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>',
'<div [prop]="v1" (e)="do()" attr="v2" noValue>', '</div>'
],
[html.Attribute, '[prop]', 'v1', '[prop]="v1"'],
@ -668,8 +598,8 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
it('should set the start and end source spans', () => {
const node = <html.Element>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
expect(node.startSourceSpan.start.offset).toEqual(0);
expect(node.startSourceSpan.end.offset).toEqual(5);
expect(node.startSourceSpan!.start.offset).toEqual(0);
expect(node.startSourceSpan!.end.offset).toEqual(5);
expect(node.endSourceSpan!.start.offset).toEqual(6);
expect(node.endSourceSpan!.end.offset).toEqual(12);
@ -677,14 +607,14 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
it('should not set the end source span for void elements', () => {
expect(humanizeDomSourceSpans(parser.parse('<div><br></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0, '<div><br></div>', '<div>', '</div>'],
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
[html.Element, 'br', 1, '<br>', '<br>', null],
]);
});
it('should not set the end source span for multiple void elements', () => {
expect(humanizeDomSourceSpans(parser.parse('<div><br><hr></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0, '<div><br><hr></div>', '<div>', '</div>'],
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
[html.Element, 'br', 1, '<br>', '<br>', null],
[html.Element, 'hr', 1, '<hr>', '<hr>', null],
]);
@ -704,19 +634,19 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
it('should set the end source span for self-closing elements', () => {
expect(humanizeDomSourceSpans(parser.parse('<div><br/></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0, '<div><br/></div>', '<div>', '</div>'],
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
[html.Element, 'br', 1, '<br/>', '<br/>', '<br/>'],
]);
});
it('should not set the end source span for elements that are implicitly closed', () => {
expect(humanizeDomSourceSpans(parser.parse('<div><p></div>', 'TestComp'))).toEqual([
[html.Element, 'div', 0, '<div><p></div>', '<div>', '</div>'],
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
[html.Element, 'p', 1, '<p>', '<p>', null],
]);
expect(humanizeDomSourceSpans(parser.parse('<div><li>A<li>B</div>', 'TestComp')))
.toEqual([
[html.Element, 'div', 0, '<div><li>A<li>B</div>', '<div>', '</div>'],
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
[html.Element, 'li', 1, '<li>', '<li>', null],
[html.Text, 'A', 2, 'A'],
[html.Element, 'li', 1, '<li>', '<li>', null],
@ -729,7 +659,7 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spe
'<div>{count, plural, =0 {msg}}</div>', 'TestComp',
{tokenizeExpansionForms: true})))
.toEqual([
[html.Element, 'div', 0, '<div>{count, plural, =0 {msg}}</div>', '<div>', '</div>'],
[html.Element, 'div', 0, '<div>', '<div>', '</div>'],
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
]);

View File

@ -56,10 +56,10 @@ import {humanizeNodes} from './ast_spec_utils';
const nodes = expand(`{messages.length, plural,=0 {<b>bold</b>}}`).nodes;
const container: html.Element = <html.Element>nodes[0];
expect(container.sourceSpan.start.col).toEqual(0);
expect(container.sourceSpan.end.col).toEqual(42);
expect(container.startSourceSpan.start.col).toEqual(0);
expect(container.startSourceSpan.end.col).toEqual(42);
expect(container.sourceSpan!.start.col).toEqual(0);
expect(container.sourceSpan!.end.col).toEqual(42);
expect(container.startSourceSpan!.start.col).toEqual(0);
expect(container.startSourceSpan!.end.col).toEqual(42);
expect(container.endSourceSpan!.start.col).toEqual(0);
expect(container.endSourceSpan!.end.col).toEqual(42);
@ -68,15 +68,15 @@ import {humanizeNodes} from './ast_spec_utils';
expect(switchExp.sourceSpan.end.col).toEqual(16);
const template: html.Element = <html.Element>container.children[0];
expect(template.sourceSpan.start.col).toEqual(25);
expect(template.sourceSpan.end.col).toEqual(41);
expect(template.sourceSpan!.start.col).toEqual(25);
expect(template.sourceSpan!.end.col).toEqual(41);
const switchCheck = template.attrs[0];
expect(switchCheck.sourceSpan.start.col).toEqual(25);
expect(switchCheck.sourceSpan.end.col).toEqual(28);
const b: html.Element = <html.Element>template.children[0];
expect(b.sourceSpan.start.col).toEqual(29);
expect(b.sourceSpan!.start.col).toEqual(29);
expect(b.endSourceSpan!.end.col).toEqual(40);
});

View File

@ -885,114 +885,75 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u
describe('[line ending normalization', () => {
describe('{escapedString: true}', () => {
it('should normalize line-endings in expansion forms if `i18nNormalizeLineEndingsInICUs` is true',
() => {
const result = tokenizeWithoutErrors(
`{\r\n` +
` messages.length,\r\n` +
` plural,\r\n` +
` =0 {You have \r\nno\r\n messages}\r\n` +
` =1 {One {{message}}}}\r\n`,
{
tokenizeExpansionForms: true,
escapedString: true,
i18nNormalizeLineEndingsInICUs: true
});
it('should normalize line-endings in expansion forms', () => {
const result = tokenizeWithoutErrors(
`{\r\n` +
` messages.length,\r\n` +
` plural,\r\n` +
` =0 {You have \r\nno\r\n messages}\r\n` +
` =1 {One {{message}}}}\r\n`,
{
tokenizeExpansionForms: true,
escapedString: true,
});
expect(humanizeParts(result.tokens)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\n messages.length'],
[lex.TokenType.RAW_TEXT, 'plural'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'You have \nno\n messages'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, '=1'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'One {{message}}'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, '\n'],
[lex.TokenType.EOF],
]);
expect(humanizeParts(result.tokens)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\n messages.length'],
[lex.TokenType.RAW_TEXT, 'plural'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'You have \nno\n messages'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, '=1'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'One {{message}}'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, '\n'],
[lex.TokenType.EOF],
]);
expect(result.nonNormalizedIcuExpressions).toEqual([]);
});
expect(result.nonNormalizedIcuExpressions).toEqual([]);
});
it('should not normalize line-endings in ICU expressions when `i18nNormalizeLineEndingsInICUs` is not defined',
() => {
const result = tokenizeWithoutErrors(
`{\r\n` +
` messages.length,\r\n` +
` plural,\r\n` +
` =0 {You have \r\nno\r\n messages}\r\n` +
` =1 {One {{message}}}}\r\n`,
{tokenizeExpansionForms: true, escapedString: true});
it('should normalize line endings in nested expansion forms for inline templates', () => {
const result = tokenizeWithoutErrors(
`{\r\n` +
` messages.length, plural,\r\n` +
` =0 { zero \r\n` +
` {\r\n` +
` p.gender, select,\r\n` +
` male {m}\r\n` +
` }\r\n` +
` }\r\n` +
`}`,
{tokenizeExpansionForms: true, escapedString: true});
expect(humanizeParts(result.tokens)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\n messages.length'],
[lex.TokenType.RAW_TEXT, 'plural'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'zero \n '],
expect(humanizeParts(result.tokens)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\r\n messages.length'],
[lex.TokenType.RAW_TEXT, 'plural'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'You have \nno\n messages'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, '=1'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'One {{message}}'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, '\n'],
[lex.TokenType.EOF],
]);
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\n p.gender'],
[lex.TokenType.RAW_TEXT, 'select'],
[lex.TokenType.EXPANSION_CASE_VALUE, 'male'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'm'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
expect(result.nonNormalizedIcuExpressions!.length).toBe(1);
expect(result.nonNormalizedIcuExpressions![0].sourceSpan.toString())
.toEqual('\r\n messages.length');
});
[lex.TokenType.TEXT, '\n '],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
it('should not normalize line endings in nested expansion forms when `i18nNormalizeLineEndingsInICUs` is not defined',
() => {
const result = tokenizeWithoutErrors(
`{\r\n` +
` messages.length, plural,\r\n` +
` =0 { zero \r\n` +
` {\r\n` +
` p.gender, select,\r\n` +
` male {m}\r\n` +
` }\r\n` +
` }\r\n` +
`}`,
{tokenizeExpansionForms: true, escapedString: true});
expect(humanizeParts(result.tokens)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\r\n messages.length'],
[lex.TokenType.RAW_TEXT, 'plural'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=0'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'zero \n '],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, '\r\n p.gender'],
[lex.TokenType.RAW_TEXT, 'select'],
[lex.TokenType.EXPANSION_CASE_VALUE, 'male'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'm'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, '\n '],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
expect(result.nonNormalizedIcuExpressions!.length).toBe(2);
expect(result.nonNormalizedIcuExpressions![0].sourceSpan.toString())
.toEqual('\r\n messages.length');
expect(result.nonNormalizedIcuExpressions![1].sourceSpan.toString())
.toEqual('\r\n p.gender');
});
expect(result.nonNormalizedIcuExpressions).toEqual([]);
});
});
describe('{escapedString: false}', () => {

View File

@ -173,7 +173,7 @@ describe('R3 AST source spans', () => {
describe('templates', () => {
it('is correct for * directives', () => {
expectFromHtml('<div *ngIf></div>').toEqual([
['Template', '0:17', '0:11', '11:17'],
['Template', '0:11', '0:11', '11:17'],
['TextAttribute', '5:10', '<empty>'],
['Element', '0:17', '0:11', '11:17'],
]);
@ -181,48 +181,48 @@ describe('R3 AST source spans', () => {
it('is correct for <ng-template>', () => {
expectFromHtml('<ng-template></ng-template>').toEqual([
['Template', '0:27', '0:13', '13:27'],
['Template', '0:13', '0:13', '13:27'],
]);
});
it('is correct for reference via #...', () => {
expectFromHtml('<ng-template #a></ng-template>').toEqual([
['Template', '0:30', '0:16', '16:30'],
['Template', '0:16', '0:16', '16:30'],
['Reference', '13:15', '<empty>'],
]);
});
it('is correct for reference with name', () => {
expectFromHtml('<ng-template #a="b"></ng-template>').toEqual([
['Template', '0:34', '0:20', '20:34'],
['Template', '0:20', '0:20', '20:34'],
['Reference', '13:19', '17:18'],
]);
});
it('is correct for reference via ref-...', () => {
expectFromHtml('<ng-template ref-a></ng-template>').toEqual([
['Template', '0:33', '0:19', '19:33'],
['Template', '0:19', '0:19', '19:33'],
['Reference', '13:18', '<empty>'],
]);
});
it('is correct for variables via let-...', () => {
expectFromHtml('<ng-template let-a="b"></ng-template>').toEqual([
['Template', '0:37', '0:23', '23:37'],
['Template', '0:23', '0:23', '23:37'],
['Variable', '13:22', '20:21'],
]);
});
it('is correct for attributes', () => {
expectFromHtml('<ng-template k1="v1"></ng-template>').toEqual([
['Template', '0:35', '0:21', '21:35'],
['Template', '0:21', '0:21', '21:35'],
['TextAttribute', '13:20', '17:19'],
]);
});
it('is correct for bound attributes', () => {
expectFromHtml('<ng-template [k1]="v1"></ng-template>').toEqual([
['Template', '0:37', '0:23', '23:37'],
['Template', '0:23', '0:23', '23:37'],
['BoundAttribute', '13:22', '19:21'],
]);
});
@ -236,7 +236,7 @@ describe('R3 AST source spans', () => {
// <div></div>
// </ng-template>
expectFromHtml('<div *ngFor="let item of items"></div>').toEqual([
['Template', '0:38', '0:32', '32:38'],
['Template', '0:32', '0:32', '32:38'],
['TextAttribute', '5:31', '<empty>'],
['BoundAttribute', '5:31', '25:30'], // *ngFor="let item of items" -> items
['Variable', '13:22', '<empty>'], // let item
@ -250,7 +250,7 @@ describe('R3 AST source spans', () => {
// <div></div>
// </ng-template>
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
['Template', '0:34', '0:28', '28:34'],
['Template', '0:28', '0:28', '28:34'],
['BoundAttribute', '5:27', '13:17'], // ngFor="item of items" -> item
['BoundAttribute', '5:27', '21:26'], // ngFor="item of items" -> items
['Element', '0:34', '0:28', '28:34'],
@ -259,7 +259,7 @@ describe('R3 AST source spans', () => {
it('is correct for variables via let ...', () => {
expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([
['Template', '0:27', '0:21', '21:27'],
['Template', '0:21', '0:21', '21:27'],
['TextAttribute', '5:20', '<empty>'],
['Variable', '12:19', '18:19'], // let a=b -> b
['Element', '0:27', '0:21', '21:27'],
@ -268,7 +268,7 @@ describe('R3 AST source spans', () => {
it('is correct for variables via as ...', () => {
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
['Template', '0:33', '0:27', '27:33'],
['Template', '0:27', '0:27', '27:33'],
['BoundAttribute', '5:26', '12:16'], // ngIf="expr as local" -> expr
['Variable', '6:25', '6:10'], // ngIf="expr as local -> ngIf
['Element', '0:33', '0:27', '27:33'],

View File

@ -140,7 +140,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
private _appendSourceSpan(ast: TemplateAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
input.push(ast.sourceSpan!.toString());
return input;
}
}
@ -2046,7 +2046,7 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should support embedded template', () => {
expect(humanizeTplAstSourceSpans(parse('<ng-template></ng-template>', []))).toEqual([
[EmbeddedTemplateAst, '<ng-template></ng-template>']
[EmbeddedTemplateAst, '<ng-template>']
]);
});
@ -2058,14 +2058,14 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should support references', () => {
expect(humanizeTplAstSourceSpans(parse('<div #a></div>', []))).toEqual([
[ElementAst, 'div', '<div #a></div>'], [ReferenceAst, 'a', null, '#a']
[ElementAst, 'div', '<div #a>'], [ReferenceAst, 'a', null, '#a']
]);
});
it('should support variables', () => {
expect(humanizeTplAstSourceSpans(parse('<ng-template let-a="b"></ng-template>', [])))
.toEqual([
[EmbeddedTemplateAst, '<ng-template let-a="b"></ng-template>'],
[EmbeddedTemplateAst, '<ng-template let-a="b">'],
[VariableAst, 'a', 'b', 'let-a="b"'],
]);
});
@ -2128,7 +2128,7 @@ Property binding a not used by any directive on an embedded template. Make sure
expect(humanizeTplAstSourceSpans(
parse('<svg><circle /><use xlink:href="Port" /></svg>', [tagSel, attrSel])))
.toEqual([
[ElementAst, ':svg:svg', '<svg><circle /><use xlink:href="Port" /></svg>'],
[ElementAst, ':svg:svg', '<svg>'],
[ElementAst, ':svg:circle', '<circle />'],
[DirectiveAst, tagSel, '<circle />'],
[ElementAst, ':svg:use', '<use xlink:href="Port" />'],
@ -2144,8 +2144,7 @@ Property binding a not used by any directive on an embedded template. Make sure
inputs: ['aProp']
}).toSummary();
expect(humanizeTplAstSourceSpans(parse('<div [aProp]="foo"></div>', [dirA]))).toEqual([
[ElementAst, 'div', '<div [aProp]="foo"></div>'],
[DirectiveAst, dirA, '<div [aProp]="foo"></div>'],
[ElementAst, 'div', '<div [aProp]="foo">'], [DirectiveAst, dirA, '<div [aProp]="foo">'],
[BoundDirectivePropertyAst, 'aProp', 'foo', '[aProp]="foo"']
]);
});

View File

@ -12,4 +12,4 @@
* Change detection enables data binding in Angular.
*/
export {ChangeDetectionStrategy, ChangeDetectorRef, DefaultIterableDiffer, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers, NgIterable, PipeTransform, SimpleChange, SimpleChanges, TrackByFunction, WrappedValue} from './change_detection/change_detection';
export {ChangeDetectionStrategy, ChangeDetectorRef, CollectionChangeRecord, DefaultIterableDiffer, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers, NgIterable, PipeTransform, SimpleChange, SimpleChanges, TrackByFunction, WrappedValue} from './change_detection/change_detection';

View File

@ -18,7 +18,7 @@ export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionS
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
export {DefaultIterableDiffer} from './differs/default_iterable_differ';
export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
export {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
export {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
export {PipeTransform} from './pipe_transform';

View File

@ -593,7 +593,7 @@ export class IterableChangeRecord_<V> implements IterableChangeRecord<V> {
constructor(public item: V, public trackById: any) {}
}
// A linked list of IterableChangeRecords with the same IterableChangeRecord_.item
// A linked list of CollectionChangeRecords with the same IterableChangeRecord_.item
class _DuplicateItemRecordList<V> {
/** @internal */
_head: IterableChangeRecord_<V>|null = null;

View File

@ -112,6 +112,12 @@ export interface IterableChangeRecord<V> {
readonly trackById: any;
}
/**
* @deprecated v4.0.0 - Use IterableChangeRecord instead.
* @publicApi
*/
export interface CollectionChangeRecord<V> extends IterableChangeRecord<V> {}
/**
* An optional function passed into the `NgForOf` directive that defines how to track
* changes for items in an iterable.

View File

@ -1149,15 +1149,8 @@ export class FormControl extends AbstractControl {
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
this._applyFormState(formState);
this._setUpdateStrategy(validatorOrOpts);
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._initObservables();
this.updateValueAndValidity({
onlySelf: true,
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
// `VALID` or `INVALID`.
// The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent`
// to `true` to allow that during the control creation process.
emitEvent: !!asyncValidator
});
}
/**
@ -1410,13 +1403,7 @@ export class FormGroup extends AbstractControl {
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({
onlySelf: true,
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
// `VALID` or `INVALID`. The status should be broadcasted via the `statusChanges` observable,
// so we set `emitEvent` to `true` to allow that during the control creation process.
emitEvent: !!asyncValidator
});
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
/**
@ -1836,14 +1823,7 @@ export class FormArray extends AbstractControl {
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({
onlySelf: true,
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
// `VALID` or `INVALID`.
// The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent`
// to `true` to allow that during the control creation process.
emitEvent: !!asyncValidator
});
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
/**

View File

@ -1825,391 +1825,6 @@ describe('FormGroup', () => {
});
});
describe('emit `statusChanges` and `valueChanges` with/without async/sync validators', () => {
const attachEventsLogger = (control: AbstractControl, log: string[], controlName?: string) => {
const name = controlName ? ` (${controlName})` : '';
control.statusChanges.subscribe(status => log.push(`status${name}: ${status}`));
control.valueChanges.subscribe(value => log.push(`value${name}: ${JSON.stringify(value)}`));
};
describe('stand alone controls', () => {
it('should run the async validator on stand alone controls and set status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c =
new FormControl('', null, simpleAsyncValidator({timeout: 0, shouldFail: true}));
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
tick(1);
c.setValue('new!', {emitEvent: true});
tick(1);
// Note that above `simpleAsyncValidator` is called with `timeout:0`. When the timeout
// is set to `0`, the function returns `of(error)`, and the function behaves in a
// synchronous manner. Because of this there is no `PENDING` state as seen in the
// `logs`.
expect(logs).toEqual([
'status: INVALID', // status change emitted as a result of initial async validator run
'value: "new!"', // value change emitted by `setValue`
'status: INVALID' // async validator run after `setValue` call
]);
}));
it('should run the async validator on stand alone controls and set status to `VALID`',
fakeAsync(() => {
const logs: string[] = [];
const c = new FormControl('', null, asyncValidator('new!'));
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
tick(1);
c.setValue('new!', {emitEvent: true});
tick(1);
expect(logs).toEqual([
'status: INVALID', // status change emitted as a result of initial async validator run
'value: "new!"', // value change emitted by `setValue`
'status: PENDING', // status change emitted by `setValue`
'status: VALID' // async validator run after `setValue` call
]);
}));
it('should run the async validator on stand alone controls, include `PENDING` and set status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c =
new FormControl('', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
tick(1);
c.setValue('new!', {emitEvent: true});
tick(1);
expect(logs).toEqual([
'status: INVALID', // status change emitted as a result of initial async validator run
'value: "new!"', // value change emitted by `setValue`
'status: PENDING', // status change emitted by `setValue`
'status: INVALID' // async validator run after `setValue` call
]);
}));
it('should run setValue before the initial async validator and set status to `VALID`',
fakeAsync(() => {
const logs: string[] = [];
const c = new FormControl('', null, asyncValidator('new!'));
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
c.setValue('new!', {emitEvent: true});
tick(1);
// The `setValue` call invoked synchronously cancels the initial run of the
// `asyncValidator` (which would cause the control status to be changed to `INVALID`), so
// the log contains only events after calling `setValue`.
expect(logs).toEqual([
'value: "new!"', // value change emitted by `setValue`
'status: PENDING', // status change emitted by `setValue`
'status: VALID' // async validator run after `setValue` call
]);
}));
it('should run setValue before the initial async validator and set status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c =
new FormControl('', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
c.setValue('new!', {emitEvent: true});
tick(1);
// The `setValue` call invoked synchronously cancels the initial run of the
// `asyncValidator` (which would cause the control status to be changed to `INVALID`), so
// the log contains only events after calling `setValue`.
expect(logs).toEqual([
'value: "new!"', // value change emitted by `setValue`
'status: PENDING', // status change emitted by `setValue`
'status: INVALID' // async validator run after `setValue` call
]);
}));
it('should cancel initial run of the async validator and not emit anything', fakeAsync(() => {
const logger: string[] = [];
const c =
new FormControl('', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(c, logger);
expect(logger.length).toBe(0);
c.setValue('new!', {emitEvent: false});
tick(1);
// Because we are calling `setValue` with `emitEvent: false`, nothing is emitted
// and our logger remains empty
expect(logger).toEqual([]);
}));
it('should run the sync validator on stand alone controls and set status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c = new FormControl('new!', Validators.required);
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
tick(1);
c.setValue('', {emitEvent: true});
tick(1);
expect(logs).toEqual([
'value: ""', // value change emitted by `setValue`
'status: INVALID' // status change emitted by `setValue`
]);
}));
it('should run the sync validator on stand alone controls and set status to `VALID`',
fakeAsync(() => {
const logs: string[] = [];
const c = new FormControl('', Validators.required);
attachEventsLogger(c, logs);
expect(logs.length).toBe(0);
tick(1);
c.setValue('new!', {emitEvent: true});
tick(1);
expect(logs).toEqual([
'value: "new!"', // value change emitted by `setValue`
'status: VALID' // status change emitted by `setValue`
]);
}));
});
describe('combination of multiple form controls', () => {
it('should run the async validator on the FormControl added to the FormGroup and set status to `VALID`',
fakeAsync(() => {
const logs: string[] = [];
const c1 = new FormControl('one');
const g1 = new FormGroup({'one': c1});
// Initial state of the controls
expect(currentStateOf([c1, g1])).toEqual([
{errors: null, pending: false, status: 'VALID'}, // Control 1
{errors: null, pending: false, status: 'VALID'}, // Group
]);
attachEventsLogger(g1, logs, 'g1');
const c2 = new FormControl('new!', null, asyncValidator('new!'));
attachEventsLogger(c2, logs, 'c2');
// Initial state of the new control
expect(currentStateOf([c2])).toEqual([
{errors: null, pending: true, status: 'PENDING'}, // Control 2
]);
expect(logs.length).toBe(0);
g1.setControl('one', c2);
tick(1);
expect(logs).toEqual([
'value (g1): {"one":"new!"}', // value change emitted by `setControl`
'status (g1): PENDING', // value change emitted by `setControl`
'status (c2): VALID', // async validator run after `setControl` call
'status (g1): VALID' // status changed from the `setControl` call
]);
// Final state of all controls
expect(currentStateOf([g1, c2])).toEqual([
{errors: null, pending: false, status: 'VALID'}, // Group
{errors: null, pending: false, status: 'VALID'}, // Control 2
]);
}));
it('should run the async validator on the FormControl added to the FormGroup and set status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c1 = new FormControl('one');
const g1 = new FormGroup({'one': c1});
// Initial state of the controls
expect(currentStateOf([c1, g1])).toEqual([
{errors: null, pending: false, status: 'VALID'}, // Control 1
{errors: null, pending: false, status: 'VALID'}, // Group
]);
attachEventsLogger(g1, logs, 'g1');
const c2 =
new FormControl('new!', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(c2, logs, 'c2');
// Initial state of the new control
expect(currentStateOf([c2])).toEqual([
{errors: null, pending: true, status: 'PENDING'}, // Control 2
]);
expect(logs.length).toBe(0);
g1.setControl('one', c2);
tick(1);
expect(logs).toEqual([
'value (g1): {"one":"new!"}',
'status (g1): PENDING', // g1 async validator is invoked after `g1.setControl` call
'status (c2): INVALID', // c2 async validator trigger at c2 init, completed with the
// `INVALID` status
'status (g1): INVALID' // g1 validator completed with the `INVALID` status
]);
// Final state of all controls
expect(currentStateOf([g1, c2])).toEqual([
{errors: null, pending: false, status: 'INVALID'}, // Group
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control 2
]);
}));
it('should run the async validator at `FormControl` and `FormGroup` level and set status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c1 = new FormControl('one');
const g1 = new FormGroup(
{'one': c1}, null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
// Initial state of the controls
expect(currentStateOf([c1, g1])).toEqual([
{errors: null, pending: false, status: 'VALID'}, // Control 1
{errors: null, pending: true, status: 'PENDING'}, // Group
]);
attachEventsLogger(g1, logs, 'g1');
const c2 =
new FormControl('new!', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(c2, logs, 'c2');
// Initial state of the new control
expect(currentStateOf([c2])).toEqual([
{errors: null, pending: true, status: 'PENDING'}, // Control 2
]);
expect(logs.length).toBe(0);
g1.setControl('one', c2);
tick(1);
expect(logs).toEqual([
'value (g1): {"one":"new!"}',
'status (g1): PENDING', // g1 async validator is invoked after `g1.setControl` call
'status (c2): INVALID', // c2 async validator trigger at c2 init, completed with the
// `INVALID` status
'status (g1): PENDING', // c2 update triggered g1 to re-run validation
'status (g1): INVALID' // g1 validator completed with the `INVALID` status
]);
// Final state of all controls
expect(currentStateOf([g1, c2])).toEqual([
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control 2
]);
}));
it('should run the async validator on a `FormArray` and a `FormControl` and status to `INVALID`',
fakeAsync(() => {
const logs: string[] = [];
const c1 = new FormControl('one');
const g1 = new FormGroup(
{'one': c1}, null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
const fa =
new FormArray([g1], null!, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(g1, logs, 'g1');
// Initial state of the controls
expect(currentStateOf([c1, g1, fa])).toEqual([
{errors: null, pending: false, status: 'VALID'}, // Control 1
{errors: null, pending: true, status: 'PENDING'}, // Group
{errors: null, pending: true, status: 'PENDING'}, // FormArray
]);
attachEventsLogger(fa, logs, 'fa');
const c2 =
new FormControl('new!', null, simpleAsyncValidator({timeout: 1, shouldFail: true}));
attachEventsLogger(c2, logs, 'c2');
// Initial state of the new control
expect(currentStateOf([c2])).toEqual([
{errors: null, pending: true, status: 'PENDING'}, // Control 2
]);
expect(logs.length).toBe(0);
g1.setControl('one', c2);
tick(1);
expect(logs).toEqual([
'value (g1): {"one":"new!"}', // g1's call to `setControl` triggered value update
'status (g1): PENDING', // g1's call to `setControl` triggered status update
'value (fa): [{"one":"new!"}]', // g1 update triggers the `FormArray` value update
'status (fa): PENDING', // g1 update triggers the `FormArray` status update
'status (c2): INVALID', // async validator run after `setControl` call
'status (g1): PENDING', // async validator run after `setControl` call
'status (fa): PENDING', // async validator run after `setControl` call
'status (g1): INVALID', // g1 validator completed with the `INVALID` status
'status (fa): PENDING', // fa validator still running
'status (fa): INVALID' // fa validator completed with the `INVALID` status
]);
// Final state of all controls
expect(currentStateOf([g1, fa, c2])).toEqual([
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
{errors: {async: true}, pending: false, status: 'INVALID'}, // FormArray
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control 2
]);
}));
});
});
describe('pending', () => {
let c: FormControl;
let g: FormGroup;

View File

@ -14,7 +14,7 @@ import {findNodeAtPosition, isExpressionNode, isTemplateNode} from '../hybrid_vi
interface ParseResult {
nodes: t.Node[];
errors: ParseError[]|null;
errors?: ParseError[];
position: number;
}
@ -34,7 +34,7 @@ function parse(template: string): ParseResult {
describe('findNodeAtPosition for template AST', () => {
it('should locate element in opening tag', () => {
const {errors, nodes, position} = parse(`<di¦v></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -42,7 +42,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate element in closing tag', () => {
const {errors, nodes, position} = parse(`<div></di¦v>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -50,7 +50,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate element when cursor is at the beginning', () => {
const {errors, nodes, position} = parse(`<¦div></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -58,7 +58,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate element when cursor is at the end', () => {
const {errors, nodes, position} = parse(`<div¦></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -66,7 +66,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate attribute key', () => {
const {errors, nodes, position} = parse(`<div cla¦ss="foo"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
@ -74,7 +74,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate attribute value', () => {
const {errors, nodes, position} = parse(`<div class="fo¦o"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
// TODO: Note that we do not have the ability to detect the RHS (yet)
@ -83,7 +83,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate bound attribute key', () => {
const {errors, nodes, position} = parse(`<test-cmp [fo¦o]="bar"></test-cmp>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
@ -91,7 +91,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate bound attribute value', () => {
const {errors, nodes, position} = parse(`<test-cmp [foo]="b¦ar"></test-cmp>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -99,7 +99,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate bound event key', () => {
const {errors, nodes, position} = parse(`<test-cmp (fo¦o)="bar()"></test-cmp>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundEvent);
@ -107,7 +107,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate bound event value', () => {
const {errors, nodes, position} = parse(`<test-cmp (foo)="b¦ar()"></test-cmp>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.MethodCall);
@ -115,7 +115,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate element children', () => {
const {errors, nodes, position} = parse(`<div><sp¦an></span></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -124,7 +124,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate element reference', () => {
const {errors, nodes, position} = parse(`<div #my¦div></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
@ -132,7 +132,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template text attribute', () => {
const {errors, nodes, position} = parse(`<ng-template ng¦If></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
@ -140,7 +140,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template bound attribute key', () => {
const {errors, nodes, position} = parse(`<ng-template [ng¦If]="foo"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
@ -148,7 +148,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template bound attribute value', () => {
const {errors, nodes, position} = parse(`<ng-template [ngIf]="f¦oo"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -156,7 +156,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template bound attribute key in two-way binding', () => {
const {errors, nodes, position} = parse(`<ng-template [(f¦oo)]="bar"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
@ -165,7 +165,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template bound attribute value in two-way binding', () => {
const {errors, nodes, position} = parse(`<ng-template [(foo)]="b¦ar"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -174,7 +174,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template bound event key', () => {
const {errors, nodes, position} = parse(`<ng-template (cl¦ick)="foo()"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundEvent);
@ -182,14 +182,14 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template bound event value', () => {
const {errors, nodes, position} = parse(`<ng-template (click)="f¦oo()"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(node).toBeInstanceOf(e.MethodCall);
});
it('should locate template attribute key', () => {
const {errors, nodes, position} = parse(`<ng-template i¦d="foo"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
@ -197,7 +197,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template attribute value', () => {
const {errors, nodes, position} = parse(`<ng-template id="f¦oo"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
// TODO: Note that we do not have the ability to detect the RHS (yet)
@ -206,7 +206,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template reference key via the # notation', () => {
const {errors, nodes, position} = parse(`<ng-template #f¦oo></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
@ -215,7 +215,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template reference key via the ref- notation', () => {
const {errors, nodes, position} = parse(`<ng-template ref-fo¦o></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
@ -224,7 +224,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template reference value via the # notation', () => {
const {errors, nodes, position} = parse(`<ng-template #foo="export¦As"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
@ -234,7 +234,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template reference value via the ref- notation', () => {
const {errors, nodes, position} = parse(`<ng-template ref-foo="export¦As"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Reference);
@ -244,7 +244,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template variable key', () => {
const {errors, nodes, position} = parse(`<ng-template let-f¦oo="bar"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
@ -252,7 +252,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template variable value', () => {
const {errors, nodes, position} = parse(`<ng-template let-foo="b¦ar"></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
@ -260,7 +260,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate template children', () => {
const {errors, nodes, position} = parse(`<ng-template><d¦iv></div></ng-template>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -268,7 +268,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate ng-content', () => {
const {errors, nodes, position} = parse(`<ng-co¦ntent></ng-content>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Content);
@ -276,7 +276,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate ng-content attribute key', () => {
const {errors, nodes, position} = parse('<ng-content cla¦ss="red"></ng-content>');
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.TextAttribute);
@ -284,7 +284,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate ng-content attribute value', () => {
const {errors, nodes, position} = parse('<ng-content class="r¦ed"></ng-content>');
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
// TODO: Note that we do not have the ability to detect the RHS (yet)
expect(isTemplateNode(node!)).toBe(true);
@ -293,7 +293,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should not locate implicit receiver', () => {
const {errors, nodes, position} = parse(`<div [foo]="¦bar"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -301,7 +301,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate bound attribute key in two-way binding', () => {
const {errors, nodes, position} = parse(`<cmp [(f¦oo)]="bar"></cmp>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
@ -310,7 +310,7 @@ describe('findNodeAtPosition for template AST', () => {
it('should locate bound attribute value in two-way binding', () => {
const {errors, nodes, position} = parse(`<cmp [(foo)]="b¦ar"></cmp>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -321,7 +321,7 @@ describe('findNodeAtPosition for template AST', () => {
describe('findNodeAtPosition for expression AST', () => {
it('should not locate implicit receiver', () => {
const {errors, nodes, position} = parse(`{{ ¦title }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -330,7 +330,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate property read', () => {
const {errors, nodes, position} = parse(`{{ ti¦tle }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -339,7 +339,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate safe property read', () => {
const {errors, nodes, position} = parse(`{{ foo?¦.bar }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.SafePropertyRead);
@ -348,7 +348,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate keyed read', () => {
const {errors, nodes, position} = parse(`{{ foo['bar']¦ }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.KeyedRead);
@ -356,7 +356,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate property write', () => {
const {errors, nodes, position} = parse(`<div (foo)="b¦ar=$event"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyWrite);
@ -364,7 +364,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate keyed write', () => {
const {errors, nodes, position} = parse(`<div (foo)="bar['baz']¦=$event"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.KeyedWrite);
@ -372,7 +372,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate binary', () => {
const {errors, nodes, position} = parse(`{{ 1 +¦ 2 }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Binary);
@ -380,7 +380,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate binding pipe with an identifier', () => {
const {errors, nodes, position} = parse(`{{ title | p¦ }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.BindingPipe);
@ -391,7 +391,7 @@ describe('findNodeAtPosition for expression AST', () => {
// TODO: We are not able to locate pipe if identifier is missing because the
// parser throws an error. This case is important for autocomplete.
// const {errors, nodes, position} = parse(`{{ title | ¦ }}`);
// expect(errors).toBe(null);
// expect(errors).toBeUndefined();
// const node = findNodeAtPosition(nodes, position);
// expect(isExpressionNode(node!)).toBe(true);
// expect(node).toBeInstanceOf(e.BindingPipe);
@ -399,7 +399,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate method call', () => {
const {errors, nodes, position} = parse(`{{ title.toString(¦) }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.MethodCall);
@ -407,7 +407,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate safe method call', () => {
const {errors, nodes, position} = parse(`{{ title?.toString(¦) }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.SafeMethodCall);
@ -415,7 +415,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate literal primitive in interpolation', () => {
const {errors, nodes, position} = parse(`{{ title.indexOf('t¦') }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralPrimitive);
@ -424,7 +424,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate literal primitive in binding', () => {
const {errors, nodes, position} = parse(`<div [id]="'t¦'"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralPrimitive);
@ -433,7 +433,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate empty expression', () => {
const {errors, nodes, position} = parse(`<div [id]="¦"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.EmptyExpr);
@ -441,7 +441,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate literal array', () => {
const {errors, nodes, position} = parse(`{{ [1, 2,¦ 3] }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralArray);
@ -449,7 +449,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate literal map', () => {
const {errors, nodes, position} = parse(`{{ { hello:¦ "world" } }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.LiteralMap);
@ -457,7 +457,7 @@ describe('findNodeAtPosition for expression AST', () => {
it('should locate conditional', () => {
const {errors, nodes, position} = parse(`{{ cond ?¦ true : false }}`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.Conditional);
@ -467,7 +467,7 @@ describe('findNodeAtPosition for expression AST', () => {
describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template key', () => {
const {errors, nodes, position} = parse(`<div *ng¦If="foo"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
@ -475,7 +475,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template value', () => {
const {errors, nodes, position} = parse(`<div *ngIf="f¦oo"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -485,7 +485,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
const {errors, nodes, position} = parse(`<div *ng¦For="let item of items"></div>`);
// ngFor is a text attribute because the desugared form is
// <ng-template ngFor let-item [ngForOf]="items">
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
// TODO: this is currently wrong because it should point to ngFor text
// attribute instead of ngForOf bound attribute
@ -493,7 +493,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate not let keyword', () => {
const {errors, nodes, position} = parse(`<div *ngFor="l¦et item of items"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
// TODO: this is currently wrong because node is currently pointing to
// "item". In this case, it should return undefined.
@ -501,7 +501,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate let variable', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let i¦tem of items"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
@ -510,7 +510,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate bound attribute key', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item o¦f items"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.BoundAttribute);
@ -519,7 +519,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate bound attribute value', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of it¦ems"></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -528,7 +528,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate template children', () => {
const {errors, nodes, position} = parse(`<di¦v *ngIf></div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
@ -540,7 +540,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
<div *ngFor="let item of items; let i=index">
{{ i¦ }}
</div>`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
@ -548,7 +548,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate LHS of variable declaration', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i¦=index">`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);
@ -558,7 +558,7 @@ describe('findNodeAtPosition for microsyntax expression', () => {
it('should locate RHS of variable declaration', () => {
const {errors, nodes, position} = parse(`<div *ngFor="let item of items; let i=in¦dex">`);
expect(errors).toBe(null);
expect(errors).toBeUndefined();
const node = findNodeAtPosition(nodes, position);
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Variable);

View File

@ -65,7 +65,7 @@ function getBoundedWordSpan(
// The HTML tag may include `-` (e.g. `app-root`),
// so use the HtmlAst to get the span before ayazhafiz refactor the code.
return {
start: templateInfo.template.span.start + ast.startSourceSpan.start.offset + 1,
start: templateInfo.template.span.start + ast.startSourceSpan!.start.offset + 1,
length: ast.name.length
};
}

View File

@ -48,7 +48,7 @@ export function parseInnerRange(element: Element): ParseTreeResult {
* @param element The element whose inner range we want to compute.
*/
function getInnerRange(element: Element): LexerRange {
const start = element.startSourceSpan.end;
const start = element.startSourceSpan!.end;
const end = element.endSourceSpan!.start;
return {
startPos: start.offset,

View File

@ -7,8 +7,8 @@
*/
import {Injector, NgModuleRef} from '@angular/core';
import {defer, EmptyError, from, Observable, Observer, of} from 'rxjs';
import {catchError, combineAll, concatMap, first, map, mergeMap, tap} from 'rxjs/operators';
import {defer, EmptyError, Observable, Observer, of} from 'rxjs';
import {catchError, concatAll, first, map, mergeMap, tap} from 'rxjs/operators';
import {LoadedRouterConfig, Route, Routes} from './config';
import {CanLoadFn} from './interfaces';
@ -17,7 +17,6 @@ import {RouterConfigLoader} from './router_config_loader';
import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {forEach, waitForMap, wrapIntoObservable} from './utils/collection';
import {getOutlet, groupRoutesByOutlet} from './utils/config';
import {isCanLoad, isFunction, isUrlTree} from './utils/type_guards';
class NoMatch {
@ -149,52 +148,28 @@ class ApplyRedirects {
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[],
segments: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
// We need to expand each outlet group independently to ensure that we not only load modules
// for routes matching the given `outlet`, but also those which will be activated because
// their path is empty string. This can result in multiple outlets being activated at once.
const routesByOutlet: Map<string, Route[]> = groupRoutesByOutlet(routes);
if (!routesByOutlet.has(outlet)) {
routesByOutlet.set(outlet, []);
}
const expandRoutes = (routes: Route[]) => {
return from(routes).pipe(
concatMap((r: Route) => {
const expanded$ = this.expandSegmentAgainstRoute(
ngModule, segmentGroup, routes, r, segments, outlet, allowRedirects);
return expanded$.pipe(catchError(e => {
if (e instanceof NoMatch) {
return of(null);
}
throw e;
}));
}),
first((s: UrlSegmentGroup|null): s is UrlSegmentGroup => s !== null),
catchError(e => {
if (e instanceof EmptyError || e.name === 'EmptyError') {
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
return of(new UrlSegmentGroup([], {}));
}
throw new NoMatch(segmentGroup);
return of(...routes).pipe(
map((r: any) => {
const expanded$ = this.expandSegmentAgainstRoute(
ngModule, segmentGroup, routes, r, segments, outlet, allowRedirects);
return expanded$.pipe(catchError((e: any) => {
if (e instanceof NoMatch) {
// TODO(i): this return type doesn't match the declared Observable<UrlSegmentGroup> -
// talk to Jason
return of(null) as any;
}
throw e;
}),
);
};
const expansions = Array.from(routesByOutlet.entries()).map(([routeOutlet, routes]) => {
const expanded = expandRoutes(routes);
// Map all results from outlets we aren't activating to `null` so they can be ignored later
return routeOutlet === outlet ? expanded :
expanded.pipe(map(() => null), catchError(() => of(null)));
});
return from(expansions)
.pipe(
combineAll(),
first(),
// Return only the expansion for the route outlet we are trying to activate.
map(results => results.find(result => result !== null)!),
);
}));
}),
concatAll(), first((s: any) => !!s), catchError((e: any, _: any) => {
if (e instanceof EmptyError || e.name === 'EmptyError') {
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
return of(new UrlSegmentGroup([], {}));
}
throw new NoMatch(segmentGroup);
}
throw e;
}));
}
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
@ -205,9 +180,7 @@ class ApplyRedirects {
private expandSegmentAgainstRoute(
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
// Empty string segments are special because multiple outlets can match a single path, i.e.
// `[{path: '', component: B}, {path: '', loadChildren: () => {}, outlet: "about"}]`
if (getOutlet(route) !== outlet && route.path !== '') {
if (getOutlet(route) !== outlet) {
return noMatch(segmentGroup);
}
@ -578,3 +551,7 @@ function isEmptyPathRedirect(
return r.path === '' && r.redirectTo !== undefined;
}
function getOutlet(route: Route): string {
return route.outlet || PRIMARY_OUTLET;
}

View File

@ -113,21 +113,3 @@ export function standardizeConfig(r: Route): Route {
}
return c;
}
/** Returns of `Map` of outlet names to the `Route`s for that outlet. */
export function groupRoutesByOutlet(routes: Route[]): Map<string, Route[]> {
return routes.reduce((map, route) => {
const routeOutlet = getOutlet(route);
if (map.has(routeOutlet)) {
map.get(routeOutlet)!.push(route);
} else {
map.set(routeOutlet, [route]);
}
return map;
}, new Map<string, Route[]>());
}
/** Returns the `route.outlet` or PRIMARY_OUTLET if none exists. */
export function getOutlet(route: Route): string {
return route.outlet || PRIMARY_OUTLET;
}

View File

@ -7,9 +7,9 @@
*/
import {NgModuleRef} from '@angular/core';
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';
import {Observable, of} from 'rxjs';
import {delay, tap} from 'rxjs/operators';
import {delay} from 'rxjs/operators';
import {applyRedirects} from '../src/apply_redirects';
import {LoadedRouterConfig, Route, Routes} from '../src/config';
@ -482,88 +482,6 @@ describe('applyRedirects', () => {
expect((config[0] as any)._loadedConfig).toBe(loadedConfig);
});
});
it('should load all matching configurations of empty path, including an auxiliary outlets',
fakeAsync(() => {
const loadedConfig =
new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
let loadCalls = 0;
let loaded: string[] = [];
const loader = {
load: (injector: any, p: Route) => {
loadCalls++;
return of(loadedConfig)
.pipe(
delay(100 * loadCalls),
tap(() => loaded.push(p.loadChildren! as string)),
);
}
};
const config: Routes =
[{path: '', loadChildren: 'root'}, {path: '', loadChildren: 'aux', outlet: 'popup'}];
applyRedirects(testModule.injector, <any>loader, serializer, tree(''), config).subscribe();
expect(loadCalls).toBe(2);
tick(100);
expect(loaded).toEqual(['root']);
tick(100);
expect(loaded).toEqual(['root', 'aux']);
}));
it('loads only the first match when two Routes with the same outlet have the same path', () => {
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
let loadCalls = 0;
let loaded: string[] = [];
const loader = {
load: (injector: any, p: Route) => {
loadCalls++;
return of(loadedConfig)
.pipe(
tap(() => loaded.push(p.loadChildren! as string)),
);
}
};
const config: Routes =
[{path: 'a', loadChildren: 'first'}, {path: 'a', loadChildren: 'second'}];
applyRedirects(testModule.injector, <any>loader, serializer, tree('a'), config).subscribe();
expect(loadCalls).toBe(1);
expect(loaded).toEqual(['first']);
});
it('should load the configuration of empty root path if the entry is an aux outlet',
fakeAsync(() => {
const loadedConfig =
new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
let loaded: string[] = [];
const rootDelay = 100;
const auxDelay = 1;
const loader = {
load: (injector: any, p: Route) => {
const delayMs = p.loadChildren! as string === 'aux' ? auxDelay : rootDelay;
return of(loadedConfig)
.pipe(
delay(delayMs),
tap(() => loaded.push(p.loadChildren! as string)),
);
}
};
const config: Routes = [
// Define aux route first so it matches before the primary outlet
{path: 'modal', loadChildren: 'aux', outlet: 'popup'},
{path: '', loadChildren: 'root'},
];
applyRedirects(testModule.injector, <any>loader, serializer, tree('(popup:modal)'), config)
.subscribe();
tick(auxDelay);
expect(loaded).toEqual(['aux']);
tick(rootDelay);
expect(loaded).toEqual(['aux', 'root']);
}));
});
describe('empty paths', () => {
@ -836,46 +754,6 @@ describe('applyRedirects', () => {
});
});
describe('multiple matches with empty path named outlets', () => {
it('should work with redirects when other outlet comes before the one being activated', () => {
applyRedirects(
testModule.injector, null!, serializer, tree(''),
[
{
path: '',
children: [
{path: '', component: ComponentA, outlet: 'aux'},
{path: '', redirectTo: 'b', pathMatch: 'full'},
{path: 'b', component: ComponentB},
],
},
])
.subscribe(
(tree: UrlTree) => {
expect(tree.toString()).toEqual('/b');
},
() => {
fail('should not be reached');
});
});
it('should work when entry point is named outlet', () => {
applyRedirects(
testModule.injector, null!, serializer, tree('(popup:modal)'),
[
{path: '', component: ComponentA},
{path: 'modal', component: ComponentB, outlet: 'popup'},
])
.subscribe(
(tree: UrlTree) => {
expect(tree.toString()).toEqual('/(popup:modal)');
},
(e) => {
fail('should not be reached' + e.message);
});
});
});
describe('redirecting to named outlets', () => {
it('should work when using absolute redirects', () => {
checkRedirect(
@ -916,18 +794,6 @@ describe('applyRedirects', () => {
});
});
});
// internal failure b/165719418
it('does not fail with large configs', () => {
const config: Routes = [];
for (let i = 0; i < 400; i++) {
config.push({path: 'no_match', component: ComponentB});
}
config.push({path: 'match', component: ComponentA});
applyRedirects(testModule.injector, null!, serializer, tree('match'), config).forEach(r => {
expectTreeToBe(r, 'match');
});
});
});
function checkRedirect(config: Routes, url: string, callback: any): void {

View File

@ -14,7 +14,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {UnrecoverableStateEvent, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
export {UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
export {ServiceWorkerModule, SwRegistrationOptions} from './module';
export {SwPush} from './push';
export {SwUpdate} from './update';

View File

@ -14,8 +14,6 @@ export const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not support
/**
* An event emitted when a new version of the app is available.
*
* @see {@link guide/service-worker-communications Service worker communication guide}
*
* @publicApi
*/
export interface UpdateAvailableEvent {
@ -27,8 +25,6 @@ export interface UpdateAvailableEvent {
/**
* An event emitted when a new version of the app has been downloaded and activated.
*
* @see {@link guide/service-worker-communications Service worker communication guide}
*
* @publicApi
*/
export interface UpdateActivatedEvent {
@ -37,24 +33,6 @@ export interface UpdateActivatedEvent {
current: {hash: string, appData?: Object};
}
/**
* An event emitted when the version of the app used by the service worker to serve this client is
* in a broken state that cannot be recovered from and a full page reload is required.
*
* For example, the service worker may not be able to retrieve a required resource, neither from the
* cache nor from the server. This could happen if a new version is deployed to the server and the
* service worker cache has been partially cleaned by the browser, removing some files of a previous
* app version but not all.
*
* @see {@link guide/service-worker-communications Service worker communication guide}
*
* @publicApi
*/
export interface UnrecoverableStateEvent {
type: 'UNRECOVERABLE_STATE';
reason: string;
}
/**
* An event emitted when a `PushEvent` is received by the service worker.
*/
@ -63,7 +41,7 @@ export interface PushEvent {
data: any;
}
export type IncomingEvent = UpdateAvailableEvent|UpdateActivatedEvent|UnrecoverableStateEvent;
export type IncomingEvent = UpdateAvailableEvent|UpdateActivatedEvent;
export interface TypedEvent {
type: string;

View File

@ -9,7 +9,7 @@
import {Injectable} from '@angular/core';
import {NEVER, Observable} from 'rxjs';
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UnrecoverableStateEvent, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
@ -17,8 +17,6 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, UnrecoverableStateEvent, UpdateAc
* Subscribe to update notifications from the Service Worker, trigger update
* checks, and forcibly activate updates.
*
* @see {@link guide/service-worker-communications Service worker communication guide}
*
* @publicApi
*/
@Injectable()
@ -33,13 +31,6 @@ export class SwUpdate {
*/
readonly activated: Observable<UpdateActivatedEvent>;
/**
* Emits an `UnrecoverableStateEvent` event whenever the version of the app used by the service
* worker to serve this client is in a broken state that cannot be recovered from without a full
* page reload.
*/
readonly unrecoverable: Observable<UnrecoverableStateEvent>;
/**
* True if the Service Worker is enabled (supported by the browser and enabled via
* `ServiceWorkerModule`).
@ -52,12 +43,10 @@ export class SwUpdate {
if (!sw.isEnabled) {
this.available = NEVER;
this.activated = NEVER;
this.unrecoverable = NEVER;
return;
}
this.available = this.sw.eventsOfType<UpdateAvailableEvent>('UPDATE_AVAILABLE');
this.activated = this.sw.eventsOfType<UpdateActivatedEvent>('UPDATE_ACTIVATED');
this.unrecoverable = this.sw.eventsOfType<UnrecoverableStateEvent>('UNRECOVERABLE_STATE');
}
checkForUpdate(): Promise<void> {

View File

@ -435,14 +435,6 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
},
});
});
it('processes unrecoverable notifications when sent', done => {
update.unrecoverable.subscribe(event => {
expect(event.reason).toEqual('Invalid Resource');
expect(event.type).toEqual('UNRECOVERABLE_STATE');
done();
});
mock.sendMessage({type: 'UNRECOVERABLE_STATE', reason: 'Invalid Resource'});
});
it('processes update activation notifications when sent', done => {
update.activated.subscribe(event => {
expect(event.previous).toEqual({hash: 'A'});
@ -508,7 +500,6 @@ import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockS
update = new SwUpdate(comm);
update.available.toPromise().catch(err => fail(err));
update.activated.toPromise().catch(err => fail(err));
update.unrecoverable.toPromise().catch(err => fail(err));
});
it('gives an error when checking for updates', done => {
update = new SwUpdate(comm);

View File

@ -9,7 +9,7 @@
import {Adapter, Context} from './adapter';
import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api';
import {Database, Table} from './database';
import {errorToString, SwCriticalError, SwUnrecoverableStateError} from './error';
import {errorToString, SwCriticalError} from './error';
import {IdleScheduler} from './idle';
import {AssetGroupConfig} from './manifest';
import {sha1Binary} from './sha1';
@ -145,7 +145,6 @@ export abstract class AssetGroup {
return cachedResponse;
}
}
// No already-cached response exists, so attempt a fetch/cache operation. The original request
// may specify things like credential inclusion, but for assets these are not honored in order
// to avoid issues with opaque responses. The SW requests the data itself.
@ -394,8 +393,8 @@ export abstract class AssetGroup {
// reasons: either the non-cache-busted request failed (hopefully transiently) or if the
// hash of the content retrieved does not match the canonical hash from the manifest. It's
// only valid to access the content of the first response if the request was successful.
let makeCacheBustedRequest: boolean = !networkResult.ok;
if (networkResult.ok) {
let makeCacheBustedRequest: boolean = networkResult.ok;
if (makeCacheBustedRequest) {
// The request was successful. A cache-busted request is only necessary if the hashes
// don't match. Compare them, making sure to clone the response so it can be used later
// if it proves to be valid.
@ -415,16 +414,10 @@ export abstract class AssetGroup {
// If the response was unsuccessful, there's nothing more that can be done.
if (!cacheBustedResult.ok) {
if (cacheBustedResult.status === 404) {
throw new SwUnrecoverableStateError(
`Failed to retrieve hashed resource from the server. (AssetGroup: ${
this.config.name} | URL: ${url})`);
} else {
throw new SwCriticalError(
`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${
req.url} returned response ${cacheBustedResult.status} ${
cacheBustedResult.statusText}`);
}
throw new SwCriticalError(
`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${
req.url} returned response ${cacheBustedResult.status} ${
cacheBustedResult.statusText}`);
}
// Hash the contents.

View File

@ -436,9 +436,6 @@ export class Driver implements Debuggable, UpdateSource {
// network.
res = await appVersion.handleFetch(event.request, event);
} catch (err) {
if (err.isUnrecoverableState) {
await this.notifyClientsAboutUnrecoverableState(appVersion, err.message);
}
if (err.isCritical) {
// Something went wrong with the activation of this version.
await this.versionFailed(appVersion, err);
@ -1012,26 +1009,6 @@ export class Driver implements Debuggable, UpdateSource {
};
}
async notifyClientsAboutUnrecoverableState(appVersion: AppVersion, reason: string):
Promise<void> {
const broken =
Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
if (broken === undefined) {
// This version is no longer in use anyway, so nobody cares.
return;
}
const brokenHash = broken[0];
const affectedClients = Array.from(this.clientVersionMap.entries())
.filter(([clientId, hash]) => hash === brokenHash)
.map(([clientId]) => clientId);
affectedClients.forEach(async clientId => {
const client = await this.scope.clients.get(clientId);
client.postMessage({type: 'UNRECOVERABLE_STATE', reason});
});
}
async notifyClientsAboutUpdate(next: AppVersion): Promise<void> {
await this.initialized;

View File

@ -17,7 +17,3 @@ export function errorToString(error: any): string {
return `${error}`;
}
}
export class SwUnrecoverableStateError extends SwCriticalError {
readonly isUnrecoverableState: boolean = true;
}

View File

@ -1738,169 +1738,6 @@ describe('Driver', () => {
expect(requestUrls2).toContain(httpsRequestUrl);
});
describe('unrecoverable state', () => {
const generateMockServerState = (fileSystem: MockFileSystem) => {
const manifest: Manifest = {
configVersion: 1,
timestamp: 1234567890123,
index: '/index.html',
assetGroups: [{
name: 'assets',
installMode: 'prefetch',
updateMode: 'prefetch',
urls: fileSystem.list(),
patterns: [],
cacheQueryOptions: {ignoreVary: true},
}],
dataGroups: [],
navigationUrls: processNavigationUrls(''),
hashTable: tmpHashTableForFs(fileSystem),
};
return {
serverState: new MockServerStateBuilder()
.withManifest(manifest)
.withStaticFiles(fileSystem)
.build(),
manifest,
};
};
it('notifies affected clients', async () => {
const {serverState: serverState1} = generateMockServerState(
new MockFileSystemBuilder()
.addFile('/index.html', '<script src="foo.hash.js"></script>')
.addFile('/foo.hash.js', 'console.log("FOO");')
.build());
const {serverState: serverState2, manifest: manifest2} = generateMockServerState(
new MockFileSystemBuilder()
.addFile('/index.html', '<script src="bar.hash.js"></script>')
.addFile('/bar.hash.js', 'console.log("BAR");')
.build());
const {serverState: serverState3} = generateMockServerState(
new MockFileSystemBuilder()
.addFile('/index.html', '<script src="baz.hash.js"></script>')
.addFile('/baz.hash.js', 'console.log("BAZ");')
.build());
// Create initial server state and initialize the SW.
scope = new SwTestHarnessBuilder().withServerState(serverState1).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
// Verify that all three clients are able to make the request.
expect(await makeRequest(scope, '/foo.hash.js', 'client1')).toBe('console.log("FOO");');
expect(await makeRequest(scope, '/foo.hash.js', 'client2')).toBe('console.log("FOO");');
expect(await makeRequest(scope, '/foo.hash.js', 'client3')).toBe('console.log("FOO");');
await driver.initialized;
serverState1.clearRequests();
// Verify that the `foo.hash.js` file is cached.
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
serverState1.assertNoRequestFor('/foo.hash.js');
// Update the ServiceWorker to the second version.
scope.updateServerState(serverState2);
expect(await driver.checkForUpdate()).toEqual(true);
// Update the first two clients to the latest version, keep `client3` as is.
const [client1, client2] =
await Promise.all([scope.clients.get('client1'), scope.clients.get('client2')]);
await Promise.all([driver.updateClient(client1), driver.updateClient(client2)]);
// Update the ServiceWorker to the latest version
scope.updateServerState(serverState3);
expect(await driver.checkForUpdate()).toEqual(true);
// Remove `bar.hash.js` from the cache to emulate the browser evicting files from the cache.
await removeAssetFromCache(scope, manifest2, '/bar.hash.js');
// Get all clients and verify their messages
const mockClient1 = scope.clients.getMock('client1')!;
const mockClient2 = scope.clients.getMock('client2')!;
const mockClient3 = scope.clients.getMock('client3')!;
// Try to retrieve `bar.hash.js`, which is neither in the cache nor on the server.
// This should put the SW in an unrecoverable state and notify clients.
expect(await makeRequest(scope, '/bar.hash.js', 'client1')).toBeNull();
serverState2.assertSawRequestFor('/bar.hash.js');
const unrecoverableMessage = {
type: 'UNRECOVERABLE_STATE',
reason:
'Failed to retrieve hashed resource from the server. (AssetGroup: assets | URL: /bar.hash.js)'
};
expect(mockClient1.messages).toContain(unrecoverableMessage);
expect(mockClient2.messages).toContain(unrecoverableMessage);
expect(mockClient3.messages).not.toContain(unrecoverableMessage);
// Because `client1` failed, `client1` and `client2` have been moved to the latest version.
// Verify that by retrieving `baz.hash.js`.
expect(await makeRequest(scope, '/baz.hash.js', 'client1')).toBe('console.log("BAZ");');
serverState2.assertNoRequestFor('/baz.hash.js');
expect(await makeRequest(scope, '/baz.hash.js', 'client2')).toBe('console.log("BAZ");');
serverState2.assertNoRequestFor('/baz.hash.js');
// Ensure that `client3` remains on the first version and can request `foo.hash.js`.
expect(await makeRequest(scope, '/foo.hash.js', 'client3')).toBe('console.log("FOO");');
serverState2.assertNoRequestFor('/foo.hash.js');
});
it('enters degraded mode', async () => {
const originalFiles = new MockFileSystemBuilder()
.addFile('/index.html', '<script src="foo.hash.js"></script>')
.addFile('/foo.hash.js', 'console.log("FOO");')
.build();
const updatedFiles = new MockFileSystemBuilder()
.addFile('/index.html', '<script src="bar.hash.js"></script>')
.addFile('/bar.hash.js', 'console.log("BAR");')
.build();
const {serverState: originalServer, manifest} = generateMockServerState(originalFiles);
const {serverState: updatedServer} = generateMockServerState(updatedFiles);
// Create initial server state and initialize the SW.
scope = new SwTestHarnessBuilder().withServerState(originalServer).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
await driver.initialized;
originalServer.clearRequests();
// Verify that the `foo.hash.js` file is cached.
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
originalServer.assertNoRequestFor('/foo.hash.js');
// Update the server state to emulate deploying a new version (where `foo.hash.js` does not
// exist any more). Keep the cache though.
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withServerState(updatedServer)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
// The SW is still able to serve `foo.hash.js` from the cache.
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
updatedServer.assertNoRequestFor('/foo.hash.js');
// Remove `foo.hash.js` from the cache to emulate the browser evicting files from the cache.
await removeAssetFromCache(scope, manifest, '/foo.hash.js');
// Try to retrieve `foo.hash.js`, which is neither in the cache nor on the server.
// This should put the SW in an unrecoverable state and notify clients.
expect(await makeRequest(scope, '/foo.hash.js')).toBeNull();
updatedServer.assertSawRequestFor('/foo.hash.js');
// This should also enter the `SW` into degraded mode, because the broken version was the
// latest one.
expect(driver.state).toEqual(DriverReadyState.EXISTING_CLIENTS_ONLY);
});
});
describe('backwards compatibility with v5', () => {
beforeEach(() => {
const serverV5 = new MockServerStateBuilder()
@ -1925,16 +1762,6 @@ describe('Driver', () => {
});
})();
async function removeAssetFromCache(
scope: SwTestHarness, appVersionManifest: Manifest, assetPath: string) {
const assetGroupName =
appVersionManifest.assetGroups?.find(group => group.urls.includes(assetPath))?.name;
const cacheName = `${scope.cacheNamePrefix}:${sha1(JSON.stringify(appVersionManifest))}:assets:${
assetGroupName}:cache`;
const cache = await scope.caches.open(cacheName);
return cache.delete(assetPath);
}
async function makeRequest(
scope: SwTestHarness, url: string, clientId: string|null = 'default',
init?: Object): Promise<string|null> {

View File

@ -102,20 +102,6 @@ function _tryDefineProperty(obj: any, prop: string, desc: any, originalConfigura
try {
return _defineProperty(obj, prop, desc);
} catch (error) {
let swallowError = false;
if (prop === 'createdCallback' || prop === 'attachedCallback' ||
prop === 'detachedCallback' || prop === 'attributeChangedCallback') {
// We only swallow the error in registerElement patch
// this is the work around since some applications
// fail if we throw the error
swallowError = true;
}
if (!swallowError) {
throw error;
}
// TODO: @JiaLiPassion, Some application such as `registerElement` patch
// still need to swallow the error, in the future after these applications
// are updated, the following logic can be removed.
let descJson: string|null = null;
try {
descJson = JSON.stringify(desc);