Compare commits

...

37 Commits

Author SHA1 Message Date
f24972b1b1 release: cut the v10.0.3 release 2020-07-08 13:44:29 -07:00
d2886b3bb4 build: filter out duplicate cherry-picked commits in changelog (#37956)
Often changelogs are generated from the patch branch and then
cherry-picked into the `CHANGELOG.md` file in `master` for
better access and readability. This is problematic though as
`conventional-changelog` (the tool we use for generating the
changelog), will duplicate commits when a future changelog
is generated from `master` then (i.e. for a new minor release).

This happens because conventional-changelog always generates the
changelog from the latest tag in a given branch to `HEAD`. The
tag in the patch branch does not correspond to any SHA in `master`
so the intersection of commits is not automatically omitted.

We work around this naively (until we have a better tool provided
by dev-infra), by deduping commits that are already part of the
changelog. This has proven to work as expected in the components
repo.

PR Close #37956
2020-07-08 12:04:47 -07:00
f296fea112 docs: minor fixes to docs related to updating to v10 (#37897)
This commit includes a couple of minor fixes to docs related to updating
to v10:
- Fix markdown link in "Updating to Angular version 10" guide.
- Correctly display numbered list in
  "Solution-style `tsconfig.json` migration" guide.

PR Close #37897
2020-07-07 12:17:05 -07:00
2605fc46e7 feat(dev-infra): merge script should link to original commit when cherry-picking with API strategy (#37889)
The merge script uses `git cherry-pick` for both the API merge strategy
and the autosquash strategy. It uses cherry-pick to push commits to
different target branches (e.g. into the `10.0.x` branch).

Those commits never point to the commits that landed in the primary
Github branch though. For the autosquash strategy the pull request number
is always included, so there is a way to go back to the source. On the other
hand though, for commits cherry-picked in the API merge strategy, the
pull request number might not always be included (due to Github's
implementation of the rebase merge method).

e.g.
27f52711c0

For those cases we'd want to link the cherry-picked commits to the
original commits so that the corresponding PR is easier to track
down. This is not needed for the autosquash strategy (as outlined
before), but it would have been good for consistency. Unfortunately
though this would rather complicate the strategy as the autosquash
strategy cherry-picks directly from the PR head, so the SHAs that
are used in the primary branch are not known.

PR Close #37889
2020-07-07 12:16:22 -07:00
9d54b3a14b fix(docs-infra): prevent search-bar from overlapping nav-items (#37938)
As part of angular.io's responsive layout, the menu shown in the top-bar
is collapsed into the sidenav on narrow screens at the point where the
search-bar (on the right side of the top-bar) would overlap with the
menu's nav-items.

Previously, the value used as break-point would work on marketing pages,
where the hamburger button is not shown on wide screens. However, on
docs pages (where the hamburger button is always shown, pushing the menu
further to the right), the search-bar would still overlap the menu
nav-items on some resolutions.

This commit fixes it by raising the screen width threshold at a value
that ensures there is no overlap even on pages where the hamburger
button is visible alongside the top-bar menu.

Fixes #37937

PR Close #37938
2020-07-06 13:57:38 -07:00
d09a6283ed refactor(docs-infra): decouple showing the top-menu in top-bar from showing the sidenav (#37938)
As part of angular.io's responsive layout, the following rules are
applied:
- On wide screens, a menu is shown in the top-bar and the sidenav is
  shown side-by-side with the docs content.
- On narrow screens, the top-menu is moved from the top-bar to the
  sidenav and the sidenav is closed by default and floats over the
  content when manually opened.

Previously, the break-points at which the top-menu was shown in the
top-bar and the sidenav was shown side-by-side with the content were the
same (using a single variable).

This commit decouples the two break-points to make it possible to use
different values in the future.

PR Close #37938
2020-07-06 13:57:38 -07:00
1c168c3a44 refactor(docs-infra): use Sass variable for top-bar hamburger button show/hide threshold (#37938)
Use a Sass variable for the screen width break-point at which the
top-bar hamburger button is hidden/shown. This allows more easily
updating the break-point.

PR Close #37938
2020-07-06 13:57:38 -07:00
0f74479c47 build(docs-infra): improve applying post-install patches (#37896)
In `aio/`, we have a mechanism to apply patches in a `postinstall` hook.
See `aio/tools/cli-patches/README.md` for more info.

Previously, we had to update `aio/tools/cli-patches/patch.js` to list
each `.patch` file separately. While working on #37688, I found it
helpful for the script to automatically pick up `.patch` files.

This commit updates the script to automatically pick up and apply
`.patch` files from the `aio/tools/cli-patches/` directory. If one wants
to keep a `.patch` file but not apply it, they can change the file's
extension or move it to a sub-directory (without having to update the
script).

PR Close #37896
2020-07-06 13:56:15 -07:00
790bb949f6 fix(core): handle spaces after select and plural ICU keywords (#37866)
Currently when the `plural` or `select` keywords in an ICU contain trailing spaces (e.g. `{count, select , ...}`), these spaces are also included into the key names in ICU vars (e.g. "VAR_SELECT "). These trailing spaces are not desirable, since they will later be converted into `_` symbols while normalizing placeholder names, thus causing mismatches at runtime (i.e. placeholder will not be replaced with the correct value). This commit updates the code to trim these spaces while generating an object with placeholders, to make sure the runtime logic can replace these placeholders with the right values.

PR Close #37866
2020-07-06 13:55:48 -07:00
2adcad6dd2 fix(dev-infra): fix typo in ng-dev config (#37862)
The logic to exclude certain types of commits (specifically 'docs' ones) was implemented in c5b125b7db. The ng-dev config was updated in the followup commit acf3cff9ee, but there was a typo that prevented the new logic from being activated. This commit updates the name of the config option in the ng-dev config to the right one (minBodyLengthTypeExcludes).

PR Close #37862
2020-07-06 13:55:19 -07:00
242ef1ace1 docs: mention for depreciation for Testbed.get() (#37815)
As mention in https://angular.io/guide/deprecations for this API, it may be important mention for this to make developers migrate or avoid using it

PR Close #37815
2020-07-06 13:42:28 -07:00
842b6a1247 docs: clean up api doc in core (#37053)
Add introductions to usage examples and edit descriptions to be more complete and consistent with current API reference styles

PR Close #37053
2020-06-30 12:11:16 -07:00
98335529eb docs: add missing single quote (#37854)
The current code is missing a single quote at the end of the import.

(cherry picked from commit e13171ea2960dd0fa0666cb964b53799d2883e3a)

PR Close #37854
2020-06-30 12:10:14 -07:00
ca7ee794bf release: cut the v10.0.2 release 2020-06-30 11:40:40 -07:00
f9f2ba6faf docs: add Sonu Kapoor to the collaborator list (#37777)
After 6 months of continuous contributions, Sonu Kapoor did finally make
it into the collaborator list.

PR Close #37777
2020-06-30 10:47:55 -07:00
aea1d211d4 docs: update /config/app-package-json redirect (#37774)
With this change we change the redirect for `/config/app-package-json` from `https://webpack.js.org/configuration/optimization/#optimizationsideeffects` to `https://angular.io/guide/strict-mode#non-local-side-effects-in-applications`

The latter page has more details.

PR Close #37774
2020-06-30 10:45:53 -07:00
57a518a36d perf(compiler-cli): fix memory leak in retained incremental state (#37835)
Incremental compilation allows for the output state of one compilation to be
reused as input to the next compilation. This involves retaining references
to instances from prior compilations, which must be done carefully to avoid
memory leaks.

This commit fixes such a leak with a complicated retention chain:

* `TrackedIncrementalBuildStrategy` unnecessarily hangs on to the previous
  `IncrementalDriver` (state of the previous compilation) once the current
  compilation completes.

  In general this is unnecessary, but should be safe as long as the chain
  only goes back one level - if the `IncrementalDriver` doesn't retain any
  previous `TrackedIncrementalBuildStrategy` instances. However, this does
  happen:

* `NgCompiler` indirectly causes retention of previous `NgCompiler`
  instances (and thus previous `TrackedIncrementalBuildStrategy` instances)
  through accidental capture of the `this` context in a closure created in
  its constructor. This closure is wrapped in a `ts.ModuleResolutionCache`
  used to create a `ModuleResolver` class, which is passed to the program's
  `TraitCompiler` on construction.

* The `IncrementalDriver` retains a reference to the `TraitCompiler` of the
  previous compilation, completing the reference chain.

The final retention chain thus looks like:

* `TrackedIncrementalBuildStrategy` of current program
* `.previous`: `IncrementalDriver` of previous program
* `.lastGood.traitCompiler`: `TraitCompiler`
* `.handlers[..].moduleResolver.moduleResolutionCache`: cache
* (via `getCanonicalFileName` closure): `NgCompiler`
* `.incrementalStrategy`: `TrackedIncrementalBuildStrategy` of previous
  program.

The closure link is the "real" leak here. `NgCompiler` is creating a closure
for `getCanonicalFileName`, delegating to its
`this.adapter.getCanonicalFileName`, for the purposes of creating a
`ts.ModuleResolutionCache`. The fact that the closure references
`NgCompiler` thus eventually causes previous `NgCompiler` iterations to be
retained. This is also potentially problematic due to the shared nature of
`ts.ModuleResolutionCache`, which is potentially retained across multiple
compilations intentionally.

This commit fixes the first two links in the retention chain: the build
strategy is patched to not retain a `previous` pointer, and the `NgCompiler`
is patched to not create a closure in the first place, but instead pass a
bound function. This ensures that the `NgCompiler` does not retain previous
instances of itself in the first place, even if the build strategy does
end up retaining the previous incremental state unnecessarily.

The third link (`IncrementalDriver` unnecessarily retaining the whole
`TraitCompiler`) is not addressed in this commit as it's a more
architectural problem that will require some refactoring. However, the leak
potential of this retention is eliminated thanks to fixing the first two
issues.

PR Close #37835
2020-06-29 16:34:52 -07:00
29b83189b0 build(docs-infra): update to latest dgeni-packages (#37793)
This update of dgeni-packages to 0.28.4 fixes the
rendering of type initializers for classes and interfaces.

Closes #37694

PR Close #37793
2020-06-29 15:01:16 -07:00
1d3df7885d docs: correct the spelling mistake in observables error handling code (#36437)
This commit fixes a spelling error in the word error in the
observables.md guide. It is currently
spelled errror  and the mistake is not intentional.

PR Close #36437
2020-06-29 15:00:39 -07:00
fd06ffa2af docs: change definition of providedIn any (#35292)
change in the definition of providedIn:any any instance creates a singleton instance
for each lazy loaded module and one instance for eager loaded module

PR Close #35292
2020-06-29 15:00:01 -07:00
36a1622dd1 docs: correct left nav to remove duplicated page links (#37833)
The major sections Angular Libraries, Schematics, and CLI Builders appear twice, in their old location under Techniques, and in the new correct location under Extending Angular.

PR Close #37833
2020-06-29 14:57:37 -07:00
7a91b23cb5 fix(core): fake_async_fallback should have the same logic with fake-async (#37680)
PR https://github.com/angular/angular/pull/37523 failed when trying to use `rxjs delay` operator
inside `fakeAsync`, and the reasons are:

1. we need to import `rxjs-fake-async` patch to make the integration work.
2. since in `angular` repo, the bazel target `/tools/testing:node` not using `zone-testing` bundle,
instead it load `zone-spec` packages seperately, so it causes one issue which is the `zone.js/testing/fake-async`
package is not loaded, we do have a fallback logic under `packages/core/testing` calles `fake_async_fallback`,
but the logic is out of date with `fake-async` under `zone.js` package.

So this PR, I updated the content of `fake_async_fallback` to make it consistent with
`fake-async`. And I will make another PR to try to remove the `fallback` logic.

PR Close #37680
2020-06-29 12:22:52 -07:00
4b90b6a226 fix(ngcc): prevent including JavaScript sources outside of the package (#37596)
When ngcc creates an entry-point program, the `allowJs` option is enabled
in order to operate on the JavaScript source files of the entry-point.
A side-effect of this approach is that external modules that don't ship
declaration files will also have their JavaScript source files loaded
into the program, as the `allowJs` flag allows for them to be imported.
This may pose an issue in certain edge cases, where ngcc would inadvertently
operate on these external modules. This can introduce all sorts of undesirable
behavior and incompatibilities, e.g. the reflection host that is selected for
the entry-point's format could be incompatible with that of the external
module's JavaScript bundles.

To avoid these kinds of issues, module resolution that would resolve to
a JavaScript file located outside of the package will instead be rejected,
as if the file would not exist. This would have been the behavior when
`allowJs` is set to false, which is the case in typical Angular compilations.

Fixes #37508

PR Close #37596
2020-06-29 12:21:23 -07:00
b13daa4cdf refactor(ngcc): let isWithinPackage operate on paths instead of source files (#37596)
Changes `isWithinPackage` to take an `AbsoluteFsPath` instead of `ts.SourceFile`,
to allow for an upcoming change to use it when no `ts.SourceFile` is available,
but just a path.

PR Close #37596
2020-06-29 12:21:23 -07:00
0c6f026828 docs: Changing typo Stacblitz into Stackblitz in the Tour of Hereos tutorial docs page (#37794)
Changing the typo of Stacblitz into Stackblitz in the tour of hereos tutorial docs page since that is the actual name of the service

PR Close #37794
2020-06-29 12:17:41 -07:00
a2520bd267 docs: remove first person from 2 sentences (#37768)
This commit removes two instances of the first person in the
Dependency injection providers documentation.

PR Close #37768
2020-06-29 12:17:04 -07:00
b928a209a4 docs: add Amadou Sall to GDE page (#36509)
This commit adds Amadou Sall to the Angular GDE page along with a
biography, his role at Air France, and a photograph.

PR Close #36509
2020-06-29 12:16:23 -07:00
89e16ed6a5 fix(elements): fire custom element output events during component initialization (#37570)
Previously, event listeners for component output events attached on an
Angular custom element before inserting it into the DOM (i.e. before
instantiating the underlying component) didn't fire for events emitted
during initialization lifecycle hooks, such as `ngAfterContentInit`,
`ngAfterViewInit`, `ngOnChanges` (initial call) and `ngOnInit`.
The reason was that `NgElementImpl` [subscribed to events][1] _after_
calling [ngElementStrategy#connect()][2], which is where the
[initial change detection][3] takes place (running the initialization
lifecycle hooks).

This commit fixes this by:
1. Ensuring `ComponentNgElementStrategy#events` is defined and available
   for subscribing to, even before instantiating the component.
2. Changing `NgElementImpl` to subscribe to `NgElementStrategy#events`
   (if available) before calling `NgElementStrategy#connect()` (which
   initializes the component instance) if available.
3. Falling back to the old behavior (subscribing to `events` after
   calling `connect()` for strategies that do not initialize `events`
   before their `connect()` is run).

NOTE:
By falling back to the old behavior when `NgElementStrategy#events` is
not initialized before calling `NgElementStrategy#connect()`, we avoid
breaking existing custom `NgElementStrategy` implementations (with
@remackgeek's [ElementZoneStrategy][4] being a commonly used example).

Jira issue: [FW-2010](https://angular-team.atlassian.net/browse/FW-2010)

[1]: c0143cb2ab/packages/elements/src/create-custom-element.ts (L167-L170)
[2]: c0143cb2ab/packages/elements/src/create-custom-element.ts (L164)
[3]: c0143cb2ab/packages/elements/src/component-factory-strategy.ts (L158)
[4]: f1b6699495/projects/elements-zone-strategy/src/lib/element-zone-strategy.ts

Fixes #36141

PR Close #37570
2020-06-29 10:33:40 -07:00
1a1f99af37 fix(ngcc): ensure lockfile is removed when analyzeFn fails (#37739)
Previously an error thrown in the `analyzeFn` would cause
the ngcc process to exit immediately without removing the
lockfile, and potentially before the unlocker process had been
successfully spawned resulting in the lockfile being orphaned
and left behind.

Now we catch these errors and remove the lockfile as needed.

PR Close #37739
2020-06-29 10:29:12 -07:00
df2cd37ed2 fix(core): error when invoking callbacks registered via ViewRef.onDestroy (#37543) (#37783)
Invoking a callback registered through `ViewRef.onDestroy` throws an error, because we weren't registering it correctly in the internal data structure. These changes also remove the `storeCleanupFn` function, because it was mostly identical to `storeCleanupWithContext` and was only used in one place.

Fixes #36213.

PR Close #37543

PR Close #37783
2020-06-29 10:27:39 -07:00
12a71bc6bc fix(core): determine required DOMParser feature availability (#36578) (#37783)
Verify that HTML parsing is supported in addition to DOMParser existence.
This maybe wasn't as important before when DOMParser was used just as a
fallback on Firefox, but now that DOMParser is the default choice, we need
to be more accurate.

PR Close #37783
2020-06-29 10:27:39 -07:00
7d270c235a refactor(core): split inert strategies to separate classes (#36578) (#37783)
The `inertDocument` member is only needed when using the InertDocument
strategy. By separating the DOMParser and InertDocument strategies into
separate classes, we can easily avoid creating the inert document
unnecessarily when using DOMParser.

PR Close #37783
2020-06-29 10:27:39 -07:00
b0b7248504 fix(core): do not trigger CSP alert/report in Firefox and Chrome (#36578) (#37783)
If [innerHTML] is used in a component and a Content-Security-Policy is set
that does not allow inline styles then Firefox and Chrome show the following
message:

> Content Security Policy: The page’s settings observed the loading of a
resource at self (“default-src”). A CSP report is being sent.

This message is caused because Angular is creating an inline style tag to
test for a browser bug that we use to decide what sanitization strategy to
use, which causes CSP violation errors if inline CSS is prohibited.

This test is no longer necessary, since the `DOMParser` is now safe to use
and the `style` based check is redundant.

In this fix, we default to using `DOMParser` if it is available and fall back
to `createHTMLDocument()` if needed. This is the approach used by DOMPurify
too.

The related unit tests in `html_sanitizer_spec.ts`, "should not allow
JavaScript execution when creating inert document" and "should not allow
JavaScript hidden in badly formed HTML to get through sanitization (Firefox
bug)", are left untouched to assert that the behavior hasn't changed in
those scenarios.

Fixes #25214.

PR Close #37783
2020-06-29 10:27:38 -07:00
78460c1848 test(core): update symbols used in the test app (#37785)
This commit updates the golden file that contains the set of symbols used in the test TODO app. The `storeCleanupFn` function was replaced by `storeCleanupWithContext` in 75b119eafc and this commit updates the golden file to reflect that.

PR Close #37785
2020-06-26 16:44:00 -07:00
75b119eafc fix(core): error when invoking callbacks registered via ViewRef.onDestroy (#37543)
Invoking a callback registered through `ViewRef.onDestroy` throws an error, because we weren't registering it correctly in the internal data structure. These changes also remove the `storeCleanupFn` function, because it was mostly identical to `storeCleanupWithContext` and was only used in one place.

Fixes #36213.

PR Close #37543
2020-06-26 15:02:43 -07:00
64b0ae93f7 fix(core): don't consider inherited NG_ELEMENT_ID during DI (#37574)
Special DI tokens like `ChangeDetectorRef` and `ElementRef` can provide a factory via `NG_ELEMENT_ID`. The problem is that we were reading it off the token as `token[NG_ELEMENT_ID]` which will go up the prototype chain if it couldn't be found on the current token, resulting in the private `ViewRef` API being exposed, because it extends `ChangeDetectorRef`.

These changes fix the issue by guarding the property access with `hasOwnProperty`.

Fixes #36235.

PR Close #37574
2020-06-26 15:01:21 -07:00
7c0b25f5a6 fix(language-service): incorrect autocomplete results on unknown symbol (#37518)
This commit fixes a bug whereby the language service would incorrectly
return HTML elements if autocomplete is requested for an unknown symbol.
This is because we walk through every possible scenario, and fallback to
element autocomplete if none of the scenarios match.

The fix here is to return results from interpolation if we know for sure
we are in a bound text. This means we will now return an empty results if
there is no suggestions.

This commit also refactors the code a little to make it easier to
understand.

PR Close #37518
2020-06-26 14:51:33 -07:00
73 changed files with 962 additions and 507 deletions

View File

@ -4,7 +4,7 @@ import {MergeConfig} from '../dev-infra/pr/merge/config';
const commitMessage = {
'maxLength': 120,
'minBodyLength': 100,
'minBodyLengthExcludes': ['docs'],
'minBodyLengthTypeExcludes': ['docs'],
'types': [
'build',
'ci',

View File

@ -1,3 +1,37 @@
<a name="10.0.3"></a>
## 10.0.3 (2020-07-08)
### Bug Fixes
* **core:** handle spaces after `select` and `plural` ICU keywords ([#37866](https://github.com/angular/angular/issues/37866)) ([790bb94](https://github.com/angular/angular/commit/790bb94))
<a name="10.0.2"></a>
## [10.0.2](https://github.com/angular/angular/compare/10.0.1...10.0.2) (2020-06-30)
### Bug Fixes
* **core:** determine required DOMParser feature availability ([#36578](https://github.com/angular/angular/issues/36578)) ([#37783](https://github.com/angular/angular/issues/37783)) ([12a71bc](https://github.com/angular/angular/commit/12a71bc))
* **core:** do not trigger CSP alert/report in Firefox and Chrome ([#36578](https://github.com/angular/angular/issues/36578)) ([#37783](https://github.com/angular/angular/issues/37783)) ([b0b7248](https://github.com/angular/angular/commit/b0b7248)), closes [#25214](https://github.com/angular/angular/issues/25214)
* **core:** don't consider inherited NG_ELEMENT_ID during DI ([#37574](https://github.com/angular/angular/issues/37574)) ([64b0ae9](https://github.com/angular/angular/commit/64b0ae9)), closes [#36235](https://github.com/angular/angular/issues/36235)
* **core:** error when invoking callbacks registered via ViewRef.onDestroy ([#37543](https://github.com/angular/angular/issues/37543)) ([75b119e](https://github.com/angular/angular/commit/75b119e)), closes [#36213](https://github.com/angular/angular/issues/36213)
* **core:** error when invoking callbacks registered via ViewRef.onDestroy ([#37543](https://github.com/angular/angular/issues/37543)) ([#37783](https://github.com/angular/angular/issues/37783)) ([df2cd37](https://github.com/angular/angular/commit/df2cd37)), closes [#36213](https://github.com/angular/angular/issues/36213)
* **core:** fake_async_fallback should have the same logic with fake-async ([#37680](https://github.com/angular/angular/issues/37680)) ([7a91b23](https://github.com/angular/angular/commit/7a91b23))
* **elements:** fire custom element output events during component initialization ([#37570](https://github.com/angular/angular/issues/37570)) ([89e16ed](https://github.com/angular/angular/commit/89e16ed)), closes [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts#L167-L170](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts/issues/L167-L170) [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts#L164](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/create-custom-element.ts/issues/L164) [/github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/component-factory-strategy.ts#L158](https://github.com//github.com/angular/angular/blob/c0143cb2abdd172de1b95fd1d2c4cfc738640e28/packages/elements/src/component-factory-strategy.ts/issues/L158) [#36141](https://github.com/angular/angular/issues/36141)
* **language-service:** incorrect autocomplete results on unknown symbol ([#37518](https://github.com/angular/angular/issues/37518)) ([7c0b25f](https://github.com/angular/angular/commit/7c0b25f))
* **ngcc:** ensure lockfile is removed when analyzeFn fails ([#37739](https://github.com/angular/angular/issues/37739)) ([1a1f99a](https://github.com/angular/angular/commit/1a1f99a))
* **ngcc:** prevent including JavaScript sources outside of the package ([#37596](https://github.com/angular/angular/issues/37596)) ([4b90b6a](https://github.com/angular/angular/commit/4b90b6a)), closes [#37508](https://github.com/angular/angular/issues/37508)
### Performance Improvements
* **compiler-cli:** fix memory leak in retained incremental state ([#37835](https://github.com/angular/angular/issues/37835)) ([57a518a](https://github.com/angular/angular/commit/57a518a))
<a name="10.0.1"></a>
## [10.0.1](https://github.com/angular/angular/compare/10.0.0...10.0.1) (2020-06-26)

View File

@ -197,11 +197,11 @@ Like `EvenBetterLogger`, `HeroService` needs to know if the user is authorized
That authorization can change during the course of a single application session,
as when you log in a different user.
Let's say you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
Imagine that you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
`HeroService` won't have direct access to the user information to decide
who is authorized and who isn't.
To resolve this, we give the `HeroService` constructor a boolean flag to control display of secret heroes.
To resolve this, give the `HeroService` constructor a boolean flag to control display of secret heroes.
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" header="src/app/heroes/hero.service.ts (excerpt)"></code-example>

View File

@ -834,7 +834,7 @@ Global variants of the locale data are available in [`@angular/common/locales/gl
The following example imports the global variants for French (`fr`):
<code-example language="javascript" header="app.module.ts">
import '@angular/common/locales/global/fr;
import '@angular/common/locales/global/fr';
</code-example>
{@a custom-id}

View File

@ -5,6 +5,7 @@
This migration adds support to existing projects for TypeScript's new ["solution-style" tsconfig feature](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig).
Support is added by making two changes:
1. Renaming the workspace-level `tsconfig.json` to `tsconfig.base.json`.
All project [TypeScript configuration files](guide/typescript-configuration) will extend from this base which contains the common options used throughout the workspace.

View File

@ -112,7 +112,7 @@ Because observables produce values asynchronously, try/catch will not effectivel
<code-example>
myObservable.subscribe({
next(num) { console.log('Next num: ' + num)},
error(err) { console.log('Received an errror: ' + err)}
error(err) { console.log('Received an error: ' + err)}
});
</code-example>

View File

@ -359,10 +359,11 @@ Then inject it inside a test by calling `TestBed.inject()` with the service clas
<div class="alert is-helpful">
**Note:** We used to have `TestBed.get()` instead of `TestBed.inject()`.
The `get` method wasn't type safe, it always returned `any`, and this is error prone.
We decided to migrate to a new function instead of updating the existing one given
the large scale use that would have an immense amount of breaking changes.
**Note:** `TestBed.get()` was deprecated as of Angular version 9.
To help minimize breaking changes, Angular introduces a
new function called `TestBed.inject()`, which you should use instead.
For information on the removal of `TestBed.get()`,
see its entry in the [Deprecations index](guide/deprecations#index).
</div>

View File

@ -30,7 +30,7 @@ If you're curious about the specific migrations being run by the CLI, see the [a
* The `minLength` and `maxLength` validators only validate values that have a numeric `length` property. See [PR 36157](https://github.com/angular/angular/pull/36157).
* Templates with unknown property bindings or unknown element names now log errors instead of warnings. See [PR 36399](https://github.com/angular/angular/pull/36399).
* `UrlMatcher` can now return `null` values. See [PR 36402](https://github.com/angular/angular/pull/36402).
* Transplanted views now refresh at insertion point only. See PR 35968](https://github.com/angular/angular/pull/35968).
* Transplanted views now refresh at insertion point only. See [PR 35968](https://github.com/angular/angular/pull/35968).
* Formatting times with the `b` or `B` format codes now supports time periods that cross midnight. See [PR 36611](https://github.com/angular/angular/pull/36611).
* Navigation is canceled for routes with at least one empty resolver. See [PR 24621](https://github.com/angular/angular/pull/24621).

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -596,6 +596,13 @@
"twitter": "devjoost",
"bio": "Joost is a Software Engineer from the Netherlands with an interest in open source software who likes to learn something new every day. He works at Blueriq during the day and contributes to Angular in his spare time, by working on the Angular compiler and runtime. He may review your PR even if you never asked for it ;)"
},
"sonukapoor": {
"name": "Sonu Kapoor",
"groups": ["Collaborators"],
"picture": "sonukapoor.jpg",
"website": "https://www.linkedin.com/in/sonu-kapoor/",
"bio": "Sonu is a Software Engineer from Toronto, with a high interest in front-end technologies and algorithms."
},
"jschwarty": {
"name": "Justin Schwartzenberger",
"picture": "justinschwartzenberger.jpg",
@ -815,5 +822,13 @@
"website": "https://wellwind.idv.tw/blog/",
"bio": "Mike is a full-stack developer, consultant, blogger, instructor, and conference speaker. He has over 10 years of web development experience and passion to share his knowledge.",
"groups": ["GDE"]
},
"ahasall": {
"name": "Amadou Sall",
"picture": "ahasall.jpg",
"groups": ["GDE"],
"twitter": "ahasall",
"website": "https://www.amadousall.com",
"bio": "Amadou is a Frontend Software Engineer from Senegal based in France. He currently works at Air France where he helps developers build better Angular applications. Passionate about web technologies, Amadou is an international speaker, a technical writer, and a Google Developer Expert in Angular."
}
}

View File

@ -506,80 +506,6 @@
"url": "guide/universal",
"title": "Server-side Rendering",
"tooltip": "Render HTML server-side with Angular Universal."
},
{
"title": "Upgrading from AngularJS",
"tooltip": "Incrementally upgrade an AngularJS application to Angular.",
"children": [
{
"url": "guide/upgrade-setup",
"title": "Setup for Upgrading from AngularJS",
"tooltip": "Use code from the Angular QuickStart seed as part of upgrading from AngularJS.",
"hidden": true
},
{
"url": "guide/upgrade",
"title": "Upgrading Instructions",
"tooltip": "Incrementally upgrade an AngularJS application to Angular."
},
{
"url": "guide/upgrade-performance",
"title": "Upgrading for Performance",
"tooltip": "Upgrade from AngularJS to Angular in a more flexible way."
},
{
"url": "guide/ajs-quick-reference",
"title": "AngularJS-Angular Concepts",
"tooltip": "Learn how AngularJS concepts and techniques map to Angular."
}
]
},
{
"title": "Angular Libraries",
"tooltip": "Extending Angular with shared libraries.",
"children": [
{
"url": "guide/libraries",
"title": "Libraries Overview",
"tooltip": "Understand how and when to use or create libraries."
},
{
"url": "guide/using-libraries",
"title": "Using Published Libraries",
"tooltip": "Integrate published libraries into an app."
},
{
"url": "guide/creating-libraries",
"title": "Creating Libraries",
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
}
]
},
{
"title": "Schematics",
"tooltip": "Using CLI schematics for code generation.",
"children": [
{
"url": "guide/schematics",
"title": "Schematics Overview",
"tooltip": "How the CLI uses schematics to generate code."
},
{
"url": "guide/schematics-authoring",
"title": "Authoring Schematics",
"tooltip": "Understand the structure of a schematic."
},
{
"url": "guide/schematics-for-libraries",
"title": "Schematics for Libraries",
"tooltip": "Use schematics to integrate your library with the Angular CLI."
}
]
},
{
"url": "guide/cli-builder",
"title": "CLI Builders",
"tooltip": "Using builders to customize Angular CLI."
}
]
},

View File

@ -6,7 +6,7 @@
In this tutorial, you build your own app from the ground up, providing experience with the typical development process, as well as an introduction to basic app-design concepts, tools, and terminology.
If you're completely new to Angular, you might want to try the [**Try it now**](start) quick-start app first.
It is based on a ready-made partially-completed project, which you can examine and modify in the StacBlitz interactive development environment, where you can see the results in real time.
It is based on a ready-made partially-completed project, which you can examine and modify in the StackBlitz interactive development environment, where you can see the results in real time.
The "Try it" tutorial covers the same major topics&mdash;components, template syntax, routing, services, and accessing data via HTTP&mdash;in a condensed format, following the most current best practices.

View File

@ -128,7 +128,7 @@
// The below paths are referenced in users projects generated by the CLI
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
{"type": 301, "source": "/config/app-package-json", "destination": "https://webpack.js.org/configuration/optimization/#optimizationsideeffects"}
{"type": 301, "source": "/config/app-package-json", "destination": "/guide/strict-mode#non-local-side-effects-in-applications"}
],
"rewrites": [
{

View File

@ -123,7 +123,7 @@
"cross-spawn": "^5.1.0",
"css-selector-parser": "^1.3.0",
"dgeni": "^0.4.11",
"dgeni-packages": "^0.28.3",
"dgeni-packages": "^0.28.4",
"entities": "^1.1.1",
"eslint": "^3.19.0",
"eslint-plugin-jasmine": "^2.2.0",
@ -175,4 +175,4 @@
"xregexp": "^4.0.0",
"yargs": "^7.0.2"
}
}
}

View File

@ -14,11 +14,11 @@
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
<mat-icon svgIcon="menu"></mat-icon>
</button>
<a class="nav-link home" href="/" [ngSwitch]="isSideBySide">
<a class="nav-link home" href="/" [ngSwitch]="showTopMenu">
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
</a>
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
<div class="toolbar-external-icons-container">
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
@ -35,9 +35,9 @@
<mat-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
<mat-sidenav [ngClass]="{'collapsed': !isSideBySide}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu>
<mat-sidenav [ngClass]="{'collapsed': !dockSideNav}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
<aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="dockSideNav"></aio-nav-menu>
<div class="doc-version">
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select>

View File

@ -25,11 +25,9 @@ import { first, mapTo } from 'rxjs/operators';
import { MockLocationService } from 'testing/location.service';
import { MockLogger } from 'testing/logger.service';
import { MockSearchService } from 'testing/search.service';
import { AppComponent } from './app.component';
import { AppComponent, dockSideNavWidth, showFloatingTocWidth, showTopMenuWidth } from './app.component';
import { AppModule } from './app.module';
const sideBySideBreakPoint = 992;
const hideToCBreakPoint = 800;
const startedDelay = 100;
describe('AppComponent', () => {
@ -58,7 +56,7 @@ describe('AppComponent', () => {
component = fixture.componentInstance;
fixture.detectChanges();
component.onResize(sideBySideBreakPoint + 1); // wide by default
component.onResize(showTopMenuWidth + 1); // wide by default
const de = fixture.debugElement;
const docViewerDe = de.query(By.css('aio-doc-viewer'));
@ -99,7 +97,7 @@ describe('AppComponent', () => {
});
it('should be false on narrow screens', () => {
component.onResize(hideToCBreakPoint - 1);
component.onResize(showFloatingTocWidth - 1);
tocService.tocList.next([{}, {}, {}] as TocItem[]);
expect(component.hasFloatingToc).toBe(false);
@ -112,7 +110,7 @@ describe('AppComponent', () => {
});
it('should be true on wide screens unless the toc is empty', () => {
component.onResize(hideToCBreakPoint + 1);
component.onResize(showFloatingTocWidth + 1);
tocService.tocList.next([{}, {}, {}] as TocItem[]);
expect(component.hasFloatingToc).toBe(true);
@ -127,37 +125,47 @@ describe('AppComponent', () => {
it('should be false when toc is empty', () => {
tocService.tocList.next([]);
component.onResize(hideToCBreakPoint + 1);
component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(false);
component.onResize(hideToCBreakPoint - 1);
component.onResize(showFloatingTocWidth - 1);
expect(component.hasFloatingToc).toBe(false);
component.onResize(hideToCBreakPoint + 1);
component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(false);
});
it('should be true when toc is not empty unless the screen is narrow', () => {
tocService.tocList.next([{}, {}, {}] as TocItem[]);
component.onResize(hideToCBreakPoint + 1);
component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(true);
component.onResize(hideToCBreakPoint - 1);
component.onResize(showFloatingTocWidth - 1);
expect(component.hasFloatingToc).toBe(false);
component.onResize(hideToCBreakPoint + 1);
component.onResize(showFloatingTocWidth + 1);
expect(component.hasFloatingToc).toBe(true);
});
});
describe('isSideBySide', () => {
describe('showTopMenu', () => {
it('should be updated on resize', () => {
component.onResize(sideBySideBreakPoint - 1);
expect(component.isSideBySide).toBe(false);
component.onResize(showTopMenuWidth - 1);
expect(component.showTopMenu).toBe(false);
component.onResize(sideBySideBreakPoint + 1);
expect(component.isSideBySide).toBe(true);
component.onResize(showTopMenuWidth + 1);
expect(component.showTopMenu).toBe(true);
});
});
describe('dockSideNav', () => {
it('should be updated on resize', () => {
component.onResize(dockSideNavWidth - 1);
expect(component.dockSideNav).toBe(false);
component.onResize(dockSideNavWidth + 1);
expect(component.dockSideNav).toBe(true);
});
});
@ -185,8 +193,8 @@ describe('AppComponent', () => {
fixture.detectChanges();
};
describe('when side-by-side (wide)', () => {
beforeEach(() => resizeTo(sideBySideBreakPoint + 1)); // side-by-side
describe('when view is wide', () => {
beforeEach(() => resizeTo(dockSideNavWidth + 1)); // wide view
it('should open when navigating to a guide page (guide/pipes)', () => {
navigateTo('guide/pipes');
@ -232,8 +240,8 @@ describe('AppComponent', () => {
});
});
describe('when NOT side-by-side (narrow)', () => {
beforeEach(() => resizeTo(sideBySideBreakPoint - 1)); // NOT side-by-side
describe('when view is narrow', () => {
beforeEach(() => resizeTo(dockSideNavWidth - 1)); // narrow view
it('should be closed when navigating to a guide page (guide/pipes)', () => {
navigateTo('guide/pipes');
@ -286,30 +294,30 @@ describe('AppComponent', () => {
});
});
describe('when changing side-by-side (narrow --> wide)', () => {
describe('when changing from narrow to wide view', () => {
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
const nonSidenavDocs = ['features', 'about'];
sidenavDocs.forEach(doc => {
it(`should open when on a sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1);
resizeTo(dockSideNavWidth - 1);
navigateTo(doc);
expect(sidenav.opened).toBe(false);
resizeTo(sideBySideBreakPoint + 1);
resizeTo(dockSideNavWidth + 1);
expect(sidenav.opened).toBe(true);
});
});
nonSidenavDocs.forEach(doc => {
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1);
resizeTo(dockSideNavWidth - 1);
navigateTo(doc);
expect(sidenav.opened).toBe(false);
resizeTo(sideBySideBreakPoint + 1);
resizeTo(dockSideNavWidth + 1);
expect(sidenav.opened).toBe(false);
});
});
@ -317,33 +325,33 @@ describe('AppComponent', () => {
describe('when manually opened', () => {
sidenavDocs.forEach(doc => {
it(`should remain opened when on a sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1);
resizeTo(dockSideNavWidth - 1);
navigateTo(doc);
toggleSidenav();
expect(sidenav.opened).toBe(true);
resizeTo(sideBySideBreakPoint + 1);
resizeTo(dockSideNavWidth + 1);
expect(sidenav.opened).toBe(true);
});
});
nonSidenavDocs.forEach(doc => {
it(`should close when on a non-sidenav doc (${doc})`, () => {
resizeTo(sideBySideBreakPoint - 1);
resizeTo(dockSideNavWidth - 1);
navigateTo(doc);
toggleSidenav();
expect(sidenav.opened).toBe(true);
resizeTo(sideBySideBreakPoint + 1);
resizeTo(showTopMenuWidth + 1);
expect(sidenav.opened).toBe(false);
});
});
});
});
describe('when changing side-by-side (wide --> narrow)', () => {
describe('when changing from wide to narrow view', () => {
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
const nonSidenavDocs = ['features', 'about'];
@ -352,7 +360,7 @@ describe('AppComponent', () => {
navigateTo(doc);
expect(sidenav.opened).toBe(true);
resizeTo(sideBySideBreakPoint - 1);
resizeTo(dockSideNavWidth - 1);
expect(sidenav.opened).toBe(false);
});
});
@ -362,7 +370,7 @@ describe('AppComponent', () => {
navigateTo(doc);
expect(sidenav.opened).toBe(false);
resizeTo(sideBySideBreakPoint - 1);
resizeTo(dockSideNavWidth - 1);
expect(sidenav.opened).toBe(false);
});
});
@ -376,7 +384,7 @@ describe('AppComponent', () => {
async function setupSelectorForTesting(mode?: string) {
createTestingModule('a/b', mode);
await initializeTest();
component.onResize(sideBySideBreakPoint + 1); // side-by-side
component.onResize(dockSideNavWidth + 1); // wide view
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
selectComponent = selectElement.componentInstance;
}

View File

@ -14,6 +14,9 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { first, map } from 'rxjs/operators';
const sideNavView = 'SideNav';
export const showTopMenuWidth = 1048;
export const dockSideNavWidth = 992;
export const showFloatingTocWidth = 800;
@Component({
selector: 'aio-shell',
@ -57,18 +60,17 @@ export class AppComponent implements OnInit {
isStarting = true;
isTransitioning = true;
isFetching = false;
isSideBySide = false;
showTopMenu = false;
dockSideNav = false;
private isFetchingTimeout: any;
private isSideNavDoc = false;
private sideBySideWidth = 992;
sideNavNodes: NavigationNode[];
topMenuNodes: NavigationNode[];
topMenuNarrowNodes: NavigationNode[];
hasFloatingToc = false;
private showFloatingToc = new BehaviorSubject(false);
private showFloatingTocWidth = 800;
tocMaxHeight: string;
private tocMaxHeightOffset = 0;
@ -76,8 +78,8 @@ export class AppComponent implements OnInit {
private currentUrl: string;
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
get mode() { return this.isSideBySide ? 'side' : 'over'; }
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
get mode() { return this.dockSideNav && (this.isSideNavDoc || this.showTopMenu) ? 'side' : 'over'; }
// Search related properties
showSearchResults = false;
@ -239,13 +241,14 @@ export class AppComponent implements OnInit {
@HostListener('window:resize', ['$event.target.innerWidth'])
onResize(width: number) {
this.isSideBySide = width >= this.sideBySideWidth;
this.showFloatingToc.next(width > this.showFloatingTocWidth);
this.showTopMenu = width >= showTopMenuWidth;
this.dockSideNav = width >= dockSideNavWidth;
this.showFloatingToc.next(width > showFloatingTocWidth);
if (this.isSideBySide && !this.isSideNavDoc) {
if (this.showTopMenu && !this.isSideNavDoc) {
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu
// items in the top-bar, ensure the sidenav is closed.
// (This condition can only be met when the resize event changes the value of `isSideBySide`
// (This condition can only be met when the resize event changes the value of `showTopMenu`
// from `false` to `true` while on a non-sidenav doc.)
this.sidenav.toggle(false);
}
@ -338,7 +341,7 @@ export class AppComponent implements OnInit {
}
// May be open or closed when wide; always closed when narrow.
this.sidenav.toggle(this.isSideBySide && openSideNav);
this.sidenav.toggle(this.dockSideNav && openSideNav);
}
// Dynamically change height of table of contents container

View File

@ -1,4 +1,6 @@
// VARIABLES
$showTopMenuWidth: 1048px;
$hideTopMenuWidth: $showTopMenuWidth - 1;
$hamburgerShownMargin: 0 8px 0 0;
$hamburgerHiddenMargin: 0 16px 0 -64px;
@ -59,9 +61,9 @@ aio-shell.folder-docs mat-toolbar.mat-toolbar,
aio-shell.folder-guide mat-toolbar.mat-toolbar,
aio-shell.folder-start mat-toolbar.mat-toolbar,
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
@media (min-width: 992px) {
@media (min-width: $showTopMenuWidth) {
.hamburger.mat-button {
// Hamburger shown on non-marketing pages on large screens.
// Hamburger shown on non-marketing pages even on large screens.
margin: $hamburgerShownMargin;
}
}
@ -73,7 +75,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
margin: $hamburgerShownMargin;
padding: 0;
@media (min-width: 992px) {
@media (min-width: $showTopMenuWidth) {
// Hamburger hidden by default on large screens.
// (Will be shown per doc.)
margin: $hamburgerHiddenMargin;
@ -111,7 +113,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
outline-offset: 4px;
}
@media screen and (max-width: 991px) {
@media screen and (max-width: $hideTopMenuWidth) {
padding: 4px 0;
}
@ -125,7 +127,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
top: 12px;
height: 40px;
@media (max-width: 991px) {
@media (max-width: $hideTopMenuWidth) {
&:hover {
transform: scale(1.1);
}
@ -237,6 +239,7 @@ aio-search-box.search-container {
width: 80%;
}
}
@media (max-width: 480px) {
width: 150px;
}

View File

@ -1,9 +1,14 @@
const fs = require('fs');
const sh = require('shelljs');
const PATCH_LOCK = 'node_modules/@angular/cli/.patched';
if (!fs.existsSync(PATCH_LOCK)) {
sh.set('-e');
sh.cd(`${__dirname}/../../`);
if (!sh.test('-f', PATCH_LOCK)) {
sh.ls('-l', __dirname)
.filter(stat => stat.isFile() && /\.patch$/i.test(stat.name))
.forEach(stat => sh.exec(`patch -p0 -i "${__dirname}/${stat.name}"`));
sh.touch(PATCH_LOCK);
}

View File

@ -4467,10 +4467,10 @@ dezalgo@^1.0.0:
asap "^2.0.0"
wrappy "1"
dgeni-packages@^0.28.3:
version "0.28.3"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.3.tgz#2e1e55f341c389b67ebb28933ce1e7e9ad05c49b"
integrity sha512-WyVzY3Q4ylfnc2677le5G7a7WqkF88rBSjU9IrAofqro71yzZeWLoEdr/gJY+lJZ0PrDyuRW05pFvIbvX8N0PQ==
dgeni-packages@^0.28.4:
version "0.28.4"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.4.tgz#53a3e6700b8d8f6be168cadcc9fdb36e1d7011d3"
integrity sha512-7AUG3pKpWtn69c3v2Mzgh+i5gd+L0AxFfYGWGzBdlJqMlQfaQPQjaS54iYCvnOlK9rXBn9j39yO6EU70gDZuFw==
dependencies:
canonical-path "^1.0.0"
catharsis "^0.8.1"

View File

@ -135,7 +135,13 @@ export class GithubApiMergeStrategy extends MergeStrategy {
// Cherry pick the merged commits into the remaining target branches.
const failedBranches = await this.cherryPickIntoTargetBranches(
`${targetSha}~${targetCommitsCount}..${targetSha}`, cherryPickTargetBranches);
`${targetSha}~${targetCommitsCount}..${targetSha}`, cherryPickTargetBranches, {
// Commits that have been created by the Github API do not necessarily contain
// a reference to the source pull request (unless the squash strategy is used).
// To ensure that original commits can be found when a commit is viewed in a
// target branch, we add a link to the original commits when cherry-picking.
linkToOriginalCommits: true,
});
// We already checked whether the PR can be cherry-picked into the target branches,
// but in case the cherry-pick somehow fails, we still handle the conflicts here. The

View File

@ -69,7 +69,8 @@ export abstract class MergeStrategy {
* @returns A list of branches for which the revisions could not be cherry-picked into.
*/
protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: {
dryRun?: boolean
dryRun?: boolean,
linkToOriginalCommits?: boolean,
} = {}) {
const cherryPickArgs = [revisionRange];
const failedBranches: string[] = [];
@ -82,6 +83,14 @@ export abstract class MergeStrategy {
cherryPickArgs.push('--no-commit');
}
if (options.linkToOriginalCommits) {
// We add `-x` when cherry-picking as that will allow us to easily jump to original
// commits for cherry-picked commits. With that flag set, Git will automatically append
// the original SHA/revision to the commit message. e.g. `(cherry picked from commit <..>)`.
// https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--x.
cherryPickArgs.push('-x');
}
// Cherry-pick the refspec into all determined target branches.
for (const branchName of targetBranches) {
const localTargetBranch = this.getLocalTargetBranchName(branchName);

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 451406,
"main-es2015": 450883,
"polyfills-es2015": 52630
}
}
@ -21,8 +21,8 @@
"master": {
"uncompressed": {
"runtime-es2015": 3097,
"main-es2015": 428886,
"polyfills-es2015": 52195
"main-es2015": 428031,
"polyfills-es2015": 52261
}
}
}

View File

@ -30,8 +30,8 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 136302,
"polyfills-es2015": 37246
"main-es2015": 135533,
"polyfills-es2015": 37248
}
}
},
@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 246085,
"main-es2015": 245488,
"polyfills-es2015": 36938,
"5-es2015": 751
}
@ -62,7 +62,7 @@
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
"bundle": 1210239
"bundle": 1209651
}
}
}

View File

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

View File

@ -12,7 +12,7 @@ import {ParsedConfiguration} from '../../..';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
@ -148,7 +148,8 @@ export class DecorationAnalyzer {
*/
analyzeProgram(): DecorationAnalyses {
for (const sourceFile of this.program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile && isWithinPackage(this.packagePath, sourceFile)) {
if (!sourceFile.isDeclarationFile &&
isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile))) {
this.compiler.analyzeFile(sourceFile);
}
}

View File

@ -7,7 +7,7 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {MetadataReader} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
@ -44,7 +44,7 @@ export class DefaultMigrationHost implements MigrationHost {
}
isInScope(clazz: ClassDeclaration): boolean {
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
return isWithinPackage(this.entryPointPath, absoluteFromSourceFile(clazz.getSourceFile()));
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {isWithinPackage} from './util';
@ -35,7 +35,7 @@ export class SwitchMarkerAnalyzer {
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
const analyzedFiles = new SwitchMarkerAnalyses();
program.getSourceFiles()
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
.filter(sourceFile => isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile)))
.forEach(sourceFile => {
const declarations = this.host.getSwitchableDeclarations(sourceFile);
if (declarations.length) {

View File

@ -5,13 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
import {DependencyTracker} from '../../../src/ngtsc/incremental/api';
export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile));
export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteFsPath): boolean {
const relativePath = relative(packagePath, filePath);
return !relativePath.startsWith('..') && !relativePath.startsWith('node_modules/');
}

View File

@ -28,13 +28,13 @@ export class ClusterExecutor implements Executor {
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
Promise<void> {
return this.lockFile.lock(() => {
return this.lockFile.lock(async () => {
this.logger.debug(
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
const master = new ClusterMaster(
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
analyzeEntryPoints, this.createTaskCompletedCallback);
return master.run();
return await master.run();
});
}
}

View File

@ -7,6 +7,7 @@
*/
import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util';
@ -2525,7 +2526,7 @@ function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
const rootFile = bundle.program.getSourceFile(bundle.path);
return bundle.program.getSourceFiles().filter(
f => (f !== rootFile) && isWithinPackage(bundle.package, f));
f => (f !== rootFile) && isWithinPackage(bundle.package, absoluteFromSourceFile(f)));
}
function isTopLevel(node: ts.Node): boolean {

View File

@ -37,7 +37,11 @@ export class AsyncLocker {
*/
async lock<T>(fn: () => Promise<T>): Promise<T> {
await this.create();
return fn().finally(() => this.lockFile.remove());
try {
return await fn();
} finally {
this.lockFile.remove();
}
}
protected async create() {

View File

@ -50,7 +50,7 @@ export function makeEntryPointBundle(
const rootDir = entryPoint.packagePath;
const options: ts
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.path);
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
const dtsHost = new NgtscCompilerHost(fs, options);
// Create the bundle programs, as necessary.
@ -63,7 +63,7 @@ export function makeEntryPointBundle(
[];
const dts = transformDts ? makeBundleProgram(
fs, isCore, entryPoint.packagePath, typingsPath, 'r3_symbols.d.ts',
options, dtsHost, additionalDtsFiles) :
{...options, allowJs: false}, dtsHost, additionalDtsFiles) :
null;
const isFlatCore = isCore && src.r3SymbolsFile === null;

View File

@ -7,7 +7,8 @@
*/
import * as ts from 'typescript';
import {FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {isWithinPackage} from '../analysis/util';
import {isRelativePath} from '../utils';
/**
@ -20,7 +21,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
private cache = ts.createModuleResolutionCache(
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
constructor(fs: FileSystem, options: ts.CompilerOptions, protected entryPointPath: string) {
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
super(fs, options);
}
@ -36,13 +37,24 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
// file was in the same directory. This is undesirable, as we need to have the actual
// JavaScript being present in the program. This logic recognizes this scenario and rewrites
// the resolved .d.ts declaration file to its .js counterpart, if it exists.
if (resolvedModule !== undefined && resolvedModule.extension === ts.Extension.Dts &&
containingFile.endsWith('.js') && isRelativePath(moduleName)) {
if (resolvedModule?.extension === ts.Extension.Dts && containingFile.endsWith('.js') &&
isRelativePath(moduleName)) {
const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js');
if (this.fileExists(jsFile)) {
return {...resolvedModule, resolvedFileName: jsFile, extension: ts.Extension.Js};
}
}
// Prevent loading JavaScript source files outside of the package root, which would happen for
// packages that don't have .d.ts files. As ngcc should only operate on the .js files
// contained within the package, any files outside the package are simply discarded. This does
// result in a partial program with error diagnostics, however ngcc won't gather diagnostics
// for the program it creates so these diagnostics won't be reported.
if (resolvedModule?.extension === ts.Extension.Js &&
!isWithinPackage(this.packagePath, this.fs.resolve(resolvedModule.resolvedFileName))) {
return undefined;
}
return resolvedModule;
});
}

View File

@ -5,7 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {isWithinPackage} from '../../src/analysis/util';
@ -18,15 +17,13 @@ runInEachFileSystem(() => {
it('should return true if the source-file is contained in the package', () => {
const packagePath = _('/node_modules/test');
const file =
ts.createSourceFile(_('/node_modules/test/src/index.js'), '', ts.ScriptTarget.ES2015);
const file = _('/node_modules/test/src/index.js');
expect(isWithinPackage(packagePath, file)).toBe(true);
});
it('should return false if the source-file is not contained in the package', () => {
const packagePath = _('/node_modules/test');
const file =
ts.createSourceFile(_('/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
const file = _('/node_modules/other/src/index.js');
expect(isWithinPackage(packagePath, file)).toBe(false);
});
@ -34,13 +31,11 @@ runInEachFileSystem(() => {
const packagePath = _('/node_modules/test');
// An external file inside the package's `node_modules/`.
const file1 = ts.createSourceFile(
_('/node_modules/test/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
const file1 = _('/node_modules/test/node_modules/other/src/index.js');
expect(isWithinPackage(packagePath, file1)).toBe(false);
// An internal file starting with `node_modules`.
const file2 = ts.createSourceFile(
_('/node_modules/test/node_modules_optimizer.js'), '', ts.ScriptTarget.ES2015);
const file2 = _('/node_modules/test/node_modules_optimizer.js');
expect(isWithinPackage(packagePath, file2)).toBe(true);
});
});

View File

@ -34,7 +34,7 @@ runInEachFileSystem(() => {
beforeEach(() => {
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
.and.returnValue(Promise.resolve('CusterMaster#run()' as any));
.and.returnValue(Promise.resolve('ClusterMaster#run()' as any));
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
mockLogger = new MockLogger();
@ -63,7 +63,7 @@ runInEachFileSystem(() => {
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
.toBe('CusterMaster#run()' as any);
.toBe('ClusterMaster#run()' as any);
expect(masterRunSpy).toHaveBeenCalledWith();
@ -78,6 +78,22 @@ runInEachFileSystem(() => {
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should call LockFile.write() and LockFile.remove() if analyzeFn fails', async () => {
const analyzeEntryPointsSpy =
jasmine.createSpy('analyzeEntryPoints').and.throwError('analyze error');
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
let error = '';
try {
await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy);
} catch (e) {
error = e.message;
}
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
expect(createCompilerFnSpy).not.toHaveBeenCalled();
expect(error).toEqual('analyze error');
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should call LockFile.write() and LockFile.remove() if master runner fails', async () => {
const anyFn: () => any = () => undefined;
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));

View File

@ -68,7 +68,7 @@ export function makeTestBundleProgram(
const rootDir = fs.dirname(entryPointPath);
const options: ts.CompilerOptions =
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
const host = new NgccSourcesCompilerHost(fs, options, entryPointPath);
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
return makeBundleProgram(
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
}

View File

@ -401,6 +401,121 @@ runInEachFileSystem(() => {
expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
});
it('should not crash when scanning for ModuleWithProviders needs to evaluate code from an external package',
() => {
// Regression test for https://github.com/angular/angular/issues/37508
// During `ModuleWithProviders` analysis, return statements in methods are evaluated using
// the partial evaluator to identify whether they correspond with a `ModuleWithProviders`
// function. If an arbitrary method has a return statement that calls into an external
// module which doesn't have declaration files, ngcc would attempt to reflect on said
// module using the reflection host of the entry-point. This would crash in the case where
// e.g. the entry-point is UMD and the external module would be CommonJS, as the UMD
// reflection host would throw because it is unable to deal with CommonJS.
// Setup a non-TS package with CommonJS module format
loadTestFiles([
{
name: _(`/node_modules/identity/package.json`),
contents: `{"name": "identity", "main": "./index.js"}`,
},
{
name: _(`/node_modules/identity/index.js`),
contents: `
function identity(x) { return x; };
exports.identity = identity;
module.exports = identity;
`,
},
]);
// Setup an Angular entry-point with UMD module format that references an export of the
// CommonJS package.
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: '{"name": "test-package", "main": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package/index.js'),
contents: `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('identity')) :
typeof define === 'function' && define.amd ? define('test', ['exports', 'identity'], factory) :
(factory(global.test, global.identity));
}(this, (function (exports, identity) { 'use strict';
function Foo(x) {
// The below statement is analyzed for 'ModuleWithProviders', so is evaluated
// by ngcc. The reference into the non-TS CommonJS package used to crash ngcc.
return identity.identity(x);
}
exports.Foo = Foo;
})));
`
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents: 'export declare class Foo { static doSomething(x: any): any; }'
},
{name: _('/node_modules/test-package/index.metadata.json'), contents: 'DUMMY DATA'},
]);
expect(() => mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['main'],
}))
.not.toThrow();
});
it('should not be able to evaluate code in external packages when no .d.ts files are present',
() => {
loadTestFiles([
{
name: _(`/node_modules/external/package.json`),
contents: `{"name": "external", "main": "./index.js"}`,
},
{
name: _(`/node_modules/external/index.js`),
contents: `
export const selector = 'my-selector';
`,
},
]);
compileIntoApf('test-package', {
'/index.ts': `
import {NgModule, Component} from '@angular/core';
import {selector} from 'external';
@Component({
selector,
template: ''
})
export class FooComponent {
}
@NgModule({
declarations: [FooComponent],
})
export class FooModule {}
`,
});
try {
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015', 'esm5'],
});
fail('should have thrown');
} catch (e) {
expect(e.message).toContain(
'Failed to compile entry-point test-package (esm2015 as esm2015) due to compilation errors:');
expect(e.message).toContain('NG1010');
expect(e.message).toContain('selector must be a string');
}
});
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `

View File

@ -13,8 +13,12 @@ import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
runInEachFileSystem(() => {
describe('entry point bundle', () => {
let _: typeof absoluteFrom;
beforeEach(() => {
_ = absoluteFrom;
});
function setupMockFileSystem(): void {
const _ = absoluteFrom;
loadTestFiles([
{
name: _('/node_modules/test/package.json'),
@ -210,6 +214,103 @@ runInEachFileSystem(() => {
].map(p => absoluteFrom(p).toString())));
});
it('does not include .js files outside of the package when no .d.ts file is available', () => {
// Declare main "test" package with "entry" entry-point that imports all sorts of
// internal and external modules.
loadTestFiles([
{
name: _('/node_modules/test/entry/package.json'),
contents: `{"name": "test", "main": "./index.js", "typings": "./index.d.ts"}`,
},
{
name: _('/node_modules/test/entry/index.d.ts'),
contents: `
import 'external-js';
import 'external-ts';
import 'nested-js';
import './local';
import '../package';
`,
},
{
name: _('/node_modules/test/entry/index.js'),
contents: `
import 'external-js';
import 'external-ts';
import 'nested-js';
import './local';
import '../package';
`,
},
{name: _('/node_modules/test/entry/local.d.ts'), contents: `export {};`},
{name: _('/node_modules/test/entry/local.js'), contents: `export {};`},
{name: _('/node_modules/test/package.d.ts'), contents: `export {};`},
{name: _('/node_modules/test/package.js'), contents: `export {};`},
]);
// Declare "external-js" package outside of the "test" package without .d.ts files, should
// not be included in the program.
loadTestFiles([
{
name: _('/node_modules/external-js/package.json'),
contents: `{"name": "external-js", "main": "./index.js"}`,
},
{name: _('/node_modules/external-js/index.js'), contents: 'export {};'},
]);
// Same as "external-js" but located in a nested node_modules directory, which should also
// not be included in the program.
loadTestFiles([
{
name: _('/node_modules/test/node_modules/nested-js/package.json'),
contents: `{"name": "nested-js", "main": "./index.js"}`,
},
{name: _('/node_modules/test/node_modules/nested-js/index.js'), contents: 'export {}'},
]);
// Declare "external-ts" which does have .d.ts files, so the .d.ts should be
// loaded into the program.
loadTestFiles([
{
name: _('/node_modules/external-ts/package.json'),
contents: `{"name": "external-ts", "main": "./index.js", "typings": "./index.d.ts"}`,
},
{name: _('/node_modules/external-ts/index.d.ts'), contents: 'export {};'},
{name: _('/node_modules/external-ts/index.js'), contents: 'export {};'},
]);
const fs = getFileSystem();
const entryPoint: EntryPoint = {
name: 'test/entry',
path: absoluteFrom('/node_modules/test/entry'),
packageName: 'test',
packagePath: absoluteFrom('/node_modules/test'),
packageJson: {name: 'test/entry'},
typings: absoluteFrom('/node_modules/test/entry/index.d.ts'),
compiledByAngular: true,
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
.toEqual(jasmine.arrayWithExactContents([
_('/node_modules/test/entry/index.js'),
_('/node_modules/test/entry/local.js'),
_('/node_modules/test/package.js'),
_('/node_modules/external-ts/index.d.ts'),
]));
expect(esm5bundle.dts!.program.getSourceFiles().map(sf => _(sf.fileName)))
.toEqual(jasmine.arrayWithExactContents([
_('/node_modules/test/entry/index.d.ts'),
_('/node_modules/test/entry/local.d.ts'),
_('/node_modules/test/package.d.ts'),
_('/node_modules/external-ts/index.d.ts'),
]));
});
describe(
'including equivalently named, internally imported, src files in the typings program',
() => {

View File

@ -116,7 +116,13 @@ export class NgCompiler {
const moduleResolutionCache = ts.createModuleResolutionCache(
this.adapter.getCurrentDirectory(),
fileName => this.adapter.getCanonicalFileName(fileName));
// Note: this used to be an arrow-function closure. However, JS engines like v8 have some
// strange behaviors with retaining the lexical scope of the closure. Even if this function
// doesn't retain a reference to `this`, if other closures in the constructor here reference
// `this` internally then a closure created here would retain them. This can cause major
// memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its
// way into all kinds of places inside TS internal objects.
this.adapter.getCanonicalFileName.bind(this.adapter));
this.moduleResolver =
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
this.resourceManager = new AdapterResourceLoader(adapter, this.options);

View File

@ -42,20 +42,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
* Tracks an `IncrementalDriver` within the strategy itself.
*/
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
private previous: IncrementalDriver|null = null;
private next: IncrementalDriver|null = null;
private driver: IncrementalDriver|null = null;
private isSet: boolean = false;
getIncrementalDriver(): IncrementalDriver|null {
return this.next !== null ? this.next : this.previous;
return this.driver;
}
setIncrementalDriver(driver: IncrementalDriver): void {
this.next = driver;
this.driver = driver;
this.isSet = true;
}
toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
const strategy = new TrackedIncrementalBuildStrategy();
strategy.previous = this.next;
// Only reuse a driver that was explicitly set via `setIncrementalDriver`.
strategy.driver = this.isSet ? this.driver : null;
return strategy;
}
}

View File

@ -3661,6 +3661,39 @@ describe('i18n support in the template compiler', () => {
verify(input, output);
});
it('should produce proper messages when `select` or `plural` keywords have spaces after them',
() => {
const input = `
<div i18n>
{count, select , 1 {one} other {more than one}}
{count, plural , =1 {one} other {more than one}}
</div>
`;
const output = String.raw`
var $I18N_1$;
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
const $MSG_EXTERNAL_199763560911211963$$APP_SPEC_TS_2$ = goog.getMsg("{VAR_SELECT , select , 1 {one} other {more than one}}");
$I18N_1$ = $MSG_EXTERNAL_199763560911211963$$APP_SPEC_TS_2$;
}
else {
$I18N_1$ = $localize \`{VAR_SELECT , select , 1 {one} other {more than one}}\`;
}
$I18N_1$ = i0.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" });
var $I18N_3$;
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
const $MSG_EXTERNAL_3383986062053865025$$APP_SPEC_TS_4$ = goog.getMsg("{VAR_PLURAL , plural , =1 {one} other {more than one}}");
$I18N_3$ = $MSG_EXTERNAL_3383986062053865025$$APP_SPEC_TS_4$;
}
else {
$I18N_3$ = $localize \`{VAR_PLURAL , plural , =1 {one} other {more than one}}\`;
}
$I18N_3$ = i0.ɵɵi18nPostprocess($I18N_3$, { "VAR_PLURAL": "\uFFFD1\uFFFD" });
`;
verify(input, output);
});
});
describe('$localize legacy message ids', () => {

View File

@ -273,10 +273,20 @@ class HtmlAstToIvyAst implements html.Visitor {
const value = message.placeholders[key];
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
const config = this.bindingParser.interpolationConfig;
// ICU expression is a plain string, not wrapped into start
// and end tags, so we wrap it before passing to binding parser
const wrapped = `${config.start}${value}${config.end}`;
vars[key] = this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText;
// Currently when the `plural` or `select` keywords in an ICU contain trailing spaces (e.g.
// `{count, select , ...}`), these spaces are also included into the key names in ICU vars
// (e.g. "VAR_SELECT "). These trailing spaces are not desirable, since they will later be
// converted into `_` symbols while normalizing placeholder names, which might lead to
// mismatches at runtime (i.e. placeholder will not be replaced with the correct value).
const formattedKey = key.trim();
vars[formattedKey] =
this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText;
} else {
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
}

View File

@ -12,22 +12,26 @@ import {Inject, Injectable, InjectionToken, Optional} from './di';
/**
* An injection token that allows you to provide one or more initialization functions.
* These function are injected at application startup and executed during
* A [DI token](guide/glossary#di-token "DI token definition") that you can use to provide
* one or more initialization functions.
*
* The provided function are injected at application startup and executed during
* app initialization. If any of these functions returns a Promise, initialization
* does not complete until the Promise is resolved.
*
* You can, for example, create a factory function that loads language data
* or an external configuration, and provide that function to the `APP_INITIALIZER` token.
* That way, the function is executed during the application bootstrap process,
* The function is executed during the application bootstrap process,
* and the needed data is available on startup.
*
* @see `ApplicationInitStatus`
*
* @publicApi
*/
export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Application Initializer');
/**
* A class that reflects the state of running {@link APP_INITIALIZER}s.
* A class that reflects the state of running {@link APP_INITIALIZER} functions.
*
* @publicApi
*/

View File

@ -11,13 +11,14 @@ import {ComponentRef} from './linker/component_factory';
/**
* A DI Token representing a unique string id assigned to the application by Angular and used
* A [DI token](guide/glossary#di-token "DI token definition") representing a unique string ID, used
* primarily for prefixing application attributes and CSS styles when
* {@link ViewEncapsulation#Emulated ViewEncapsulation.Emulated} is being used.
*
* If you need to avoid randomly generated value to be used as an application id, you can provide
* a custom value via a DI provider <!-- TODO: provider --> configuring the root {@link Injector}
* using this token.
* BY default, the value is randomly generated and assigned to the application by Angular.
* To provide a custom ID value, use a DI provider <!-- TODO: provider --> to configure
* the root {@link Injector} that uses this token.
*
* @publicApi
*/
export const APP_ID = new InjectionToken<string>('AppId');
@ -27,7 +28,7 @@ export function _appIdRandomProviderFactory() {
}
/**
* Providers that will generate a random APP_ID_TOKEN.
* Providers that generate a random `APP_ID_TOKEN`.
* @publicApi
*/
export const APP_ID_RANDOM_PROVIDER = {
@ -41,22 +42,24 @@ function _randomChar(): string {
}
/**
* A function that will be executed when a platform is initialized.
* A function that is executed when a platform is initialized.
* @publicApi
*/
export const PLATFORM_INITIALIZER = new InjectionToken<Array<() => void>>('Platform Initializer');
/**
* A token that indicates an opaque platform id.
* A token that indicates an opaque platform ID.
* @publicApi
*/
export const PLATFORM_ID = new InjectionToken<Object>('Platform ID');
/**
* All callbacks provided via this token will be called for every component that is bootstrapped.
* Signature of the callback:
* A [DI token](guide/glossary#di-token "DI token definition") that provides a set of callbacks to
* be called for every component that is bootstrapped.
*
* `(componentRef: ComponentRef) => void`.
* Each callback must take a `ComponentRef` instance and return nothing.
*
* `(componentRef: ComponentRef) => void`
*
* @publicApi
*/
@ -64,7 +67,8 @@ export const APP_BOOTSTRAP_LISTENER =
new InjectionToken<Array<(compRef: ComponentRef<any>) => void>>('appBootstrapListener');
/**
* A token which indicates the root directory of the application
* A [DI token](guide/glossary#di-token "DI token definition") that indicates the root directory of
* the application
* @publicApi
*/
export const PACKAGE_ROOT_URL = new InjectionToken<string>('Application Packages Root URL');

View File

@ -9,10 +9,13 @@
import {injectChangeDetectorRef as render3InjectChangeDetectorRef} from '../render3/view_engine_compatibility';
/**
* Base class for Angular Views, provides change detection functionality.
* Base class that provides change detection functionality.
* A change-detection tree collects all views that are to be checked for changes.
* Use the methods to add and remove views from the tree, initiate change-detection,
* and explicitly mark views as _dirty_, meaning that they have changed and need to be rerendered.
* and explicitly mark views as _dirty_, meaning that they have changed and need to be re-rendered.
*
* @see [Using change detection hooks](guide/lifecycle-hooks#using-change-detection-hooks)
* @see [Defining custom change detection](guide/lifecycle-hooks#defining-custom-change-detection)
*
* @usageNotes
*

View File

@ -11,6 +11,8 @@
* The strategy that the default change detector uses to detect changes.
* When set, takes effect the next time change detection is triggered.
*
* @see {@link ChangeDetectorRef#usage-notes Change detection usage}
*
* @publicApi
*/
export enum ChangeDetectionStrategy {

View File

@ -70,8 +70,8 @@ export interface Injectable {
* - 'root' : The application-level injector in most apps.
* - 'platform' : A special singleton platform injector shared by all
* applications on the page.
* - 'any' : Provides a unique instance in every module (including lazy modules) that injects the
* token.
* - 'any' : Provides a unique instance in each lazy loaded module while all eagerly loaded
* modules share one instance.
*
*/
providedIn?: Type<any>|'root'|'platform'|'any'|null;

View File

@ -20,8 +20,6 @@ export interface InjectDecorator {
* Parameter decorator on a dependency parameter of a class constructor
* that specifies a custom provider of the dependency.
*
* Learn more in the ["Dependency Injection Guide"](guide/dependency-injection).
*
* @usageNotes
* The following example shows a class constructor that specifies a
* custom provider of a dependency using the parameter decorator.
@ -31,6 +29,9 @@ export interface InjectDecorator {
*
* <code-example path="core/di/ts/metadata_spec.ts" region="InjectWithoutDecorator">
* </code-example>
*
* @see ["Dependency Injection Guide"](guide/dependency-injection)
*
*/
(token: any): any;
new(token: any): Inject;
@ -71,8 +72,6 @@ export interface OptionalDecorator {
* Can be used together with other parameter decorators
* that modify how dependency injection operates.
*
* Learn more in the ["Dependency Injection Guide"](guide/dependency-injection).
*
* @usageNotes
*
* The following code allows the possibility of a null result:
@ -80,6 +79,7 @@ export interface OptionalDecorator {
* <code-example path="core/di/ts/metadata_spec.ts" region="Optional">
* </code-example>
*
* @see ["Dependency Injection Guide"](guide/dependency-injection).
*/
(): any;
new(): Optional;
@ -122,7 +122,6 @@ export interface SelfDecorator {
* <code-example path="core/di/ts/metadata_spec.ts" region="Self">
* </code-example>
*
*
* @see `SkipSelf`
* @see `Optional`
*
@ -148,7 +147,7 @@ export const Self: SelfDecorator = makeParamDecorator('Self');
/**
* Type of the SkipSelf decorator / constructor function.
* Type of the `SkipSelf` decorator / constructor function.
*
* @publicApi
*/
@ -167,9 +166,7 @@ export interface SkipSelfDecorator {
* <code-example path="core/di/ts/metadata_spec.ts" region="SkipSelf">
* </code-example>
*
* Learn more in the
* [Dependency Injection guide](guide/dependency-injection-in-action#skip).
*
* @see [Dependency Injection guide](guide/dependency-injection-in-action#skip).
* @see `Self`
* @see `Optional`
*
@ -179,14 +176,14 @@ export interface SkipSelfDecorator {
}
/**
* Type of the SkipSelf metadata.
* Type of the `SkipSelf` metadata.
*
* @publicApi
*/
export interface SkipSelf {}
/**
* SkipSelf decorator and metadata.
* `SkipSelf` decorator and metadata.
*
* @Annotation
* @publicApi
@ -194,7 +191,7 @@ export interface SkipSelf {}
export const SkipSelf: SkipSelfDecorator = makeParamDecorator('SkipSelf');
/**
* Type of the Host decorator / constructor function.
* Type of the `Host` decorator / constructor function.
*
* @publicApi
*/
@ -204,15 +201,15 @@ export interface HostDecorator {
* that tells the DI framework to resolve the view by checking injectors of child
* elements, and stop when reaching the host element of the current component.
*
* For an extended example, see
* ["Dependency Injection Guide"](guide/dependency-injection-in-action#optional).
*
* @usageNotes
*
* The following shows use with the `@Optional` decorator, and allows for a null result.
*
* <code-example path="core/di/ts/metadata_spec.ts" region="Host">
* </code-example>
*
* For an extended example, see ["Dependency Injection
* Guide"](guide/dependency-injection-in-action#optional).
*/
(): any;
new(): Host;
@ -252,11 +249,11 @@ export interface AttributeDecorator {
* <input type="text">
* ```
*
* The following example uses the decorator to inject the string literal `text`.
* The following example uses the decorator to inject the string literal `text` in a directive.
*
* {@example core/ts/metadata/metadata.ts region='attributeMetadata'}
*
* ### Example as TypeScript Decorator
* The following example uses the decorator in a component constructor.
*
* {@example core/ts/metadata/metadata.ts region='attributeFactory'}
*

View File

@ -13,27 +13,25 @@ import {ComponentFactoryResolver} from './component_factory_resolver';
/**
* Represents an instance of an NgModule created via a {@link NgModuleFactory}.
*
* `NgModuleRef` provides access to the NgModule Instance as well other objects related to this
* NgModule Instance.
* Represents an instance of an `NgModule` created by an `NgModuleFactory`.
* Provides access to the `NgModule` instance and related objects.
*
* @publicApi
*/
export abstract class NgModuleRef<T> {
/**
* The injector that contains all of the providers of the NgModule.
* The injector that contains all of the providers of the `NgModule`.
*/
abstract get injector(): Injector;
/**
* The ComponentFactoryResolver to get hold of the ComponentFactories
* The resolver that can retrieve the component factories
* declared in the `entryComponents` property of the module.
*/
abstract get componentFactoryResolver(): ComponentFactoryResolver;
/**
* The NgModule instance.
* The `NgModule` instance.
*/
abstract get instance(): T;
@ -43,7 +41,7 @@ export abstract class NgModuleRef<T> {
abstract destroy(): void;
/**
* Allows to register a callback that will be called when the module is destroyed.
* Registers a callback to be executed when the module is destroyed.
*/
abstract onDestroy(callback: () => void): void;
}

View File

@ -10,12 +10,9 @@ import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
/**
* Represents an Angular [view](guide/glossary#view),
* specifically the [host view](guide/glossary#view-tree) that is defined by a component.
* Also serves as the base class
* that adds destroy methods for [embedded views](guide/glossary#view-tree).
* Represents an Angular [view](guide/glossary#view "Definition").
*
* @see `EmbeddedViewRef`
* @see {@link ChangeDetectorRef#usage-notes Change detection usage}
*
* @publicApi
*/

View File

@ -282,10 +282,10 @@ export interface Directive {
host?: {[key: string]: string};
/**
* If true, this directive/component will be skipped by the AOT compiler and so will always be
* compiled using JIT.
*
* This exists to support future Ivy work and has no effect currently.
* When present, this directive/component is ignored by the AOT compiler.
* It remains in distributed code, and the JIT compiler attempts to compile it
* at run time, in the browser.
* To ensure the correct behavior, the app must import `@angular/compiler`.
*/
jit?: true;
}
@ -314,7 +314,8 @@ export interface ComponentDecorator {
* An Angular app contains a tree of Angular components.
*
* Angular components are a subset of directives, always associated with a template.
* Unlike other directives, only one component can be instantiated per an element in a template.
* Unlike other directives, only one component can be instantiated for a given element in a
* template.
*
* A component must belong to an NgModule in order for it to be available
* to another component or application. To make it a member of an NgModule,

View File

@ -80,12 +80,10 @@ export interface NgModuleDef<T> {
}
/**
* A wrapper around an NgModule that associates it with the providers.
* A wrapper around an NgModule that associates it with [providers](guide/glossary#provider
* "Definition"). Usage without a generic type is deprecated.
*
* @param T the module type.
*
* Note that using ModuleWithProviders without a generic type is deprecated.
* The generic will become required in a future version of Angular.
* @see [Deprecations](guide/deprecations#modulewithproviders-type-without-a-generic)
*
* @publicApi
*/
@ -296,10 +294,10 @@ export interface NgModule {
id?: string;
/**
* If true, this module will be skipped by the AOT compiler and so will always be compiled
* using JIT.
*
* This exists to support future Ivy work and has no effect currently.
* When present, this module is ignored by the AOT compiler.
* It remains in distributed code, and the JIT compiler attempts to compile it
* at run time, in the browser.
* To ensure the correct behavior, the app must import `@angular/compiler`.
*/
jit?: true;
}

View File

@ -99,8 +99,12 @@ let nextNgElementId = 0;
export function bloomAdd(
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
let id: number|undefined =
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
let id: number|undefined;
if (typeof type === 'string') {
id = type.charCodeAt(0) || 0;
} else if (type.hasOwnProperty(NG_ELEMENT_ID)) {
id = (type as any)[NG_ELEMENT_ID];
}
// Set a unique ID on the directive type, so if something tries to inject the directive,
// we can easily retrieve the ID and hash it into the bloom bit that should be checked.
@ -584,7 +588,9 @@ export function bloomHashBitOrFactory(token: Type<any>|InjectionToken<any>|strin
if (typeof token === 'string') {
return token.charCodeAt(0) || 0;
}
const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID];
const tokenId: number|undefined =
// First check with `hasOwnProperty` so we don't get an inherited ID.
token.hasOwnProperty(NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
// Negative token IDs are used for special objects such as `Injector`
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
}

View File

@ -9,7 +9,7 @@ import '../util/ng_i18n_closure_mode';
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
import {InertBodyHelper} from '../sanitization/inert_body';
import {getInertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {addAllToArray} from '../util/array_utils';
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
@ -1233,7 +1233,7 @@ function icuStart(
function parseIcuCase(
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
expandoStartIndex: number): IcuCase {
const inertBodyHelper = new InertBodyHelper(getDocument());
const inertBodyHelper = getInertBodyHelper(getDocument());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
if (!inertBodyElement) {
throw new Error('Unable to generate inert body element');

View File

@ -795,22 +795,6 @@ export function storeCleanupWithContext(
}
}
/**
* Saves the cleanup function itself in LView.cleanupInstances.
*
* This is necessary for functions that are wrapped with their contexts, like in renderer2
* listeners.
*
* On the first template pass, the index of the cleanup function is saved in TView.
*/
export function storeCleanupFn(tView: TView, lView: LView, cleanupFn: Function): void {
getLCleanup(lView).push(cleanupFn);
if (tView.firstCreatePass) {
getTViewCleanup(tView).push(lView[CLEANUP]!.length - 1, null);
}
}
/**
* Constructs a TNode object from the arguments.
*

View File

@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {assertDefined} from '../util/assert';
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared';
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupWithContext} from './instructions/shared';
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {isLContainer} from './interfaces/type_checks';
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
}
onDestroy(callback: Function) {
storeCleanupFn(this._lView[TVIEW], this._lView, callback);
storeCleanupWithContext(this._lView[TVIEW], this._lView, null, callback);
}
/**

View File

@ -7,7 +7,7 @@
*/
import {isDevMode} from '../util/is_dev_mode';
import {InertBodyHelper} from './inert_body';
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
function tagSet(tags: string): {[k: string]: boolean} {
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
let inertBodyElement: HTMLElement|null = null;
try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);

View File

@ -7,89 +7,29 @@
*/
/**
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
* This helper is used to get hold of an inert tree of DOM elements containing dirty HTML
* that needs sanitizing.
* Depending upon browser support we must use one of three strategies for doing this.
* Support: Safari 10.x -> XHR strategy
* Support: Firefox -> DomParser strategy
* Default: InertDocument strategy
* Depending upon browser support we use one of two strategies for doing this.
* Default: DOMParser strategy
* Fallback: InertDocument strategy
*/
export class InertBodyHelper {
private inertDocument: Document;
constructor(private defaultDoc: Document) {
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
let inertBodyElement = this.inertDocument.body;
if (inertBodyElement == null) {
// usually there should be only one body element in the document, but IE doesn't have any, so
// we need to create one.
const inertHtml = this.inertDocument.createElement('html');
this.inertDocument.appendChild(inertHtml);
inertBodyElement = this.inertDocument.createElement('body');
inertHtml.appendChild(inertBodyElement);
}
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
if (inertBodyElement.querySelector && !inertBodyElement.querySelector('svg')) {
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
// so use the XHR strategy.
this.getInertBodyElement = this.getInertBodyElement_XHR;
return;
}
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
if (inertBodyElement.querySelector && inertBodyElement.querySelector('svg img')) {
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
// so use the DOMParser strategy, if it is available.
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
// fall through to the default strategy below.
if (isDOMParserAvailable()) {
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
return;
}
}
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
}
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
}
export interface InertBodyHelper {
/**
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
* The implementation of this is determined in the constructor, when the class is instantiated.
*/
getInertBodyElement: (html: string) => HTMLElement | null;
}
/**
* Use XHR to create and fill an inert body element (on Safari 10.1)
* See
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
*/
private getInertBodyElement_XHR(html: string) {
// We add these extra elements to ensure that the rest of the content is parsed as expected
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
// `<head>` tag.
html = '<body><remove></remove>' + html + '</body>';
try {
html = encodeURI(html);
} catch {
return null;
}
const xhr = new XMLHttpRequest();
xhr.responseType = 'document';
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
xhr.send(undefined);
const body: HTMLBodyElement = xhr.response.body;
body.removeChild(body.firstChild!);
return body;
}
/**
* Use DOMParser to create and fill an inert body element (on Firefox)
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
*
*/
private getInertBodyElement_DOMParser(html: string) {
/**
* Uses DOMParser to create and fill an inert body element.
* This is the default strategy used in browsers that support it.
*/
class DOMParserHelper implements InertBodyHelper {
getInertBodyElement(html: string): HTMLElement|null {
// We add these extra elements to ensure that the rest of the content is parsed as expected
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
// `<head>` tag.
@ -103,14 +43,30 @@ export class InertBodyHelper {
return null;
}
}
}
/**
* Use an HTML5 `template` element, if supported, or an inert body element created via
* `createHtmlDocument` to create and fill an inert DOM element.
* This is the default sane strategy to use if the browser does not require one of the specialised
* strategies above.
*/
private getInertBodyElement_InertDocument(html: string) {
/**
* Use an HTML5 `template` element, if supported, or an inert body element created via
* `createHtmlDocument` to create and fill an inert DOM element.
* This is the fallback strategy if the browser does not support DOMParser.
*/
class InertDocumentHelper implements InertBodyHelper {
private inertDocument: Document;
constructor(private defaultDoc: Document) {
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
if (this.inertDocument.body == null) {
// usually there should be only one body element in the document, but IE doesn't have any, so
// we need to create one.
const inertHtml = this.inertDocument.createElement('html');
this.inertDocument.appendChild(inertHtml);
const inertBodyElement = this.inertDocument.createElement('body');
inertHtml.appendChild(inertBodyElement);
}
}
getInertBodyElement(html: string): HTMLElement|null {
// Prefer using <template> element if supported.
const templateEl = this.inertDocument.createElement('template');
if ('content' in templateEl) {
@ -164,15 +120,15 @@ export class InertBodyHelper {
}
/**
* We need to determine whether the DOMParser exists in the global context.
* The try-catch is because, on some browsers, trying to access this property
* on window can actually throw an error.
* We need to determine whether the DOMParser exists in the global context and
* supports parsing HTML; HTML parsing support is not as wide as other formats, see
* https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
*
* @suppress {uselessCode}
*/
function isDOMParserAvailable() {
export function isDOMParserAvailable() {
try {
return !!(window as any).DOMParser;
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
} catch {
return false;
}

View File

@ -7,9 +7,9 @@
*/
import {CommonModule} from '@angular/common';
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
import {ViewRef} from '@angular/core/src/render3/view_ref';
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
import {TestBed} from '@angular/core/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs';
@ -1627,7 +1627,8 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect((pipeInstance!.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
expect((pipeInstance!.cdr as ViewRefInternal<MyApp>).context)
.toBe(fixture.componentInstance);
});
it('should inject current component ChangeDetectorRef into directives on the same node as components',
@ -1643,7 +1644,7 @@ describe('di', () => {
fixture.detectChanges();
const app = fixture.componentInstance;
const comp = fixture.componentInstance.component;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(app.directive.value).toContain('ViewRef');
@ -1664,7 +1665,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef');
@ -1692,7 +1693,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const app = fixture.componentInstance;
expect((app!.cdr as ViewRef<MyApp>).context).toBe(app);
expect((app!.cdr as ViewRefInternal<MyApp>).context).toBe(app);
const comp = fixture.componentInstance.component;
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(app.directive.value).toContain('ViewRef');
@ -1720,7 +1721,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef');
@ -1743,7 +1744,7 @@ describe('di', () => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
expect(comp.directive.value).toContain('ViewRef');
@ -1773,7 +1774,8 @@ describe('di', () => {
TestBed.configureTestingModule({declarations: [MyApp, MyDirective]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect((dirInstance!.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
expect((dirInstance!.cdr as ViewRefInternal<MyApp>).context)
.toBe(fixture.componentInstance);
});
});
});
@ -2300,4 +2302,14 @@ describe('di', () => {
expect(fixture.componentInstance.dir.token).toBe('parent');
});
});
it('should not be able to inject ViewRef', () => {
@Component({template: ''})
class App {
constructor(_viewRef: ViewRef) {}
}
TestBed.configureTestingModule({declarations: [App]});
expect(() => TestBed.createComponent(App)).toThrowError(/NullInjectorError/);
});
});

View File

@ -627,6 +627,24 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(element.textContent).toContain('ICU start --> Autre <-- ICU end');
});
it('when `select` or `plural` keywords have spaces after them', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT , select , 10 {ten} 20 {twenty} other {other}}')]:
'{VAR_SELECT , select , 10 {dix} 20 {vingt} other {autre}}',
[computeMsgId('{VAR_PLURAL , plural , =0 {zero} =1 {one} other {other}}')]:
'{VAR_PLURAL , plural , =0 {zéro} =1 {une} other {autre}}'
});
const fixture = initWithTemplate(AppComp, `
<div i18n>
{count, select , 10 {ten} 20 {twenty} other {other}} -
{count, plural , =0 {zero} =1 {one} other {other}}
</div>
`);
const element = fixture.nativeElement;
expect(element.textContent).toContain('autre - zéro');
});
it('with no root node and text and DOM nodes surrounding ICU', () => {
loadTranslations({
[computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]:

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core';
import {ApplicationRef, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core';
import {InternalViewRef} from '@angular/core/src/linker/view_ref';
import {TestBed} from '@angular/core/testing';
@ -54,4 +54,22 @@ describe('ViewRef', () => {
fixture.detectChanges();
expect(document.body.querySelector('dynamic-cpt')).toBeFalsy();
});
it('should invoke the onDestroy callback of a view ref', () => {
let called = false;
@Component({template: ''})
class App {
constructor(changeDetectorRef: ChangeDetectorRef) {
(changeDetectorRef as InternalViewRef).onDestroy(() => called = true);
}
}
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();
expect(called).toBe(true);
});
});

View File

@ -1164,7 +1164,7 @@
"name": "shouldSearchParent"
},
{
"name": "storeCleanupFn"
"name": "storeCleanupWithContext"
},
{
"name": "stringify"

View File

@ -9,6 +9,7 @@
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
{
describe('HTML sanitizer', () => {
@ -229,18 +230,3 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
}
});
}
/**
* We need to determine whether the DOMParser exists in the global context.
* The try-catch is because, on some browsers, trying to access this property
* on window can actually throw an error.
*
* @suppress {uselessCode}
*/
function isDOMParserAvailable() {
try {
return !!(window as any).DOMParser;
} catch (e) {
return false;
}
}

View File

@ -27,6 +27,9 @@ let _fakeAsyncTestZoneSpec: any = null;
* @publicApi
*/
export function resetFakeAsyncZoneFallback() {
if (_fakeAsyncTestZoneSpec) {
_fakeAsyncTestZoneSpec.unlockDatePatch();
}
_fakeAsyncTestZoneSpec = null;
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
@ -73,6 +76,7 @@ export function fakeAsyncFallback(fn: Function): (...args: any[]) => any {
let res: any;
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
_fakeAsyncTestZoneSpec.lockDatePatch();
try {
res = fn.apply(this, args);
flushMicrotasksFallback();

View File

@ -7,8 +7,8 @@
*/
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
import {merge, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {merge, Observable, ReplaySubject} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
import {extractProjectableNodes} from './extract-projectable-nodes';
@ -43,9 +43,11 @@ export class ComponentNgElementStrategyFactory implements NgElementStrategyFacto
* @publicApi
*/
export class ComponentNgElementStrategy implements NgElementStrategy {
// Subject of `NgElementStrategyEvent` observables corresponding to the component's outputs.
private eventEmitters = new ReplaySubject<Observable<NgElementStrategyEvent>[]>(1);
/** Merged stream of the component's output events. */
// TODO(issue/24571): remove '!'.
events!: Observable<NgElementStrategyEvent>;
readonly events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters)));
/** Reference to the component that was created on connect. */
private componentRef: ComponentRef<any>|null = null;
@ -187,12 +189,13 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
protected initializeOutputs(componentRef: ComponentRef<any>): void {
const eventEmitters = this.componentFactory.outputs.map(({propName, templateName}) => {
const emitter: EventEmitter<any> = componentRef.instance[propName];
return emitter.pipe(map(value => ({name: templateName, value})));
});
const eventEmitters: Observable<NgElementStrategyEvent>[] =
this.componentFactory.outputs.map(({propName, templateName}) => {
const emitter: EventEmitter<any> = componentRef.instance[propName];
return emitter.pipe(map(value => ({name: templateName, value})));
});
this.events = merge(...eventEmitters);
this.eventEmitters.next(eventEmitters);
}
/** Calls ngOnChanges with all the inputs that have changed since the last call. */

View File

@ -187,13 +187,30 @@ export function createCustomElement<P>(
}
connectedCallback(): void {
// For historical reasons, some strategies may not have initialized the `events` property
// until after `connect()` is run. Subscribe to `events` if it is available before running
// `connect()` (in order to capture events emitted suring inittialization), otherwise
// subscribe afterwards.
//
// TODO: Consider deprecating/removing the post-connect subscription in a future major version
// (e.g. v11).
let subscribedToEvents = false;
if (this.ngElementStrategy.events) {
// `events` are already available: Subscribe to it asap.
this.subscribeToEvents();
subscribedToEvents = true;
}
this.ngElementStrategy.connect(this);
// Listen for events from the strategy and dispatch them as custom events
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value);
this.dispatchEvent(customEvent);
});
if (!subscribedToEvents) {
// `events` were not initialized before running `connect()`: Subscribe to them now.
// The events emitted during the component initialization have been missed, but at least
// future events will be captured.
this.subscribeToEvents();
}
}
disconnectedCallback(): void {
@ -207,6 +224,14 @@ export function createCustomElement<P>(
this.ngElementEventsSubscription = null;
}
}
private subscribeToEvents(): void {
// Listen for events from the strategy and dispatch them as custom events.
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value);
this.dispatchEvent(customEvent);
});
}
}
// TypeScript 3.9+ defines getters/setters as configurable but non-enumerable properties (in

View File

@ -41,6 +41,33 @@ describe('ComponentFactoryNgElementStrategy', () => {
expect(strategyFactory.create(injector)).toBeTruthy();
});
describe('before connected', () => {
it('should allow subscribing to output events', () => {
const events: NgElementStrategyEvent[] = [];
strategy.events.subscribe(e => events.push(e));
// No events before connecting (since `componentRef` is not even on the strategy yet).
componentRef.instance.output1.next('output-1a');
componentRef.instance.output1.next('output-1b');
componentRef.instance.output2.next('output-2a');
expect(events).toEqual([]);
// No events upon connecting (since events are not cached/played back).
strategy.connect(document.createElement('div'));
expect(events).toEqual([]);
// Events emitted once connected.
componentRef.instance.output1.next('output-1c');
componentRef.instance.output1.next('output-1d');
componentRef.instance.output2.next('output-2b');
expect(events).toEqual([
{name: 'templateOutput1', value: 'output-1c'},
{name: 'templateOutput1', value: 'output-1d'},
{name: 'templateOutput2', value: 'output-2b'},
]);
});
});
describe('after connected', () => {
beforeEach(() => {
// Set up an initial value to make sure it is passed to the component

View File

@ -40,12 +40,7 @@ if (browserDetection.supportsCustomElements) {
strategyFactory = new TestStrategyFactory();
strategy = strategyFactory.testStrategy;
const {selector, ElementCtor} = createTestCustomElement();
NgElementCtor = ElementCtor;
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
// new instances of the NgElement which extends HTMLElement, as long as we define it.
customElements.define(selector, NgElementCtor);
NgElementCtor = createAndRegisterTestCustomElement(strategyFactory);
})
.then(done, done.fail);
});
@ -117,6 +112,47 @@ if (browserDetection.supportsCustomElements) {
expect(eventValue).toEqual(null);
});
it('should listen to output events during initialization', () => {
const events: string[] = [];
const element = new NgElementCtor(injector);
element.addEventListener('strategy-event', evt => events.push((evt as CustomEvent).detail));
element.connectedCallback();
expect(events).toEqual(['connect']);
});
it('should not break if `NgElementStrategy#events` is not available before calling `NgElementStrategy#connect()`',
() => {
class TestStrategyWithLateEvents extends TestStrategy {
events: Subject<NgElementStrategyEvent> = undefined!;
connect(element: HTMLElement): void {
this.connectedElement = element;
this.events = new Subject<NgElementStrategyEvent>();
this.events.next({name: 'strategy-event', value: 'connect'});
}
}
const strategyWithLateEvents = new TestStrategyWithLateEvents();
const capturedEvents: string[] = [];
const NgElementCtorWithLateEventsStrategy =
createAndRegisterTestCustomElement({create: () => strategyWithLateEvents});
const element = new NgElementCtorWithLateEventsStrategy(injector);
element.addEventListener(
'strategy-event', evt => capturedEvents.push((evt as CustomEvent).detail));
element.connectedCallback();
// The "connect" event (emitted during initialization) was missed, but things didn't break.
expect(capturedEvents).toEqual([]);
// Subsequent events are still captured.
strategyWithLateEvents.events.next({name: 'strategy-event', value: 'after-connect'});
expect(capturedEvents).toEqual(['after-connect']);
});
it('should properly set getters/setters on the element', () => {
const element = new NgElementCtor(injector);
element.fooFoo = 'foo-foo-value';
@ -144,7 +180,7 @@ if (browserDetection.supportsCustomElements) {
it('should capture properties set before upgrading the element', () => {
// Create a regular element and set properties on it.
const {selector, ElementCtor} = createTestCustomElement();
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
const element = Object.assign(document.createElement(selector), {
fooFoo: 'foo-prop-value',
barBar: 'bar-prop-value',
@ -165,7 +201,7 @@ if (browserDetection.supportsCustomElements) {
it('should capture properties set after upgrading the element but before inserting it into the DOM',
() => {
// Create a regular element and set properties on it.
const {selector, ElementCtor} = createTestCustomElement();
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
const element = Object.assign(document.createElement(selector), {
fooFoo: 'foo-prop-value',
barBar: 'bar-prop-value',
@ -193,7 +229,7 @@ if (browserDetection.supportsCustomElements) {
it('should allow overwriting properties with attributes after upgrading the element but before inserting it into the DOM',
() => {
// Create a regular element and set properties on it.
const {selector, ElementCtor} = createTestCustomElement();
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
const element = Object.assign(document.createElement(selector), {
fooFoo: 'foo-prop-value',
barBar: 'bar-prop-value',
@ -219,7 +255,17 @@ if (browserDetection.supportsCustomElements) {
});
// Helpers
function createTestCustomElement() {
function createAndRegisterTestCustomElement(strategyFactory: NgElementStrategyFactory) {
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
// new instances of the NgElement which extends HTMLElement, as long as we define it.
customElements.define(selector, ElementCtor);
return ElementCtor;
}
function createTestCustomElement(strategyFactory: NgElementStrategyFactory) {
return {
selector: `test-element-${++selectorUid}`,
ElementCtor: createCustomElement<WithFooBar>(TestComponent, {injector, strategyFactory}),
@ -255,6 +301,7 @@ if (browserDetection.supportsCustomElements) {
events = new Subject<NgElementStrategyEvent>();
connect(element: HTMLElement): void {
this.events.next({name: 'strategy-event', value: 'connect'});
this.connectedElement = element;
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding} from '@angular/compiler';
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, Visitor} from '@angular/compiler';
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
import {ATTR, getBindingDescriptor} from './binding_utils';
@ -127,72 +127,18 @@ function getBoundedWordSpan(
export function getTemplateCompletions(
templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
let result: ng.CompletionEntry[] = [];
const {htmlAst, template} = templateInfo;
// The templateNode starts at the delimiter character so we add 1 to skip it.
// Calculate the position relative to the start of the template. This is needed
// because spans in HTML AST are relative. Inline template has non-zero start position.
const templatePosition = position - template.span.start;
const path = getPathToNodeAtPosition(htmlAst, templatePosition);
const mostSpecific = path.tail;
if (path.empty || !mostSpecific) {
result = elementCompletions(templateInfo);
} else {
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
mostSpecific.visit(
{
visitElement(ast) {
const startTagSpan = spanOf(ast.sourceSpan);
const tagLen = ast.name.length;
// + 1 for the opening angle bracket
if (templatePosition <= startTagSpan.start + tagLen + 1) {
// If we are in the tag then return the element completions.
result = elementCompletions(templateInfo);
} else if (templatePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
result = attributeCompletionsForElement(templateInfo, ast.name);
}
},
visitAttribute(ast: Attribute) {
// An attribute consists of two parts, LHS="RHS".
// Determine if completions are requested for LHS or RHS
if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
// RHS completion
result = attributeValueCompletions(templateInfo, path);
} else {
// LHS completion
result = attributeCompletions(templateInfo, path);
}
},
visitText(ast) {
result = interpolationCompletions(templateInfo, templatePosition);
if (result.length) return result;
const element = path.first(Element);
if (element) {
const definition = getHtmlTagDefinition(element.name);
if (definition.contentType === TagContentType.PARSABLE_DATA) {
result = voidElementAttributeCompletions(templateInfo, path);
if (!result.length) {
// If the element can hold content, show element completions.
result = elementCompletions(templateInfo);
}
}
} else {
// If no element container, implies parsable data so show elements.
result = voidElementAttributeCompletions(templateInfo, path);
if (!result.length) {
result = elementCompletions(templateInfo);
}
}
},
visitComment() {},
visitExpansion() {},
visitExpansionCase() {}
},
null);
}
const htmlPath: HtmlAstPath = getPathToNodeAtPosition(htmlAst, templatePosition);
const mostSpecific = htmlPath.tail;
const visitor = new HtmlVisitor(templateInfo, htmlPath);
const results: ng.CompletionEntry[] = mostSpecific ?
mostSpecific.visit(visitor, null /* context */) :
elementCompletions(templateInfo);
const replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
return result.map(entry => {
return results.map(entry => {
return {
...entry,
replacementSpan,
@ -200,6 +146,78 @@ export function getTemplateCompletions(
});
}
class HtmlVisitor implements Visitor {
/**
* Position relative to the start of the template.
*/
private readonly relativePosition: number;
constructor(private readonly templateInfo: ng.AstResult, private readonly htmlPath: HtmlAstPath) {
this.relativePosition = htmlPath.position;
}
// Note that every visitor method must explicitly specify return type because
// Visitor returns `any` for all methods.
visitElement(ast: Element): ng.CompletionEntry[] {
const startTagSpan = spanOf(ast.sourceSpan);
const tagLen = ast.name.length;
// + 1 for the opening angle bracket
if (this.relativePosition <= startTagSpan.start + tagLen + 1) {
// If we are in the tag then return the element completions.
return elementCompletions(this.templateInfo);
}
if (this.relativePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
return attributeCompletionsForElement(this.templateInfo, ast.name);
}
return [];
}
visitAttribute(ast: Attribute): ng.CompletionEntry[] {
// An attribute consists of two parts, LHS="RHS".
// Determine if completions are requested for LHS or RHS
if (ast.valueSpan && inSpan(this.relativePosition, spanOf(ast.valueSpan))) {
// RHS completion
return attributeValueCompletions(this.templateInfo, this.htmlPath);
}
// LHS completion
return attributeCompletions(this.templateInfo, this.htmlPath);
}
visitText(): ng.CompletionEntry[] {
const templatePath = findTemplateAstAt(this.templateInfo.templateAst, this.relativePosition);
if (templatePath.tail instanceof BoundTextAst) {
// If we know that this is an interpolation then do not try other scenarios.
const visitor = new ExpressionVisitor(
this.templateInfo, this.relativePosition,
() =>
getExpressionScope(diagnosticInfoFromTemplateInfo(this.templateInfo), templatePath));
templatePath.tail?.visit(visitor, null);
return visitor.results;
}
// TODO(kyliau): Not sure if this check is really needed since we don't have
// any test cases for it.
const element = this.htmlPath.first(Element);
if (element &&
getHtmlTagDefinition(element.name).contentType !== TagContentType.PARSABLE_DATA) {
return [];
}
// This is to account for cases like <h1> <a> text | </h1> where the
// closest element has no closing tag and thus is considered plain text.
const results = voidElementAttributeCompletions(this.templateInfo, this.htmlPath);
if (results.length) {
return results;
}
return elementCompletions(this.templateInfo);
}
visitComment(): ng.CompletionEntry[] {
return [];
}
visitExpansion(): ng.CompletionEntry[] {
return [];
}
visitExpansionCase(): ng.CompletionEntry[] {
return [];
}
}
function attributeCompletions(info: ng.AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
const attr = path.tail;
const elem = path.parentOf(attr);
@ -356,18 +374,6 @@ function elementCompletions(info: ng.AstResult): ng.CompletionEntry[] {
return results;
}
function interpolationCompletions(info: ng.AstResult, position: number): ng.CompletionEntry[] {
// Look for an interpolation in at the position.
const templatePath = findTemplateAstAt(info.templateAst, position);
if (!templatePath.tail) {
return [];
}
const visitor = new ExpressionVisitor(
info, position, () => getExpressionScope(diagnosticInfoFromTemplateInfo(info), templatePath));
templatePath.tail.visit(visitor, null);
return visitor.results;
}
// There is a special case of HTML where text that contains a unclosed tag is treated as
// text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
// element "Some <a text". We, however, want to treat this as if the user was requesting

View File

@ -841,6 +841,13 @@ describe('completions', () => {
'trim',
]);
});
it('should not return any results for unknown symbol', () => {
mockHost.override(TEST_TEMPLATE, '{{ doesnotexist.~{cursor} }}');
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
expect(completions).toBeUndefined();
});
});
function expectContain(

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
const {readFileSync} = require('fs');
const {bold, yellow} = require('chalk');
module.exports = (gulp) => () => {
const conventionalChangelog = require('gulp-conventional-changelog');
const ignoredScopes = [
@ -16,11 +19,55 @@ module.exports = (gulp) => () => {
];
return gulp.src('CHANGELOG.md')
.pipe(conventionalChangelog({preset: 'angular'}, {}, {
// Ignore commits that start with `<type>(<scope>)` for any of the ignored scopes.
extendedRegexp: true,
grep: `^[^(]+\\((${ignoredScopes.join('|')})\\)`,
invertGrep: true,
}))
.pipe(conventionalChangelog(
/* core options */ {preset: 'angular'},
/* context options */ {},
/* raw-commit options */ {
// Ignore commits that start with `<type>(<scope>)` for any of the ignored scopes.
extendedRegexp: true,
grep: `^[^(]+\\((${ignoredScopes.join('|')})\\)`,
invertGrep: true,
},
/* commit parser options */ null,
/* writer options*/ createDedupeWriterOptions()))
.pipe(gulp.dest('./'));
};
/**
* Creates changelog writer options which ensure that commits are not showing up multiple times.
* Commits can show up multiple times if a changelog has been generated on a publish branch
* and has been cherry-picked into "master". In that case, the changelog will already contain
* commits from master which might be added to the changelog again. This is because usually
* patch and minor releases are tagged from the publish branches and therefore
* conventional-changelog tries to build the changelog from last minor version to HEAD when a
* new minor version is being published from the "master" branch. We naively match commit
* headers as otherwise we would need to query Git and diff commits between a given patch branch.
* The commit header is reliable enough as it contains a direct reference to the source PR.
*/
function createDedupeWriterOptions() {
const existingChangelogContent = readFileSync('CHANGELOG.md', 'utf8');
return {
// Specify a writer option that can be used to modify the content of a new changelog section.
// See: conventional-changelog/tree/master/packages/conventional-changelog-writer
finalizeContext: (context) => {
context.commitGroups = context.commitGroups.filter((group) => {
group.commits = group.commits.filter((commit) => {
// NOTE: We cannot compare the SHAs because the commits will have a different SHA
// if they are being cherry-picked into a different branch.
if (existingChangelogContent.includes(commit.subject)) {
console.info(yellow(` ↺ Skipping duplicate: "${bold(commit.header)}"`));
return false;
}
return true;
});
// Filter out commit groups which don't have any commits. Commit groups will become
// empty if we filter out all duplicated commits.
return group.commits.length !== 0;
});
return context;
}
};
}