Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
f24972b1b1 | |||
d2886b3bb4 | |||
f296fea112 | |||
2605fc46e7 | |||
9d54b3a14b | |||
d09a6283ed | |||
1c168c3a44 | |||
0f74479c47 | |||
790bb949f6 | |||
2adcad6dd2 | |||
242ef1ace1 | |||
842b6a1247 | |||
98335529eb | |||
ca7ee794bf | |||
f9f2ba6faf | |||
aea1d211d4 | |||
57a518a36d | |||
29b83189b0 | |||
1d3df7885d | |||
fd06ffa2af | |||
36a1622dd1 | |||
7a91b23cb5 | |||
4b90b6a226 | |||
b13daa4cdf | |||
0c6f026828 | |||
a2520bd267 | |||
b928a209a4 | |||
89e16ed6a5 | |||
1a1f99af37 | |||
df2cd37ed2 | |||
12a71bc6bc | |||
7d270c235a | |||
b0b7248504 | |||
78460c1848 | |||
75b119eafc | |||
64b0ae93f7 | |||
7c0b25f5a6 |
@ -4,7 +4,7 @@ import {MergeConfig} from '../dev-infra/pr/merge/config';
|
|||||||
const commitMessage = {
|
const commitMessage = {
|
||||||
'maxLength': 120,
|
'maxLength': 120,
|
||||||
'minBodyLength': 100,
|
'minBodyLength': 100,
|
||||||
'minBodyLengthExcludes': ['docs'],
|
'minBodyLengthTypeExcludes': ['docs'],
|
||||||
'types': [
|
'types': [
|
||||||
'build',
|
'build',
|
||||||
'ci',
|
'ci',
|
||||||
|
34
CHANGELOG.md
34
CHANGELOG.md
@ -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>
|
<a name="10.0.1"></a>
|
||||||
## [10.0.1](https://github.com/angular/angular/compare/10.0.0...10.0.1) (2020-06-26)
|
## [10.0.1](https://github.com/angular/angular/compare/10.0.0...10.0.1) (2020-06-26)
|
||||||
|
|
||||||
|
@ -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,
|
That authorization can change during the course of a single application session,
|
||||||
as when you log in a different user.
|
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
|
`HeroService` won't have direct access to the user information to decide
|
||||||
who is authorized and who isn't.
|
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>
|
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" header="src/app/heroes/hero.service.ts (excerpt)"></code-example>
|
||||||
|
|
||||||
|
@ -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`):
|
The following example imports the global variants for French (`fr`):
|
||||||
|
|
||||||
<code-example language="javascript" header="app.module.ts">
|
<code-example language="javascript" header="app.module.ts">
|
||||||
import '@angular/common/locales/global/fr;
|
import '@angular/common/locales/global/fr';
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
{@a custom-id}
|
{@a custom-id}
|
||||||
|
@ -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).
|
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:
|
Support is added by making two changes:
|
||||||
|
|
||||||
1. Renaming the workspace-level `tsconfig.json` to `tsconfig.base.json`.
|
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.
|
All project [TypeScript configuration files](guide/typescript-configuration) will extend from this base which contains the common options used throughout the workspace.
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ Because observables produce values asynchronously, try/catch will not effectivel
|
|||||||
<code-example>
|
<code-example>
|
||||||
myObservable.subscribe({
|
myObservable.subscribe({
|
||||||
next(num) { console.log('Next num: ' + num)},
|
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>
|
</code-example>
|
||||||
|
|
||||||
|
@ -359,10 +359,11 @@ Then inject it inside a test by calling `TestBed.inject()` with the service clas
|
|||||||
|
|
||||||
<div class="alert is-helpful">
|
<div class="alert is-helpful">
|
||||||
|
|
||||||
**Note:** We used to have `TestBed.get()` instead of `TestBed.inject()`.
|
**Note:** `TestBed.get()` was deprecated as of Angular version 9.
|
||||||
The `get` method wasn't type safe, it always returned `any`, and this is error prone.
|
To help minimize breaking changes, Angular introduces a
|
||||||
We decided to migrate to a new function instead of updating the existing one given
|
new function called `TestBed.inject()`, which you should use instead.
|
||||||
the large scale use that would have an immense amount of breaking changes.
|
For information on the removal of `TestBed.get()`,
|
||||||
|
see its entry in the [Deprecations index](guide/deprecations#index).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -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).
|
* 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).
|
* 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).
|
* `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).
|
* 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).
|
* Navigation is canceled for routes with at least one empty resolver. See [PR 24621](https://github.com/angular/angular/pull/24621).
|
||||||
|
|
||||||
|
BIN
aio/content/images/bios/ahasall.jpg
Normal file
BIN
aio/content/images/bios/ahasall.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
aio/content/images/bios/sonukapoor.jpg
Normal file
BIN
aio/content/images/bios/sonukapoor.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
@ -596,6 +596,13 @@
|
|||||||
"twitter": "devjoost",
|
"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 ;)"
|
"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": {
|
"jschwarty": {
|
||||||
"name": "Justin Schwartzenberger",
|
"name": "Justin Schwartzenberger",
|
||||||
"picture": "justinschwartzenberger.jpg",
|
"picture": "justinschwartzenberger.jpg",
|
||||||
@ -815,5 +822,13 @@
|
|||||||
"website": "https://wellwind.idv.tw/blog/",
|
"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.",
|
"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"]
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -506,80 +506,6 @@
|
|||||||
"url": "guide/universal",
|
"url": "guide/universal",
|
||||||
"title": "Server-side Rendering",
|
"title": "Server-side Rendering",
|
||||||
"tooltip": "Render HTML server-side with Angular Universal."
|
"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."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -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.
|
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.
|
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—components, template syntax, routing, services, and accessing data via HTTP—in a condensed format, following the most current best practices.
|
The "Try it" tutorial covers the same major topics—components, template syntax, routing, services, and accessing data via HTTP—in a condensed format, following the most current best practices.
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@
|
|||||||
// The below paths are referenced in users projects generated by the CLI
|
// 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/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/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": [
|
"rewrites": [
|
||||||
{
|
{
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
"cross-spawn": "^5.1.0",
|
"cross-spawn": "^5.1.0",
|
||||||
"css-selector-parser": "^1.3.0",
|
"css-selector-parser": "^1.3.0",
|
||||||
"dgeni": "^0.4.11",
|
"dgeni": "^0.4.11",
|
||||||
"dgeni-packages": "^0.28.3",
|
"dgeni-packages": "^0.28.4",
|
||||||
"entities": "^1.1.1",
|
"entities": "^1.1.1",
|
||||||
"eslint": "^3.19.0",
|
"eslint": "^3.19.0",
|
||||||
"eslint-plugin-jasmine": "^2.2.0",
|
"eslint-plugin-jasmine": "^2.2.0",
|
||||||
@ -175,4 +175,4 @@
|
|||||||
"xregexp": "^4.0.0",
|
"xregexp": "^4.0.0",
|
||||||
"yargs": "^7.0.2"
|
"yargs": "^7.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,11 @@
|
|||||||
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
|
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
|
||||||
<mat-icon svgIcon="menu"></mat-icon>
|
<mat-icon svgIcon="menu"></mat-icon>
|
||||||
</button>
|
</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 *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">
|
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
|
||||||
</a>
|
</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>
|
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
|
||||||
<div class="toolbar-external-icons-container">
|
<div class="toolbar-external-icons-container">
|
||||||
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
|
<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-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()">
|
<mat-sidenav [ngClass]="{'collapsed': !dockSideNav}" #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 *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
|
||||||
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu>
|
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="dockSideNav"></aio-nav-menu>
|
||||||
|
|
||||||
<div class="doc-version">
|
<div class="doc-version">
|
||||||
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select>
|
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select>
|
||||||
|
@ -25,11 +25,9 @@ import { first, mapTo } from 'rxjs/operators';
|
|||||||
import { MockLocationService } from 'testing/location.service';
|
import { MockLocationService } from 'testing/location.service';
|
||||||
import { MockLogger } from 'testing/logger.service';
|
import { MockLogger } from 'testing/logger.service';
|
||||||
import { MockSearchService } from 'testing/search.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';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
const sideBySideBreakPoint = 992;
|
|
||||||
const hideToCBreakPoint = 800;
|
|
||||||
const startedDelay = 100;
|
const startedDelay = 100;
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
@ -58,7 +56,7 @@ describe('AppComponent', () => {
|
|||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.onResize(sideBySideBreakPoint + 1); // wide by default
|
component.onResize(showTopMenuWidth + 1); // wide by default
|
||||||
|
|
||||||
const de = fixture.debugElement;
|
const de = fixture.debugElement;
|
||||||
const docViewerDe = de.query(By.css('aio-doc-viewer'));
|
const docViewerDe = de.query(By.css('aio-doc-viewer'));
|
||||||
@ -99,7 +97,7 @@ describe('AppComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be false on narrow screens', () => {
|
it('should be false on narrow screens', () => {
|
||||||
component.onResize(hideToCBreakPoint - 1);
|
component.onResize(showFloatingTocWidth - 1);
|
||||||
|
|
||||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||||
expect(component.hasFloatingToc).toBe(false);
|
expect(component.hasFloatingToc).toBe(false);
|
||||||
@ -112,7 +110,7 @@ describe('AppComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be true on wide screens unless the toc is empty', () => {
|
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[]);
|
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||||
expect(component.hasFloatingToc).toBe(true);
|
expect(component.hasFloatingToc).toBe(true);
|
||||||
@ -127,37 +125,47 @@ describe('AppComponent', () => {
|
|||||||
it('should be false when toc is empty', () => {
|
it('should be false when toc is empty', () => {
|
||||||
tocService.tocList.next([]);
|
tocService.tocList.next([]);
|
||||||
|
|
||||||
component.onResize(hideToCBreakPoint + 1);
|
component.onResize(showFloatingTocWidth + 1);
|
||||||
expect(component.hasFloatingToc).toBe(false);
|
expect(component.hasFloatingToc).toBe(false);
|
||||||
|
|
||||||
component.onResize(hideToCBreakPoint - 1);
|
component.onResize(showFloatingTocWidth - 1);
|
||||||
expect(component.hasFloatingToc).toBe(false);
|
expect(component.hasFloatingToc).toBe(false);
|
||||||
|
|
||||||
component.onResize(hideToCBreakPoint + 1);
|
component.onResize(showFloatingTocWidth + 1);
|
||||||
expect(component.hasFloatingToc).toBe(false);
|
expect(component.hasFloatingToc).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be true when toc is not empty unless the screen is narrow', () => {
|
it('should be true when toc is not empty unless the screen is narrow', () => {
|
||||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||||
|
|
||||||
component.onResize(hideToCBreakPoint + 1);
|
component.onResize(showFloatingTocWidth + 1);
|
||||||
expect(component.hasFloatingToc).toBe(true);
|
expect(component.hasFloatingToc).toBe(true);
|
||||||
|
|
||||||
component.onResize(hideToCBreakPoint - 1);
|
component.onResize(showFloatingTocWidth - 1);
|
||||||
expect(component.hasFloatingToc).toBe(false);
|
expect(component.hasFloatingToc).toBe(false);
|
||||||
|
|
||||||
component.onResize(hideToCBreakPoint + 1);
|
component.onResize(showFloatingTocWidth + 1);
|
||||||
expect(component.hasFloatingToc).toBe(true);
|
expect(component.hasFloatingToc).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isSideBySide', () => {
|
describe('showTopMenu', () => {
|
||||||
it('should be updated on resize', () => {
|
it('should be updated on resize', () => {
|
||||||
component.onResize(sideBySideBreakPoint - 1);
|
component.onResize(showTopMenuWidth - 1);
|
||||||
expect(component.isSideBySide).toBe(false);
|
expect(component.showTopMenu).toBe(false);
|
||||||
|
|
||||||
component.onResize(sideBySideBreakPoint + 1);
|
component.onResize(showTopMenuWidth + 1);
|
||||||
expect(component.isSideBySide).toBe(true);
|
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();
|
fixture.detectChanges();
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('when side-by-side (wide)', () => {
|
describe('when view is wide', () => {
|
||||||
beforeEach(() => resizeTo(sideBySideBreakPoint + 1)); // side-by-side
|
beforeEach(() => resizeTo(dockSideNavWidth + 1)); // wide view
|
||||||
|
|
||||||
it('should open when navigating to a guide page (guide/pipes)', () => {
|
it('should open when navigating to a guide page (guide/pipes)', () => {
|
||||||
navigateTo('guide/pipes');
|
navigateTo('guide/pipes');
|
||||||
@ -232,8 +240,8 @@ describe('AppComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when NOT side-by-side (narrow)', () => {
|
describe('when view is narrow', () => {
|
||||||
beforeEach(() => resizeTo(sideBySideBreakPoint - 1)); // NOT side-by-side
|
beforeEach(() => resizeTo(dockSideNavWidth - 1)); // narrow view
|
||||||
|
|
||||||
it('should be closed when navigating to a guide page (guide/pipes)', () => {
|
it('should be closed when navigating to a guide page (guide/pipes)', () => {
|
||||||
navigateTo('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 sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
|
||||||
const nonSidenavDocs = ['features', 'about'];
|
const nonSidenavDocs = ['features', 'about'];
|
||||||
|
|
||||||
sidenavDocs.forEach(doc => {
|
sidenavDocs.forEach(doc => {
|
||||||
it(`should open when on a sidenav doc (${doc})`, () => {
|
it(`should open when on a sidenav doc (${doc})`, () => {
|
||||||
resizeTo(sideBySideBreakPoint - 1);
|
resizeTo(dockSideNavWidth - 1);
|
||||||
|
|
||||||
navigateTo(doc);
|
navigateTo(doc);
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
|
|
||||||
resizeTo(sideBySideBreakPoint + 1);
|
resizeTo(dockSideNavWidth + 1);
|
||||||
expect(sidenav.opened).toBe(true);
|
expect(sidenav.opened).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
nonSidenavDocs.forEach(doc => {
|
nonSidenavDocs.forEach(doc => {
|
||||||
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
|
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
|
||||||
resizeTo(sideBySideBreakPoint - 1);
|
resizeTo(dockSideNavWidth - 1);
|
||||||
|
|
||||||
navigateTo(doc);
|
navigateTo(doc);
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
|
|
||||||
resizeTo(sideBySideBreakPoint + 1);
|
resizeTo(dockSideNavWidth + 1);
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -317,33 +325,33 @@ describe('AppComponent', () => {
|
|||||||
describe('when manually opened', () => {
|
describe('when manually opened', () => {
|
||||||
sidenavDocs.forEach(doc => {
|
sidenavDocs.forEach(doc => {
|
||||||
it(`should remain opened when on a sidenav doc (${doc})`, () => {
|
it(`should remain opened when on a sidenav doc (${doc})`, () => {
|
||||||
resizeTo(sideBySideBreakPoint - 1);
|
resizeTo(dockSideNavWidth - 1);
|
||||||
|
|
||||||
navigateTo(doc);
|
navigateTo(doc);
|
||||||
toggleSidenav();
|
toggleSidenav();
|
||||||
expect(sidenav.opened).toBe(true);
|
expect(sidenav.opened).toBe(true);
|
||||||
|
|
||||||
resizeTo(sideBySideBreakPoint + 1);
|
resizeTo(dockSideNavWidth + 1);
|
||||||
expect(sidenav.opened).toBe(true);
|
expect(sidenav.opened).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
nonSidenavDocs.forEach(doc => {
|
nonSidenavDocs.forEach(doc => {
|
||||||
it(`should close when on a non-sidenav doc (${doc})`, () => {
|
it(`should close when on a non-sidenav doc (${doc})`, () => {
|
||||||
resizeTo(sideBySideBreakPoint - 1);
|
resizeTo(dockSideNavWidth - 1);
|
||||||
|
|
||||||
navigateTo(doc);
|
navigateTo(doc);
|
||||||
toggleSidenav();
|
toggleSidenav();
|
||||||
expect(sidenav.opened).toBe(true);
|
expect(sidenav.opened).toBe(true);
|
||||||
|
|
||||||
resizeTo(sideBySideBreakPoint + 1);
|
resizeTo(showTopMenuWidth + 1);
|
||||||
expect(sidenav.opened).toBe(false);
|
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 sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
|
||||||
const nonSidenavDocs = ['features', 'about'];
|
const nonSidenavDocs = ['features', 'about'];
|
||||||
|
|
||||||
@ -352,7 +360,7 @@ describe('AppComponent', () => {
|
|||||||
navigateTo(doc);
|
navigateTo(doc);
|
||||||
expect(sidenav.opened).toBe(true);
|
expect(sidenav.opened).toBe(true);
|
||||||
|
|
||||||
resizeTo(sideBySideBreakPoint - 1);
|
resizeTo(dockSideNavWidth - 1);
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -362,7 +370,7 @@ describe('AppComponent', () => {
|
|||||||
navigateTo(doc);
|
navigateTo(doc);
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
|
|
||||||
resizeTo(sideBySideBreakPoint - 1);
|
resizeTo(dockSideNavWidth - 1);
|
||||||
expect(sidenav.opened).toBe(false);
|
expect(sidenav.opened).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -376,7 +384,7 @@ describe('AppComponent', () => {
|
|||||||
async function setupSelectorForTesting(mode?: string) {
|
async function setupSelectorForTesting(mode?: string) {
|
||||||
createTestingModule('a/b', mode);
|
createTestingModule('a/b', mode);
|
||||||
await initializeTest();
|
await initializeTest();
|
||||||
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
component.onResize(dockSideNavWidth + 1); // wide view
|
||||||
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
|
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
|
||||||
selectComponent = selectElement.componentInstance;
|
selectComponent = selectElement.componentInstance;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,9 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
|||||||
import { first, map } from 'rxjs/operators';
|
import { first, map } from 'rxjs/operators';
|
||||||
|
|
||||||
const sideNavView = 'SideNav';
|
const sideNavView = 'SideNav';
|
||||||
|
export const showTopMenuWidth = 1048;
|
||||||
|
export const dockSideNavWidth = 992;
|
||||||
|
export const showFloatingTocWidth = 800;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-shell',
|
selector: 'aio-shell',
|
||||||
@ -57,18 +60,17 @@ export class AppComponent implements OnInit {
|
|||||||
isStarting = true;
|
isStarting = true;
|
||||||
isTransitioning = true;
|
isTransitioning = true;
|
||||||
isFetching = false;
|
isFetching = false;
|
||||||
isSideBySide = false;
|
showTopMenu = false;
|
||||||
|
dockSideNav = false;
|
||||||
private isFetchingTimeout: any;
|
private isFetchingTimeout: any;
|
||||||
private isSideNavDoc = false;
|
private isSideNavDoc = false;
|
||||||
|
|
||||||
private sideBySideWidth = 992;
|
|
||||||
sideNavNodes: NavigationNode[];
|
sideNavNodes: NavigationNode[];
|
||||||
topMenuNodes: NavigationNode[];
|
topMenuNodes: NavigationNode[];
|
||||||
topMenuNarrowNodes: NavigationNode[];
|
topMenuNarrowNodes: NavigationNode[];
|
||||||
|
|
||||||
hasFloatingToc = false;
|
hasFloatingToc = false;
|
||||||
private showFloatingToc = new BehaviorSubject(false);
|
private showFloatingToc = new BehaviorSubject(false);
|
||||||
private showFloatingTocWidth = 800;
|
|
||||||
tocMaxHeight: string;
|
tocMaxHeight: string;
|
||||||
private tocMaxHeightOffset = 0;
|
private tocMaxHeightOffset = 0;
|
||||||
|
|
||||||
@ -76,8 +78,8 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
private currentUrl: string;
|
private currentUrl: string;
|
||||||
|
|
||||||
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
|
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
|
||||||
get mode() { return this.isSideBySide ? 'side' : 'over'; }
|
get mode() { return this.dockSideNav && (this.isSideNavDoc || this.showTopMenu) ? 'side' : 'over'; }
|
||||||
|
|
||||||
// Search related properties
|
// Search related properties
|
||||||
showSearchResults = false;
|
showSearchResults = false;
|
||||||
@ -239,13 +241,14 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
@HostListener('window:resize', ['$event.target.innerWidth'])
|
@HostListener('window:resize', ['$event.target.innerWidth'])
|
||||||
onResize(width: number) {
|
onResize(width: number) {
|
||||||
this.isSideBySide = width >= this.sideBySideWidth;
|
this.showTopMenu = width >= showTopMenuWidth;
|
||||||
this.showFloatingToc.next(width > this.showFloatingTocWidth);
|
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
|
// 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.
|
// 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.)
|
// from `false` to `true` while on a non-sidenav doc.)
|
||||||
this.sidenav.toggle(false);
|
this.sidenav.toggle(false);
|
||||||
}
|
}
|
||||||
@ -338,7 +341,7 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// May be open or closed when wide; always closed when narrow.
|
// 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
|
// Dynamically change height of table of contents container
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// VARIABLES
|
// VARIABLES
|
||||||
|
$showTopMenuWidth: 1048px;
|
||||||
|
$hideTopMenuWidth: $showTopMenuWidth - 1;
|
||||||
$hamburgerShownMargin: 0 8px 0 0;
|
$hamburgerShownMargin: 0 8px 0 0;
|
||||||
$hamburgerHiddenMargin: 0 16px 0 -64px;
|
$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-guide mat-toolbar.mat-toolbar,
|
||||||
aio-shell.folder-start mat-toolbar.mat-toolbar,
|
aio-shell.folder-start mat-toolbar.mat-toolbar,
|
||||||
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||||
@media (min-width: 992px) {
|
@media (min-width: $showTopMenuWidth) {
|
||||||
.hamburger.mat-button {
|
.hamburger.mat-button {
|
||||||
// Hamburger shown on non-marketing pages on large screens.
|
// Hamburger shown on non-marketing pages even on large screens.
|
||||||
margin: $hamburgerShownMargin;
|
margin: $hamburgerShownMargin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +75,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
|||||||
margin: $hamburgerShownMargin;
|
margin: $hamburgerShownMargin;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: $showTopMenuWidth) {
|
||||||
// Hamburger hidden by default on large screens.
|
// Hamburger hidden by default on large screens.
|
||||||
// (Will be shown per doc.)
|
// (Will be shown per doc.)
|
||||||
margin: $hamburgerHiddenMargin;
|
margin: $hamburgerHiddenMargin;
|
||||||
@ -111,7 +113,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
|||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 991px) {
|
@media screen and (max-width: $hideTopMenuWidth) {
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +127,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
|||||||
top: 12px;
|
top: 12px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
@media (max-width: $hideTopMenuWidth) {
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
@ -237,6 +239,7 @@ aio-search-box.search-container {
|
|||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const sh = require('shelljs');
|
const sh = require('shelljs');
|
||||||
|
|
||||||
const PATCH_LOCK = 'node_modules/@angular/cli/.patched';
|
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);
|
sh.touch(PATCH_LOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4467,10 +4467,10 @@ dezalgo@^1.0.0:
|
|||||||
asap "^2.0.0"
|
asap "^2.0.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
dgeni-packages@^0.28.3:
|
dgeni-packages@^0.28.4:
|
||||||
version "0.28.3"
|
version "0.28.4"
|
||||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.3.tgz#2e1e55f341c389b67ebb28933ce1e7e9ad05c49b"
|
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.4.tgz#53a3e6700b8d8f6be168cadcc9fdb36e1d7011d3"
|
||||||
integrity sha512-WyVzY3Q4ylfnc2677le5G7a7WqkF88rBSjU9IrAofqro71yzZeWLoEdr/gJY+lJZ0PrDyuRW05pFvIbvX8N0PQ==
|
integrity sha512-7AUG3pKpWtn69c3v2Mzgh+i5gd+L0AxFfYGWGzBdlJqMlQfaQPQjaS54iYCvnOlK9rXBn9j39yO6EU70gDZuFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
canonical-path "^1.0.0"
|
canonical-path "^1.0.0"
|
||||||
catharsis "^0.8.1"
|
catharsis "^0.8.1"
|
||||||
|
@ -135,7 +135,13 @@ export class GithubApiMergeStrategy extends MergeStrategy {
|
|||||||
|
|
||||||
// Cherry pick the merged commits into the remaining target branches.
|
// Cherry pick the merged commits into the remaining target branches.
|
||||||
const failedBranches = await this.cherryPickIntoTargetBranches(
|
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,
|
// 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
|
// but in case the cherry-pick somehow fails, we still handle the conflicts here. The
|
||||||
|
@ -69,7 +69,8 @@ export abstract class MergeStrategy {
|
|||||||
* @returns A list of branches for which the revisions could not be cherry-picked into.
|
* @returns A list of branches for which the revisions could not be cherry-picked into.
|
||||||
*/
|
*/
|
||||||
protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: {
|
protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: {
|
||||||
dryRun?: boolean
|
dryRun?: boolean,
|
||||||
|
linkToOriginalCommits?: boolean,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const cherryPickArgs = [revisionRange];
|
const cherryPickArgs = [revisionRange];
|
||||||
const failedBranches: string[] = [];
|
const failedBranches: string[] = [];
|
||||||
@ -82,6 +83,14 @@ export abstract class MergeStrategy {
|
|||||||
cherryPickArgs.push('--no-commit');
|
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.
|
// Cherry-pick the refspec into all determined target branches.
|
||||||
for (const branchName of targetBranches) {
|
for (const branchName of targetBranches) {
|
||||||
const localTargetBranch = this.getLocalTargetBranchName(branchName);
|
const localTargetBranch = this.getLocalTargetBranchName(branchName);
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2987,
|
"runtime-es2015": 2987,
|
||||||
"main-es2015": 451406,
|
"main-es2015": 450883,
|
||||||
"polyfills-es2015": 52630
|
"polyfills-es2015": 52630
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,8 +21,8 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3097,
|
"runtime-es2015": 3097,
|
||||||
"main-es2015": 428886,
|
"main-es2015": 428031,
|
||||||
"polyfills-es2015": 52195
|
"polyfills-es2015": 52261
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,8 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 136302,
|
"main-es2015": 135533,
|
||||||
"polyfills-es2015": 37246
|
"polyfills-es2015": 37248
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2289,
|
"runtime-es2015": 2289,
|
||||||
"main-es2015": 246085,
|
"main-es2015": 245488,
|
||||||
"polyfills-es2015": 36938,
|
"polyfills-es2015": 36938,
|
||||||
"5-es2015": 751
|
"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): 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): (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": "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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "angular-srcs",
|
"name": "angular-srcs",
|
||||||
"version": "10.0.1",
|
"version": "10.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Angular - a web framework for modern web apps",
|
"description": "Angular - a web framework for modern web apps",
|
||||||
"homepage": "https://github.com/angular/angular",
|
"homepage": "https://github.com/angular/angular",
|
||||||
|
@ -12,7 +12,7 @@ import {ParsedConfiguration} from '../../..';
|
|||||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
||||||
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
||||||
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
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 {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 {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
|
||||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||||
@ -148,7 +148,8 @@ export class DecorationAnalyzer {
|
|||||||
*/
|
*/
|
||||||
analyzeProgram(): DecorationAnalyses {
|
analyzeProgram(): DecorationAnalyses {
|
||||||
for (const sourceFile of this.program.getSourceFiles()) {
|
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);
|
this.compiler.analyzeFile(sourceFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
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 {MetadataReader} from '../../../src/ngtsc/metadata';
|
||||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||||
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||||
@ -44,7 +44,7 @@ export class DefaultMigrationHost implements MigrationHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isInScope(clazz: ClassDeclaration): boolean {
|
isInScope(clazz: ClassDeclaration): boolean {
|
||||||
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
|
return isWithinPackage(this.entryPointPath, absoluteFromSourceFile(clazz.getSourceFile()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
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 {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
||||||
import {isWithinPackage} from './util';
|
import {isWithinPackage} from './util';
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export class SwitchMarkerAnalyzer {
|
|||||||
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
|
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
|
||||||
const analyzedFiles = new SwitchMarkerAnalyses();
|
const analyzedFiles = new SwitchMarkerAnalyses();
|
||||||
program.getSourceFiles()
|
program.getSourceFiles()
|
||||||
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
|
.filter(sourceFile => isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile)))
|
||||||
.forEach(sourceFile => {
|
.forEach(sourceFile => {
|
||||||
const declarations = this.host.getSwitchableDeclarations(sourceFile);
|
const declarations = this.host.getSwitchableDeclarations(sourceFile);
|
||||||
if (declarations.length) {
|
if (declarations.length) {
|
||||||
|
@ -5,13 +5,11 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
import {AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
|
||||||
|
|
||||||
import {absoluteFromSourceFile, AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
|
|
||||||
import {DependencyTracker} from '../../../src/ngtsc/incremental/api';
|
import {DependencyTracker} from '../../../src/ngtsc/incremental/api';
|
||||||
|
|
||||||
export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
|
export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteFsPath): boolean {
|
||||||
const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile));
|
const relativePath = relative(packagePath, filePath);
|
||||||
return !relativePath.startsWith('..') && !relativePath.startsWith('node_modules/');
|
return !relativePath.startsWith('..') && !relativePath.startsWith('node_modules/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,13 +28,13 @@ export class ClusterExecutor implements Executor {
|
|||||||
|
|
||||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
|
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
return this.lockFile.lock(() => {
|
return this.lockFile.lock(async () => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
|
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
|
||||||
const master = new ClusterMaster(
|
const master = new ClusterMaster(
|
||||||
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
|
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
|
||||||
analyzeEntryPoints, this.createTaskCompletedCallback);
|
analyzeEntryPoints, this.createTaskCompletedCallback);
|
||||||
return master.run();
|
return await master.run();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ts from 'typescript';
|
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 {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';
|
import {isWithinPackage} from '../analysis/util';
|
||||||
@ -2525,7 +2526,7 @@ function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
|
|||||||
function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
|
function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
|
||||||
const rootFile = bundle.program.getSourceFile(bundle.path);
|
const rootFile = bundle.program.getSourceFile(bundle.path);
|
||||||
return bundle.program.getSourceFiles().filter(
|
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 {
|
function isTopLevel(node: ts.Node): boolean {
|
||||||
|
@ -37,7 +37,11 @@ export class AsyncLocker {
|
|||||||
*/
|
*/
|
||||||
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
await this.create();
|
await this.create();
|
||||||
return fn().finally(() => this.lockFile.remove());
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
this.lockFile.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async create() {
|
protected async create() {
|
||||||
|
@ -50,7 +50,7 @@ export function makeEntryPointBundle(
|
|||||||
const rootDir = entryPoint.packagePath;
|
const rootDir = entryPoint.packagePath;
|
||||||
const options: ts
|
const options: ts
|
||||||
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
|
.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);
|
const dtsHost = new NgtscCompilerHost(fs, options);
|
||||||
|
|
||||||
// Create the bundle programs, as necessary.
|
// Create the bundle programs, as necessary.
|
||||||
@ -63,7 +63,7 @@ export function makeEntryPointBundle(
|
|||||||
[];
|
[];
|
||||||
const dts = transformDts ? makeBundleProgram(
|
const dts = transformDts ? makeBundleProgram(
|
||||||
fs, isCore, entryPoint.packagePath, typingsPath, 'r3_symbols.d.ts',
|
fs, isCore, entryPoint.packagePath, typingsPath, 'r3_symbols.d.ts',
|
||||||
options, dtsHost, additionalDtsFiles) :
|
{...options, allowJs: false}, dtsHost, additionalDtsFiles) :
|
||||||
null;
|
null;
|
||||||
const isFlatCore = isCore && src.r3SymbolsFile === null;
|
const isFlatCore = isCore && src.r3SymbolsFile === null;
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
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';
|
import {isRelativePath} from '../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,7 +21,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
|||||||
private cache = ts.createModuleResolutionCache(
|
private cache = ts.createModuleResolutionCache(
|
||||||
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
|
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);
|
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
|
// 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
|
// 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.
|
// the resolved .d.ts declaration file to its .js counterpart, if it exists.
|
||||||
if (resolvedModule !== undefined && resolvedModule.extension === ts.Extension.Dts &&
|
if (resolvedModule?.extension === ts.Extension.Dts && containingFile.endsWith('.js') &&
|
||||||
containingFile.endsWith('.js') && isRelativePath(moduleName)) {
|
isRelativePath(moduleName)) {
|
||||||
const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js');
|
const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js');
|
||||||
if (this.fileExists(jsFile)) {
|
if (this.fileExists(jsFile)) {
|
||||||
return {...resolvedModule, resolvedFileName: jsFile, extension: ts.Extension.Js};
|
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;
|
return resolvedModule;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
|
||||||
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
import {isWithinPackage} from '../../src/analysis/util';
|
import {isWithinPackage} from '../../src/analysis/util';
|
||||||
@ -18,15 +17,13 @@ runInEachFileSystem(() => {
|
|||||||
|
|
||||||
it('should return true if the source-file is contained in the package', () => {
|
it('should return true if the source-file is contained in the package', () => {
|
||||||
const packagePath = _('/node_modules/test');
|
const packagePath = _('/node_modules/test');
|
||||||
const file =
|
const file = _('/node_modules/test/src/index.js');
|
||||||
ts.createSourceFile(_('/node_modules/test/src/index.js'), '', ts.ScriptTarget.ES2015);
|
|
||||||
expect(isWithinPackage(packagePath, file)).toBe(true);
|
expect(isWithinPackage(packagePath, file)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the source-file is not contained in the package', () => {
|
it('should return false if the source-file is not contained in the package', () => {
|
||||||
const packagePath = _('/node_modules/test');
|
const packagePath = _('/node_modules/test');
|
||||||
const file =
|
const file = _('/node_modules/other/src/index.js');
|
||||||
ts.createSourceFile(_('/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
|
|
||||||
expect(isWithinPackage(packagePath, file)).toBe(false);
|
expect(isWithinPackage(packagePath, file)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -34,13 +31,11 @@ runInEachFileSystem(() => {
|
|||||||
const packagePath = _('/node_modules/test');
|
const packagePath = _('/node_modules/test');
|
||||||
|
|
||||||
// An external file inside the package's `node_modules/`.
|
// An external file inside the package's `node_modules/`.
|
||||||
const file1 = ts.createSourceFile(
|
const file1 = _('/node_modules/test/node_modules/other/src/index.js');
|
||||||
_('/node_modules/test/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
|
|
||||||
expect(isWithinPackage(packagePath, file1)).toBe(false);
|
expect(isWithinPackage(packagePath, file1)).toBe(false);
|
||||||
|
|
||||||
// An internal file starting with `node_modules`.
|
// An internal file starting with `node_modules`.
|
||||||
const file2 = ts.createSourceFile(
|
const file2 = _('/node_modules/test/node_modules_optimizer.js');
|
||||||
_('/node_modules/test/node_modules_optimizer.js'), '', ts.ScriptTarget.ES2015);
|
|
||||||
expect(isWithinPackage(packagePath, file2)).toBe(true);
|
expect(isWithinPackage(packagePath, file2)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,7 @@ runInEachFileSystem(() => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
|
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');
|
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
|
||||||
|
|
||||||
mockLogger = new MockLogger();
|
mockLogger = new MockLogger();
|
||||||
@ -63,7 +63,7 @@ runInEachFileSystem(() => {
|
|||||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||||
|
|
||||||
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
|
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
|
||||||
.toBe('CusterMaster#run()' as any);
|
.toBe('ClusterMaster#run()' as any);
|
||||||
|
|
||||||
expect(masterRunSpy).toHaveBeenCalledWith();
|
expect(masterRunSpy).toHaveBeenCalledWith();
|
||||||
|
|
||||||
@ -78,6 +78,22 @@ runInEachFileSystem(() => {
|
|||||||
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
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 () => {
|
it('should call LockFile.write() and LockFile.remove() if master runner fails', async () => {
|
||||||
const anyFn: () => any = () => undefined;
|
const anyFn: () => any = () => undefined;
|
||||||
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
||||||
|
@ -68,7 +68,7 @@ export function makeTestBundleProgram(
|
|||||||
const rootDir = fs.dirname(entryPointPath);
|
const rootDir = fs.dirname(entryPointPath);
|
||||||
const options: ts.CompilerOptions =
|
const options: ts.CompilerOptions =
|
||||||
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
|
{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(
|
return makeBundleProgram(
|
||||||
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
||||||
}
|
}
|
||||||
|
@ -401,6 +401,121 @@ runInEachFileSystem(() => {
|
|||||||
expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
|
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', () => {
|
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
|
||||||
compileIntoFlatEs2015Package('test-package', {
|
compileIntoFlatEs2015Package('test-package', {
|
||||||
'/index.ts': `
|
'/index.ts': `
|
||||||
|
@ -13,8 +13,12 @@ import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
|||||||
|
|
||||||
runInEachFileSystem(() => {
|
runInEachFileSystem(() => {
|
||||||
describe('entry point bundle', () => {
|
describe('entry point bundle', () => {
|
||||||
|
let _: typeof absoluteFrom;
|
||||||
|
beforeEach(() => {
|
||||||
|
_ = absoluteFrom;
|
||||||
|
});
|
||||||
|
|
||||||
function setupMockFileSystem(): void {
|
function setupMockFileSystem(): void {
|
||||||
const _ = absoluteFrom;
|
|
||||||
loadTestFiles([
|
loadTestFiles([
|
||||||
{
|
{
|
||||||
name: _('/node_modules/test/package.json'),
|
name: _('/node_modules/test/package.json'),
|
||||||
@ -210,6 +214,103 @@ runInEachFileSystem(() => {
|
|||||||
].map(p => absoluteFrom(p).toString())));
|
].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(
|
describe(
|
||||||
'including equivalently named, internally imported, src files in the typings program',
|
'including equivalently named, internally imported, src files in the typings program',
|
||||||
() => {
|
() => {
|
||||||
|
@ -116,7 +116,13 @@ export class NgCompiler {
|
|||||||
|
|
||||||
const moduleResolutionCache = ts.createModuleResolutionCache(
|
const moduleResolutionCache = ts.createModuleResolutionCache(
|
||||||
this.adapter.getCurrentDirectory(),
|
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 =
|
this.moduleResolver =
|
||||||
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
|
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
|
||||||
this.resourceManager = new AdapterResourceLoader(adapter, this.options);
|
this.resourceManager = new AdapterResourceLoader(adapter, this.options);
|
||||||
|
@ -42,20 +42,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
|||||||
* Tracks an `IncrementalDriver` within the strategy itself.
|
* Tracks an `IncrementalDriver` within the strategy itself.
|
||||||
*/
|
*/
|
||||||
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
||||||
private previous: IncrementalDriver|null = null;
|
private driver: IncrementalDriver|null = null;
|
||||||
private next: IncrementalDriver|null = null;
|
private isSet: boolean = false;
|
||||||
|
|
||||||
getIncrementalDriver(): IncrementalDriver|null {
|
getIncrementalDriver(): IncrementalDriver|null {
|
||||||
return this.next !== null ? this.next : this.previous;
|
return this.driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIncrementalDriver(driver: IncrementalDriver): void {
|
setIncrementalDriver(driver: IncrementalDriver): void {
|
||||||
this.next = driver;
|
this.driver = driver;
|
||||||
|
this.isSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
|
toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
|
||||||
const strategy = new 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;
|
return strategy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3661,6 +3661,39 @@ describe('i18n support in the template compiler', () => {
|
|||||||
|
|
||||||
verify(input, output);
|
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', () => {
|
describe('$localize legacy message ids', () => {
|
||||||
|
@ -273,10 +273,20 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||||||
const value = message.placeholders[key];
|
const value = message.placeholders[key];
|
||||||
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
||||||
const config = this.bindingParser.interpolationConfig;
|
const config = this.bindingParser.interpolationConfig;
|
||||||
|
|
||||||
// ICU expression is a plain string, not wrapped into start
|
// ICU expression is a plain string, not wrapped into start
|
||||||
// and end tags, so we wrap it before passing to binding parser
|
// and end tags, so we wrap it before passing to binding parser
|
||||||
const wrapped = `${config.start}${value}${config.end}`;
|
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 {
|
} else {
|
||||||
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
||||||
}
|
}
|
||||||
|
@ -12,22 +12,26 @@ import {Inject, Injectable, InjectionToken, Optional} from './di';
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An injection token that allows you to provide one or more initialization functions.
|
* A [DI token](guide/glossary#di-token "DI token definition") that you can use to provide
|
||||||
* These function are injected at application startup and executed during
|
* 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
|
* app initialization. If any of these functions returns a Promise, initialization
|
||||||
* does not complete until the Promise is resolved.
|
* does not complete until the Promise is resolved.
|
||||||
*
|
*
|
||||||
* You can, for example, create a factory function that loads language data
|
* 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.
|
* 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.
|
* and the needed data is available on startup.
|
||||||
*
|
*
|
||||||
|
* @see `ApplicationInitStatus`
|
||||||
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Application Initializer');
|
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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
* primarily for prefixing application attributes and CSS styles when
|
||||||
* {@link ViewEncapsulation#Emulated ViewEncapsulation.Emulated} is being used.
|
* {@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
|
* BY default, the value is randomly generated and assigned to the application by Angular.
|
||||||
* a custom value via a DI provider <!-- TODO: provider --> configuring the root {@link Injector}
|
* To provide a custom ID value, use a DI provider <!-- TODO: provider --> to configure
|
||||||
* using this token.
|
* the root {@link Injector} that uses this token.
|
||||||
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export const APP_ID = new InjectionToken<string>('AppId');
|
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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export const APP_ID_RANDOM_PROVIDER = {
|
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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export const PLATFORM_INITIALIZER = new InjectionToken<Array<() => void>>('Platform Initializer');
|
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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export const PLATFORM_ID = new InjectionToken<Object>('Platform ID');
|
export const PLATFORM_ID = new InjectionToken<Object>('Platform ID');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All callbacks provided via this token will be called for every component that is bootstrapped.
|
* A [DI token](guide/glossary#di-token "DI token definition") that provides a set of callbacks to
|
||||||
* Signature of the callback:
|
* 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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
@ -64,7 +67,8 @@ export const APP_BOOTSTRAP_LISTENER =
|
|||||||
new InjectionToken<Array<(compRef: ComponentRef<any>) => void>>('appBootstrapListener');
|
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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export const PACKAGE_ROOT_URL = new InjectionToken<string>('Application Packages Root URL');
|
export const PACKAGE_ROOT_URL = new InjectionToken<string>('Application Packages Root URL');
|
||||||
|
@ -9,10 +9,13 @@
|
|||||||
import {injectChangeDetectorRef as render3InjectChangeDetectorRef} from '../render3/view_engine_compatibility';
|
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.
|
* 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,
|
* 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
|
* @usageNotes
|
||||||
*
|
*
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
* The strategy that the default change detector uses to detect changes.
|
* The strategy that the default change detector uses to detect changes.
|
||||||
* When set, takes effect the next time change detection is triggered.
|
* When set, takes effect the next time change detection is triggered.
|
||||||
*
|
*
|
||||||
|
* @see {@link ChangeDetectorRef#usage-notes Change detection usage}
|
||||||
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export enum ChangeDetectionStrategy {
|
export enum ChangeDetectionStrategy {
|
||||||
|
@ -70,8 +70,8 @@ export interface Injectable {
|
|||||||
* - 'root' : The application-level injector in most apps.
|
* - 'root' : The application-level injector in most apps.
|
||||||
* - 'platform' : A special singleton platform injector shared by all
|
* - 'platform' : A special singleton platform injector shared by all
|
||||||
* applications on the page.
|
* applications on the page.
|
||||||
* - 'any' : Provides a unique instance in every module (including lazy modules) that injects the
|
* - 'any' : Provides a unique instance in each lazy loaded module while all eagerly loaded
|
||||||
* token.
|
* modules share one instance.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
providedIn?: Type<any>|'root'|'platform'|'any'|null;
|
providedIn?: Type<any>|'root'|'platform'|'any'|null;
|
||||||
|
@ -20,8 +20,6 @@ export interface InjectDecorator {
|
|||||||
* Parameter decorator on a dependency parameter of a class constructor
|
* Parameter decorator on a dependency parameter of a class constructor
|
||||||
* that specifies a custom provider of the dependency.
|
* that specifies a custom provider of the dependency.
|
||||||
*
|
*
|
||||||
* Learn more in the ["Dependency Injection Guide"](guide/dependency-injection).
|
|
||||||
*
|
|
||||||
* @usageNotes
|
* @usageNotes
|
||||||
* The following example shows a class constructor that specifies a
|
* The following example shows a class constructor that specifies a
|
||||||
* custom provider of a dependency using the parameter decorator.
|
* 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 path="core/di/ts/metadata_spec.ts" region="InjectWithoutDecorator">
|
||||||
* </code-example>
|
* </code-example>
|
||||||
|
*
|
||||||
|
* @see ["Dependency Injection Guide"](guide/dependency-injection)
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
(token: any): any;
|
(token: any): any;
|
||||||
new(token: any): Inject;
|
new(token: any): Inject;
|
||||||
@ -71,8 +72,6 @@ export interface OptionalDecorator {
|
|||||||
* Can be used together with other parameter decorators
|
* Can be used together with other parameter decorators
|
||||||
* that modify how dependency injection operates.
|
* that modify how dependency injection operates.
|
||||||
*
|
*
|
||||||
* Learn more in the ["Dependency Injection Guide"](guide/dependency-injection).
|
|
||||||
*
|
|
||||||
* @usageNotes
|
* @usageNotes
|
||||||
*
|
*
|
||||||
* The following code allows the possibility of a null result:
|
* 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 path="core/di/ts/metadata_spec.ts" region="Optional">
|
||||||
* </code-example>
|
* </code-example>
|
||||||
*
|
*
|
||||||
|
* @see ["Dependency Injection Guide"](guide/dependency-injection).
|
||||||
*/
|
*/
|
||||||
(): any;
|
(): any;
|
||||||
new(): Optional;
|
new(): Optional;
|
||||||
@ -122,7 +122,6 @@ export interface SelfDecorator {
|
|||||||
* <code-example path="core/di/ts/metadata_spec.ts" region="Self">
|
* <code-example path="core/di/ts/metadata_spec.ts" region="Self">
|
||||||
* </code-example>
|
* </code-example>
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @see `SkipSelf`
|
* @see `SkipSelf`
|
||||||
* @see `Optional`
|
* @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
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
@ -167,9 +166,7 @@ export interface SkipSelfDecorator {
|
|||||||
* <code-example path="core/di/ts/metadata_spec.ts" region="SkipSelf">
|
* <code-example path="core/di/ts/metadata_spec.ts" region="SkipSelf">
|
||||||
* </code-example>
|
* </code-example>
|
||||||
*
|
*
|
||||||
* Learn more in the
|
* @see [Dependency Injection guide](guide/dependency-injection-in-action#skip).
|
||||||
* [Dependency Injection guide](guide/dependency-injection-in-action#skip).
|
|
||||||
*
|
|
||||||
* @see `Self`
|
* @see `Self`
|
||||||
* @see `Optional`
|
* @see `Optional`
|
||||||
*
|
*
|
||||||
@ -179,14 +176,14 @@ export interface SkipSelfDecorator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of the SkipSelf metadata.
|
* Type of the `SkipSelf` metadata.
|
||||||
*
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export interface SkipSelf {}
|
export interface SkipSelf {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SkipSelf decorator and metadata.
|
* `SkipSelf` decorator and metadata.
|
||||||
*
|
*
|
||||||
* @Annotation
|
* @Annotation
|
||||||
* @publicApi
|
* @publicApi
|
||||||
@ -194,7 +191,7 @@ export interface SkipSelf {}
|
|||||||
export const SkipSelf: SkipSelfDecorator = makeParamDecorator('SkipSelf');
|
export const SkipSelf: SkipSelfDecorator = makeParamDecorator('SkipSelf');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of the Host decorator / constructor function.
|
* Type of the `Host` decorator / constructor function.
|
||||||
*
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
@ -204,15 +201,15 @@ export interface HostDecorator {
|
|||||||
* that tells the DI framework to resolve the view by checking injectors of child
|
* 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.
|
* 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
|
* @usageNotes
|
||||||
*
|
*
|
||||||
* The following shows use with the `@Optional` decorator, and allows for a null result.
|
* 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 path="core/di/ts/metadata_spec.ts" region="Host">
|
||||||
* </code-example>
|
* </code-example>
|
||||||
|
*
|
||||||
|
* For an extended example, see ["Dependency Injection
|
||||||
|
* Guide"](guide/dependency-injection-in-action#optional).
|
||||||
*/
|
*/
|
||||||
(): any;
|
(): any;
|
||||||
new(): Host;
|
new(): Host;
|
||||||
@ -252,11 +249,11 @@ export interface AttributeDecorator {
|
|||||||
* <input type="text">
|
* <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 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'}
|
* {@example core/ts/metadata/metadata.ts region='attributeFactory'}
|
||||||
*
|
*
|
||||||
|
@ -13,27 +13,25 @@ import {ComponentFactoryResolver} from './component_factory_resolver';
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an instance of an NgModule created via a {@link NgModuleFactory}.
|
* Represents an instance of an `NgModule` created by an `NgModuleFactory`.
|
||||||
*
|
* Provides access to the `NgModule` instance and related objects.
|
||||||
* `NgModuleRef` provides access to the NgModule Instance as well other objects related to this
|
|
||||||
* NgModule Instance.
|
|
||||||
*
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export abstract class NgModuleRef<T> {
|
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;
|
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.
|
* declared in the `entryComponents` property of the module.
|
||||||
*/
|
*/
|
||||||
abstract get componentFactoryResolver(): ComponentFactoryResolver;
|
abstract get componentFactoryResolver(): ComponentFactoryResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The NgModule instance.
|
* The `NgModule` instance.
|
||||||
*/
|
*/
|
||||||
abstract get instance(): T;
|
abstract get instance(): T;
|
||||||
|
|
||||||
@ -43,7 +41,7 @@ export abstract class NgModuleRef<T> {
|
|||||||
abstract destroy(): void;
|
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;
|
abstract onDestroy(callback: () => void): void;
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,9 @@ import {ApplicationRef} from '../application_ref';
|
|||||||
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
|
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an Angular [view](guide/glossary#view),
|
* Represents an Angular [view](guide/glossary#view "Definition").
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
* @see `EmbeddedViewRef`
|
* @see {@link ChangeDetectorRef#usage-notes Change detection usage}
|
||||||
*
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
|
@ -282,10 +282,10 @@ export interface Directive {
|
|||||||
host?: {[key: string]: string};
|
host?: {[key: string]: string};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, this directive/component will be skipped by the AOT compiler and so will always be
|
* When present, this directive/component is ignored by the AOT compiler.
|
||||||
* compiled using JIT.
|
* It remains in distributed code, and the JIT compiler attempts to compile it
|
||||||
*
|
* at run time, in the browser.
|
||||||
* This exists to support future Ivy work and has no effect currently.
|
* To ensure the correct behavior, the app must import `@angular/compiler`.
|
||||||
*/
|
*/
|
||||||
jit?: true;
|
jit?: true;
|
||||||
}
|
}
|
||||||
@ -314,7 +314,8 @@ export interface ComponentDecorator {
|
|||||||
* An Angular app contains a tree of Angular components.
|
* An Angular app contains a tree of Angular components.
|
||||||
*
|
*
|
||||||
* Angular components are a subset of directives, always associated with a template.
|
* 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
|
* 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,
|
* to another component or application. To make it a member of an NgModule,
|
||||||
|
@ -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.
|
* @see [Deprecations](guide/deprecations#modulewithproviders-type-without-a-generic)
|
||||||
*
|
|
||||||
* Note that using ModuleWithProviders without a generic type is deprecated.
|
|
||||||
* The generic will become required in a future version of Angular.
|
|
||||||
*
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
@ -296,10 +294,10 @@ export interface NgModule {
|
|||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, this module will be skipped by the AOT compiler and so will always be compiled
|
* When present, this module is ignored by the AOT compiler.
|
||||||
* using JIT.
|
* It remains in distributed code, and the JIT compiler attempts to compile it
|
||||||
*
|
* at run time, in the browser.
|
||||||
* This exists to support future Ivy work and has no effect currently.
|
* To ensure the correct behavior, the app must import `@angular/compiler`.
|
||||||
*/
|
*/
|
||||||
jit?: true;
|
jit?: true;
|
||||||
}
|
}
|
||||||
|
@ -99,8 +99,12 @@ let nextNgElementId = 0;
|
|||||||
export function bloomAdd(
|
export function bloomAdd(
|
||||||
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
|
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
|
||||||
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
|
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
|
||||||
let id: number|undefined =
|
let id: number|undefined;
|
||||||
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
|
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,
|
// 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.
|
// 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') {
|
if (typeof token === 'string') {
|
||||||
return token.charCodeAt(0) || 0;
|
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`
|
// Negative token IDs are used for special objects such as `Injector`
|
||||||
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
|
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import '../util/ng_i18n_closure_mode';
|
|||||||
|
|
||||||
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
|
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
|
||||||
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
|
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 {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
||||||
import {addAllToArray} from '../util/array_utils';
|
import {addAllToArray} from '../util/array_utils';
|
||||||
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
|
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
|
||||||
@ -1233,7 +1233,7 @@ function icuStart(
|
|||||||
function parseIcuCase(
|
function parseIcuCase(
|
||||||
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
|
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
|
||||||
expandoStartIndex: number): IcuCase {
|
expandoStartIndex: number): IcuCase {
|
||||||
const inertBodyHelper = new InertBodyHelper(getDocument());
|
const inertBodyHelper = getInertBodyHelper(getDocument());
|
||||||
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||||
if (!inertBodyElement) {
|
if (!inertBodyElement) {
|
||||||
throw new Error('Unable to generate inert body element');
|
throw new Error('Unable to generate inert body element');
|
||||||
|
@ -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.
|
* Constructs a TNode object from the arguments.
|
||||||
*
|
*
|
||||||
|
@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
|
|||||||
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
|
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
|
||||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
|
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
|
||||||
import {assertDefined} from '../util/assert';
|
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 {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
||||||
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
||||||
import {isLContainer} from './interfaces/type_checks';
|
import {isLContainer} from './interfaces/type_checks';
|
||||||
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(callback: Function) {
|
onDestroy(callback: Function) {
|
||||||
storeCleanupFn(this._lView[TVIEW], this._lView, callback);
|
storeCleanupWithContext(this._lView[TVIEW], this._lView, null, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {isDevMode} from '../util/is_dev_mode';
|
import {isDevMode} from '../util/is_dev_mode';
|
||||||
import {InertBodyHelper} from './inert_body';
|
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
|
||||||
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
||||||
|
|
||||||
function tagSet(tags: string): {[k: string]: boolean} {
|
function tagSet(tags: string): {[k: string]: boolean} {
|
||||||
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
|
|||||||
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||||
let inertBodyElement: HTMLElement|null = null;
|
let inertBodyElement: HTMLElement|null = null;
|
||||||
try {
|
try {
|
||||||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
|
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
|
||||||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||||||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||||
|
@ -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.
|
* that needs sanitizing.
|
||||||
* Depending upon browser support we must use one of three strategies for doing this.
|
* Depending upon browser support we use one of two strategies for doing this.
|
||||||
* Support: Safari 10.x -> XHR strategy
|
* Default: DOMParser strategy
|
||||||
* Support: Firefox -> DomParser strategy
|
* Fallback: InertDocument strategy
|
||||||
* Default: InertDocument strategy
|
|
||||||
*/
|
*/
|
||||||
export class InertBodyHelper {
|
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
|
||||||
private inertDocument: Document;
|
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
|
||||||
|
}
|
||||||
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 interface InertBodyHelper {
|
||||||
/**
|
/**
|
||||||
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
* 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;
|
getInertBodyElement: (html: string) => HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use XHR to create and fill an inert body element (on Safari 10.1)
|
* Uses DOMParser to create and fill an inert body element.
|
||||||
* See
|
* This is the default strategy used in browsers that support it.
|
||||||
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
*/
|
||||||
*/
|
class DOMParserHelper implements InertBodyHelper {
|
||||||
private getInertBodyElement_XHR(html: string) {
|
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.
|
|
||||||
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) {
|
|
||||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
// 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
|
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||||
// `<head>` tag.
|
// `<head>` tag.
|
||||||
@ -103,14 +43,30 @@ export class InertBodyHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
* `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
|
* This is the fallback strategy if the browser does not support DOMParser.
|
||||||
* strategies above.
|
*/
|
||||||
*/
|
class InertDocumentHelper implements InertBodyHelper {
|
||||||
private getInertBodyElement_InertDocument(html: string) {
|
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.
|
// Prefer using <template> element if supported.
|
||||||
const templateEl = this.inertDocument.createElement('template');
|
const templateEl = this.inertDocument.createElement('template');
|
||||||
if ('content' in templateEl) {
|
if ('content' in templateEl) {
|
||||||
@ -164,15 +120,15 @@ export class InertBodyHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to determine whether the DOMParser exists in the global context.
|
* We need to determine whether the DOMParser exists in the global context and
|
||||||
* The try-catch is because, on some browsers, trying to access this property
|
* supports parsing HTML; HTML parsing support is not as wide as other formats, see
|
||||||
* on window can actually throw an error.
|
* https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
|
||||||
*
|
*
|
||||||
* @suppress {uselessCode}
|
* @suppress {uselessCode}
|
||||||
*/
|
*/
|
||||||
function isDOMParserAvailable() {
|
export function isDOMParserAvailable() {
|
||||||
try {
|
try {
|
||||||
return !!(window as any).DOMParser;
|
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {CommonModule} from '@angular/common';
|
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 {ɵ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 {TestBed} from '@angular/core/testing';
|
||||||
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
||||||
import {BehaviorSubject} from 'rxjs';
|
import {BehaviorSubject} from 'rxjs';
|
||||||
@ -1627,7 +1627,8 @@ describe('di', () => {
|
|||||||
TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]});
|
TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]});
|
||||||
const fixture = TestBed.createComponent(MyApp);
|
const fixture = TestBed.createComponent(MyApp);
|
||||||
fixture.detectChanges();
|
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',
|
it('should inject current component ChangeDetectorRef into directives on the same node as components',
|
||||||
@ -1643,7 +1644,7 @@ describe('di', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
const comp = fixture.componentInstance.component;
|
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
|
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||||
expect(app.directive.value).toContain('ViewRef');
|
expect(app.directive.value).toContain('ViewRef');
|
||||||
|
|
||||||
@ -1664,7 +1665,7 @@ describe('di', () => {
|
|||||||
const fixture = TestBed.createComponent(MyComp);
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const comp = fixture.componentInstance;
|
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
|
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||||
expect(comp.directive.value).toContain('ViewRef');
|
expect(comp.directive.value).toContain('ViewRef');
|
||||||
|
|
||||||
@ -1692,7 +1693,7 @@ describe('di', () => {
|
|||||||
const fixture = TestBed.createComponent(MyApp);
|
const fixture = TestBed.createComponent(MyApp);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const app = fixture.componentInstance;
|
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;
|
const comp = fixture.componentInstance.component;
|
||||||
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||||
expect(app.directive.value).toContain('ViewRef');
|
expect(app.directive.value).toContain('ViewRef');
|
||||||
@ -1720,7 +1721,7 @@ describe('di', () => {
|
|||||||
const fixture = TestBed.createComponent(MyComp);
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const comp = fixture.componentInstance;
|
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
|
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||||
expect(comp.directive.value).toContain('ViewRef');
|
expect(comp.directive.value).toContain('ViewRef');
|
||||||
|
|
||||||
@ -1743,7 +1744,7 @@ describe('di', () => {
|
|||||||
const fixture = TestBed.createComponent(MyComp);
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const comp = fixture.componentInstance;
|
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
|
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||||
expect(comp.directive.value).toContain('ViewRef');
|
expect(comp.directive.value).toContain('ViewRef');
|
||||||
|
|
||||||
@ -1773,7 +1774,8 @@ describe('di', () => {
|
|||||||
TestBed.configureTestingModule({declarations: [MyApp, MyDirective]});
|
TestBed.configureTestingModule({declarations: [MyApp, MyDirective]});
|
||||||
const fixture = TestBed.createComponent(MyApp);
|
const fixture = TestBed.createComponent(MyApp);
|
||||||
fixture.detectChanges();
|
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');
|
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/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -627,6 +627,24 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
|||||||
expect(element.textContent).toContain('ICU start --> Autre <-- ICU end');
|
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', () => {
|
it('with no root node and text and DOM nodes surrounding ICU', () => {
|
||||||
loadTranslations({
|
loadTranslations({
|
||||||
[computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]:
|
[computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {InternalViewRef} from '@angular/core/src/linker/view_ref';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
@ -54,4 +54,22 @@ describe('ViewRef', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(document.body.querySelector('dynamic-cpt')).toBeFalsy();
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1164,7 +1164,7 @@
|
|||||||
"name": "shouldSearchParent"
|
"name": "shouldSearchParent"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "storeCleanupFn"
|
"name": "storeCleanupWithContext"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "stringify"
|
"name": "stringify"
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
|
|
||||||
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
|
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
|
||||||
|
import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||||
|
|
||||||
{
|
{
|
||||||
describe('HTML sanitizer', () => {
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -27,6 +27,9 @@ let _fakeAsyncTestZoneSpec: any = null;
|
|||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export function resetFakeAsyncZoneFallback() {
|
export function resetFakeAsyncZoneFallback() {
|
||||||
|
if (_fakeAsyncTestZoneSpec) {
|
||||||
|
_fakeAsyncTestZoneSpec.unlockDatePatch();
|
||||||
|
}
|
||||||
_fakeAsyncTestZoneSpec = null;
|
_fakeAsyncTestZoneSpec = null;
|
||||||
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
|
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
|
||||||
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
|
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
|
||||||
@ -73,6 +76,7 @@ export function fakeAsyncFallback(fn: Function): (...args: any[]) => any {
|
|||||||
let res: any;
|
let res: any;
|
||||||
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
|
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
|
||||||
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
|
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
|
||||||
|
_fakeAsyncTestZoneSpec.lockDatePatch();
|
||||||
try {
|
try {
|
||||||
res = fn.apply(this, args);
|
res = fn.apply(this, args);
|
||||||
flushMicrotasksFallback();
|
flushMicrotasksFallback();
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||||
import {merge, Observable} from 'rxjs';
|
import {merge, Observable, ReplaySubject} from 'rxjs';
|
||||||
import {map} from 'rxjs/operators';
|
import {map, switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
||||||
import {extractProjectableNodes} from './extract-projectable-nodes';
|
import {extractProjectableNodes} from './extract-projectable-nodes';
|
||||||
@ -43,9 +43,11 @@ export class ComponentNgElementStrategyFactory implements NgElementStrategyFacto
|
|||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export class ComponentNgElementStrategy implements NgElementStrategy {
|
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. */
|
/** Merged stream of the component's output events. */
|
||||||
// TODO(issue/24571): remove '!'.
|
readonly events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters)));
|
||||||
events!: Observable<NgElementStrategyEvent>;
|
|
||||||
|
|
||||||
/** Reference to the component that was created on connect. */
|
/** Reference to the component that was created on connect. */
|
||||||
private componentRef: ComponentRef<any>|null = null;
|
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. */
|
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
|
||||||
protected initializeOutputs(componentRef: ComponentRef<any>): void {
|
protected initializeOutputs(componentRef: ComponentRef<any>): void {
|
||||||
const eventEmitters = this.componentFactory.outputs.map(({propName, templateName}) => {
|
const eventEmitters: Observable<NgElementStrategyEvent>[] =
|
||||||
const emitter: EventEmitter<any> = componentRef.instance[propName];
|
this.componentFactory.outputs.map(({propName, templateName}) => {
|
||||||
return emitter.pipe(map(value => ({name: templateName, value})));
|
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. */
|
/** Calls ngOnChanges with all the inputs that have changed since the last call. */
|
||||||
|
@ -187,13 +187,30 @@ export function createCustomElement<P>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
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);
|
this.ngElementStrategy.connect(this);
|
||||||
|
|
||||||
// Listen for events from the strategy and dispatch them as custom events
|
if (!subscribedToEvents) {
|
||||||
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
|
// `events` were not initialized before running `connect()`: Subscribe to them now.
|
||||||
const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value);
|
// The events emitted during the component initialization have been missed, but at least
|
||||||
this.dispatchEvent(customEvent);
|
// future events will be captured.
|
||||||
});
|
this.subscribeToEvents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
@ -207,6 +224,14 @@ export function createCustomElement<P>(
|
|||||||
this.ngElementEventsSubscription = null;
|
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
|
// TypeScript 3.9+ defines getters/setters as configurable but non-enumerable properties (in
|
||||||
|
@ -41,6 +41,33 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
|||||||
expect(strategyFactory.create(injector)).toBeTruthy();
|
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', () => {
|
describe('after connected', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Set up an initial value to make sure it is passed to the component
|
// Set up an initial value to make sure it is passed to the component
|
||||||
|
@ -40,12 +40,7 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
strategyFactory = new TestStrategyFactory();
|
strategyFactory = new TestStrategyFactory();
|
||||||
strategy = strategyFactory.testStrategy;
|
strategy = strategyFactory.testStrategy;
|
||||||
|
|
||||||
const {selector, ElementCtor} = createTestCustomElement();
|
NgElementCtor = createAndRegisterTestCustomElement(strategyFactory);
|
||||||
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);
|
|
||||||
})
|
})
|
||||||
.then(done, done.fail);
|
.then(done, done.fail);
|
||||||
});
|
});
|
||||||
@ -117,6 +112,47 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
expect(eventValue).toEqual(null);
|
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', () => {
|
it('should properly set getters/setters on the element', () => {
|
||||||
const element = new NgElementCtor(injector);
|
const element = new NgElementCtor(injector);
|
||||||
element.fooFoo = 'foo-foo-value';
|
element.fooFoo = 'foo-foo-value';
|
||||||
@ -144,7 +180,7 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
|
|
||||||
it('should capture properties set before upgrading the element', () => {
|
it('should capture properties set before upgrading the element', () => {
|
||||||
// Create a regular element and set properties on it.
|
// 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), {
|
const element = Object.assign(document.createElement(selector), {
|
||||||
fooFoo: 'foo-prop-value',
|
fooFoo: 'foo-prop-value',
|
||||||
barBar: 'bar-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',
|
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.
|
// 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), {
|
const element = Object.assign(document.createElement(selector), {
|
||||||
fooFoo: 'foo-prop-value',
|
fooFoo: 'foo-prop-value',
|
||||||
barBar: 'bar-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',
|
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.
|
// 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), {
|
const element = Object.assign(document.createElement(selector), {
|
||||||
fooFoo: 'foo-prop-value',
|
fooFoo: 'foo-prop-value',
|
||||||
barBar: 'bar-prop-value',
|
barBar: 'bar-prop-value',
|
||||||
@ -219,7 +255,17 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helpers
|
// 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 {
|
return {
|
||||||
selector: `test-element-${++selectorUid}`,
|
selector: `test-element-${++selectorUid}`,
|
||||||
ElementCtor: createCustomElement<WithFooBar>(TestComponent, {injector, strategyFactory}),
|
ElementCtor: createCustomElement<WithFooBar>(TestComponent, {injector, strategyFactory}),
|
||||||
@ -255,6 +301,7 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
events = new Subject<NgElementStrategyEvent>();
|
events = new Subject<NgElementStrategyEvent>();
|
||||||
|
|
||||||
connect(element: HTMLElement): void {
|
connect(element: HTMLElement): void {
|
||||||
|
this.events.next({name: 'strategy-event', value: 'connect'});
|
||||||
this.connectedElement = element;
|
this.connectedElement = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
||||||
|
|
||||||
import {ATTR, getBindingDescriptor} from './binding_utils';
|
import {ATTR, getBindingDescriptor} from './binding_utils';
|
||||||
@ -127,72 +127,18 @@ function getBoundedWordSpan(
|
|||||||
|
|
||||||
export function getTemplateCompletions(
|
export function getTemplateCompletions(
|
||||||
templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
|
templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
|
||||||
let result: ng.CompletionEntry[] = [];
|
|
||||||
const {htmlAst, template} = templateInfo;
|
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 templatePosition = position - template.span.start;
|
||||||
const path = getPathToNodeAtPosition(htmlAst, templatePosition);
|
const htmlPath: HtmlAstPath = getPathToNodeAtPosition(htmlAst, templatePosition);
|
||||||
const mostSpecific = path.tail;
|
const mostSpecific = htmlPath.tail;
|
||||||
if (path.empty || !mostSpecific) {
|
const visitor = new HtmlVisitor(templateInfo, htmlPath);
|
||||||
result = elementCompletions(templateInfo);
|
const results: ng.CompletionEntry[] = mostSpecific ?
|
||||||
} else {
|
mostSpecific.visit(visitor, null /* context */) :
|
||||||
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
|
elementCompletions(templateInfo);
|
||||||
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 replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
|
const replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
|
||||||
return result.map(entry => {
|
return results.map(entry => {
|
||||||
return {
|
return {
|
||||||
...entry,
|
...entry,
|
||||||
replacementSpan,
|
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[] {
|
function attributeCompletions(info: ng.AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
||||||
const attr = path.tail;
|
const attr = path.tail;
|
||||||
const elem = path.parentOf(attr);
|
const elem = path.parentOf(attr);
|
||||||
@ -356,18 +374,6 @@ function elementCompletions(info: ng.AstResult): ng.CompletionEntry[] {
|
|||||||
return results;
|
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
|
// 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
|
// 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
|
// element "Some <a text". We, however, want to treat this as if the user was requesting
|
||||||
|
@ -841,6 +841,13 @@ describe('completions', () => {
|
|||||||
'trim',
|
'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(
|
function expectContain(
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const {readFileSync} = require('fs');
|
||||||
|
const {bold, yellow} = require('chalk');
|
||||||
|
|
||||||
module.exports = (gulp) => () => {
|
module.exports = (gulp) => () => {
|
||||||
const conventionalChangelog = require('gulp-conventional-changelog');
|
const conventionalChangelog = require('gulp-conventional-changelog');
|
||||||
const ignoredScopes = [
|
const ignoredScopes = [
|
||||||
@ -16,11 +19,55 @@ module.exports = (gulp) => () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return gulp.src('CHANGELOG.md')
|
return gulp.src('CHANGELOG.md')
|
||||||
.pipe(conventionalChangelog({preset: 'angular'}, {}, {
|
.pipe(conventionalChangelog(
|
||||||
// Ignore commits that start with `<type>(<scope>)` for any of the ignored scopes.
|
/* core options */ {preset: 'angular'},
|
||||||
extendedRegexp: true,
|
/* context options */ {},
|
||||||
grep: `^[^(]+\\((${ignoredScopes.join('|')})\\)`,
|
/* raw-commit options */ {
|
||||||
invertGrep: true,
|
// 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('./'));
|
.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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user