Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
e1071615c6 | |||
8bd5374cfd | |||
b9b9cc2ba8 | |||
a6e10ef869 | |||
9724169bf4 | |||
c0ed57db76 | |||
0bd50e2e50 | |||
0ceb27041f | |||
ec2affe104 | |||
c590e8ca7a | |||
254b9ea44c | |||
2a53f47159 | |||
722d9397b0 | |||
03de31a78e | |||
b22c5a953d | |||
24222e0c1f | |||
95f45e8070 | |||
18be33a9d1 | |||
a22d4f6c98 | |||
5ae8473c6b | |||
fd7c39e3cf | |||
d85d91df66 | |||
15930d21c7 | |||
61a7f98b98 | |||
c3c7bf6509 | |||
b2e7ce47ec | |||
94e518e3c7 | |||
0fa5ac8d0d | |||
f2fca3e243 | |||
5bab49828d | |||
db4e93d0ca | |||
479a59be43 | |||
52aab63dd9 | |||
506beeddc1 | |||
0075078179 | |||
bb7edc52aa | |||
ed2b0e945e | |||
da159bde83 | |||
06a9809e32 | |||
1e4fb74ec8 | |||
797c306306 | |||
972fc06135 | |||
a9117061d0 | |||
fe1d9bacc3 | |||
08b8b51486 | |||
1d4af3f734 | |||
609d81c65e | |||
af30efddc5 | |||
15115f6179 | |||
eec9b6bbb5 | |||
45fd77ead1 | |||
f16587e9b7 | |||
4f9991534e | |||
51a0ed2222 | |||
a5ea100e7c | |||
0429c7f5e9 | |||
1756cced4a |
@ -236,7 +236,7 @@ jobs:
|
||||
git config user.name "angular-ci"
|
||||
git config user.email "angular-ci"
|
||||
# Rebase PR on top of target branch.
|
||||
node tools/rebase-pr.js angular/angular ${CIRCLE_PR_NUMBER}
|
||||
node tools/rebase-pr.js
|
||||
else
|
||||
echo "This build is not over a PR, nothing to do."
|
||||
fi
|
||||
|
@ -22,6 +22,7 @@ else
|
||||
####################################################################################################
|
||||
# See https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables for more info.
|
||||
####################################################################################################
|
||||
setPublicVar CI "$CI"
|
||||
setPublicVar PROJECT_ROOT "$projectDir";
|
||||
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
||||
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
||||
@ -36,9 +37,8 @@ else
|
||||
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
||||
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
||||
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
||||
|
||||
# Store a PR's refs and shas so they don't need to be requested multiple times.
|
||||
setPublicVar GITHUB_REFS_AND_SHAS $(node tools/utils/get-refs-and-shas-for-target.js ${CIRCLE_PR_NUMBER:-false} | awk '{ gsub(/"/,"\\\"") } 1');
|
||||
setPublicVar CI_PR_REPONAME "$CIRCLE_PR_REPONAME";
|
||||
setPublicVar CI_PR_USERNAME "$CIRCLE_PR_USERNAME";
|
||||
|
||||
|
||||
####################################################################################################
|
||||
@ -82,7 +82,7 @@ else
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "master"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "598db096e668aa7e9debd56eedfd127b7a55e371"
|
||||
|
||||
|
||||
# Save the created BASH_ENV into the bash env cache file.
|
||||
cat "$BASH_ENV" >> $bashEnvCachePath;
|
||||
fi
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,3 +1,22 @@
|
||||
<a name="9.1.3"></a>
|
||||
## [9.1.3](https://github.com/angular/angular/compare/9.1.2...9.1.3) (2020-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** avoid generating i18n attributes in plain form ([#36422](https://github.com/angular/angular/issues/36422)) ([08b8b51](https://github.com/angular/angular/commit/08b8b51))
|
||||
* **core:** do not use unbound attributes as inputs to structural directives ([#36441](https://github.com/angular/angular/issues/36441)) ([c0ed57d](https://github.com/angular/angular/commit/c0ed57d))
|
||||
* **core:** handle empty translations correctly ([#36499](https://github.com/angular/angular/issues/36499)) ([a5ea100](https://github.com/angular/angular/commit/a5ea100)), closes [#36476](https://github.com/angular/angular/issues/36476)
|
||||
* **core:** missing-injectable migration should not migrate `@NgModule` classes ([#36369](https://github.com/angular/angular/issues/36369)) ([0bd50e2](https://github.com/angular/angular/commit/0bd50e2)), closes [#35700](https://github.com/angular/angular/issues/35700)
|
||||
* **core:** pipes injecting viewProviders when used on a component host node ([#36512](https://github.com/angular/angular/issues/36512)) ([5ae8473](https://github.com/angular/angular/commit/5ae8473)), closes [#36146](https://github.com/angular/angular/issues/36146)
|
||||
* **core:** prevent unknown property check for AOT-compiled components ([#36072](https://github.com/angular/angular/issues/36072)) ([fe1d9ba](https://github.com/angular/angular/commit/fe1d9ba)), closes [#35945](https://github.com/angular/angular/issues/35945)
|
||||
* **core:** properly identify modules affected by overrides in TestBed ([#36649](https://github.com/angular/angular/issues/36649)) ([9724169](https://github.com/angular/angular/commit/9724169)), closes [#36619](https://github.com/angular/angular/issues/36619)
|
||||
* **language-service:** properly evaluate types in comparable expressions ([#36529](https://github.com/angular/angular/issues/36529)) ([5bab498](https://github.com/angular/angular/commit/5bab498))
|
||||
* **ngcc:** display unlocker process output in sync mode ([#36637](https://github.com/angular/angular/issues/36637)) ([da159bd](https://github.com/angular/angular/commit/da159bd)), closes [/github.com/nodejs/node/issues/3596#issuecomment-250890218](https://github.com//github.com/nodejs/node/issues/3596/issues/issuecomment-250890218)
|
||||
* **ngcc:** do not use cached file-system ([#36687](https://github.com/angular/angular/issues/36687)) ([18be33a](https://github.com/angular/angular/commit/18be33a)), closes [/github.com/angular/angular-cli/issues/16860#issuecomment-614694269](https://github.com//github.com/angular/angular-cli/issues/16860/issues/issuecomment-614694269)
|
||||
|
||||
|
||||
|
||||
<a name="9.1.2"></a>
|
||||
## [9.1.2](https://github.com/angular/angular/compare/9.1.1...9.1.2) (2020-04-15)
|
||||
|
||||
|
5
aio/content/examples/.gitignore
vendored
5
aio/content/examples/.gitignore
vendored
@ -82,9 +82,6 @@ upgrade-phonecat-2-hybrid/aot/**/*
|
||||
# styleguide
|
||||
!styleguide/src/systemjs.custom.js
|
||||
|
||||
# universal
|
||||
!universal/webpack.server.config.js
|
||||
|
||||
# stackblitz
|
||||
*stackblitz.no-link.html
|
||||
|
||||
@ -97,4 +94,4 @@ upgrade-phonecat-3-final/rollup-config.js
|
||||
!upgrade-phonecat-*/**/karma-test-shim.js
|
||||
|
||||
# schematics
|
||||
!schematics-for-libraries/projects/my-lib/package.json
|
||||
!schematics-for-libraries/projects/my-lib/package.json
|
||||
|
11
aio/content/examples/router/src/app/app-routing.module.7.ts
Normal file
11
aio/content/examples/router/src/app/app-routing.module.7.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router'; // CLI imports router
|
||||
|
||||
const routes: Routes = []; // sets up routes constant where you define your routes
|
||||
|
||||
// configures NgModule imports and exports
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
26
aio/content/examples/router/src/app/app-routing.module.8.ts
Normal file
26
aio/content/examples/router/src/app/app-routing.module.8.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// #docplaster
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router'; // CLI imports router
|
||||
|
||||
// #docregion routes, routes-with-wildcard, redirect
|
||||
const routes: Routes = [
|
||||
{ path: 'first-component', component: FirstComponent },
|
||||
{ path: 'second-component', component: SecondComponent },
|
||||
// #enddocregion routes
|
||||
{ path: '', redirectTo: '/first-component', pathMatch: 'full' }, // redirect to `first-component`
|
||||
{ path: '**', component: FirstComponent },
|
||||
// #enddocregion redirect
|
||||
{ path: '**', component: PageNotFoundComponent }, // Wildcard route for a 404 page
|
||||
// #docregion routes
|
||||
// #docregion redirect
|
||||
];
|
||||
// #enddocregion routes, routes-with-wildcard, redirect
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
|
28
aio/content/examples/router/src/app/app-routing.module.9.ts
Normal file
28
aio/content/examples/router/src/app/app-routing.module.9.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// #docplaster
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router'; // CLI imports router
|
||||
|
||||
// #docregion child-routes
|
||||
const routes: Routes = [
|
||||
{ path: 'first-component',
|
||||
component: FirstComponent, // this is the component with the <router-outlet> in the template
|
||||
children: [
|
||||
{
|
||||
path: 'child-a', // child route path
|
||||
component: ChildAComponent // child route component that the router renders
|
||||
},
|
||||
{
|
||||
path: 'child-b',
|
||||
component: ChildBComponent // another child route component that the router renders
|
||||
}
|
||||
] },
|
||||
// #enddocregion child-routes
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
|
15
aio/content/examples/router/src/app/app.component.4.ts
Normal file
15
aio/content/examples/router/src/app/app.component.4.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
// #docregion relative-to
|
||||
goToItems() {
|
||||
this.router.navigate(['items'], { relativeTo: this.route });
|
||||
}
|
||||
// #enddocregion relative-to
|
||||
|
||||
}
|
10
aio/content/examples/router/src/app/app.component.7.html
Normal file
10
aio/content/examples/router/src/app/app.component.7.html
Normal file
@ -0,0 +1,10 @@
|
||||
<h1>Angular Router App</h1>
|
||||
<!-- This nav gives you links to click, which tells the router which route to use (defined in the routes constant in AppRoutingModule) -->
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a routerLink="/first-component" routerLinkActive="active">First Component</a></li>
|
||||
<li><a routerLink="/second-component" routerLinkActive="active">Second Component</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- The routed views render in the <router-outlet>-->
|
||||
<router-outlet></router-outlet>
|
26
aio/content/examples/router/src/app/app.component.8.html
Normal file
26
aio/content/examples/router/src/app/app.component.8.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!-- #docregion child-routes-->
|
||||
<h2>First Component</h2>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a routerLink="child-a">Child A</a></li>
|
||||
<li><a routerLink="child-b">Child B</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<!-- #enddocregion child-routes-->
|
||||
|
||||
|
||||
<!-- #docregion relative-route-->
|
||||
|
||||
<h2>First Component</h2>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a routerLink="../second-component">Relative Route to second component</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<!-- #enddocregion relative-route-->
|
17
aio/content/examples/router/src/app/app.module.8.ts
Normal file
17
aio/content/examples/router/src/app/app.module.8.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AppRoutingModule } from './app-routing.module'; // CLI imports AppRoutingModule
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule // CLI adds AppRoutingModule to the AppModule's imports array
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
@ -2,7 +2,9 @@
|
||||
// #docregion
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
// #docregion imports-route-info
|
||||
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||
// #enddocregion imports-route-info
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { HeroService } from '../hero.service';
|
||||
@ -16,11 +18,16 @@ import { Hero } from '../hero';
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
hero$: Observable<Hero>;
|
||||
|
||||
// #docregion activated-route
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
// #enddocregion activated-route
|
||||
private router: Router,
|
||||
private service: HeroService
|
||||
// #docregion activated-route
|
||||
) {}
|
||||
// #enddocregion activated-route
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.hero$ = this.route.paramMap.pipe(
|
||||
|
@ -8,4 +8,6 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
||||
|
@ -9,5 +9,6 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
// #enddocregion
|
||||
|
@ -9,5 +9,6 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
// #enddocregion
|
||||
|
@ -9,5 +9,6 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
// #enddocregion
|
||||
|
@ -8,4 +8,5 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
@ -19,7 +19,6 @@ button {
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
|
@ -8,4 +8,5 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
@ -1,5 +1,5 @@
|
||||
// #docplaster
|
||||
// #docregion, v1
|
||||
// #docregion , v1
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@ -59,6 +59,5 @@ import { MessagesComponent } from './messages/messages.component';
|
||||
// #docregion import-httpclientmodule
|
||||
})
|
||||
// #enddocregion import-httpclientmodule
|
||||
|
||||
export class AppModule { }
|
||||
// #enddocregion , v1
|
||||
|
@ -18,7 +18,7 @@ button {
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer; cursor: hand;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
|
@ -33,10 +33,10 @@ export class HeroDetailComponent implements OnInit {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
// #docregion save
|
||||
save(): void {
|
||||
// #docregion save
|
||||
save(): void {
|
||||
this.heroService.updateHero(this.hero)
|
||||
.subscribe(() => this.goBack());
|
||||
}
|
||||
// #enddocregion save
|
||||
// #enddocregion save
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export class HeroService {
|
||||
// #docregion getHeroes, getHeroes-1
|
||||
/** GET heroes from the server */
|
||||
// #docregion getHeroes-2
|
||||
getHeroes (): Observable<Hero[]> {
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
return this.http.get<Hero[]>(this.heroesUrl)
|
||||
// #enddocregion getHeroes-1
|
||||
.pipe(
|
||||
@ -98,7 +98,7 @@ export class HeroService {
|
||||
|
||||
// #docregion addHero
|
||||
/** POST: add a new hero to the server */
|
||||
addHero (hero: Hero): Observable<Hero> {
|
||||
addHero(hero: Hero): Observable<Hero> {
|
||||
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
|
||||
catchError(this.handleError<Hero>('addHero'))
|
||||
@ -108,7 +108,7 @@ export class HeroService {
|
||||
|
||||
// #docregion deleteHero
|
||||
/** DELETE: delete the hero from the server */
|
||||
deleteHero (hero: Hero | number): Observable<Hero> {
|
||||
deleteHero(hero: Hero | number): Observable<Hero> {
|
||||
const id = typeof hero === 'number' ? hero : hero.id;
|
||||
const url = `${this.heroesUrl}/${id}`;
|
||||
|
||||
@ -121,7 +121,7 @@ export class HeroService {
|
||||
|
||||
// #docregion updateHero
|
||||
/** PUT: update the hero on the server */
|
||||
updateHero (hero: Hero): Observable<any> {
|
||||
updateHero(hero: Hero): Observable<any> {
|
||||
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
||||
catchError(this.handleError<any>('updateHero'))
|
||||
@ -136,7 +136,7 @@ export class HeroService {
|
||||
* @param operation - name of the operation that failed
|
||||
* @param result - optional value to return as the observable result
|
||||
*/
|
||||
private handleError<T> (operation = 'operation', result?: T) {
|
||||
private handleError<T>(operation = 'operation', result?: T) {
|
||||
return (error: any): Observable<T> => {
|
||||
|
||||
// TODO: send the error to remote logging infrastructure
|
||||
|
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.heroes a:hover {
|
||||
color:#607D8B;
|
||||
color: #607D8B;
|
||||
}
|
||||
|
||||
.heroes .badge {
|
||||
@ -38,7 +38,7 @@
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.8em 0.7em 0 0.7em;
|
||||
background-color:#405061;
|
||||
background-color: #405061;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// #docregion , init
|
||||
import { Injectable } from '@angular/core';
|
||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||
import { Hero } from './hero';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -9,4 +9,5 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
300
aio/content/examples/universal/e2e/src/app.e2e-spec.ts
Normal file
300
aio/content/examples/universal/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,300 @@
|
||||
import { browser, by, element, ElementArrayFinder, ElementFinder, logging } from 'protractor';
|
||||
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
// Factory methods
|
||||
|
||||
// Hero from string formatted as '<id> <name>'.
|
||||
static fromString(s: string): Hero {
|
||||
return {
|
||||
id: +s.substr(0, s.indexOf(' ')),
|
||||
name: s.substr(s.indexOf(' ') + 1),
|
||||
};
|
||||
}
|
||||
|
||||
// Hero from hero list <li> element.
|
||||
static async fromLi(li: ElementFinder): Promise<Hero> {
|
||||
const stringsFromA = await li.all(by.css('a')).getText();
|
||||
const strings = stringsFromA[0].split(' ');
|
||||
return { id: +strings[0], name: strings[1] };
|
||||
}
|
||||
|
||||
// Hero id and name from the given detail element.
|
||||
static async fromDetail(detail: ElementFinder): Promise<Hero> {
|
||||
// Get hero id from the first <div>
|
||||
const id = await detail.all(by.css('div')).first().getText();
|
||||
// Get name from the h2
|
||||
const name = await detail.element(by.css('h2')).getText();
|
||||
return {
|
||||
id: +id.substr(id.indexOf(' ') + 1),
|
||||
name: name.substr(0, name.lastIndexOf(' '))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('Universal', () => {
|
||||
const expectedH1 = 'Tour of Heroes';
|
||||
const expectedTitle = `${expectedH1}`;
|
||||
const targetHero = { id: 15, name: 'Magneta' };
|
||||
const targetHeroDashboardIndex = 3;
|
||||
const nameSuffix = 'X';
|
||||
const newHeroName = targetHero.name + nameSuffix;
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
const severeLogs = logs.filter(entry => entry.level === logging.Level.SEVERE);
|
||||
expect(severeLogs).toEqual([]);
|
||||
});
|
||||
|
||||
describe('Initial page', () => {
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it(`has title '${expectedTitle}'`, () => {
|
||||
expect(browser.getTitle()).toEqual(expectedTitle);
|
||||
});
|
||||
|
||||
it(`has h1 '${expectedH1}'`, () => {
|
||||
expectHeading(1, expectedH1);
|
||||
});
|
||||
|
||||
const expectedViewNames = ['Dashboard', 'Heroes'];
|
||||
it(`has views ${expectedViewNames}`, () => {
|
||||
const viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText());
|
||||
expect(viewNames).toEqual(expectedViewNames);
|
||||
});
|
||||
|
||||
it('has dashboard as the active view', () => {
|
||||
const page = getPageElts();
|
||||
expect(page.appDashboard.isPresent()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dashboard tests', () => {
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it('has top heroes', () => {
|
||||
const page = getPageElts();
|
||||
expect(page.topHeroes.count()).toEqual(4);
|
||||
});
|
||||
|
||||
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`cancels and shows ${targetHero.name} in Dashboard`, () => {
|
||||
element(by.buttonText('go back')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||
|
||||
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||
});
|
||||
|
||||
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`saves and shows ${newHeroName} in Dashboard`, () => {
|
||||
element(by.buttonText('save')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||
|
||||
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(newHeroName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Heroes tests', () => {
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it('can switch to Heroes view', () => {
|
||||
getPageElts().appHeroesHref.click();
|
||||
const page = getPageElts();
|
||||
expect(page.appHeroes.isPresent()).toBeTruthy();
|
||||
expect(page.allHeroes.count()).toEqual(10, 'number of heroes');
|
||||
});
|
||||
|
||||
it('can route to hero details', async () => {
|
||||
getHeroLiEltById(targetHero.id).click();
|
||||
|
||||
const page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
const hero = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero.id).toEqual(targetHero.id);
|
||||
expect(hero.name).toEqual(targetHero.name.toUpperCase());
|
||||
});
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`shows ${newHeroName} in Heroes list`, () => {
|
||||
element(by.buttonText('save')).click();
|
||||
browser.waitForAngular();
|
||||
const expectedText = `${targetHero.id} ${newHeroName}`;
|
||||
expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText);
|
||||
});
|
||||
|
||||
it(`deletes ${newHeroName} from Heroes list`, async () => {
|
||||
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||
const li = getHeroLiEltById(targetHero.id);
|
||||
li.element(by.buttonText('x')).click();
|
||||
|
||||
const page = getPageElts();
|
||||
expect(page.appHeroes.isPresent()).toBeTruthy();
|
||||
expect(page.allHeroes.count()).toEqual(9, 'number of heroes');
|
||||
const heroesAfter = await toHeroArray(page.allHeroes);
|
||||
// console.log(await Hero.fromLi(page.allHeroes[0]));
|
||||
const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName);
|
||||
expect(heroesAfter).toEqual(expectedHeroes);
|
||||
// expect(page.selectedHeroSubview.isPresent()).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`adds back ${targetHero.name}`, async () => {
|
||||
const updatedHeroName = 'Alice';
|
||||
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||
const numHeroes = heroesBefore.length;
|
||||
|
||||
element(by.css('input')).sendKeys(updatedHeroName);
|
||||
element(by.buttonText('add')).click();
|
||||
|
||||
const page = getPageElts();
|
||||
const heroesAfter = await toHeroArray(page.allHeroes);
|
||||
expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');
|
||||
|
||||
expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');
|
||||
|
||||
const maxId = heroesBefore[heroesBefore.length - 1].id;
|
||||
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: updatedHeroName});
|
||||
});
|
||||
|
||||
it('displays correctly styled buttons', async () => {
|
||||
element.all(by.buttonText('x')).then(buttons => {
|
||||
for (const button of buttons) {
|
||||
// Inherited styles from styles.css
|
||||
expect(button.getCssValue('font-family')).toBe('Arial');
|
||||
expect(button.getCssValue('border')).toContain('none');
|
||||
expect(button.getCssValue('padding')).toBe('5px 10px');
|
||||
expect(button.getCssValue('border-radius')).toBe('4px');
|
||||
// Styles defined in heroes.component.css
|
||||
expect(button.getCssValue('left')).toBe('194px');
|
||||
expect(button.getCssValue('top')).toBe('-32px');
|
||||
}
|
||||
});
|
||||
|
||||
const addButton = element(by.buttonText('add'));
|
||||
// Inherited styles from styles.css
|
||||
expect(addButton.getCssValue('font-family')).toBe('Arial');
|
||||
expect(addButton.getCssValue('border')).toContain('none');
|
||||
expect(addButton.getCssValue('padding')).toBe('5px 10px');
|
||||
expect(addButton.getCssValue('border-radius')).toBe('4px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Progressive hero search', () => {
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it(`searches for 'Ma'`, async () => {
|
||||
getPageElts().searchBox.sendKeys('Ma');
|
||||
browser.sleep(1000);
|
||||
|
||||
expect(getPageElts().searchResults.count()).toBe(4);
|
||||
});
|
||||
|
||||
it(`continues search with 'g'`, async () => {
|
||||
getPageElts().searchBox.sendKeys('g');
|
||||
browser.sleep(1000);
|
||||
expect(getPageElts().searchResults.count()).toBe(2);
|
||||
});
|
||||
|
||||
it(`continues search with 'e' and gets ${targetHero.name}`, async () => {
|
||||
getPageElts().searchBox.sendKeys('n');
|
||||
browser.sleep(1000);
|
||||
const page = getPageElts();
|
||||
expect(page.searchResults.count()).toBe(1);
|
||||
const hero = page.searchResults.get(0);
|
||||
expect(hero.getText()).toEqual(targetHero.name);
|
||||
});
|
||||
|
||||
it(`navigates to ${targetHero.name} details view`, async () => {
|
||||
const hero = getPageElts().searchResults.get(0);
|
||||
expect(hero.getText()).toEqual(targetHero.name);
|
||||
hero.click();
|
||||
|
||||
const page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
const hero2 = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero2.id).toEqual(targetHero.id);
|
||||
expect(hero2.name).toEqual(targetHero.name.toUpperCase());
|
||||
});
|
||||
});
|
||||
|
||||
// Helpers
|
||||
function addToHeroName(text: string): Promise<void> {
|
||||
return element(by.css('input')).sendKeys(text) as Promise<void>;
|
||||
}
|
||||
|
||||
async function dashboardSelectTargetHero(): Promise<void> {
|
||||
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||
targetHeroElt.click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||
|
||||
const page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
const hero = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero.id).toEqual(targetHero.id);
|
||||
expect(hero.name).toEqual(targetHero.name.toUpperCase());
|
||||
}
|
||||
|
||||
function expectHeading(hLevel: number, expectedText: string): void {
|
||||
const hTag = `h${hLevel}`;
|
||||
const hText = element(by.css(hTag)).getText();
|
||||
expect(hText).toEqual(expectedText, hTag);
|
||||
}
|
||||
|
||||
function getHeroAEltById(id: number): ElementFinder {
|
||||
const spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||
return spanForId.element(by.xpath('..'));
|
||||
}
|
||||
|
||||
function getHeroLiEltById(id: number): ElementFinder {
|
||||
const spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||
return spanForId.element(by.xpath('../..'));
|
||||
}
|
||||
|
||||
function getPageElts() {
|
||||
const navElts = element.all(by.css('app-root nav a'));
|
||||
|
||||
return {
|
||||
navElts,
|
||||
|
||||
appDashboardHref: navElts.get(0),
|
||||
appDashboard: element(by.css('app-root app-dashboard')),
|
||||
topHeroes: element.all(by.css('app-root app-dashboard > div h4')),
|
||||
|
||||
appHeroesHref: navElts.get(1),
|
||||
appHeroes: element(by.css('app-root app-heroes')),
|
||||
allHeroes: element.all(by.css('app-root app-heroes li')),
|
||||
selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')),
|
||||
|
||||
heroDetail: element(by.css('app-root app-hero-detail > div')),
|
||||
|
||||
searchBox: element(by.css('#search-box')),
|
||||
searchResults: element.all(by.css('.search-result li'))
|
||||
};
|
||||
}
|
||||
|
||||
async function toHeroArray(allHeroes: ElementArrayFinder): Promise<Hero[]> {
|
||||
return await allHeroes.map(Hero.fromLi);
|
||||
}
|
||||
|
||||
async function updateHeroNameInDetailView(): Promise<void> {
|
||||
// Assumes that the current view is the hero details view.
|
||||
addToHeroName(nameSuffix);
|
||||
|
||||
const page = getPageElts();
|
||||
const hero = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero.id).toEqual(targetHero.id);
|
||||
expect(hero.name).toEqual(newHeroName.toUpperCase());
|
||||
}
|
||||
});
|
@ -1,3 +1,7 @@
|
||||
{
|
||||
"projectType": "universal"
|
||||
"projectType": "universal",
|
||||
"e2e": [
|
||||
{"cmd": "yarn", "args": ["e2e", "--prod", "--protractor-config=e2e/protractor-puppeteer.conf.js", "--no-webdriver-update", "--port={PORT}"]},
|
||||
{"cmd": "yarn", "args": ["run", "build:ssr"]}
|
||||
]
|
||||
}
|
||||
|
@ -6,24 +6,28 @@ import { join } from 'path';
|
||||
|
||||
import { AppServerModule } from './src/main.server';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app() {
|
||||
const server = express();
|
||||
const distFolder = join(process.cwd(), 'dist/express-engine-ivy/browser');
|
||||
const distFolder = join(process.cwd(), 'dist/browser');
|
||||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
||||
|
||||
// #docregion ngExpressEngine
|
||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||
server.engine('html', ngExpressEngine({
|
||||
bootstrap: AppServerModule,
|
||||
}));
|
||||
// #enddocregion ngExpressEngine
|
||||
|
||||
server.set('view engine', 'html');
|
||||
server.set('views', distFolder);
|
||||
|
||||
// #docregion data-request
|
||||
// TODO: implement data requests securely
|
||||
server.get('/api/*', (req, res) => {
|
||||
res.status(404).send('data requests are not supported');
|
||||
server.get('/api/**', (req, res) => {
|
||||
res.status(404).send('data requests are not yet supported');
|
||||
});
|
||||
// #enddocregion data-request
|
||||
|
||||
@ -37,7 +41,7 @@ export function app() {
|
||||
// #docregion navigation-request
|
||||
// All regular routes use the Universal engine
|
||||
server.get('*', (req, res) => {
|
||||
res.render('index', { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
|
||||
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
|
||||
});
|
||||
// #enddocregion navigation-request
|
||||
|
||||
@ -59,7 +63,8 @@ function run() {
|
||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||
declare const __non_webpack_require__: NodeRequire;
|
||||
const mainModule = __non_webpack_require__.main;
|
||||
if (mainModule && mainModule.filename === __filename) {
|
||||
const moduleFilename = mainModule && mainModule.filename || '';
|
||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||
run();
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
/* AppComponent's private CSS styles */
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
color: #999;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h2 {
|
||||
@ -18,7 +17,7 @@ nav a {
|
||||
border-radius: 4px;
|
||||
}
|
||||
nav a:visited, a:link {
|
||||
color: #607D8B;
|
||||
color: #334953;
|
||||
}
|
||||
nav a:hover {
|
||||
color: #039be5;
|
||||
|
@ -14,8 +14,6 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
|
||||
import { HeroesComponent } from './heroes/heroes.component';
|
||||
import { HeroSearchComponent } from './hero-search/hero-search.component';
|
||||
import { HeroService } from './hero.service';
|
||||
import { MessageService } from './message.service';
|
||||
import { MessagesComponent } from './messages/messages.component';
|
||||
|
||||
// #docregion platform-detection
|
||||
@ -32,6 +30,10 @@ import { isPlatformBrowser } from '@angular/common';
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
|
||||
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
|
||||
// and returns simulated server responses.
|
||||
// Remove it when a real server is ready to receive requests.
|
||||
HttpClientInMemoryWebApiModule.forRoot(
|
||||
InMemoryDataService, { dataEncapsulation: false }
|
||||
)
|
||||
@ -44,7 +46,6 @@ import { isPlatformBrowser } from '@angular/common';
|
||||
MessagesComponent,
|
||||
HeroSearchComponent
|
||||
],
|
||||
providers: [ HeroService, MessageService ],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './app.component';
|
||||
@ -9,11 +8,10 @@ import { AppComponent } from './app.component';
|
||||
imports: [
|
||||
AppModule,
|
||||
ServerModule,
|
||||
ModuleMapLoaderModule
|
||||
],
|
||||
providers: [
|
||||
// Add universal-only providers here
|
||||
// Add server-only providers here.
|
||||
],
|
||||
bootstrap: [ AppComponent ],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
|
@ -34,7 +34,7 @@ h4 {
|
||||
color: #eee;
|
||||
max-height: 120px;
|
||||
min-width: 120px;
|
||||
background-color: #607D8B;
|
||||
background-color: #3f525c;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.module:hover {
|
||||
|
@ -8,4 +8,4 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hero-search></hero-search>
|
||||
<app-hero-search></app-hero-search>
|
||||
|
@ -18,6 +18,6 @@ export class DashboardComponent implements OnInit {
|
||||
|
||||
getHeroes(): void {
|
||||
this.heroService.getHeroes()
|
||||
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
|
||||
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ button {
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div *ngIf="hero">
|
||||
<h2>{{ hero.name | uppercase }} Details</h2>
|
||||
<h2>{{hero.name | uppercase}} Details</h2>
|
||||
<div><span>id: </span>{{hero.id}}</div>
|
||||
<div>
|
||||
<label>name:
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@ -11,7 +11,7 @@ import { HeroService } from '../hero.service';
|
||||
styleUrls: [ './hero-detail.component.css' ]
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
hero: Hero;
|
||||
@Input() hero: Hero;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -33,7 +33,7 @@ export class HeroDetailComponent implements OnInit {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
save(): void {
|
||||
save(): void {
|
||||
this.heroService.updateHero(this.hero)
|
||||
.subscribe(() => this.goBack());
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
<div id="search-component">
|
||||
<h4>Hero Search</h4>
|
||||
<h4><label for="search-box">Hero Search</label></h4>
|
||||
|
||||
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
|
||||
|
||||
<ul class="search-result">
|
||||
<li *ngFor="let hero of heroes | async" >
|
||||
<li *ngFor="let hero of heroes$ | async" >
|
||||
<a routerLink="/detail/{{hero.id}}">
|
||||
{{hero.name}}
|
||||
</a>
|
||||
|
@ -10,12 +10,12 @@ import { Hero } from '../hero';
|
||||
import { HeroService } from '../hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'hero-search',
|
||||
selector: 'app-hero-search',
|
||||
templateUrl: './hero-search.component.html',
|
||||
styleUrls: [ './hero-search.component.css' ]
|
||||
})
|
||||
export class HeroSearchComponent implements OnInit {
|
||||
heroes: Observable<Hero[]>;
|
||||
heroes$: Observable<Hero[]>;
|
||||
private searchTerms = new Subject<string>();
|
||||
|
||||
constructor(private heroService: HeroService) {}
|
||||
@ -26,7 +26,7 @@ export class HeroSearchComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.heroes = this.searchTerms.pipe(
|
||||
this.heroes$ = this.searchTerms.pipe(
|
||||
// wait 300ms after each keystroke before considering the term
|
||||
debounceTime(300),
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Injectable, Inject, Optional } from '@angular/core';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { HttpClient, HttpHeaders }from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
@ -8,30 +7,26 @@ import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { Hero } from './hero';
|
||||
import { MessageService } from './message.service';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HeroService {
|
||||
|
||||
private heroesUrl = 'api/heroes'; // URL to web api
|
||||
|
||||
// #docregion ctor
|
||||
httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||
};
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private messageService: MessageService,
|
||||
@Optional() @Inject(APP_BASE_HREF) origin?: string) {
|
||||
this.heroesUrl = `${origin}${this.heroesUrl}`;
|
||||
}
|
||||
// #enddocregion ctor
|
||||
private messageService: MessageService) { }
|
||||
|
||||
/** GET heroes from the server */
|
||||
getHeroes (): Observable<Hero[]> {
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
return this.http.get<Hero[]>(this.heroesUrl)
|
||||
.pipe(
|
||||
tap(heroes => this.log('fetched heroes')),
|
||||
catchError(this.handleError('getHeroes', []))
|
||||
tap(_ => this.log('fetched heroes')),
|
||||
catchError(this.handleError<Hero[]>('getHeroes', []))
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,7 +60,9 @@ export class HeroService {
|
||||
return of([]);
|
||||
}
|
||||
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
|
||||
tap(_ => this.log(`found heroes matching "${term}"`)),
|
||||
tap(x => x.length ?
|
||||
this.log(`found heroes matching "${term}"`) :
|
||||
this.log(`no heroes matching "${term}"`)),
|
||||
catchError(this.handleError<Hero[]>('searchHeroes', []))
|
||||
);
|
||||
}
|
||||
@ -73,29 +70,27 @@ export class HeroService {
|
||||
//////// Save methods //////////
|
||||
|
||||
/** POST: add a new hero to the server */
|
||||
addHero (name: string): Observable<Hero> {
|
||||
const hero = { name };
|
||||
|
||||
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
|
||||
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
|
||||
addHero(hero: Hero): Observable<Hero> {
|
||||
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
|
||||
catchError(this.handleError<Hero>('addHero'))
|
||||
);
|
||||
}
|
||||
|
||||
/** DELETE: delete the hero from the server */
|
||||
deleteHero (hero: Hero | number): Observable<Hero> {
|
||||
deleteHero(hero: Hero | number): Observable<Hero> {
|
||||
const id = typeof hero === 'number' ? hero : hero.id;
|
||||
const url = `${this.heroesUrl}/${id}`;
|
||||
|
||||
return this.http.delete<Hero>(url, httpOptions).pipe(
|
||||
return this.http.delete<Hero>(url, this.httpOptions).pipe(
|
||||
tap(_ => this.log(`deleted hero id=${id}`)),
|
||||
catchError(this.handleError<Hero>('deleteHero'))
|
||||
);
|
||||
}
|
||||
|
||||
/** PUT: update the hero on the server */
|
||||
updateHero (hero: Hero): Observable<any> {
|
||||
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
|
||||
updateHero(hero: Hero): Observable<any> {
|
||||
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
|
||||
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
||||
catchError(this.handleError<any>('updateHero'))
|
||||
);
|
||||
@ -107,7 +102,7 @@ export class HeroService {
|
||||
* @param operation - name of the operation that failed
|
||||
* @param result - optional value to return as the observable result
|
||||
*/
|
||||
private handleError<T> (operation = 'operation', result?: T) {
|
||||
private handleError<T>(operation = 'operation', result?: T) {
|
||||
return (error: any): Observable<T> => {
|
||||
|
||||
// TODO: send the error to remote logging infrastructure
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.heroes a {
|
||||
color: #888;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.heroes a:hover {
|
||||
color:#607D8B;
|
||||
color: #607D8B;
|
||||
}
|
||||
|
||||
.heroes .badge {
|
||||
@ -38,7 +38,7 @@
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.8em 0.7em 0 0.7em;
|
||||
background-color: #607D8B;
|
||||
background-color: #405061;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
@ -50,7 +50,7 @@
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.button {
|
||||
button {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
|
@ -16,6 +16,6 @@
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
||||
</a>
|
||||
<button class="delete" title="delete hero"
|
||||
(click)="delete(hero);$event.stopPropagation()">x</button>
|
||||
(click)="delete(hero)">x</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -25,17 +25,15 @@ export class HeroesComponent implements OnInit {
|
||||
add(name: string): void {
|
||||
name = name.trim();
|
||||
if (!name) { return; }
|
||||
this.heroService.addHero(name)
|
||||
this.heroService.addHero({ name } as Hero)
|
||||
.subscribe(hero => {
|
||||
this.heroes.push(hero);
|
||||
});
|
||||
}
|
||||
|
||||
delete(hero: Hero): void {
|
||||
this.heroService.deleteHero(hero)
|
||||
.subscribe(() => {
|
||||
this.heroes = this.heroes.filter(h => h !== hero);
|
||||
});
|
||||
this.heroes = this.heroes.filter(h => h !== hero);
|
||||
this.heroService.deleteHero(hero).subscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||
import { Hero } from './hero';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class InMemoryDataService implements InMemoryDbService {
|
||||
createDb() {
|
||||
const heroes = [
|
||||
@ -16,4 +21,13 @@ export class InMemoryDataService implements InMemoryDbService {
|
||||
];
|
||||
return {heroes};
|
||||
}
|
||||
|
||||
// Overrides the genId method to ensure that a hero always has an id.
|
||||
// If the heroes array is empty,
|
||||
// the method below returns the initial number (11).
|
||||
// if the heroes array is not empty, the method below returns the highest
|
||||
// hero id + 1.
|
||||
genId(heroes: Hero[]): number {
|
||||
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MessageService {
|
||||
messages: string[] = [];
|
||||
|
||||
|
@ -30,6 +30,6 @@ button:disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
button.clear {
|
||||
color: #888;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
@ -8,4 +8,7 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
});
|
||||
|
@ -1,32 +0,0 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: { server: './server.ts' },
|
||||
resolve: { extensions: ['.js', '.ts'] },
|
||||
target: 'node',
|
||||
mode: 'none',
|
||||
// this makes sure we include node_modules and other 3rd party libraries
|
||||
externals: [/node_modules/],
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
|
||||
},
|
||||
plugins: [
|
||||
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
|
||||
// for 'WARNING Critical dependency: the request of a dependency is an expression'
|
||||
new webpack.ContextReplacementPlugin(
|
||||
/(.+)?angular(\\|\/)core(.+)?/,
|
||||
path.join(__dirname, 'src'), // location of your src
|
||||
{} // a map of your routes
|
||||
),
|
||||
new webpack.ContextReplacementPlugin(
|
||||
/(.+)?express(\\|\/)(.+)?/,
|
||||
path.join(__dirname, 'src'),
|
||||
{}
|
||||
)
|
||||
]
|
||||
};
|
@ -10,6 +10,12 @@ For an in-depth introduction to issues and techniques for designing accessible a
|
||||
This page discusses best practices for designing Angular applications that
|
||||
work well for all users, including those who rely on assistive technologies.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For the sample app that this page describes, see the <live-example></live-example>.
|
||||
|
||||
</div>
|
||||
|
||||
## Accessibility attributes
|
||||
|
||||
Building accessible web experience often involves setting [ARIA attributes](https://developers.google.com/web/fundamentals/accessibility/semantics-aria)
|
||||
@ -92,8 +98,6 @@ The following example shows how to make a simple progress bar accessible by usin
|
||||
<code-example path="accessibility/src/app/app.component.html" header="src/app/app.component.html" region="template"></code-example>
|
||||
|
||||
|
||||
To see the progress bar in a working example app, refer to the <live-example></live-example>.
|
||||
|
||||
## Routing and focus management
|
||||
|
||||
Tracking and controlling [focus](https://developers.google.com/web/fundamentals/accessibility/focus/) in a UI is an important consideration in designing for accessibility.
|
||||
|
@ -415,8 +415,8 @@ The following are some of the key AngularJS built-in directives and their equiva
|
||||
<code-example hideCopy path="ajs-quick-reference/src/app/app.component.html" region="router-link"></code-example>
|
||||
|
||||
|
||||
For more information on routing, see the [RouterLink binding](guide/router#router-link)
|
||||
section of the [Routing & Navigation](guide/router) page.
|
||||
For more information on routing, see [Defining a basic route](guide/router#basic-route)
|
||||
in the [Routing & Navigation](guide/router) page.
|
||||
|
||||
</td>
|
||||
|
||||
|
@ -19,12 +19,17 @@ Both components and services are simply classes, with *decorators* that mark the
|
||||
|
||||
An app's components typically define many views, arranged hierarchically. Angular provides the `Router` service to help you define navigation paths among views. The router provides sophisticated in-browser navigational capabilities.
|
||||
|
||||
<div class="alert is-helpful>
|
||||
<div class="alert is-helpful">
|
||||
|
||||
See the [Angular Glossary](guide/glossary) for basic definitions of important Angular terms and usage.
|
||||
|
||||
</div>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
For the sample app that this page describes, see the <live-example></live-example>.
|
||||
</div>
|
||||
|
||||
## Modules
|
||||
|
||||
Angular *NgModules* differ from and complement JavaScript (ES2015) modules. An NgModule declares a compilation context for a set of components that is dedicated to an application domain, a workflow, or a closely related set of capabilities. An NgModule can associate its components with related code, such as services, to form functional units.
|
||||
@ -148,10 +153,5 @@ Each of these subjects is introduced in more detail in the following pages.
|
||||
|
||||
* [Introduction to services and dependency injection](guide/architecture-services)
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Note that the code referenced on these pages is available as a <live-example></live-example>.
|
||||
</div>
|
||||
|
||||
When you're familiar with these fundamental building blocks, you can explore them in more detail in the documentation. To learn about more tools and techniques that are available to help you build and deploy Angular applications, see [Next steps: tools and techniques](guide/architecture-next-steps).
|
||||
</div>
|
||||
|
@ -303,7 +303,7 @@ Some features of Angular may require additional polyfills.
|
||||
<td>
|
||||
|
||||
[Router](guide/router) when using
|
||||
[hash-based routing](guide/router#appendix-locationstrategy-and-browser-url-styles)
|
||||
[hash-based routing](guide/router#location-strategy)
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
@ -311,11 +311,11 @@ To use CSS grid with IE10/11, you must explicitly enable it using the `autoplace
|
||||
To do this, add the following to the top of the global styles file (or within a specific css selector scope):
|
||||
|
||||
```
|
||||
/* autoprefixer grid: autoplace /
|
||||
/* autoprefixer grid: autoplace */
|
||||
```
|
||||
or
|
||||
```
|
||||
/ autoprefixer grid: no-autoplace */
|
||||
/* autoprefixer grid: no-autoplace */
|
||||
```
|
||||
|
||||
For more information, see [Autoprefixer documentation](https://autoprefixer.github.io/).
|
||||
|
@ -321,7 +321,7 @@ absolutely must be present when the app starts.
|
||||
|
||||
Configure the Angular Router to defer loading of all other modules (and their associated code), either by
|
||||
[waiting until the app has launched](guide/router#preloading "Preloading")
|
||||
or by [_lazy loading_](guide/router#asynchronous-routing "Lazy loading")
|
||||
or by [_lazy loading_](guide/router#lazy-loading "Lazy loading")
|
||||
them on demand.
|
||||
|
||||
<div class="callout is-helpful">
|
||||
|
@ -318,6 +318,7 @@ const routes: Routes = [{
|
||||
|
||||
|
||||
{@a activatedroute-props}
|
||||
|
||||
### ActivatedRoute params and queryParams properties
|
||||
|
||||
[ActivatedRoute](api/router/ActivatedRoute) contains two [properties](api/router/ActivatedRoute#properties) that are less capable than their replacements and may be deprecated in a future Angular version.
|
||||
@ -327,7 +328,7 @@ const routes: Routes = [{
|
||||
| `params` | `paramMap` |
|
||||
| `queryParams` | `queryParamMap` |
|
||||
|
||||
For more information see the [Router guide](guide/router#activated-route).
|
||||
For more information see the [Getting route information](guide/router#activated-route) section of the [Router guide](guide/router).
|
||||
|
||||
|
||||
{@a reflect-metadata}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -103,9 +103,8 @@ Version | Status | Released | Active Ends | LTS Ends
|
||||
------- | ------ | ------------ | ------------ | ------------
|
||||
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
|
||||
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020
|
||||
^7.0.0 | LTS | Oct 18, 2018 | Apr 18, 2019 | Apr 18, 2020
|
||||
|
||||
Angular versions ^4.0.0, ^5.0.0 and ^6.0.0 are no longer under support.
|
||||
Angular versions ^4.0.0, ^5.0.0, ^6.0.0 and ^7.0.0 are no longer under support.
|
||||
|
||||
{@a deprecation}
|
||||
## Deprecation practices
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1958,7 +1958,7 @@ for the `id` to change during its lifetime.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The [Router](guide/router#route-parameters) guide covers `ActivatedRoute.paramMap` in more detail.
|
||||
The [ActivatedRoute in action](guide/router#activated-route-in-action) section of the [Router](guide/router) guide covers `ActivatedRoute.paramMap` in more detail.
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -15,7 +15,7 @@ The CLI schematic `@nguniversal/express-engine` performs the required steps, as
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
**Note:** [Download the finished sample code](generated/zips/universal/universal.zip),
|
||||
**Note:** <live-example downloadOnly>Download the finished sample code</live-example>,
|
||||
which runs in a [Node.js® Express](https://expressjs.com/) server.
|
||||
|
||||
</div>
|
||||
@ -27,7 +27,7 @@ The [Tour of Heroes tutorial](tutorial) is the foundation for this walkthrough.
|
||||
|
||||
In this example, the Angular CLI compiles and bundles the Universal version of the app with the
|
||||
[Ahead-of-Time (AOT) compiler](guide/aot-compiler).
|
||||
A Node Express web server compiles HTML pages with Universal based on client requests.
|
||||
A Node.js Express web server compiles HTML pages with Universal based on client requests.
|
||||
|
||||
To create the server-side app module, `app.server.module.ts`, run the following CLI command.
|
||||
|
||||
@ -62,10 +62,10 @@ The files marked with `*` are new and not in the original tutorial sample.
|
||||
To start rendering your app with Universal on your local system, use the following command.
|
||||
|
||||
<code-example language="bash">
|
||||
npm run build:ssr && npm run serve:ssr
|
||||
npm run dev:ssr
|
||||
</code-example>
|
||||
|
||||
Open a browser and navigate to http://localhost:4000/.
|
||||
Open a browser and navigate to http://localhost:4200/.
|
||||
You should see the familiar Tour of Heroes dashboard page.
|
||||
|
||||
Navigation via `routerLinks` works correctly because they use the native anchor (`<a>`) tags.
|
||||
@ -158,13 +158,12 @@ The sample web server for this guide is based on the popular [Express](https://e
|
||||
Universal applications use the Angular `platform-server` package (as opposed to `platform-browser`), which provides
|
||||
server implementations of the DOM, `XMLHttpRequest`, and other low-level features that don't rely on a browser.
|
||||
|
||||
The server ([Node Express](https://expressjs.com/) in this guide's example)
|
||||
The server ([Node.js Express](https://expressjs.com/) in this guide's example)
|
||||
passes client requests for application pages to the NgUniversal `ngExpressEngine`. Under the hood, this
|
||||
calls Universal's `renderModule()` function, while providing caching and other helpful utilities.
|
||||
|
||||
The `renderModule()` function takes as inputs a *template* HTML page (usually `index.html`),
|
||||
an Angular *module* containing components,
|
||||
and a *route* that determines which components to display.
|
||||
an Angular *module* containing components, and a *route* that determines which components to display.
|
||||
The route comes from the client's request to the server.
|
||||
|
||||
Each request results in the appropriate view for the requested route.
|
||||
@ -188,71 +187,6 @@ Similarly, without mouse or keyboard events, a server-side app can't rely on a u
|
||||
The app must determine what to render based solely on the incoming client request.
|
||||
This is a good argument for making the app [routable](guide/router).
|
||||
|
||||
{@a http-urls}
|
||||
### Using absolute URLs for server requests
|
||||
|
||||
The tutorial's `HeroService` and `HeroSearchService` delegate to the Angular `HttpClient` module to fetch application data.
|
||||
These services send requests to _relative_ URLs such as `api/heroes`.
|
||||
In a Universal app, HTTP URLs must be _absolute_ (for example, `https://my-server.com/api/heroes`).
|
||||
This means you need to change your services to make requests with absolute URLs when running on the server and with relative
|
||||
URLs when running in the browser.
|
||||
|
||||
One solution is to provide the full URL to your application on the server, and write an interceptor that can retrieve this
|
||||
value and prepend it to the request URL. If you're using the `ngExpressEngine`, as shown in the example in this guide, half
|
||||
the work is already done. We'll assume this is the case, but it's trivial to provide the same functionality.
|
||||
|
||||
Start by creating an [HttpInterceptor](api/common/http/HttpInterceptor).
|
||||
|
||||
<code-example language="typescript" header="universal-interceptor.ts">
|
||||
|
||||
import {Injectable, Inject, Optional} from '@angular/core';
|
||||
import {HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders} from '@angular/common/http';
|
||||
import {Request} from 'express';
|
||||
import {REQUEST} from '@nguniversal/express-engine/tokens';
|
||||
|
||||
@Injectable()
|
||||
export class UniversalInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(@Optional() @Inject(REQUEST) protected request?: Request) {}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler) {
|
||||
let serverReq: HttpRequest<any> = req;
|
||||
if (this.request) {
|
||||
let newUrl = `${this.request.protocol}://${this.request.get('host')}`;
|
||||
if (!req.url.startsWith('/')) {
|
||||
newUrl += '/';
|
||||
}
|
||||
newUrl += req.url;
|
||||
serverReq = req.clone({url: newUrl});
|
||||
}
|
||||
return next.handle(serverReq);
|
||||
}
|
||||
}
|
||||
|
||||
</code-example>
|
||||
|
||||
Next, provide the interceptor in the providers for the server `AppModule`.
|
||||
|
||||
<code-example language="typescript" header="app.server.module.ts">
|
||||
|
||||
import {HTTP_INTERCEPTORS} from '@angular/common/http';
|
||||
import {UniversalInterceptor} from './universal-interceptor';
|
||||
|
||||
@NgModule({
|
||||
...
|
||||
providers: [{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: UniversalInterceptor,
|
||||
multi: true
|
||||
}],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
|
||||
</code-example>
|
||||
|
||||
Now, on every HTTP request made on the server, this interceptor will fire and replace the request URL with the absolute
|
||||
URL provided in the Express `Request` object.
|
||||
|
||||
{@a universal-engine}
|
||||
### Universal template engine
|
||||
|
||||
@ -262,16 +196,10 @@ The important bit in the `server.ts` file is the `ngExpressEngine()` function.
|
||||
</code-example>
|
||||
|
||||
The `ngExpressEngine()` function is a wrapper around Universal's `renderModule()` function which turns a client's
|
||||
requests into server-rendered HTML pages.
|
||||
requests into server-rendered HTML pages. It accepts an object with the following properties:
|
||||
|
||||
* The first parameter is `AppServerModule`.
|
||||
It's the bridge between the Universal server-side renderer and the Angular application.
|
||||
|
||||
* The second parameter, `extraProviders`, is optional. It lets you specify dependency providers that apply only when
|
||||
running on this server.
|
||||
You can do this when your app needs information that can only be determined by the currently running server instance.
|
||||
One example could be the running server's *origin*, which could be used to [calculate absolute HTTP URLs](#http-urls) if
|
||||
not using the `Request` token as shown above.
|
||||
* `bootstrap`: The root `NgModule` or `NgModule` factory to use for bootstraping the app when rendering on the server. For the example app, it is `AppServerModule`. It's the bridge between the Universal server-side renderer and the Angular application.
|
||||
* `extraProviders`: This is optional and lets you specify dependency providers that apply only when rendering the app on the server. You can do this when your app needs information that can only be determined by the currently running server instance.
|
||||
|
||||
The `ngExpressEngine()` function returns a `Promise` callback that resolves to the rendered page.
|
||||
It's up to the engine to decide what to do with that page.
|
||||
@ -287,7 +215,7 @@ which then forwards it to the client in the HTTP response.
|
||||
|
||||
### Filtering request URLs
|
||||
|
||||
NOTE: the basic behavior described below is handled automatically when using the NgUniversal Express schematic, this
|
||||
NOTE: The basic behavior described below is handled automatically when using the NgUniversal Express schematic. This
|
||||
is helpful when trying to understand the underlying behavior or replicate it without using the schematic.
|
||||
|
||||
The web server must distinguish _app page requests_ from other kinds of requests.
|
||||
@ -307,8 +235,8 @@ Because we use routing, we can easily recognize the three types of requests and
|
||||
1. **App navigation**: request URL with no file extension.
|
||||
1. **Static asset**: all other requests.
|
||||
|
||||
A Node Express server is a pipeline of middleware that filters and processes requests one after the other.
|
||||
You configure the Node Express server pipeline with calls to `app.get()` like this one for data requests.
|
||||
A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other.
|
||||
You configure the Node.js Express server pipeline with calls to `server.get()` like this one for data requests.
|
||||
|
||||
<code-example path="universal/server.ts" header="server.ts (data URL)" region="data-request"></code-example>
|
||||
|
||||
@ -328,13 +256,32 @@ The following code filters for request URLs with no extensions and treats them a
|
||||
|
||||
### Serving static files safely
|
||||
|
||||
A single `app.use()` treats all other URLs as requests for static assets
|
||||
A single `server.use()` treats all other URLs as requests for static assets
|
||||
such as JavaScript, image, and style files.
|
||||
|
||||
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in
|
||||
the `/dist` folder and only honor requests for files from the `/dist` folder.
|
||||
|
||||
The following Node Express code routes all remaining requests to `/dist`, and returns a `404 - NOT FOUND` error if the
|
||||
The following Node.js Express code routes all remaining requests to `/dist`, and returns a `404 - NOT FOUND` error if the
|
||||
file isn't found.
|
||||
|
||||
<code-example path="universal/server.ts" header="server.ts (static files)" region="static"></code-example>
|
||||
|
||||
### Using absolute URLs for HTTP (data) requests on the server
|
||||
|
||||
The tutorial's `HeroService` and `HeroSearchService` delegate to the Angular `HttpClient` module to fetch application data.
|
||||
These services send requests to _relative_ URLs such as `api/heroes`.
|
||||
In a server-side rendered app, HTTP URLs must be _absolute_ (for example, `https://my-server.com/api/heroes`).
|
||||
This means that the URLs must be somehow converted to absolute when running on the server and be left relative when running in the browser.
|
||||
|
||||
If you are using one of the `@nguniversal/*-engine` packages (such as `@nguniversal/express-engine`), this is taken care for you automatically.
|
||||
You don't need to do anything to make relative URLs work on the server.
|
||||
|
||||
If, for some reason, you are not using an `@nguniversal/*-engine` package, you may need to handle it yourself.
|
||||
|
||||
The recommended solution is to pass the full request URL to the `options` argument of [renderModule()](api/platform-server/renderModule) or [renderModuleFactory()](api/platform-server/renderModuleFactory) (depending on what you use to render `AppServerModule` on the server).
|
||||
This option is the least intrusive as it does not require any changes to the app.
|
||||
Here, "request URL" refers to the URL of the request as a response to which the app is being rendered on the server.
|
||||
For example, if the client requested `https://my-server.com/dashboard` and you are rendering the app on the server to respond to that request, `options.url` should be set to `https://my-server.com/dashboard`.
|
||||
|
||||
Now, on every HTTP request made as part of rendering the app on the server, Angular can correctly resolve the request URL to an absolute URL, using the provided `options.url`.
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.0 KiB |
BIN
aio/content/images/bios/rockument69.jpg
Normal file
BIN
aio/content/images/bios/rockument69.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
@ -44,15 +44,6 @@
|
||||
"groups": ["Angular"],
|
||||
"lead": "juleskremer"
|
||||
},
|
||||
"robwormald": {
|
||||
"name": "Rob Wormald",
|
||||
"picture": "rob-wormald.jpg",
|
||||
"twitter": "robwormald",
|
||||
"website": "http://github.com/robwormald",
|
||||
"bio": "Rob is a Developer Advocate on the Angular team at Google. He's the Angular team's resident reactive programming geek and founded the Reactive Extensions for Angular project, ngrx.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "stephenfluin"
|
||||
},
|
||||
"alexeagle": {
|
||||
"name": "Alex Eagle",
|
||||
"picture": "alex-eagle.jpg",
|
||||
@ -667,6 +658,13 @@
|
||||
"groups": ["Angular"],
|
||||
"lead": "dennispbrown"
|
||||
},
|
||||
"rockument69": {
|
||||
"name": "Tony Bove",
|
||||
"picture": "rockument69.jpg",
|
||||
"bio": "Tony is a technical writer with Expert Support. His lifelong passions are helping people use technology, writing fiction, and playing music. When he's not working or playing the harmonica with friends in a bluegrass band, he's swimming and snorkeling on a Kauai beach and playing ball with his Irish Wolfhound. He's worked at home for decades before it became a thing.",
|
||||
"groups": ["Angular"],
|
||||
"lead": "aikidave"
|
||||
},
|
||||
"kapunahelewong": {
|
||||
"name": "Kapunahele Wong",
|
||||
"picture": "kapunahele.jpg",
|
||||
|
@ -101,6 +101,12 @@ This section walks you through using the cart service to add a product to the ca
|
||||
|
||||
<code-example header="src/app/product-details/product-details.component.html" path="getting-started/src/app/product-details/product-details.component.html">
|
||||
</code-example>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The line, `<h4>{{ product.price | currency }}</h4>` uses the `currency` pipe to transform `product.price` from a number to a currency string. A pipe is a way you can transform data in your HTML template. For more information about Angular pipes, see [Pipes](guide/pipes "Pipes").
|
||||
|
||||
</div>
|
||||
|
||||
1. To see the new "Buy" button, refresh the application and click on a product's name to display its details.
|
||||
|
||||
|
@ -18,7 +18,6 @@
|
||||
],
|
||||
"devDependencies": [
|
||||
"@angular/compiler-cli",
|
||||
"@angular/platform-server",
|
||||
"@types/jasmine",
|
||||
"@types/node",
|
||||
"jasmine-core",
|
||||
|
@ -1,27 +1,29 @@
|
||||
{
|
||||
"scripts": [
|
||||
{ "name": "ng", "command": "ng" },
|
||||
{ "name": "build", "command": "ng build" },
|
||||
{ "name": "start", "command": "ng serve" },
|
||||
{ "name": "test", "command": "ng test" },
|
||||
{ "name": "lint", "command": "ng lint" },
|
||||
{ "name": "e2e", "command": "ng e2e" },
|
||||
{ "name": "build:ssr", "command": "npm run build:client-and-server-bundles && npm run webpack:server" },
|
||||
{ "name": "serve:ssr", "command": "node dist/server.js" },
|
||||
{ "name": "build:client-and-server-bundles", "command": "ng build --prod && ng run angular.io-example:server" },
|
||||
{ "name": "webpack:server", "command": "webpack --config webpack.server.config.js --progress --colors" }
|
||||
{ "name": "dev:ssr", "command": "ng run angular.io-example:serve-ssr" },
|
||||
{ "name": "build:ssr", "command": "ng build --prod && ng run angular.io-example:server:production" },
|
||||
{ "name": "serve:ssr", "command": "node dist/server/main.js" },
|
||||
{ "name": "prerender", "command": "ng run angular.io-example:prerender" }
|
||||
],
|
||||
"dependencies": [
|
||||
"@angular/platform-server",
|
||||
"@nguniversal/express-engine",
|
||||
"@nguniversal/module-map-ngfactory-loader"
|
||||
"express"
|
||||
],
|
||||
"devDependencies": [
|
||||
"@angular-devkit/build-angular",
|
||||
"@angular/cli",
|
||||
"@nguniversal/builders",
|
||||
"@types/express",
|
||||
"@types/jasminewd2",
|
||||
"jasmine-spec-reporter",
|
||||
"karma-coverage-istanbul-reporter",
|
||||
"ts-loader",
|
||||
"ts-node",
|
||||
"webpack-cli"
|
||||
"ts-node"
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# How to update the CLI project
|
||||
# How to update the CLI project
|
||||
|
||||
The Angular CLI default setup is updated using `ng update`. Any necessary file changes will be done automatically through migration schematics.
|
||||
|
||||
@ -46,5 +46,5 @@ The specific changes to each project type are listed below:
|
||||
- Includes a `server` target in the `build` architect runners
|
||||
- package.json
|
||||
- Includes custom scripts for building the `server`
|
||||
- Includes additional `dependencies` on `@nguniversal/common`, `@nguniversal/express-engine`, and `@nguniversal/module-map-ngfactory-loader`
|
||||
- Includes `devDependencies` on `@angular/platform-server`, and `ts-loader`
|
||||
- Includes additional `dependencies` on `@angular/platform-server`, `@nguniversal/express-engine`, and `express`
|
||||
- Includes additional `devDependencies` on `@nguniversal/builders` and `@types/express`
|
||||
|
@ -122,8 +122,47 @@
|
||||
"builder": "@angular-devkit/build-angular:server",
|
||||
"options": {
|
||||
"outputPath": "dist/server",
|
||||
"main": "src/main.server.ts",
|
||||
"main": "server.ts",
|
||||
"tsConfig": "tsconfig.server.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "media",
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"sourceMap": false,
|
||||
"optimization": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-ssr": {
|
||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular.io-example:build",
|
||||
"serverTarget": "angular.io-example:server"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular.io-example:build:production",
|
||||
"serverTarget": "angular.io-example:server:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"prerender": {
|
||||
"builder": "@nguniversal/builders:prerender",
|
||||
"options": {
|
||||
"browserTarget": "angular.io-example:build:production",
|
||||
"serverTarget": "angular.io-example:server:production",
|
||||
"routes": [
|
||||
"/"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,10 @@
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
|
||||
"serve:ssr": "node dist/server.js",
|
||||
"build:client-and-server-bundles": "ng build --prod && ng run angular.io-example:server",
|
||||
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
|
||||
"dev:ssr": "ng run angular.io-example:serve-ssr",
|
||||
"serve:ssr": "node dist/server/main.js",
|
||||
"build:ssr": "ng build --prod && ng run angular.io-example:server:production",
|
||||
"prerender": "ng run angular.io-example:prerender"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@ -23,12 +23,11 @@
|
||||
"@angular/forms": "~9.0.6",
|
||||
"@angular/platform-browser": "~9.0.6",
|
||||
"@angular/platform-browser-dynamic": "~9.0.6",
|
||||
"@angular/platform-server": "~9.0.6",
|
||||
"@angular/router": "~9.0.6",
|
||||
"@nguniversal/common": "~9.0.1",
|
||||
"@nguniversal/express-engine": "~9.0.1",
|
||||
"@nguniversal/module-map-ngfactory-loader": "~9.0.0-next.9",
|
||||
"angular-in-memory-web-api": "~0.9.0",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.15.2",
|
||||
"rxjs": "~6.5.4",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.3"
|
||||
@ -38,8 +37,8 @@
|
||||
"@angular/cli": "~9.0.6",
|
||||
"@angular/compiler-cli": "~9.0.6",
|
||||
"@angular/language-service": "~9.0.6",
|
||||
"@angular/platform-server": "~9.0.6",
|
||||
"@types/express": "^4.17.2",
|
||||
"@nguniversal/builders": "^9.0.2",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jasmine": "~3.5.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
@ -53,10 +52,8 @@
|
||||
"karma-jasmine": "~2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.2",
|
||||
"protractor": "~5.4.3",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~5.18.0",
|
||||
"typescript": "~3.7.5",
|
||||
"webpack-cli": "^3.3.10"
|
||||
"typescript": "~3.7.5"
|
||||
}
|
||||
}
|
||||
|
@ -28,18 +28,17 @@
|
||||
"@angular/forms": "~9.0.6",
|
||||
"@angular/platform-browser": "~9.0.6",
|
||||
"@angular/platform-browser-dynamic": "~9.0.6",
|
||||
"@angular/platform-server": "~9.0.6",
|
||||
"@angular/router": "~9.0.6",
|
||||
"@angular/service-worker": "~9.0.6",
|
||||
"@angular/upgrade": "~9.0.6",
|
||||
"@nguniversal/common": "~9.0.1",
|
||||
"@nguniversal/express-engine": "~9.0.1",
|
||||
"@nguniversal/module-map-ngfactory-loader": "~9.0.0-next.9",
|
||||
"@webcomponents/custom-elements": "^1.4.1",
|
||||
"angular": "1.7.9",
|
||||
"angular-in-memory-web-api": "~0.9.0",
|
||||
"angular-route": "1.7.9",
|
||||
"core-js": "^2.5.4",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.15.2",
|
||||
"rxjs": "~6.5.4",
|
||||
"systemjs": "0.19.39",
|
||||
"tslib": "^1.10.0",
|
||||
@ -50,13 +49,13 @@
|
||||
"@angular/cli": "~9.0.6",
|
||||
"@angular/compiler-cli": "~9.0.6",
|
||||
"@angular/language-service": "~9.0.6",
|
||||
"@angular/platform-server": "~9.0.6",
|
||||
"@nguniversal/builders": "^9.0.2",
|
||||
"@types/angular": "1.6.47",
|
||||
"@types/angular-animate": "1.5.10",
|
||||
"@types/angular-mocks": "1.6.0",
|
||||
"@types/angular-resource": "1.5.14",
|
||||
"@types/angular-route": "1.3.5",
|
||||
"@types/express": "4.0.35",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jasmine": "~3.5.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/jquery": "3.3.28",
|
||||
@ -83,10 +82,8 @@
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-uglify": "^1.0.1",
|
||||
"source-map-explorer": "^1.3.2",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~5.18.0",
|
||||
"typescript": "~3.7.5",
|
||||
"webpack-cli": "^3.3.10"
|
||||
"typescript": "~3.7.5"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -28,56 +28,100 @@ module.exports = function autoLinkCode(getDocFromAlias) {
|
||||
return autoLinkCodeImpl;
|
||||
|
||||
function autoLinkCodeImpl() {
|
||||
return (ast) => {
|
||||
return (ast, file) => {
|
||||
visit(ast, 'element', (node, ancestors) => {
|
||||
// Only interested in code elements that:
|
||||
// * do not have `no-auto-link` class
|
||||
// * do not have an ignored language
|
||||
// * are not inside links
|
||||
if (autoLinkCodeImpl.codeElements.some(elementType => is(node, elementType)) &&
|
||||
(!node.properties.className || !node.properties.className.includes('no-auto-link')) &&
|
||||
!autoLinkCodeImpl.ignoredLanguages.includes(node.properties.language) &&
|
||||
ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
||||
visit(node, 'text', (node, ancestors) => {
|
||||
// Only interested in text nodes that are not inside links
|
||||
if (ancestors.every(ancestor => !is(ancestor, 'a'))) {
|
||||
const parent = ancestors[ancestors.length - 1];
|
||||
const index = parent.children.indexOf(node);
|
||||
|
||||
// Can we convert the whole text node into a doc link?
|
||||
const docs = getDocFromAlias(node.value);
|
||||
if (foundValidDoc(docs)) {
|
||||
parent.children.splice(index, 1, createLinkNode(docs[0], node.value));
|
||||
} else {
|
||||
// Parse the text for words that we can convert to links
|
||||
const nodes =
|
||||
textContent(node)
|
||||
.split(/([A-Za-z0-9_.-]+)/)
|
||||
.filter(word => word.length)
|
||||
.map((word, index, words) => {
|
||||
// remove docs that fail the custom filter tests
|
||||
const filteredDocs = autoLinkCodeImpl.customFilters.reduce(
|
||||
(docs, filter) => filter(docs, words, index), getDocFromAlias(word));
|
||||
return foundValidDoc(filteredDocs) ?
|
||||
// Create a link wrapping the text node.
|
||||
createLinkNode(filteredDocs[0], word) :
|
||||
// this is just text so push a new text node
|
||||
{type: 'text', value: word};
|
||||
});
|
||||
|
||||
// Replace the text node with the links and leftover text nodes
|
||||
Array.prototype.splice.apply(parent.children, [index, 1].concat(nodes));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!isValidCodeElement(node, ancestors)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visit(node, 'text', (node, ancestors) => {
|
||||
const isInLink = isInsideLink(ancestors);
|
||||
if (isInLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = ancestors[ancestors.length - 1];
|
||||
const index = parent.children.indexOf(node);
|
||||
|
||||
// Can we convert the whole text node into a doc link?
|
||||
const docs = getDocFromAlias(node.value);
|
||||
if (foundValidDoc(docs, node.value, file)) {
|
||||
parent.children.splice(index, 1, createLinkNode(docs[0], node.value));
|
||||
} else {
|
||||
// Parse the text for words that we can convert to links
|
||||
const nodes = getNodes(node, file);
|
||||
// Replace the text node with the links and leftover text nodes
|
||||
Array.prototype.splice.apply(parent.children, [index, 1].concat(nodes));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function foundValidDoc(docs) {
|
||||
return docs.length === 1 && !docs[0].internal &&
|
||||
autoLinkCodeImpl.docTypes.indexOf(docs[0].docType) !== -1;
|
||||
function isValidCodeElement(node, ancestors) {
|
||||
// Only interested in code elements that:
|
||||
// * do not have `no-auto-link` class
|
||||
// * do not have an ignored language
|
||||
// * are not inside links
|
||||
const isCodeElement = autoLinkCodeImpl.codeElements.some(elementType => is(node, elementType));
|
||||
const hasNoAutoLink = node.properties.className && node.properties.className.includes('no-auto-link');
|
||||
const isLanguageSupported = !autoLinkCodeImpl.ignoredLanguages.includes(node.properties.language);
|
||||
const isInLink = isInsideLink(ancestors);
|
||||
return isCodeElement && !hasNoAutoLink && isLanguageSupported && !isInLink;
|
||||
}
|
||||
|
||||
function isInsideLink(ancestors) {
|
||||
return ancestors.some(ancestor => is(ancestor, 'a'));
|
||||
}
|
||||
|
||||
function getNodes(node, file) {
|
||||
return textContent(node)
|
||||
.split(/([A-Za-z0-9_.-]+)/)
|
||||
.filter(word => word.length)
|
||||
.map((word, index, words) => {
|
||||
// remove docs that fail the custom filter tests
|
||||
const filteredDocs = autoLinkCodeImpl.customFilters.reduce(
|
||||
(docs, filter) => filter(docs, words, index), getDocFromAlias(word));
|
||||
|
||||
return foundValidDoc(filteredDocs, word, file) ?
|
||||
// Create a link wrapping the text node.
|
||||
createLinkNode(filteredDocs[0], word) :
|
||||
// this is just text so push a new text node
|
||||
{type: 'text', value: word};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the docs to be used to generate the links. The validation ensures
|
||||
* that the docs are not `internal` and that the `docType` is supported. The `path`
|
||||
* can be empty when the `API` is not public.
|
||||
*
|
||||
* @param {Array<Object>} docs An array of objects containing the doc details
|
||||
*
|
||||
* @param {string} keyword The keyword the doc applies to
|
||||
*/
|
||||
function foundValidDoc(docs, keyword, file) {
|
||||
if (docs.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var doc = docs[0];
|
||||
|
||||
const isInvalidDoc = doc.docType === 'member' && !keyword.includes('.');
|
||||
if (isInvalidDoc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (doc.path === '') {
|
||||
var message = `
|
||||
autoLinkCode: Doc path is empty for "${doc.id}" - link will not be generated for "${keyword}".
|
||||
Please make sure if the doc should be public. If not, it should probably not be referenced in the docs.`;
|
||||
|
||||
file.message(message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return !doc.internal && autoLinkCodeImpl.docTypes.includes(doc.docType);
|
||||
}
|
||||
|
||||
function createLinkNode(doc, text) {
|
||||
|
@ -126,6 +126,24 @@ describe('autoLinkCode post-processor', () => {
|
||||
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
||||
});
|
||||
|
||||
it('should ignore code items that match an API doc but have no path set',
|
||||
() => {
|
||||
aliasMap.addDoc(
|
||||
{docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: ''});
|
||||
const doc = {docType: 'test-doc', renderedContent: '<code>MyClass</code>'};
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code>MyClass</code>');
|
||||
});
|
||||
|
||||
it('should ignore documents when the `docType` is set to `member` and the keyword doesn\'t include `.`',
|
||||
() => {
|
||||
aliasMap.addDoc(
|
||||
{docType: 'member', id: 'MyEnum', aliases: ['MyEnum'], path: 'a/b/c'});
|
||||
const doc = {docType: 'test-doc', renderedContent: '<code>MyEnum</code>'};
|
||||
processor.$process([doc]);
|
||||
expect(doc.renderedContent).toEqual('<code>MyEnum</code>');
|
||||
});
|
||||
|
||||
it('should insert anchors for individual text nodes within a code block', () => {
|
||||
aliasMap.addDoc({docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass'});
|
||||
const doc = {
|
||||
|
@ -5,6 +5,6 @@
|
||||
*/
|
||||
|
||||
module.exports = function ignoreGenericWords() {
|
||||
const ignoredWords = new Set(['a', 'classes', 'create', 'error', 'group', 'request', 'target', 'value']);
|
||||
const ignoredWords = new Set(['a', 'classes', 'create', 'error', 'group', 'request', 'target', 'value', '_']);
|
||||
return (docs, words, index) => ignoredWords.has(words[index].toLowerCase()) ? [] : docs;
|
||||
};
|
||||
|
@ -11,6 +11,9 @@ import {parseCommitMessage, validateCommitMessage, ValidateCommitMessageOptions}
|
||||
// Whether the provided commit is a fixup commit.
|
||||
const isNonFixup = (m: string) => !parseCommitMessage(m).isFixup;
|
||||
|
||||
// Extracts commit header (first line of commit message).
|
||||
const extractCommitHeader = (m: string) => parseCommitMessage(m).header;
|
||||
|
||||
/** Validate all commits in a provided git commit range. */
|
||||
export function validateCommitRange(range: string) {
|
||||
// A random value is used as a string to allow for a definite split point in the git log result.
|
||||
@ -35,11 +38,18 @@ export function validateCommitRange(range: string) {
|
||||
const allCommitsInRangeValid = commits.every((m, i) => {
|
||||
const options: ValidateCommitMessageOptions = {
|
||||
disallowSquash: true,
|
||||
nonFixupCommitHeaders: isNonFixup(m) ? undefined : commits.slice(0, i).filter(isNonFixup)
|
||||
nonFixupCommitHeaders: isNonFixup(m) ?
|
||||
undefined :
|
||||
commits.slice(0, i).filter(isNonFixup).map(extractCommitHeader)
|
||||
};
|
||||
return validateCommitMessage(m, options);
|
||||
});
|
||||
|
||||
if (allCommitsInRangeValid) {
|
||||
console.info('√ All commit messages in range valid.');
|
||||
} else {
|
||||
// Exit with a non-zero exit code if invalid commit messages have
|
||||
// been discovered.
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
82
dev-infra/ts-circular-dependencies/README.md
Normal file
82
dev-infra/ts-circular-dependencies/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
### ts-circular-dependencies
|
||||
|
||||
This tool requires a test configuration that declares a set of source files which
|
||||
should be checked for cyclic dependencies. e.g.
|
||||
|
||||
```
|
||||
yarn ts-circular-deps --config ./test-config.js <check|approve>
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
In order to detect cycles, the tool currently visits each source file and runs
|
||||
depth first search. If the DFS comes across any node that is part of the current
|
||||
DFS path, then a cycle has been detected and the tool will capture it.
|
||||
|
||||
This algorithm has limitations. For example, consider the following graph:
|
||||
|
||||

|
||||
|
||||
Depending on which source file is considered first, the output of the circular dependency tool
|
||||
will be different. This is because the tool does not recursively find _all_ possible cycles. This
|
||||
would be too inefficient for large graphs (especially in the `angular/angular` repository).
|
||||
|
||||
In this concrete example, the tool will visit `r3_test_bed` first. Then the first neighbour
|
||||
(based on the import in the source file) will be visited. This is `test_bed`. Once done, the
|
||||
tool will visit the first neighbour of `test_bed`. This is `r3_test_bed` again. The node has
|
||||
already been visited, and also is part of the current DFS path. The tool captures this as cycle.
|
||||
|
||||
As no more nodes can be visited within that path, the tool continues (as per DFS algorithm)
|
||||
with visiting the remaining neighbours of `r3_test_bed`. It will visit `test_bed_common` and
|
||||
then come across `test_bed`. The tool only knows that `test_bed` has already been visited, but
|
||||
it does not know that it would close a cycle. The tool certainly could know this by recursively
|
||||
checking neighbours of `test_bed` again, but this is inefficient and will cause the algorithm
|
||||
to eventually degenerate into brute-force.
|
||||
|
||||
In summary, the tool is unable to capture _all_ elementary cycles in the graph. This does not
|
||||
mean though that the tool is incorrectly suggesting that there are _no_ cycles in a graph. The
|
||||
tool is still able to correctly detect whether there are _any_ cycles in a graph or not. For
|
||||
example, if edge from `r3_test_bed` to `test_bed` is removed, then the tool will be able to
|
||||
capture at least one of the other cycles. The golden will change in an unexpected way, but it's
|
||||
**expected** given the trade-off we take for an acceptable running time.
|
||||
|
||||
Other algorithms exist which are proven to print out _all_ the elementary cycles in a directed
|
||||
graph. For example:
|
||||
|
||||
* [Johnson's algorithm for finding simple cycles][johnson-cycles].
|
||||
* [Tarjan's algorithm for enumerating elementary circuits][tarjan-cycles].
|
||||
|
||||
Experiments with these algorithms unveiled that usual source file graphs we have in Angular
|
||||
repositories are too large to be processed in acceptable time. At the time of writing, the
|
||||
source file graph of `angular/angular` consists of 3350 nodes and 8730 edges.
|
||||
|
||||
Algorithms like the one from Donald B. Johnson, which first split the graph into strongly
|
||||
connected components, and then search for elementary cycles in all components with at least
|
||||
two vertices, are too inefficient for the source files graphs we have. Time complexity for
|
||||
such algorithms is described to be `O((n + e)(c + 1))` where `c` is the number of elementary
|
||||
circuits. Donald B. Johnson describes the number of elementary circuits the followed:
|
||||
|
||||
> Thus the number of elementary circuits in a directed graph can grow faster with n than
|
||||
the exponential 2"
|
||||
|
||||
This shows quite well that these algorithms become quickly inefficient the more vertices, edges
|
||||
and simple cycles a graph has. Finding elementary cycles of arbitrary length seems NP-complete as
|
||||
finding a Hamiltonian cycle with length of `n` is NP-complete too. Below is a quote from a
|
||||
[paper describing a randomized algorithm](np-complete-cycles) for finding simple cycles of a
|
||||
_fixed_ length that seems to confirm this hypothesis:
|
||||
|
||||
> It is well known that finding the longest cycle in a graph is a hard problem, since finding
|
||||
a hamiltonian cycle is NP-complete. Hence finding a simple cycle of length k, for an arbitrary
|
||||
k, is NP-complete.
|
||||
|
||||
Other tools like `madge` or `dpdm` have the same limitations.
|
||||
|
||||
**Resources**:
|
||||
|
||||
* [Finding all the elementary circuits of a directed graph - Donald. B. Johnson][johnson-cycles]
|
||||
* [Enumeration of the elementary circuits of a directed graph - Robert Tarjan][tarjan-cycles]
|
||||
* [Once again: Finding simple cycles in graphs - Carsten Dorgerlohx; Jürgen Wirtgen][np-complete-cycles]
|
||||
|
||||
[johnson-cycles]: https://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF
|
||||
[tarjan-cycles]: https://ecommons.cornell.edu/bitstream/handle/1813/5941/72-145.pdf?sequence=1&isAllowed=y
|
||||
[np-complete-cycles]: https://pdfs.semanticscholar.org/16b2/d1a3cf4a8a5dbcad10bb901724631ebead33.pdf
|
BIN
dev-infra/ts-circular-dependencies/example-graph.png
Normal file
BIN
dev-infra/ts-circular-dependencies/example-graph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
@ -173,8 +173,7 @@ assertSucceeded "Expected 'ngcc' to log 'Compiling'."
|
||||
assertEquals 1 `cat node_modules/@angular/material/button/button.d.ts | grep 'import \* as ɵngcc0' | wc -l`
|
||||
|
||||
# Re-compile packages (which requires cleaning up those compiled by a different ngcc version).
|
||||
# (Use sync mode to ensure all tasks share the same `CachedFileSystem` instance.)
|
||||
ngcc --no-async --properties main
|
||||
ngcc --properties main
|
||||
assertSucceeded "Expected 'ngcc' to successfully re-compile the packages."
|
||||
|
||||
# Ensure previously compiled packages were correctly cleaned up (i.e. no multiple
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "9.1.2",
|
||||
"version": "9.1.3",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
@ -5,19 +5,18 @@
|
||||
* 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 {CachedFileSystem, NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';
|
||||
import {NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';
|
||||
|
||||
import {AsyncNgccOptions, mainNgcc, NgccOptions, SyncNgccOptions} from './src/main';
|
||||
import {mainNgcc} from './src/main';
|
||||
import {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options';
|
||||
|
||||
export {ConsoleLogger} from './src/logging/console_logger';
|
||||
export {Logger, LogLevel} from './src/logging/logger';
|
||||
export {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/main';
|
||||
export {PathMappings} from './src/utils';
|
||||
export {AsyncNgccOptions, NgccOptions, PathMappings, SyncNgccOptions} from './src/ngcc_options';
|
||||
|
||||
export function process(options: AsyncNgccOptions): Promise<void>;
|
||||
export function process(options: SyncNgccOptions): void;
|
||||
export function process(options: NgccOptions): void|Promise<void> {
|
||||
// Recreate the file system on each call to reset the cache
|
||||
setFileSystem(new CachedFileSystem(new NodeJSFileSystem()));
|
||||
setFileSystem(new NodeJSFileSystem());
|
||||
return mainNgcc(options);
|
||||
}
|
||||
|
@ -6,150 +6,21 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {resolve, setFileSystem, CachedFileSystem, NodeJSFileSystem} from '../src/ngtsc/file_system';
|
||||
import {mainNgcc} from './src/main';
|
||||
import {ConsoleLogger} from './src/logging/console_logger';
|
||||
import {LogLevel} from './src/logging/logger';
|
||||
import {parseCommandLineOptions} from './src/command_line_options';
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
process.title = 'ngcc';
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const options =
|
||||
yargs
|
||||
.option('s', {
|
||||
alias: 'source',
|
||||
describe:
|
||||
'A path (relative to the working directory) of the `node_modules` folder to process.',
|
||||
default: './node_modules'
|
||||
})
|
||||
.option('f', {alias: 'formats', hidden: true, array: true})
|
||||
.option('p', {
|
||||
alias: 'properties',
|
||||
array: true,
|
||||
describe:
|
||||
'An array of names of properties in package.json to compile (e.g. `module` or `es2015`)\n' +
|
||||
'Each of these properties should hold the path to a bundle-format.\n' +
|
||||
'If provided, only the specified properties are considered for processing.\n' +
|
||||
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.'
|
||||
})
|
||||
.option('t', {
|
||||
alias: 'target',
|
||||
describe:
|
||||
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
|
||||
'If this property is provided then `error-on-failed-entry-point` is forced to true',
|
||||
})
|
||||
.option('first-only', {
|
||||
describe:
|
||||
'If specified then only the first matching package.json property will be compiled.',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('create-ivy-entry-points', {
|
||||
describe:
|
||||
'If specified then new `*_ivy_ngcc` entry-points will be added to package.json rather than modifying the ones in-place.\n' +
|
||||
'For this to work you need to have custom resolution set up (e.g. in webpack) to look for these new entry-points.\n' +
|
||||
'The Angular CLI does this already, so it is safe to use this option if the project is being built via the CLI.',
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('legacy-message-ids', {
|
||||
describe: 'Render `$localize` messages with legacy format ids.\n' +
|
||||
'The default value is `true`. Only set this to `false` if you do not want legacy message ids to\n' +
|
||||
'be rendered. For example, if you are not using legacy message ids in your translation files\n' +
|
||||
'AND are not doing compile-time inlining of translations, in which case the extra message ids\n' +
|
||||
'would add unwanted size to the final source bundle.\n' +
|
||||
'It is safe to leave this set to true if you are doing compile-time inlining because the extra\n' +
|
||||
'legacy message ids will all be stripped during translation.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
.option('async', {
|
||||
describe:
|
||||
'Whether to compile asynchronously. This is enabled by default as it allows compilations to be parallelized.\n' +
|
||||
'Disabling asynchronous compilation may be useful for debugging.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
.option('l', {
|
||||
alias: 'loglevel',
|
||||
describe: 'The lowest severity logging message that should be output.',
|
||||
choices: ['debug', 'info', 'warn', 'error'],
|
||||
})
|
||||
.option('invalidate-entry-point-manifest', {
|
||||
describe:
|
||||
'If this is set then ngcc will not read an entry-point manifest file from disk.\n' +
|
||||
'Instead it will walk the directory tree as normal looking for entry-points, and then write a new manifest file.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('error-on-failed-entry-point', {
|
||||
describe:
|
||||
'Set this option in order to terminate immediately with an error code if an entry-point fails to be processed.\n' +
|
||||
'If `-t`/`--target` is provided then this property is always true and cannot be changed. Otherwise the default is false.\n' +
|
||||
'When set to false, ngcc will continue to process entry-points after a failure. In which case it will log an error and resume processing other entry-points.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('tsconfig', {
|
||||
describe:
|
||||
'A path to a tsconfig.json file that will be used to configure the Angular compiler and module resolution used by ngcc.\n' +
|
||||
'If not provided, ngcc will attempt to read a `tsconfig.json` file from the folder above that given by the `-s` option.\n' +
|
||||
'Set to false (via `--no-tsconfig`) if you do not want ngcc to use any `tsconfig.json` file.',
|
||||
type: 'string',
|
||||
})
|
||||
.strict()
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
if (options['f'] && options['f'].length) {
|
||||
console.error(
|
||||
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
setFileSystem(new CachedFileSystem(new NodeJSFileSystem()));
|
||||
|
||||
const baseSourcePath = resolve(options['s'] || './node_modules');
|
||||
const propertiesToConsider: string[] = options['p'];
|
||||
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
|
||||
const compileAllFormats = !options['first-only'];
|
||||
const createNewEntryPointFormats = options['create-ivy-entry-points'];
|
||||
const logLevel = options['l'] as keyof typeof LogLevel | undefined;
|
||||
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
||||
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
||||
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
||||
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
|
||||
// string "false" to capture the `tsconfig=false` option.
|
||||
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
|
||||
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
|
||||
|
||||
const options = parseCommandLineOptions(process.argv.slice(2));
|
||||
(async () => {
|
||||
try {
|
||||
const logger = logLevel && new ConsoleLogger(LogLevel[logLevel]);
|
||||
|
||||
await mainNgcc({
|
||||
basePath: baseSourcePath,
|
||||
propertiesToConsider,
|
||||
targetEntryPointPath,
|
||||
compileAllFormats,
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
async: options['async'],
|
||||
invalidateEntryPointManifest,
|
||||
errorOnFailedEntryPoint,
|
||||
tsConfigPath
|
||||
});
|
||||
|
||||
if (logger) {
|
||||
await mainNgcc(options);
|
||||
if (options.logger) {
|
||||
const duration = Math.round((Date.now() - startTime) / 1000);
|
||||
logger.debug(`Run ngcc in ${duration}s.`);
|
||||
options.logger.debug(`Run ngcc in ${duration}s.`);
|
||||
}
|
||||
|
||||
process.exitCode = 0;
|
||||
} catch (e) {
|
||||
console.error(e.stack || e.message);
|
||||
|
139
packages/compiler-cli/ngcc/src/command_line_options.ts
Normal file
139
packages/compiler-cli/ngcc/src/command_line_options.ts
Normal file
@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {resolve, setFileSystem, NodeJSFileSystem} from '../../src/ngtsc/file_system';
|
||||
import {ConsoleLogger} from './logging/console_logger';
|
||||
import {LogLevel} from './logging/logger';
|
||||
import {NgccOptions} from './ngcc_options';
|
||||
|
||||
export function parseCommandLineOptions(args: string[]): NgccOptions {
|
||||
const options =
|
||||
yargs
|
||||
.option('s', {
|
||||
alias: 'source',
|
||||
describe:
|
||||
'A path (relative to the working directory) of the `node_modules` folder to process.',
|
||||
default: './node_modules'
|
||||
})
|
||||
.option('f', {alias: 'formats', hidden: true, array: true})
|
||||
.option('p', {
|
||||
alias: 'properties',
|
||||
array: true,
|
||||
describe:
|
||||
'An array of names of properties in package.json to compile (e.g. `module` or `es2015`)\n' +
|
||||
'Each of these properties should hold the path to a bundle-format.\n' +
|
||||
'If provided, only the specified properties are considered for processing.\n' +
|
||||
'If not provided, all the supported format properties (e.g. fesm2015, fesm5, es2015, esm2015, esm5, main, module) in the package.json are considered.'
|
||||
})
|
||||
.option('t', {
|
||||
alias: 'target',
|
||||
describe:
|
||||
'A relative path (from the `source` path) to a single entry-point to process (plus its dependencies).\n' +
|
||||
'If this property is provided then `error-on-failed-entry-point` is forced to true',
|
||||
})
|
||||
.option('first-only', {
|
||||
describe:
|
||||
'If specified then only the first matching package.json property will be compiled.',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('create-ivy-entry-points', {
|
||||
describe:
|
||||
'If specified then new `*_ivy_ngcc` entry-points will be added to package.json rather than modifying the ones in-place.\n' +
|
||||
'For this to work you need to have custom resolution set up (e.g. in webpack) to look for these new entry-points.\n' +
|
||||
'The Angular CLI does this already, so it is safe to use this option if the project is being built via the CLI.',
|
||||
type: 'boolean',
|
||||
})
|
||||
.option('legacy-message-ids', {
|
||||
describe: 'Render `$localize` messages with legacy format ids.\n' +
|
||||
'The default value is `true`. Only set this to `false` if you do not want legacy message ids to\n' +
|
||||
'be rendered. For example, if you are not using legacy message ids in your translation files\n' +
|
||||
'AND are not doing compile-time inlining of translations, in which case the extra message ids\n' +
|
||||
'would add unwanted size to the final source bundle.\n' +
|
||||
'It is safe to leave this set to true if you are doing compile-time inlining because the extra\n' +
|
||||
'legacy message ids will all be stripped during translation.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
.option('async', {
|
||||
describe:
|
||||
'Whether to compile asynchronously. This is enabled by default as it allows compilations to be parallelized.\n' +
|
||||
'Disabling asynchronous compilation may be useful for debugging.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
.option('l', {
|
||||
alias: 'loglevel',
|
||||
describe: 'The lowest severity logging message that should be output.',
|
||||
choices: ['debug', 'info', 'warn', 'error'],
|
||||
})
|
||||
.option('invalidate-entry-point-manifest', {
|
||||
describe:
|
||||
'If this is set then ngcc will not read an entry-point manifest file from disk.\n' +
|
||||
'Instead it will walk the directory tree as normal looking for entry-points, and then write a new manifest file.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('error-on-failed-entry-point', {
|
||||
describe:
|
||||
'Set this option in order to terminate immediately with an error code if an entry-point fails to be processed.\n' +
|
||||
'If `-t`/`--target` is provided then this property is always true and cannot be changed. Otherwise the default is false.\n' +
|
||||
'When set to false, ngcc will continue to process entry-points after a failure. In which case it will log an error and resume processing other entry-points.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('tsconfig', {
|
||||
describe:
|
||||
'A path to a tsconfig.json file that will be used to configure the Angular compiler and module resolution used by ngcc.\n' +
|
||||
'If not provided, ngcc will attempt to read a `tsconfig.json` file from the folder above that given by the `-s` option.\n' +
|
||||
'Set to false (via `--no-tsconfig`) if you do not want ngcc to use any `tsconfig.json` file.',
|
||||
type: 'string',
|
||||
})
|
||||
.strict()
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
if (options['f'] && options['f'].length) {
|
||||
console.error(
|
||||
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
setFileSystem(new NodeJSFileSystem());
|
||||
|
||||
const baseSourcePath = resolve(options['s'] || './node_modules');
|
||||
const propertiesToConsider: string[] = options['p'];
|
||||
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
|
||||
const compileAllFormats = !options['first-only'];
|
||||
const createNewEntryPointFormats = options['create-ivy-entry-points'];
|
||||
const logLevel = options['l'] as keyof typeof LogLevel | undefined;
|
||||
const enableI18nLegacyMessageIdFormat = options['legacy-message-ids'];
|
||||
const invalidateEntryPointManifest = options['invalidate-entry-point-manifest'];
|
||||
const errorOnFailedEntryPoint = options['error-on-failed-entry-point'];
|
||||
// yargs is not so great at mixed string+boolean types, so we have to test tsconfig against a
|
||||
// string "false" to capture the `tsconfig=false` option.
|
||||
// And we have to convert the option to a string to handle `no-tsconfig`, which will be `false`.
|
||||
const tsConfigPath = `${options['tsconfig']}` === 'false' ? null : options['tsconfig'];
|
||||
|
||||
const logger = logLevel && new ConsoleLogger(LogLevel[logLevel]);
|
||||
|
||||
return {
|
||||
basePath: baseSourcePath,
|
||||
propertiesToConsider,
|
||||
targetEntryPointPath,
|
||||
compileAllFormats,
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
async: options['async'],
|
||||
invalidateEntryPointManifest,
|
||||
errorOnFailedEntryPoint,
|
||||
tsConfigPath
|
||||
};
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {PathMappings} from '../utils';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
import {EsmDependencyHost} from './esm_dependency_host';
|
||||
import {ModuleResolver} from './module_resolver';
|
||||
|
||||
|
@ -6,7 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {absoluteFrom, AbsoluteFsPath, dirname, FileSystem, isRoot, join, resolve} from '../../../src/ngtsc/file_system';
|
||||
import {isRelativePath, PathMappings, resolveFileWithPostfixes} from '../utils';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
import {isRelativePath, resolveFileWithPostfixes} from '../utils';
|
||||
|
||||
/**
|
||||
* This is a very cut-down implementation of the TypeScript module resolution strategy.
|
||||
|
@ -9,10 +9,10 @@ import {AbsoluteFsPath, FileSystem, PathSegment} from '../../../src/ngtsc/file_s
|
||||
import {EntryPointWithDependencies} from '../dependencies/dependency_host';
|
||||
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
import {NgccConfiguration} from '../packages/configuration';
|
||||
import {getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from '../packages/entry_point';
|
||||
import {EntryPointManifest} from '../packages/entry_point_manifest';
|
||||
import {PathMappings} from '../utils';
|
||||
import {NGCC_DIRECTORY} from '../writing/new_entry_point_file_writer';
|
||||
|
||||
import {EntryPointFinder} from './interface';
|
||||
|
@ -9,10 +9,10 @@ import {AbsoluteFsPath, FileSystem, join, PathSegment, relative, relativeFrom} f
|
||||
import {EntryPointWithDependencies} from '../dependencies/dependency_host';
|
||||
import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
import {hasBeenProcessed} from '../packages/build_marker';
|
||||
import {NgccConfiguration} from '../packages/configuration';
|
||||
import {EntryPoint, EntryPointJsonProperty, getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT} from '../packages/entry_point';
|
||||
import {PathMappings} from '../utils';
|
||||
|
||||
import {EntryPointFinder} from './interface';
|
||||
import {getBasePaths} from './utils';
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
import {AbsoluteFsPath, getFileSystem, relative, resolve} from '../../../src/ngtsc/file_system';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {PathMappings} from '../utils';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
|
||||
/**
|
||||
* Extract all the base-paths that we need to search for entry-points.
|
||||
|
172
packages/compiler-cli/ngcc/src/execution/analyze_entry_points.ts
Normal file
172
packages/compiler-cli/ngcc/src/execution/analyze_entry_points.ts
Normal file
@ -0,0 +1,172 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {DepGraph} from 'dependency-graph';
|
||||
|
||||
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {InvalidEntryPoint} from '../dependencies/dependency_resolver';
|
||||
import {EntryPointFinder} from '../entry_point_finder/interface';
|
||||
import {ParallelTaskQueue} from '../execution/tasks/queues/parallel_task_queue';
|
||||
import {SerialTaskQueue} from '../execution/tasks/queues/serial_task_queue';
|
||||
import {computeTaskDependencies} from '../execution/tasks/utils';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {hasBeenProcessed} from '../packages/build_marker';
|
||||
import {EntryPoint, EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../packages/entry_point';
|
||||
import {cleanOutdatedPackages} from '../writing/cleaning/package_cleaner';
|
||||
|
||||
import {AnalyzeEntryPointsFn} from './api';
|
||||
import {PartiallyOrderedTasks, TaskQueue} from './tasks/api';
|
||||
|
||||
/**
|
||||
* Create the function for performing the analysis of the entry-points.
|
||||
*/
|
||||
export function getAnalyzeEntryPointsFn(
|
||||
logger: Logger, finder: EntryPointFinder, fileSystem: FileSystem,
|
||||
supportedPropertiesToConsider: EntryPointJsonProperty[], compileAllFormats: boolean,
|
||||
propertiesToConsider: string[], inParallel: boolean): AnalyzeEntryPointsFn {
|
||||
return () => {
|
||||
logger.debug('Analyzing entry-points...');
|
||||
const startTime = Date.now();
|
||||
|
||||
let entryPointInfo = finder.findEntryPoints();
|
||||
const cleaned = cleanOutdatedPackages(fileSystem, entryPointInfo.entryPoints);
|
||||
if (cleaned) {
|
||||
// If we had to clean up one or more packages then we must read in the entry-points again.
|
||||
entryPointInfo = finder.findEntryPoints();
|
||||
}
|
||||
|
||||
const {entryPoints, invalidEntryPoints, graph} = entryPointInfo;
|
||||
logInvalidEntryPoints(logger, invalidEntryPoints);
|
||||
|
||||
const unprocessableEntryPointPaths: string[] = [];
|
||||
// The tasks are partially ordered by virtue of the entry-points being partially ordered too.
|
||||
const tasks: PartiallyOrderedTasks = [] as any;
|
||||
|
||||
for (const entryPoint of entryPoints) {
|
||||
const packageJson = entryPoint.packageJson;
|
||||
const hasProcessedTypings = hasBeenProcessed(packageJson, 'typings');
|
||||
const {propertiesToProcess, equivalentPropertiesMap} =
|
||||
getPropertiesToProcess(packageJson, supportedPropertiesToConsider, compileAllFormats);
|
||||
let processDts = !hasProcessedTypings;
|
||||
|
||||
if (propertiesToProcess.length === 0) {
|
||||
// This entry-point is unprocessable (i.e. there is no format property that is of interest
|
||||
// and can be processed). This will result in an error, but continue looping over
|
||||
// entry-points in order to collect all unprocessable ones and display a more informative
|
||||
// error.
|
||||
unprocessableEntryPointPaths.push(entryPoint.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const formatProperty of propertiesToProcess) {
|
||||
if (hasBeenProcessed(entryPoint.packageJson, formatProperty)) {
|
||||
// The format-path which the property maps to is already processed - nothing to do.
|
||||
logger.debug(`Skipping ${entryPoint.name} : ${formatProperty} (already compiled).`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const formatPropertiesToMarkAsProcessed = equivalentPropertiesMap.get(formatProperty)!;
|
||||
tasks.push({entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts});
|
||||
|
||||
// Only process typings for the first property (if not already processed).
|
||||
processDts = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for entry-points for which we could not process any format at all.
|
||||
if (unprocessableEntryPointPaths.length > 0) {
|
||||
throw new Error(
|
||||
'Unable to process any formats for the following entry-points (tried ' +
|
||||
`${propertiesToConsider.join(', ')}): ` +
|
||||
unprocessableEntryPointPaths.map(path => `\n - ${path}`).join(''));
|
||||
}
|
||||
|
||||
const duration = Math.round((Date.now() - startTime) / 100) / 10;
|
||||
logger.debug(
|
||||
`Analyzed ${entryPoints.length} entry-points in ${duration}s. ` +
|
||||
`(Total tasks: ${tasks.length})`);
|
||||
|
||||
return getTaskQueue(logger, inParallel, tasks, graph);
|
||||
};
|
||||
}
|
||||
|
||||
function logInvalidEntryPoints(logger: Logger, invalidEntryPoints: InvalidEntryPoint[]): void {
|
||||
invalidEntryPoints.forEach(invalidEntryPoint => {
|
||||
logger.debug(
|
||||
`Invalid entry-point ${invalidEntryPoint.entryPoint.path}.`,
|
||||
`It is missing required dependencies:\n` +
|
||||
invalidEntryPoint.missingDependencies.map(dep => ` - ${dep}`).join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function computes and returns the following:
|
||||
* - `propertiesToProcess`: An (ordered) list of properties that exist and need to be processed,
|
||||
* based on the provided `propertiesToConsider`, the properties in `package.json` and their
|
||||
* corresponding format-paths. NOTE: Only one property per format-path needs to be processed.
|
||||
* - `equivalentPropertiesMap`: A mapping from each property in `propertiesToProcess` to the list of
|
||||
* other format properties in `package.json` that need to be marked as processed as soon as the
|
||||
* former has been processed.
|
||||
*/
|
||||
function getPropertiesToProcess(
|
||||
packageJson: EntryPointPackageJson, propertiesToConsider: EntryPointJsonProperty[],
|
||||
compileAllFormats: boolean): {
|
||||
propertiesToProcess: EntryPointJsonProperty[];
|
||||
equivalentPropertiesMap: Map<EntryPointJsonProperty, EntryPointJsonProperty[]>;
|
||||
} {
|
||||
const formatPathsToConsider = new Set<string>();
|
||||
|
||||
const propertiesToProcess: EntryPointJsonProperty[] = [];
|
||||
for (const prop of propertiesToConsider) {
|
||||
const formatPath = packageJson[prop];
|
||||
|
||||
// Ignore properties that are not defined in `package.json`.
|
||||
if (typeof formatPath !== 'string') continue;
|
||||
|
||||
// Ignore properties that map to the same format-path as a preceding property.
|
||||
if (formatPathsToConsider.has(formatPath)) continue;
|
||||
|
||||
// Process this property, because it is the first one to map to this format-path.
|
||||
formatPathsToConsider.add(formatPath);
|
||||
propertiesToProcess.push(prop);
|
||||
|
||||
// If we only need one format processed, there is no need to process any more properties.
|
||||
if (!compileAllFormats) break;
|
||||
}
|
||||
|
||||
const formatPathToProperties: {[formatPath: string]: EntryPointJsonProperty[]} = {};
|
||||
for (const prop of SUPPORTED_FORMAT_PROPERTIES) {
|
||||
const formatPath = packageJson[prop];
|
||||
|
||||
// Ignore properties that are not defined in `package.json`.
|
||||
if (typeof formatPath !== 'string') continue;
|
||||
|
||||
// Ignore properties that do not map to a format-path that will be considered.
|
||||
if (!formatPathsToConsider.has(formatPath)) continue;
|
||||
|
||||
// Add this property to the map.
|
||||
const list = formatPathToProperties[formatPath] || (formatPathToProperties[formatPath] = []);
|
||||
list.push(prop);
|
||||
}
|
||||
|
||||
const equivalentPropertiesMap = new Map<EntryPointJsonProperty, EntryPointJsonProperty[]>();
|
||||
for (const prop of propertiesToConsider) {
|
||||
const formatPath = packageJson[prop]!;
|
||||
const equivalentProperties = formatPathToProperties[formatPath];
|
||||
equivalentPropertiesMap.set(prop, equivalentProperties);
|
||||
}
|
||||
|
||||
return {propertiesToProcess, equivalentPropertiesMap};
|
||||
}
|
||||
|
||||
function getTaskQueue(
|
||||
logger: Logger, inParallel: boolean, tasks: PartiallyOrderedTasks,
|
||||
graph: DepGraph<EntryPoint>): TaskQueue {
|
||||
const dependencies = computeTaskDependencies(tasks, graph);
|
||||
return inParallel ? new ParallelTaskQueue(logger, tasks, dependencies) :
|
||||
new SerialTaskQueue(logger, tasks, dependencies);
|
||||
}
|
@ -5,11 +5,7 @@
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as cluster from 'cluster';
|
||||
|
||||
import {FileSystem} from '../../../../src/ngtsc/file_system';
|
||||
import {AsyncLocker} from '../../locking/async_locker';
|
||||
import {Logger} from '../../logging/logger';
|
||||
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
||||
@ -17,8 +13,6 @@ import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
|
||||
import {CreateTaskCompletedCallback} from '../tasks/api';
|
||||
|
||||
import {ClusterMaster} from './master';
|
||||
import {ClusterWorker} from './worker';
|
||||
|
||||
|
||||
/**
|
||||
* An `Executor` that processes tasks in parallel (on multiple processes) and completes
|
||||
@ -26,26 +20,19 @@ import {ClusterWorker} from './worker';
|
||||
*/
|
||||
export class ClusterExecutor implements Executor {
|
||||
constructor(
|
||||
private workerCount: number, private logger: Logger,
|
||||
private workerCount: number, private fileSystem: FileSystem, private logger: Logger,
|
||||
private pkgJsonUpdater: PackageJsonUpdater, private lockFile: AsyncLocker,
|
||||
private createTaskCompletedCallback: CreateTaskCompletedCallback) {}
|
||||
|
||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
|
||||
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
|
||||
Promise<void> {
|
||||
if (cluster.isMaster) {
|
||||
// This process is the cluster master.
|
||||
return this.lockFile.lock(() => {
|
||||
this.logger.debug(`Running ngcc on ${this.constructor.name} (using ${
|
||||
this.workerCount} worker processes).`);
|
||||
const master = new ClusterMaster(
|
||||
this.workerCount, this.logger, this.pkgJsonUpdater, analyzeEntryPoints,
|
||||
this.createTaskCompletedCallback);
|
||||
return master.run();
|
||||
});
|
||||
} else {
|
||||
// This process is a cluster worker.
|
||||
const worker = new ClusterWorker(this.logger, createCompileFn);
|
||||
return worker.run();
|
||||
}
|
||||
return this.lockFile.lock(() => {
|
||||
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.pkgJsonUpdater, analyzeEntryPoints,
|
||||
this.createTaskCompletedCallback);
|
||||
return master.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import {ChildProcess} from 'child_process';
|
||||
import * as cluster from 'cluster';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
|
||||
import {LockFileWithChildProcess} from '../../locking/lock_file_with_child_process';
|
||||
|
||||
|
||||
/**
|
||||
* A `LockFileWithChildProcess` that is `cluster`-aware and does not spawn unlocker processes from
|
||||
* worker processes (only from the master process, which does the locking).
|
||||
*/
|
||||
export class ClusterLockFileWithChildProcess extends LockFileWithChildProcess {
|
||||
write(): void {
|
||||
if (!cluster.isMaster) {
|
||||
// This is a worker process:
|
||||
// This method should only be on the master process.
|
||||
throw new Error('Tried to create a lock-file from a worker process.');
|
||||
}
|
||||
|
||||
return super.write();
|
||||
}
|
||||
|
||||
protected createUnlocker(path: AbsoluteFsPath): ChildProcess|null {
|
||||
if (cluster.isMaster) {
|
||||
// This is the master process:
|
||||
// Create the unlocker.
|
||||
return super.createUnlocker(path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as cluster from 'cluster';
|
||||
|
||||
import {resolve} from '../../../../src/ngtsc/file_system';
|
||||
import {FileSystem} from '../../../../src/ngtsc/file_system';
|
||||
import {Logger} from '../../logging/logger';
|
||||
import {PackageJsonUpdater} from '../../writing/package_json_updater';
|
||||
import {AnalyzeEntryPointsFn} from '../api';
|
||||
@ -33,13 +33,16 @@ export class ClusterMaster {
|
||||
private onTaskCompleted: TaskCompletedCallback;
|
||||
|
||||
constructor(
|
||||
private maxWorkerCount: number, private logger: Logger,
|
||||
private maxWorkerCount: number, private fileSystem: FileSystem, private logger: Logger,
|
||||
private pkgJsonUpdater: PackageJsonUpdater, analyzeEntryPoints: AnalyzeEntryPointsFn,
|
||||
createTaskCompletedCallback: CreateTaskCompletedCallback) {
|
||||
if (!cluster.isMaster) {
|
||||
throw new Error('Tried to instantiate `ClusterMaster` on a worker process.');
|
||||
}
|
||||
|
||||
// Set the worker entry-point
|
||||
cluster.setupMaster({exec: this.fileSystem.resolve(__dirname, 'worker.js')});
|
||||
|
||||
this.taskQueue = analyzeEntryPoints();
|
||||
this.onTaskCompleted = createTaskCompletedCallback(this.taskQueue);
|
||||
}
|
||||
@ -227,7 +230,7 @@ export class ClusterMaster {
|
||||
JSON.stringify(msg));
|
||||
}
|
||||
|
||||
const expectedPackageJsonPath = resolve(task.entryPoint.path, 'package.json');
|
||||
const expectedPackageJsonPath = this.fileSystem.resolve(task.entryPoint.path, 'package.json');
|
||||
const parsedPackageJson = task.entryPoint.packageJson;
|
||||
|
||||
if (expectedPackageJsonPath !== msg.packageJsonPath) {
|
||||
|
@ -18,26 +18,26 @@ import {sendMessageToMaster} from './utils';
|
||||
|
||||
|
||||
/**
|
||||
* A `PackageJsonUpdater` that can safely handle update operations on multiple processes.
|
||||
* A `PackageJsonUpdater` for cluster workers that will send update changes to the master process so
|
||||
* that it can safely handle update operations on multiple processes.
|
||||
*/
|
||||
export class ClusterPackageJsonUpdater implements PackageJsonUpdater {
|
||||
constructor(private delegate: PackageJsonUpdater) {}
|
||||
export class ClusterWorkerPackageJsonUpdater implements PackageJsonUpdater {
|
||||
constructor() {
|
||||
if (cluster.isMaster) {
|
||||
throw new Error('Tried to create cluster worker PackageJsonUpdater on the master process.');
|
||||
}
|
||||
}
|
||||
|
||||
createUpdate(): PackageJsonUpdate {
|
||||
return new PackageJsonUpdate((...args) => this.writeChanges(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the changes in-memory (if necessary) and send a message to the master process.
|
||||
*/
|
||||
writeChanges(
|
||||
changes: PackageJsonChange[], packageJsonPath: AbsoluteFsPath,
|
||||
preExistingParsedJson?: JsonObject): void {
|
||||
if (cluster.isMaster) {
|
||||
// This is the master process:
|
||||
// Actually apply the changes to the file on disk.
|
||||
return this.delegate.writeChanges(changes, packageJsonPath, preExistingParsedJson);
|
||||
}
|
||||
|
||||
// This is a worker process:
|
||||
// Apply the changes in-memory (if necessary) and send a message to the master process.
|
||||
if (preExistingParsedJson) {
|
||||
for (const [propPath, value] of changes) {
|
||||
if (propPath.length === 0) {
|
||||
|
@ -5,58 +5,86 @@
|
||||
* 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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as cluster from 'cluster';
|
||||
|
||||
import {Logger} from '../../logging/logger';
|
||||
import {CompileFn, CreateCompileFn} from '../api';
|
||||
import {parseCommandLineOptions} from '../../command_line_options';
|
||||
import {ConsoleLogger} from '../../logging/console_logger';
|
||||
import {Logger, LogLevel} from '../../logging/logger';
|
||||
import {getSharedSetup} from '../../ngcc_options';
|
||||
import {CreateCompileFn} from '../api';
|
||||
import {getCreateCompileFn} from '../create_compile_function';
|
||||
import {stringifyTask} from '../tasks/utils';
|
||||
|
||||
import {MessageToWorker} from './api';
|
||||
import {ClusterWorkerPackageJsonUpdater} from './package_json_updater';
|
||||
import {sendMessageToMaster} from './utils';
|
||||
|
||||
// Cluster worker entry point
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
process.title = 'ngcc (worker)';
|
||||
|
||||
/**
|
||||
* A cluster worker is responsible for processing one task (i.e. one format property for a specific
|
||||
* entry-point) at a time and reporting results back to the cluster master.
|
||||
*/
|
||||
export class ClusterWorker {
|
||||
private compile: CompileFn;
|
||||
try {
|
||||
const {
|
||||
createNewEntryPointFormats = false,
|
||||
logger = new ConsoleLogger(LogLevel.info),
|
||||
pathMappings,
|
||||
errorOnFailedEntryPoint = false,
|
||||
enableI18nLegacyMessageIdFormat = true,
|
||||
fileSystem,
|
||||
tsConfig
|
||||
} = getSharedSetup(parseCommandLineOptions(process.argv.slice(2)));
|
||||
|
||||
constructor(private logger: Logger, createCompileFn: CreateCompileFn) {
|
||||
if (cluster.isMaster) {
|
||||
throw new Error('Tried to instantiate `ClusterWorker` on the master process.');
|
||||
// NOTE: To avoid file corruption, `ngcc` invocation only creates _one_ instance of
|
||||
// `PackageJsonUpdater` that actually writes to disk (across all processes).
|
||||
// In cluster workers we use a `PackageJsonUpdater` that delegates to the cluster master.
|
||||
const pkgJsonUpdater = new ClusterWorkerPackageJsonUpdater();
|
||||
|
||||
// The function for creating the `compile()` function.
|
||||
const createCompileFn = getCreateCompileFn(
|
||||
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint,
|
||||
enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
|
||||
|
||||
await startWorker(logger, createCompileFn);
|
||||
process.exitCode = 0;
|
||||
} catch (e) {
|
||||
console.error(e.stack || e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.compile = createCompileFn(
|
||||
(_task, outcome, message) =>
|
||||
sendMessageToMaster({type: 'task-completed', outcome, message}));
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
// Listen for `ProcessTaskMessage`s and process tasks.
|
||||
cluster.worker.on('message', (msg: MessageToWorker) => {
|
||||
try {
|
||||
switch (msg.type) {
|
||||
case 'process-task':
|
||||
this.logger.debug(
|
||||
`[Worker #${cluster.worker.id}] Processing task: ${stringifyTask(msg.task)}`);
|
||||
return this.compile(msg.task);
|
||||
default:
|
||||
throw new Error(
|
||||
`[Worker #${cluster.worker.id}] Invalid message received: ${JSON.stringify(msg)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
sendMessageToMaster({
|
||||
type: 'error',
|
||||
error: (err instanceof Error) ? (err.stack || err.message) : err,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Return a promise that is never resolved.
|
||||
return new Promise(() => undefined);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export async function startWorker(logger: Logger, createCompileFn: CreateCompileFn): Promise<void> {
|
||||
if (cluster.isMaster) {
|
||||
throw new Error('Tried to run cluster worker on the master process.');
|
||||
}
|
||||
|
||||
const compile = createCompileFn(
|
||||
(_task, outcome, message) => sendMessageToMaster({type: 'task-completed', outcome, message}));
|
||||
|
||||
|
||||
// Listen for `ProcessTaskMessage`s and process tasks.
|
||||
cluster.worker.on('message', (msg: MessageToWorker) => {
|
||||
try {
|
||||
switch (msg.type) {
|
||||
case 'process-task':
|
||||
logger.debug(
|
||||
`[Worker #${cluster.worker.id}] Processing task: ${stringifyTask(msg.task)}`);
|
||||
return compile(msg.task);
|
||||
default:
|
||||
throw new Error(
|
||||
`[Worker #${cluster.worker.id}] Invalid message received: ${JSON.stringify(msg)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
sendMessageToMaster({
|
||||
type: 'error',
|
||||
error: (err instanceof Error) ? (err.stack || err.message) : err,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Return a promise that is never resolved.
|
||||
return new Promise(() => undefined);
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {replaceTsWithNgInErrors} from '../../../src/ngtsc/diagnostics';
|
||||
import {FileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {ParsedConfiguration} from '../../../src/perform_compile';
|
||||
import {Logger} from '../logging/logger';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
import {getEntryPointFormat} from '../packages/entry_point';
|
||||
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
|
||||
import {FileWriter} from '../writing/file_writer';
|
||||
import {InPlaceFileWriter} from '../writing/in_place_file_writer';
|
||||
import {NewEntryPointFileWriter} from '../writing/new_entry_point_file_writer';
|
||||
import {PackageJsonUpdater} from '../writing/package_json_updater';
|
||||
|
||||
import {CreateCompileFn} from './api';
|
||||
import {Task, TaskProcessingOutcome} from './tasks/api';
|
||||
|
||||
/**
|
||||
* The function for creating the `compile()` function.
|
||||
*/
|
||||
export function getCreateCompileFn(
|
||||
fileSystem: FileSystem, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
||||
createNewEntryPointFormats: boolean, errorOnFailedEntryPoint: boolean,
|
||||
enableI18nLegacyMessageIdFormat: boolean, tsConfig: ParsedConfiguration|null,
|
||||
pathMappings: PathMappings|undefined): CreateCompileFn {
|
||||
return onTaskCompleted => {
|
||||
const fileWriter = getFileWriter(
|
||||
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint);
|
||||
const {Transformer} = require('../packages/transformer');
|
||||
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
||||
|
||||
return (task: Task) => {
|
||||
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
||||
|
||||
const isCore = entryPoint.name === '@angular/core'; // Are we compiling the Angular core?
|
||||
const packageJson = entryPoint.packageJson;
|
||||
const formatPath = packageJson[formatProperty];
|
||||
const format = getEntryPointFormat(fileSystem, entryPoint, formatProperty);
|
||||
|
||||
// All properties listed in `propertiesToProcess` are guaranteed to point to a format-path
|
||||
// (i.e. they are defined in `entryPoint.packageJson`). Furthermore, they are also guaranteed
|
||||
// to be among `SUPPORTED_FORMAT_PROPERTIES`.
|
||||
// Based on the above, `formatPath` should always be defined and `getEntryPointFormat()`
|
||||
// should always return a format here (and not `undefined`).
|
||||
if (!formatPath || !format) {
|
||||
// This should never happen.
|
||||
throw new Error(
|
||||
`Invariant violated: No format-path or format for ${entryPoint.path} : ` +
|
||||
`${formatProperty} (formatPath: ${formatPath} | format: ${format})`);
|
||||
}
|
||||
|
||||
const bundle = makeEntryPointBundle(
|
||||
fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true,
|
||||
enableI18nLegacyMessageIdFormat);
|
||||
|
||||
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
|
||||
|
||||
const result = transformer.transform(bundle);
|
||||
if (result.success) {
|
||||
if (result.diagnostics.length > 0) {
|
||||
logger.warn(replaceTsWithNgInErrors(
|
||||
ts.formatDiagnosticsWithColorAndContext(result.diagnostics, bundle.src.host)));
|
||||
}
|
||||
fileWriter.writeBundle(bundle, result.transformedFiles, formatPropertiesToMarkAsProcessed);
|
||||
|
||||
logger.debug(` Successfully compiled ${entryPoint.name} : ${formatProperty}`);
|
||||
|
||||
onTaskCompleted(task, TaskProcessingOutcome.Processed, null);
|
||||
} else {
|
||||
const errors = replaceTsWithNgInErrors(
|
||||
ts.formatDiagnosticsWithColorAndContext(result.diagnostics, bundle.src.host));
|
||||
onTaskCompleted(task, TaskProcessingOutcome.Failed, `compilation errors:\n${errors}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getFileWriter(
|
||||
fs: FileSystem, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
||||
createNewEntryPointFormats: boolean, errorOnFailedEntryPoint: boolean): FileWriter {
|
||||
return createNewEntryPointFormats ?
|
||||
new NewEntryPointFileWriter(fs, logger, errorOnFailedEntryPoint, pkgJsonUpdater) :
|
||||
new InPlaceFileWriter(fs, logger, errorOnFailedEntryPoint);
|
||||
}
|
@ -5,10 +5,9 @@
|
||||
* 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 {ChildProcess, ChildProcessByStdio, fork} from 'child_process';
|
||||
import {Readable, Writable} from 'stream';
|
||||
import {ChildProcess, fork} from 'child_process';
|
||||
|
||||
import {AbsoluteFsPath, CachedFileSystem, FileSystem} from '../../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, FileSystem} from '../../../../src/ngtsc/file_system';
|
||||
import {Logger, LogLevel} from '../../logging/logger';
|
||||
import {getLockFilePath, LockFile} from '../lock_file';
|
||||
|
||||
@ -58,11 +57,6 @@ export class LockFileWithChildProcess implements LockFile {
|
||||
|
||||
read(): string {
|
||||
try {
|
||||
if (this.fs instanceof CachedFileSystem) {
|
||||
// The lock-file file is "volatile", it might be changed by an external process,
|
||||
// so we must not rely upon the cached value when reading it.
|
||||
this.fs.invalidateCaches(this.path);
|
||||
}
|
||||
return this.fs.readFile(this.path);
|
||||
} catch {
|
||||
return '{unknown}';
|
||||
@ -78,18 +72,18 @@ export class LockFileWithChildProcess implements LockFile {
|
||||
}
|
||||
}
|
||||
|
||||
protected createUnlocker(path: AbsoluteFsPath): ChildProcess|null {
|
||||
protected createUnlocker(path: AbsoluteFsPath): ChildProcess {
|
||||
this.logger.debug('Forking unlocker child-process');
|
||||
const logLevel =
|
||||
this.logger.level !== undefined ? this.logger.level.toString() : LogLevel.info.toString();
|
||||
|
||||
const unlocker = fork(this.fs.resolve(__dirname, './unlocker.js'), [path, logLevel], {
|
||||
detached: true,
|
||||
stdio: 'pipe',
|
||||
}) as ChildProcessByStdio<Writable, Readable, Readable>;
|
||||
unlocker.stdout.on('data', data => process.stdout.write(data));
|
||||
unlocker.stderr.on('data', data => process.stderr.write(data));
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const unlocker = fork(
|
||||
this.fs.resolve(__dirname, './unlocker.js'), [path, logLevel],
|
||||
{detached: true, stdio: isWindows ? 'pipe' : 'inherit'});
|
||||
if (isWindows) {
|
||||
unlocker.stdout?.on('data', process.stdout.write.bind(process.stdout));
|
||||
unlocker.stderr?.on('data', process.stderr.write.bind(process.stderr));
|
||||
}
|
||||
return unlocker;
|
||||
}
|
||||
}
|
||||
|
@ -8,16 +8,12 @@
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import {DepGraph} from 'dependency-graph';
|
||||
import * as os from 'os';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {readConfiguration} from '../..';
|
||||
import {replaceTsWithNgInErrors} from '../../src/ngtsc/diagnostics';
|
||||
import {absoluteFrom, AbsoluteFsPath, dirname, FileSystem, getFileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, FileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||
|
||||
import {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host';
|
||||
import {DependencyResolver, InvalidEntryPoint} from './dependencies/dependency_resolver';
|
||||
import {DependencyResolver} from './dependencies/dependency_resolver';
|
||||
import {DtsDependencyHost} from './dependencies/dts_dependency_host';
|
||||
import {EsmDependencyHost} from './dependencies/esm_dependency_host';
|
||||
import {ModuleResolver} from './dependencies/module_resolver';
|
||||
@ -25,152 +21,23 @@ import {UmdDependencyHost} from './dependencies/umd_dependency_host';
|
||||
import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder';
|
||||
import {EntryPointFinder} from './entry_point_finder/interface';
|
||||
import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder';
|
||||
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from './execution/api';
|
||||
import {getAnalyzeEntryPointsFn} from './execution/analyze_entry_points';
|
||||
import {Executor} from './execution/api';
|
||||
import {ClusterExecutor} from './execution/cluster/executor';
|
||||
import {ClusterLockFileWithChildProcess} from './execution/cluster/lock_file_with_child_process';
|
||||
import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater';
|
||||
import {getCreateCompileFn} from './execution/create_compile_function';
|
||||
import {SingleProcessExecutorAsync, SingleProcessExecutorSync} from './execution/single_process_executor';
|
||||
import {CreateTaskCompletedCallback, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/tasks/api';
|
||||
import {CreateTaskCompletedCallback, TaskProcessingOutcome} from './execution/tasks/api';
|
||||
import {composeTaskCompletedCallbacks, createLogErrorHandler, createMarkAsProcessedHandler, createThrowErrorHandler} from './execution/tasks/completion';
|
||||
import {ParallelTaskQueue} from './execution/tasks/queues/parallel_task_queue';
|
||||
import {SerialTaskQueue} from './execution/tasks/queues/serial_task_queue';
|
||||
import {computeTaskDependencies} from './execution/tasks/utils';
|
||||
import {AsyncLocker} from './locking/async_locker';
|
||||
import {LockFileWithChildProcess} from './locking/lock_file_with_child_process';
|
||||
import {SyncLocker} from './locking/sync_locker';
|
||||
import {ConsoleLogger} from './logging/console_logger';
|
||||
import {Logger, LogLevel} from './logging/logger';
|
||||
import {hasBeenProcessed} from './packages/build_marker';
|
||||
import {Logger} from './logging/logger';
|
||||
import {AsyncNgccOptions, getSharedSetup, NgccOptions, PathMappings, SyncNgccOptions} from './ngcc_options';
|
||||
import {NgccConfiguration} from './packages/configuration';
|
||||
import {EntryPoint, EntryPointJsonProperty, EntryPointPackageJson, getEntryPointFormat, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
|
||||
import {makeEntryPointBundle} from './packages/entry_point_bundle';
|
||||
import {EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
|
||||
import {EntryPointManifest, InvalidatingEntryPointManifest} from './packages/entry_point_manifest';
|
||||
import {PathMappings} from './utils';
|
||||
import {cleanOutdatedPackages} from './writing/cleaning/package_cleaner';
|
||||
import {FileWriter} from './writing/file_writer';
|
||||
import {InPlaceFileWriter} from './writing/in_place_file_writer';
|
||||
import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer';
|
||||
import {DirectPackageJsonUpdater, PackageJsonUpdater} from './writing/package_json_updater';
|
||||
|
||||
/**
|
||||
* The options to configure the ngcc compiler for synchronous execution.
|
||||
*/
|
||||
export interface SyncNgccOptions {
|
||||
/** The absolute path to the `node_modules` folder that contains the packages to process. */
|
||||
basePath: string;
|
||||
|
||||
/**
|
||||
* The path to the primary package to be processed. If not absolute then it must be relative to
|
||||
* `basePath`.
|
||||
*
|
||||
* All its dependencies will need to be processed too.
|
||||
*
|
||||
* If this property is provided then `errorOnFailedEntryPoint` is forced to true.
|
||||
*/
|
||||
targetEntryPointPath?: string;
|
||||
|
||||
/**
|
||||
* Which entry-point properties in the package.json to consider when processing an entry-point.
|
||||
* Each property should hold a path to the particular bundle format for the entry-point.
|
||||
* Defaults to all the properties in the package.json.
|
||||
*/
|
||||
propertiesToConsider?: string[];
|
||||
|
||||
/**
|
||||
* Whether to process all formats specified by (`propertiesToConsider`) or to stop processing
|
||||
* this entry-point at the first matching format. Defaults to `true`.
|
||||
*/
|
||||
compileAllFormats?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to create new entry-points bundles rather than overwriting the original files.
|
||||
*/
|
||||
createNewEntryPointFormats?: boolean;
|
||||
|
||||
/**
|
||||
* Provide a logger that will be called with log messages.
|
||||
*/
|
||||
logger?: Logger;
|
||||
|
||||
/**
|
||||
* Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`.
|
||||
* These are used to resolve paths to locally built Angular libraries.
|
||||
*
|
||||
* Note that `pathMappings` specified here take precedence over any `pathMappings` loaded from a
|
||||
* TS config file.
|
||||
*/
|
||||
pathMappings?: PathMappings;
|
||||
|
||||
/**
|
||||
* Provide a file-system service that will be used by ngcc for all file interactions.
|
||||
*/
|
||||
fileSystem?: FileSystem;
|
||||
|
||||
/**
|
||||
* Whether the compilation should run and return asynchronously. Allowing asynchronous execution
|
||||
* may speed up the compilation by utilizing multiple CPU cores (if available).
|
||||
*
|
||||
* Default: `false` (i.e. run synchronously)
|
||||
*/
|
||||
async?: false;
|
||||
|
||||
/**
|
||||
* Set to true in order to terminate immediately with an error code if an entry-point fails to be
|
||||
* processed.
|
||||
*
|
||||
* If `targetEntryPointPath` is provided then this property is always true and cannot be
|
||||
* changed. Otherwise the default is false.
|
||||
*
|
||||
* When set to false, ngcc will continue to process entry-points after a failure. In which case it
|
||||
* will log an error and resume processing other entry-points.
|
||||
*/
|
||||
errorOnFailedEntryPoint?: boolean;
|
||||
|
||||
/**
|
||||
* Render `$localize` messages with legacy format ids.
|
||||
*
|
||||
* The default value is `true`. Only set this to `false` if you do not want legacy message ids to
|
||||
* be rendered. For example, if you are not using legacy message ids in your translation files
|
||||
* AND are not doing compile-time inlining of translations, in which case the extra message ids
|
||||
* would add unwanted size to the final source bundle.
|
||||
*
|
||||
* It is safe to leave this set to true if you are doing compile-time inlining because the extra
|
||||
* legacy message ids will all be stripped during translation.
|
||||
*/
|
||||
enableI18nLegacyMessageIdFormat?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to invalidate any entry-point manifest file that is on disk. Instead, walk the
|
||||
* directory tree looking for entry-points, and then write a new entry-point manifest, if
|
||||
* possible.
|
||||
*
|
||||
* Default: `false` (i.e. the manifest will be used if available)
|
||||
*/
|
||||
invalidateEntryPointManifest?: boolean;
|
||||
|
||||
/**
|
||||
* An absolute path to a TS config file (e.g. `tsconfig.json`) or a directory containing one, that
|
||||
* will be used to configure module resolution with things like path mappings, if not specified
|
||||
* explicitly via the `pathMappings` property to `mainNgcc`.
|
||||
*
|
||||
* If `undefined`, ngcc will attempt to load a `tsconfig.json` file from the directory above the
|
||||
* `basePath`.
|
||||
*
|
||||
* If `null`, ngcc will not attempt to load any TS config file at all.
|
||||
*/
|
||||
tsConfigPath?: string|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options to configure the ngcc compiler for asynchronous execution.
|
||||
*/
|
||||
export type AsyncNgccOptions = Omit<SyncNgccOptions, 'async'>&{async: true};
|
||||
|
||||
/**
|
||||
* The options to configure the ngcc compiler.
|
||||
*/
|
||||
export type NgccOptions = AsyncNgccOptions|SyncNgccOptions;
|
||||
|
||||
/**
|
||||
* This is the main entry-point into ngcc (aNGular Compatibility Compiler).
|
||||
*
|
||||
@ -181,46 +48,26 @@ export type NgccOptions = AsyncNgccOptions|SyncNgccOptions;
|
||||
*/
|
||||
export function mainNgcc(options: AsyncNgccOptions): Promise<void>;
|
||||
export function mainNgcc(options: SyncNgccOptions): void;
|
||||
export function mainNgcc({
|
||||
basePath,
|
||||
targetEntryPointPath,
|
||||
propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||
compileAllFormats = true,
|
||||
createNewEntryPointFormats = false,
|
||||
logger = new ConsoleLogger(LogLevel.info),
|
||||
pathMappings,
|
||||
async = false,
|
||||
errorOnFailedEntryPoint = false,
|
||||
enableI18nLegacyMessageIdFormat = true,
|
||||
invalidateEntryPointManifest = false,
|
||||
tsConfigPath
|
||||
}: NgccOptions): void|Promise<void> {
|
||||
if (!!targetEntryPointPath) {
|
||||
// targetEntryPointPath forces us to error if an entry-point fails.
|
||||
errorOnFailedEntryPoint = true;
|
||||
}
|
||||
export function mainNgcc(options: NgccOptions): void|Promise<void> {
|
||||
const {
|
||||
basePath,
|
||||
targetEntryPointPath,
|
||||
propertiesToConsider,
|
||||
compileAllFormats,
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
pathMappings,
|
||||
async,
|
||||
errorOnFailedEntryPoint,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
invalidateEntryPointManifest,
|
||||
fileSystem,
|
||||
absBasePath,
|
||||
projectPath,
|
||||
tsConfig
|
||||
} = getSharedSetup(options);
|
||||
|
||||
// Execute in parallel, if async execution is acceptable and there are more than 1 CPU cores.
|
||||
const inParallel = async && (os.cpus().length > 1);
|
||||
|
||||
// Instantiate common utilities that are always used.
|
||||
// NOTE: Avoid eagerly instantiating anything that might not be used when running sync/async or in
|
||||
// master/worker process.
|
||||
const fileSystem = getFileSystem();
|
||||
const absBasePath = absoluteFrom(basePath);
|
||||
const projectPath = dirname(absBasePath);
|
||||
const config = new NgccConfiguration(fileSystem, projectPath);
|
||||
const tsConfig = tsConfigPath !== null ? readConfiguration(tsConfigPath || projectPath) : null;
|
||||
|
||||
// If `pathMappings` is not provided directly, then try getting it from `tsConfig`, if available.
|
||||
if (tsConfig !== null && pathMappings === undefined && tsConfig.options.baseUrl !== undefined &&
|
||||
tsConfig.options.paths) {
|
||||
pathMappings = {
|
||||
baseUrl: resolve(projectPath, tsConfig.options.baseUrl),
|
||||
paths: tsConfig.options.paths,
|
||||
};
|
||||
}
|
||||
|
||||
const dependencyResolver = getDependencyResolver(fileSystem, logger, config, pathMappings);
|
||||
const entryPointManifest = invalidateEntryPointManifest ?
|
||||
new InvalidatingEntryPointManifest(fileSystem, config, logger) :
|
||||
@ -239,129 +86,20 @@ export function mainNgcc({
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: To avoid file corruption, ensure that each `ngcc` invocation only creates _one_ instance
|
||||
// of `PackageJsonUpdater` that actually writes to disk (across all processes).
|
||||
// This is hard to enforce automatically, when running on multiple processes, so needs to be
|
||||
// enforced manually.
|
||||
const pkgJsonUpdater = getPackageJsonUpdater(inParallel, fileSystem);
|
||||
// Execute in parallel, if async execution is acceptable and there are more than 1 CPU cores.
|
||||
const inParallel = async && (os.cpus().length > 1);
|
||||
|
||||
// The function for performing the analysis.
|
||||
const analyzeEntryPoints: AnalyzeEntryPointsFn = () => {
|
||||
logger.debug('Analyzing entry-points...');
|
||||
const startTime = Date.now();
|
||||
const analyzeEntryPoints = getAnalyzeEntryPointsFn(
|
||||
logger, finder, fileSystem, supportedPropertiesToConsider, compileAllFormats,
|
||||
propertiesToConsider, inParallel);
|
||||
|
||||
let entryPointInfo = finder.findEntryPoints();
|
||||
const cleaned = cleanOutdatedPackages(fileSystem, entryPointInfo.entryPoints);
|
||||
if (cleaned) {
|
||||
// If we had to clean up one or more packages then we must read in the entry-points again.
|
||||
entryPointInfo = finder.findEntryPoints();
|
||||
}
|
||||
|
||||
const {entryPoints, invalidEntryPoints, graph} = entryPointInfo;
|
||||
logInvalidEntryPoints(logger, invalidEntryPoints);
|
||||
|
||||
const unprocessableEntryPointPaths: string[] = [];
|
||||
// The tasks are partially ordered by virtue of the entry-points being partially ordered too.
|
||||
const tasks: PartiallyOrderedTasks = [] as any;
|
||||
|
||||
for (const entryPoint of entryPoints) {
|
||||
const packageJson = entryPoint.packageJson;
|
||||
const hasProcessedTypings = hasBeenProcessed(packageJson, 'typings');
|
||||
const {propertiesToProcess, equivalentPropertiesMap} =
|
||||
getPropertiesToProcess(packageJson, supportedPropertiesToConsider, compileAllFormats);
|
||||
let processDts = !hasProcessedTypings;
|
||||
|
||||
if (propertiesToProcess.length === 0) {
|
||||
// This entry-point is unprocessable (i.e. there is no format property that is of interest
|
||||
// and can be processed). This will result in an error, but continue looping over
|
||||
// entry-points in order to collect all unprocessable ones and display a more informative
|
||||
// error.
|
||||
unprocessableEntryPointPaths.push(entryPoint.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const formatProperty of propertiesToProcess) {
|
||||
if (hasBeenProcessed(entryPoint.packageJson, formatProperty)) {
|
||||
// The format-path which the property maps to is already processed - nothing to do.
|
||||
logger.debug(`Skipping ${entryPoint.name} : ${formatProperty} (already compiled).`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const formatPropertiesToMarkAsProcessed = equivalentPropertiesMap.get(formatProperty)!;
|
||||
tasks.push({entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts});
|
||||
|
||||
// Only process typings for the first property (if not already processed).
|
||||
processDts = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for entry-points for which we could not process any format at all.
|
||||
if (unprocessableEntryPointPaths.length > 0) {
|
||||
throw new Error(
|
||||
'Unable to process any formats for the following entry-points (tried ' +
|
||||
`${propertiesToConsider.join(', ')}): ` +
|
||||
unprocessableEntryPointPaths.map(path => `\n - ${path}`).join(''));
|
||||
}
|
||||
|
||||
const duration = Math.round((Date.now() - startTime) / 100) / 10;
|
||||
logger.debug(
|
||||
`Analyzed ${entryPoints.length} entry-points in ${duration}s. ` +
|
||||
`(Total tasks: ${tasks.length})`);
|
||||
|
||||
return getTaskQueue(logger, inParallel, tasks, graph);
|
||||
};
|
||||
// Create an updater that will actually write to disk. In
|
||||
const pkgJsonUpdater = new DirectPackageJsonUpdater(fileSystem);
|
||||
|
||||
// The function for creating the `compile()` function.
|
||||
const createCompileFn: CreateCompileFn = onTaskCompleted => {
|
||||
const fileWriter = getFileWriter(
|
||||
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint);
|
||||
const {Transformer} = require('./packages/transformer');
|
||||
const transformer = new Transformer(fileSystem, logger, tsConfig);
|
||||
|
||||
return (task: Task) => {
|
||||
const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task;
|
||||
|
||||
const isCore = entryPoint.name === '@angular/core'; // Are we compiling the Angular core?
|
||||
const packageJson = entryPoint.packageJson;
|
||||
const formatPath = packageJson[formatProperty];
|
||||
const format = getEntryPointFormat(fileSystem, entryPoint, formatProperty);
|
||||
|
||||
// All properties listed in `propertiesToProcess` are guaranteed to point to a format-path
|
||||
// (i.e. they are defined in `entryPoint.packageJson`). Furthermore, they are also guaranteed
|
||||
// to be among `SUPPORTED_FORMAT_PROPERTIES`.
|
||||
// Based on the above, `formatPath` should always be defined and `getEntryPointFormat()`
|
||||
// should always return a format here (and not `undefined`).
|
||||
if (!formatPath || !format) {
|
||||
// This should never happen.
|
||||
throw new Error(
|
||||
`Invariant violated: No format-path or format for ${entryPoint.path} : ` +
|
||||
`${formatProperty} (formatPath: ${formatPath} | format: ${format})`);
|
||||
}
|
||||
|
||||
const bundle = makeEntryPointBundle(
|
||||
fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true,
|
||||
enableI18nLegacyMessageIdFormat);
|
||||
|
||||
logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`);
|
||||
|
||||
const result = transformer.transform(bundle);
|
||||
if (result.success) {
|
||||
if (result.diagnostics.length > 0) {
|
||||
logger.warn(replaceTsWithNgInErrors(
|
||||
ts.formatDiagnosticsWithColorAndContext(result.diagnostics, bundle.src.host)));
|
||||
}
|
||||
fileWriter.writeBundle(bundle, result.transformedFiles, formatPropertiesToMarkAsProcessed);
|
||||
|
||||
logger.debug(` Successfully compiled ${entryPoint.name} : ${formatProperty}`);
|
||||
|
||||
onTaskCompleted(task, TaskProcessingOutcome.Processed, null);
|
||||
} else {
|
||||
const errors = replaceTsWithNgInErrors(
|
||||
ts.formatDiagnosticsWithColorAndContext(result.diagnostics, bundle.src.host));
|
||||
onTaskCompleted(task, TaskProcessingOutcome.Failed, `compilation errors:\n${errors}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
const createCompileFn = getCreateCompileFn(
|
||||
fileSystem, logger, pkgJsonUpdater, createNewEntryPointFormats, errorOnFailedEntryPoint,
|
||||
enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
|
||||
|
||||
// The executor for actually planning and getting the work done.
|
||||
const createTaskCompletedCallback =
|
||||
@ -394,27 +132,6 @@ function ensureSupportedProperties(properties: string[]): EntryPointJsonProperty
|
||||
return supportedProperties;
|
||||
}
|
||||
|
||||
function getPackageJsonUpdater(inParallel: boolean, fs: FileSystem): PackageJsonUpdater {
|
||||
const directPkgJsonUpdater = new DirectPackageJsonUpdater(fs);
|
||||
return inParallel ? new ClusterPackageJsonUpdater(directPkgJsonUpdater) : directPkgJsonUpdater;
|
||||
}
|
||||
|
||||
function getFileWriter(
|
||||
fs: FileSystem, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
||||
createNewEntryPointFormats: boolean, errorOnFailedEntryPoint: boolean): FileWriter {
|
||||
return createNewEntryPointFormats ?
|
||||
new NewEntryPointFileWriter(fs, logger, errorOnFailedEntryPoint, pkgJsonUpdater) :
|
||||
new InPlaceFileWriter(fs, logger, errorOnFailedEntryPoint);
|
||||
}
|
||||
|
||||
function getTaskQueue(
|
||||
logger: Logger, inParallel: boolean, tasks: PartiallyOrderedTasks,
|
||||
graph: DepGraph<EntryPoint>): TaskQueue {
|
||||
const dependencies = computeTaskDependencies(tasks, graph);
|
||||
return inParallel ? new ParallelTaskQueue(logger, tasks, dependencies) :
|
||||
new SerialTaskQueue(logger, tasks, dependencies);
|
||||
}
|
||||
|
||||
function getCreateTaskCompletedCallback(
|
||||
pkgJsonUpdater: PackageJsonUpdater, errorOnFailedEntryPoint: boolean, logger: Logger,
|
||||
fileSystem: FileSystem): CreateTaskCompletedCallback {
|
||||
@ -429,8 +146,7 @@ function getCreateTaskCompletedCallback(
|
||||
function getExecutor(
|
||||
async: boolean, inParallel: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater,
|
||||
fileSystem: FileSystem, createTaskCompletedCallback: CreateTaskCompletedCallback): Executor {
|
||||
const lockFile = inParallel ? new ClusterLockFileWithChildProcess(fileSystem, logger) :
|
||||
new LockFileWithChildProcess(fileSystem, logger);
|
||||
const lockFile = new LockFileWithChildProcess(fileSystem, logger);
|
||||
if (async) {
|
||||
// Execute asynchronously (either serially or in parallel)
|
||||
const locker = new AsyncLocker(lockFile, logger, 500, 50);
|
||||
@ -438,7 +154,7 @@ function getExecutor(
|
||||
// Execute in parallel. Use up to 8 CPU cores for workers, always reserving one for master.
|
||||
const workerCount = Math.min(8, os.cpus().length - 1);
|
||||
return new ClusterExecutor(
|
||||
workerCount, logger, pkgJsonUpdater, locker, createTaskCompletedCallback);
|
||||
workerCount, fileSystem, logger, pkgJsonUpdater, locker, createTaskCompletedCallback);
|
||||
} else {
|
||||
// Execute serially, on a single thread (async).
|
||||
return new SingleProcessExecutorAsync(logger, locker, createTaskCompletedCallback);
|
||||
@ -481,72 +197,3 @@ function getEntryPointFinder(
|
||||
fs, config, logger, resolver, entryPointManifest, basePath, pathMappings);
|
||||
}
|
||||
}
|
||||
|
||||
function logInvalidEntryPoints(logger: Logger, invalidEntryPoints: InvalidEntryPoint[]): void {
|
||||
invalidEntryPoints.forEach(invalidEntryPoint => {
|
||||
logger.debug(
|
||||
`Invalid entry-point ${invalidEntryPoint.entryPoint.path}.`,
|
||||
`It is missing required dependencies:\n` +
|
||||
invalidEntryPoint.missingDependencies.map(dep => ` - ${dep}`).join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function computes and returns the following:
|
||||
* - `propertiesToProcess`: An (ordered) list of properties that exist and need to be processed,
|
||||
* based on the provided `propertiesToConsider`, the properties in `package.json` and their
|
||||
* corresponding format-paths. NOTE: Only one property per format-path needs to be processed.
|
||||
* - `equivalentPropertiesMap`: A mapping from each property in `propertiesToProcess` to the list of
|
||||
* other format properties in `package.json` that need to be marked as processed as soon as the
|
||||
* former has been processed.
|
||||
*/
|
||||
function getPropertiesToProcess(
|
||||
packageJson: EntryPointPackageJson, propertiesToConsider: EntryPointJsonProperty[],
|
||||
compileAllFormats: boolean): {
|
||||
propertiesToProcess: EntryPointJsonProperty[];
|
||||
equivalentPropertiesMap: Map<EntryPointJsonProperty, EntryPointJsonProperty[]>;
|
||||
} {
|
||||
const formatPathsToConsider = new Set<string>();
|
||||
|
||||
const propertiesToProcess: EntryPointJsonProperty[] = [];
|
||||
for (const prop of propertiesToConsider) {
|
||||
const formatPath = packageJson[prop];
|
||||
|
||||
// Ignore properties that are not defined in `package.json`.
|
||||
if (typeof formatPath !== 'string') continue;
|
||||
|
||||
// Ignore properties that map to the same format-path as a preceding property.
|
||||
if (formatPathsToConsider.has(formatPath)) continue;
|
||||
|
||||
// Process this property, because it is the first one to map to this format-path.
|
||||
formatPathsToConsider.add(formatPath);
|
||||
propertiesToProcess.push(prop);
|
||||
|
||||
// If we only need one format processed, there is no need to process any more properties.
|
||||
if (!compileAllFormats) break;
|
||||
}
|
||||
|
||||
const formatPathToProperties: {[formatPath: string]: EntryPointJsonProperty[]} = {};
|
||||
for (const prop of SUPPORTED_FORMAT_PROPERTIES) {
|
||||
const formatPath = packageJson[prop];
|
||||
|
||||
// Ignore properties that are not defined in `package.json`.
|
||||
if (typeof formatPath !== 'string') continue;
|
||||
|
||||
// Ignore properties that do not map to a format-path that will be considered.
|
||||
if (!formatPathsToConsider.has(formatPath)) continue;
|
||||
|
||||
// Add this property to the map.
|
||||
const list = formatPathToProperties[formatPath] || (formatPathToProperties[formatPath] = []);
|
||||
list.push(prop);
|
||||
}
|
||||
|
||||
const equivalentPropertiesMap = new Map<EntryPointJsonProperty, EntryPointJsonProperty[]>();
|
||||
for (const prop of propertiesToConsider) {
|
||||
const formatPath = packageJson[prop]!;
|
||||
const equivalentProperties = formatPathToProperties[formatPath];
|
||||
equivalentPropertiesMap.set(prop, equivalentProperties);
|
||||
}
|
||||
|
||||
return {propertiesToProcess, equivalentPropertiesMap};
|
||||
}
|
||||
|
214
packages/compiler-cli/ngcc/src/ngcc_options.ts
Normal file
214
packages/compiler-cli/ngcc/src/ngcc_options.ts
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, resolve} from '../../src/ngtsc/file_system';
|
||||
import {ParsedConfiguration, readConfiguration} from '../../src/perform_compile';
|
||||
|
||||
import {ConsoleLogger} from './logging/console_logger';
|
||||
import {Logger, LogLevel} from './logging/logger';
|
||||
import {SUPPORTED_FORMAT_PROPERTIES} from './packages/entry_point';
|
||||
|
||||
/**
|
||||
* The options to configure the ngcc compiler for synchronous execution.
|
||||
*/
|
||||
export interface SyncNgccOptions {
|
||||
/** The absolute path to the `node_modules` folder that contains the packages to process. */
|
||||
basePath: string;
|
||||
|
||||
/**
|
||||
* The path to the primary package to be processed. If not absolute then it must be relative to
|
||||
* `basePath`.
|
||||
*
|
||||
* All its dependencies will need to be processed too.
|
||||
*
|
||||
* If this property is provided then `errorOnFailedEntryPoint` is forced to true.
|
||||
*/
|
||||
targetEntryPointPath?: string;
|
||||
|
||||
/**
|
||||
* Which entry-point properties in the package.json to consider when processing an entry-point.
|
||||
* Each property should hold a path to the particular bundle format for the entry-point.
|
||||
* Defaults to all the properties in the package.json.
|
||||
*/
|
||||
propertiesToConsider?: string[];
|
||||
|
||||
/**
|
||||
* Whether to process all formats specified by (`propertiesToConsider`) or to stop processing
|
||||
* this entry-point at the first matching format. Defaults to `true`.
|
||||
*/
|
||||
compileAllFormats?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to create new entry-points bundles rather than overwriting the original files.
|
||||
*/
|
||||
createNewEntryPointFormats?: boolean;
|
||||
|
||||
/**
|
||||
* Provide a logger that will be called with log messages.
|
||||
*/
|
||||
logger?: Logger;
|
||||
|
||||
/**
|
||||
* Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`.
|
||||
* These are used to resolve paths to locally built Angular libraries.
|
||||
*
|
||||
* Note that `pathMappings` specified here take precedence over any `pathMappings` loaded from a
|
||||
* TS config file.
|
||||
*/
|
||||
pathMappings?: PathMappings;
|
||||
|
||||
/**
|
||||
* Provide a file-system service that will be used by ngcc for all file interactions.
|
||||
*/
|
||||
fileSystem?: FileSystem;
|
||||
|
||||
/**
|
||||
* Whether the compilation should run and return asynchronously. Allowing asynchronous execution
|
||||
* may speed up the compilation by utilizing multiple CPU cores (if available).
|
||||
*
|
||||
* Default: `false` (i.e. run synchronously)
|
||||
*/
|
||||
async?: false;
|
||||
|
||||
/**
|
||||
* Set to true in order to terminate immediately with an error code if an entry-point fails to be
|
||||
* processed.
|
||||
*
|
||||
* If `targetEntryPointPath` is provided then this property is always true and cannot be
|
||||
* changed. Otherwise the default is false.
|
||||
*
|
||||
* When set to false, ngcc will continue to process entry-points after a failure. In which case it
|
||||
* will log an error and resume processing other entry-points.
|
||||
*/
|
||||
errorOnFailedEntryPoint?: boolean;
|
||||
|
||||
/**
|
||||
* Render `$localize` messages with legacy format ids.
|
||||
*
|
||||
* The default value is `true`. Only set this to `false` if you do not want legacy message ids to
|
||||
* be rendered. For example, if you are not using legacy message ids in your translation files
|
||||
* AND are not doing compile-time inlining of translations, in which case the extra message ids
|
||||
* would add unwanted size to the final source bundle.
|
||||
*
|
||||
* It is safe to leave this set to true if you are doing compile-time inlining because the extra
|
||||
* legacy message ids will all be stripped during translation.
|
||||
*/
|
||||
enableI18nLegacyMessageIdFormat?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to invalidate any entry-point manifest file that is on disk. Instead, walk the
|
||||
* directory tree looking for entry-points, and then write a new entry-point manifest, if
|
||||
* possible.
|
||||
*
|
||||
* Default: `false` (i.e. the manifest will be used if available)
|
||||
*/
|
||||
invalidateEntryPointManifest?: boolean;
|
||||
|
||||
/**
|
||||
* An absolute path to a TS config file (e.g. `tsconfig.json`) or a directory containing one, that
|
||||
* will be used to configure module resolution with things like path mappings, if not specified
|
||||
* explicitly via the `pathMappings` property to `mainNgcc`.
|
||||
*
|
||||
* If `undefined`, ngcc will attempt to load a `tsconfig.json` file from the directory above the
|
||||
* `basePath`.
|
||||
*
|
||||
* If `null`, ngcc will not attempt to load any TS config file at all.
|
||||
*/
|
||||
tsConfigPath?: string|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options to configure the ngcc compiler for asynchronous execution.
|
||||
*/
|
||||
export type AsyncNgccOptions = Omit<SyncNgccOptions, 'async'>&{async: true};
|
||||
|
||||
/**
|
||||
* The options to configure the ngcc compiler.
|
||||
*/
|
||||
export type NgccOptions = AsyncNgccOptions|SyncNgccOptions;
|
||||
|
||||
export type PathMappings = {
|
||||
baseUrl: string,
|
||||
paths: {[key: string]: string[]}
|
||||
};
|
||||
|
||||
/**
|
||||
* If `pathMappings` is not provided directly, then try getting it from `tsConfig`, if available.
|
||||
*/
|
||||
export function getPathMappingsFromTsConfig(
|
||||
tsConfig: ParsedConfiguration|null, projectPath: AbsoluteFsPath): PathMappings|undefined {
|
||||
if (tsConfig !== null && tsConfig.options.baseUrl !== undefined &&
|
||||
tsConfig.options.paths !== undefined) {
|
||||
return {
|
||||
baseUrl: resolve(projectPath, tsConfig.options.baseUrl),
|
||||
paths: tsConfig.options.paths,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type OptionalNgccOptionKeys = 'targetEntryPointPath'|'tsConfigPath'|'pathMappings';
|
||||
export type RequiredNgccOptions = Required<Omit<NgccOptions, OptionalNgccOptionKeys>>;
|
||||
export type OptionalNgccOptions = Pick<NgccOptions, OptionalNgccOptionKeys>;
|
||||
export type SharedSetup = {
|
||||
fileSystem: FileSystem; absBasePath: AbsoluteFsPath; projectPath: AbsoluteFsPath;
|
||||
tsConfig: ParsedConfiguration | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate common utilities that are always used and fix up options with defaults, as necessary.
|
||||
*
|
||||
* NOTE: Avoid eagerly instantiating anything that might not be used when running sync/async.
|
||||
*/
|
||||
export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOptions&
|
||||
OptionalNgccOptions {
|
||||
const fileSystem = getFileSystem();
|
||||
const absBasePath = absoluteFrom(options.basePath);
|
||||
const projectPath = fileSystem.dirname(absBasePath);
|
||||
const tsConfig =
|
||||
options.tsConfigPath !== null ? readConfiguration(options.tsConfigPath || projectPath) : null;
|
||||
|
||||
let {
|
||||
basePath,
|
||||
targetEntryPointPath,
|
||||
propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
|
||||
compileAllFormats = true,
|
||||
createNewEntryPointFormats = false,
|
||||
logger = new ConsoleLogger(LogLevel.info),
|
||||
pathMappings,
|
||||
async = false,
|
||||
errorOnFailedEntryPoint = false,
|
||||
enableI18nLegacyMessageIdFormat = true,
|
||||
invalidateEntryPointManifest = false,
|
||||
tsConfigPath,
|
||||
} = options;
|
||||
|
||||
pathMappings = options.pathMappings || getPathMappingsFromTsConfig(tsConfig, projectPath);
|
||||
|
||||
if (!!options.targetEntryPointPath) {
|
||||
// targetEntryPointPath forces us to error if an entry-point fails.
|
||||
errorOnFailedEntryPoint = true;
|
||||
}
|
||||
|
||||
return {
|
||||
basePath,
|
||||
targetEntryPointPath,
|
||||
propertiesToConsider,
|
||||
compileAllFormats,
|
||||
createNewEntryPointFormats,
|
||||
logger,
|
||||
pathMappings,
|
||||
async,
|
||||
errorOnFailedEntryPoint,
|
||||
enableI18nLegacyMessageIdFormat,
|
||||
invalidateEntryPointManifest,
|
||||
tsConfigPath,
|
||||
fileSystem,
|
||||
absBasePath,
|
||||
projectPath,
|
||||
tsConfig,
|
||||
};
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
|
||||
import {PathMappings} from '../utils';
|
||||
import {PathMappings} from '../ngcc_options';
|
||||
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||
import {NgccSourcesCompilerHost} from './ngcc_compiler_host';
|
||||
|
@ -77,11 +77,6 @@ export function hasNameIdentifier(declaration: ts.Declaration): declaration is t
|
||||
return namedDeclaration.name !== undefined && ts.isIdentifier(namedDeclaration.name);
|
||||
}
|
||||
|
||||
export type PathMappings = {
|
||||
baseUrl: string,
|
||||
paths: {[key: string]: string[]}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test whether a path is "relative".
|
||||
*
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user