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 = {
|
||||
'maxLength': 120,
|
||||
'minBodyLength': 100,
|
||||
'minBodyLengthExcludes': ['docs'],
|
||||
'minBodyLengthTypeExcludes': ['docs'],
|
||||
'types': [
|
||||
'build',
|
||||
'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>
|
||||
## [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,
|
||||
as when you log in a different user.
|
||||
|
||||
Let's say you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
|
||||
Imagine that you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
|
||||
`HeroService` won't have direct access to the user information to decide
|
||||
who is authorized and who isn't.
|
||||
|
||||
To resolve this, we give the `HeroService` constructor a boolean flag to control display of secret heroes.
|
||||
To resolve this, give the `HeroService` constructor a boolean flag to control display of secret heroes.
|
||||
|
||||
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" header="src/app/heroes/hero.service.ts (excerpt)"></code-example>
|
||||
|
||||
|
@ -834,7 +834,7 @@ Global variants of the locale data are available in [`@angular/common/locales/gl
|
||||
The following example imports the global variants for French (`fr`):
|
||||
|
||||
<code-example language="javascript" header="app.module.ts">
|
||||
import '@angular/common/locales/global/fr;
|
||||
import '@angular/common/locales/global/fr';
|
||||
</code-example>
|
||||
|
||||
{@a custom-id}
|
||||
|
@ -5,6 +5,7 @@
|
||||
This migration adds support to existing projects for TypeScript's new ["solution-style" tsconfig feature](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig).
|
||||
|
||||
Support is added by making two changes:
|
||||
|
||||
1. Renaming the workspace-level `tsconfig.json` to `tsconfig.base.json`.
|
||||
All project [TypeScript configuration files](guide/typescript-configuration) will extend from this base which contains the common options used throughout the workspace.
|
||||
|
||||
|
@ -112,7 +112,7 @@ Because observables produce values asynchronously, try/catch will not effectivel
|
||||
<code-example>
|
||||
myObservable.subscribe({
|
||||
next(num) { console.log('Next num: ' + num)},
|
||||
error(err) { console.log('Received an errror: ' + err)}
|
||||
error(err) { console.log('Received an error: ' + err)}
|
||||
});
|
||||
</code-example>
|
||||
|
||||
|
@ -359,10 +359,11 @@ Then inject it inside a test by calling `TestBed.inject()` with the service clas
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
**Note:** We used to have `TestBed.get()` instead of `TestBed.inject()`.
|
||||
The `get` method wasn't type safe, it always returned `any`, and this is error prone.
|
||||
We decided to migrate to a new function instead of updating the existing one given
|
||||
the large scale use that would have an immense amount of breaking changes.
|
||||
**Note:** `TestBed.get()` was deprecated as of Angular version 9.
|
||||
To help minimize breaking changes, Angular introduces a
|
||||
new function called `TestBed.inject()`, which you should use instead.
|
||||
For information on the removal of `TestBed.get()`,
|
||||
see its entry in the [Deprecations index](guide/deprecations#index).
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -30,7 +30,7 @@ If you're curious about the specific migrations being run by the CLI, see the [a
|
||||
* The `minLength` and `maxLength` validators only validate values that have a numeric `length` property. See [PR 36157](https://github.com/angular/angular/pull/36157).
|
||||
* Templates with unknown property bindings or unknown element names now log errors instead of warnings. See [PR 36399](https://github.com/angular/angular/pull/36399).
|
||||
* `UrlMatcher` can now return `null` values. See [PR 36402](https://github.com/angular/angular/pull/36402).
|
||||
* Transplanted views now refresh at insertion point only. See PR 35968](https://github.com/angular/angular/pull/35968).
|
||||
* Transplanted views now refresh at insertion point only. See [PR 35968](https://github.com/angular/angular/pull/35968).
|
||||
* Formatting times with the `b` or `B` format codes now supports time periods that cross midnight. See [PR 36611](https://github.com/angular/angular/pull/36611).
|
||||
* Navigation is canceled for routes with at least one empty resolver. See [PR 24621](https://github.com/angular/angular/pull/24621).
|
||||
|
||||
|
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",
|
||||
"bio": "Joost is a Software Engineer from the Netherlands with an interest in open source software who likes to learn something new every day. He works at Blueriq during the day and contributes to Angular in his spare time, by working on the Angular compiler and runtime. He may review your PR even if you never asked for it ;)"
|
||||
},
|
||||
"sonukapoor": {
|
||||
"name": "Sonu Kapoor",
|
||||
"groups": ["Collaborators"],
|
||||
"picture": "sonukapoor.jpg",
|
||||
"website": "https://www.linkedin.com/in/sonu-kapoor/",
|
||||
"bio": "Sonu is a Software Engineer from Toronto, with a high interest in front-end technologies and algorithms."
|
||||
},
|
||||
"jschwarty": {
|
||||
"name": "Justin Schwartzenberger",
|
||||
"picture": "justinschwartzenberger.jpg",
|
||||
@ -815,5 +822,13 @@
|
||||
"website": "https://wellwind.idv.tw/blog/",
|
||||
"bio": "Mike is a full-stack developer, consultant, blogger, instructor, and conference speaker. He has over 10 years of web development experience and passion to share his knowledge.",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"ahasall": {
|
||||
"name": "Amadou Sall",
|
||||
"picture": "ahasall.jpg",
|
||||
"groups": ["GDE"],
|
||||
"twitter": "ahasall",
|
||||
"website": "https://www.amadousall.com",
|
||||
"bio": "Amadou is a Frontend Software Engineer from Senegal based in France. He currently works at Air France where he helps developers build better Angular applications. Passionate about web technologies, Amadou is an international speaker, a technical writer, and a Google Developer Expert in Angular."
|
||||
}
|
||||
}
|
||||
|
@ -506,80 +506,6 @@
|
||||
"url": "guide/universal",
|
||||
"title": "Server-side Rendering",
|
||||
"tooltip": "Render HTML server-side with Angular Universal."
|
||||
},
|
||||
{
|
||||
"title": "Upgrading from AngularJS",
|
||||
"tooltip": "Incrementally upgrade an AngularJS application to Angular.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/upgrade-setup",
|
||||
"title": "Setup for Upgrading from AngularJS",
|
||||
"tooltip": "Use code from the Angular QuickStart seed as part of upgrading from AngularJS.",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"url": "guide/upgrade",
|
||||
"title": "Upgrading Instructions",
|
||||
"tooltip": "Incrementally upgrade an AngularJS application to Angular."
|
||||
},
|
||||
{
|
||||
"url": "guide/upgrade-performance",
|
||||
"title": "Upgrading for Performance",
|
||||
"tooltip": "Upgrade from AngularJS to Angular in a more flexible way."
|
||||
},
|
||||
{
|
||||
"url": "guide/ajs-quick-reference",
|
||||
"title": "AngularJS-Angular Concepts",
|
||||
"tooltip": "Learn how AngularJS concepts and techniques map to Angular."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Angular Libraries",
|
||||
"tooltip": "Extending Angular with shared libraries.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/libraries",
|
||||
"title": "Libraries Overview",
|
||||
"tooltip": "Understand how and when to use or create libraries."
|
||||
},
|
||||
{
|
||||
"url": "guide/using-libraries",
|
||||
"title": "Using Published Libraries",
|
||||
"tooltip": "Integrate published libraries into an app."
|
||||
},
|
||||
{
|
||||
"url": "guide/creating-libraries",
|
||||
"title": "Creating Libraries",
|
||||
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Schematics",
|
||||
"tooltip": "Using CLI schematics for code generation.",
|
||||
"children": [
|
||||
{
|
||||
"url": "guide/schematics",
|
||||
"title": "Schematics Overview",
|
||||
"tooltip": "How the CLI uses schematics to generate code."
|
||||
},
|
||||
{
|
||||
"url": "guide/schematics-authoring",
|
||||
"title": "Authoring Schematics",
|
||||
"tooltip": "Understand the structure of a schematic."
|
||||
},
|
||||
{
|
||||
"url": "guide/schematics-for-libraries",
|
||||
"title": "Schematics for Libraries",
|
||||
"tooltip": "Use schematics to integrate your library with the Angular CLI."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "guide/cli-builder",
|
||||
"title": "CLI Builders",
|
||||
"tooltip": "Using builders to customize Angular CLI."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -6,7 +6,7 @@
|
||||
In this tutorial, you build your own app from the ground up, providing experience with the typical development process, as well as an introduction to basic app-design concepts, tools, and terminology.
|
||||
|
||||
If you're completely new to Angular, you might want to try the [**Try it now**](start) quick-start app first.
|
||||
It is based on a ready-made partially-completed project, which you can examine and modify in the StacBlitz interactive development environment, where you can see the results in real time.
|
||||
It is based on a ready-made partially-completed project, which you can examine and modify in the StackBlitz interactive development environment, where you can see the results in real time.
|
||||
|
||||
The "Try it" tutorial covers the same major topics—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
|
||||
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
|
||||
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
|
||||
{"type": 301, "source": "/config/app-package-json", "destination": "https://webpack.js.org/configuration/optimization/#optimizationsideeffects"}
|
||||
{"type": 301, "source": "/config/app-package-json", "destination": "/guide/strict-mode#non-local-side-effects-in-applications"}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
|
@ -123,7 +123,7 @@
|
||||
"cross-spawn": "^5.1.0",
|
||||
"css-selector-parser": "^1.3.0",
|
||||
"dgeni": "^0.4.11",
|
||||
"dgeni-packages": "^0.28.3",
|
||||
"dgeni-packages": "^0.28.4",
|
||||
"entities": "^1.1.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
@ -175,4 +175,4 @@
|
||||
"xregexp": "^4.0.0",
|
||||
"yargs": "^7.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,11 @@
|
||||
<button mat-button class="hamburger" [class.starting]="isStarting" (click)="sidenav.toggle()" title="Docs menu">
|
||||
<mat-icon svgIcon="menu"></mat-icon>
|
||||
</button>
|
||||
<a class="nav-link home" href="/" [ngSwitch]="isSideBySide">
|
||||
<a class="nav-link home" href="/" [ngSwitch]="showTopMenu">
|
||||
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
|
||||
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
|
||||
</a>
|
||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
|
||||
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes?.TopBar"></aio-top-menu>
|
||||
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
|
||||
<div class="toolbar-external-icons-container">
|
||||
<a href="https://twitter.com/angular" title="Twitter" aria-label="Angular on twitter">
|
||||
@ -35,9 +35,9 @@
|
||||
|
||||
<mat-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">
|
||||
|
||||
<mat-sidenav [ngClass]="{'collapsed': !isSideBySide}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
|
||||
<aio-nav-menu *ngIf="!isSideBySide" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
|
||||
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="isSideBySide"></aio-nav-menu>
|
||||
<mat-sidenav [ngClass]="{'collapsed': !dockSideNav}" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
|
||||
<aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes?.TopBarNarrow" [isWide]="false"></aio-nav-menu>
|
||||
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes?.SideNav" [isWide]="dockSideNav"></aio-nav-menu>
|
||||
|
||||
<div class="doc-version">
|
||||
<aio-select (change)="onDocVersionChange($event.index)" [options]="docVersions" [selected]="currentDocVersion"></aio-select>
|
||||
|
@ -25,11 +25,9 @@ import { first, mapTo } from 'rxjs/operators';
|
||||
import { MockLocationService } from 'testing/location.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { MockSearchService } from 'testing/search.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppComponent, dockSideNavWidth, showFloatingTocWidth, showTopMenuWidth } from './app.component';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
const sideBySideBreakPoint = 992;
|
||||
const hideToCBreakPoint = 800;
|
||||
const startedDelay = 100;
|
||||
|
||||
describe('AppComponent', () => {
|
||||
@ -58,7 +56,7 @@ describe('AppComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
component.onResize(sideBySideBreakPoint + 1); // wide by default
|
||||
component.onResize(showTopMenuWidth + 1); // wide by default
|
||||
|
||||
const de = fixture.debugElement;
|
||||
const docViewerDe = de.query(By.css('aio-doc-viewer'));
|
||||
@ -99,7 +97,7 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
it('should be false on narrow screens', () => {
|
||||
component.onResize(hideToCBreakPoint - 1);
|
||||
component.onResize(showFloatingTocWidth - 1);
|
||||
|
||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
@ -112,7 +110,7 @@ describe('AppComponent', () => {
|
||||
});
|
||||
|
||||
it('should be true on wide screens unless the toc is empty', () => {
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
|
||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||
expect(component.hasFloatingToc).toBe(true);
|
||||
@ -127,37 +125,47 @@ describe('AppComponent', () => {
|
||||
it('should be false when toc is empty', () => {
|
||||
tocService.tocList.next([]);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
||||
component.onResize(hideToCBreakPoint - 1);
|
||||
component.onResize(showFloatingTocWidth - 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true when toc is not empty unless the screen is narrow', () => {
|
||||
tocService.tocList.next([{}, {}, {}] as TocItem[]);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(true);
|
||||
|
||||
component.onResize(hideToCBreakPoint - 1);
|
||||
component.onResize(showFloatingTocWidth - 1);
|
||||
expect(component.hasFloatingToc).toBe(false);
|
||||
|
||||
component.onResize(hideToCBreakPoint + 1);
|
||||
component.onResize(showFloatingTocWidth + 1);
|
||||
expect(component.hasFloatingToc).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSideBySide', () => {
|
||||
describe('showTopMenu', () => {
|
||||
it('should be updated on resize', () => {
|
||||
component.onResize(sideBySideBreakPoint - 1);
|
||||
expect(component.isSideBySide).toBe(false);
|
||||
component.onResize(showTopMenuWidth - 1);
|
||||
expect(component.showTopMenu).toBe(false);
|
||||
|
||||
component.onResize(sideBySideBreakPoint + 1);
|
||||
expect(component.isSideBySide).toBe(true);
|
||||
component.onResize(showTopMenuWidth + 1);
|
||||
expect(component.showTopMenu).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dockSideNav', () => {
|
||||
it('should be updated on resize', () => {
|
||||
component.onResize(dockSideNavWidth - 1);
|
||||
expect(component.dockSideNav).toBe(false);
|
||||
|
||||
component.onResize(dockSideNavWidth + 1);
|
||||
expect(component.dockSideNav).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -185,8 +193,8 @@ describe('AppComponent', () => {
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
describe('when side-by-side (wide)', () => {
|
||||
beforeEach(() => resizeTo(sideBySideBreakPoint + 1)); // side-by-side
|
||||
describe('when view is wide', () => {
|
||||
beforeEach(() => resizeTo(dockSideNavWidth + 1)); // wide view
|
||||
|
||||
it('should open when navigating to a guide page (guide/pipes)', () => {
|
||||
navigateTo('guide/pipes');
|
||||
@ -232,8 +240,8 @@ describe('AppComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when NOT side-by-side (narrow)', () => {
|
||||
beforeEach(() => resizeTo(sideBySideBreakPoint - 1)); // NOT side-by-side
|
||||
describe('when view is narrow', () => {
|
||||
beforeEach(() => resizeTo(dockSideNavWidth - 1)); // narrow view
|
||||
|
||||
it('should be closed when navigating to a guide page (guide/pipes)', () => {
|
||||
navigateTo('guide/pipes');
|
||||
@ -286,30 +294,30 @@ describe('AppComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing side-by-side (narrow --> wide)', () => {
|
||||
describe('when changing from narrow to wide view', () => {
|
||||
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
|
||||
const nonSidenavDocs = ['features', 'about'];
|
||||
|
||||
sidenavDocs.forEach(doc => {
|
||||
it(`should open when on a sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(dockSideNavWidth + 1);
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
nonSidenavDocs.forEach(doc => {
|
||||
it(`should remain closed when on a non-sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(dockSideNavWidth + 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -317,33 +325,33 @@ describe('AppComponent', () => {
|
||||
describe('when manually opened', () => {
|
||||
sidenavDocs.forEach(doc => {
|
||||
it(`should remain opened when on a sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
toggleSidenav();
|
||||
expect(sidenav.opened).toBe(true);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(dockSideNavWidth + 1);
|
||||
expect(sidenav.opened).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
nonSidenavDocs.forEach(doc => {
|
||||
it(`should close when on a non-sidenav doc (${doc})`, () => {
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
|
||||
navigateTo(doc);
|
||||
toggleSidenav();
|
||||
expect(sidenav.opened).toBe(true);
|
||||
|
||||
resizeTo(sideBySideBreakPoint + 1);
|
||||
resizeTo(showTopMenuWidth + 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing side-by-side (wide --> narrow)', () => {
|
||||
describe('when changing from wide to narrow view', () => {
|
||||
const sidenavDocs = ['api/a/b/c/d', 'guide/pipes'];
|
||||
const nonSidenavDocs = ['features', 'about'];
|
||||
|
||||
@ -352,7 +360,7 @@ describe('AppComponent', () => {
|
||||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(true);
|
||||
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -362,7 +370,7 @@ describe('AppComponent', () => {
|
||||
navigateTo(doc);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
|
||||
resizeTo(sideBySideBreakPoint - 1);
|
||||
resizeTo(dockSideNavWidth - 1);
|
||||
expect(sidenav.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -376,7 +384,7 @@ describe('AppComponent', () => {
|
||||
async function setupSelectorForTesting(mode?: string) {
|
||||
createTestingModule('a/b', mode);
|
||||
await initializeTest();
|
||||
component.onResize(sideBySideBreakPoint + 1); // side-by-side
|
||||
component.onResize(dockSideNavWidth + 1); // wide view
|
||||
selectElement = fixture.debugElement.query(By.directive(SelectComponent));
|
||||
selectComponent = selectElement.componentInstance;
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
const sideNavView = 'SideNav';
|
||||
export const showTopMenuWidth = 1048;
|
||||
export const dockSideNavWidth = 992;
|
||||
export const showFloatingTocWidth = 800;
|
||||
|
||||
@Component({
|
||||
selector: 'aio-shell',
|
||||
@ -57,18 +60,17 @@ export class AppComponent implements OnInit {
|
||||
isStarting = true;
|
||||
isTransitioning = true;
|
||||
isFetching = false;
|
||||
isSideBySide = false;
|
||||
showTopMenu = false;
|
||||
dockSideNav = false;
|
||||
private isFetchingTimeout: any;
|
||||
private isSideNavDoc = false;
|
||||
|
||||
private sideBySideWidth = 992;
|
||||
sideNavNodes: NavigationNode[];
|
||||
topMenuNodes: NavigationNode[];
|
||||
topMenuNarrowNodes: NavigationNode[];
|
||||
|
||||
hasFloatingToc = false;
|
||||
private showFloatingToc = new BehaviorSubject(false);
|
||||
private showFloatingTocWidth = 800;
|
||||
tocMaxHeight: string;
|
||||
private tocMaxHeightOffset = 0;
|
||||
|
||||
@ -76,8 +78,8 @@ export class AppComponent implements OnInit {
|
||||
|
||||
private currentUrl: string;
|
||||
|
||||
get isOpened() { return this.isSideBySide && this.isSideNavDoc; }
|
||||
get mode() { return this.isSideBySide ? 'side' : 'over'; }
|
||||
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
|
||||
get mode() { return this.dockSideNav && (this.isSideNavDoc || this.showTopMenu) ? 'side' : 'over'; }
|
||||
|
||||
// Search related properties
|
||||
showSearchResults = false;
|
||||
@ -239,13 +241,14 @@ export class AppComponent implements OnInit {
|
||||
|
||||
@HostListener('window:resize', ['$event.target.innerWidth'])
|
||||
onResize(width: number) {
|
||||
this.isSideBySide = width >= this.sideBySideWidth;
|
||||
this.showFloatingToc.next(width > this.showFloatingTocWidth);
|
||||
this.showTopMenu = width >= showTopMenuWidth;
|
||||
this.dockSideNav = width >= dockSideNavWidth;
|
||||
this.showFloatingToc.next(width > showFloatingTocWidth);
|
||||
|
||||
if (this.isSideBySide && !this.isSideNavDoc) {
|
||||
if (this.showTopMenu && !this.isSideNavDoc) {
|
||||
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu
|
||||
// items in the top-bar, ensure the sidenav is closed.
|
||||
// (This condition can only be met when the resize event changes the value of `isSideBySide`
|
||||
// (This condition can only be met when the resize event changes the value of `showTopMenu`
|
||||
// from `false` to `true` while on a non-sidenav doc.)
|
||||
this.sidenav.toggle(false);
|
||||
}
|
||||
@ -338,7 +341,7 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
|
||||
// May be open or closed when wide; always closed when narrow.
|
||||
this.sidenav.toggle(this.isSideBySide && openSideNav);
|
||||
this.sidenav.toggle(this.dockSideNav && openSideNav);
|
||||
}
|
||||
|
||||
// Dynamically change height of table of contents container
|
||||
|
@ -1,4 +1,6 @@
|
||||
// VARIABLES
|
||||
$showTopMenuWidth: 1048px;
|
||||
$hideTopMenuWidth: $showTopMenuWidth - 1;
|
||||
$hamburgerShownMargin: 0 8px 0 0;
|
||||
$hamburgerHiddenMargin: 0 16px 0 -64px;
|
||||
|
||||
@ -59,9 +61,9 @@ aio-shell.folder-docs mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-guide mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-start mat-toolbar.mat-toolbar,
|
||||
aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
@media (min-width: 992px) {
|
||||
@media (min-width: $showTopMenuWidth) {
|
||||
.hamburger.mat-button {
|
||||
// Hamburger shown on non-marketing pages on large screens.
|
||||
// Hamburger shown on non-marketing pages even on large screens.
|
||||
margin: $hamburgerShownMargin;
|
||||
}
|
||||
}
|
||||
@ -73,7 +75,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
margin: $hamburgerShownMargin;
|
||||
padding: 0;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
@media (min-width: $showTopMenuWidth) {
|
||||
// Hamburger hidden by default on large screens.
|
||||
// (Will be shown per doc.)
|
||||
margin: $hamburgerHiddenMargin;
|
||||
@ -111,7 +113,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
@media screen and (max-width: $hideTopMenuWidth) {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
@ -125,7 +127,7 @@ aio-shell.folder-tutorial mat-toolbar.mat-toolbar {
|
||||
top: 12px;
|
||||
height: 40px;
|
||||
|
||||
@media (max-width: 991px) {
|
||||
@media (max-width: $hideTopMenuWidth) {
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
@ -237,6 +239,7 @@ aio-search-box.search-container {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 150px;
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
const fs = require('fs');
|
||||
const sh = require('shelljs');
|
||||
|
||||
const PATCH_LOCK = 'node_modules/@angular/cli/.patched';
|
||||
|
||||
if (!fs.existsSync(PATCH_LOCK)) {
|
||||
sh.set('-e');
|
||||
sh.cd(`${__dirname}/../../`);
|
||||
|
||||
if (!sh.test('-f', PATCH_LOCK)) {
|
||||
sh.ls('-l', __dirname)
|
||||
.filter(stat => stat.isFile() && /\.patch$/i.test(stat.name))
|
||||
.forEach(stat => sh.exec(`patch -p0 -i "${__dirname}/${stat.name}"`));
|
||||
|
||||
sh.touch(PATCH_LOCK);
|
||||
}
|
||||
|
||||
|
@ -4467,10 +4467,10 @@ dezalgo@^1.0.0:
|
||||
asap "^2.0.0"
|
||||
wrappy "1"
|
||||
|
||||
dgeni-packages@^0.28.3:
|
||||
version "0.28.3"
|
||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.3.tgz#2e1e55f341c389b67ebb28933ce1e7e9ad05c49b"
|
||||
integrity sha512-WyVzY3Q4ylfnc2677le5G7a7WqkF88rBSjU9IrAofqro71yzZeWLoEdr/gJY+lJZ0PrDyuRW05pFvIbvX8N0PQ==
|
||||
dgeni-packages@^0.28.4:
|
||||
version "0.28.4"
|
||||
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.4.tgz#53a3e6700b8d8f6be168cadcc9fdb36e1d7011d3"
|
||||
integrity sha512-7AUG3pKpWtn69c3v2Mzgh+i5gd+L0AxFfYGWGzBdlJqMlQfaQPQjaS54iYCvnOlK9rXBn9j39yO6EU70gDZuFw==
|
||||
dependencies:
|
||||
canonical-path "^1.0.0"
|
||||
catharsis "^0.8.1"
|
||||
|
@ -135,7 +135,13 @@ export class GithubApiMergeStrategy extends MergeStrategy {
|
||||
|
||||
// Cherry pick the merged commits into the remaining target branches.
|
||||
const failedBranches = await this.cherryPickIntoTargetBranches(
|
||||
`${targetSha}~${targetCommitsCount}..${targetSha}`, cherryPickTargetBranches);
|
||||
`${targetSha}~${targetCommitsCount}..${targetSha}`, cherryPickTargetBranches, {
|
||||
// Commits that have been created by the Github API do not necessarily contain
|
||||
// a reference to the source pull request (unless the squash strategy is used).
|
||||
// To ensure that original commits can be found when a commit is viewed in a
|
||||
// target branch, we add a link to the original commits when cherry-picking.
|
||||
linkToOriginalCommits: true,
|
||||
});
|
||||
|
||||
// We already checked whether the PR can be cherry-picked into the target branches,
|
||||
// but in case the cherry-pick somehow fails, we still handle the conflicts here. The
|
||||
|
@ -69,7 +69,8 @@ export abstract class MergeStrategy {
|
||||
* @returns A list of branches for which the revisions could not be cherry-picked into.
|
||||
*/
|
||||
protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: {
|
||||
dryRun?: boolean
|
||||
dryRun?: boolean,
|
||||
linkToOriginalCommits?: boolean,
|
||||
} = {}) {
|
||||
const cherryPickArgs = [revisionRange];
|
||||
const failedBranches: string[] = [];
|
||||
@ -82,6 +83,14 @@ export abstract class MergeStrategy {
|
||||
cherryPickArgs.push('--no-commit');
|
||||
}
|
||||
|
||||
if (options.linkToOriginalCommits) {
|
||||
// We add `-x` when cherry-picking as that will allow us to easily jump to original
|
||||
// commits for cherry-picked commits. With that flag set, Git will automatically append
|
||||
// the original SHA/revision to the commit message. e.g. `(cherry picked from commit <..>)`.
|
||||
// https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--x.
|
||||
cherryPickArgs.push('-x');
|
||||
}
|
||||
|
||||
// Cherry-pick the refspec into all determined target branches.
|
||||
for (const branchName of targetBranches) {
|
||||
const localTargetBranch = this.getLocalTargetBranchName(branchName);
|
||||
|
@ -12,7 +12,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2987,
|
||||
"main-es2015": 451406,
|
||||
"main-es2015": 450883,
|
||||
"polyfills-es2015": 52630
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,8 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 3097,
|
||||
"main-es2015": 428886,
|
||||
"polyfills-es2015": 52195
|
||||
"main-es2015": 428031,
|
||||
"polyfills-es2015": 52261
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,8 +30,8 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 1485,
|
||||
"main-es2015": 136302,
|
||||
"polyfills-es2015": 37246
|
||||
"main-es2015": 135533,
|
||||
"polyfills-es2015": 37248
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -39,7 +39,7 @@
|
||||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 246085,
|
||||
"main-es2015": 245488,
|
||||
"polyfills-es2015": 36938,
|
||||
"5-es2015": 751
|
||||
}
|
||||
@ -62,7 +62,7 @@
|
||||
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
|
||||
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
|
||||
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
|
||||
"bundle": 1210239
|
||||
"bundle": 1209651
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.0.1",
|
||||
"version": "10.0.3",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
@ -12,7 +12,7 @@ import {ParsedConfiguration} from '../../..';
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
||||
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
||||
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
||||
import {absoluteFrom, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
|
||||
import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
|
||||
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
@ -148,7 +148,8 @@ export class DecorationAnalyzer {
|
||||
*/
|
||||
analyzeProgram(): DecorationAnalyses {
|
||||
for (const sourceFile of this.program.getSourceFiles()) {
|
||||
if (!sourceFile.isDeclarationFile && isWithinPackage(this.packagePath, sourceFile)) {
|
||||
if (!sourceFile.isDeclarationFile &&
|
||||
isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile))) {
|
||||
this.compiler.analyzeFile(sourceFile);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {MetadataReader} from '../../../src/ngtsc/metadata';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||
@ -44,7 +44,7 @@ export class DefaultMigrationHost implements MigrationHost {
|
||||
}
|
||||
|
||||
isInScope(clazz: ClassDeclaration): boolean {
|
||||
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
|
||||
return isWithinPackage(this.entryPointPath, absoluteFromSourceFile(clazz.getSourceFile()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
||||
import {isWithinPackage} from './util';
|
||||
|
||||
@ -35,7 +35,7 @@ export class SwitchMarkerAnalyzer {
|
||||
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
|
||||
const analyzedFiles = new SwitchMarkerAnalyses();
|
||||
program.getSourceFiles()
|
||||
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
|
||||
.filter(sourceFile => isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile)))
|
||||
.forEach(sourceFile => {
|
||||
const declarations = this.host.getSwitchableDeclarations(sourceFile);
|
||||
if (declarations.length) {
|
||||
|
@ -5,13 +5,11 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
|
||||
import {DependencyTracker} from '../../../src/ngtsc/incremental/api';
|
||||
|
||||
export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
|
||||
const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile));
|
||||
export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteFsPath): boolean {
|
||||
const relativePath = relative(packagePath, filePath);
|
||||
return !relativePath.startsWith('..') && !relativePath.startsWith('node_modules/');
|
||||
}
|
||||
|
||||
|
@ -28,13 +28,13 @@ export class ClusterExecutor implements Executor {
|
||||
|
||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
|
||||
Promise<void> {
|
||||
return this.lockFile.lock(() => {
|
||||
return this.lockFile.lock(async () => {
|
||||
this.logger.debug(
|
||||
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
|
||||
const master = new ClusterMaster(
|
||||
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
|
||||
analyzeEntryPoints, this.createTaskCompletedCallback);
|
||||
return master.run();
|
||||
return await master.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
||||
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
@ -2525,7 +2526,7 @@ function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
|
||||
function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
|
||||
const rootFile = bundle.program.getSourceFile(bundle.path);
|
||||
return bundle.program.getSourceFiles().filter(
|
||||
f => (f !== rootFile) && isWithinPackage(bundle.package, f));
|
||||
f => (f !== rootFile) && isWithinPackage(bundle.package, absoluteFromSourceFile(f)));
|
||||
}
|
||||
|
||||
function isTopLevel(node: ts.Node): boolean {
|
||||
|
@ -37,7 +37,11 @@ export class AsyncLocker {
|
||||
*/
|
||||
async lock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
await this.create();
|
||||
return fn().finally(() => this.lockFile.remove());
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
this.lockFile.remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected async create() {
|
||||
|
@ -50,7 +50,7 @@ export function makeEntryPointBundle(
|
||||
const rootDir = entryPoint.packagePath;
|
||||
const options: ts
|
||||
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
|
||||
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.path);
|
||||
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
|
||||
const dtsHost = new NgtscCompilerHost(fs, options);
|
||||
|
||||
// Create the bundle programs, as necessary.
|
||||
@ -63,7 +63,7 @@ export function makeEntryPointBundle(
|
||||
[];
|
||||
const dts = transformDts ? makeBundleProgram(
|
||||
fs, isCore, entryPoint.packagePath, typingsPath, 'r3_symbols.d.ts',
|
||||
options, dtsHost, additionalDtsFiles) :
|
||||
{...options, allowJs: false}, dtsHost, additionalDtsFiles) :
|
||||
null;
|
||||
const isFlatCore = isCore && src.r3SymbolsFile === null;
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {isWithinPackage} from '../analysis/util';
|
||||
import {isRelativePath} from '../utils';
|
||||
|
||||
/**
|
||||
@ -20,7 +21,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
private cache = ts.createModuleResolutionCache(
|
||||
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
|
||||
|
||||
constructor(fs: FileSystem, options: ts.CompilerOptions, protected entryPointPath: string) {
|
||||
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
|
||||
super(fs, options);
|
||||
}
|
||||
|
||||
@ -36,13 +37,24 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
|
||||
// file was in the same directory. This is undesirable, as we need to have the actual
|
||||
// JavaScript being present in the program. This logic recognizes this scenario and rewrites
|
||||
// the resolved .d.ts declaration file to its .js counterpart, if it exists.
|
||||
if (resolvedModule !== undefined && resolvedModule.extension === ts.Extension.Dts &&
|
||||
containingFile.endsWith('.js') && isRelativePath(moduleName)) {
|
||||
if (resolvedModule?.extension === ts.Extension.Dts && containingFile.endsWith('.js') &&
|
||||
isRelativePath(moduleName)) {
|
||||
const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js');
|
||||
if (this.fileExists(jsFile)) {
|
||||
return {...resolvedModule, resolvedFileName: jsFile, extension: ts.Extension.Js};
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent loading JavaScript source files outside of the package root, which would happen for
|
||||
// packages that don't have .d.ts files. As ngcc should only operate on the .js files
|
||||
// contained within the package, any files outside the package are simply discarded. This does
|
||||
// result in a partial program with error diagnostics, however ngcc won't gather diagnostics
|
||||
// for the program it creates so these diagnostics won't be reported.
|
||||
if (resolvedModule?.extension === ts.Extension.Js &&
|
||||
!isWithinPackage(this.packagePath, this.fs.resolve(resolvedModule.resolvedFileName))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolvedModule;
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {isWithinPackage} from '../../src/analysis/util';
|
||||
@ -18,15 +17,13 @@ runInEachFileSystem(() => {
|
||||
|
||||
it('should return true if the source-file is contained in the package', () => {
|
||||
const packagePath = _('/node_modules/test');
|
||||
const file =
|
||||
ts.createSourceFile(_('/node_modules/test/src/index.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file = _('/node_modules/test/src/index.js');
|
||||
expect(isWithinPackage(packagePath, file)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the source-file is not contained in the package', () => {
|
||||
const packagePath = _('/node_modules/test');
|
||||
const file =
|
||||
ts.createSourceFile(_('/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file = _('/node_modules/other/src/index.js');
|
||||
expect(isWithinPackage(packagePath, file)).toBe(false);
|
||||
});
|
||||
|
||||
@ -34,13 +31,11 @@ runInEachFileSystem(() => {
|
||||
const packagePath = _('/node_modules/test');
|
||||
|
||||
// An external file inside the package's `node_modules/`.
|
||||
const file1 = ts.createSourceFile(
|
||||
_('/node_modules/test/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file1 = _('/node_modules/test/node_modules/other/src/index.js');
|
||||
expect(isWithinPackage(packagePath, file1)).toBe(false);
|
||||
|
||||
// An internal file starting with `node_modules`.
|
||||
const file2 = ts.createSourceFile(
|
||||
_('/node_modules/test/node_modules_optimizer.js'), '', ts.ScriptTarget.ES2015);
|
||||
const file2 = _('/node_modules/test/node_modules_optimizer.js');
|
||||
expect(isWithinPackage(packagePath, file2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ runInEachFileSystem(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
|
||||
.and.returnValue(Promise.resolve('CusterMaster#run()' as any));
|
||||
.and.returnValue(Promise.resolve('ClusterMaster#run()' as any));
|
||||
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
|
||||
|
||||
mockLogger = new MockLogger();
|
||||
@ -63,7 +63,7 @@ runInEachFileSystem(() => {
|
||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||
|
||||
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
|
||||
.toBe('CusterMaster#run()' as any);
|
||||
.toBe('ClusterMaster#run()' as any);
|
||||
|
||||
expect(masterRunSpy).toHaveBeenCalledWith();
|
||||
|
||||
@ -78,6 +78,22 @@ runInEachFileSystem(() => {
|
||||
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||
});
|
||||
|
||||
it('should call LockFile.write() and LockFile.remove() if analyzeFn fails', async () => {
|
||||
const analyzeEntryPointsSpy =
|
||||
jasmine.createSpy('analyzeEntryPoints').and.throwError('analyze error');
|
||||
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
|
||||
let error = '';
|
||||
try {
|
||||
await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
|
||||
expect(createCompilerFnSpy).not.toHaveBeenCalled();
|
||||
expect(error).toEqual('analyze error');
|
||||
expect(lockFileLog).toEqual(['write()', 'remove()']);
|
||||
});
|
||||
|
||||
it('should call LockFile.write() and LockFile.remove() if master runner fails', async () => {
|
||||
const anyFn: () => any = () => undefined;
|
||||
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));
|
||||
|
@ -68,7 +68,7 @@ export function makeTestBundleProgram(
|
||||
const rootDir = fs.dirname(entryPointPath);
|
||||
const options: ts.CompilerOptions =
|
||||
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
|
||||
const host = new NgccSourcesCompilerHost(fs, options, entryPointPath);
|
||||
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
|
||||
return makeBundleProgram(
|
||||
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
|
||||
}
|
||||
|
@ -401,6 +401,121 @@ runInEachFileSystem(() => {
|
||||
expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
|
||||
});
|
||||
|
||||
it('should not crash when scanning for ModuleWithProviders needs to evaluate code from an external package',
|
||||
() => {
|
||||
// Regression test for https://github.com/angular/angular/issues/37508
|
||||
// During `ModuleWithProviders` analysis, return statements in methods are evaluated using
|
||||
// the partial evaluator to identify whether they correspond with a `ModuleWithProviders`
|
||||
// function. If an arbitrary method has a return statement that calls into an external
|
||||
// module which doesn't have declaration files, ngcc would attempt to reflect on said
|
||||
// module using the reflection host of the entry-point. This would crash in the case where
|
||||
// e.g. the entry-point is UMD and the external module would be CommonJS, as the UMD
|
||||
// reflection host would throw because it is unable to deal with CommonJS.
|
||||
|
||||
// Setup a non-TS package with CommonJS module format
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _(`/node_modules/identity/package.json`),
|
||||
contents: `{"name": "identity", "main": "./index.js"}`,
|
||||
},
|
||||
{
|
||||
name: _(`/node_modules/identity/index.js`),
|
||||
contents: `
|
||||
function identity(x) { return x; };
|
||||
exports.identity = identity;
|
||||
module.exports = identity;
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
// Setup an Angular entry-point with UMD module format that references an export of the
|
||||
// CommonJS package.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test-package/package.json'),
|
||||
contents: '{"name": "test-package", "main": "./index.js", "typings": "./index.d.ts"}'
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test-package/index.js'),
|
||||
contents: `
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('identity')) :
|
||||
typeof define === 'function' && define.amd ? define('test', ['exports', 'identity'], factory) :
|
||||
(factory(global.test, global.identity));
|
||||
}(this, (function (exports, identity) { 'use strict';
|
||||
function Foo(x) {
|
||||
// The below statement is analyzed for 'ModuleWithProviders', so is evaluated
|
||||
// by ngcc. The reference into the non-TS CommonJS package used to crash ngcc.
|
||||
return identity.identity(x);
|
||||
}
|
||||
exports.Foo = Foo;
|
||||
})));
|
||||
`
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test-package/index.d.ts'),
|
||||
contents: 'export declare class Foo { static doSomething(x: any): any; }'
|
||||
},
|
||||
{name: _('/node_modules/test-package/index.metadata.json'), contents: 'DUMMY DATA'},
|
||||
]);
|
||||
|
||||
expect(() => mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
targetEntryPointPath: 'test-package',
|
||||
propertiesToConsider: ['main'],
|
||||
}))
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it('should not be able to evaluate code in external packages when no .d.ts files are present',
|
||||
() => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _(`/node_modules/external/package.json`),
|
||||
contents: `{"name": "external", "main": "./index.js"}`,
|
||||
},
|
||||
{
|
||||
name: _(`/node_modules/external/index.js`),
|
||||
contents: `
|
||||
export const selector = 'my-selector';
|
||||
`,
|
||||
},
|
||||
]);
|
||||
|
||||
compileIntoApf('test-package', {
|
||||
'/index.ts': `
|
||||
import {NgModule, Component} from '@angular/core';
|
||||
import {selector} from 'external';
|
||||
|
||||
@Component({
|
||||
selector,
|
||||
template: ''
|
||||
})
|
||||
export class FooComponent {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooComponent],
|
||||
})
|
||||
export class FooModule {}
|
||||
`,
|
||||
});
|
||||
|
||||
try {
|
||||
mainNgcc({
|
||||
basePath: '/node_modules',
|
||||
targetEntryPointPath: 'test-package',
|
||||
propertiesToConsider: ['esm2015', 'esm5'],
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'Failed to compile entry-point test-package (esm2015 as esm2015) due to compilation errors:');
|
||||
expect(e.message).toContain('NG1010');
|
||||
expect(e.message).toContain('selector must be a string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
|
||||
compileIntoFlatEs2015Package('test-package', {
|
||||
'/index.ts': `
|
||||
|
@ -13,8 +13,12 @@ import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('entry point bundle', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
beforeEach(() => {
|
||||
_ = absoluteFrom;
|
||||
});
|
||||
|
||||
function setupMockFileSystem(): void {
|
||||
const _ = absoluteFrom;
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test/package.json'),
|
||||
@ -210,6 +214,103 @@ runInEachFileSystem(() => {
|
||||
].map(p => absoluteFrom(p).toString())));
|
||||
});
|
||||
|
||||
it('does not include .js files outside of the package when no .d.ts file is available', () => {
|
||||
// Declare main "test" package with "entry" entry-point that imports all sorts of
|
||||
// internal and external modules.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test/entry/package.json'),
|
||||
contents: `{"name": "test", "main": "./index.js", "typings": "./index.d.ts"}`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test/entry/index.d.ts'),
|
||||
contents: `
|
||||
import 'external-js';
|
||||
import 'external-ts';
|
||||
import 'nested-js';
|
||||
import './local';
|
||||
import '../package';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: _('/node_modules/test/entry/index.js'),
|
||||
contents: `
|
||||
import 'external-js';
|
||||
import 'external-ts';
|
||||
import 'nested-js';
|
||||
import './local';
|
||||
import '../package';
|
||||
`,
|
||||
},
|
||||
{name: _('/node_modules/test/entry/local.d.ts'), contents: `export {};`},
|
||||
{name: _('/node_modules/test/entry/local.js'), contents: `export {};`},
|
||||
{name: _('/node_modules/test/package.d.ts'), contents: `export {};`},
|
||||
{name: _('/node_modules/test/package.js'), contents: `export {};`},
|
||||
]);
|
||||
|
||||
// Declare "external-js" package outside of the "test" package without .d.ts files, should
|
||||
// not be included in the program.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/external-js/package.json'),
|
||||
contents: `{"name": "external-js", "main": "./index.js"}`,
|
||||
},
|
||||
{name: _('/node_modules/external-js/index.js'), contents: 'export {};'},
|
||||
]);
|
||||
|
||||
// Same as "external-js" but located in a nested node_modules directory, which should also
|
||||
// not be included in the program.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/test/node_modules/nested-js/package.json'),
|
||||
contents: `{"name": "nested-js", "main": "./index.js"}`,
|
||||
},
|
||||
{name: _('/node_modules/test/node_modules/nested-js/index.js'), contents: 'export {}'},
|
||||
]);
|
||||
|
||||
// Declare "external-ts" which does have .d.ts files, so the .d.ts should be
|
||||
// loaded into the program.
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/node_modules/external-ts/package.json'),
|
||||
contents: `{"name": "external-ts", "main": "./index.js", "typings": "./index.d.ts"}`,
|
||||
},
|
||||
{name: _('/node_modules/external-ts/index.d.ts'), contents: 'export {};'},
|
||||
{name: _('/node_modules/external-ts/index.js'), contents: 'export {};'},
|
||||
]);
|
||||
|
||||
const fs = getFileSystem();
|
||||
const entryPoint: EntryPoint = {
|
||||
name: 'test/entry',
|
||||
path: absoluteFrom('/node_modules/test/entry'),
|
||||
packageName: 'test',
|
||||
packagePath: absoluteFrom('/node_modules/test'),
|
||||
packageJson: {name: 'test/entry'},
|
||||
typings: absoluteFrom('/node_modules/test/entry/index.d.ts'),
|
||||
compiledByAngular: true,
|
||||
ignoreMissingDependencies: false,
|
||||
generateDeepReexports: false,
|
||||
};
|
||||
const esm5bundle = makeEntryPointBundle(
|
||||
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
|
||||
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
|
||||
|
||||
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
|
||||
.toEqual(jasmine.arrayWithExactContents([
|
||||
_('/node_modules/test/entry/index.js'),
|
||||
_('/node_modules/test/entry/local.js'),
|
||||
_('/node_modules/test/package.js'),
|
||||
_('/node_modules/external-ts/index.d.ts'),
|
||||
]));
|
||||
expect(esm5bundle.dts!.program.getSourceFiles().map(sf => _(sf.fileName)))
|
||||
.toEqual(jasmine.arrayWithExactContents([
|
||||
_('/node_modules/test/entry/index.d.ts'),
|
||||
_('/node_modules/test/entry/local.d.ts'),
|
||||
_('/node_modules/test/package.d.ts'),
|
||||
_('/node_modules/external-ts/index.d.ts'),
|
||||
]));
|
||||
});
|
||||
|
||||
describe(
|
||||
'including equivalently named, internally imported, src files in the typings program',
|
||||
() => {
|
||||
|
@ -116,7 +116,13 @@ export class NgCompiler {
|
||||
|
||||
const moduleResolutionCache = ts.createModuleResolutionCache(
|
||||
this.adapter.getCurrentDirectory(),
|
||||
fileName => this.adapter.getCanonicalFileName(fileName));
|
||||
// Note: this used to be an arrow-function closure. However, JS engines like v8 have some
|
||||
// strange behaviors with retaining the lexical scope of the closure. Even if this function
|
||||
// doesn't retain a reference to `this`, if other closures in the constructor here reference
|
||||
// `this` internally then a closure created here would retain them. This can cause major
|
||||
// memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its
|
||||
// way into all kinds of places inside TS internal objects.
|
||||
this.adapter.getCanonicalFileName.bind(this.adapter));
|
||||
this.moduleResolver =
|
||||
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
|
||||
this.resourceManager = new AdapterResourceLoader(adapter, this.options);
|
||||
|
@ -42,20 +42,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
||||
* Tracks an `IncrementalDriver` within the strategy itself.
|
||||
*/
|
||||
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
|
||||
private previous: IncrementalDriver|null = null;
|
||||
private next: IncrementalDriver|null = null;
|
||||
private driver: IncrementalDriver|null = null;
|
||||
private isSet: boolean = false;
|
||||
|
||||
getIncrementalDriver(): IncrementalDriver|null {
|
||||
return this.next !== null ? this.next : this.previous;
|
||||
return this.driver;
|
||||
}
|
||||
|
||||
setIncrementalDriver(driver: IncrementalDriver): void {
|
||||
this.next = driver;
|
||||
this.driver = driver;
|
||||
this.isSet = true;
|
||||
}
|
||||
|
||||
toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
|
||||
const strategy = new TrackedIncrementalBuildStrategy();
|
||||
strategy.previous = this.next;
|
||||
// Only reuse a driver that was explicitly set via `setIncrementalDriver`.
|
||||
strategy.driver = this.isSet ? this.driver : null;
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
|
@ -3661,6 +3661,39 @@ describe('i18n support in the template compiler', () => {
|
||||
|
||||
verify(input, output);
|
||||
});
|
||||
|
||||
it('should produce proper messages when `select` or `plural` keywords have spaces after them',
|
||||
() => {
|
||||
const input = `
|
||||
<div i18n>
|
||||
{count, select , 1 {one} other {more than one}}
|
||||
{count, plural , =1 {one} other {more than one}}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const output = String.raw`
|
||||
var $I18N_1$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
const $MSG_EXTERNAL_199763560911211963$$APP_SPEC_TS_2$ = goog.getMsg("{VAR_SELECT , select , 1 {one} other {more than one}}");
|
||||
$I18N_1$ = $MSG_EXTERNAL_199763560911211963$$APP_SPEC_TS_2$;
|
||||
}
|
||||
else {
|
||||
$I18N_1$ = $localize \`{VAR_SELECT , select , 1 {one} other {more than one}}\`;
|
||||
}
|
||||
$I18N_1$ = i0.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" });
|
||||
var $I18N_3$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
const $MSG_EXTERNAL_3383986062053865025$$APP_SPEC_TS_4$ = goog.getMsg("{VAR_PLURAL , plural , =1 {one} other {more than one}}");
|
||||
$I18N_3$ = $MSG_EXTERNAL_3383986062053865025$$APP_SPEC_TS_4$;
|
||||
}
|
||||
else {
|
||||
$I18N_3$ = $localize \`{VAR_PLURAL , plural , =1 {one} other {more than one}}\`;
|
||||
}
|
||||
$I18N_3$ = i0.ɵɵi18nPostprocess($I18N_3$, { "VAR_PLURAL": "\uFFFD1\uFFFD" });
|
||||
`;
|
||||
|
||||
verify(input, output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$localize legacy message ids', () => {
|
||||
|
@ -273,10 +273,20 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||
const value = message.placeholders[key];
|
||||
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
||||
const config = this.bindingParser.interpolationConfig;
|
||||
|
||||
// ICU expression is a plain string, not wrapped into start
|
||||
// and end tags, so we wrap it before passing to binding parser
|
||||
const wrapped = `${config.start}${value}${config.end}`;
|
||||
vars[key] = this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText;
|
||||
|
||||
// Currently when the `plural` or `select` keywords in an ICU contain trailing spaces (e.g.
|
||||
// `{count, select , ...}`), these spaces are also included into the key names in ICU vars
|
||||
// (e.g. "VAR_SELECT "). These trailing spaces are not desirable, since they will later be
|
||||
// converted into `_` symbols while normalizing placeholder names, which might lead to
|
||||
// mismatches at runtime (i.e. placeholder will not be replaced with the correct value).
|
||||
const formattedKey = key.trim();
|
||||
|
||||
vars[formattedKey] =
|
||||
this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText;
|
||||
} else {
|
||||
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
||||
}
|
||||
|
@ -12,22 +12,26 @@ import {Inject, Injectable, InjectionToken, Optional} from './di';
|
||||
|
||||
|
||||
/**
|
||||
* An injection token that allows you to provide one or more initialization functions.
|
||||
* These function are injected at application startup and executed during
|
||||
* A [DI token](guide/glossary#di-token "DI token definition") that you can use to provide
|
||||
* one or more initialization functions.
|
||||
*
|
||||
* The provided function are injected at application startup and executed during
|
||||
* app initialization. If any of these functions returns a Promise, initialization
|
||||
* does not complete until the Promise is resolved.
|
||||
*
|
||||
* You can, for example, create a factory function that loads language data
|
||||
* or an external configuration, and provide that function to the `APP_INITIALIZER` token.
|
||||
* That way, the function is executed during the application bootstrap process,
|
||||
* The function is executed during the application bootstrap process,
|
||||
* and the needed data is available on startup.
|
||||
*
|
||||
* @see `ApplicationInitStatus`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const APP_INITIALIZER = new InjectionToken<Array<() => void>>('Application Initializer');
|
||||
|
||||
/**
|
||||
* A class that reflects the state of running {@link APP_INITIALIZER}s.
|
||||
* A class that reflects the state of running {@link APP_INITIALIZER} functions.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
|
@ -11,13 +11,14 @@ import {ComponentRef} from './linker/component_factory';
|
||||
|
||||
|
||||
/**
|
||||
* A DI Token representing a unique string id assigned to the application by Angular and used
|
||||
* A [DI token](guide/glossary#di-token "DI token definition") representing a unique string ID, used
|
||||
* primarily for prefixing application attributes and CSS styles when
|
||||
* {@link ViewEncapsulation#Emulated ViewEncapsulation.Emulated} is being used.
|
||||
*
|
||||
* If you need to avoid randomly generated value to be used as an application id, you can provide
|
||||
* a custom value via a DI provider <!-- TODO: provider --> configuring the root {@link Injector}
|
||||
* using this token.
|
||||
* BY default, the value is randomly generated and assigned to the application by Angular.
|
||||
* To provide a custom ID value, use a DI provider <!-- TODO: provider --> to configure
|
||||
* the root {@link Injector} that uses this token.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const APP_ID = new InjectionToken<string>('AppId');
|
||||
@ -27,7 +28,7 @@ export function _appIdRandomProviderFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Providers that will generate a random APP_ID_TOKEN.
|
||||
* Providers that generate a random `APP_ID_TOKEN`.
|
||||
* @publicApi
|
||||
*/
|
||||
export const APP_ID_RANDOM_PROVIDER = {
|
||||
@ -41,22 +42,24 @@ function _randomChar(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that will be executed when a platform is initialized.
|
||||
* A function that is executed when a platform is initialized.
|
||||
* @publicApi
|
||||
*/
|
||||
export const PLATFORM_INITIALIZER = new InjectionToken<Array<() => void>>('Platform Initializer');
|
||||
|
||||
/**
|
||||
* A token that indicates an opaque platform id.
|
||||
* A token that indicates an opaque platform ID.
|
||||
* @publicApi
|
||||
*/
|
||||
export const PLATFORM_ID = new InjectionToken<Object>('Platform ID');
|
||||
|
||||
/**
|
||||
* All callbacks provided via this token will be called for every component that is bootstrapped.
|
||||
* Signature of the callback:
|
||||
* A [DI token](guide/glossary#di-token "DI token definition") that provides a set of callbacks to
|
||||
* be called for every component that is bootstrapped.
|
||||
*
|
||||
* `(componentRef: ComponentRef) => void`.
|
||||
* Each callback must take a `ComponentRef` instance and return nothing.
|
||||
*
|
||||
* `(componentRef: ComponentRef) => void`
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
@ -64,7 +67,8 @@ export const APP_BOOTSTRAP_LISTENER =
|
||||
new InjectionToken<Array<(compRef: ComponentRef<any>) => void>>('appBootstrapListener');
|
||||
|
||||
/**
|
||||
* A token which indicates the root directory of the application
|
||||
* A [DI token](guide/glossary#di-token "DI token definition") that indicates the root directory of
|
||||
* the application
|
||||
* @publicApi
|
||||
*/
|
||||
export const PACKAGE_ROOT_URL = new InjectionToken<string>('Application Packages Root URL');
|
||||
|
@ -9,10 +9,13 @@
|
||||
import {injectChangeDetectorRef as render3InjectChangeDetectorRef} from '../render3/view_engine_compatibility';
|
||||
|
||||
/**
|
||||
* Base class for Angular Views, provides change detection functionality.
|
||||
* Base class that provides change detection functionality.
|
||||
* A change-detection tree collects all views that are to be checked for changes.
|
||||
* Use the methods to add and remove views from the tree, initiate change-detection,
|
||||
* and explicitly mark views as _dirty_, meaning that they have changed and need to be rerendered.
|
||||
* and explicitly mark views as _dirty_, meaning that they have changed and need to be re-rendered.
|
||||
*
|
||||
* @see [Using change detection hooks](guide/lifecycle-hooks#using-change-detection-hooks)
|
||||
* @see [Defining custom change detection](guide/lifecycle-hooks#defining-custom-change-detection)
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
|
@ -11,6 +11,8 @@
|
||||
* The strategy that the default change detector uses to detect changes.
|
||||
* When set, takes effect the next time change detection is triggered.
|
||||
*
|
||||
* @see {@link ChangeDetectorRef#usage-notes Change detection usage}
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export enum ChangeDetectionStrategy {
|
||||
|
@ -70,8 +70,8 @@ export interface Injectable {
|
||||
* - 'root' : The application-level injector in most apps.
|
||||
* - 'platform' : A special singleton platform injector shared by all
|
||||
* applications on the page.
|
||||
* - 'any' : Provides a unique instance in every module (including lazy modules) that injects the
|
||||
* token.
|
||||
* - 'any' : Provides a unique instance in each lazy loaded module while all eagerly loaded
|
||||
* modules share one instance.
|
||||
*
|
||||
*/
|
||||
providedIn?: Type<any>|'root'|'platform'|'any'|null;
|
||||
|
@ -20,8 +20,6 @@ export interface InjectDecorator {
|
||||
* Parameter decorator on a dependency parameter of a class constructor
|
||||
* that specifies a custom provider of the dependency.
|
||||
*
|
||||
* Learn more in the ["Dependency Injection Guide"](guide/dependency-injection).
|
||||
*
|
||||
* @usageNotes
|
||||
* The following example shows a class constructor that specifies a
|
||||
* custom provider of a dependency using the parameter decorator.
|
||||
@ -31,6 +29,9 @@ export interface InjectDecorator {
|
||||
*
|
||||
* <code-example path="core/di/ts/metadata_spec.ts" region="InjectWithoutDecorator">
|
||||
* </code-example>
|
||||
*
|
||||
* @see ["Dependency Injection Guide"](guide/dependency-injection)
|
||||
*
|
||||
*/
|
||||
(token: any): any;
|
||||
new(token: any): Inject;
|
||||
@ -71,8 +72,6 @@ export interface OptionalDecorator {
|
||||
* Can be used together with other parameter decorators
|
||||
* that modify how dependency injection operates.
|
||||
*
|
||||
* Learn more in the ["Dependency Injection Guide"](guide/dependency-injection).
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* The following code allows the possibility of a null result:
|
||||
@ -80,6 +79,7 @@ export interface OptionalDecorator {
|
||||
* <code-example path="core/di/ts/metadata_spec.ts" region="Optional">
|
||||
* </code-example>
|
||||
*
|
||||
* @see ["Dependency Injection Guide"](guide/dependency-injection).
|
||||
*/
|
||||
(): any;
|
||||
new(): Optional;
|
||||
@ -122,7 +122,6 @@ export interface SelfDecorator {
|
||||
* <code-example path="core/di/ts/metadata_spec.ts" region="Self">
|
||||
* </code-example>
|
||||
*
|
||||
*
|
||||
* @see `SkipSelf`
|
||||
* @see `Optional`
|
||||
*
|
||||
@ -148,7 +147,7 @@ export const Self: SelfDecorator = makeParamDecorator('Self');
|
||||
|
||||
|
||||
/**
|
||||
* Type of the SkipSelf decorator / constructor function.
|
||||
* Type of the `SkipSelf` decorator / constructor function.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
@ -167,9 +166,7 @@ export interface SkipSelfDecorator {
|
||||
* <code-example path="core/di/ts/metadata_spec.ts" region="SkipSelf">
|
||||
* </code-example>
|
||||
*
|
||||
* Learn more in the
|
||||
* [Dependency Injection guide](guide/dependency-injection-in-action#skip).
|
||||
*
|
||||
* @see [Dependency Injection guide](guide/dependency-injection-in-action#skip).
|
||||
* @see `Self`
|
||||
* @see `Optional`
|
||||
*
|
||||
@ -179,14 +176,14 @@ export interface SkipSelfDecorator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the SkipSelf metadata.
|
||||
* Type of the `SkipSelf` metadata.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface SkipSelf {}
|
||||
|
||||
/**
|
||||
* SkipSelf decorator and metadata.
|
||||
* `SkipSelf` decorator and metadata.
|
||||
*
|
||||
* @Annotation
|
||||
* @publicApi
|
||||
@ -194,7 +191,7 @@ export interface SkipSelf {}
|
||||
export const SkipSelf: SkipSelfDecorator = makeParamDecorator('SkipSelf');
|
||||
|
||||
/**
|
||||
* Type of the Host decorator / constructor function.
|
||||
* Type of the `Host` decorator / constructor function.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
@ -204,15 +201,15 @@ export interface HostDecorator {
|
||||
* that tells the DI framework to resolve the view by checking injectors of child
|
||||
* elements, and stop when reaching the host element of the current component.
|
||||
*
|
||||
* For an extended example, see
|
||||
* ["Dependency Injection Guide"](guide/dependency-injection-in-action#optional).
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* The following shows use with the `@Optional` decorator, and allows for a null result.
|
||||
*
|
||||
* <code-example path="core/di/ts/metadata_spec.ts" region="Host">
|
||||
* </code-example>
|
||||
*
|
||||
* For an extended example, see ["Dependency Injection
|
||||
* Guide"](guide/dependency-injection-in-action#optional).
|
||||
*/
|
||||
(): any;
|
||||
new(): Host;
|
||||
@ -252,11 +249,11 @@ export interface AttributeDecorator {
|
||||
* <input type="text">
|
||||
* ```
|
||||
*
|
||||
* The following example uses the decorator to inject the string literal `text`.
|
||||
* The following example uses the decorator to inject the string literal `text` in a directive.
|
||||
*
|
||||
* {@example core/ts/metadata/metadata.ts region='attributeMetadata'}
|
||||
*
|
||||
* ### Example as TypeScript Decorator
|
||||
* The following example uses the decorator in a component constructor.
|
||||
*
|
||||
* {@example core/ts/metadata/metadata.ts region='attributeFactory'}
|
||||
*
|
||||
|
@ -13,27 +13,25 @@ import {ComponentFactoryResolver} from './component_factory_resolver';
|
||||
|
||||
|
||||
/**
|
||||
* Represents an instance of an NgModule created via a {@link NgModuleFactory}.
|
||||
*
|
||||
* `NgModuleRef` provides access to the NgModule Instance as well other objects related to this
|
||||
* NgModule Instance.
|
||||
* Represents an instance of an `NgModule` created by an `NgModuleFactory`.
|
||||
* Provides access to the `NgModule` instance and related objects.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export abstract class NgModuleRef<T> {
|
||||
/**
|
||||
* The injector that contains all of the providers of the NgModule.
|
||||
* The injector that contains all of the providers of the `NgModule`.
|
||||
*/
|
||||
abstract get injector(): Injector;
|
||||
|
||||
/**
|
||||
* The ComponentFactoryResolver to get hold of the ComponentFactories
|
||||
* The resolver that can retrieve the component factories
|
||||
* declared in the `entryComponents` property of the module.
|
||||
*/
|
||||
abstract get componentFactoryResolver(): ComponentFactoryResolver;
|
||||
|
||||
/**
|
||||
* The NgModule instance.
|
||||
* The `NgModule` instance.
|
||||
*/
|
||||
abstract get instance(): T;
|
||||
|
||||
@ -43,7 +41,7 @@ export abstract class NgModuleRef<T> {
|
||||
abstract destroy(): void;
|
||||
|
||||
/**
|
||||
* Allows to register a callback that will be called when the module is destroyed.
|
||||
* Registers a callback to be executed when the module is destroyed.
|
||||
*/
|
||||
abstract onDestroy(callback: () => void): void;
|
||||
}
|
||||
|
@ -10,12 +10,9 @@ import {ApplicationRef} from '../application_ref';
|
||||
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
|
||||
|
||||
/**
|
||||
* Represents an Angular [view](guide/glossary#view),
|
||||
* specifically the [host view](guide/glossary#view-tree) that is defined by a component.
|
||||
* Also serves as the base class
|
||||
* that adds destroy methods for [embedded views](guide/glossary#view-tree).
|
||||
* Represents an Angular [view](guide/glossary#view "Definition").
|
||||
*
|
||||
* @see `EmbeddedViewRef`
|
||||
* @see {@link ChangeDetectorRef#usage-notes Change detection usage}
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
|
@ -282,10 +282,10 @@ export interface Directive {
|
||||
host?: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* If true, this directive/component will be skipped by the AOT compiler and so will always be
|
||||
* compiled using JIT.
|
||||
*
|
||||
* This exists to support future Ivy work and has no effect currently.
|
||||
* When present, this directive/component is ignored by the AOT compiler.
|
||||
* It remains in distributed code, and the JIT compiler attempts to compile it
|
||||
* at run time, in the browser.
|
||||
* To ensure the correct behavior, the app must import `@angular/compiler`.
|
||||
*/
|
||||
jit?: true;
|
||||
}
|
||||
@ -314,7 +314,8 @@ export interface ComponentDecorator {
|
||||
* An Angular app contains a tree of Angular components.
|
||||
*
|
||||
* Angular components are a subset of directives, always associated with a template.
|
||||
* Unlike other directives, only one component can be instantiated per an element in a template.
|
||||
* Unlike other directives, only one component can be instantiated for a given element in a
|
||||
* template.
|
||||
*
|
||||
* A component must belong to an NgModule in order for it to be available
|
||||
* to another component or application. To make it a member of an NgModule,
|
||||
|
@ -80,12 +80,10 @@ export interface NgModuleDef<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around an NgModule that associates it with the providers.
|
||||
* A wrapper around an NgModule that associates it with [providers](guide/glossary#provider
|
||||
* "Definition"). Usage without a generic type is deprecated.
|
||||
*
|
||||
* @param T the module type.
|
||||
*
|
||||
* Note that using ModuleWithProviders without a generic type is deprecated.
|
||||
* The generic will become required in a future version of Angular.
|
||||
* @see [Deprecations](guide/deprecations#modulewithproviders-type-without-a-generic)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
@ -296,10 +294,10 @@ export interface NgModule {
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* If true, this module will be skipped by the AOT compiler and so will always be compiled
|
||||
* using JIT.
|
||||
*
|
||||
* This exists to support future Ivy work and has no effect currently.
|
||||
* When present, this module is ignored by the AOT compiler.
|
||||
* It remains in distributed code, and the JIT compiler attempts to compile it
|
||||
* at run time, in the browser.
|
||||
* To ensure the correct behavior, the app must import `@angular/compiler`.
|
||||
*/
|
||||
jit?: true;
|
||||
}
|
||||
|
@ -99,8 +99,12 @@ let nextNgElementId = 0;
|
||||
export function bloomAdd(
|
||||
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
|
||||
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
|
||||
let id: number|undefined =
|
||||
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
|
||||
let id: number|undefined;
|
||||
if (typeof type === 'string') {
|
||||
id = type.charCodeAt(0) || 0;
|
||||
} else if (type.hasOwnProperty(NG_ELEMENT_ID)) {
|
||||
id = (type as any)[NG_ELEMENT_ID];
|
||||
}
|
||||
|
||||
// Set a unique ID on the directive type, so if something tries to inject the directive,
|
||||
// we can easily retrieve the ID and hash it into the bloom bit that should be checked.
|
||||
@ -584,7 +588,9 @@ export function bloomHashBitOrFactory(token: Type<any>|InjectionToken<any>|strin
|
||||
if (typeof token === 'string') {
|
||||
return token.charCodeAt(0) || 0;
|
||||
}
|
||||
const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID];
|
||||
const tokenId: number|undefined =
|
||||
// First check with `hasOwnProperty` so we don't get an inherited ID.
|
||||
token.hasOwnProperty(NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
|
||||
// Negative token IDs are used for special objects such as `Injector`
|
||||
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import '../util/ng_i18n_closure_mode';
|
||||
|
||||
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
|
||||
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
|
||||
import {InertBodyHelper} from '../sanitization/inert_body';
|
||||
import {getInertBodyHelper} from '../sanitization/inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
||||
import {addAllToArray} from '../util/array_utils';
|
||||
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
|
||||
@ -1233,7 +1233,7 @@ function icuStart(
|
||||
function parseIcuCase(
|
||||
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
|
||||
expandoStartIndex: number): IcuCase {
|
||||
const inertBodyHelper = new InertBodyHelper(getDocument());
|
||||
const inertBodyHelper = getInertBodyHelper(getDocument());
|
||||
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
if (!inertBodyElement) {
|
||||
throw new Error('Unable to generate inert body element');
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
|
||||
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
|
||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
|
||||
import {assertDefined} from '../util/assert';
|
||||
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared';
|
||||
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupWithContext} from './instructions/shared';
|
||||
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
||||
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
||||
import {isLContainer} from './interfaces/type_checks';
|
||||
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
|
||||
}
|
||||
|
||||
onDestroy(callback: Function) {
|
||||
storeCleanupFn(this._lView[TVIEW], this._lView, callback);
|
||||
storeCleanupWithContext(this._lView[TVIEW], this._lView, null, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {isDevMode} from '../util/is_dev_mode';
|
||||
import {InertBodyHelper} from './inert_body';
|
||||
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
||||
|
||||
function tagSet(tags: string): {[k: string]: boolean} {
|
||||
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
|
||||
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
let inertBodyElement: HTMLElement|null = null;
|
||||
try {
|
||||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
|
||||
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
|
||||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||||
|
@ -7,89 +7,29 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* This helper is used to get hold of an inert tree of DOM elements containing dirty HTML
|
||||
* that needs sanitizing.
|
||||
* Depending upon browser support we must use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
* Default: InertDocument strategy
|
||||
* Depending upon browser support we use one of two strategies for doing this.
|
||||
* Default: DOMParser strategy
|
||||
* Fallback: InertDocument strategy
|
||||
*/
|
||||
export class InertBodyHelper {
|
||||
private inertDocument: Document;
|
||||
|
||||
constructor(private defaultDoc: Document) {
|
||||
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
||||
let inertBodyElement = this.inertDocument.body;
|
||||
|
||||
if (inertBodyElement == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.inertDocument.createElement('html');
|
||||
this.inertDocument.appendChild(inertHtml);
|
||||
inertBodyElement = this.inertDocument.createElement('body');
|
||||
inertHtml.appendChild(inertBodyElement);
|
||||
}
|
||||
|
||||
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
|
||||
if (inertBodyElement.querySelector && !inertBodyElement.querySelector('svg')) {
|
||||
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
// so use the XHR strategy.
|
||||
this.getInertBodyElement = this.getInertBodyElement_XHR;
|
||||
return;
|
||||
}
|
||||
|
||||
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
|
||||
if (inertBodyElement.querySelector && inertBodyElement.querySelector('svg img')) {
|
||||
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
// so use the DOMParser strategy, if it is available.
|
||||
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
|
||||
// fall through to the default strategy below.
|
||||
if (isDOMParserAvailable()) {
|
||||
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
|
||||
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
|
||||
}
|
||||
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
|
||||
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
|
||||
}
|
||||
|
||||
export interface InertBodyHelper {
|
||||
/**
|
||||
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
||||
* The implementation of this is determined in the constructor, when the class is instantiated.
|
||||
*/
|
||||
getInertBodyElement: (html: string) => HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use XHR to create and fill an inert body element (on Safari 10.1)
|
||||
* See
|
||||
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
*/
|
||||
private getInertBodyElement_XHR(html: string) {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(undefined);
|
||||
const body: HTMLBodyElement = xhr.response.body;
|
||||
body.removeChild(body.firstChild!);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use DOMParser to create and fill an inert body element (on Firefox)
|
||||
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
*
|
||||
*/
|
||||
private getInertBodyElement_DOMParser(html: string) {
|
||||
/**
|
||||
* Uses DOMParser to create and fill an inert body element.
|
||||
* This is the default strategy used in browsers that support it.
|
||||
*/
|
||||
class DOMParserHelper implements InertBodyHelper {
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
@ -103,14 +43,30 @@ export class InertBodyHelper {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the default sane strategy to use if the browser does not require one of the specialised
|
||||
* strategies above.
|
||||
*/
|
||||
private getInertBodyElement_InertDocument(html: string) {
|
||||
/**
|
||||
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
||||
* `createHtmlDocument` to create and fill an inert DOM element.
|
||||
* This is the fallback strategy if the browser does not support DOMParser.
|
||||
*/
|
||||
class InertDocumentHelper implements InertBodyHelper {
|
||||
private inertDocument: Document;
|
||||
|
||||
constructor(private defaultDoc: Document) {
|
||||
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
||||
|
||||
if (this.inertDocument.body == null) {
|
||||
// usually there should be only one body element in the document, but IE doesn't have any, so
|
||||
// we need to create one.
|
||||
const inertHtml = this.inertDocument.createElement('html');
|
||||
this.inertDocument.appendChild(inertHtml);
|
||||
const inertBodyElement = this.inertDocument.createElement('body');
|
||||
inertHtml.appendChild(inertBodyElement);
|
||||
}
|
||||
}
|
||||
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// Prefer using <template> element if supported.
|
||||
const templateEl = this.inertDocument.createElement('template');
|
||||
if ('content' in templateEl) {
|
||||
@ -164,15 +120,15 @@ export class InertBodyHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
* We need to determine whether the DOMParser exists in the global context and
|
||||
* supports parsing HTML; HTML parsing support is not as wide as other formats, see
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
export function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
|
||||
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Injectable, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, ViewRef, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
|
||||
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
|
||||
import {ViewRef} from '@angular/core/src/render3/view_ref';
|
||||
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
@ -1627,7 +1627,8 @@ describe('di', () => {
|
||||
TestBed.configureTestingModule({declarations: [MyApp, MyPipe], imports: [CommonModule]});
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
fixture.detectChanges();
|
||||
expect((pipeInstance!.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
|
||||
expect((pipeInstance!.cdr as ViewRefInternal<MyApp>).context)
|
||||
.toBe(fixture.componentInstance);
|
||||
});
|
||||
|
||||
it('should inject current component ChangeDetectorRef into directives on the same node as components',
|
||||
@ -1643,7 +1644,7 @@ describe('di', () => {
|
||||
fixture.detectChanges();
|
||||
const app = fixture.componentInstance;
|
||||
const comp = fixture.componentInstance.component;
|
||||
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
|
||||
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
|
||||
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||
expect(app.directive.value).toContain('ViewRef');
|
||||
|
||||
@ -1664,7 +1665,7 @@ describe('di', () => {
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
fixture.detectChanges();
|
||||
const comp = fixture.componentInstance;
|
||||
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
|
||||
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
|
||||
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||
expect(comp.directive.value).toContain('ViewRef');
|
||||
|
||||
@ -1692,7 +1693,7 @@ describe('di', () => {
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
fixture.detectChanges();
|
||||
const app = fixture.componentInstance;
|
||||
expect((app!.cdr as ViewRef<MyApp>).context).toBe(app);
|
||||
expect((app!.cdr as ViewRefInternal<MyApp>).context).toBe(app);
|
||||
const comp = fixture.componentInstance.component;
|
||||
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||
expect(app.directive.value).toContain('ViewRef');
|
||||
@ -1720,7 +1721,7 @@ describe('di', () => {
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
fixture.detectChanges();
|
||||
const comp = fixture.componentInstance;
|
||||
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
|
||||
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
|
||||
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||
expect(comp.directive.value).toContain('ViewRef');
|
||||
|
||||
@ -1743,7 +1744,7 @@ describe('di', () => {
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
fixture.detectChanges();
|
||||
const comp = fixture.componentInstance;
|
||||
expect((comp!.cdr as ViewRef<MyComp>).context).toBe(comp);
|
||||
expect((comp!.cdr as ViewRefInternal<MyComp>).context).toBe(comp);
|
||||
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
||||
expect(comp.directive.value).toContain('ViewRef');
|
||||
|
||||
@ -1773,7 +1774,8 @@ describe('di', () => {
|
||||
TestBed.configureTestingModule({declarations: [MyApp, MyDirective]});
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
fixture.detectChanges();
|
||||
expect((dirInstance!.cdr as ViewRef<MyApp>).context).toBe(fixture.componentInstance);
|
||||
expect((dirInstance!.cdr as ViewRefInternal<MyApp>).context)
|
||||
.toBe(fixture.componentInstance);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2300,4 +2302,14 @@ describe('di', () => {
|
||||
expect(fixture.componentInstance.dir.token).toBe('parent');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be able to inject ViewRef', () => {
|
||||
@Component({template: ''})
|
||||
class App {
|
||||
constructor(_viewRef: ViewRef) {}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App]});
|
||||
expect(() => TestBed.createComponent(App)).toThrowError(/NullInjectorError/);
|
||||
});
|
||||
});
|
||||
|
@ -627,6 +627,24 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||
expect(element.textContent).toContain('ICU start --> Autre <-- ICU end');
|
||||
});
|
||||
|
||||
it('when `select` or `plural` keywords have spaces after them', () => {
|
||||
loadTranslations({
|
||||
[computeMsgId('{VAR_SELECT , select , 10 {ten} 20 {twenty} other {other}}')]:
|
||||
'{VAR_SELECT , select , 10 {dix} 20 {vingt} other {autre}}',
|
||||
[computeMsgId('{VAR_PLURAL , plural , =0 {zero} =1 {one} other {other}}')]:
|
||||
'{VAR_PLURAL , plural , =0 {zéro} =1 {une} other {autre}}'
|
||||
});
|
||||
const fixture = initWithTemplate(AppComp, `
|
||||
<div i18n>
|
||||
{count, select , 10 {ten} 20 {twenty} other {other}} -
|
||||
{count, plural , =0 {zero} =1 {one} other {other}}
|
||||
</div>
|
||||
`);
|
||||
|
||||
const element = fixture.nativeElement;
|
||||
expect(element.textContent).toContain('autre - zéro');
|
||||
});
|
||||
|
||||
it('with no root node and text and DOM nodes surrounding ICU', () => {
|
||||
loadTranslations({
|
||||
[computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]:
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core';
|
||||
import {ApplicationRef, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core';
|
||||
import {InternalViewRef} from '@angular/core/src/linker/view_ref';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
@ -54,4 +54,22 @@ describe('ViewRef', () => {
|
||||
fixture.detectChanges();
|
||||
expect(document.body.querySelector('dynamic-cpt')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should invoke the onDestroy callback of a view ref', () => {
|
||||
let called = false;
|
||||
|
||||
@Component({template: ''})
|
||||
class App {
|
||||
constructor(changeDetectorRef: ChangeDetectorRef) {
|
||||
(changeDetectorRef as InternalViewRef).onDestroy(() => called = true);
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
fixture.destroy();
|
||||
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -1164,7 +1164,7 @@
|
||||
"name": "shouldSearchParent"
|
||||
},
|
||||
{
|
||||
"name": "storeCleanupFn"
|
||||
"name": "storeCleanupWithContext"
|
||||
},
|
||||
{
|
||||
"name": "stringify"
|
||||
|
@ -9,6 +9,7 @@
|
||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||
|
||||
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
|
||||
import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
|
||||
{
|
||||
describe('HTML sanitizer', () => {
|
||||
@ -229,18 +230,3 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to determine whether the DOMParser exists in the global context.
|
||||
* The try-catch is because, on some browsers, trying to access this property
|
||||
* on window can actually throw an error.
|
||||
*
|
||||
* @suppress {uselessCode}
|
||||
*/
|
||||
function isDOMParserAvailable() {
|
||||
try {
|
||||
return !!(window as any).DOMParser;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ let _fakeAsyncTestZoneSpec: any = null;
|
||||
* @publicApi
|
||||
*/
|
||||
export function resetFakeAsyncZoneFallback() {
|
||||
if (_fakeAsyncTestZoneSpec) {
|
||||
_fakeAsyncTestZoneSpec.unlockDatePatch();
|
||||
}
|
||||
_fakeAsyncTestZoneSpec = null;
|
||||
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
|
||||
ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
|
||||
@ -73,6 +76,7 @@ export function fakeAsyncFallback(fn: Function): (...args: any[]) => any {
|
||||
let res: any;
|
||||
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
|
||||
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
|
||||
_fakeAsyncTestZoneSpec.lockDatePatch();
|
||||
try {
|
||||
res = fn.apply(this, args);
|
||||
flushMicrotasksFallback();
|
||||
|
@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
import {merge, Observable} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {merge, Observable, ReplaySubject} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
||||
import {extractProjectableNodes} from './extract-projectable-nodes';
|
||||
@ -43,9 +43,11 @@ export class ComponentNgElementStrategyFactory implements NgElementStrategyFacto
|
||||
* @publicApi
|
||||
*/
|
||||
export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
// Subject of `NgElementStrategyEvent` observables corresponding to the component's outputs.
|
||||
private eventEmitters = new ReplaySubject<Observable<NgElementStrategyEvent>[]>(1);
|
||||
|
||||
/** Merged stream of the component's output events. */
|
||||
// TODO(issue/24571): remove '!'.
|
||||
events!: Observable<NgElementStrategyEvent>;
|
||||
readonly events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters)));
|
||||
|
||||
/** Reference to the component that was created on connect. */
|
||||
private componentRef: ComponentRef<any>|null = null;
|
||||
@ -187,12 +189,13 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
|
||||
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
|
||||
protected initializeOutputs(componentRef: ComponentRef<any>): void {
|
||||
const eventEmitters = this.componentFactory.outputs.map(({propName, templateName}) => {
|
||||
const emitter: EventEmitter<any> = componentRef.instance[propName];
|
||||
return emitter.pipe(map(value => ({name: templateName, value})));
|
||||
});
|
||||
const eventEmitters: Observable<NgElementStrategyEvent>[] =
|
||||
this.componentFactory.outputs.map(({propName, templateName}) => {
|
||||
const emitter: EventEmitter<any> = componentRef.instance[propName];
|
||||
return emitter.pipe(map(value => ({name: templateName, value})));
|
||||
});
|
||||
|
||||
this.events = merge(...eventEmitters);
|
||||
this.eventEmitters.next(eventEmitters);
|
||||
}
|
||||
|
||||
/** Calls ngOnChanges with all the inputs that have changed since the last call. */
|
||||
|
@ -187,13 +187,30 @@ export function createCustomElement<P>(
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
// For historical reasons, some strategies may not have initialized the `events` property
|
||||
// until after `connect()` is run. Subscribe to `events` if it is available before running
|
||||
// `connect()` (in order to capture events emitted suring inittialization), otherwise
|
||||
// subscribe afterwards.
|
||||
//
|
||||
// TODO: Consider deprecating/removing the post-connect subscription in a future major version
|
||||
// (e.g. v11).
|
||||
|
||||
let subscribedToEvents = false;
|
||||
|
||||
if (this.ngElementStrategy.events) {
|
||||
// `events` are already available: Subscribe to it asap.
|
||||
this.subscribeToEvents();
|
||||
subscribedToEvents = true;
|
||||
}
|
||||
|
||||
this.ngElementStrategy.connect(this);
|
||||
|
||||
// Listen for events from the strategy and dispatch them as custom events
|
||||
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
|
||||
const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value);
|
||||
this.dispatchEvent(customEvent);
|
||||
});
|
||||
if (!subscribedToEvents) {
|
||||
// `events` were not initialized before running `connect()`: Subscribe to them now.
|
||||
// The events emitted during the component initialization have been missed, but at least
|
||||
// future events will be captured.
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
@ -207,6 +224,14 @@ export function createCustomElement<P>(
|
||||
this.ngElementEventsSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToEvents(): void {
|
||||
// Listen for events from the strategy and dispatch them as custom events.
|
||||
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
|
||||
const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value);
|
||||
this.dispatchEvent(customEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TypeScript 3.9+ defines getters/setters as configurable but non-enumerable properties (in
|
||||
|
@ -41,6 +41,33 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||
expect(strategyFactory.create(injector)).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('before connected', () => {
|
||||
it('should allow subscribing to output events', () => {
|
||||
const events: NgElementStrategyEvent[] = [];
|
||||
strategy.events.subscribe(e => events.push(e));
|
||||
|
||||
// No events before connecting (since `componentRef` is not even on the strategy yet).
|
||||
componentRef.instance.output1.next('output-1a');
|
||||
componentRef.instance.output1.next('output-1b');
|
||||
componentRef.instance.output2.next('output-2a');
|
||||
expect(events).toEqual([]);
|
||||
|
||||
// No events upon connecting (since events are not cached/played back).
|
||||
strategy.connect(document.createElement('div'));
|
||||
expect(events).toEqual([]);
|
||||
|
||||
// Events emitted once connected.
|
||||
componentRef.instance.output1.next('output-1c');
|
||||
componentRef.instance.output1.next('output-1d');
|
||||
componentRef.instance.output2.next('output-2b');
|
||||
expect(events).toEqual([
|
||||
{name: 'templateOutput1', value: 'output-1c'},
|
||||
{name: 'templateOutput1', value: 'output-1d'},
|
||||
{name: 'templateOutput2', value: 'output-2b'},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after connected', () => {
|
||||
beforeEach(() => {
|
||||
// Set up an initial value to make sure it is passed to the component
|
||||
|
@ -40,12 +40,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
strategyFactory = new TestStrategyFactory();
|
||||
strategy = strategyFactory.testStrategy;
|
||||
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
NgElementCtor = ElementCtor;
|
||||
|
||||
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
|
||||
// new instances of the NgElement which extends HTMLElement, as long as we define it.
|
||||
customElements.define(selector, NgElementCtor);
|
||||
NgElementCtor = createAndRegisterTestCustomElement(strategyFactory);
|
||||
})
|
||||
.then(done, done.fail);
|
||||
});
|
||||
@ -117,6 +112,47 @@ if (browserDetection.supportsCustomElements) {
|
||||
expect(eventValue).toEqual(null);
|
||||
});
|
||||
|
||||
it('should listen to output events during initialization', () => {
|
||||
const events: string[] = [];
|
||||
|
||||
const element = new NgElementCtor(injector);
|
||||
element.addEventListener('strategy-event', evt => events.push((evt as CustomEvent).detail));
|
||||
element.connectedCallback();
|
||||
|
||||
expect(events).toEqual(['connect']);
|
||||
});
|
||||
|
||||
it('should not break if `NgElementStrategy#events` is not available before calling `NgElementStrategy#connect()`',
|
||||
() => {
|
||||
class TestStrategyWithLateEvents extends TestStrategy {
|
||||
events: Subject<NgElementStrategyEvent> = undefined!;
|
||||
|
||||
connect(element: HTMLElement): void {
|
||||
this.connectedElement = element;
|
||||
this.events = new Subject<NgElementStrategyEvent>();
|
||||
this.events.next({name: 'strategy-event', value: 'connect'});
|
||||
}
|
||||
}
|
||||
|
||||
const strategyWithLateEvents = new TestStrategyWithLateEvents();
|
||||
const capturedEvents: string[] = [];
|
||||
|
||||
const NgElementCtorWithLateEventsStrategy =
|
||||
createAndRegisterTestCustomElement({create: () => strategyWithLateEvents});
|
||||
|
||||
const element = new NgElementCtorWithLateEventsStrategy(injector);
|
||||
element.addEventListener(
|
||||
'strategy-event', evt => capturedEvents.push((evt as CustomEvent).detail));
|
||||
element.connectedCallback();
|
||||
|
||||
// The "connect" event (emitted during initialization) was missed, but things didn't break.
|
||||
expect(capturedEvents).toEqual([]);
|
||||
|
||||
// Subsequent events are still captured.
|
||||
strategyWithLateEvents.events.next({name: 'strategy-event', value: 'after-connect'});
|
||||
expect(capturedEvents).toEqual(['after-connect']);
|
||||
});
|
||||
|
||||
it('should properly set getters/setters on the element', () => {
|
||||
const element = new NgElementCtor(injector);
|
||||
element.fooFoo = 'foo-foo-value';
|
||||
@ -144,7 +180,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
|
||||
it('should capture properties set before upgrading the element', () => {
|
||||
// Create a regular element and set properties on it.
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
const element = Object.assign(document.createElement(selector), {
|
||||
fooFoo: 'foo-prop-value',
|
||||
barBar: 'bar-prop-value',
|
||||
@ -165,7 +201,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
it('should capture properties set after upgrading the element but before inserting it into the DOM',
|
||||
() => {
|
||||
// Create a regular element and set properties on it.
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
const element = Object.assign(document.createElement(selector), {
|
||||
fooFoo: 'foo-prop-value',
|
||||
barBar: 'bar-prop-value',
|
||||
@ -193,7 +229,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
it('should allow overwriting properties with attributes after upgrading the element but before inserting it into the DOM',
|
||||
() => {
|
||||
// Create a regular element and set properties on it.
|
||||
const {selector, ElementCtor} = createTestCustomElement();
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
const element = Object.assign(document.createElement(selector), {
|
||||
fooFoo: 'foo-prop-value',
|
||||
barBar: 'bar-prop-value',
|
||||
@ -219,7 +255,17 @@ if (browserDetection.supportsCustomElements) {
|
||||
});
|
||||
|
||||
// Helpers
|
||||
function createTestCustomElement() {
|
||||
function createAndRegisterTestCustomElement(strategyFactory: NgElementStrategyFactory) {
|
||||
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
|
||||
|
||||
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
|
||||
// new instances of the NgElement which extends HTMLElement, as long as we define it.
|
||||
customElements.define(selector, ElementCtor);
|
||||
|
||||
return ElementCtor;
|
||||
}
|
||||
|
||||
function createTestCustomElement(strategyFactory: NgElementStrategyFactory) {
|
||||
return {
|
||||
selector: `test-element-${++selectorUid}`,
|
||||
ElementCtor: createCustomElement<WithFooBar>(TestComponent, {injector, strategyFactory}),
|
||||
@ -255,6 +301,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
events = new Subject<NgElementStrategyEvent>();
|
||||
|
||||
connect(element: HTMLElement): void {
|
||||
this.events.next({name: 'strategy-event', value: 'connect'});
|
||||
this.connectedElement = element;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding} from '@angular/compiler';
|
||||
import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, Visitor} from '@angular/compiler';
|
||||
import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
||||
|
||||
import {ATTR, getBindingDescriptor} from './binding_utils';
|
||||
@ -127,72 +127,18 @@ function getBoundedWordSpan(
|
||||
|
||||
export function getTemplateCompletions(
|
||||
templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
|
||||
let result: ng.CompletionEntry[] = [];
|
||||
const {htmlAst, template} = templateInfo;
|
||||
// The templateNode starts at the delimiter character so we add 1 to skip it.
|
||||
// Calculate the position relative to the start of the template. This is needed
|
||||
// because spans in HTML AST are relative. Inline template has non-zero start position.
|
||||
const templatePosition = position - template.span.start;
|
||||
const path = getPathToNodeAtPosition(htmlAst, templatePosition);
|
||||
const mostSpecific = path.tail;
|
||||
if (path.empty || !mostSpecific) {
|
||||
result = elementCompletions(templateInfo);
|
||||
} else {
|
||||
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
|
||||
mostSpecific.visit(
|
||||
{
|
||||
visitElement(ast) {
|
||||
const startTagSpan = spanOf(ast.sourceSpan);
|
||||
const tagLen = ast.name.length;
|
||||
// + 1 for the opening angle bracket
|
||||
if (templatePosition <= startTagSpan.start + tagLen + 1) {
|
||||
// If we are in the tag then return the element completions.
|
||||
result = elementCompletions(templateInfo);
|
||||
} else if (templatePosition < startTagSpan.end) {
|
||||
// We are in the attribute section of the element (but not in an attribute).
|
||||
// Return the attribute completions.
|
||||
result = attributeCompletionsForElement(templateInfo, ast.name);
|
||||
}
|
||||
},
|
||||
visitAttribute(ast: Attribute) {
|
||||
// An attribute consists of two parts, LHS="RHS".
|
||||
// Determine if completions are requested for LHS or RHS
|
||||
if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
|
||||
// RHS completion
|
||||
result = attributeValueCompletions(templateInfo, path);
|
||||
} else {
|
||||
// LHS completion
|
||||
result = attributeCompletions(templateInfo, path);
|
||||
}
|
||||
},
|
||||
visitText(ast) {
|
||||
result = interpolationCompletions(templateInfo, templatePosition);
|
||||
if (result.length) return result;
|
||||
const element = path.first(Element);
|
||||
if (element) {
|
||||
const definition = getHtmlTagDefinition(element.name);
|
||||
if (definition.contentType === TagContentType.PARSABLE_DATA) {
|
||||
result = voidElementAttributeCompletions(templateInfo, path);
|
||||
if (!result.length) {
|
||||
// If the element can hold content, show element completions.
|
||||
result = elementCompletions(templateInfo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no element container, implies parsable data so show elements.
|
||||
result = voidElementAttributeCompletions(templateInfo, path);
|
||||
if (!result.length) {
|
||||
result = elementCompletions(templateInfo);
|
||||
}
|
||||
}
|
||||
},
|
||||
visitComment() {},
|
||||
visitExpansion() {},
|
||||
visitExpansionCase() {}
|
||||
},
|
||||
null);
|
||||
}
|
||||
|
||||
const htmlPath: HtmlAstPath = getPathToNodeAtPosition(htmlAst, templatePosition);
|
||||
const mostSpecific = htmlPath.tail;
|
||||
const visitor = new HtmlVisitor(templateInfo, htmlPath);
|
||||
const results: ng.CompletionEntry[] = mostSpecific ?
|
||||
mostSpecific.visit(visitor, null /* context */) :
|
||||
elementCompletions(templateInfo);
|
||||
const replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
|
||||
return result.map(entry => {
|
||||
return results.map(entry => {
|
||||
return {
|
||||
...entry,
|
||||
replacementSpan,
|
||||
@ -200,6 +146,78 @@ export function getTemplateCompletions(
|
||||
});
|
||||
}
|
||||
|
||||
class HtmlVisitor implements Visitor {
|
||||
/**
|
||||
* Position relative to the start of the template.
|
||||
*/
|
||||
private readonly relativePosition: number;
|
||||
constructor(private readonly templateInfo: ng.AstResult, private readonly htmlPath: HtmlAstPath) {
|
||||
this.relativePosition = htmlPath.position;
|
||||
}
|
||||
// Note that every visitor method must explicitly specify return type because
|
||||
// Visitor returns `any` for all methods.
|
||||
visitElement(ast: Element): ng.CompletionEntry[] {
|
||||
const startTagSpan = spanOf(ast.sourceSpan);
|
||||
const tagLen = ast.name.length;
|
||||
// + 1 for the opening angle bracket
|
||||
if (this.relativePosition <= startTagSpan.start + tagLen + 1) {
|
||||
// If we are in the tag then return the element completions.
|
||||
return elementCompletions(this.templateInfo);
|
||||
}
|
||||
if (this.relativePosition < startTagSpan.end) {
|
||||
// We are in the attribute section of the element (but not in an attribute).
|
||||
// Return the attribute completions.
|
||||
return attributeCompletionsForElement(this.templateInfo, ast.name);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
visitAttribute(ast: Attribute): ng.CompletionEntry[] {
|
||||
// An attribute consists of two parts, LHS="RHS".
|
||||
// Determine if completions are requested for LHS or RHS
|
||||
if (ast.valueSpan && inSpan(this.relativePosition, spanOf(ast.valueSpan))) {
|
||||
// RHS completion
|
||||
return attributeValueCompletions(this.templateInfo, this.htmlPath);
|
||||
}
|
||||
// LHS completion
|
||||
return attributeCompletions(this.templateInfo, this.htmlPath);
|
||||
}
|
||||
visitText(): ng.CompletionEntry[] {
|
||||
const templatePath = findTemplateAstAt(this.templateInfo.templateAst, this.relativePosition);
|
||||
if (templatePath.tail instanceof BoundTextAst) {
|
||||
// If we know that this is an interpolation then do not try other scenarios.
|
||||
const visitor = new ExpressionVisitor(
|
||||
this.templateInfo, this.relativePosition,
|
||||
() =>
|
||||
getExpressionScope(diagnosticInfoFromTemplateInfo(this.templateInfo), templatePath));
|
||||
templatePath.tail?.visit(visitor, null);
|
||||
return visitor.results;
|
||||
}
|
||||
// TODO(kyliau): Not sure if this check is really needed since we don't have
|
||||
// any test cases for it.
|
||||
const element = this.htmlPath.first(Element);
|
||||
if (element &&
|
||||
getHtmlTagDefinition(element.name).contentType !== TagContentType.PARSABLE_DATA) {
|
||||
return [];
|
||||
}
|
||||
// This is to account for cases like <h1> <a> text | </h1> where the
|
||||
// closest element has no closing tag and thus is considered plain text.
|
||||
const results = voidElementAttributeCompletions(this.templateInfo, this.htmlPath);
|
||||
if (results.length) {
|
||||
return results;
|
||||
}
|
||||
return elementCompletions(this.templateInfo);
|
||||
}
|
||||
visitComment(): ng.CompletionEntry[] {
|
||||
return [];
|
||||
}
|
||||
visitExpansion(): ng.CompletionEntry[] {
|
||||
return [];
|
||||
}
|
||||
visitExpansionCase(): ng.CompletionEntry[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function attributeCompletions(info: ng.AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
||||
const attr = path.tail;
|
||||
const elem = path.parentOf(attr);
|
||||
@ -356,18 +374,6 @@ function elementCompletions(info: ng.AstResult): ng.CompletionEntry[] {
|
||||
return results;
|
||||
}
|
||||
|
||||
function interpolationCompletions(info: ng.AstResult, position: number): ng.CompletionEntry[] {
|
||||
// Look for an interpolation in at the position.
|
||||
const templatePath = findTemplateAstAt(info.templateAst, position);
|
||||
if (!templatePath.tail) {
|
||||
return [];
|
||||
}
|
||||
const visitor = new ExpressionVisitor(
|
||||
info, position, () => getExpressionScope(diagnosticInfoFromTemplateInfo(info), templatePath));
|
||||
templatePath.tail.visit(visitor, null);
|
||||
return visitor.results;
|
||||
}
|
||||
|
||||
// There is a special case of HTML where text that contains a unclosed tag is treated as
|
||||
// text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
|
||||
// element "Some <a text". We, however, want to treat this as if the user was requesting
|
||||
|
@ -841,6 +841,13 @@ describe('completions', () => {
|
||||
'trim',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not return any results for unknown symbol', () => {
|
||||
mockHost.override(TEST_TEMPLATE, '{{ doesnotexist.~{cursor} }}');
|
||||
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'cursor');
|
||||
const completions = ngLS.getCompletionsAtPosition(TEST_TEMPLATE, marker.start);
|
||||
expect(completions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function expectContain(
|
||||
|
@ -6,6 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const {readFileSync} = require('fs');
|
||||
const {bold, yellow} = require('chalk');
|
||||
|
||||
module.exports = (gulp) => () => {
|
||||
const conventionalChangelog = require('gulp-conventional-changelog');
|
||||
const ignoredScopes = [
|
||||
@ -16,11 +19,55 @@ module.exports = (gulp) => () => {
|
||||
];
|
||||
|
||||
return gulp.src('CHANGELOG.md')
|
||||
.pipe(conventionalChangelog({preset: 'angular'}, {}, {
|
||||
// Ignore commits that start with `<type>(<scope>)` for any of the ignored scopes.
|
||||
extendedRegexp: true,
|
||||
grep: `^[^(]+\\((${ignoredScopes.join('|')})\\)`,
|
||||
invertGrep: true,
|
||||
}))
|
||||
.pipe(conventionalChangelog(
|
||||
/* core options */ {preset: 'angular'},
|
||||
/* context options */ {},
|
||||
/* raw-commit options */ {
|
||||
// Ignore commits that start with `<type>(<scope>)` for any of the ignored scopes.
|
||||
extendedRegexp: true,
|
||||
grep: `^[^(]+\\((${ignoredScopes.join('|')})\\)`,
|
||||
invertGrep: true,
|
||||
},
|
||||
/* commit parser options */ null,
|
||||
/* writer options*/ createDedupeWriterOptions()))
|
||||
.pipe(gulp.dest('./'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates changelog writer options which ensure that commits are not showing up multiple times.
|
||||
* Commits can show up multiple times if a changelog has been generated on a publish branch
|
||||
* and has been cherry-picked into "master". In that case, the changelog will already contain
|
||||
* commits from master which might be added to the changelog again. This is because usually
|
||||
* patch and minor releases are tagged from the publish branches and therefore
|
||||
* conventional-changelog tries to build the changelog from last minor version to HEAD when a
|
||||
* new minor version is being published from the "master" branch. We naively match commit
|
||||
* headers as otherwise we would need to query Git and diff commits between a given patch branch.
|
||||
* The commit header is reliable enough as it contains a direct reference to the source PR.
|
||||
*/
|
||||
function createDedupeWriterOptions() {
|
||||
const existingChangelogContent = readFileSync('CHANGELOG.md', 'utf8');
|
||||
|
||||
return {
|
||||
// Specify a writer option that can be used to modify the content of a new changelog section.
|
||||
// See: conventional-changelog/tree/master/packages/conventional-changelog-writer
|
||||
finalizeContext: (context) => {
|
||||
context.commitGroups = context.commitGroups.filter((group) => {
|
||||
group.commits = group.commits.filter((commit) => {
|
||||
// NOTE: We cannot compare the SHAs because the commits will have a different SHA
|
||||
// if they are being cherry-picked into a different branch.
|
||||
if (existingChangelogContent.includes(commit.subject)) {
|
||||
console.info(yellow(` ↺ Skipping duplicate: "${bold(commit.header)}"`));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Filter out commit groups which don't have any commits. Commit groups will become
|
||||
// empty if we filter out all duplicated commits.
|
||||
return group.commits.length !== 0;
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user