Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
31e8a52722 | |||
e3c9f9d794 | |||
e7c4e94c9a | |||
26dc826821 | |||
287247ef05 | |||
af170d2ae9 | |||
a01ed0d7b4 | |||
a7b94783b5 | |||
789dd6a0da | |||
e5b18e810c | |||
b8f86ad7d5 | |||
6f2e8afaed | |||
699c705a8d | |||
adc869ff88 | |||
46f2dcc470 | |||
2d9c4c1d82 | |||
0757702d63 | |||
174777443d | |||
baf2d9ddbc | |||
da444984a8 | |||
84a3daf609 | |||
c88cdea9ac | |||
7b3bd219f7 | |||
dc76c14e31 | |||
0ffc1d0d21 | |||
3d1b82be67 | |||
dec7e5286f | |||
2f812f31d5 | |||
18bac15ddd | |||
a1fe52b41c |
16
.bazelrc
16
.bazelrc
@ -36,22 +36,6 @@ build --incompatible_strict_action_env
|
||||
run --incompatible_strict_action_env
|
||||
test --incompatible_strict_action_env
|
||||
|
||||
###############################
|
||||
# Saucelabs support #
|
||||
# Turn on these settings with #
|
||||
# --config=saucelabs #
|
||||
###############################
|
||||
|
||||
# Expose SauceLabs environment to actions
|
||||
# These environment variables are needed by
|
||||
# web_test_karma to run on Saucelabs
|
||||
test:saucelabs --action_env=SAUCE_USERNAME
|
||||
test:saucelabs --action_env=SAUCE_ACCESS_KEY
|
||||
test:saucelabs --action_env=SAUCE_READY_FILE
|
||||
test:saucelabs --action_env=SAUCE_PID_FILE
|
||||
test:saucelabs --action_env=SAUCE_TUNNEL_IDENTIFIER
|
||||
test:saucelabs --define=KARMA_WEB_TEST_MODE=SL_REQUIRED
|
||||
|
||||
###############################
|
||||
# Release support #
|
||||
# Turn on these settings with #
|
||||
|
@ -37,5 +37,5 @@ build --verbose_failures=true
|
||||
# > Example job: https://circleci.com/gh/angular/angular/385517
|
||||
# We expect that TypeScript compilations will parallelize wider than the number of local cores anyway
|
||||
# so we should saturate remote workers with TS compilations
|
||||
build --strategy=TypeScriptCompile=standalone
|
||||
build --strategy=AngularTemplateCompile=standalone
|
||||
build --strategy=AngularTemplateCompile=local
|
||||
build --strategy=TypeScriptCompile=local
|
||||
|
@ -143,7 +143,7 @@ var_14: ¬ify_dev_infra_on_fail
|
||||
|
||||
# Cache key for the Material unit tests job. **Note** when updating the SHA in the cache keys,
|
||||
# also update the SHA for the "MATERIAL_REPO_COMMIT" environment variable.
|
||||
var_15: &material_unit_tests_cache_key v4-angular-material-701302dc482d7e4b77990b24e3b5ab330bbf1aa5
|
||||
var_15: &material_unit_tests_cache_key v4-angular-material-18b9ef3f5529f0fa8f034944681486447af7b879
|
||||
var_16: &material_unit_tests_cache_key_short v4-angular-material
|
||||
|
||||
version: 2
|
||||
@ -256,23 +256,19 @@ jobs:
|
||||
- *init_environment
|
||||
- *setup_circleci_bazel_config
|
||||
- run:
|
||||
name: Preparing environment for running tests on Saucelabs.
|
||||
command: setSecretVar SAUCE_ACCESS_KEY $(echo $SAUCE_ACCESS_KEY | rev)
|
||||
- run:
|
||||
name: Starting Saucelabs tunnel
|
||||
command: ./scripts/saucelabs/start-tunnel.sh
|
||||
background: true
|
||||
# Waits for the Saucelabs tunnel to be ready. This ensures that we don't run tests
|
||||
# too early without Saucelabs not being ready.
|
||||
- run: ./scripts/saucelabs/wait-for-tunnel.sh
|
||||
# All web tests are contained within a single //:test_web_all target for Saucelabs
|
||||
# as running each set of tests as a separate target will attempt to acquire too
|
||||
# many browsers on Saucelabs (7 per target currently) and some tests will always
|
||||
# fail to acquire browsers. For example:
|
||||
# 14 02 2019 19:52:33.170:WARN [launcher]: chrome beta on SauceLabs have not captured in 180000 ms, killing.
|
||||
# //packages/forms/test:web_test_sauce TIMEOUT in 315.0s
|
||||
- run: yarn bazel test --config=saucelabs //:test_web_all
|
||||
- run: ./scripts/saucelabs/stop-tunnel.sh
|
||||
name: Run Bazel tests in saucelabs
|
||||
# All web tests are contained within a single //:test_web_all target for Saucelabs
|
||||
# as running each set of tests as a separate target will attempt to acquire too
|
||||
# many browsers on Saucelabs (7 per target currently) and some tests will always
|
||||
# fail to acquire browsers. For example:
|
||||
# 14 02 2019 19:52:33.170:WARN [launcher]: chrome beta on SauceLabs have not captured in 180000 ms, killing.
|
||||
# //packages/forms/test:web_test_sauce TIMEOUT in 315.0s
|
||||
command: |
|
||||
./scripts/saucelabs/run-bazel-via-tunnel.sh \
|
||||
--tunnel-id angular-${CIRCLE_BUILD_NUM}-${CIRCLE_NODE_INDEX} \
|
||||
--username $SAUCE_USERNAME \
|
||||
--key $(echo $SAUCE_ACCESS_KEY | rev) \
|
||||
yarn bazel test //:test_web_all
|
||||
- *notify_dev_infra_on_fail
|
||||
|
||||
test_aio:
|
||||
@ -673,7 +669,10 @@ workflows:
|
||||
version: 2
|
||||
default_workflow:
|
||||
jobs:
|
||||
- setup
|
||||
- setup:
|
||||
filters:
|
||||
branches:
|
||||
ignore: g3
|
||||
- lint:
|
||||
requires:
|
||||
- setup
|
||||
@ -784,9 +783,3 @@ workflows:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
# TODO:
|
||||
# - don't build the g3 branch
|
||||
# - verify that we are bootstrapping with the right yarn version coming from the docker image
|
||||
# - check local chrome version pulled from docker image
|
||||
# - remove /tools/ngcontainer
|
||||
|
@ -61,6 +61,7 @@ else
|
||||
setPublicVar SAUCE_USERNAME "angular-ci";
|
||||
setSecretVar SAUCE_ACCESS_KEY "9b988f434ff8-fbca-8aa4-4ae3-35442987";
|
||||
fi
|
||||
# TODO(josephperrott): Remove environment variables once all saucelabs tests are via bazel method.
|
||||
setPublicVar SAUCE_LOG_FILE /tmp/angular/sauce-connect.log
|
||||
setPublicVar SAUCE_READY_FILE /tmp/angular/sauce-connect-ready-file.lock
|
||||
setPublicVar SAUCE_PID_FILE /tmp/angular/sauce-connect-pid-file.lock
|
||||
@ -79,7 +80,7 @@ setPublicVar MATERIAL_REPO_TMP_DIR "/tmp/material2"
|
||||
setPublicVar MATERIAL_REPO_URL "https://github.com/angular/material2.git"
|
||||
setPublicVar MATERIAL_REPO_BRANCH "master"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI "config.yml".
|
||||
setPublicVar MATERIAL_REPO_COMMIT "701302dc482d7e4b77990b24e3b5ab330bbf1aa5"
|
||||
setPublicVar MATERIAL_REPO_COMMIT "18b9ef3f5529f0fa8f034944681486447af7b879"
|
||||
|
||||
# Source `$BASH_ENV` to make the variables available immediately.
|
||||
source $BASH_ENV;
|
||||
|
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@ -101,7 +101,6 @@
|
||||
#
|
||||
# - brandonroberts
|
||||
# - gkalpak
|
||||
# - jenniferfell
|
||||
# - petebacondarwin
|
||||
|
||||
|
||||
@ -911,14 +910,6 @@ testing/** @angular/fw-test
|
||||
|
||||
|
||||
|
||||
# ================================================
|
||||
# Material CI
|
||||
# ================================================
|
||||
|
||||
/tools/material-ci/** @angular/fw-core @angular/framework-global-approvers
|
||||
|
||||
|
||||
|
||||
# ================================================
|
||||
# Public API
|
||||
# ================================================
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
||||
<a name="8.2.3"></a>
|
||||
## [8.2.3](https://github.com/angular/angular/compare/8.2.2...8.2.3) (2019-08-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bazel:** pin `[@microsoft](https://github.com/microsoft)/api-extractor` ([#32187](https://github.com/angular/angular/issues/32187)) ([a7b9478](https://github.com/angular/angular/commit/a7b9478))
|
||||
|
||||
|
||||
|
||||
<a name="8.2.2"></a>
|
||||
## [8.2.2](https://github.com/angular/angular/compare/8.2.1...8.2.2) (2019-08-12)
|
||||
|
||||
|
@ -233,6 +233,7 @@ There are currently a few exceptions to the "use package name" rule:
|
||||
* **docs-infra**: used for docs-app (angular.io) related changes within the /aio directory of the
|
||||
repo
|
||||
* **ivy**: used for changes to the [Ivy renderer](https://github.com/angular/angular/issues/21706).
|
||||
* **ngcc**: used for changes to the [Angular Compatibility Compiler](./packages/compiler-cli/ngcc/README.md)
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all
|
||||
packages (e.g. `style: add missing semicolons`) and for docs changes that are not related to a
|
||||
specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
# Angular
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript and other languages.
|
||||
Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages.
|
||||
|
||||
## Quickstart
|
||||
|
||||
@ -14,7 +14,7 @@ Angular is a development platform for building mobile and desktop web applicatio
|
||||
|
||||
## Changelog
|
||||
|
||||
[Learn about the latest improvements][changelog].
|
||||
[Learn about the latest improvements][changelog].
|
||||
|
||||
## Want to help?
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
import { logging } from 'selenium-webdriver';
|
||||
|
||||
describe('Providers and ViewProviders', function () {
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
it('shows basic flower emoji', function() {
|
||||
expect(element.all(by.css('p')).get(0).getText()).toContain('🌺');
|
||||
});
|
||||
|
||||
it('shows whale emoji', function() {
|
||||
expect(element.all(by.css('p')).get(1).getText()).toContain('🐳');
|
||||
});
|
||||
|
||||
it('shows sunflower from FlowerService', function() {
|
||||
expect(element.all(by.css('p')).get(8).getText()).toContain('🌻');
|
||||
});
|
||||
|
||||
});
|
||||
|
10
aio/content/examples/providers-viewproviders/src/app/animal.service.ts
Executable file
10
aio/content/examples/providers-viewproviders/src/app/animal.service.ts
Executable file
@ -0,0 +1,10 @@
|
||||
// #docregion animal-service
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AnimalService {
|
||||
emoji = '🐳';
|
||||
}
|
||||
// #enddocregion animal-service
|
@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FlowerService } from './flower.service';
|
||||
import { AnimalService } from './animal.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.css' ]
|
||||
})
|
||||
// #docregion injection
|
||||
export class AppComponent {
|
||||
constructor(public flower: FlowerService) {}
|
||||
}
|
||||
// #enddocregion injection
|
15
aio/content/examples/providers-viewproviders/src/app/app.component.html
Executable file
15
aio/content/examples/providers-viewproviders/src/app/app.component.html
Executable file
@ -0,0 +1,15 @@
|
||||
|
||||
<h2>From AppComponent:</h2>
|
||||
<!-- #docregion binding-flower -->
|
||||
<p>Emoji from FlowerService: {{flower.emoji}}</p>
|
||||
<!-- #enddocregion binding-flower -->
|
||||
<!-- #docregion binding-animal -->
|
||||
<p>Emoji from AnimalService: {{animal.emoji}}</p>
|
||||
<!-- #enddocregion binding-animal -->
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>From ChildComponent:</h2>
|
||||
<!-- #docregion content-projection -->
|
||||
<app-child><app-inspector></app-inspector></app-child>
|
||||
<!-- #enddocregion content-projection -->
|
15
aio/content/examples/providers-viewproviders/src/app/app.component.ts
Executable file
15
aio/content/examples/providers-viewproviders/src/app/app.component.ts
Executable file
@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FlowerService } from './flower.service';
|
||||
import { AnimalService } from './animal.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.css' ]
|
||||
})
|
||||
// #docregion inject-animal-service
|
||||
export class AppComponent {
|
||||
constructor(public flower: FlowerService, public animal: AnimalService) {}
|
||||
}
|
||||
// #enddocregion inject-animal-service
|
17
aio/content/examples/providers-viewproviders/src/app/app.module.ts
Executable file
17
aio/content/examples/providers-viewproviders/src/app/app.module.ts
Executable file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { ChildComponent } from './child/child.component';
|
||||
import { InspectorComponent } from './inspector/inspector.component';
|
||||
|
||||
// #docregion appmodule
|
||||
@NgModule({
|
||||
imports: [ BrowserModule, FormsModule ],
|
||||
declarations: [ AppComponent, ChildComponent, InspectorComponent ],
|
||||
bootstrap: [ AppComponent ],
|
||||
providers: []
|
||||
})
|
||||
export class AppModule { }
|
||||
// #enddocregion appmodule
|
@ -0,0 +1,19 @@
|
||||
import { Component, OnInit, Host, SkipSelf, Optional } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
|
||||
// #docregion flowerservice
|
||||
@Component({
|
||||
selector: 'app-child',
|
||||
templateUrl: './child.component.html',
|
||||
styleUrls: ['./child.component.css'],
|
||||
// use the providers array to provide a service
|
||||
providers: [{ provide: FlowerService, useValue: { emoji: '🌻' } }]
|
||||
})
|
||||
|
||||
export class ChildComponent {
|
||||
// inject the service
|
||||
constructor( public flower: FlowerService) { }
|
||||
}
|
||||
|
||||
// #enddocregion flowerservice
|
||||
|
@ -0,0 +1,4 @@
|
||||
.container {
|
||||
border: 1px solid darkblue;
|
||||
padding: 1rem;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<!-- #docplaster -->
|
||||
<!-- #docregion child-component -->
|
||||
<!-- #docregion flower-binding -->
|
||||
<p>Emoji from FlowerService: {{flower.emoji}}</p>
|
||||
<!-- #enddocregion flower-binding -->
|
||||
<!-- #docregion animal-binding -->
|
||||
<p>Emoji from AnimalService: {{animal.emoji}}</p>
|
||||
<!-- #enddocregion animal-binding -->
|
||||
|
||||
<div class="container">
|
||||
<h3>Content projection</h3>
|
||||
<!-- #enddocregion child-component -->
|
||||
<p>The following is coming from content. It doesn't get to see the puppy because the puppy is declared inside the view only.</p>
|
||||
<!-- #docregion child-component -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
<h3>Inside the view</h3>
|
||||
<!-- #enddocregion child-component -->
|
||||
<p>The following is inside the view so it does see the puppy.</p>
|
||||
<!-- #docregion child-component -->
|
||||
<app-inspector></app-inspector>
|
||||
<!-- #enddocregion child-component -->
|
||||
|
@ -0,0 +1,44 @@
|
||||
// #docplaster
|
||||
import { Component, OnInit, Host, SkipSelf, Optional } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
import { AnimalService } from '../animal.service';
|
||||
|
||||
// #docregion provide-animal-service
|
||||
@Component({
|
||||
selector: 'app-child',
|
||||
templateUrl: './child.component.html',
|
||||
styleUrls: ['./child.component.css'],
|
||||
// provide services
|
||||
providers: [{ provide: FlowerService, useValue: { emoji: '🌻' } }],
|
||||
viewProviders: [{ provide: AnimalService, useValue: { emoji: '🐶' } }]
|
||||
})
|
||||
|
||||
export class ChildComponent {
|
||||
// inject service
|
||||
constructor( public flower: FlowerService, public animal: AnimalService) { }
|
||||
// #enddocregion provide-animal-service
|
||||
|
||||
// viewProviders ensures that only the view gets to see this.
|
||||
// With the AnimalService in the viewProviders, the
|
||||
// InspectorComponent doesn't get to see it because the
|
||||
// inspector is in the content.
|
||||
|
||||
|
||||
// constructor( public flower: FlowerService, @Optional() @Host() public animal: AnimalService) { }
|
||||
|
||||
// Comment out the above constructor and alternately
|
||||
// uncomment the two following constructors to see the
|
||||
// effects of @Host() and @Host() + @SkipSelf().
|
||||
|
||||
// constructor(
|
||||
// @Host() public animal : AnimalService,
|
||||
// @Host() @Optional() public flower : FlowerService) { }
|
||||
|
||||
// constructor(
|
||||
// @SkipSelf() @Host() public animal : AnimalService,
|
||||
// @SkipSelf() @Host() @Optional() public flower : FlowerService) { }
|
||||
|
||||
// #docregion provide-animal-service
|
||||
}
|
||||
// #enddocregion provide-animal-service
|
||||
|
11
aio/content/examples/providers-viewproviders/src/app/flower.service.ts
Executable file
11
aio/content/examples/providers-viewproviders/src/app/flower.service.ts
Executable file
@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// #docregion flowerservice
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FlowerService {
|
||||
emoji = '🌺';
|
||||
}
|
||||
// #enddocregion flowerservice
|
||||
|
@ -0,0 +1,4 @@
|
||||
<!-- #docregion binding -->
|
||||
<p>Emoji from FlowerService: {{flower.emoji}}</p>
|
||||
<p>Emoji from AnimalService: {{animal.emoji}}</p>
|
||||
<!-- #enddocregion binding -->
|
@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
import { AnimalService } from '../animal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-inspector',
|
||||
templateUrl: './inspector.component.html',
|
||||
styleUrls: ['./inspector.component.css']
|
||||
})
|
||||
// #docregion injection
|
||||
export class InspectorComponent {
|
||||
constructor(public flower: FlowerService, public animal: AnimalService) { }
|
||||
}
|
||||
// #enddocregion injection
|
14
aio/content/examples/providers-viewproviders/src/index.html
Normal file
14
aio/content/examples/providers-viewproviders/src/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>providers vs. viewProviders</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
12
aio/content/examples/providers-viewproviders/src/main.ts
Normal file
12
aio/content/examples/providers-viewproviders/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.log(err));
|
10
aio/content/examples/providers-viewproviders/stackblitz.json
Normal file
10
aio/content/examples/providers-viewproviders/stackblitz.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Inputs and Outputs",
|
||||
"files": [
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[1,2].*"
|
||||
],
|
||||
"file": "src/app/app.component.ts",
|
||||
"tags": ["Inputs and Outputs"]
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
|
||||
describe('Resolution-modifiers-example', function () {
|
||||
|
||||
beforeAll(function () {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
it('shows basic flower emoji', function() {
|
||||
expect(element.all(by.css('p')).get(0).getText()).toContain('🌸');
|
||||
});
|
||||
|
||||
it('shows basic leaf emoji', function() {
|
||||
expect(element.all(by.css('p')).get(1).getText()).toContain('🌿');
|
||||
});
|
||||
|
||||
it('shows yellow flower in host child', function() {
|
||||
expect(element.all(by.css('p')).get(9).getText()).toContain('🌼');
|
||||
});
|
||||
|
||||
});
|
0
aio/content/examples/resolution-modifiers/src/app/app.component.css
Executable file
0
aio/content/examples/resolution-modifiers/src/app/app.component.css
Executable file
14
aio/content/examples/resolution-modifiers/src/app/app.component.html
Executable file
14
aio/content/examples/resolution-modifiers/src/app/app.component.html
Executable file
@ -0,0 +1,14 @@
|
||||
<h1>DI resolution modifiers</h1>
|
||||
|
||||
<p>Basic flower service: {{flower.emoji}}</p>
|
||||
<p>Basic leaf service: {{leaf.emoji}}</p>
|
||||
|
||||
<app-optional></app-optional>
|
||||
|
||||
<app-self></app-self>
|
||||
|
||||
<app-self-no-data></app-self-no-data>
|
||||
|
||||
<app-skipself></app-skipself>
|
||||
|
||||
<app-host-parent></app-host-parent>
|
13
aio/content/examples/resolution-modifiers/src/app/app.component.ts
Executable file
13
aio/content/examples/resolution-modifiers/src/app/app.component.ts
Executable file
@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { LeafService } from './leaf.service';
|
||||
import { FlowerService } from './flower.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: [ './app.component.css' ]
|
||||
})
|
||||
export class AppComponent {
|
||||
name = 'Angular';
|
||||
constructor(public flower: FlowerService, public leaf: LeafService) {}
|
||||
}
|
33
aio/content/examples/resolution-modifiers/src/app/app.module.ts
Executable file
33
aio/content/examples/resolution-modifiers/src/app/app.module.ts
Executable file
@ -0,0 +1,33 @@
|
||||
;
|
||||
import { OptionalComponent } from './optional/optional.component';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { SelfNoDataComponent } from './self-no-data/self-no-data.component';
|
||||
import { HostComponent } from './host/host.component';
|
||||
import { SelfComponent } from './self/self.component';
|
||||
import { SkipselfComponent } from './skipself/skipself.component';
|
||||
import { HostParentComponent } from './host-parent/host-parent.component';
|
||||
import { HostChildComponent } from './host-child/host-child.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
OptionalComponent,
|
||||
SelfComponent,
|
||||
SelfNoDataComponent,
|
||||
HostComponent,
|
||||
SkipselfComponent,
|
||||
HostParentComponent,
|
||||
HostChildComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
providers: []
|
||||
})
|
||||
export class AppModule { }
|
9
aio/content/examples/resolution-modifiers/src/app/flower.service.ts
Executable file
9
aio/content/examples/resolution-modifiers/src/app/flower.service.ts
Executable file
@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root' // provide this service in the root ModuleInjector
|
||||
|
||||
})
|
||||
export class FlowerService {
|
||||
emoji = '🌸';
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div class="section">
|
||||
<h2>Child of @Host() Component</h2>
|
||||
<p>Flower emoji: {{flower.emoji}}</p>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-host-child',
|
||||
templateUrl: './host-child.component.html',
|
||||
styleUrls: ['./host-child.component.css']
|
||||
})
|
||||
export class HostChildComponent {
|
||||
constructor(public flower: FlowerService) { }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<div class="section">
|
||||
<h2>Parent of @Host() Component</h2>
|
||||
<p>Flower emoji: {{flower.emoji}}</p>
|
||||
<app-host></app-host>
|
||||
</div>
|
@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-host-parent',
|
||||
templateUrl: './host-parent.component.html',
|
||||
styleUrls: ['./host-parent.component.css'],
|
||||
providers: [{ provide: FlowerService, useValue: { emoji: '🌺' } }]
|
||||
|
||||
})
|
||||
export class HostParentComponent {
|
||||
|
||||
constructor(public flower: FlowerService) { }
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<div class="section">
|
||||
<h2>@Host() Component</h2>
|
||||
<p>Flower emoji: {{flower.emoji}}</p>
|
||||
<p><i>(@Host() stops it here)</i></p>
|
||||
<app-host-child></app-host-child>
|
||||
</div>
|
21
aio/content/examples/resolution-modifiers/src/app/host/host.component.ts
Executable file
21
aio/content/examples/resolution-modifiers/src/app/host/host.component.ts
Executable file
@ -0,0 +1,21 @@
|
||||
import { Component, Host, Optional } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
|
||||
// #docregion host-component
|
||||
@Component({
|
||||
selector: 'app-host',
|
||||
templateUrl: './host.component.html',
|
||||
styleUrls: ['./host.component.css'],
|
||||
// provide the service
|
||||
providers: [{ provide: FlowerService, useValue: { emoji: '🌼' } }]
|
||||
})
|
||||
export class HostComponent {
|
||||
// use @Host() in the constructor when injecting the service
|
||||
constructor(@Host() @Optional() public flower: FlowerService) { }
|
||||
|
||||
}
|
||||
// #enddocregion host-component
|
||||
|
||||
// if you take out @Host() and the providers array, flower will be red hibiscus
|
||||
|
||||
|
11
aio/content/examples/resolution-modifiers/src/app/leaf.service.ts
Executable file
11
aio/content/examples/resolution-modifiers/src/app/leaf.service.ts
Executable file
@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
// #docregion leafservice
|
||||
export class LeafService {
|
||||
emoji = '🌿';
|
||||
}
|
||||
// #enddocregion leafservice
|
||||
|
7
aio/content/examples/resolution-modifiers/src/app/optional.service.ts
Executable file
7
aio/content/examples/resolution-modifiers/src/app/optional.service.ts
Executable file
@ -0,0 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalService {
|
||||
}
|
||||
|
||||
// This service isn't provided anywhere.
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div class="section">
|
||||
<h2>@Optional() Component</h2>
|
||||
<p>This component still works even though the OptionalService (notice @Optional() in the consturctor isn't provided or configured anywhere. Angular goes through tree and visibilty rules, and if it doesn't find the requested service, returns null.</p>
|
||||
</div>
|
@ -0,0 +1,21 @@
|
||||
import { Component, Optional } from '@angular/core';
|
||||
import { OptionalService } from '../optional.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-optional',
|
||||
templateUrl: './optional.component.html',
|
||||
styleUrls: ['./optional.component.css']
|
||||
})
|
||||
|
||||
// #docregion optional-component
|
||||
export class OptionalComponent {
|
||||
constructor(@Optional() public optional: OptionalService) {}
|
||||
}
|
||||
// #enddocregion optional-component
|
||||
|
||||
// The OptionalService isn't provided here, in the @Injectable()
|
||||
// providers array, or in the NgModule. If you remove @Optional()
|
||||
// from the constructor, you'll get an error.
|
||||
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div class="section">
|
||||
<h2>@Self() Component (without a provider)</h2>
|
||||
<p>Leaf emoji: {{leaf?.emoji}}</p>
|
||||
</div>
|
@ -0,0 +1,18 @@
|
||||
import { Component, Self, Optional } from '@angular/core';
|
||||
import { LeafService } from '../leaf.service';
|
||||
|
||||
// #docregion self-no-data-component
|
||||
@Component({
|
||||
selector: 'app-self-no-data',
|
||||
templateUrl: './self-no-data.component.html',
|
||||
styleUrls: ['./self-no-data.component.css']
|
||||
})
|
||||
export class SelfNoDataComponent {
|
||||
constructor(@Self() @Optional() public leaf: LeafService) { }
|
||||
}
|
||||
|
||||
// #enddocregion self-no-data-component
|
||||
|
||||
// The app doesn't break because the value being available at self is optional.
|
||||
// If you remove @Optional(), the app breaks.
|
||||
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div class="section">
|
||||
<h2>@Self() Component</h2>
|
||||
<p>Flower emoji: {{flower?.emoji}}</p>
|
||||
</div>
|
19
aio/content/examples/resolution-modifiers/src/app/self/self.component.ts
Executable file
19
aio/content/examples/resolution-modifiers/src/app/self/self.component.ts
Executable file
@ -0,0 +1,19 @@
|
||||
import { Component, Self } from '@angular/core';
|
||||
import { FlowerService } from '../flower.service';
|
||||
|
||||
// #docregion self-component
|
||||
@Component({
|
||||
selector: 'app-self',
|
||||
templateUrl: './self.component.html',
|
||||
styleUrls: ['./self.component.css'],
|
||||
providers: [{ provide: FlowerService, useValue: { emoji: '🌼' } }]
|
||||
|
||||
})
|
||||
export class SelfComponent {
|
||||
constructor(@Self() public flower: FlowerService) {}
|
||||
}
|
||||
// #enddocregion self-component
|
||||
|
||||
// This component provides the FlowerService so the injector
|
||||
// doesn't have to look further up the injector tree
|
||||
|
@ -0,0 +1,5 @@
|
||||
.section {
|
||||
border: 2px solid #369;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div class="section">
|
||||
<h2>@SkipSelf() Component</h2>
|
||||
<p>Leaf emoji: {{leaf.emoji}}</p>
|
||||
</div>
|
@ -0,0 +1,18 @@
|
||||
import { Component, SkipSelf } from '@angular/core';
|
||||
import { LeafService } from '../leaf.service';
|
||||
|
||||
// #docregion skipself-component
|
||||
@Component({
|
||||
selector: 'app-skipself',
|
||||
templateUrl: './skipself.component.html',
|
||||
styleUrls: ['./skipself.component.css'],
|
||||
// Angular would ignore this LeafService instance
|
||||
providers: [{ provide: LeafService, useValue: { emoji: '🍁' } }]
|
||||
})
|
||||
export class SkipselfComponent {
|
||||
// Use @SkipSelf() in the constructor
|
||||
constructor(@SkipSelf() public leaf: LeafService) { }
|
||||
}
|
||||
// #enddocregion skipself-component
|
||||
|
||||
// @SkipSelf(): Specifies that the dependency resolution should start from the parent injector, not here.
|
14
aio/content/examples/resolution-modifiers/src/index.html
Normal file
14
aio/content/examples/resolution-modifiers/src/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DI Resolution Modifiers Example</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root>Loading...</app-root>
|
||||
</body>
|
||||
</html>
|
11
aio/content/examples/resolution-modifiers/src/main.ts
Normal file
11
aio/content/examples/resolution-modifiers/src/main.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
10
aio/content/examples/resolution-modifiers/stackblitz.json
Normal file
10
aio/content/examples/resolution-modifiers/stackblitz.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "NgModules",
|
||||
"files": [
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[1,2].*"
|
||||
],
|
||||
"file": "src/app/app.component.ts",
|
||||
"tags": ["NgModules"]
|
||||
}
|
@ -30,7 +30,7 @@ The Filter/Stagger tab in the live example shows a list of heroes with an introd
|
||||
|
||||
The following example demonstrates how to use `query()` and `stagger()` functions on the entry of an animated element.
|
||||
|
||||
* Use `query()` to look for any element entering or leaving the page. The query specifies elements meeting certain CSS class criteria.
|
||||
* Use `query()` to look for an element entering the page that meets certain criteria.
|
||||
|
||||
* For each of these elements, use `style()` to set the same initial style for the element. Make it invisible and use `transform` to move it out of position so that it can slide into place.
|
||||
|
||||
|
@ -134,7 +134,7 @@ The `@NgModule()` and `@Component()` decorators have the `providers` metadata op
|
||||
|
||||
Components are directives, and the `providers` option is inherited from `@Directive()`. You can also configure providers for directives and pipes at the same level as the component.
|
||||
|
||||
Learn more about [where to configure providers](guide/hierarchical-dependency-injection#where-to-register).
|
||||
Learn more about [where to configure providers](guide/hierarchical-dependency-injection).
|
||||
|
||||
</div>
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -137,8 +137,9 @@ export interface DataGroup {
|
||||
Similar to `assetGroups`, every data group has a `name` which uniquely identifies it.
|
||||
|
||||
### `urls`
|
||||
A list of URL patterns. URLs that match these patterns will be cached according to this data group's policy.<br>
|
||||
_(Negative glob patterns are not supported and `?` will be matched literally; i.e. it will not match any character other than `?`.)_
|
||||
A list of URL patterns. URLs that match these patterns are cached according to this data group's policy. Only non-mutating requests (GET and HEAD) are cached.
|
||||
* Negative glob patterns are not supported.
|
||||
* `?` is matched literally; that is, it matches *only* the character `?`.
|
||||
|
||||
### `version`
|
||||
Occasionally APIs change formats in a way that is not backward-compatible. A new version of the app may not be compatible with the old API format and thus may not be compatible with existing cached resources from that API.
|
||||
|
@ -31,11 +31,34 @@ Installing the Angular service worker is as simple as including an `NgModule`. I
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Your application must run in a web browser that supports service workers. Currently, service workers are supported in the latest versions of Chrome, Firefox, Edge, Safari, Opera, UC Browser (Android version) and Samsung Internet. Browsers like IE and Opera Mini do not provide the support. To learn more about other browsers that are service worker ready, see the [Can I Use](https://caniuse.com/#feat=serviceworkers) page and [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API).
|
||||
To make use of all the features of Angular service worker, use the latest versions of Angular and the Angular CLI.
|
||||
|
||||
In addition, in order for service workers to be registered, the app must be accessed over HTTPS, not HTTP. Browsers will ignore service workers on pages that are served over an insecure connection. The reason is that service workers are quite powerful, so extra care needs to be taken to ensure the service worker script has not been tampered with.
|
||||
In order for service workers to be registered, the app must be accessed over HTTPS, not HTTP.
|
||||
Browsers ignore service workers on pages that are served over an insecure connection.
|
||||
The reason is that service workers are quite powerful, so extra care needs to be taken to ensure the service worker script has not been tampered with.
|
||||
|
||||
There is one exception to this rule: to make local development easier, browsers do _not_ require a secure connection when accessing an app on `localhost`.
|
||||
|
||||
### Browser support
|
||||
|
||||
To benefit from the Angular service worker, your app must run in a web browser that supports service workers in general.
|
||||
Currently, service workers are supported in the latest versions of Chrome, Firefox, Edge, Safari, Opera, UC Browser (Android version) and Samsung Internet.
|
||||
Browsers like IE and Opera Mini do not support service workers.
|
||||
|
||||
If the user is accessing your app via a browser that does not support service workers, the service worker is not registered and related behavior such as offline cache management and push notifications does not happen.
|
||||
More specifically:
|
||||
|
||||
* The browser does not download the service worker script and `ngsw.json` manifest file.
|
||||
* Active attempts to interact with the service worker, such as calling `SwUpdate.checkForUpdate()`, return rejected promises.
|
||||
* The observable events of related services, such as `SwUpdate.available`, are not triggered.
|
||||
|
||||
It is highly recommended that you ensure that your app works even without service worker support in the browser.
|
||||
Although an unsupported browser ignores service worker caching, it will still report errors if the app attempts to interact with the service worker.
|
||||
For example, calling `SwUpdate.checkForUpdate()` will return rejected promises.
|
||||
To avoid such an error, you can check whether the Angular service worker is enabled using `SwUpdate.isEnabled()`.
|
||||
|
||||
To learn more about other browsers that are service worker ready, see the [Can I Use](https://caniuse.com/#feat=serviceworkers) page and [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API).
|
||||
|
||||
There is one exception to this rule: To make local development easier, browsers do _not_ require a secure connection when accessing an app on `localhost`.
|
||||
|
||||
## Related resources
|
||||
|
||||
@ -46,7 +69,6 @@ For more information about browser support, see the [browser support](https://de
|
||||
|
||||
The remainder of this Angular documentation specifically addresses the Angular implementation of service workers.
|
||||
|
||||
## More on Angular service workers
|
||||
## Next steps
|
||||
|
||||
You may also be interested in the following:
|
||||
* [Getting Started with service workers](guide/service-worker-getting-started).
|
||||
To begin using Angular service workers, see [Getting Started with service workers](guide/service-worker-getting-started).
|
||||
|
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 600 445" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><g id="NullInjector"><rect x="11.864" y="16.952" width="575.424" height="114.429" style="fill:#fff;stroke:#0159d3;stroke-width:3.81px;"/><text x="208.488px" y="67.96px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:30.514px;">NullInjector()</text><g transform="matrix(0.847458,0,0,0.847619,-81.3559,-80.5238)"><text x="286.27px" y="212px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:24px;">always throws an error unless</text><text x="334.768px" y="236.785px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:24px;">you use @Optional()</text></g></g><path d="M286.822,144.941l-7.161,0l11.017,-11.017l11.017,11.017l-7.161,0l0,38.992l-7.712,0l0,-38.992Z" style="fill:#0159d3;stroke:#0159d3;stroke-width:3.81px;"/><path d="M286.822,297.512l-7.161,0l11.017,-11.017l11.017,11.017l-7.161,0l0,38.993l-7.712,0l0,-38.993Z" style="fill:#0159d3;stroke:#0159d3;stroke-width:3.81px;"/><g id="Moduleinjector"><rect x="11.864" y="168.913" width="575.424" height="114.429" style="fill:#fff;stroke:#0159d3;stroke-width:3.81px;"/><text x="215.313px" y="211.956px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:25.429px;">ModuleInjector</text><g transform="matrix(0.847458,0,0,0.847619,-24.5763,-79.6762)"><text x="215.592px" y="385px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:24px;">(configured by PlatformModule)</text><text x="74.879px" y="409.785px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:24px;">has special things like DomSanitizer => platformBrowser()</text></g></g><g id="root"><rect x="10.599" y="320.428" width="577.966" height="113.581" style="fill:#fff;stroke:#0159d3;stroke-width:3.81px;"/><g transform="matrix(0.847458,0,0,0.847619,-59.3163,262.743)"><text x="280.144px" y="115px" style="font-family:'Courier';font-size:30px;">root</text><text x="370.158px" y="115px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:30px;">ModuleInjector</text></g><g transform="matrix(0.847458,0,0,0.847619,15.5593,-82.219)"><text x="165.889px" y="559px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:24px;">(configured by <tspan x="324.209px 338.014px 351.361px " y="559px 559px 559px ">You</tspan>rAppModule)</text><text x="5.57px" y="583.785px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:24px;">has things for your app => bootstrapModule(Y<tspan x="498.332px 511.68px " y="583.785px 583.785px ">ou</tspan>rAppModule)</text></g></g></svg>
|
After Width: | Height: | Size: 2.9 KiB |
@ -276,8 +276,7 @@
|
||||
"twitter": "deborahkurata",
|
||||
"website": "http://blogs.msmvps.com/deborahk/",
|
||||
"bio": "Deborah is a software developer, author, and Google Developer Expert. She is author of several Pluralsight courses including: 'Angular 2: Getting Started' and ‘Angular Routing’",
|
||||
"groups": ["Collaborators", "GDE"],
|
||||
"mentor": "kara"
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"alyssa": {
|
||||
"name": "Alyssa Nicoll",
|
||||
|
@ -13,36 +13,18 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- ng-conf 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ng-conf.org/" title="ng-conf">ng-conf</a></th>
|
||||
<td>Salt Lake City, Utah</td>
|
||||
<td>May 1-3, 2019</td>
|
||||
</tr>
|
||||
<!-- ngVikings 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
|
||||
<td>Copenhagen, Denmark</td>
|
||||
<td>May 26 (workshops), 27-28 (conference), 2019</td>
|
||||
</tr>
|
||||
<!-- ngJapan-->
|
||||
<tr>
|
||||
<th><a href="https://ngjapan.org" title="ng-japan">ng-japan</a></th>
|
||||
<td>Tokyo, Japan</td>
|
||||
<td>July 13, 2019</td>
|
||||
</tr>
|
||||
<!-- AngularConnect 2019-->
|
||||
<tr>
|
||||
<th><a href="https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral" title="AngularConnect">AngularConnect</a></th>
|
||||
<td>London, UK</td>
|
||||
<td>September 19-20, 2019</td>
|
||||
</tr>
|
||||
<!-- NG-DE 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ng-de.org/" title="NG-DE">NG-DE</a></th>
|
||||
<td>Berlin, Germany</td>
|
||||
<td>August 29th workshops, 30-31 conference, 2019</td>
|
||||
</tr>
|
||||
<!-- AngularConnect 2019-->
|
||||
<tr>
|
||||
<th><a href="https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral" title="AngularConnect">AngularConnect</a></th>
|
||||
<td>London, UK</td>
|
||||
<td>September 19-20, 2019</td>
|
||||
</tr>
|
||||
<!-- ReactiveConf 2019 -->
|
||||
<tr>
|
||||
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
|
||||
@ -62,6 +44,24 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- ngJapan-->
|
||||
<tr>
|
||||
<th><a href="https://ngjapan.org" title="ng-japan">ng-japan</a></th>
|
||||
<td>Tokyo, Japan</td>
|
||||
<td>July 13, 2019</td>
|
||||
</tr>
|
||||
<!-- ngVikings 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
|
||||
<td>Copenhagen, Denmark</td>
|
||||
<td>May 26 (workshops), 27-28 (conference), 2019</td>
|
||||
</tr>
|
||||
<!-- ng-conf 2019-->
|
||||
<tr>
|
||||
<th><a href="https://ng-conf.org/" title="ng-conf">ng-conf</a></th>
|
||||
<td>Salt Lake City, Utah</td>
|
||||
<td>May 1-3, 2019</td>
|
||||
</tr>
|
||||
<!-- ng-India 2019-->
|
||||
<tr>
|
||||
<th><a href="https://www.ng-ind.com/" title="ng-India">ng-India</a></th>
|
||||
|
@ -259,6 +259,12 @@
|
||||
"UI Components": {
|
||||
"order": 4,
|
||||
"resources": {
|
||||
"AngularUIToolkit": {
|
||||
"desc": "Angular UI Toolkit: 115 professionally maintained UI components ranging from a robust grid to charts and more. Try for free & build Angular apps faster.",
|
||||
"rev": true,
|
||||
"title": "Angular UI Toolkit",
|
||||
"url": "https://www.angular-ui-tools.com"
|
||||
},
|
||||
"IgniteUIforAngular": {
|
||||
"desc": "Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps.",
|
||||
"rev": true,
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-with-ivy": "yarn ~~build",
|
||||
"prebuild-with-ivy-ci": "yarn setup-local --no-build-packages && node scripts/switch-to-ivy",
|
||||
"build-with-ivy-ci": "yarn ~~build --progress=false",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js de49294bf",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js e0055d293",
|
||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||
"test": "yarn check-env && ng test",
|
||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||
|
@ -50,140 +50,140 @@
|
||||
background: $lightgray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.toc-heading {
|
||||
mat-icon.rotating-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: relative;
|
||||
left: -4px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&:hover:not(.embedded) {
|
||||
color: $accentblue;
|
||||
}
|
||||
&.toc-heading {
|
||||
mat-icon.rotating-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: relative;
|
||||
left: -4px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.toc-more-items {
|
||||
color: $mediumgray;
|
||||
top: 10px;
|
||||
&:hover:not(.embedded) {
|
||||
color: $accentblue;
|
||||
}
|
||||
}
|
||||
|
||||
&.toc-more-items {
|
||||
color: $mediumgray;
|
||||
top: 10px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: $accentblue;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: 'expand_less';
|
||||
}
|
||||
|
||||
&.collapsed::after {
|
||||
content: 'more_horiz';
|
||||
}
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
&.collapsed {
|
||||
@include rotate(0deg);
|
||||
}
|
||||
|
||||
&:not(.collapsed) {
|
||||
@include rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
ul.toc-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0 8px 0 0;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
@include font-size(12);
|
||||
@include line-height(16);
|
||||
padding: 3px 0 3px 12px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.h1:after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 40%;
|
||||
margin: 7px 0 4px 0;
|
||||
background: $lightgray;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&.h3 {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($darkgray, 10);
|
||||
overflow: visible;
|
||||
@include font-size(12);
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $accentblue;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: 'expand_less';
|
||||
}
|
||||
|
||||
&.collapsed::after {
|
||||
content: 'more_horiz';
|
||||
}
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
&.collapsed {
|
||||
@include rotate(0deg);
|
||||
}
|
||||
|
||||
&:not(.collapsed) {
|
||||
@include rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
ul.toc-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0 8px 0 0;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
@include font-size(12);
|
||||
@include line-height(16);
|
||||
padding: 3px 0 3px 12px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.h1:after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 40%;
|
||||
margin: 7px 0 4px 0;
|
||||
background: $lightgray;
|
||||
clear: both;
|
||||
* {
|
||||
color: $accentblue;
|
||||
}
|
||||
}
|
||||
|
||||
&.h3 {
|
||||
padding-left: 24px;
|
||||
}
|
||||
&.active {
|
||||
* {
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
|
||||
a {
|
||||
color: lighten($darkgray, 10);
|
||||
overflow: visible;
|
||||
@include font-size(12);
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
* {
|
||||
color: $accentblue;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
* {
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
left: -3px;
|
||||
top: 12px;
|
||||
background: $blue;
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
left: -3px;
|
||||
top: 12px;
|
||||
background: $blue;
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.embedded) li {
|
||||
&:before {
|
||||
border-left: 1px solid $lightgray;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
&:not(.embedded) li {
|
||||
&:before {
|
||||
border-left: 1px solid $lightgray;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:first-child:before {
|
||||
top: 13px;
|
||||
}
|
||||
&:first-child:before {
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
&:last-child:before {
|
||||
bottom: calc(100% - 14px);
|
||||
}
|
||||
&:last-child:before {
|
||||
bottom: calc(100% - 14px);
|
||||
}
|
||||
|
||||
&:not(.active):hover a:before {
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
left: -3px;
|
||||
top: 12px;
|
||||
background: $lightgray;
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
&:not(.active):hover a:before {
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
left: -3px;
|
||||
top: 12px;
|
||||
background: $lightgray;
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
40
docs/DEBUG_MATERIAL_IVY.md
Normal file
40
docs/DEBUG_MATERIAL_IVY.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Debugging the Material unit tests job
|
||||
|
||||
Currently all changes to Ivy are validated against the test suite of the
|
||||
`angular/components` repository. In order to debug the `material-unit-tests` CI
|
||||
job, the following steps can be used:
|
||||
|
||||
1\) Build the Ivy package output by running `./scripts/build-ivy-npm-packages.sh` in
|
||||
the `angular/angular` repo.
|
||||
|
||||
2\) Clone the `angular/components` repository if not done yet ([quick link to repo](https://github.com/angular/components)).
|
||||
|
||||
3\) Set up the package output in the `components` repository by running the following
|
||||
command in the `angular/angular` repo:
|
||||
|
||||
```bash
|
||||
node ./scripts/ci/update-deps-to-dist-packages.js {COMPONENTS_REPO}/package.json ./dist/packages-dist-ivy-aot
|
||||
```
|
||||
|
||||
4\) Switch into the `components` repository and run the tests by using the
|
||||
following command:
|
||||
|
||||
```bash
|
||||
yarn test --deleted_packages=//src/dev-app --define=compile=aot
|
||||
```
|
||||
|
||||
### Running tests for individual entry-points
|
||||
|
||||
The `yarn test` script from the `components` repository runs all tests in the project.
|
||||
This is sometimes not desired because it involves building and testing of all packages
|
||||
and entry-points. Running tests for an individual entry-point is possible by explicitly
|
||||
selecting a given test target.
|
||||
|
||||
Here is an example of commands that run individual test targets. Note that it is
|
||||
**important** to specify the `--define=compile=aot` flag in order to run tests with Ivy.
|
||||
|
||||
```bash
|
||||
yarn bazel test --define=compile=aot src/material/slider:unit_tests
|
||||
yarn bazel test --define=compile=aot src/cdk/a11y:unit_tests
|
||||
yarn bazel test --define=compile=aot src/material/toolbar:unit_tests
|
||||
```
|
@ -12,7 +12,6 @@ The owner of the component is then responsible for the secondary / component-lev
|
||||
The caretaker should be able to determine which component the issue belongs to.
|
||||
The components have a clear piece of source code associated with it within the `/packages/` folder of this repo.
|
||||
|
||||
* `comp: docs-infra` - the angular.io application
|
||||
* `comp: animations`
|
||||
* `comp: bazel` - @angular/bazel rules
|
||||
* `comp: benchpress`
|
||||
@ -21,6 +20,8 @@ The components have a clear piece of source code associated with it within the `
|
||||
* `comp: core & compiler` - because core, compiler, compiler-cli and
|
||||
browser-platforms are very intertwined, we will be treating them as one
|
||||
* `comp: ivy` - a subset of core representing the new Ivy renderer.
|
||||
* `comp: ngcc` - a subset of ivy representing the [Angular Compatibility Compiler](../packages/compiler-cli/ngcc/README.md)
|
||||
* `comp: docs-infra` - the angular.io application and docs-related tooling
|
||||
* `comp: elements`
|
||||
* `comp: forms`
|
||||
* `comp: http`
|
||||
@ -170,7 +171,9 @@ If a PR is missing the `PR target: *` label, or if the label is set to "TBD" whe
|
||||
|
||||
Before a PR can be merged it must be approved by the appropriate reviewer(s).
|
||||
|
||||
To ensure that the right people review each change, we configured [GitHub CODEOWNERS](https://help.github.com/articles/about-codeowners/) (via `.github/CODEOWNERS`) and require that each PR has at least one approval from the appropriate code owner.
|
||||
To ensure that the right people review each change, we configured [GitHub CODEOWNERS](https://help.github.com/articles/about-codeowners/) (via `.github/CODEOWNERS`) and require that each PR has at least one approval from an appropriate code owner.
|
||||
|
||||
If the PR author is a code owner themselves, the approval can come from _any_ repo collaborator (person with write access). GitHub CODEOWNERs does not support this scenario, so the `merge-assistance` label must be added, mentioning that the PR author is a code owner. In any case, the reviewer should actually look through the code and provide feedback if necessary.
|
||||
|
||||
Note that approved state does not mean a PR is ready to be merged.
|
||||
For example, a reviewer might approve the PR but request a minor tweak that doesn't need further review, e.g., a rebase or small uncontroversial change.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "8.2.2",
|
||||
"version": "8.2.3",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -39,7 +39,7 @@
|
||||
"@bazel/karma": "0.34.0",
|
||||
"@bazel/protractor": "0.34.0",
|
||||
"@bazel/typescript": "0.34.0",
|
||||
"@microsoft/api-extractor": "^7.0.21",
|
||||
"@microsoft/api-extractor": "7.0.21",
|
||||
"@schematics/angular": "^8.0.0-beta.15",
|
||||
"@types/angular": "^1.6.47",
|
||||
"@types/base64-js": "1.2.5",
|
||||
|
@ -29,7 +29,7 @@
|
||||
"@angular-devkit/architect": "^0.800.0-beta.15",
|
||||
"@angular-devkit/core": "^8.0.0-beta.15",
|
||||
"@angular-devkit/schematics": "^8.0.0-beta.15",
|
||||
"@microsoft/api-extractor": "^7.0.21",
|
||||
"@microsoft/api-extractor": "7.0.21",
|
||||
"@schematics/angular": "^8.0.0-beta.15",
|
||||
"@types/node": "6.0.84",
|
||||
"semver": "^5.6.0",
|
||||
|
@ -134,7 +134,8 @@ function formatNumberToLocaleString(
|
||||
* such as "$" or "Canadian Dollar". Used in output string, but does not affect the operation
|
||||
* of the function.
|
||||
* @param currencyCode The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)
|
||||
* currency code to use in the result string, such as `USD` for the US dollar and `EUR` for the euro.
|
||||
* currency code, such as `USD` for the US dollar and `EUR` for the euro.
|
||||
* Used to determine the number of digits in the decimal part.
|
||||
* @param digitInfo Decimal representation options, specified by a string in the following format:
|
||||
* `{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}`. See `DecimalPipe` for more details.
|
||||
*
|
||||
|
@ -25,12 +25,12 @@ export interface PopStateEvent {
|
||||
*
|
||||
* A service that applications can use to interact with a browser's URL.
|
||||
*
|
||||
* Depending on the `LocationStrategy` used, `Location` will either persist
|
||||
* Depending on the `LocationStrategy` used, `Location` persists
|
||||
* to the URL's path or the URL's hash segment.
|
||||
*
|
||||
* @usageNotes
|
||||
*
|
||||
* It's better to use the {@link Router#navigate} service to trigger route changes. Use
|
||||
* It's better to use the `Router#navigate` service to trigger route changes. Use
|
||||
* `Location` only if you need to interact with or create normalized URLs outside of
|
||||
* routing.
|
||||
*
|
||||
@ -77,9 +77,9 @@ export class Location {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized URL path.
|
||||
* Normalizes the URL path for this location.
|
||||
*
|
||||
* @param includeHash Whether path has an anchor fragment.
|
||||
* @param includeHash True to include an anchor fragment in the path.
|
||||
*
|
||||
* @returns The normalized URL path.
|
||||
*/
|
||||
@ -90,17 +90,18 @@ export class Location {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the history.state object.
|
||||
* Reports the current state of the location history.
|
||||
* @returns The current value of the `history.state` object.
|
||||
*/
|
||||
getState(): unknown { return this._platformLocation.getState(); }
|
||||
|
||||
/**
|
||||
* Normalizes the given path and compares to the current normalized path.
|
||||
*
|
||||
* @param path The given URL path
|
||||
* @param query Query parameters
|
||||
* @param path The given URL path.
|
||||
* @param query Query parameters.
|
||||
*
|
||||
* @returns `true` if the given URL path is equal to the current normalized path, `false`
|
||||
* @returns True if the given URL path is equal to the current normalized path, false
|
||||
* otherwise.
|
||||
*/
|
||||
isCurrentPathEqualTo(path: string, query: string = ''): boolean {
|
||||
@ -108,23 +109,21 @@ export class Location {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representing a URL, returns the URL path after stripping the
|
||||
* trailing slashes.
|
||||
* Normalizes a URL path by stripping any trailing slashes.
|
||||
*
|
||||
* @param url String representing a URL.
|
||||
*
|
||||
* @returns Normalized URL string.
|
||||
* @returns The normalized URL string.
|
||||
*/
|
||||
normalize(url: string): string {
|
||||
return Location.stripTrailingSlash(_stripBaseHref(this._baseHref, _stripIndexHtml(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representing a URL, returns the platform-specific external URL path.
|
||||
* If the given URL doesn't begin with a leading slash (`'/'`), this method adds one
|
||||
* before normalizing. This method also adds a hash if `HashLocationStrategy` is
|
||||
* used, or the `APP_BASE_HREF` if the `PathLocationStrategy` is in use.
|
||||
*
|
||||
* Normalizes an external URL path.
|
||||
* If the given URL doesn't begin with a leading slash (`'/'`), adds one
|
||||
* before normalizing. Adds a hash if `HashLocationStrategy` is
|
||||
* in use, or the `APP_BASE_HREF` if the `PathLocationStrategy` is in use.
|
||||
*
|
||||
* @param url String representing a URL.
|
||||
*
|
||||
@ -139,12 +138,12 @@ export class Location {
|
||||
|
||||
// TODO: rename this method to pushState
|
||||
/**
|
||||
* Changes the browsers URL to a normalized version of the given URL, and pushes a
|
||||
* Changes the browser's URL to a normalized version of a given URL, and pushes a
|
||||
* new item onto the platform's history.
|
||||
*
|
||||
* @param path URL path to normalizze
|
||||
* @param query Query parameters
|
||||
* @param state Location history state
|
||||
* @param path URL path to normalize.
|
||||
* @param query Query parameters.
|
||||
* @param state Location history state.
|
||||
*
|
||||
*/
|
||||
go(path: string, query: string = '', state: any = null): void {
|
||||
@ -157,9 +156,9 @@ export class Location {
|
||||
* Changes the browser's URL to a normalized version of the given URL, and replaces
|
||||
* the top item on the platform's history stack.
|
||||
*
|
||||
* @param path URL path to normalizze
|
||||
* @param query Query parameters
|
||||
* @param state Location history state
|
||||
* @param path URL path to normalize.
|
||||
* @param query Query parameters.
|
||||
* @param state Location history state.
|
||||
*/
|
||||
replaceState(path: string, query: string = '', state: any = null): void {
|
||||
this._platformStrategy.replaceState(state, '', path, query);
|
||||
@ -178,8 +177,10 @@ export class Location {
|
||||
back(): void { this._platformStrategy.back(); }
|
||||
|
||||
/**
|
||||
* Register URL change listeners. This API can be used to catch updates performed by the Angular
|
||||
* framework. These are not detectible through "popstate" or "hashchange" events.
|
||||
* Registers a URL change listener. Use to catch updates performed by the Angular
|
||||
* framework that are not detectible through "popstate" or "hashchange" events.
|
||||
*
|
||||
* @param fn The change handler function, which take a URL and a location history state.
|
||||
*/
|
||||
onUrlChange(fn: (url: string, state: unknown) => void) {
|
||||
this._urlChangeListeners.push(fn);
|
||||
@ -192,7 +193,7 @@ export class Location {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the platform's `popState` events.
|
||||
* Subscribes to the platform's `popState` events.
|
||||
*
|
||||
* @param value Event that is triggered when the state history changes.
|
||||
* @param exception The exception to throw.
|
||||
@ -206,25 +207,24 @@ export class Location {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string of url parameters, prepend with `?` if needed, otherwise return the
|
||||
* parameters as is.
|
||||
* Normalizes URL parameters by prepending with `?` if needed.
|
||||
*
|
||||
* @param params String of URL parameters
|
||||
* @param params String of URL parameters.
|
||||
*
|
||||
* @returns URL parameters prepended with `?` or the parameters as is.
|
||||
* @returns The normalized URL parameters string.
|
||||
*/
|
||||
public static normalizeQueryParams(params: string): string {
|
||||
return params && params[0] !== '?' ? '?' + params : params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given 2 parts of a URL, join them with a slash if needed.
|
||||
* Joins two parts of a URL with a slash if needed.
|
||||
*
|
||||
* @param start URL string
|
||||
* @param end URL string
|
||||
*
|
||||
*
|
||||
* @returns Given URL strings joined with a slash, if needed.
|
||||
* @returns The joined URL string.
|
||||
*/
|
||||
public static joinWithSlash(start: string, end: string): string {
|
||||
if (start.length == 0) {
|
||||
@ -250,14 +250,13 @@ export class Location {
|
||||
}
|
||||
|
||||
/**
|
||||
* If URL has a trailing slash, remove it, otherwise return the URL as is. The
|
||||
* method looks for the first occurrence of either `#`, `?`, or the end of the
|
||||
* Removes a trailing slash from a URL string if needed.
|
||||
* Looks for the first occurrence of either `#`, `?`, or the end of the
|
||||
* line as `/` characters and removes the trailing slash if one exists.
|
||||
*
|
||||
* @param url URL string
|
||||
* @param url URL string.
|
||||
*
|
||||
* @returns Returns a URL string after removing the trailing slash if one exists, otherwise
|
||||
* returns the string as is.
|
||||
* @returns The URL string, modified if needed.
|
||||
*/
|
||||
public static stripTrailingSlash(url: string): string {
|
||||
const match = url.match(/#|\?|$/);
|
||||
|
@ -10,7 +10,6 @@ npm_package(
|
||||
srcs = ["migrations.json"],
|
||||
visibility = ["//packages/core:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/injectable-pipe",
|
||||
"//packages/core/schematics/migrations/move-document",
|
||||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
"//packages/core/schematics/migrations/static-queries",
|
||||
|
@ -6,7 +6,6 @@ ts_library(
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = ["//packages/core/schematics/test:__pkg__"],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/injectable-pipe",
|
||||
"//packages/core/schematics/migrations/missing-injectable",
|
||||
"//packages/core/schematics/migrations/missing-injectable/google3",
|
||||
"//packages/core/schematics/migrations/static-queries",
|
||||
|
@ -1,55 +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
|
||||
*/
|
||||
|
||||
import {Replacement, RuleFailure, Rules} from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {InjectablePipeVisitor} from '../injectable-pipe/angular/injectable_pipe_visitor';
|
||||
import {INJECTABLE_DECORATOR_NAME, addImport, getNamedImports} from '../injectable-pipe/util';
|
||||
|
||||
|
||||
/**
|
||||
* TSLint rule that flags `@Pipe` classes that haven't been marked as `@Injectable`.
|
||||
*/
|
||||
export class Rule extends Rules.TypedRule {
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
|
||||
const visitor = new InjectablePipeVisitor(program.getTypeChecker());
|
||||
const printer = ts.createPrinter();
|
||||
const failures: RuleFailure[] = [];
|
||||
|
||||
visitor.visitNode(sourceFile);
|
||||
|
||||
visitor.missingInjectablePipes.forEach(data => {
|
||||
const {pipeDecorator, importDeclarationMissingImport} = data;
|
||||
const fixes = [new Replacement(
|
||||
pipeDecorator.getStart(), pipeDecorator.getWidth(),
|
||||
`@${INJECTABLE_DECORATOR_NAME}()\n${pipeDecorator.getText()}`)];
|
||||
|
||||
if (importDeclarationMissingImport) {
|
||||
const namedImports = getNamedImports(importDeclarationMissingImport);
|
||||
|
||||
// Add another fix that'll add the missing import.
|
||||
if (namedImports) {
|
||||
fixes.push(new Replacement(
|
||||
namedImports.getStart(), namedImports.getWidth(),
|
||||
printer.printNode(
|
||||
ts.EmitHint.Unspecified, addImport(namedImports, INJECTABLE_DECORATOR_NAME),
|
||||
sourceFile)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add a failure on Pipe decorators that are missing the Injectable decorator.
|
||||
failures.push(new RuleFailure(
|
||||
sourceFile, pipeDecorator.getStart(), pipeDecorator.getWidth(),
|
||||
'Classes with @Pipe should be decorated with @Injectable so that they can be injected.',
|
||||
this.ruleName, fixes));
|
||||
});
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "injectable-pipe",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||
visibility = [
|
||||
"//packages/core/schematics:__pkg__",
|
||||
"//packages/core/schematics/migrations/google3:__pkg__",
|
||||
"//packages/core/schematics/test:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core/schematics/utils",
|
||||
"@npm//@angular-devkit/schematics",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
@ -1,22 +0,0 @@
|
||||
## Injectable annotation on pipes
|
||||
|
||||
In ViewEngine it was possible to inject a class that was annotated as a `Pipe`, however this no
|
||||
longer works in Ivy if the class also doesn't have the `Injectable` decorator. This migration
|
||||
adds `Injectable` automatically to all `Pipe` classes.
|
||||
|
||||
### Before
|
||||
```ts
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'myPipe' })
|
||||
class MyPipe {}
|
||||
```
|
||||
|
||||
### After
|
||||
```ts
|
||||
import { Pipe, Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
@Pipe({ name: 'myPipe' })
|
||||
class MyPipe {}
|
||||
```
|
@ -1,67 +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
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {getAngularDecorators} from '../../../utils/ng_decorators';
|
||||
import {INJECTABLE_DECORATOR_NAME} from '../util';
|
||||
|
||||
/**
|
||||
* Goes through all of the descendant nodes of a given node and lists out all of the pipes
|
||||
* that don't have `@Injectable`, as well as their `@Pipe` decorator and the import declaration
|
||||
* from which we'd need to import the `Injectable` decorator.
|
||||
*/
|
||||
export class InjectablePipeVisitor {
|
||||
/**
|
||||
* Keeps track of all the classes that have a `Pipe` decorator, but not `Injectable`, as well
|
||||
* as a reference to the `Pipe` decorator itself and import declarations from which we'll have
|
||||
* to import the `Injectable` decorator.
|
||||
*/
|
||||
missingInjectablePipes: {
|
||||
classDeclaration: ts.ClassDeclaration,
|
||||
importDeclarationMissingImport: ts.ImportDeclaration|null,
|
||||
pipeDecorator: ts.Decorator
|
||||
}[] = [];
|
||||
|
||||
constructor(private _typeChecker: ts.TypeChecker) {}
|
||||
|
||||
visitNode(node: ts.Node) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
this._visitClassDeclaration(node as ts.ClassDeclaration);
|
||||
break;
|
||||
}
|
||||
|
||||
ts.forEachChild(node, node => this.visitNode(node));
|
||||
}
|
||||
|
||||
private _visitClassDeclaration(node: ts.ClassDeclaration) {
|
||||
if (!node.decorators || !node.decorators.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ngDecorators = getAngularDecorators(this._typeChecker, node.decorators);
|
||||
const pipeDecorator = ngDecorators.find(decorator => decorator.name === 'Pipe');
|
||||
const hasInjectableDecorator =
|
||||
!ngDecorators.some(decorator => decorator.name === INJECTABLE_DECORATOR_NAME);
|
||||
|
||||
// Skip non-pipe classes and pipes that are already marked as injectable.
|
||||
if (pipeDecorator && hasInjectableDecorator) {
|
||||
const importNode = pipeDecorator.importNode;
|
||||
const namedImports = importNode.importClause && importNode.importClause.namedBindings;
|
||||
const needsImport = namedImports && ts.isNamedImports(namedImports) &&
|
||||
!namedImports.elements.some(element => element.name.text === INJECTABLE_DECORATOR_NAME);
|
||||
|
||||
this.missingInjectablePipes.push({
|
||||
classDeclaration: node,
|
||||
importDeclarationMissingImport: needsImport ? importNode : null,
|
||||
pipeDecorator: pipeDecorator.node
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +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
|
||||
*/
|
||||
|
||||
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {dirname, relative} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
|
||||
|
||||
import {InjectablePipeVisitor} from './angular/injectable_pipe_visitor';
|
||||
import {INJECTABLE_DECORATOR_NAME, addImport, getNamedImports} from './util';
|
||||
|
||||
/**
|
||||
* Runs a migration over a TypeScript project that adds an `@Injectable`
|
||||
* annotation to all classes that have `@Pipe`.
|
||||
*/
|
||||
export default function(): Rule {
|
||||
return (tree: Tree) => {
|
||||
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
|
||||
const basePath = process.cwd();
|
||||
const allPaths = [...buildPaths, ...testPaths];
|
||||
|
||||
if (!allPaths.length) {
|
||||
throw new SchematicsException(
|
||||
'Could not find any tsconfig file. Cannot add Injectable annotation to pipes.');
|
||||
}
|
||||
|
||||
for (const tsconfigPath of allPaths) {
|
||||
runInjectablePipeMigration(tree, tsconfigPath, basePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runInjectablePipeMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
|
||||
const host = ts.createCompilerHost(parsed.options, true);
|
||||
|
||||
// We need to overwrite the host "readFile" method, as we want the TypeScript
|
||||
// program to be based on the file contents in the virtual file tree. Otherwise
|
||||
// if we run the migration for multiple tsconfig files which have intersecting
|
||||
// source files, it can end up updating query definitions multiple times.
|
||||
host.readFile = fileName => {
|
||||
const buffer = tree.read(relative(basePath, fileName));
|
||||
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which
|
||||
// which breaks the CLI UpdateRecorder.
|
||||
// See: https://github.com/angular/angular/pull/30719
|
||||
return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined;
|
||||
};
|
||||
|
||||
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const visitor = new InjectablePipeVisitor(typeChecker);
|
||||
const sourceFiles = program.getSourceFiles().filter(
|
||||
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
|
||||
const printer = ts.createPrinter();
|
||||
|
||||
sourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));
|
||||
|
||||
visitor.missingInjectablePipes.forEach(data => {
|
||||
const {classDeclaration, importDeclarationMissingImport} = data;
|
||||
const sourceFile = classDeclaration.getSourceFile();
|
||||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||
|
||||
// Note that we don't need to go through the AST to insert the decorator, because the change
|
||||
// is pretty basic. Also this has a better chance of preserving the user's formatting.
|
||||
update.insertLeft(classDeclaration.getStart(), `@${INJECTABLE_DECORATOR_NAME}()\n`);
|
||||
|
||||
// Add @Injectable to the imports if it isn't imported already. Note that this doesn't deal with
|
||||
// the case where there aren't any imports for `@angular/core` at all. We don't need to handle
|
||||
// it because the Pipe decorator won't be recognized if it hasn't been imported from Angular.
|
||||
if (importDeclarationMissingImport) {
|
||||
const namedImports = getNamedImports(importDeclarationMissingImport);
|
||||
|
||||
if (namedImports) {
|
||||
update.remove(namedImports.getStart(), namedImports.getWidth());
|
||||
update.insertRight(
|
||||
namedImports.getStart(),
|
||||
printer.printNode(
|
||||
ts.EmitHint.Unspecified, addImport(namedImports, INJECTABLE_DECORATOR_NAME),
|
||||
sourceFile));
|
||||
}
|
||||
}
|
||||
|
||||
tree.commitUpdate(update);
|
||||
});
|
||||
}
|
@ -1,36 +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
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/** Name of the Injectable decorator. */
|
||||
export const INJECTABLE_DECORATOR_NAME = 'Injectable';
|
||||
|
||||
/**
|
||||
* Adds an import to a named import node, if the import does not exist already.
|
||||
* @param node Node to which to add the import.
|
||||
* @param importName Name of the import that should be added.
|
||||
*/
|
||||
export function addImport(node: ts.NamedImports, importName: string) {
|
||||
const elements = node.elements;
|
||||
const isAlreadyImported = elements.some(element => element.name.text === importName);
|
||||
|
||||
if (!isAlreadyImported) {
|
||||
return ts.updateNamedImports(
|
||||
node, [...elements, ts.createImportSpecifier(undefined, ts.createIdentifier(importName))]);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Gets the named imports node from an import declaration. */
|
||||
export function getNamedImports(node: ts.ImportDeclaration): ts.NamedImports|null {
|
||||
const importClause = node.importClause;
|
||||
const namedImports = importClause && importClause.namedBindings;
|
||||
return (namedImports && ts.isNamedImports(namedImports)) ? namedImports : null;
|
||||
}
|
@ -166,16 +166,18 @@ function filterQueryClassMemberNodes(
|
||||
// (1) queries used in the "ngOnInit" lifecycle hook are static.
|
||||
// (2) inputs with setters can access queries statically.
|
||||
return classDecl.members
|
||||
.filter(m => {
|
||||
if (ts.isMethodDeclaration(m) && m.body && hasPropertyNameText(m.name) &&
|
||||
STATIC_QUERY_LIFECYCLE_HOOKS[query.type].indexOf(m.name.text) !== -1) {
|
||||
return true;
|
||||
} else if (
|
||||
knownInputNames && ts.isSetAccessor(m) && m.body && hasPropertyNameText(m.name) &&
|
||||
knownInputNames.indexOf(m.name.text) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((member: ts.SetAccessorDeclaration | ts.MethodDeclaration) => member.body !);
|
||||
.filter(
|
||||
(m):
|
||||
m is(ts.SetAccessorDeclaration | ts.MethodDeclaration) => {
|
||||
if (ts.isMethodDeclaration(m) && m.body && hasPropertyNameText(m.name) &&
|
||||
STATIC_QUERY_LIFECYCLE_HOOKS[query.type].indexOf(m.name.text) !== -1) {
|
||||
return true;
|
||||
} else if (
|
||||
knownInputNames && ts.isSetAccessor(m) && m.body &&
|
||||
hasPropertyNameText(m.name) && knownInputNames.indexOf(m.name.text) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(member => member.body !);
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ ts_library(
|
||||
],
|
||||
deps = [
|
||||
"//packages/core/schematics/migrations/google3",
|
||||
"//packages/core/schematics/migrations/injectable-pipe",
|
||||
"//packages/core/schematics/migrations/missing-injectable",
|
||||
"//packages/core/schematics/migrations/move-document",
|
||||
"//packages/core/schematics/migrations/renderer-to-renderer2",
|
||||
|
@ -1,93 +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
|
||||
*/
|
||||
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
import {dirname, join} from 'path';
|
||||
import * as shx from 'shelljs';
|
||||
import {Configuration, Linter} from 'tslint';
|
||||
|
||||
describe('Google3 injectable pipe TSLint rule', () => {
|
||||
const rulesDirectory = dirname(require.resolve('../../migrations/google3/injectablePipeRule'));
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = join(process.env['TEST_TMPDIR'] !, 'google3-test');
|
||||
shx.mkdir('-p', tmpDir);
|
||||
|
||||
writeFile('tsconfig.json', JSON.stringify({compilerOptions: {module: 'es2015'}}));
|
||||
});
|
||||
|
||||
afterEach(() => shx.rm('-r', tmpDir));
|
||||
|
||||
function runTSLint(fix = true) {
|
||||
const program = Linter.createProgram(join(tmpDir, 'tsconfig.json'));
|
||||
const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program);
|
||||
const config = Configuration.parseConfigFile(
|
||||
{rules: {'injectable-pipe': true}, linterOptions: {typeCheck: true}});
|
||||
|
||||
program.getRootFileNames().forEach(fileName => {
|
||||
linter.lint(fileName, program.getSourceFile(fileName) !.getFullText(), config);
|
||||
});
|
||||
|
||||
return linter;
|
||||
}
|
||||
|
||||
function writeFile(fileName: string, content: string) {
|
||||
writeFileSync(join(tmpDir, fileName), content);
|
||||
}
|
||||
|
||||
function getFile(fileName: string) { return readFileSync(join(tmpDir, fileName), 'utf8'); }
|
||||
|
||||
it('should report pipes that are not marked as Injectable', () => {
|
||||
writeFile('index.ts', `
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'myPipe' })
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
const linter = runTSLint(false);
|
||||
const failures = linter.getResult().failures;
|
||||
|
||||
expect(failures.length).toBe(1);
|
||||
expect(failures[0].getFailure()).toMatch(/@Pipe should be decorated with @Injectable/);
|
||||
});
|
||||
|
||||
it('should add @Injectable to pipes that do not have it', () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'myPipe' })
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
runTSLint();
|
||||
expect(getFile('/index.ts'))
|
||||
.toMatch(/@Injectable\(\)\s+@Pipe\(\{ name: 'myPipe' \}\)\s+export class MyPipe/);
|
||||
});
|
||||
|
||||
it('should add an import for Injectable to the @angular/core import declaration', () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe()
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
runTSLint();
|
||||
|
||||
const content = getFile('/index.ts');
|
||||
expect(content).toContain('import { Pipe, Injectable } from \'@angular/core\'');
|
||||
expect((content.match(/import/g) || []).length).toBe(1, 'Expected only one import statement');
|
||||
});
|
||||
|
||||
});
|
@ -1,143 +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
|
||||
*/
|
||||
|
||||
import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
|
||||
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||
import {HostTree} from '@angular-devkit/schematics';
|
||||
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||
import * as shx from 'shelljs';
|
||||
|
||||
describe('injectable pipe migration', () => {
|
||||
let runner: SchematicTestRunner;
|
||||
let host: TempScopedNodeJsSyncHost;
|
||||
let tree: UnitTestTree;
|
||||
let tmpDirPath: string;
|
||||
let previousWorkingDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
runner = new SchematicTestRunner('test', require.resolve('./test-migrations.json'));
|
||||
host = new TempScopedNodeJsSyncHost();
|
||||
tree = new UnitTestTree(new HostTree(host));
|
||||
|
||||
writeFile('/tsconfig.json', JSON.stringify({
|
||||
compilerOptions: {
|
||||
lib: ['es2015'],
|
||||
}
|
||||
}));
|
||||
writeFile('/angular.json', JSON.stringify({
|
||||
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
|
||||
}));
|
||||
|
||||
previousWorkingDir = shx.pwd();
|
||||
tmpDirPath = getSystemPath(host.root);
|
||||
|
||||
// Switch into the temporary directory path. This allows us to run
|
||||
// the schematic against our custom unit test tree.
|
||||
shx.cd(tmpDirPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shx.cd(previousWorkingDir);
|
||||
shx.rm('-r', tmpDirPath);
|
||||
});
|
||||
|
||||
it('should add @Injectable to pipes that do not have it', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'myPipe' })
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
expect(tree.readContent('/index.ts'))
|
||||
.toMatch(/@Injectable\(\)\s+@Pipe\(\{ name: 'myPipe' \}\)\s+export class MyPipe/);
|
||||
});
|
||||
|
||||
it('should add @Injectable to pipes that do not have it (BOM)', () => {
|
||||
writeFile('/index.ts', `\uFEFF
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'myPipe' })
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
runMigration();
|
||||
expect(tree.readContent('/index.ts'))
|
||||
.toMatch(/@Injectable\(\)\s+@Pipe\(\{ name: 'myPipe' \}\)\s+export class MyPipe/);
|
||||
});
|
||||
|
||||
it('should add an import for Injectable to the @angular/core import declaration', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { Pipe } from '@angular/core';
|
||||
|
||||
@Pipe()
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
const content = tree.readContent('/index.ts');
|
||||
expect(content).toContain('import { Pipe, Injectable } from \'@angular/core\'');
|
||||
expect((content.match(/import/g) || []).length).toBe(1, 'Expected only one import statement');
|
||||
});
|
||||
|
||||
it('should not add an import for Injectable if it is imported already', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { Pipe, Injectable, NgModule } from '@angular/core';
|
||||
|
||||
@Pipe()
|
||||
export class MyPipe {
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
expect(tree.readContent('/index.ts'))
|
||||
.toContain('import { Pipe, Injectable, NgModule } from \'@angular/core\'');
|
||||
});
|
||||
|
||||
it('should do nothing if the pipe is marked as injectable already', async() => {
|
||||
const source = `
|
||||
import { Injectable, Pipe } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
@Pipe()
|
||||
export class MyPipe {
|
||||
}
|
||||
`;
|
||||
|
||||
writeFile('/index.ts', source);
|
||||
await runMigration();
|
||||
expect(tree.readContent('/index.ts')).toBe(source);
|
||||
});
|
||||
|
||||
it('should not add @Injectable if @Pipe was not imported from @angular/core', async() => {
|
||||
const source = `
|
||||
import { Pipe } from '@not-angular/core';
|
||||
|
||||
@Pipe()
|
||||
export class MyPipe {
|
||||
}
|
||||
`;
|
||||
|
||||
writeFile('/index.ts', source);
|
||||
await runMigration();
|
||||
expect(tree.readContent('/index.ts')).toBe(source);
|
||||
});
|
||||
|
||||
function writeFile(filePath: string, contents: string) {
|
||||
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||
}
|
||||
|
||||
function runMigration() {
|
||||
runner.runSchematicAsync('migration-injectable-pipe', {}, tree).toPromise();
|
||||
}
|
||||
});
|
@ -2,10 +2,6 @@
|
||||
// Migrations which are not publicly enabled but still run as part of tests need to
|
||||
// be part of a schematic collection in order to be able to run it.
|
||||
"schematics": {
|
||||
"migration-injectable-pipe": {
|
||||
"description": "Migrates all Pipe classes so that they have an Injectable annotation",
|
||||
"factory": "../migrations/injectable-pipe/index"
|
||||
},
|
||||
"migration-missing-injectable": {
|
||||
"description": "Migrates all declared undecorated providers with the @Injectable decorator",
|
||||
"factory": "../migrations/missing-injectable/index"
|
||||
|
@ -1,10 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitAny": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"strict": true,
|
||||
"lib": ["es2015"],
|
||||
"types": [],
|
||||
"baseUrl": ".",
|
||||
|
@ -7,6 +7,7 @@ ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//packages:tsconfig-build.json",
|
||||
"//packages:tsconfig-test",
|
||||
],
|
||||
)
|
||||
|
@ -279,7 +279,7 @@ function defaultRouterHook(snapshot: RouterStateSnapshot, runExtras: {
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
* An NgModule that provides navigation and URL manipulation capabilities.
|
||||
* A service that provides navigation and URL manipulation capabilities.
|
||||
*
|
||||
* @see `Route`.
|
||||
* @see [Routing and Navigation Guide](guide/router).
|
||||
|
@ -21,10 +21,6 @@ cd ${MATERIAL_REPO_TMP_DIR}
|
||||
# Note that it's not necessary to perform a yarn install, as Bazel performs its own yarn install.
|
||||
node ${angular_dir}/scripts/ci/update-deps-to-dist-packages.js ${MATERIAL_REPO_TMP_DIR}/package.json ${angular_dir}/dist/packages-dist-ivy-aot/
|
||||
|
||||
# Append the test blocklist into angular/material2's karma-test-shim.js.
|
||||
# This filters out known-failing tests because the goal is to prevent regressions.
|
||||
cat ${angular_dir}/tools/material-ci/angular_material_test_blocklist.js >> ./test/karma-test-shim.js
|
||||
|
||||
# Create a symlink for the Bazel binary installed through NPM, as running through Yarn introduces OOM errors.
|
||||
./scripts/circleci/setup_bazel_binary.sh
|
||||
|
||||
|
204
scripts/saucelabs/run-bazel-via-tunnel.sh
Executable file
204
scripts/saucelabs/run-bazel-via-tunnel.sh
Executable file
@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -u -e -o pipefail
|
||||
|
||||
# Prints out usage information for the script.
|
||||
function printUsage {
|
||||
echo -e "\e[1mrun-bazel-via-tunnel.sh\e[0m - Runs a bazel command using a saucelabs tunnel
|
||||
|
||||
\e[1mUsage:\e[0m $0 --tunnel-id=<tunnel_id> \\
|
||||
--username=<saucelabs_username> --key=<saucelabs_key> <bazel command>
|
||||
|
||||
\e[1mExample:\e[0m ./run-bazel-via-tunnel.sh --tunnel-id=<tunnel_id> \\
|
||||
--username=<saucelabs_username> --key=<saucelabs_key> \\
|
||||
yarn bazel test //src:everything
|
||||
|
||||
Flags:
|
||||
--username: The saucelabs username
|
||||
--key: The saucelabs access key
|
||||
--tunnel-id: An identifier for the saucelabs tunnel";
|
||||
}
|
||||
|
||||
# Ensures a file is created, creating directories for the full path as needed.
|
||||
function touch-safe {
|
||||
for f in "$@"; do
|
||||
[ -d $f:h ] || mkdir -p $f:h && command touch $f
|
||||
done
|
||||
}
|
||||
|
||||
# The root directory of the git project the script is running in.
|
||||
readonly GIT_ROOT_DIR=$(git rev-parse --show-toplevel 2> /dev/null)
|
||||
# Location for the saucelabs log file.
|
||||
readonly SAUCE_LOG_FILE=/tmp/angular/sauce-connect.log
|
||||
# Location for the saucelabs ready to connect lock file.
|
||||
readonly SAUCE_READY_FILE=/tmp/angular/sauce-connect-ready-file.lock
|
||||
# Location for the saucelabs ready to connection process id lock file.
|
||||
readonly SAUCE_PID_FILE=/tmp/angular/sauce-connect-pid-file.lock
|
||||
# Amount of seconds we wait for sauceconnect to establish a tunnel instance. In order to not
|
||||
# acquire CircleCI instances for too long if sauceconnect failed, we need a connect timeout.
|
||||
readonly SAUCE_READY_FILE_TIMEOUT=120
|
||||
|
||||
# Create saucelabs log file if it doesn't already exist.
|
||||
touch-safe $SAUCE_LOG_FILE;
|
||||
|
||||
# Handle configuration of script from command line flags and arguments
|
||||
OPTIONS=$(getopt -u -l tunnel-id:,username:,key:,help --options "" -- "$@")
|
||||
# Exit if flag parsing fails.
|
||||
if [ $? != 0 ] ; then echo "Failed to parse flags, exiting" && printUsage >&2 ; exit 1 ; fi
|
||||
set -- $OPTIONS
|
||||
while true; do
|
||||
case "$1" in
|
||||
--tunnel-id)
|
||||
shift
|
||||
SAUCE_TUNNEL_IDENTIFIER=$1
|
||||
;;
|
||||
--username)
|
||||
shift
|
||||
SAUCE_USERNAME=$1
|
||||
;;
|
||||
--key)
|
||||
shift
|
||||
SAUCE_ACCESS_KEY=$1
|
||||
;;
|
||||
--help)
|
||||
printUsage
|
||||
exit 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
USER_COMMAND=$@
|
||||
break
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check each required flag and parameter
|
||||
if [[ -z ${SAUCE_TUNNEL_IDENTIFIER+x} ]]; then
|
||||
echo "Missing required flag: --tunnel-id"
|
||||
badCommandSyntax=1
|
||||
fi
|
||||
if [[ -z ${SAUCE_USERNAME+x} ]]; then
|
||||
echo "Missing required flag: --username"
|
||||
badCommandSyntax=1
|
||||
fi
|
||||
if [[ -z ${SAUCE_ACCESS_KEY+x} ]]; then
|
||||
echo "Missing required flag: --key"
|
||||
badCommandSyntax=1
|
||||
fi
|
||||
if [[ "${USER_COMMAND}" == "" ]]; then
|
||||
echo "Missing required bazel command: Bazel command for running in saucelabs tunnel"
|
||||
badCommandSyntax=1
|
||||
elif [[ ! $USER_COMMAND =~ ^(yarn bazel) ]]; then
|
||||
echo "The command provided must be a bazel command run via yarn, beginning with \"yarn bazel\""
|
||||
badCommandSyntax=1
|
||||
fi
|
||||
|
||||
# If any required flag or parameter were found to be missing or incorrect, exit the script.
|
||||
if [[ ${badCommandSyntax+x} ]]; then
|
||||
echo
|
||||
printUsage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Command arguments that will be passed to sauce-connect.
|
||||
# By default we disable SSL bumping for all requests. This is because SSL bumping is
|
||||
# not needed for our test setup and in order to perform the SSL bumping, Saucelabs
|
||||
# intercepts all HTTP requests in the tunnel VM and modifies them. This can cause
|
||||
# flakiness as it makes all requests dependent on the SSL bumping middleware.
|
||||
# See: https://wiki.saucelabs.com/display/DOCS/Troubleshooting+Sauce+Connect#TroubleshootingSauceConnect-DisablingSSLBumping
|
||||
sauceArgs="--no-ssl-bump-domains all"
|
||||
sauceArgs="${sauceArgs} --logfile ${SAUCE_LOG_FILE}"
|
||||
sauceArgs="${sauceArgs} --readyfile ${SAUCE_READY_FILE}"
|
||||
sauceArgs="${sauceArgs} --pidfile ${SAUCE_PID_FILE}"
|
||||
sauceArgs="${sauceArgs} --tunnel-identifier ${SAUCE_TUNNEL_IDENTIFIER}"
|
||||
sauceArgs="${sauceArgs} -u ${SAUCE_USERNAME}"
|
||||
|
||||
#########################
|
||||
# Open saucelabs tunnel #
|
||||
#########################
|
||||
|
||||
|
||||
${GIT_ROOT_DIR}/node_modules/sauce-connect/bin/sc -k $SAUCE_ACCESS_KEY ${sauceArgs} &
|
||||
|
||||
|
||||
########################################
|
||||
# Wait for saucelabs tunnel to connect #
|
||||
########################################
|
||||
counter=0
|
||||
|
||||
while [[ ! -f ${SAUCE_READY_FILE} ]]; do
|
||||
counter=$((counter + 1))
|
||||
|
||||
# Counter needs to be multiplied by two because the while loop only sleeps a half second.
|
||||
# This has been made in favor of better progress logging (printing dots every half second)
|
||||
if [ $counter -gt $[${SAUCE_READY_FILE_TIMEOUT} * 2] ]; then
|
||||
echo "Timed out after ${SAUCE_READY_FILE_TIMEOUT} seconds waiting for tunnel ready file."
|
||||
echo "Printing logfile output:"
|
||||
echo ""
|
||||
cat ${SAUCE_LOG_FILE}
|
||||
exit 5
|
||||
fi
|
||||
|
||||
printf "."
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
#########################
|
||||
# Execute Bazel command #
|
||||
#########################
|
||||
|
||||
# Prevent immediate exit for Bazel test failures
|
||||
set +e
|
||||
|
||||
(
|
||||
cd $GIT_ROOT_DIR && \
|
||||
# Run bazel command with saucelabs specific environment variables passed to the action
|
||||
# The KARMA_WEB_TEST_MODE and SAUCE_TUNNEL_IDENTIFIER environment variables provide
|
||||
# envirnment variables to be read in the karma configuration file to set correct
|
||||
# configurations for karma saucelabs and browser configs.
|
||||
# Usage of these envirnment variables can be seen in this repo in
|
||||
# /karma-js.conf.js and /browser-providers.conf.js
|
||||
eval "$USER_COMMAND --define=KARMA_WEB_TEST_MODE=SL_REQUIRED \
|
||||
--action_env=SAUCE_USERNAME=$SAUCE_USERNAME \
|
||||
--action_env=SAUCE_ACCESS_KEY=$SAUCE_ACCESS_KEY \
|
||||
--action_env=SAUCE_READY_FILE=$SAUCE_READY_FILE \
|
||||
--action_env=SAUCE_PID_FILE=$SAUCE_PID_FILE \
|
||||
--action_env=SAUCE_TUNNEL_IDENTIFIER=$SAUCE_TUNNEL_IDENTIFIER"
|
||||
)
|
||||
BAZEL_EXIT_CODE=$?
|
||||
echo "Exit code for bazel command was: $BAZEL_EXIT_CODE"
|
||||
|
||||
# Reenable immediate exit for failure exit code
|
||||
set -e
|
||||
|
||||
##############################
|
||||
# Close the saucelabs tunnel #
|
||||
##############################
|
||||
|
||||
if [[ ! -f ${SAUCE_PID_FILE} ]]; then
|
||||
echo "Could not find Saucelabs tunnel PID file. Cannot stop tunnel.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shutting down Sauce Connect tunnel"
|
||||
|
||||
# The process id for the sauce-connect instance is stored inside of the pidfile.
|
||||
tunnelProcessId=$(cat ${SAUCE_PID_FILE})
|
||||
|
||||
# Kill the process by using the PID that has been read from the pidfile. Note that
|
||||
# we cannot use killall because CircleCI base container images don't have it installed.
|
||||
kill ${tunnelProcessId}
|
||||
|
||||
while (ps -p ${tunnelProcessId} &> /dev/null); do
|
||||
printf "."
|
||||
sleep .5
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Sauce Connect tunnel has been shut down"
|
||||
|
||||
exit $BAZEL_EXIT_CODE
|
@ -11,7 +11,6 @@ module.exports = (gulp) => () => {
|
||||
const ignoredScopes = [
|
||||
'aio',
|
||||
'docs-infra',
|
||||
'ivy',
|
||||
'zone.js',
|
||||
];
|
||||
|
||||
|
@ -1,20 +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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Blocklist of unit tests from angular/material2 with ivy that are skipped when running on
|
||||
* angular/angular. As bugs are resolved, items should be removed from this blocklist.
|
||||
*
|
||||
* The `notes` section should be used to keep track of specific issues associated with the failures.
|
||||
*/
|
||||
|
||||
// clang-format off
|
||||
// tslint:disable
|
||||
|
||||
window.testBlocklist = {};
|
||||
// clang-format on
|
@ -1,24 +0,0 @@
|
||||
### Unit tests for Angular CDK/Material
|
||||
The unit tests from angular/material2 run on CircleCI under the `material-unit-tests` job.
|
||||
Known failing tests are skipped based on the blocklist in
|
||||
`tools/material-ci/angular_material_test_blocklist.js`. Whenever the root cause of a known failure
|
||||
is identified, the `notes` field for the corresponding tests should be updated. Whenever a failure
|
||||
is resolved, the corresponding tests should be removed from the blocklist.
|
||||
|
||||
### Debugging
|
||||
To debug a failure, you need to work against the angular/material2 repo:
|
||||
1. Clone `angular/material2`
|
||||
2. Checkout the `ivy-2019` branch
|
||||
3. Run `yarn`
|
||||
4. Run `scripts/ivy/install-angular.sh path/to/local/angular/repo`
|
||||
5. Run `gulp test`
|
||||
|
||||
### Regenerating the blocklist
|
||||
If a problem has been fixed, you can regenerate the blocklist by:
|
||||
1. Clone `angular/material2`
|
||||
2. Checkout the `ivy-2019` branch
|
||||
3. Run `yarn`
|
||||
4. Run `scripts/ivy/install-angular.sh path/to/local/angular/repo`
|
||||
5. Run `gulp test`. Let it finish. It will take a few minutes.
|
||||
6. Run `scripts/ivy/generate-blocklist.js path/to/local/angular/repo`
|
||||
7. Copy the new blocklist from `dist/angular_material_test_blocklist.js`
|
@ -27,6 +27,7 @@
|
||||
"http",
|
||||
"ivy",
|
||||
"language-service",
|
||||
"ngcc",
|
||||
"platform-browser",
|
||||
"platform-browser-dynamic",
|
||||
"platform-server",
|
||||
|
@ -301,7 +301,7 @@
|
||||
through2 "^2.0.0"
|
||||
xdg-basedir "^3.0.0"
|
||||
|
||||
"@microsoft/api-extractor@^7.0.21":
|
||||
"@microsoft/api-extractor@7.0.21":
|
||||
version "7.0.21"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.0.21.tgz#d5ff9bba4ff7283503aac83ad489b16cad293fc7"
|
||||
integrity sha512-7lFcHNykVz0tvgOz9juXqP+a1j0EmnJ9J080CBE/171IxL4fBrpslPhqN86dNuavuPragRpBLc8Okv/bV7FJPQ==
|
||||
|
Reference in New Issue
Block a user