Compare commits

..

37 Commits

Author SHA1 Message Date
c4553bbed9 release: cut the v10.1.5 release 2020-10-07 14:20:12 -07:00
cc13b37446 build(docs-infra): render optional decorator options with a ? (#39167)
Decorator API pages list all their available options in an overview
table and also in a detailed view. Now the rendered syntax of each
option will show a `?` after the name if the option is not required.
This is inline with how class and interface members are rendered.

PR Close #39167
2020-10-07 13:33:26 -07:00
166df5d2ca refactor(common): remove i (ignore case) regex flag where it is not needed (#39077)
It is not necessary to set IGNORECASE flag when the regex pattern does
not contain alphabetic characters

PR Close #39077
2020-10-07 13:32:00 -07:00
a5b474580c docs: change heading to a sub-heading (#39131)
This commit changes the heading of the section
`Use TypeScript path mapping for peer dependencies` to a sub-heading of
`Linked Libraries`.

Fixes #39130

PR Close #39131
2020-10-07 13:31:24 -07:00
9f132d0d93 docs: remove explicit boolean type in examples as TS infers it automatically (#39081)
In the `packages/examples/common/ngif/module.ts` file, the field `show` is given an explicit
boolean type. Since typescript infers boolean type, it is redundant and this commit removes it.

PR Close #39081
2020-10-06 08:39:02 -07:00
8dee378b3e docs: add info about working with fixup commits (#39110)
Using fixup commits when addressing review feedback can considerably
improve the review experience on subsequent reviews.

This commit adds information and guidelines for contributors on how to
work with fixup commits.

Fixes #33042

PR Close #39110
2020-10-06 08:38:35 -07:00
0845d1148f build: update bazelversion (#39123)
Updates to the latest version of bazel

PR Close #39123
2020-10-05 17:08:08 -07:00
7cfa57a5f7 ci: use larger resource classes for bazel builds (#39124)
Migrates to using larger resource classes for windows CI runs as well as updating
the bazel rcs for windows and linux to use all/more of the resources available in
the executors

PR Close #39124
2020-10-05 17:06:49 -07:00
f80c22002b test(compiler): add tests for parsing of malformed property reads (#38998)
The expression parser already has support for recovering on malformed
property reads, but did not have tests describing the recovered ast in
such cases. This commit adds tests to demonstrate such cases; in
particular, the recovered ast is a full PropertyRead but with an empty
property name. This is likely the most preferred option, as it does not
constrain consumers of the AST to what the property name should look
like. Furthermore, we cannot mark the property name as empty in any
other way (e.g. an EmptyExpr) because the property name, as of present,
is a string field rather than an AST itself.

Note that tokens past a malformed property read are not preserved in the
AST (for example in `foo.1234`, `1234` is not preserved in the AST).
This is because the extra tokens do not belong to the singular
expression created by the property read, and there is not a meaningful
way to interpret a secondary expression in a single parsed expression.

Part of #38596

PR Close #38998
2020-10-05 14:24:47 -07:00
7aa12412f3 docs: Migrate section, view encapsulation, from Component Styles topic into its own topic. (#38986)
PR Close #38986
2020-10-05 13:23:41 -07:00
072b707b38 ci: update to latest version of sauce-connect (#39073)
Update to the latest version of sauce-connect, 4.6.2.

PR Close #39073
2020-10-05 12:53:28 -07:00
223b80cb7d docs(common): update docs for HttpClient methods (#38878)
PR Close #38878
2020-10-05 12:43:48 -07:00
416403fc63 docs: Made a minor change in the documentation (#38917)
PR Close #38917
2020-10-05 12:43:15 -07:00
878e2f0deb feat(dev-infra): show CI status of all active release trains (#39067)
As part of the ng-dev caretaker check command, show the status of the
lastest CircleCI run for each active release train.

PR Close #39067
2020-10-05 10:23:34 -07:00
4c30f5135b docs(upgrade): expand upon change detection implications for downgraded components (#39100)
PR Close #39100
2020-10-05 08:08:31 -07:00
6791cd79af refactor(compiler): iteratively parse interpolations (#38977)
This patch refactors the interpolation parser to do so iteratively
rather than using a regex. Doing so prepares us for supporting granular
recovery on poorly-formed interpolations, for example when an
interpolation does not terminate (`{{ 1 + 2`) or is not terminated
properly (`{{ 1 + 2 {{ 2 + 3 }}`).

Part of #38596

PR Close #38977
2020-10-02 15:13:24 -07:00
f50313f54d feat(compiler): Recover on malformed keyed reads and keyed writes (#39004)
This patch adds support for recovering well-formed (and near-complete)
ASTs for semantically malformed keyed reads and keyed writes. See the
added tests for details on the types of semantics we can now recover;
in particular, notice that some assumptions are made about the form of
a keyed read/write intended by a user. For example, in the malformed
expression `a[1 + = 2`, we assume that the user meant to write a binary
expression for the key of `a`, and assign that key the value `2`. In
particular, we now parse this as `a[1 + <empty expression>] = 2`. There
are some different interpretations that can be made here, but I think
this is reasonable.

The actual changes in the parser code are fairly minimal (a nice
surprise!); the biggest addition is a `writeContext` that marks whether
the `=` operator can serve as a recovery point after error detection.

Part of #38596

PR Close #39004
2020-10-02 11:37:03 -07:00
30433a0710 fix(docs-infra): better distinguish wrapped headings from other entries in TOC (#39092)
Previously, when a heading was longer than the Table of Content's  (TOC)
width and it had to be wrapped into multiple lines, it was hard to
distinguish the subsequent lines from other TOC entries (i.e. other
headings).

This commit makes it easier to visually distinguish wrapped heading
lines from other headings by reducing the spacing between wrapped lines
of the same heading (making it more obvious that they belong together).

PR Close #39092
2020-10-02 10:59:41 -07:00
86ab9f92b4 Revert "build: upgrade angular build, integration/bazel and @angular/bazel package to rule_nodejs 2.2.0 (#37727)" (#39097)
This reverts commit db56cf18ba.

PR Close #39097
2020-10-02 10:56:53 -07:00
42f9679376 build: upgrade angular build, integration/bazel and @angular/bazel package to rule_nodejs 2.2.0 (#37727)
Updates to rules_nodejs 2.2.0. This is the first major release in 7 months and includes a number of features as well
as breaking changes.

Release notes: https://github.com/bazelbuild/rules_nodejs/releases/tag/2.0.0

Features of note for angular/angular:

* stdout/stderr/exit code capture; this could be potentially be useful

* TypeScript (ts_project); a simpler tsc rule that ts_library that can be used in the repo where ts_library is too
  heavy weight

Breaking changes of note for angular/angular:

* loading custom rules from npm packages: `ts_library` is no longer loaded from `@npm_bazel_typescript//:index.bzl`
  (which no longer exists) but is now loaded from `@npm//@bazel/typescript:index.bzl`

* with the loading changes above, `load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")` is
  no longer needed in the WORKSPACE which also means that yarn_install does not need to run unless building/testing
  a target that depends on @npm. In angular/angular this is a minor improvement as almost everything depends on @npm.

* @angular/bazel package is also updated in this PR to support the new load location; Angular + Bazel users that
  require it for ng_package (ng_module is no longer needed in OSS with Angular 10) will need to load from
  `@npm//@angular/bazel:index.bzl`. I investigated if it was possible to maintain backward compatability for the old
  load location `@npm_angular_bazel` but it is not since the package itself needs to be updated to load from
  `@npm//@bazel/typescript:index.bzl` instead of `@npm_bazel_typescript//:index.bzl` as it depends on ts_library
  internals for ng_module.

* runfiles.resolve will now throw instead of returning undefined to match behavior of node require

Other changes in angular/angular:

* integration/bazel has been updated to use both ng_module and ts_libary with use_angular_plugin=true.
  The latter is the recommended way for rules_nodejs users to compile Angular 10 with Ivy. Bazel + Angular ViewEngine is
  supported with @angular/bazel <= 9.0.5 and Angular <= 8. There is still Angular ViewEngine example on rules_nodejs
  https://github.com/bazelbuild/rules_nodejs/tree/stable/examples/angular_view_engine on these older versions but users
  that want to update to Angular 10 and are on Bazel must switch to Ivy and at that point ts_library with
  use_angular_plugin=true is more performant that ng_module. Angular example in rules_nodejs is configured this way
  as well: https://github.com/bazelbuild/rules_nodejs/tree/stable/examples/angular. As an aside, we also have an
  example of building Angular 10 with architect() rule directly instead of using ts_library with angular plugin:
  https://github.com/bazelbuild/rules_nodejs/tree/stable/examples/angular_bazel_architect.

NB: ng_module is still required for angular/angular repository as it still builds ViewEngine & @angular/bazel
also provides the ng_package rule. ng_module can be removed in the future if ViewEngine is no longer needed in
angular repo.

* JSModuleInfo provider added to ng_module. this is for forward compat for future rules_nodejs versions.
  @josephperrott, this touches `packages/bazel/src/external.bzl` which will make the sync to g3 non-trivial.

PR Close #37727
2020-10-01 15:34:37 -07:00
bee10574d8 ci: update g3 synced file list (#39084) (#39087)
Update the list of files synced into g3

PR Close #39087
2020-10-01 15:33:17 -07:00
afce0f5038 fix(dev-infra): correct matching logic for g3 comparison (#39084) (#39087)
Corrects the matching logic for g3 comparison check, previously more matches were
found than were correct.

PR Close #39087
2020-10-01 15:33:17 -07:00
bb19e61848 feat(dev-infra): add a command to verify NgBot YAML config syntax (#39071)
This commit adds a new command to the `ng-dev` suite, which verifies that the NgBot YAML config is
correct. It also adds this command to the `lint` CircleCI job so that we execute this check while
running CI.

This should help prevent syntax errors similar to the one introduced in:
393ce5574b

PR Close #39071
2020-10-01 12:31:45 -07:00
50e83e2566 refactor(docs-infra): leave TODO comments for aligning tslint with CLI (#39018)
This commit adds some TODO comments in `tslint.json` regardling rules
that need to be enabled or removed to more closely align `tslint.json`
with the one generated by the latest Angular CLI for new apps.

Updating these rules generates a lot of linting failures, so fixing
them is outside the scope of this PR.

PR Close #39018
2020-10-01 09:32:58 -07:00
d80c4890be build(docs-infra): remove the only-arrow-functions tslint rule to align with CLI (#39018)
This commit removes the `only-arrow-functions: false` tslint rule to
more closely align `tslint.json` with the one generated by the latest
Angular CLI for new apps.

PR Close #39018
2020-10-01 09:32:58 -07:00
4f8b716c13 build(docs-infra): update the object-literal-key-quotes tslint rule to align with CLI (#39018)
This commit updates the `object-literal-key-quotes` tslint rule to more
closely align `tslint.json` with the one generated by the latest Angular
CLI for new apps.

PR Close #39018
2020-10-01 09:32:58 -07:00
cb85d69450 build(docs-infra): remove the no-string-literal tslint rule to align with CLI (#39018)
This commit removes the `no-string-literal: false` tslint rule to
more closely align `tslint.json` with the one generated by the latest
Angular CLI for new apps.

PR Close #39018
2020-10-01 09:32:58 -07:00
4a446878fa build(docs-infra): enable the no-redundant-jsdoc tslint rule to align with CLI (#39018)
This commit enables the `no-redundant-jsdoc` tslint rule to more closely
align `tslint.json` with the one generated by the latest Angular CLI for
new apps.

PR Close #39018
2020-10-01 09:32:57 -07:00
cb80f46c64 refactor(docs-infra): more closely align tslint.json with CLI (#39018)
This commit re-organizes the `tslint.json` configuration file to more
closely align with the one generated by the latest Angular CLI for new
apps. This makes it easier to see the difference with new CLI versions
in the future and keep our `tslint.json` up-to-date (while keeping our
own rules).

NOTE:
This commit only re-orders rules and removes redundant ones. It does not
change the linting behavior.

PR Close #39018
2020-10-01 09:32:57 -07:00
5199d55d45 fix(dev-infra): run git fetch in quiet mode (#39068)
Runs git fetch with the -q flag during fetch while comparing the master and
g3 branches.

PR Close #39068
2020-10-01 09:30:15 -07:00
bb11fd9058 ci: change required labels for issue triage (#38932)
Issue triage _currently_ requires a component to be set and one of the following to be true for an issue to be
considered triaged:
* Marked as a bug _and_ has a severity _and_ has a frequency
* Mark as a feature
* Marked as a refactor
* Marked as a discussion
* Marked as "confusing"
* Marked as "use-case"

This PR changes the rules so that (in addition to the component), triage
requires:
* A priority label (P0 through P5)
* Marked as a feature
* Marked as a discussion

Triage may also apply additional, optional info labels to issues.

[This document outlines the new priority
scheme](https://docs.google.com/document/d/1mN2zWsr1pxChSTHC7UkOgl4PhhuoFONtG_zcMWeqLwA/preview).

While this PR is focused on issue triage and not PR triage, I have
changed the PR section triage to remove reference to the "effort: *" and
"risk: *" labels. Looking through recent PRs, Kapunahele is the only
person applying these, so it's clear that this bit is no longer widely
practiced.

This is just one step in the always-ongoing process of managing GitHub
labels. More adjustments will come over time. In writing this PR, I have
already unearthed a few more areas that can be polished in follow-ups.

PR Close #38932
2020-10-01 08:32:21 -07:00
771f7318f0 fix(router): update getRouteGuards to check if the context outlet is activated (#39049)
In certain circumstances (errors during component constructor) the router outlet may not be activated before
redirecting to a new route. If the new route requires running guards and resolvers the current logic will throw
when accessing outlet.component due to an isActivated check within the property getter.  This update brings the
logic inline with deactivateRouterAndItsChildren, namely checking outlet.isActivated before trying to access
outlet.component.

Fixes #39030

PR Close #39049
2020-09-30 14:58:47 -07:00
5d584b7728 ci: remove errant comma from angular robot config (#39066)
Remove superfluous comma from exclude list for g3 status in the angular
robot config.

PR Close #39066
2020-09-30 13:27:31 -07:00
1b5f6ee7a6 refactor(localize): avoid free-standing FileSystem functions (#39006)
These free standing functions rely upon the "current" `FileSystem`,
but it is safer to explicitly pass the `FileSystem` into functions or
classes that need it.

Fixes #38711

PR Close #39006
2020-09-30 12:49:44 -07:00
669e07580c refactor(compiler-cli): avoid free-standing FileSystem functions (#39006)
These free standing functions rely upon the "current" `FileSystem`,
but it is safer to explicitly pass the `FileSystem` into functions or
classes that need it.

PR Close #39006
2020-09-30 12:49:43 -07:00
965249d1da docs(localize): add public api markers for CLI integration (#39006)
This commit marks the functions and classes that are
used by the CLI.

PR Close #39006
2020-09-30 12:49:43 -07:00
3a01856e7c docs: fix typo in Pipes guide (aynchronous --> asynchronous) (#38996)
PR Close #38996
2020-09-30 12:46:27 -07:00
87 changed files with 1206 additions and 676 deletions

View File

@ -1,3 +1,3 @@
3.2.0
3.5.1
# [NB: this comment has to be after the first line, see https://github.com/bazelbuild/bazelisk/issues/117]
# When updating the Bazel version you also need to update the RBE toolchains version in package.bzl

View File

@ -14,8 +14,8 @@ build --repository_cache=/home/circleci/bazel_repository_cache
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
build --local_cpu_resources=8
build --local_ram_resources=14336
build --local_cpu_resources=20
build --local_ram_resources=32768
# All build executed remotely should be done using our RBE configuration.
build:remote --google_default_credentials

View File

@ -11,8 +11,8 @@ try-import %workspace%/.circleci/bazel.common.rc
build --repository_cache=C:/Users/circleci/bazel_repository_cache
# Manually set the local resources used in windows CI runs
build --local_ram_resources=13500
build --local_cpu_resources=4
build --local_ram_resources=120000
build --local_cpu_resources=32
# All windows jobs run on master and should use http caching
build --remote_http_cache=https://storage.googleapis.com/angular-team-cache

View File

@ -80,7 +80,7 @@ executors:
windows-executor:
working_directory: ~/ng
resource_class: windows.medium
resource_class: windows.2xlarge
# CircleCI windows VMs do have the GitBash shell available:
# https://github.com/CircleCI-Public/windows-preview-docs#shells
# But in this specific case we really should not use it because Bazel must not be ran from
@ -273,6 +273,7 @@ jobs:
- run: yarn -s ng-dev format changed $CI_GIT_BASE_REVISION --check
- run: yarn -s ts-circular-deps:check
- run: yarn -s ng-dev pullapprove verify
- run: yarn -s ng-dev ngbot verify
- run: yarn -s ng-dev commit-message validate-range --range $CI_COMMIT_RANGE
test:

View File

@ -41,7 +41,8 @@ copy .circleci\bazel.windows.rc ${Env:USERPROFILE}\.bazelrc
####################################################################################################
# Install specific version of node.
####################################################################################################
choco install nodejs --version 12.14.1 --no-progress
nvm install 12.14.1
nvm use 12.14.1
# These Bazel prereqs aren't needed because the CircleCI image already includes them.
# choco install yarn --version 1.16.0 --no-progress

View File

@ -38,6 +38,7 @@ merge:
- "modules/benchmarks/**"
- "modules/system.d.ts"
- "packages/**"
- "dev-infra/benchmark/driver-utilities/**"
# list of patterns to ignore for the files changed by the PR
exclude:
- "packages/*"
@ -47,8 +48,10 @@ merge:
- "packages/bazel/src/ng_package/**"
- "packages/bazel/src/protractor/**"
- "packages/bazel/src/schematics/**"
- "packages/compiler-cli/src/ngcc/**"
- "packages/compiler-cli/linker/**"
- "packages/compiler-cli/ngcc/**"
- "packages/compiler-cli/src/ngtsc/sourcemaps/**",
- "packages/compiler-cli/src/ngtsc/sourcemaps/**"
- "packages/docs/**"
- "packages/elements/schematics/**"
- "packages/examples/**"
@ -56,6 +59,8 @@ merge:
- "packages/localize/**"
- "packages/private/**"
- "packages/service-worker/**"
- "packages/common/locales/**"
- "packages/http/**"
- "**/.gitignore"
- "**/.gitkeep"
- "**/yarn.lock"
@ -137,24 +142,28 @@ triage:
# arrays of labels that determine if an issue has been fully triaged
l2TriageLabels:
-
- "type: bug/fix"
- "severity*"
- "freq*"
- "P0"
- "comp: *"
-
- "type: feature"
- "P1"
- "comp: *"
-
- "type: refactor"
- "P2"
- "comp: *"
-
- "type: RFC / Discussion / question"
- "P3"
- "comp: *"
-
- "type: confusing"
- "P4"
- "comp: *"
-
- "type: use-case"
- "P5"
- "comp: *"
-
- "feature"
- "comp: *"
-
- "discussion"
- "comp: *"
# options for the triage PR plugin

View File

@ -12,7 +12,7 @@ export const caretaker: CaretakerConfig = {
query: `is:pr is:open status:success label:"action: merge-assistance"`,
},
{
name: 'Primary Triage Queue',
name: 'Initial Triage Queue',
query: `is:open is:issue no:milestone`,
}
]

View File

@ -326,6 +326,7 @@ groups:
'aio/content/examples/component-interaction/**',
'aio/content/images/guide/component-interaction/**',
'aio/content/guide/component-styles.md',
'aio/content/guide/view-encapsulation.md',
'aio/content/examples/component-styles/**',
'aio/content/guide/dependency-injection.md',
'aio/content/examples/dependency-injection/**',
@ -1115,6 +1116,7 @@ groups:
'docs/DEBUG.md',
'docs/DEBUG_COMPONENTS_REPO_IVY.md',
'docs/DEVELOPER.md',
'docs/FIXUP_COMMITS.md',
'docs/GITHUB_PROCESS.md',
'docs/PR_REVIEW.md',
'docs/SAVED_REPLIES.md',

View File

@ -1,3 +1,14 @@
<a name="10.1.5"></a>
## 10.1.5 (2020-10-07)
### Bug Fixes
* **router:** update getRouteGuards to check if the context outlet is activated ([#39049](https://github.com/angular/angular/issues/39049)) ([771f731](https://github.com/angular/angular/commit/771f731)), closes [#39030](https://github.com/angular/angular/issues/39030)
* **compiler:** Recover on malformed keyed reads and keyed writes ([#39004](https://github.com/angular/angular/issues/39004)) ([f50313f](https://github.com/angular/angular/commit/f50313f)), closes [#38596](https://github.com/angular/angular/issues/38596)
<a name="10.1.4"></a>
## 10.1.4 (2020-09-30)

View File

@ -107,7 +107,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
Adherence to these conventions is necessary because release notes are automatically generated from these messages.
```shell
git commit -a
git commit --all
```
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
@ -119,16 +119,31 @@ Before you submit your Pull Request (PR) consider the following guidelines:
11. In GitHub, send a pull request to `angular:master`.
If we ask for changes via code reviews then:
* Make the required updates.
* Re-run the Angular test suites to ensure tests are still passing.
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
#### Addressing review feedback
```shell
git rebase master -i
git push -f
```
If we ask for changes via code reviews then:
1. Make the required updates to the code.
2. Re-run the Angular test suites to ensure tests are still passing.
3. Create a fixup commit and push to your GitHub repository (this will update your Pull Request):
```shell
git commit --all --fixup HEAD
git push
```
For more info on working with fixup commits see [here](docs/FIXUP_COMMITS.md).
> Fixup commits (as shown above) are preferred when addressing review feedback, but in some cases you may need to amend the original commit instead of creating a fixup commit (for example, if you want to update the commit message).
> To amend the last commit and update the Pull Request:
>
> ```shell
> git commit --all --amend
> git push --force-with-lease
> ```
That's it! Thank you for your contribution!

View File

@ -91,8 +91,8 @@ rbe_autoconfig(
# Need to specify a base container digest in order to ensure that we can use the checked-in
# platform configurations for the "ubuntu16_04" image. Otherwise the autoconfig rule would
# need to pull the image and run it in order determine the toolchain configuration. See:
# https://github.com/bazelbuild/bazel-toolchains/blob/3.2.0/configs/ubuntu16_04_clang/versions.bzl
base_container_digest = "sha256:5e750dd878df9fcf4e185c6f52b9826090f6e532b097f286913a428290622332",
# https://github.com/bazelbuild/bazel-toolchains/blob/3.5.1/configs/ubuntu16_04_clang/versions.bzl
base_container_digest = "sha256:f6568d8168b14aafd1b707019927a63c2d37113a03bcee188218f99bd0327ea1",
# Note that if you change the `digest`, you might also need to update the
# `base_container_digest` to make sure marketplace.gcr.io/google/rbe-ubuntu16-04-webtest:<digest>
# and marketplace.gcr.io/google/rbe-ubuntu16-04:<base_container_digest> have

View File

@ -119,7 +119,7 @@ The `/deep/` combinator also has the aliases `>>>`, and `::ng-deep`.
Use `/deep/`, `>>>` and `::ng-deep` only with *emulated* view encapsulation.
Emulated is the default and most commonly used view encapsulation. For more information, see the
[Controlling view encapsulation](guide/component-styles#view-encapsulation) section.
[View Encapsulation](guide/view-encapsulation) section.
</div>
@ -267,89 +267,3 @@ as explained in the [CLI wiki](https://github.com/angular/angular-cli/wiki/stori
Style strings added to the `@Component.styles` array _must be written in CSS_ because the CLI cannot apply a preprocessor to inline styles.
</div>
{@a view-encapsulation}
## View encapsulation
As discussed earlier, component CSS styles are encapsulated into the component's view and don't
affect the rest of the application.
To control how this encapsulation happens on a *per
component* basis, you can set the *view encapsulation mode* in the component metadata.
Choose from the following modes:
* `ShadowDom` view encapsulation uses the browser's native shadow DOM implementation (see
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
on the [MDN](https://developer.mozilla.org) site)
to attach a shadow DOM to the component's host element, and then puts the component
view inside that shadow DOM. The component's styles are included within the shadow DOM.
* `Native` view encapsulation uses a now deprecated version of the browser's native shadow DOM implementation - [learn about the changes](https://hayato.io/2016/shadowdomv1/).
* `Emulated` view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing
(and renaming) the CSS code to effectively scope the CSS to the component's view.
For details, see [Inspecting generated CSS](guide/component-styles#inspect-generated-css) below.
* `None` means that Angular does no view encapsulation.
Angular adds the CSS to the global styles.
The scoping rules, isolations, and protections discussed earlier don't apply.
This is essentially the same as pasting the component's styles into the HTML.
To set the components encapsulation mode, use the `encapsulation` property in the component metadata:
<code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.native" header="src/app/quest-summary.component.ts"></code-example>
`ShadowDom` view encapsulation only works on browsers that have native support
for shadow DOM (see [Shadow DOM v1](https://caniuse.com/#feat=shadowdomv1) on the
[Can I use](http://caniuse.com) site). The support is still limited,
which is why `Emulated` view encapsulation is the default mode and recommended
in most cases.
{@a inspect-generated-css}
## Inspecting generated CSS
When using emulated view encapsulation, Angular preprocesses
all component styles so that they approximate the standard shadow CSS scoping rules.
In the DOM of a running Angular application with emulated view
encapsulation enabled, each DOM element has some extra attributes
attached to it:
<code-example format="">
&lt;hero-details _nghost-pmm-5>
&lt;h2 _ngcontent-pmm-5>Mister Fantastic&lt;/h2>
&lt;hero-team _ngcontent-pmm-5 _nghost-pmm-6>
&lt;h3 _ngcontent-pmm-6>Team&lt;/h3>
&lt;/hero-team>
&lt;/hero-detail>
</code-example>
There are two kinds of generated attributes:
* An element that would be a shadow DOM host in native encapsulation has a
generated `_nghost` attribute. This is typically the case for component host elements.
* An element within a component's view has a `_ngcontent` attribute
that identifies to which host's emulated shadow DOM this element belongs.
The exact values of these attributes aren't important. They are automatically
generated and you never refer to them in application code. But they are targeted
by the generated component styles, which are in the `<head>` section of the DOM:
<code-example format="">
[_nghost-pmm-5] {
display: block;
border: 1px solid black;
}
h3[_ngcontent-pmm-6] {
background-color: white;
border: 1px solid #777;
}
</code-example>
These styles are post-processed so that each selector is augmented
with `_nghost` or `_ngcontent` attribute selectors.
These extra selectors enable the scoping rules described in this page.

View File

@ -156,7 +156,7 @@ The library must be rebuilt on every change.
When linking a library, make sure that the build step runs in watch mode, and that the library's `package.json` configuration points at the correct entry points.
For example, `main` should point at a JavaScript file, not a TypeScript file.
## Use TypeScript path mapping for peer dependencies
### Use TypeScript path mapping for peer dependencies
Angular libraries should list all `@angular/*` dependencies as peer dependencies.
This ensures that when modules ask for Angular, they all get the exact same module.

View File

@ -397,7 +397,7 @@ The following code example binds an observable of message strings
## Caching HTTP requests
To [communicate with backend services using HTTP](/guide/http "Communicating with backend services using HTTP"), the `HttpClient` service uses observables and offers the `HTTPClient.get()` method to fetch data from a server.
The aynchronous method sends an HTTP request, and returns an observable that emits the requested data for the response.
The asynchronous method sends an HTTP request, and returns an observable that emits the requested data for the response.
As shown in the previous section, you can use the impure `AsyncPipe` to accept an observable as input and subscribe to the input automatically.
You can also create an impure pipe to make and cache an HTTP request.

View File

@ -479,6 +479,15 @@ using the `downgradeComponent()` method. The result is an AngularJS
<code-example path="upgrade-module/src/app/downgrade-static/app.module.ts" region="downgradecomponent" header="app.module.ts">
</code-example>
<div class="alert is-helpful">
By default, Angular change detection will also run on the component for every
AngularJS `$digest` cycle. If you wish to only have change detection run when
the inputs change, you can set `propagateDigest` to `false` when calling
`downgradeComponent()`.
</div>
Because `HeroDetailComponent` is an Angular component, you must also add it to the
`declarations` in the `AppModule`.

View File

@ -0,0 +1,83 @@
# View encapsulation
In Angular, component CSS styles are encapsulated into the component's view and don't
affect the rest of the application.
To control how this encapsulation happens on a *per
component* basis, you can set the *view encapsulation mode* in the component metadata.
Choose from the following modes:
* `ShadowDom` view encapsulation uses the browser's native shadow DOM implementation (see
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
on the [MDN](https://developer.mozilla.org) site)
to attach a shadow DOM to the component's host element, and then puts the component
view inside that shadow DOM. The component's styles are included within the shadow DOM.
* `Native` view encapsulation uses a now deprecated version of the browser's native shadow DOM implementation - [learn about the changes](https://hayato.io/2016/shadowdomv1/).
* `Emulated` view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing
(and renaming) the CSS code to effectively scope the CSS to the component's view.
For details, see [Inspecting generated CSS](guide/view-encapsulation#inspect-generated-css) below.
* `None` means that Angular does no view encapsulation.
Angular adds the CSS to the global styles.
The scoping rules, isolations, and protections discussed earlier don't apply.
This is essentially the same as pasting the component's styles into the HTML.
To set the components encapsulation mode, use the `encapsulation` property in the component metadata:
<code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.native" header="src/app/quest-summary.component.ts"></code-example>
`ShadowDom` view encapsulation only works on browsers that have native support
for shadow DOM (see [Shadow DOM v1](https://caniuse.com/#feat=shadowdomv1) on the
[Can I use](http://caniuse.com) site). The support is still limited,
which is why `Emulated` view encapsulation is the default mode and recommended
in most cases.
{@a inspect-generated-css}
## Inspecting generated CSS
When using emulated view encapsulation, Angular preprocesses
all component styles so that they approximate the standard shadow CSS scoping rules.
In the DOM of a running Angular application with emulated view
encapsulation enabled, each DOM element has some extra attributes
attached to it:
<code-example format="">
&lt;hero-details _nghost-pmm-5>
&lt;h2 _ngcontent-pmm-5>Mister Fantastic&lt;/h2>
&lt;hero-team _ngcontent-pmm-5 _nghost-pmm-6>
&lt;h3 _ngcontent-pmm-6>Team&lt;/h3>
&lt;/hero-team>
&lt;/hero-detail>
</code-example>
There are two kinds of generated attributes:
* An element that would be a shadow DOM host in native encapsulation has a
generated `_nghost` attribute. This is typically the case for component host elements.
* An element within a component's view has a `_ngcontent` attribute
that identifies to which host's emulated shadow DOM this element belongs.
The exact values of these attributes aren't important. They are automatically
generated and you never refer to them in application code. But they are targeted
by the generated component styles, which are in the `<head>` section of the DOM:
<code-example format="">
[_nghost-pmm-5] {
display: block;
border: 1px solid black;
}
h3[_ngcontent-pmm-6] {
background-color: white;
border: 1px solid #777;
}
</code-example>
These styles are post-processed so that each selector is augmented
with `_nghost` or `_ngcontent` attribute selectors.
These extra selectors enable the scoping rules described in this page.

View File

@ -46,7 +46,7 @@ Most Angular code can be written with just the latest JavaScript, using [types](
<h3>You can sit with us!</h3>
We want to hear from you. [Report problems or submit suggestions for future docs.](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form")
We want to hear from you. [Report problems or submit suggestions for future docs](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form").
Contribute to Angular docs by creating
[pull requests](https://github.com/angular/angular/pulls "Angular Github pull requests")

View File

@ -116,6 +116,11 @@
"title": "Component Lifecycle",
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
},
{
"url": "guide/view-encapsulation",
"title": "View Encapsulation",
"tooltip": "Describes how component CSS styles are encapsulated into a component's view."
},
{
"url": "guide/component-interaction",
"title": "Component Interaction",

View File

@ -782,7 +782,7 @@ describe('AppComponent', () => {
it('should grab focus when the / key is pressed', () => {
const searchBox: SearchBoxComponent = fixture.debugElement.query(By.directive(SearchBoxComponent)).componentInstance;
spyOn(searchBox, 'focus');
window.document.dispatchEvent(new KeyboardEvent('keyup', { 'key': '/' }));
window.document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
fixture.detectChanges();
expect(searchBox.focus).toHaveBeenCalled();
});
@ -791,7 +791,7 @@ describe('AppComponent', () => {
const searchBox: SearchBoxComponent = fixture.debugElement.query(By.directive(SearchBoxComponent)).componentInstance;
spyOn(searchBox, 'focus');
component.showSearchResults = true;
window.document.dispatchEvent(new KeyboardEvent('keyup', { 'key': 'Escape' }));
window.document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
fixture.detectChanges();
expect(searchBox.focus).toHaveBeenCalled();
});
@ -968,23 +968,23 @@ describe('AppComponent', () => {
// Initially, `isTransitoning` is true.
expect(component.isTransitioning).toBe(true);
expect(toolbar.classes['transitioning']).toBe(true);
expect(toolbar.classes.transitioning).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isTransitioning).toBe(false);
expect(toolbar.classes['transitioning']).toBeFalsy();
expect(toolbar.classes.transitioning).toBeFalsy();
// While a document is being rendered, `isTransitoning` is set to true.
triggerDocViewerEvent('docReady');
fixture.detectChanges();
expect(component.isTransitioning).toBe(true);
expect(toolbar.classes['transitioning']).toBe(true);
expect(toolbar.classes.transitioning).toBe(true);
triggerDocViewerEvent('docRendered');
fixture.detectChanges();
expect(component.isTransitioning).toBe(false);
expect(toolbar.classes['transitioning']).toBeFalsy();
expect(toolbar.classes.transitioning).toBeFalsy();
});
it('should update the sidenav state as soon as a new document is inserted (but not before)', () => {
@ -1031,15 +1031,15 @@ describe('AppComponent', () => {
navigateTo('guide/pipes');
expect(component.pageId).toEqual('guide-pipes');
expect(container.properties['id']).toEqual('guide-pipes');
expect(container.properties.id).toEqual('guide-pipes');
navigateTo('news');
expect(component.pageId).toEqual('news');
expect(container.properties['id']).toEqual('news');
expect(container.properties.id).toEqual('news');
navigateTo('');
expect(component.pageId).toEqual('home');
expect(container.properties['id']).toEqual('home');
expect(container.properties.id).toEqual('home');
});
it('should not be affected by changes to the query', () => {
@ -1050,7 +1050,7 @@ describe('AppComponent', () => {
navigateTo('guide/other?search=http');
expect(component.pageId).toEqual('guide-other');
expect(container.properties['id']).toEqual('guide-other');
expect(container.properties.id).toEqual('guide-other');
});
});
@ -1125,7 +1125,7 @@ describe('AppComponent', () => {
function checkHostClass(type: string, value: string) {
const host = fixture.debugElement;
const classes: string = host.properties['className'];
const classes: string = host.properties.className;
const classArray = classes.split(' ').filter(c => c.indexOf(`${type}-`) === 0);
expect(classArray.length).toBeLessThanOrEqual(1, `"${classes}" should have only one class matching ${type}-*`);
expect(classArray).toEqual([`${type}-${value}`], `"${classes}" should contain ${type}-${value}`);
@ -1311,42 +1311,42 @@ class TestHttpClient {
// tslint:disable:quotemark
navJson = {
"TopBar": [
TopBar: [
{
"url": "features",
"title": "Features"
url: 'features',
title: 'Features',
},
{
"url": "no-title",
"title": "No Title"
url: 'no-title',
title: 'No Title',
},
],
"SideNav": [
SideNav: [
{
"title": "Core",
"tooltip": "Learn the core capabilities of Angular",
"children": [
title: 'Core',
tooltip: 'Learn the core capabilities of Angular',
children: [
{
"url": "guide/pipes",
"title": "Pipes",
"tooltip": "Pipes transform displayed values within a template."
url: 'guide/pipes',
title: 'Pipes',
tooltip: 'Pipes transform displayed values within a template.',
},
{
"url": "guide/bags",
"title": "Bags",
"tooltip": "Pack your bags for a code adventure."
}
]
url: 'guide/bags',
title: 'Bags',
tooltip: 'Pack your bags for a code adventure.',
},
],
},
{
"url": "api",
"title": "API",
"tooltip": "Details of the Angular classes and values."
}
url: 'api',
title: 'API',
tooltip: 'Details of the Angular classes and values.',
},
],
"docVersions": TestHttpClient.docVersions,
docVersions: TestHttpClient.docVersions,
"__versionInfo": TestHttpClient.versionInfo,
__versionInfo: TestHttpClient.versionInfo,
};
get(url: string) {

View File

@ -147,7 +147,7 @@ export class AppComponent implements OnInit {
// Compute the version picker list from the current version and the versions in the navigation map
combineLatest([
this.navigationService.versionInfo,
this.navigationService.navigationViews.pipe(map(views => views['docVersions'])),
this.navigationService.navigationViews.pipe(map(views => views.docVersions)),
]).subscribe(([versionInfo, versions]) => {
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet
const computedVersions: NavigationNode[] = [
@ -167,10 +167,10 @@ export class AppComponent implements OnInit {
});
this.navigationService.navigationViews.subscribe(views => {
this.footerNodes = views['Footer'] || [];
this.sideNavNodes = views['SideNav'] || [];
this.topMenuNodes = views['TopBar'] || [];
this.topMenuNarrowNodes = views['TopBarNarrow'] || this.topMenuNodes;
this.footerNodes = views.Footer || [];
this.sideNavNodes = views.SideNav || [];
this.topMenuNodes = views.TopBar || [];
this.topMenuNarrowNodes = views.TopBarNarrow || this.topMenuNodes;
});
this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi);

View File

@ -223,77 +223,77 @@ class TestApiService {
// tslint:disable:quotemark
const apiSections: ApiSection[] = [
{
"name": "common",
"title": "common",
"path": "api/common",
"deprecated": false,
"items": [
name: 'common',
title: 'common',
path: 'api/common',
deprecated: false,
items: [
{
"name": "class_1",
"title": "Class 1",
"path": "api/common/class_1",
"docType": "class",
"stability": "experimental",
"securityRisk": false,
name: 'class_1',
title: 'Class 1',
path: 'api/common/class_1',
docType: 'class',
stability: 'experimental',
securityRisk: false,
},
{
"name": "class_2",
"title": "Class 2",
"path": "api/common/class_2",
"docType": "class",
"stability": "stable",
"securityRisk": false,
name: 'class_2',
title: 'Class 2',
path: 'api/common/class_2',
docType: 'class',
stability: 'stable',
securityRisk: false,
},
{
"name": "directive_1",
"title": "Directive 1",
"path": "api/common/directive_1",
"docType": "directive",
"stability": "stable",
"securityRisk": true,
name: 'directive_1',
title: 'Directive 1',
path: 'api/common/directive_1',
docType: 'directive',
stability: 'stable',
securityRisk: true,
},
{
"name": "pipe_1",
"title": "Pipe 1",
"path": "api/common/pipe_1",
"docType": "pipe",
"stability": "stable",
"securityRisk": true,
name: 'pipe_1',
title: 'Pipe 1',
path: 'api/common/pipe_1',
docType: 'pipe',
stability: 'stable',
securityRisk: true,
},
]
],
},
{
"name": "core",
"title": "core",
"path": "api/core",
"deprecated": false,
"items": [
name: 'core',
title: 'core',
path: 'api/core',
deprecated: false,
items: [
{
"name": "class_3",
"title": "Class 3",
"path": "api/core/class_3",
"docType": "class",
"stability": "experimental",
"securityRisk": false,
name: 'class_3',
title: 'Class 3',
path: 'api/core/class_3',
docType: 'class',
stability: 'experimental',
securityRisk: false,
},
{
"name": "function_1",
"title": "Function 1",
"path": "api/core/function 1",
"docType": "function",
"stability": "deprecated",
"securityRisk": true,
name: 'function_1',
title: 'Function 1',
path: 'api/core/function 1',
docType: 'function',
stability: 'deprecated',
securityRisk: true,
},
{
"name": "const_1",
"title": "Const 1",
"path": "api/core/const_1",
"docType": "const",
"stability": "stable",
"securityRisk": false,
}
]
}
name: 'const_1',
title: 'Const 1',
path: 'api/core/const_1',
docType: 'const',
stability: 'stable',
securityRisk: false,
},
],
},
];
function getApiSections() { return apiSections; }

View File

@ -67,7 +67,7 @@ export class ApiService implements OnDestroy {
* API sections is an array of Angular top modules and metadata about their API documents (items).
* Updates `sections` observable
*
* @param {string} [src] - Name of the api list JSON file
* @param [src] - Name of the api list JSON file
*/
fetchSections(src?: string) {
// TODO: get URL by configuration?

View File

@ -20,13 +20,13 @@ export class PrettyPrinter {
}
private getPrettyPrintOne(): Promise<PrettyPrintOne> {
const ppo = (window as any)['prettyPrintOne'];
const ppo = (window as any).prettyPrintOne;
return ppo ? Promise.resolve(ppo) :
// `prettyPrintOne` is not on `window`, which means `prettify.js` has not been loaded yet.
// Import it; ad a side-effect it will add `prettyPrintOne` on `window`.
import('assets/js/prettify.js' as any)
.then(
() => (window as any)['prettyPrintOne'],
() => (window as any).prettyPrintOne,
err => {
const msg = `Cannot get prettify.js from server: ${err.message}`;
this.logger.error(new Error(msg));
@ -37,9 +37,9 @@ export class PrettyPrinter {
/**
* Format code snippet as HTML
* @param {string} code - the code snippet to format; should already be HTML encoded
* @param {string} [language] - The language of the code to render (could be javascript, html, typescript, etc)
* @param {string|number} [linenums] - Whether to display line numbers:
* @param code - the code snippet to format; should already be HTML encoded
* @param [language] - The language of the code to render (could be javascript, html, typescript, etc)
* @param [linenums] - Whether to display line numbers:
* - false: don't display
* - true: do display
* - number: do display but start at the given number

View File

@ -63,7 +63,7 @@ describe('ContributorListComponent', () => {
it('should set the query to the "GDE" group when user selects "GDE"', () => {
component = getComponent();
component.selectGroup('GDE');
expect(locationService.searchResult['group']).toBe('GDE');
expect(locationService.searchResult.group).toBe('GDE');
});
it('should set the query to the first group when user selects unknown name', () => {
@ -71,7 +71,7 @@ describe('ContributorListComponent', () => {
component.selectGroup('GDE'); // a legit group that isn't the first
component.selectGroup('foo'); // not a legit group name
expect(locationService.searchResult['group']).toBe('Angular');
expect(locationService.searchResult.group).toBe('Angular');
});
//// Test Helpers ////

View File

@ -29,7 +29,7 @@ export class ContributorListComponent implements OnInit {
private locationService: LocationService) { }
ngOnInit() {
const groupName = this.locationService.search()['group'] || '';
const groupName = this.locationService.search().group || '';
// no need to unsubscribe because `contributors` completes
this.contributorService.contributors
.subscribe(grps => {

View File

@ -63,7 +63,7 @@ describe('ResourceListComponent', () => {
it('should set the query to the "education" category when user selects "education"', () => {
component = getComponent();
component.selectCategory('education');
expect(locationService.searchResult['category']).toBe('education');
expect(locationService.searchResult.category).toBe('education');
});
it('should set the query to the first category when user selects unknown name', () => {
@ -71,7 +71,7 @@ describe('ResourceListComponent', () => {
component.selectCategory('education'); // a legit group that isn't the first
component.selectCategory('foo'); // not a legit group name
expect(locationService.searchResult['category']).toBe('development');
expect(locationService.searchResult.category).toBe('development');
});
//// Test Helpers ////

View File

@ -20,7 +20,7 @@ export class ResourceListComponent implements OnInit {
}
ngOnInit() {
const category = this.locationService.search()['category'] || '';
const category = this.locationService.search().category || '';
// Not using async pipe because cats appear twice in template
// No need to unsubscribe because categories observable completes.
this.resourceService.categories.subscribe(cats => {

View File

@ -86,62 +86,61 @@ describe('ResourceService', () => {
});
function getTestResources() {
// tslint:disable:quotemark
return {
"Cat 3": {
"order": 3,
"subCategories": {
"Cat3 SubCat1": {
"order": 2,
"resources": {
"Cat3 SubCat1 Res1": {
"desc": "Meetup in Barcelona, Spain. ",
"title": "Angular Beers",
"url": "http://www.meetup.com/AngularJS-Beers/"
'Cat 3': {
order: 3,
subCategories: {
'Cat3 SubCat1': {
order: 2,
resources: {
'Cat3 SubCat1 Res1': {
desc: 'Meetup in Barcelona, Spain. ',
title: 'Angular Beers',
url: 'http://www.meetup.com/AngularJS-Beers/',
},
"Cat3 SubCat1 Res2": {
"desc": "Angular Camps in Barcelona, Spain.",
"title": "Angular Camp",
"url": "http://angularcamp.org/"
}
}
'Cat3 SubCat1 Res2': {
desc: 'Angular Camps in Barcelona, Spain.',
title: 'Angular Camp',
url: 'http://angularcamp.org/',
},
},
},
"Cat3 SubCat2": {
"order": 1,
"resources": {
"Cat3 SubCat2 Res1": {
"desc": "A community index of components and libraries",
"title": "Catalog of Angular Components & Libraries",
"url": "https://a/b/c"
}
}
},
}
},
"Cat 1": {
"order": 1,
"subCategories": {
"Cat1 SubCat1": {
"order": 1,
"resources": {
"S S S": {
"desc": "SSS",
"title": "Sssss",
"url": "http://s/s/s"
'Cat3 SubCat2': {
order: 1,
resources: {
'Cat3 SubCat2 Res1': {
desc: 'A community index of components and libraries',
title: 'Catalog of Angular Components & Libraries',
url: 'https://a/b/c',
},
"A A A": {
"desc": "AAA",
"title": "Aaaa",
"url": "http://a/a/a"
},
"Z Z Z": {
"desc": "ZZZ",
"title": "Zzzzz",
"url": "http://z/z/z"
}
}
},
},
},
}
},
'Cat 1': {
order: 1,
subCategories: {
'Cat1 SubCat1': {
order: 1,
resources: {
'S S S': {
desc: 'SSS',
title: 'Sssss',
url: 'http://s/s/s',
},
'A A A': {
desc: 'AAA',
title: 'Aaaa',
url: 'http://a/a/a',
},
'Z Z Z': {
desc: 'ZZZ',
title: 'Zzzzz',
url: 'http://z/z/z',
},
},
},
},
},
};
}

View File

@ -86,7 +86,7 @@ describe('NavigationService', () => {
];
beforeEach(() => {
navService.navigationViews.subscribe(views => view = views['sideNav']);
navService.navigationViews.subscribe(views => view = views.sideNav);
httpMock.expectOne({}).flush({sideNav});
});
@ -254,7 +254,7 @@ describe('NavigationService', () => {
{...v, ...{ tooltip: v.title + '.'}})
);
navService.navigationViews.subscribe(views => actualDocVersions = views['docVersions']);
navService.navigationViews.subscribe(views => actualDocVersions = views.docVersions);
});
it('should extract the docVersions', () => {

View File

@ -41,7 +41,7 @@ export class SearchBoxComponent implements AfterViewInit {
* When we first show this search box we trigger a search if there is a search query in the URL
*/
ngAfterViewInit() {
const query = this.locationService.search()['search'];
const query = this.locationService.search().search;
if (query) {
this.query = this.decodeQuery(query);
this.doSearch();

View File

@ -26,7 +26,7 @@ function createIndex(loadIndexFn: IndexLoader): lunr.Index {
// The lunr typings are missing QueryLexer so we have to add them here manually.
const queryLexer = (lunr as any as { QueryLexer: { termSeparator: RegExp } }).QueryLexer;
queryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
return lunr(/** @this */function() {
return lunr(function() {
this.ref('path');
this.field('topics', { boost: 15 });
this.field('titleWords', { boost: 10 });
@ -44,7 +44,7 @@ function handleMessage(message: { data: WebWorkerMessage }): void {
const payload = message.data.payload;
switch (type) {
case 'load-index':
makeRequest(SEARCH_TERMS_URL, function(searchInfo: PageInfo[]) {
makeRequest(SEARCH_TERMS_URL, (searchInfo: PageInfo[]) => {
index = createIndex(loadIndex(searchInfo));
postMessage({ type, id, payload: true });
});
@ -94,7 +94,7 @@ function queryIndex(query: string): PageInfo[] {
results = index.search(query + ' ' + titleQuery);
}
// Map the hits into info about each page to be returned as results
return results.map(function(hit) { return pages[hit.ref]; });
return results.map(hit => pages[hit.ref]);
}
} catch (e) {
// If the search query cannot be parsed the index throws an error

View File

@ -11,7 +11,7 @@ export class Deployment {
* The deployment mode set from the environment provided at build time;
* or overridden by the `mode` query parameter: e.g. `...?mode=archive`
*/
mode: string = this.location.search()['mode'] || environment.mode;
mode: string = this.location.search().mode || environment.mode;
constructor(private location: LocationService) {}
}

View File

@ -14,7 +14,7 @@ export class GaService {
private previousUrl: string;
constructor(@Inject(WindowToken) private window: Window) {
this.ga('create', environment['gaId'] , 'auto');
this.ga('create', environment.gaId , 'auto');
}
locationChanged(url: string) {
@ -34,7 +34,7 @@ export class GaService {
}
ga(...args: any[]) {
const gaFn = (this.window as any)['ga'];
const gaFn = (this.window as any).ga;
if (gaFn) {
gaFn(...args);
}

View File

@ -100,7 +100,7 @@ export class LocationService {
/**
* Handle user's anchor click
*
* @param anchor {HTMLAnchorElement} - the anchor element clicked
* @param anchor The anchor element clicked
* @param button Number of the mouse button held down. 0 means left or none
* @param ctrlKey True if control key held down
* @param metaKey True if command or window key held down

View File

@ -13,14 +13,13 @@ import { Logger } from 'app/shared/logger.service';
* 1. Checks for available ServiceWorker updates once instantiated.
* 2. Re-checks every 6 hours.
* 3. Whenever an update is available, it activates the update.
*
* @property
* `updateActivated` {Observable<string>} - Emit the version hash whenever an update is activated.
*/
@Injectable()
export class SwUpdatesService implements OnDestroy {
private checkInterval = 1000 * 60 * 60 * 6; // 6 hours
private onDestroy = new Subject<void>();
/** Emit the version hash whenever an update is activated. */
updateActivated: Observable<string>;
constructor(appRef: ApplicationRef, private logger: Logger, private swu: SwUpdate) {

View File

@ -105,9 +105,7 @@
li {
box-sizing: border-box;
@include font-size(12);
@include line-height(16);
padding: 3px 0 3px 12px;
padding: 7px 0 7px 12px;
position: relative;
transition: all 0.3s ease-in-out;
@ -129,6 +127,7 @@
color: lighten($darkgray, 10);
overflow: visible;
@include font-size(12);
@include line-height(16);
display: table-cell;
}
@ -168,11 +167,11 @@
}
&:first-child:before {
top: 13px;
top: 15px;
}
&:last-child:before {
bottom: calc(100% - 14px);
bottom: calc(100% - 15px);
}
&:not(.active):hover a:before {

View File

@ -1,6 +1,6 @@
import { ApiPage } from './api.po';
describe('Api pages', function() {
describe('Api pages', () => {
it('should show direct subclasses of a class', () => {
const page = new ApiPage('api/forms/AbstractControlDirective');
expect(page.getDescendants('class', true)).toEqual(['ControlContainer', 'NgControl']);

View File

@ -1,7 +1,7 @@
import { browser, by, element, ElementFinder } from 'protractor';
import { SitePage } from './app.po';
describe('site App', function() {
describe('site App', () => {
let page: SitePage;
beforeEach(() => {

View File

@ -3,7 +3,7 @@ import { SitePage } from './app.po';
/* tslint:disable:max-line-length */
describe('onerror handler', function() {
describe('onerror handler', () => {
let page: SitePage;
beforeAll(() => {
@ -186,7 +186,7 @@ createViewNodes@???`);
});
async function callOnError(message: string, url?: string, line?: number, column?: number, error?: Error) {
await browser.executeScript(function() {
await browser.executeScript(() => {
// reset the ga queue
(window as any).ga.q.length = 0;
// post the error to the handler

View File

@ -12,12 +12,16 @@
{% for option in doc.members %}
<a id="{$ option.anchor $}"></a>
<table class="is-full-width option-table">
<thead><tr><th>
<div class="with-github-links">
<h3>{$ option.name $}</h3>
{$ github.githubLinks(option, versionInfo) $}
</div>
</th></tr></thead>
<thead>
<tr>
<th>
<div class="with-github-links">
<h3>{$ option.name $}</h3>
{$ github.githubLinks(option, versionInfo) $}
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
@ -26,8 +30,9 @@
</tr>
<tr>
<td>
<code-example language="ts" hideCopy="true" class="no-box api-heading{% if option.deprecated %} deprecated-api-item{% endif %}">
{$ option.name $}: {$ option.type | escape $}
<code-example language="ts" hideCopy="true"
class="no-box api-heading{% if option.deprecated %} deprecated-api-item{% endif %}">
{$ option.name $}{%- if option.isOptional %}?{% endif -%}: {$ option.type | escape $}
</code-example>
</td>
</tr>
@ -46,4 +51,4 @@
</table>
{% endfor %}
</section>
{% endblock %}
{% endblock %}

View File

@ -10,7 +10,7 @@
<tr class="option">
<td>
<a class="code-anchor" href="{$ doc.path $}#{$ option.anchor | urlencode $}">
<code>{$ option.name $}</code>
<code>{$ option.name $}{%- if option.isOptional %}?{% endif -%}</code>
</a>
</td>
<td>{$ option.shortDescription | marked $}</td>

View File

@ -11,32 +11,16 @@
]
},
"array-type": false,
"arrow-parens": false,
"arrow-return-shorthand": true,
"ban": [
true,
{"name": "fdescribe", "message": "Don't keep jasmine focus methods."},
{"name": "fit", "message": "Don't keep jasmine focus methods."}
],
"component-class-suffix": true,
"component-selector": [
true,
"element",
"aio",
"kebab-case"
],
"contextual-lifecycle": true,
"curly": true,
"deprecation": {
"severity": "warn"
"severity": "warning"
},
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"aio",
"camelCase"
],
"eofline": true,
"import-blacklist": [
true,
@ -48,13 +32,11 @@
"spaces"
]
},
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
@ -66,8 +48,8 @@
]
}
],
"no-conflicting-lifecycle": true,
"no-consecutive-blank-lines": false,
// TODO(gkalpak): Fix the code and enable this to align with CLI. (Failures: 114)
// "no-any": true,
"no-console": [
true,
"debug",
@ -77,24 +59,20 @@
"trace"
],
"no-empty": false,
"no-host-metadata-property": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"no-string-literal": false,
// TODO(gkalpak): Fix the code and enable this to align with CLI. (Failures: 59)
// "no-non-null-assertion": true,
"no-redundant-jsdoc": true,
// TODO(gkalpak): Fix the code and remove this to align with CLI.
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": false,
"object-literal-sort-keys": false,
"only-arrow-functions": false,
"ordered-imports": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
@ -113,9 +91,11 @@
"named": "never"
}
},
"template-banana-in-box": true,
"template-no-negated-async": true,
"trailing-comma": false,
// TODO(gkalpak): Fix the code and enable this to align with CLI. (Failures: 243)
// "typedef": [
// true,
// "call-signature"
// ],
"typedef-whitespace": {
"options": [
{
@ -134,26 +114,15 @@
}
]
},
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"variable-name": [
true,
"allow-leading-underscore",
"allow-pascal-case",
"ban-keywords",
"check-format",
"require-const-for-all-caps"
],
"template-accessibility-alt-text": true,
"template-accessibility-elements-content": true,
"template-accessibility-label-for": true,
"template-accessibility-tabindex-no-positive": true,
"template-accessibility-table-scope": true,
"template-accessibility-valid-aria": true,
"template-click-events-have-key-events": true,
"template-mouse-events-have-key-events": true,
"template-no-autofocus": true,
"template-no-distracting-elements": true,
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-leading-underscore",
"allow-pascal-case",
"require-const-for-all-caps"
]
},
"whitespace": {
"options": [
"check-branch",
@ -163,6 +132,43 @@
"check-type",
"check-typecast"
]
}
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-accessibility-alt-text": true,
"template-accessibility-elements-content": true,
"template-accessibility-label-for": true,
"template-accessibility-tabindex-no-positive": true,
"template-accessibility-table-scope": true,
"template-accessibility-valid-aria": true,
"template-banana-in-box": true,
"template-click-events-have-key-events": true,
"template-mouse-events-have-key-events": true,
"template-no-autofocus": true,
"template-no-distracting-elements": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"directive-selector": [
true,
"attribute",
"aio",
"camelCase"
],
"component-selector": [
true,
"element",
"aio",
"kebab-case"
]
}
}

View File

@ -11,6 +11,7 @@ ts_library(
"//dev-infra/caretaker",
"//dev-infra/commit-message",
"//dev-infra/format",
"//dev-infra/ngbot",
"//dev-infra/pr",
"//dev-infra/pullapprove",
"//dev-infra/release",

View File

@ -8,6 +8,7 @@ ts_library(
module_name = "@angular/dev-infra-private/caretaker",
visibility = ["//dev-infra:__subpackages__"],
deps = [
"//dev-infra/release/versioning",
"//dev-infra/utils",
"@npm//@types/node",
"@npm//@types/node-fetch",

View File

@ -7,52 +7,55 @@
*/
import fetch from 'node-fetch';
import {fetchActiveReleaseTrains} from '../../release/versioning/index';
import {bold, green, info, red} from '../../utils/console';
import {bold, debug, info} from '../../utils/console';
import {GitClient} from '../../utils/git';
/** The results of checking the status of CI. */
interface StatusCheckResult {
status: 'success'|'failed'|'canceled'|'infrastructure_fail'|'timedout'|'failed'|'no_tests';
timestamp: Date;
buildUrl: string;
status: 'success'|'failed';
}
/** Retrieve and log status of CI for the project. */
export async function printCiStatus(git: GitClient) {
const releaseTrains = await fetchActiveReleaseTrains({api: git.github, ...git.remoteConfig});
info.group(bold(`CI`));
// TODO(josephperrott): Expand list of branches checked to all active branches.
await printStatus(git, 'master');
for (const [trainName, train] of Object.entries(releaseTrains)) {
if (train === null) {
debug(`No active release train for ${trainName}`);
continue;
}
const status = await getStatusOfBranch(git, train.branchName);
await printStatus(`${trainName.padEnd(6)} (${train.branchName})`, status);
}
info.groupEnd();
info();
}
/** Log the status of CI for a given branch to the console. */
async function printStatus(git: GitClient, branch: string) {
const result = await getStatusOfBranch(git, branch);
const branchName = branch.padEnd(10);
if (result === null) {
async function printStatus(label: string, status: StatusCheckResult|null) {
const branchName = label.padEnd(16);
if (status === null) {
info(`${branchName} was not found on CircleCI`);
} else if (result.status === 'success') {
} else if (status.status === 'success') {
info(`${branchName}`);
} else {
info(`${branchName} (Ran at: ${result.timestamp.toLocaleString()})`);
info(`${branchName}`);
}
}
/** Get the CI status of a given branch from CircleCI. */
async function getStatusOfBranch(git: GitClient, branch: string): Promise<StatusCheckResult|null> {
const {owner, name} = git.remoteConfig;
const url = `https://circleci.com/api/v1.1/project/gh/${owner}/${name}/tree/${
branch}?limit=1&filter=completed&shallow=true`;
const result = (await fetch(url).then(result => result.json()))?.[0];
const url = `https://circleci.com/gh/${owner}/${name}/tree/${branch}.svg?style=shield`;
const result = await fetch(url).then(result => result.text());
if (result) {
if (result && !result.includes('no builds')) {
return {
status: result.outcome,
timestamp: new Date(result.stop_time),
buildUrl: result.build_url
status: result.includes('passing') ? 'success' : 'failed',
};
}
return null;

View File

@ -44,7 +44,8 @@ export async function printG3Comparison(git: GitClient) {
/** Url of the ref for fetching master and g3 branches. */
const refUrl = `https://github.com/${git.remoteConfig.owner}/${git.remoteConfig.name}.git`;
/** The result fo the fetch command. */
const fetchResult = git.runGraceful(['fetch', refUrl, `master:${masterRef}`, `g3:${g3Ref}`]);
const fetchResult =
git.runGraceful(['fetch', '-q', refUrl, `master:${masterRef}`, `g3:${g3Ref}`]);
// If the upstream repository does not have a g3 branch to compare to, skip the comparison.
if (fetchResult.status !== 0) {
@ -118,6 +119,6 @@ export async function printG3Comparison(git: GitClient) {
/** Determine whether the file name passes both include and exclude checks. */
function checkMatchAgainstIncludeAndExclude(
file: string, includes: string[], excludes: string[]) {
return multimatch(multimatch(file, includes), excludes, {flipNegate: true}).length !== 0;
return multimatch(file, includes).length >= 1 && multimatch(file, excludes).length === 0;
}
}

View File

@ -15,6 +15,7 @@ import {buildReleaseParser} from './release/cli';
import {buildPrParser} from './pr/cli';
import {captureLogOutputForCommand} from './utils/console';
import {buildCaretakerParser} from './caretaker/cli';
import {buildNgbotParser} from './ngbot/cli';
yargs.scriptName('ng-dev')
.middleware(captureLogOutputForCommand)
@ -27,6 +28,7 @@ yargs.scriptName('ng-dev')
.command('release <command>', '', buildReleaseParser)
.command('ts-circular-deps <command>', '', tsCircularDependenciesBuilder)
.command('caretaker <command>', '', buildCaretakerParser)
.command('ngbot <command>', false, buildNgbotParser)
.wrap(120)
.strict()
.parse();

View File

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

19
dev-infra/ngbot/cli.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as yargs from 'yargs';
import {verify} from './verify';
/** Build the parser for the NgBot commands. */
export function buildNgbotParser(localYargs: yargs.Argv) {
return localYargs.help().strict().demandCommand().command(
'verify', 'Verify the NgBot config', {}, () => verify());
}
if (require.main === module) {
buildNgbotParser(yargs).parse();
}

31
dev-infra/ngbot/verify.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {readFileSync} from 'fs';
import {resolve} from 'path';
import {parse as parseYaml} from 'yaml';
import {getRepoBaseDir} from '../utils/config';
import {error, green, info, red} from '../utils/console';
export function verify() {
/** Full path to NgBot config file */
const NGBOT_CONFIG_YAML_PATH = resolve(getRepoBaseDir(), '.github/angular-robot.yml');
/** The NgBot config file */
const ngBotYaml = readFileSync(NGBOT_CONFIG_YAML_PATH, 'utf8');
try {
// Try parsing the config file to verify that the syntax is correct.
parseYaml(ngBotYaml);
info(`${green('√')} Valid NgBot YAML config`);
} catch (e) {
error(`${red('!')} Invalid NgBot YAML config`);
error(e);
process.exitCode = 1;
}
}

95
docs/FIXUP_COMMITS.md Normal file
View File

@ -0,0 +1,95 @@
# Working with fixup commits
This document provides information and guidelines for working with fixup commits:
- [What are fixup commits](#about-fixup-commits)
- [Why use fixup commits](#why-fixup-commits)
- [Creating fixup commits](#create-fixup-commits)
- [Squashing fixup commits](#squash-fixup-commits)
[This blog post](https://thoughtbot.com/blog/autosquashing-git-commits) is also a good resource on the subject.
## <a name="about-fixup-commits"></a> What are fixup commits
At their core, fixup commits are just regular commits with a special commit message:
The first line of their commit message starts with "fixup! " (notice the space after "!") followed by the first line of the commit message of an earlier commit (it doesn't have to be the immediately preceding one).
The purpose of a fixup commit is to modify an earlier commit.
I.e. it allows adding more changes in a new commit, but "marking" them as belonging to an earlier commit.
`Git` provides tools to make it easy to squash fixup commits into the original commit at a later time (see [below](#squash-fixup-commits) for details).
For example, let's assume you have added the following commits to your branch:
```
feat: first commit
fix: second commit
```
If you want to add more changes to the first commit, you can create a new commit with the commit message:
`fixup! feat: first commit`:
```
feat: first commit
fix: second commit
fixup! feat: first commit
```
## <a name="why-fixup-commits"></a> Why use fixup commits
So, when are fixup commits useful?
During the life of a Pull Request, a reviewer might request changes.
The Pull Request author can make the requested changes and submit them for another review.
Normally, these changes should be part of one of the original commits of the Pull Request.
However, amending an existing commit with the changes makes it difficult for the reviewer to know exactly what has changed since the last time they reviewed the Pull Request.
Here is where fixup commits come in handy.
By addressing review feedback in fixup commits, you make it very straight forward for the reviewer to see what are the new changes that need to be reviewed and verify that their earlier feedback has been addressed.
This can save a lot of effort, especially on larger Pull Requests (where having to re-review _all_ the changes is pretty wasteful).
When the time comes to merge the Pull Request into the repository, the merge script [knows how to automatically squash](../dev-infra/pr/merge/strategies/autosquash-merge.ts) fixup commits with the corresponding regular commits.
## <a name="create-fixup-commits"></a> Creating fixup commits
As mentioned [above](#about-fixup-commits), the only thing that differentiates a fixup commit from a regular commit is the commit message.
You can create a fixup commit by specifying an appropriate commit message (i.e. `fixup! <original-commit-message-subject>`).
In addition, the `git` command-line tool provides an easy way to create a fixup commit via [git commit --fixup](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupltcommitgt):
```sh
# Create a fixup commit to fix up the last commit on the branch:
git commit --fixup HEAD ...
# Create a fixup commit to fix up commit with SHA <COMMIT_SHA>:
git commit --fixup <COMMIT_SHA> ...
```
## <a name="squash-fixup-commits"></a> Squashing fixup commits
As mentioned above, the merge script will [automatically squash](../dev-infra/pr/merge/strategies/autosquash-merge.ts) fixup commits.
However, sometimes you might want to manually squash a fixup commit.
### Rebasing to squash fixup commits
The easiest way to re-order and squash any commit is via [rebasing interactively](https://git-scm.com/docs/git-rebase#_interactive_mode). You move a commit right after the one you want to squash it into in the rebase TODO list and change the corresponding action from `pick` to `fixup`.
`Git` can do all these automatically for you if you pass the `--autosquash` option to `git rebase`.
See the [`git` docs](https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash) for more details.
### Additional options
You may like to consider some optional configurations:
#### Configuring `git` to auto-squash by default
By default, `git` will not automatically squash fixup commits when interactively rebasing.
If you prefer to not have to pass the `--autosquash` option every time, you can change the default behavior by setting the `rebase.autoSquash` `git` config option to true.
See the [`git` docs](https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt-rebaseautoSquash) for more details.
If you have `rebase.autoSquash` set to true, you can pass the `--no-autosquash` option to `git rebase` to override and disable this setting.

View File

@ -2,7 +2,7 @@
This document describes how the Angular team uses labels and milestones to triage issues on GitHub.
The basic idea of the process is that caretaker only assigns a component (`comp: *`) label.
The owner of the component is then responsible for the secondary / component-level triage.
The owner of the component is then responsible for the detailed / component-level triage.
## Label Types
@ -10,7 +10,8 @@ The owner of the component is then responsible for the secondary / component-lev
### Components
The caretaker should be able to determine which component the issue belongs to.
The components have a clear piece of source code associated with it within the `/packages/` folder of this repo.
The components have a clear piece of source code associated with it within the `/packages/` folder
of this repo.
* `comp: animations`
* `comp: bazel` - @angular/bazel rules
@ -48,84 +49,80 @@ We will treat them as a component even thought no specific source tree is associ
* `comp: performance`
* `comp: security`
Sometimes, especially in the case of cross-cutting issues or PRs, these PRs or issues belong to multiple components.
In these cases, all applicable component labels should be used to triage the issue or PR.
Sometimes, especially in the case of cross-cutting issues or PRs, these PRs or issues belong to
multiple components. In these cases, all applicable component labels should be used to triage the
issue or PR.
## Caretaker Triage Process (Initial Triage)
The caretaker assigns `comp: *` labels to new issues as they come in.
Untriaged issues can be found by selecting the issues with no milestone.
If an issue or PR obviously relates to a release regression, the caretaker must assign an
appropriate priority (`P0` or `P1`) and ensure that someone from the team is actively working to
resolve it.
Initial triage should occur daily so that issues can move into detailed triage.
Once the initial triage is done, the ng-bot automatically adds the milestone `needs triage`.
## Detailed Triage
Detailed triage can be done by anyone familiar with the issue subject matter.
### Step 1: Does the issue have enough information?
Gauge whether the issue has enough information to act upon. This typically includes a test case
via StackBlitz or GitHub and steps to reproduce. If the issue may be legitimate but needs more
information, add the "needs clarification" label. These labels can be revisted if the author can
provide further clarification. If the issue does have enough information, move on to step 2.
### Step 2: Bug, feature, or discussion?
By default, all issues are considered bugs. Bug reports require only a priority label.
If the issue is a feature request, apply the "feature" label. Use your judgement to determine
whether the feature request is reasonable. If it's clear that the issue requests something
infeasible, close the issue with a comment explaining why.
If the issue is an RFC or discussion, apply the "discussion" label. Use your judgement to determine
whether this discussion belongs on GitHub. Discussions here should pertain to the technical
implementation details of Angular. Redirect requests for debugging help or advice to a more
appropriate channel unless they're capturing a legitimate bug.
### Step 3: Set a Priority
For bug reports, set a priority label.
| Label | Description |
|----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| P0 | An issue that causes a full outage, breakage, or major function unavailability for everyone, without any known workaround. The issue must be fixed immediately, taking precedence over all other work. Should receive updates at least once per day. |
| P1 | An issue that significantly impacts a large percentage of users; if there is a workaround it is partial or overly painful. The issue should be resolved before the next release. |
| P2 | The issue is important to a large percentage of users, with a workaround. Issues that are significantly ugly or painful (especially first-use or install-time issues). Issues with workarounds that would otherwise be P0 or P1. |
| P3 | An issue that is relevant to core functions, but does not impede progress. Important, but not urgent. |
| P4 | A relatively minor issue that is not relevant to core functions, or relates only to the attractiveness or pleasantness of use of the system. Good to have but not necessary changes/fixes. |
| P5 | The team acknowledges the request but (due to any number of reasons) does not plan to work on or accept contributions for this request. The issue remains open for discussion. |
### Type
Issues marked with "feature" or "discussion" don't require a priority.
What kind of problem is this?
### Step 4: Apply additional information labels
* `type: RFC / discussion / question`
* `type: bug`
* `type: docs`
* `type: feature`
* `type: performance`
* `type: refactor`
* `type: use-case`
* `type: confusing`
Many optional labels provide additional context for issues. Consider adding any of the following if
they apply to the issue:
* Browser or operating system labels (`windows`, `ie11`, etc.)
* Labels that inform the severity (`regression`, `has workaround`, `no workaround`)
* Labels that categorize the bug (`performance`, `refactoring`, `memory leak`)
* Community engagement labels (`good first issue`)
## Caretaker Triage Process (Primary Triage)
It is the caretaker's responsibility to assign `comp: *` to each new issue as they come in.
Issues that haven't been triaged can be found by selecting the issues with no milestone.
If it's obvious that the issue or PR is related to a release regression, the caretaker is also responsible for assigning the `severity(5): regression` label to make the issue or PR highly visible.
The primary triage should be done on a daily basis so that the issues become available for secondary triage without much of delay.
The reason why we limit the responsibility of the caretaker to this one label is that it is likely that without domain knowledge the caretaker could mislabel issues or lack knowledge of duplicate issues.
Once the primary triage is done, the ng-bot automatically adds the milestone `needsTriage`.
## Component's owner Triage Process
The component owner is responsible for assigning one of the labels from each of these categories to the issues that have the milestone `needsTriage`:
- `type: *`
- `frequency: *` (only required for `type: bug/fix`)
- `severity: *` (only required for `type: bug/fix`)
We've adopted the issue categorization based on [user pain](http://www.lostgarden.com/2008/05/improving-bug-triage-with-user-pain.html) used by AngularJS. In this system every issue is assigned frequency and severity based on which the total user pain score is calculated.
The issues with type `type: feature`, `type: refactor` and `type: RFC / Discussion / question` do not require a frequency and severity.
Following is the definition of various frequency and severity levels:
1. `freq(score): *` How often does this issue come up? How many developers does this affect?
* low (1) - obscure issue affecting a handful of developers
* moderate (2) - impacts auxiliary usage patterns, only small number of applications are affected
* high (3) - impacts primary usage patterns, affecting most Angular apps
* critical (4) - impacts all Angular apps
1. `severity(score): *` - How bad is the issue?
* inconvenience (1) - causes ugly/boilerplate code in apps
* confusing (2) - unexpected or inconsistent behavior; hard-to-debug
* broken expected use (3) - it's hard or impossible for a developer using Angular to accomplish something that Angular should be able to do
* memory leak (4)
* regression (5) - functionality that used to work no longer works in a new release due to an unintentional change
* security issue (6)
These criteria are then used to calculate a "user pain" score as follows:
`pain = severity × frequency`
This score can then be used to estimate the impact of the issue which helps with prioritization.
Once the component's owner triage is done, the ng-bot automatically changes the milestone from `needsTriage` to `Backlog`.
Once this triage is done, the ng-bot automatically changes the milestone from `needs triage` to
`Backlog`.
## Triaging PRs
Triaging PRs is the same as triaging issues, except that the labels `frequency: *` and `severity: *` are replaced by:
- `effort*`
- `risk: *`
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:
PRs labels signal their state. Every triaged PR must have a `action: *` label assigned to it:
* `action: discuss`: Discussion is needed, to be led by the author.
* _**Who adds it:** Typically the PR author._

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.1.4",
"version": "10.1.5",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@ -194,7 +194,7 @@
"nock": "^13.0.3",
"ora": "^5.0.0",
"rewire": "2.5.2",
"sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz",
"sauce-connect": "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz",
"semver": "^6.3.0",
"ts-node": "^8.6.2",
"tslint-eslint-rules": "5.4.0",

View File

@ -29,11 +29,11 @@ def rules_angular_dev_dependencies():
_maybe(
http_archive,
name = "bazel_toolchains",
sha256 = "db48eed61552e25d36fe051a65d2a329cc0fb08442627e8f13960c5ab087a44e",
strip_prefix = "bazel-toolchains-3.2.0",
sha256 = "698d52e561cc8b5f502dc1ce83b6db72d5dabed4ae756393fb326b0243e4e20c",
strip_prefix = "bazel-toolchains-3.5.1",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/3.2.0/bazel-toolchains-3.2.0.tar.gz",
"https://github.com/bazelbuild/bazel-toolchains/releases/download/3.2.0/bazel-toolchains-3.2.0.tar.gz",
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/3.5.1/bazel-toolchains-3.5.1.tar.gz",
"https://github.com/bazelbuild/bazel-toolchains/releases/download/3.5.1/bazel-toolchains-3.5.1.tar.gz",
],
)

View File

@ -2526,8 +2526,8 @@ export class HttpClient {
}): Observable<HttpResponse<Object>>;
/**
* Constructs a `PUT` request that interprets the body as a JSON object and returns the full HTTP
* response.
* Constructs a `PUT` request that interprets the body as an instance of the requested type and
* returns the full HTTP response.
*
* @param url The endpoint URL.
* @param body The resources to add/update.
@ -2545,14 +2545,14 @@ export class HttpClient {
}): Observable<HttpResponse<T>>;
/**
* Constructs a `PUT` request that interprets the body as a JSON object and returns the response
* body as a JSON object.
* Constructs a `PUT` request that interprets the body as a JSON object
* and returns an observable of JSON object.
*
* @param url The endpoint URL.
* @param body The resources to add/update.
* @param options HTTP options
*
* @return An `Observable` of the response, with the response body as a JSON object.
* @return An `Observable` of the response as a JSON object.
*/
put(url: string, body: any|null, options?: {
headers?: HttpHeaders|{[header: string]: string | string[]},
@ -2564,15 +2564,14 @@ export class HttpClient {
}): Observable<Object>;
/**
* Constructs a `PUT` request that interprets the body as a JSON object
* and returns an observable of the response.
* Constructs a `PUT` request that interprets the body as an instance of the requested type
* and returns an observable of the requested type.
*
* @param url The endpoint URL.
* @param body The resources to add/update.
* @param options HTTP options
*
* @return An `Observable` of the `HTTPResponse` for the request, with a response body in the
* requested type.
* @return An `Observable` of the requested type.
*/
put<T>(url: string, body: any|null, options?: {
headers?: HttpHeaders|{[header: string]: string | string[]},

View File

@ -309,10 +309,7 @@ function toKeyValue(obj: {[k: string]: unknown}) {
* Logic from https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1437
*/
function encodeUriSegment(val: string) {
return encodeUriQuery(val, true)
.replace(/%26/gi, '&')
.replace(/%3D/gi, '=')
.replace(/%2B/gi, '+');
return encodeUriQuery(val, true).replace(/%26/g, '&').replace(/%3D/gi, '=').replace(/%2B/gi, '+');
}
@ -331,7 +328,7 @@ function encodeUriSegment(val: string) {
*/
function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) {
return encodeURIComponent(val)
.replace(/%40/gi, '@')
.replace(/%40/g, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')

View File

@ -8,7 +8,7 @@
import {removeComments, removeMapFileComments} from 'convert-source-map';
import {decode, encode, SourceMapMappings, SourceMapSegment} from 'sourcemap-codec';
import {AbsoluteFsPath, dirname, relative} from '../../file_system';
import {AbsoluteFsPath, FileSystem} from '../../file_system';
import {RawSourceMap} from './raw_source_map';
import {compareSegments, offsetSegment, SegmentMarker} from './segment_marker';
@ -38,7 +38,9 @@ export class SourceFile {
/** Whether this source file's source map was inline or external. */
readonly inline: boolean,
/** Any source files referenced by the raw source map associated with this source file. */
readonly sources: (SourceFile|null)[]) {
readonly sources: (SourceFile|null)[],
private fs: FileSystem,
) {
this.contents = removeSourceMapComments(contents);
this.startOfLinePositions = computeStartOfLinePositions(this.contents);
this.flattenedMappings = this.flattenMappings();
@ -75,11 +77,11 @@ export class SourceFile {
mappings[line].push(mappingArray);
}
const sourcePathDir = dirname(this.sourcePath);
const sourcePathDir = this.fs.dirname(this.sourcePath);
const sourceMap: RawSourceMap = {
version: 3,
file: relative(sourcePathDir, this.sourcePath),
sources: sources.map(sf => relative(sourcePathDir, sf.sourcePath)),
file: this.fs.relative(sourcePathDir, this.sourcePath),
sources: sources.map(sf => this.fs.relative(sourcePathDir, sf.sourcePath)),
names,
mappings: encode(mappings),
sourcesContent: sources.map(sf => sf.contents),

View File

@ -7,7 +7,7 @@
*/
import {commentRegex, fromComment, mapFileCommentRegex} from 'convert-source-map';
import {absoluteFrom, AbsoluteFsPath, FileSystem} from '../../file_system';
import {AbsoluteFsPath, FileSystem} from '../../file_system';
import {Logger} from '../../logging';
import {RawSourceMap} from './raw_source_map';
@ -83,7 +83,7 @@ export class SourceFileLoader {
inline = mapAndPath.mapPath === null;
}
return new SourceFile(sourcePath, contents, map, inline, sources);
return new SourceFile(sourcePath, contents, map, inline, sources, this.fs);
} catch (e) {
this.logger.warn(
`Unable to fully load ${sourcePath} for source-map flattening: ${e.message}`);
@ -124,7 +124,7 @@ export class SourceFileLoader {
}
}
const impliedMapPath = absoluteFrom(sourcePath + '.map');
const impliedMapPath = this.fs.resolve(sourcePath + '.map');
if (this.fs.exists(impliedMapPath)) {
return {map: this.readRawSourceMap(impliedMapPath), mapPath: impliedMapPath};
}

View File

@ -7,7 +7,7 @@
*/
import {encode} from 'sourcemap-codec';
import {absoluteFrom} from '../../file_system';
import {absoluteFrom, FileSystem, getFileSystem} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {RawSourceMap} from '../src/raw_source_map';
import {SegmentMarker} from '../src/segment_marker';
@ -15,9 +15,11 @@ import {computeStartOfLinePositions, ensureOriginalSegmentLinks, extractOriginal
runInEachFileSystem(() => {
describe('SourceFile and utilities', () => {
let fs: FileSystem;
let _: typeof absoluteFrom;
beforeEach(() => {
fs = getFileSystem();
_ = absoluteFrom;
});
@ -40,7 +42,7 @@ runInEachFileSystem(() => {
sources: ['a.js'],
version: 3
};
const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, [], fs);
const mappings = parseMappings(rawSourceMap, [originalSource], [0, 8]);
expect(mappings).toEqual([
{
@ -71,7 +73,8 @@ runInEachFileSystem(() => {
it('should parse the segments in ascending order of original position from the raw source map',
() => {
const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
const originalSource =
new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, [], fs);
const rawSourceMap: RawSourceMap = {
mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2]]]),
names: [],
@ -88,8 +91,8 @@ runInEachFileSystem(() => {
});
it('should create separate arrays for each original source file', () => {
const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, false, []);
const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, [], fs);
const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, false, [], fs);
const rawSourceMap: RawSourceMap = {
mappings:
encode([[[0, 0, 0, 0], [2, 1, 0, 3], [4, 0, 0, 2], [5, 1, 0, 5], [6, 1, 0, 2]]]),
@ -313,8 +316,8 @@ runInEachFileSystem(() => {
describe('ensureOriginalSegmentLinks', () => {
it('should add `next` properties to each segment that point to the next segment in the same source file',
() => {
const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, false, []);
const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, [], fs);
const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, false, [], fs);
const rawSourceMap: RawSourceMap = {
mappings:
encode([[[0, 0, 0, 0], [2, 1, 0, 3], [4, 0, 0, 2], [5, 1, 0, 5], [6, 1, 0, 2]]]),
@ -336,14 +339,14 @@ runInEachFileSystem(() => {
describe('flattenedMappings', () => {
it('should be an empty array for source files with no source map', () => {
const sourceFile =
new SourceFile(_('/foo/src/index.js'), 'index contents', null, false, []);
new SourceFile(_('/foo/src/index.js'), 'index contents', null, false, [], fs);
expect(sourceFile.flattenedMappings).toEqual([]);
});
it('should be empty array for source files with no source map mappings', () => {
const rawSourceMap: RawSourceMap = {mappings: '', names: [], sources: [], version: 3};
const sourceFile =
new SourceFile(_('/foo/src/index.js'), 'index contents', rawSourceMap, false, []);
new SourceFile(_('/foo/src/index.js'), 'index contents', rawSourceMap, false, [], fs);
expect(sourceFile.flattenedMappings).toEqual([]);
});
@ -355,16 +358,17 @@ runInEachFileSystem(() => {
sources: ['a.js'],
version: 3
};
const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, []);
const originalSource =
new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, false, [], fs);
const sourceFile = new SourceFile(
_('/foo/src/index.js'), 'abc123defg', rawSourceMap, false, [originalSource]);
_('/foo/src/index.js'), 'abc123defg', rawSourceMap, false, [originalSource], fs);
expect(removeOriginalSegmentLinks(sourceFile.flattenedMappings))
.toEqual(parseMappings(rawSourceMap, [originalSource], [0, 11]));
});
it('should merge mappings from flattened original source files', () => {
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123', null, false, []);
const dSource = new SourceFile(_('/foo/src/d.js'), 'aef', null, false, []);
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123', null, false, [], fs);
const dSource = new SourceFile(_('/foo/src/d.js'), 'aef', null, false, [], fs);
const bSourceMap: RawSourceMap = {
mappings: encode([[[0, 1, 0, 0], [1, 0, 0, 0], [4, 1, 0, 1]]]),
@ -372,8 +376,8 @@ runInEachFileSystem(() => {
sources: ['c.js', 'd.js'],
version: 3
};
const bSource =
new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [cSource, dSource]);
const bSource = new SourceFile(
_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [cSource, dSource], fs);
const aSourceMap: RawSourceMap = {
mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]),
@ -382,7 +386,7 @@ runInEachFileSystem(() => {
version: 3
};
const aSource =
new SourceFile(_('/foo/src/a.js'), 'abdecf', aSourceMap, false, [bSource]);
new SourceFile(_('/foo/src/a.js'), 'abdecf', aSourceMap, false, [bSource], fs);
expect(removeOriginalSegmentLinks(aSource.flattenedMappings)).toEqual([
{
@ -431,7 +435,8 @@ runInEachFileSystem(() => {
sources: ['c.js'],
version: 3
};
const bSource = new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [null]);
const bSource =
new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [null], fs);
const aSourceMap: RawSourceMap = {
mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]),
names: [],
@ -439,7 +444,7 @@ runInEachFileSystem(() => {
version: 3
};
const aSource =
new SourceFile(_('/foo/src/a.js'), 'abdecf', aSourceMap, false, [bSource]);
new SourceFile(_('/foo/src/a.js'), 'abdecf', aSourceMap, false, [bSource], fs);
// These flattened mappings are just the mappings from a to b.
// (The mappings to c are dropped since there is no source file to map to.)
@ -462,7 +467,7 @@ runInEachFileSystem(() => {
describe('renderFlattenedSourceMap()', () => {
it('should convert the flattenedMappings into a raw source-map object', () => {
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123e', null, false, []);
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123e', null, false, [], fs);
const bToCSourceMap: RawSourceMap = {
mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]),
names: [],
@ -470,7 +475,7 @@ runInEachFileSystem(() => {
version: 3
};
const bSource =
new SourceFile(_('/foo/src/b.js'), 'abcdef', bToCSourceMap, false, [cSource]);
new SourceFile(_('/foo/src/b.js'), 'abcdef', bToCSourceMap, false, [cSource], fs);
const aToBSourceMap: RawSourceMap = {
mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]),
names: [],
@ -478,7 +483,7 @@ runInEachFileSystem(() => {
version: 3
};
const aSource =
new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, false, [bSource]);
new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, false, [bSource], fs);
const aTocSourceMap = aSource.renderFlattenedSourceMap();
expect(aTocSourceMap.version).toEqual(3);
@ -493,7 +498,7 @@ runInEachFileSystem(() => {
});
it('should handle mappings that map from lines outside of the actual content lines', () => {
const bSource = new SourceFile(_('/foo/src/b.js'), 'abcdef', null, false, []);
const bSource = new SourceFile(_('/foo/src/b.js'), 'abcdef', null, false, [], fs);
const aToBSourceMap: RawSourceMap = {
mappings: encode([
[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]],
@ -506,7 +511,7 @@ runInEachFileSystem(() => {
version: 3
};
const aSource =
new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, false, [bSource]);
new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, false, [bSource], fs);
const aTocSourceMap = aSource.renderFlattenedSourceMap();
expect(aTocSourceMap.version).toEqual(3);
@ -522,13 +527,13 @@ runInEachFileSystem(() => {
describe('getOriginalLocation()', () => {
it('should return null for source files with no flattened mappings', () => {
const sourceFile =
new SourceFile(_('/foo/src/index.js'), 'index contents', null, false, []);
new SourceFile(_('/foo/src/index.js'), 'index contents', null, false, [], fs);
expect(sourceFile.getOriginalLocation(1, 1)).toEqual(null);
});
it('should return offset locations in multiple flattened original source files', () => {
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123', null, false, []);
const dSource = new SourceFile(_('/foo/src/d.js'), 'aef', null, false, []);
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123', null, false, [], fs);
const dSource = new SourceFile(_('/foo/src/d.js'), 'aef', null, false, [], fs);
const bSourceMap: RawSourceMap = {
mappings: encode([
@ -542,8 +547,8 @@ runInEachFileSystem(() => {
sources: ['c.js', 'd.js'],
version: 3
};
const bSource =
new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [cSource, dSource]);
const bSource = new SourceFile(
_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [cSource, dSource], fs);
const aSourceMap: RawSourceMap = {
mappings: encode([
@ -560,7 +565,7 @@ runInEachFileSystem(() => {
version: 3
};
const aSource =
new SourceFile(_('/foo/src/a.js'), 'abde\n cf', aSourceMap, false, [bSource]);
new SourceFile(_('/foo/src/a.js'), 'abde\n cf', aSourceMap, false, [bSource], fs);
// Line 0
expect(aSource.getOriginalLocation(0, 0)) // a
@ -592,8 +597,8 @@ runInEachFileSystem(() => {
});
it('should return offset locations across multiple lines', () => {
const originalSource =
new SourceFile(_('/foo/src/original.js'), 'abcdef\nghijk\nlmnop', null, false, []);
const originalSource = new SourceFile(
_('/foo/src/original.js'), 'abcdef\nghijk\nlmnop', null, false, [], fs);
const generatedSourceMap: RawSourceMap = {
mappings: encode([
[
@ -614,7 +619,7 @@ runInEachFileSystem(() => {
};
const generatedSource = new SourceFile(
_('/foo/src/generated.js'), 'ABC\nGHIJDEFK\nLMNOP', generatedSourceMap, false,
[originalSource]);
[originalSource], fs);
// Line 0
expect(generatedSource.getOriginalLocation(0, 0)) // A

View File

@ -185,47 +185,82 @@ export class Parser {
location, absoluteOffset, this.errors);
}
/**
* Splits a string of text into "raw" text segments and expressions present in interpolations in
* the string.
* Returns `null` if there are no interpolations, otherwise a
* `SplitInterpolation` with splits that look like
* <raw text> <expression> <raw text> ... <raw text> <expression> <raw text>
*/
splitInterpolation(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
|null {
const regexp = _getInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length <= 1) {
return null;
}
const strings: string[] = [];
const expressions: string[] = [];
const offsets: number[] = [];
const stringSpans: {start: number, end: number}[] = [];
const expressionSpans: {start: number, end: number}[] = [];
let offset = 0;
for (let i = 0; i < parts.length; i++) {
const part: string = parts[i];
if (i % 2 === 0) {
// fixed string
let i = 0;
let atInterpolation = false;
let extendLastString = false;
let {start: interpStart, end: interpEnd} = interpolationConfig;
while (i < input.length) {
if (!atInterpolation) {
// parse until starting {{
const start = i;
i = input.indexOf(interpStart, i);
if (i === -1) {
i = input.length;
}
const part = input.substring(start, i);
strings.push(part);
const start = offset;
offset += part.length;
stringSpans.push({start, end: offset});
} else if (part.trim().length > 0) {
const start = offset;
offset += interpolationConfig.start.length;
expressions.push(part);
offsets.push(offset);
offset += part.length + interpolationConfig.end.length;
expressionSpans.push({start, end: offset});
stringSpans.push({start, end: i});
atInterpolation = true;
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
expressions.push('$implicit');
offsets.push(offset);
expressionSpans.push({start: offset, end: offset});
// parse from starting {{ to ending }}
const fullStart = i;
const exprStart = fullStart + interpStart.length;
const exprEnd = input.indexOf(interpEnd, exprStart);
if (exprEnd === -1) {
// Could not find the end of the interpolation; do not parse an expression.
// Instead we should extend the content on the last raw string.
atInterpolation = false;
extendLastString = true;
break;
}
const fullEnd = exprEnd + interpEnd.length;
const part = input.substring(exprStart, exprEnd);
if (part.trim().length > 0) {
expressions.push(part);
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${i} in`, location);
expressions.push('$implicit');
}
offsets.push(exprStart);
expressionSpans.push({start: fullStart, end: fullEnd});
i = fullEnd;
atInterpolation = false;
}
}
return new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
if (!atInterpolation) {
// If we are now at a text section, add the remaining content as a raw string.
if (extendLastString) {
strings[strings.length - 1] += input.substring(i);
stringSpans[stringSpans.length - 1].end = input.length;
} else {
strings.push(input.substring(i));
stringSpans.push({start: i, end: input.length});
}
}
return expressions.length === 0 ?
null :
new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
}
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
@ -288,10 +323,24 @@ export class IvyParser extends Parser {
simpleExpressionChecker = IvySimpleExpressionChecker; //
}
/** Describes a stateful context an expression parser is in. */
enum ParseContextFlags {
None = 0,
/**
* A Writable context is one in which a value may be written to an lvalue.
* For example, after we see a property access, we may expect a write to the
* property via the "=" operator.
* prop
* ^ possible "=" after
*/
Writable = 1,
}
export class _ParseAST {
private rparensExpected = 0;
private rbracketsExpected = 0;
private rbracesExpected = 0;
private context = ParseContextFlags.None;
// Cache of expression start and input indeces to the absolute source span they map to, used to
// prevent creating superfluous source spans in `sourceSpan`.
@ -368,6 +417,16 @@ export class _ParseAST {
this.index++;
}
/**
* Executes a callback in the provided context.
*/
private withContext<T>(context: ParseContextFlags, cb: () => T): T {
this.context |= context;
const ret = cb();
this.context ^= context;
return ret;
}
consumeOptionalCharacter(code: number): boolean {
if (this.next.isCharacter(code)) {
this.advance();
@ -384,6 +443,12 @@ export class _ParseAST {
return this.next.isKeywordAs();
}
/**
* Consumes an expected character, otherwise emits an error about the missing expected character
* and skips over the token stream until reaching a recoverable point.
*
* See `this.error` and `this.skip` for more details.
*/
expectCharacter(code: number) {
if (this.consumeOptionalCharacter(code)) return;
this.error(`Missing expected ${String.fromCharCode(code)}`);
@ -631,18 +696,23 @@ export class _ParseAST {
result = this.parseAccessMemberOrMethodCall(result, true);
} else if (this.consumeOptionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const key = this.parsePipe();
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.consumeOptionalOperator('=')) {
const value = this.parseConditional();
result = new KeyedWrite(
this.span(resultStart), this.sourceSpan(resultStart), result, key, value);
} else {
result = new KeyedRead(this.span(resultStart), this.sourceSpan(resultStart), result, key);
}
this.withContext(ParseContextFlags.Writable, () => {
this.rbracketsExpected++;
const key = this.parsePipe();
if (key instanceof EmptyExpr) {
this.error(`Key access cannot be empty`);
}
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.consumeOptionalOperator('=')) {
const value = this.parseConditional();
result = new KeyedWrite(
this.span(resultStart), this.sourceSpan(resultStart), result, key, value);
} else {
result =
new KeyedRead(this.span(resultStart), this.sourceSpan(resultStart), result, key);
}
});
} else if (this.consumeOptionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
@ -994,6 +1064,10 @@ export class _ParseAST {
this.consumeOptionalCharacter(chars.$SEMICOLON) || this.consumeOptionalCharacter(chars.$COMMA);
}
/**
* Records an error and skips over the token stream until reaching a recoverable point. See
* `this.skip` for more details on token skipping.
*/
error(message: string, index: number|null = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
@ -1005,25 +1079,32 @@ export class _ParseAST {
`at the end of the expression`;
}
// Error recovery should skip tokens until it encounters a recovery point. skip() treats
// the end of input and a ';' as unconditionally a recovery point. It also treats ')',
// '}' and ']' as conditional recovery points if one of calling productions is expecting
// one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
// more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
// of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
// must be conditional as they must be skipped if none of the calling productions are not
// expecting the closing token else we will never make progress in the case of an
// extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
// parseChain() is always the root production and it expects a ';'.
// If a production expects one of these token it increments the corresponding nesting count,
// and then decrements it just prior to checking if the token is in the input.
/**
* Error recovery should skip tokens until it encounters a recovery point. skip() treats
* the end of input and a ';' as unconditionally a recovery point. It also treats ')',
* '}' and ']' as conditional recovery points if one of calling productions is expecting
* one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
* more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
* of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
* must be conditional as they must be skipped if none of the calling productions are not
* expecting the closing token else we will never make progress in the case of an
* extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
* parseChain() is always the root production and it expects a ';'.
*
* Furthermore, the presence of a stateful context can add more recovery points.
* - in a `Writable` context, we are able to recover after seeing the `=` operator, which
* signals the presence of an independent rvalue expression following the `=` operator.
*
* If a production expects one of these token it increments the corresponding nesting count,
* and then decrements it just prior to checking if the token is in the input.
*/
private skip() {
let n = this.next;
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
(this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET))) {
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET)) &&
(!(this.context & ParseContextFlags.Writable) || !n.isOperator('='))) {
if (this.next.isError()) {
this.errors.push(
new ParserError(this.next.toString()!, this.input, this.locationText(), this.location));

View File

@ -128,15 +128,22 @@ describe('parser', () => {
});
it('should only allow identifier or keyword as member names', () => {
expectActionError('x.(', 'identifier or keyword');
expectActionError('x. 1234', 'identifier or keyword');
expectActionError('x."foo"', 'identifier or keyword');
checkActionWithError('x.', 'x.', 'identifier or keyword');
checkActionWithError('x.(', 'x.', 'identifier or keyword');
checkActionWithError('x. 1234', 'x.', 'identifier or keyword');
checkActionWithError('x."foo"', 'x.', 'identifier or keyword');
});
it('should parse safe field access', () => {
checkAction('a?.a');
checkAction('a.a?.a');
});
it('should parse incomplete safe field accesses', () => {
checkActionWithError('a?.a.', 'a?.a.', 'identifier or keyword');
checkActionWithError('a.a?.a.', 'a.a?.a.', 'identifier or keyword');
checkActionWithError('a.a?.a?. 1234', 'a.a?.a?.', 'identifier or keyword');
});
});
describe('method calls', () => {
@ -154,6 +161,84 @@ describe('parser', () => {
});
});
describe('keyed read', () => {
it('should parse keyed reads', () => {
checkAction('a["a"]');
checkAction('this.a["a"]', 'a["a"]');
checkAction('a.a["a"]');
});
describe('malformed keyed reads', () => {
it('should recover on missing keys', () => {
checkActionWithError('a[]', 'a[]', 'Key access cannot be empty');
});
it('should recover on incomplete expression keys', () => {
checkActionWithError('a[1 + ]', 'a[1 + ]', 'Unexpected token ]');
});
it('should recover on unterminated keys', () => {
checkActionWithError(
'a[1 + 2', 'a[1 + 2]', 'Missing expected ] at the end of the expression');
});
it('should recover on incomplete and unterminated keys', () => {
checkActionWithError(
'a[1 + ', 'a[1 + ]', 'Missing expected ] at the end of the expression');
});
});
});
describe('keyed write', () => {
it('should parse keyed writes', () => {
checkAction('a["a"] = 1 + 2');
checkAction('this.a["a"] = 1 + 2', 'a["a"] = 1 + 2');
checkAction('a.a["a"] = 1 + 2');
});
describe('malformed keyed writes', () => {
it('should recover on empty rvalues', () => {
checkActionWithError('a["a"] = ', 'a["a"] = ', 'Unexpected end of expression');
});
it('should recover on incomplete rvalues', () => {
checkActionWithError('a["a"] = 1 + ', 'a["a"] = 1 + ', 'Unexpected end of expression');
});
it('should recover on missing keys', () => {
checkActionWithError('a[] = 1', 'a[] = 1', 'Key access cannot be empty');
});
it('should recover on incomplete expression keys', () => {
checkActionWithError('a[1 + ] = 1', 'a[1 + ] = 1', 'Unexpected token ]');
});
it('should recover on unterminated keys', () => {
checkActionWithError('a[1 + 2 = 1', 'a[1 + 2] = 1', 'Missing expected ]');
});
it('should recover on incomplete and unterminated keys', () => {
const ast = parseAction('a[1 + = 1');
expect(unparse(ast)).toEqual('a[1 + ] = 1');
validate(ast);
const errors = ast.errors.map(e => e.message);
expect(errors.length).toBe(2);
expect(errors[0]).toContain('Unexpected token =');
expect(errors[1]).toContain('Missing expected ]');
});
it('should error on writes after a keyed write', () => {
const ast = parseAction('a[1] = 1 = 2');
expect(unparse(ast)).toEqual('a[1] = 1');
validate(ast);
expect(ast.errors.length).toBe(1);
expect(ast.errors[0].message).toContain('Unexpected token \'=\'');
});
});
});
describe('conditional', () => {
it('should parse ternary/conditional expressions', () => {
checkAction('7 == 3 + 4 ? 10 : 20');
@ -650,6 +735,13 @@ describe('parser', () => {
expect(parseInterpolation('nothing')).toBe(null);
});
it('should not parse malformed interpolations as strings', () => {
const ast = parseInterpolation('{{a}} {{example}<!--->}')!.ast as Interpolation;
expect(ast.strings).toEqual(['', ' {{example}<!--->}']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
});
it('should parse no prefix/suffix interpolation', () => {
const ast = parseInterpolation('{{a}}')!.ast as Interpolation;
expect(ast.strings).toEqual(['', '']);
@ -926,3 +1018,11 @@ function expectActionError(text: string, message: string) {
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}
/**
* Check that a malformed action parses to a recovered AST while emitting an error.
*/
function checkActionWithError(text: string, expected: string, error: string) {
checkAction(text, expected);
expectActionError(text, error);
}

View File

@ -22,7 +22,7 @@ import {Subject} from 'rxjs';
`
})
export class NgIfSimple {
show: boolean = true;
show = true;
}
// #enddocregion
@ -38,7 +38,7 @@ export class NgIfSimple {
`
})
export class NgIfElse {
show: boolean = true;
show = true;
}
// #enddocregion
@ -58,7 +58,7 @@ export class NgIfElse {
})
export class NgIfThenElse implements OnInit {
thenBlock: TemplateRef<any>|null = null;
show: boolean = true;
show = true;
@ViewChild('primaryBlock', {static: true}) primaryBlock: TemplateRef<any>|null = null;
@ViewChild('secondaryBlock', {static: true}) secondaryBlock: TemplateRef<any>|null = null;

View File

@ -14,6 +14,8 @@ export type DiagnosticHandlingStrategy = 'error'|'warning'|'ignore';
/**
* This class is used to collect and then report warnings and errors that occur during the execution
* of the tools.
*
* @publicApi used by CLI
*/
export class Diagnostics {
readonly messages: {type: 'warning'|'error', message: string}[] = [];

View File

@ -23,6 +23,8 @@ export interface ExtractionOptions {
/**
* Extracts parsed messages from file contents, by parsing the contents as JavaScript
* and looking for occurrences of `$localize` in the source code.
*
* @publicApi used by CLI
*/
export class MessageExtractor {
private basePath: AbsoluteFsPath;
@ -50,8 +52,8 @@ export class MessageExtractor {
sourceRoot: this.basePath,
filename,
plugins: [
makeEs2015ExtractPlugin(messages, this.localizeName),
makeEs5ExtractPlugin(messages, this.localizeName),
makeEs2015ExtractPlugin(this.fs, messages, this.localizeName),
makeEs5ExtractPlugin(this.fs, messages, this.localizeName),
],
code: false,
ast: false

View File

@ -6,13 +6,13 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getFileSystem, setFileSystem, NodeJSFileSystem, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
import {setFileSystem, NodeJSFileSystem, AbsoluteFsPath, FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ConsoleLogger, Logger, LogLevel} from '@angular/compiler-cli/src/ngtsc/logging';
import {ɵParsedMessage} from '@angular/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics';
import {DiagnosticHandlingStrategy} from '../diagnostics';
import {checkDuplicateMessages} from './duplicates';
import {MessageExtractor} from './extraction';
@ -97,8 +97,8 @@ if (require.main === module) {
.help()
.parse(args);
const fs = new NodeJSFileSystem();
setFileSystem(fs);
const fileSystem = new NodeJSFileSystem();
setFileSystem(fileSystem);
const rootPath = options.r;
const sourceFilePaths = glob.sync(options.s, {cwd: rootPath, nodir: true});
@ -119,6 +119,7 @@ if (require.main === module) {
useLegacyIds: options.useLegacyIds,
duplicateMessageHandling,
formatOptions,
fileSystem,
});
}
@ -166,6 +167,10 @@ export interface ExtractTranslationsOptions {
* A collection of formatting options to pass to the translation file serializer.
*/
formatOptions?: FormatOptions;
/**
* The file-system abstraction to use.
*/
fileSystem: FileSystem;
}
export function extractTranslations({
@ -179,8 +184,8 @@ export function extractTranslations({
useLegacyIds,
duplicateMessageHandling,
formatOptions = {},
fileSystem: fs,
}: ExtractTranslationsOptions) {
const fs = getFileSystem();
const basePath = fs.resolve(rootPath);
const extractor = new MessageExtractor(fs, logger, {basePath, useSourceMaps});
@ -196,7 +201,7 @@ export function extractTranslations({
const outputPath = fs.resolve(rootPath, output);
const serializer =
getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions);
getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions, fs);
const translationFile = serializer.serialize(messages);
fs.ensureDir(fs.dirname(outputPath));
fs.writeFile(outputPath, translationFile);
@ -208,18 +213,20 @@ export function extractTranslations({
export function getSerializer(
format: string, sourceLocale: string, rootPath: AbsoluteFsPath, useLegacyIds: boolean,
formatOptions: FormatOptions = {}): TranslationSerializer {
formatOptions: FormatOptions = {}, fs: FileSystem): TranslationSerializer {
switch (format) {
case 'xlf':
case 'xlif':
case 'xliff':
return new Xliff1TranslationSerializer(sourceLocale, rootPath, useLegacyIds, formatOptions);
return new Xliff1TranslationSerializer(
sourceLocale, rootPath, useLegacyIds, formatOptions, fs);
case 'xlf2':
case 'xlif2':
case 'xliff2':
return new Xliff2TranslationSerializer(sourceLocale, rootPath, useLegacyIds, formatOptions);
return new Xliff2TranslationSerializer(
sourceLocale, rootPath, useLegacyIds, formatOptions, fs);
case 'xmb':
return new XmbTranslationSerializer(rootPath, useLegacyIds);
return new XmbTranslationSerializer(rootPath, useLegacyIds, fs);
case 'json':
return new SimpleJsonTranslationSerializer(sourceLocale);
}

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵparseMessage} from '@angular/localize';
import {NodePath, PluginObj} from '@babel/core';
import {TaggedTemplateExpression} from '@babel/types';
@ -12,7 +13,7 @@ import {TaggedTemplateExpression} from '@babel/types';
import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapExpressionsFromTemplateLiteral, unwrapMessagePartsFromTemplateLiteral} from '../../source_file_utils';
export function makeEs2015ExtractPlugin(
messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
fs: FileSystem, messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
return {
visitor: {
TaggedTemplateExpression(path: NodePath<TaggedTemplateExpression>) {
@ -20,10 +21,10 @@ export function makeEs2015ExtractPlugin(
if (isNamedIdentifier(tag, localizeName) && isGlobalIdentifier(tag)) {
const quasiPath = path.get('quasi');
const [messageParts, messagePartLocations] =
unwrapMessagePartsFromTemplateLiteral(quasiPath.get('quasis'));
unwrapMessagePartsFromTemplateLiteral(quasiPath.get('quasis'), fs);
const [expressions, expressionLocations] =
unwrapExpressionsFromTemplateLiteral(quasiPath);
const location = getLocation(quasiPath);
unwrapExpressionsFromTemplateLiteral(quasiPath, fs);
const location = getLocation(fs, quasiPath);
const message = ɵparseMessage(
messageParts, expressions, location, messagePartLocations, expressionLocations);
messages.push(message);

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵparseMessage} from '@angular/localize';
import {NodePath, PluginObj} from '@babel/core';
import {CallExpression} from '@babel/types';
@ -12,16 +13,18 @@ import {CallExpression} from '@babel/types';
import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapMessagePartsFromLocalizeCall, unwrapSubstitutionsFromLocalizeCall} from '../../source_file_utils';
export function makeEs5ExtractPlugin(
messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
fs: FileSystem, messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
return {
visitor: {
CallExpression(callPath: NodePath<CallExpression>) {
const calleePath = callPath.get('callee');
if (isNamedIdentifier(calleePath, localizeName) && isGlobalIdentifier(calleePath)) {
const [messageParts, messagePartLocations] = unwrapMessagePartsFromLocalizeCall(callPath);
const [expressions, expressionLocations] = unwrapSubstitutionsFromLocalizeCall(callPath);
const [messageParts, messagePartLocations] =
unwrapMessagePartsFromLocalizeCall(callPath, fs);
const [expressions, expressionLocations] =
unwrapSubstitutionsFromLocalizeCall(callPath, fs);
const [messagePartsArg, expressionsArg] = callPath.get('arguments');
const location = getLocation(messagePartsArg, expressionsArg);
const location = getLocation(fs, messagePartsArg, expressionsArg);
const message = ɵparseMessage(
messageParts, expressions, location, messagePartLocations, expressionLocations);
messages.push(message);

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
import {FormatOptions, validateOptions} from './format_options';
@ -23,11 +23,12 @@ const LEGACY_XLIFF_MESSAGE_LENGTH = 40;
* http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
*
* @see Xliff1TranslationParser
* @publicApi used by CLI
*/
export class Xliff1TranslationSerializer implements TranslationSerializer {
constructor(
private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean,
private formatOptions: FormatOptions = {}) {
private formatOptions: FormatOptions = {}, private fs: FileSystem = getFileSystem()) {
validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions);
}
@ -114,7 +115,7 @@ export class Xliff1TranslationSerializer implements TranslationSerializer {
private serializeLocation(xml: XmlFile, location: ɵSourceLocation): void {
xml.startTag('context-group', {purpose: 'location'});
this.renderContext(xml, 'sourcefile', relative(this.basePath, location.file));
this.renderContext(xml, 'sourcefile', this.fs.relative(this.basePath, location.file));
const endLineString = location.end !== undefined && location.end.line !== location.start.line ?
`,${location.end.line + 1}` :
'';

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
import {FormatOptions, validateOptions} from './format_options';
@ -22,12 +22,13 @@ const MAX_LEGACY_XLIFF_2_MESSAGE_LENGTH = 20;
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
*
* @see Xliff2TranslationParser
* @publicApi used by CLI
*/
export class Xliff2TranslationSerializer implements TranslationSerializer {
private currentPlaceholderId = 0;
constructor(
private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean,
private formatOptions: FormatOptions = {}) {
private formatOptions: FormatOptions = {}, private fs: FileSystem = getFileSystem()) {
validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions);
}
@ -63,7 +64,7 @@ export class Xliff2TranslationSerializer implements TranslationSerializer {
end !== undefined && end.line !== start.line ? `,${end.line + 1}` : '';
this.serializeNote(
xml, 'location',
`${relative(this.basePath, file)}:${start.line + 1}${endLineString}`);
`${this.fs.relative(this.basePath, file)}:${start.line + 1}${endLineString}`);
}
if (message.description) {
this.serializeNote(xml, 'description', message.description);

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
import {extractIcuPlaceholders} from './icu_parsing';
@ -18,9 +18,12 @@ import {XmlFile} from './xml_file';
* http://cldr.unicode.org/development/development-process/design-proposals/xmb
*
* @see XmbTranslationParser
* @publicApi used by CLI
*/
export class XmbTranslationSerializer implements TranslationSerializer {
constructor(private basePath: AbsoluteFsPath, private useLegacyIds: boolean) {}
constructor(
private basePath: AbsoluteFsPath, private useLegacyIds: boolean,
private fs: FileSystem = getFileSystem()) {}
serialize(messages: ɵParsedMessage[]): string {
const ids = new Set<string>();
@ -73,7 +76,8 @@ export class XmbTranslationSerializer implements TranslationSerializer {
const endLineString = location.end !== undefined && location.end.line !== location.start.line ?
`,${location.end.line + 1}` :
'';
xml.text(`${relative(this.basePath, location.file)}:${location.start.line}${endLineString}`);
xml.text(
`${this.fs.relative(this.basePath, location.file)}:${location.start.line}${endLineString}`);
xml.endTag('source');
}

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath, relative, resolve} from '@angular/compiler-cli/src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵisMissingTranslationError, ɵmakeTemplateObject, ɵParsedTranslation, ɵSourceLocation, ɵtranslate} from '@angular/localize';
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
@ -36,7 +36,9 @@ export function isNamedIdentifier(
/**
* Is the given `identifier` declared globally.
*
* @param identifier The identifier to check.
* @publicApi used by CLI
*/
export function isGlobalIdentifier(identifier: NodePath<t.Identifier>) {
return !identifier.scope || !identifier.scope.hasBinding(identifier.node.name);
@ -46,6 +48,7 @@ export function isGlobalIdentifier(identifier: NodePath<t.Identifier>) {
* Build a translated expression to replace the call to `$localize`.
* @param messageParts The static parts of the message.
* @param substitutions The expressions to substitute into the message.
* @publicApi used by CLI
*/
export function buildLocalizeReplacement(
messageParts: TemplateStringsArray, substitutions: readonly t.Expression[]): t.Expression {
@ -65,9 +68,14 @@ export function buildLocalizeReplacement(
* to a helper function like `__makeTemplateObject`.
*
* @param call The AST node of the call to process.
* @param fs The file system to use when computing source-map paths. If not provided then it uses
* the "current" FileSystem.
* @publicApi used by CLI
*/
export function unwrapMessagePartsFromLocalizeCall(call: NodePath<t.CallExpression>):
[TemplateStringsArray, (ɵSourceLocation | undefined)[]] {
export function unwrapMessagePartsFromLocalizeCall(
call: NodePath<t.CallExpression>,
fs: FileSystem = getFileSystem(),
): [TemplateStringsArray, (ɵSourceLocation | undefined)[]] {
let cooked = call.get('arguments')[0];
if (cooked === undefined) {
@ -137,14 +145,22 @@ export function unwrapMessagePartsFromLocalizeCall(call: NodePath<t.CallExpressi
raw = arg2 !== undefined ? arg2 : cooked;
}
const [cookedStrings] = unwrapStringLiteralArray(cooked);
const [rawStrings, rawLocations] = unwrapStringLiteralArray(raw);
const [cookedStrings] = unwrapStringLiteralArray(cooked, fs);
const [rawStrings, rawLocations] = unwrapStringLiteralArray(raw, fs);
return [ɵmakeTemplateObject(cookedStrings, rawStrings), rawLocations];
}
export function unwrapSubstitutionsFromLocalizeCall(call: NodePath<t.CallExpression>):
[t.Expression[], (ɵSourceLocation | undefined)[]] {
/**
* Parse the localize call expression to extract the arguments that hold the substition expressions.
*
* @param call The AST node of the call to process.
* @param fs The file system to use when computing source-map paths. If not provided then it uses
* the "current" FileSystem.
* @publicApi used by CLI
*/
export function unwrapSubstitutionsFromLocalizeCall(
call: NodePath<t.CallExpression>,
fs: FileSystem = getFileSystem()): [t.Expression[], (ɵSourceLocation | undefined)[]] {
const expressions = call.get('arguments').splice(1);
if (!isArrayOfExpressions(expressions)) {
const badExpression = expressions.find(expression => !expression.isExpression())!;
@ -153,12 +169,21 @@ export function unwrapSubstitutionsFromLocalizeCall(call: NodePath<t.CallExpress
'Invalid substitutions for `$localize` (expected all substitution arguments to be expressions).');
}
return [
expressions.map(path => path.node), expressions.map(expression => getLocation(expression))
expressions.map(path => path.node), expressions.map(expression => getLocation(fs, expression))
];
}
export function unwrapMessagePartsFromTemplateLiteral(elements: NodePath<t.TemplateElement>[]):
[TemplateStringsArray, (ɵSourceLocation | undefined)[]] {
/**
* Parse the tagged template literal to extract the message parts.
*
* @param elements The elements of the template literal to process.
* @param fs The file system to use when computing source-map paths. If not provided then it uses
* the "current" FileSystem.
* @publicApi used by CLI
*/
export function unwrapMessagePartsFromTemplateLiteral(
elements: NodePath<t.TemplateElement>[],
fs: FileSystem = getFileSystem()): [TemplateStringsArray, (ɵSourceLocation | undefined)[]] {
const cooked = elements.map(q => {
if (q.node.value.cooked === undefined) {
throw new BabelParseError(
@ -168,13 +193,22 @@ export function unwrapMessagePartsFromTemplateLiteral(elements: NodePath<t.Templ
return q.node.value.cooked;
});
const raw = elements.map(q => q.node.value.raw);
const locations = elements.map(q => getLocation(q));
const locations = elements.map(q => getLocation(fs, q));
return [ɵmakeTemplateObject(cooked, raw), locations];
}
export function unwrapExpressionsFromTemplateLiteral(quasi: NodePath<t.TemplateLiteral>):
[t.Expression[], (ɵSourceLocation | undefined)[]] {
return [quasi.node.expressions, quasi.get('expressions').map(e => getLocation(e))];
/**
* Parse the tagged template literal to extract the interpolation expressions.
*
* @param quasi The AST node of the template literal to process.
* @param fs The file system to use when computing source-map paths. If not provided then it uses
* the "current" FileSystem.
* @publicApi used by CLI
*/
export function unwrapExpressionsFromTemplateLiteral(
quasi: NodePath<t.TemplateLiteral>,
fs: FileSystem = getFileSystem()): [t.Expression[], (ɵSourceLocation | undefined)[]] {
return [quasi.node.expressions, quasi.get('expressions').map(e => getLocation(fs, e))];
}
/**
@ -194,16 +228,20 @@ export function wrapInParensIfNecessary(expression: t.Expression): t.Expression
/**
* Extract the string values from an `array` of string literals.
*
* @param array The array to unwrap.
* @param fs The file system to use when computing source-map paths. If not provided then it uses
* the "current" FileSystem.
*/
export function unwrapStringLiteralArray(array: NodePath<t.Expression>):
[string[], (ɵSourceLocation | undefined)[]] {
export function unwrapStringLiteralArray(
array: NodePath<t.Expression>,
fs: FileSystem = getFileSystem()): [string[], (ɵSourceLocation | undefined)[]] {
if (!isStringLiteralArray(array.node)) {
throw new BabelParseError(
array.node, 'Unexpected messageParts for `$localize` (expected an array of strings).');
}
const elements = array.get('elements') as NodePath<t.StringLiteral>[];
return [elements.map(str => str.node.value), elements.map(str => getLocation(str))];
return [elements.map(str => str.node.value), elements.map(str => getLocation(fs, str))];
}
/**
@ -321,6 +359,7 @@ export interface TranslatePluginOptions {
* Translate the text of the given message, using the given translations.
*
* Logs as warning if the translation is not available
* @publicApi used by CLI
*/
export function translate(
diagnostics: Diagnostics, translations: Record<string, ɵParsedTranslation>,
@ -360,15 +399,16 @@ export function buildCodeFrameError(path: NodePath, e: BabelParseError): string
return `${filename}: ${message}`;
}
export function getLocation(startPath: NodePath, endPath?: NodePath): ɵSourceLocation|undefined {
export function getLocation(
fs: FileSystem, startPath: NodePath, endPath?: NodePath): ɵSourceLocation|undefined {
const startLocation = startPath.node.loc;
const file = getFileFromPath(startPath);
const file = getFileFromPath(fs, startPath);
if (!startLocation || !file) {
return undefined;
}
const endLocation =
endPath && getFileFromPath(endPath) === file && endPath.node.loc || startLocation;
endPath && getFileFromPath(fs, endPath) === file && endPath.node.loc || startLocation;
return {
start: getLineAndColumn(startLocation.start),
@ -385,10 +425,10 @@ export function serializeLocationPosition(location: ɵSourceLocation): string {
return `${location.start.line + 1}${endLineString}`;
}
function getFileFromPath(path: NodePath|undefined): AbsoluteFsPath|null {
function getFileFromPath(fs: FileSystem, path: NodePath|undefined): AbsoluteFsPath|null {
const opts = path?.hub.file.opts;
return opts?.filename ?
resolve(opts.generatorOpts.sourceRoot ?? opts.cwd, relative(opts.cwd, opts.filename)) :
fs.resolve(opts.generatorOpts.sourceRoot ?? opts.cwd, fs.relative(opts.cwd, opts.filename)) :
null;
}

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedTranslation} from '@angular/localize';
import {NodePath, PluginObj} from '@babel/core';
import {TaggedTemplateExpression} from '@babel/types';
@ -13,10 +14,16 @@ import {Diagnostics} from '../../diagnostics';
import {buildCodeFrameError, buildLocalizeReplacement, isBabelParseError, isLocalize, translate, TranslatePluginOptions, unwrapMessagePartsFromTemplateLiteral} from '../../source_file_utils';
/**
* Create a Babel plugin that can be used to do compile-time translation of `$localize` tagged
* messages.
*
* @publicApi used by CLI
*/
export function makeEs2015TranslatePlugin(
diagnostics: Diagnostics, translations: Record<string, ɵParsedTranslation>,
{missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {}):
PluginObj {
{missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {},
fs: FileSystem = getFileSystem()): PluginObj {
return {
visitor: {
TaggedTemplateExpression(path: NodePath<TaggedTemplateExpression>) {
@ -24,7 +31,7 @@ export function makeEs2015TranslatePlugin(
const tag = path.get('tag');
if (isLocalize(tag, localizeName)) {
const [messageParts] =
unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis'));
unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis'), fs);
const translated = translate(
diagnostics, translations, messageParts, path.node.quasi.expressions,
missingTranslation);

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ɵParsedTranslation} from '@angular/localize';
import {NodePath, PluginObj} from '@babel/core';
import {CallExpression} from '@babel/types';
@ -13,18 +14,24 @@ import {Diagnostics} from '../../diagnostics';
import {buildCodeFrameError, buildLocalizeReplacement, isBabelParseError, isLocalize, translate, TranslatePluginOptions, unwrapMessagePartsFromLocalizeCall, unwrapSubstitutionsFromLocalizeCall} from '../../source_file_utils';
/**
* Create a Babel plugin that can be used to do compile-time translation of `$localize` tagged
* messages.
*
* @publicApi used by CLI
*/
export function makeEs5TranslatePlugin(
diagnostics: Diagnostics, translations: Record<string, ɵParsedTranslation>,
{missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {}):
PluginObj {
{missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {},
fs: FileSystem = getFileSystem()): PluginObj {
return {
visitor: {
CallExpression(callPath: NodePath<CallExpression>) {
try {
const calleePath = callPath.get('callee');
if (isLocalize(calleePath, localizeName)) {
const [messageParts] = unwrapMessagePartsFromLocalizeCall(callPath);
const [expressions] = unwrapSubstitutionsFromLocalizeCall(callPath);
const [messageParts] = unwrapMessagePartsFromLocalizeCall(callPath, fs);
const [expressions] = unwrapSubstitutionsFromLocalizeCall(callPath, fs);
const translated =
translate(diagnostics, translations, messageParts, expressions, missingTranslation);
callPath.replaceWith(buildLocalizeReplacement(translated[0], translated[1]));

View File

@ -21,6 +21,7 @@ import {isLocalize, TranslatePluginOptions} from '../../source_file_utils';
*
* @param locale The name of the locale to inline into the code.
* @param options Additional options including the name of the `$localize` function.
* @publicApi used by CLI
*/
export function makeLocalePlugin(
locale: string, {localizeName = '$localize'}: TranslatePluginOptions = {}): PluginObj {

View File

@ -78,8 +78,8 @@ export class SourceFileTranslationHandler implements TranslationHandler {
generatorOpts: {minified: true},
plugins: [
makeLocalePlugin(translationBundle.locale),
makeEs2015TranslatePlugin(diagnostics, translationBundle.translations, options),
makeEs5TranslatePlugin(diagnostics, translationBundle.translations, options),
makeEs2015TranslatePlugin(diagnostics, translationBundle.translations, options, this.fs),
makeEs5TranslatePlugin(diagnostics, translationBundle.translations, options, this.fs),
],
filename,
});

View File

@ -25,6 +25,7 @@ import {ParseAnalysis, ParsedTranslationBundle, TranslationParser} from './trans
* ```
*
* @see SimpleJsonTranslationSerializer
* @publicApi used by CLI
*/
export class SimpleJsonTranslationParser implements TranslationParser<Object> {
/**

View File

@ -21,6 +21,7 @@ import {addErrorsToBundle, addParseDiagnostic, addParseError, canParseXml, getAt
* http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
*
* @see Xliff1TranslationSerializer
* @publicApi used by CLI
*/
export class Xliff1TranslationParser implements TranslationParser<XmlTranslationParserHint> {
/**

View File

@ -20,6 +20,7 @@ import {addErrorsToBundle, addParseDiagnostic, addParseError, canParseXml, getAt
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
*
* @see Xliff2TranslationSerializer
* @publicApi used by CLI
*/
export class Xliff2TranslationParser implements TranslationParser<XmlTranslationParserHint> {
/**

View File

@ -22,6 +22,7 @@ import {addErrorsToBundle, addParseDiagnostic, addParseError, canParseXml, getAt
* http://cldr.unicode.org/development/development-process/design-proposals/xmb
*
* @see XmbTranslationSerializer
* @publicApi used by CLI
*/
export class XtbTranslationParser implements TranslationParser<XmlTranslationParserHint> {
/**

View File

@ -5,7 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {InvalidFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/src/invalid_file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
import {loadTestDirectory} from '@angular/compiler-cli/test/helpers';
@ -34,6 +35,7 @@ runInEachFileSystem(() => {
fs.ensureDir(fs.dirname(sourceFilePath));
loadTestDirectory(fs, __dirname + '/test_files', absoluteFrom('/project/test_files'));
setFileSystem(new InvalidFileSystem());
});
describe('extractTranslations()', () => {
@ -48,6 +50,7 @@ runInEachFileSystem(() => {
useSourceMaps: false,
useLegacyIds: false,
duplicateMessageHandling: 'ignore',
fileSystem: fs,
});
expect(fs.readFile(outputPath)).toEqual([
`{`,
@ -70,6 +73,7 @@ runInEachFileSystem(() => {
useSourceMaps: false,
useLegacyIds,
duplicateMessageHandling: 'ignore',
fileSystem: fs,
});
expect(fs.readFile(outputPath)).toEqual([
`{`,
@ -97,6 +101,7 @@ runInEachFileSystem(() => {
useSourceMaps: false,
useLegacyIds,
duplicateMessageHandling: 'ignore',
fileSystem: fs,
});
expect(fs.readFile(outputPath)).toEqual([
`<?xml version="1.0" encoding="UTF-8" ?>`,
@ -151,6 +156,7 @@ runInEachFileSystem(() => {
useLegacyIds,
duplicateMessageHandling: 'ignore',
formatOptions,
fileSystem: fs,
});
expect(fs.readFile(outputPath)).toEqual([
`<?xml version="1.0" encoding="UTF-8" ?>`,
@ -222,6 +228,7 @@ runInEachFileSystem(() => {
useLegacyIds,
duplicateMessageHandling: 'ignore',
formatOptions,
fileSystem: fs,
});
expect(fs.readFile(outputPath)).toEqual([
`<?xml version="1.0" encoding="UTF-8" ?>`,
@ -299,6 +306,7 @@ runInEachFileSystem(() => {
useSourceMaps: true,
useLegacyIds: false,
duplicateMessageHandling: 'ignore',
fileSystem: fs,
});
expect(fs.readFile(outputPath)).toEqual([
`<?xml version="1.0" encoding="UTF-8" ?>`,
@ -308,7 +316,8 @@ runInEachFileSystem(() => {
` <trans-unit id="157258427077572998" datatype="html">`,
` <source>Message in <x id="a-file" equiv-text="file"/>!</source>`,
` <context-group purpose="location">`,
// These source file paths are due to how Bazel TypeScript compilation source-maps work
// These source file paths are due to how Bazel TypeScript compilation source-maps
// work
` <context context-type="sourcefile">../packages/localize/src/tools/test/extract/integration/test_files/src/a.ts</context>`,
` <context context-type="linenumber">3</context>`,
` </context-group>`,
@ -339,6 +348,7 @@ runInEachFileSystem(() => {
useSourceMaps: false,
useLegacyIds: false,
duplicateMessageHandling: 'error',
fileSystem: fs,
}))
.toThrowError(
`Failed to extract messages\n` +
@ -360,6 +370,7 @@ runInEachFileSystem(() => {
useSourceMaps: false,
useLegacyIds: false,
duplicateMessageHandling: 'warning',
fileSystem: fs,
});
expect(logger.logs.warn).toEqual([
['Messages extracted with warnings\n' +
@ -390,6 +401,7 @@ runInEachFileSystem(() => {
useSourceMaps: false,
useLegacyIds: false,
duplicateMessageHandling: 'ignore',
fileSystem: fs,
});
expect(logger.logs.warn).toEqual([]);
expect(fs.readFile(outputPath)).toEqual([

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {absoluteFrom, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize';
@ -14,6 +14,8 @@ import {XmbTranslationSerializer} from '../../../src/extract/translation_files/x
import {mockMessage} from './mock_message';
runInEachFileSystem(() => {
let fs: FileSystem;
beforeEach(() => fs = getFileSystem());
describe('XmbTranslationSerializer', () => {
[false, true].forEach(useLegacyIds => {
describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => {
@ -61,7 +63,8 @@ runInEachFileSystem(() => {
],
[], {}),
];
const serializer = new XmbTranslationSerializer(absoluteFrom('/project'), useLegacyIds);
const serializer =
new XmbTranslationSerializer(absoluteFrom('/project'), useLegacyIds, fs);
const output = serializer.serialize(messages);
expect(output).toContain([
`<messagebundle>`,

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {absoluteFrom, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {ɵmakeTemplateObject} from '@angular/localize';
import {NodePath, TransformOptions, transformSync} from '@babel/core';
@ -16,6 +16,8 @@ import {Expression, Identifier, TaggedTemplateExpression, ExpressionStatement, C
import {isGlobalIdentifier, isNamedIdentifier, isStringLiteralArray, isArrayOfExpressions, unwrapStringLiteralArray, unwrapMessagePartsFromLocalizeCall, wrapInParensIfNecessary, buildLocalizeReplacement, unwrapSubstitutionsFromLocalizeCall, unwrapMessagePartsFromTemplateLiteral, getLocation} from '../src/source_file_utils';
runInEachFileSystem(() => {
let fs: FileSystem;
beforeEach(() => fs = getFileSystem());
describe('utils', () => {
describe('isNamedIdentifier()', () => {
it('should return true if the expression is an identifier with name `$localize`', () => {
@ -77,7 +79,7 @@ runInEachFileSystem(() => {
it('should return an array of string literals and locations from a direct call to a tag function',
() => {
const localizeCall = getLocalizeCall(`$localize(['a', 'b\\t', 'c'], 1, 2)`);
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall);
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall, fs);
expect(parts).toEqual(['a', 'b\t', 'c']);
expect(locations).toEqual([
{
@ -105,7 +107,7 @@ runInEachFileSystem(() => {
() => {
let localizeCall = getLocalizeCall(
`$localize(__makeTemplateObject(['a', 'b\\t', 'c'], ['a', 'b\\\\t', 'c']), 1, 2)`);
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall);
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall, fs);
expect(parts).toEqual(['a', 'b\t', 'c']);
expect(parts.raw).toEqual(['a', 'b\\t', 'c']);
expect(locations).toEqual([
@ -138,7 +140,7 @@ runInEachFileSystem(() => {
return _templateObject = function() { return e }, e
}
$localize(_templateObject(), 1, 2)`);
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall);
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall, fs);
expect(parts).toEqual(['a', 'b\t', 'c']);
expect(parts.raw).toEqual(['a', 'b\\t', 'c']);
expect(locations).toEqual([
@ -173,7 +175,7 @@ runInEachFileSystem(() => {
const localizeStatement = localizeCall.parentPath as NodePath<ExpressionStatement>;
const statements = localizeStatement.container as object[];
expect(statements.length).toEqual(2);
unwrapMessagePartsFromLocalizeCall(localizeCall);
unwrapMessagePartsFromLocalizeCall(localizeCall, fs);
expect(statements.length).toEqual(1);
expect(statements[0]).toBe(localizeStatement.node);
});
@ -183,7 +185,7 @@ runInEachFileSystem(() => {
it('should return the substitutions and locations from a direct call to a tag function',
() => {
const call = getLocalizeCall(`$localize(['a', 'b\t', 'c'], 1, 2)`);
const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call);
const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call, fs);
expect((substitutions as NumericLiteral[]).map(s => s.value)).toEqual([1, 2]);
expect(locations).toEqual([
{
@ -204,7 +206,7 @@ runInEachFileSystem(() => {
it('should return the substitutions and locations from a downleveled tagged template', () => {
const call = getLocalizeCall(
`$localize(__makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), 1, 2)`);
const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call);
const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call, fs);
expect((substitutions as NumericLiteral[]).map(s => s.value)).toEqual([1, 2]);
expect(locations).toEqual([
{
@ -226,7 +228,8 @@ runInEachFileSystem(() => {
describe('unwrapMessagePartsFromTemplateLiteral', () => {
it('should return a TemplateStringsArray built from the template literal elements', () => {
const taggedTemplate = getTaggedTemplate('$localize `a${1}b\\t${2}c`;');
expect(unwrapMessagePartsFromTemplateLiteral(taggedTemplate.get('quasi').get('quasis'))[0])
expect(
unwrapMessagePartsFromTemplateLiteral(taggedTemplate.get('quasi').get('quasis'), fs)[0])
.toEqual(ɵmakeTemplateObject(['a', 'b\t', 'c'], ['a', 'b\\t', 'c']));
});
});
@ -248,7 +251,7 @@ runInEachFileSystem(() => {
describe('unwrapStringLiteralArray', () => {
it('should return an array of string from an array expression', () => {
const array = getFirstExpression(`['a', 'b', 'c']`);
const [expressions, locations] = unwrapStringLiteralArray(array);
const [expressions, locations] = unwrapStringLiteralArray(array, fs);
expect(expressions).toEqual(['a', 'b', 'c']);
expect(locations).toEqual([
{
@ -274,7 +277,7 @@ runInEachFileSystem(() => {
it('should throw an error if any elements of the array are not literal strings', () => {
const array = getFirstExpression(`['a', 2, 'c']`);
expect(() => unwrapStringLiteralArray(array))
expect(() => unwrapStringLiteralArray(array, fs))
.toThrowError(
'Unexpected messageParts for `$localize` (expected an array of strings).');
});
@ -315,7 +318,7 @@ runInEachFileSystem(() => {
filename: 'src/test.js',
sourceRoot: '/root',
});
const location = getLocation(taggedTemplate)!;
const location = getLocation(fs, taggedTemplate)!;
expect(location).toBeDefined();
expect(location.start).toEqual({line: 0, column: 10});
expect(location.start.constructor.name).toEqual('Object');
@ -327,7 +330,7 @@ runInEachFileSystem(() => {
it('should return `undefined` if the NodePath has no filename', () => {
const taggedTemplate = getTaggedTemplate(
'const x = $localize ``;', {sourceRoot: '/root', filename: undefined});
const location = getLocation(taggedTemplate);
const location = getLocation(fs, taggedTemplate);
expect(location).toBeUndefined();
});
});

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {ɵcomputeMsgId, ɵparseTranslation} from '@angular/localize';
import {ɵParsedTranslation} from '@angular/localize/private';
@ -15,6 +16,8 @@ import {TranslatePluginOptions} from '../../../src/source_file_utils';
import {makeEs2015TranslatePlugin} from '../../../src/translate/source_files/es2015_translate_plugin';
runInEachFileSystem(() => {
let fs: FileSystem;
beforeEach(() => fs = getFileSystem());
describe('makeEs2015Plugin', () => {
describe('(no translations)', () => {
it('should transform `$localize` tags with binary expression', () => {
@ -168,13 +171,13 @@ runInEachFileSystem(() => {
});
});
});
});
function transformCode(
input: string, translations: Record<string, ɵParsedTranslation> = {},
pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string {
return transformSync(input, {
plugins: [makeEs2015TranslatePlugin(diagnostics, translations, pluginOptions)],
filename: '/app/dist/test.js'
})!.code!;
}
function transformCode(
input: string, translations: Record<string, ɵParsedTranslation> = {},
pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string {
return transformSync(input, {
plugins: [makeEs2015TranslatePlugin(diagnostics, translations, pluginOptions)],
filename: '/app/dist/test.js'
})!.code!;
}
});

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {ɵcomputeMsgId, ɵparseTranslation} from '@angular/localize';
import {ɵParsedTranslation} from '@angular/localize/private';
@ -15,6 +16,8 @@ import {TranslatePluginOptions} from '../../../src/source_file_utils';
import {makeEs5TranslatePlugin} from '../../../src/translate/source_files/es5_translate_plugin';
runInEachFileSystem(() => {
let fs: FileSystem;
beforeEach(() => fs = getFileSystem());
describe('makeEs5Plugin', () => {
describe('(no translations)', () => {
it('should transform `$localize` calls with binary expression', () => {
@ -357,13 +360,13 @@ runInEachFileSystem(() => {
expect(output).toEqual('"abc" + (1 + 2 + 3) + " - Hello, " + getName() + "!";');
});
});
});
function transformCode(
input: string, translations: Record<string, ɵParsedTranslation> = {},
pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string {
return transformSync(input, {
plugins: [makeEs5TranslatePlugin(diagnostics, translations, pluginOptions)],
filename: '/app/dist/test.js'
})!.code!;
}
function transformCode(
input: string, translations: Record<string, ɵParsedTranslation> = {},
pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string {
return transformSync(input, {
plugins: [makeEs5TranslatePlugin(diagnostics, translations, pluginOptions)],
filename: '/app/dist/test.js'
})!.code!;
}
});

View File

@ -121,9 +121,8 @@ function getRouteGuards(
getChildRouteGuards(futureNode, currNode, parentContexts, futurePath, checks);
}
if (shouldRun) {
const component = context && context.outlet && context.outlet.component || null;
checks.canDeactivateChecks.push(new CanDeactivate(component, curr));
if (shouldRun && context && context.outlet && context.outlet.isActivated) {
checks.canDeactivateChecks.push(new CanDeactivate(context.outlet.component, curr));
}
} else {
if (curr) {

View File

@ -3027,6 +3027,13 @@ describe('Integration', () => {
resolve: {data: 'resolver'},
},
]
},
{
path: 'throwing',
runGuardsAndResolvers,
component: ThrowingCmp,
canActivate: ['guard'],
resolve: {data: 'resolver'}
}
]);
@ -3125,6 +3132,15 @@ describe('Integration', () => {
advance(fixture);
expect(guardRunCount).toEqual(5);
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}, {data: 4}]);
// Issue #39030, always running guards and resolvers should not throw
// when navigating away from a component with a throwing constructor.
expect(() => {
router.navigateByUrl('/throwing').catch(() => {});
advance(fixture);
router.navigateByUrl('/a;p=1');
advance(fixture);
}).not.toThrow();
})));
it('should rerun rerun guards and resolvers when path params change',

View File

@ -13553,9 +13553,9 @@ sauce-connect-launcher@^1.2.4:
lodash "^4.16.6"
rimraf "^2.5.4"
"sauce-connect@https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz":
"sauce-connect@https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz":
version "0.0.0"
resolved "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz#5ca9328724c5ff16b12ea49e7a748d44f7305be5"
resolved "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz#7b7f35433af9c3380758e048894d7b9aecf3754e"
saucelabs@^1.5.0:
version "1.5.0"