Compare commits
172 Commits
7.0.0-beta
...
bazel
Author | SHA1 | Date | |
---|---|---|---|
652ac79d63 | |||
31632b27c7 | |||
08e4489cf5 | |||
0e1ca096da | |||
2546c66376 | |||
81f5656be0 | |||
a2878b0b1d | |||
5977b72e9c | |||
7373da9b11 | |||
783a682a7d | |||
d22418d417 | |||
4b1fd98093 | |||
935ef13096 | |||
f4b60588fb | |||
15dadb92ef | |||
ce06a75ebf | |||
9889276b15 | |||
d0f7eadc09 | |||
4b132c9848 | |||
46729c76a0 | |||
f22deb2e2d | |||
57de9fc41a | |||
31034f5146 | |||
c5899f4cd4 | |||
ab379ab72a | |||
32e479ffec | |||
391c708d7e | |||
c51331689f | |||
68fadd9b97 | |||
2ad1bb4eb9 | |||
794c3595d4 | |||
b807106f54 | |||
86e6a2099a | |||
9993c72335 | |||
f455518d80 | |||
7cf5807100 | |||
9523991a9b | |||
9acd04c192 | |||
c091d40fb0 | |||
b7baf632c0 | |||
4c0d4fc649 | |||
5b3c08b237 | |||
68f2e0c391 | |||
9c1c945489 | |||
ef5338663d | |||
380b3d7653 | |||
4decc8521d | |||
4d544bcb46 | |||
4c819f79b2 | |||
ac3252a73b | |||
a08af77b70 | |||
aac08e0438 | |||
63b795ae4a | |||
5f6900ecc0 | |||
325e8010e9 | |||
632b19d5c2 | |||
add1198b88 | |||
0ed2df2a36 | |||
bc1f2d6411 | |||
d7326d81ba | |||
c683f74225 | |||
b286abeabe | |||
eeebe28c0f | |||
ffc6e199bf | |||
a01acec7fe | |||
021f4344b1 | |||
f113b49909 | |||
d8d276c245 | |||
e42bd012f9 | |||
6d6b0ff1ad | |||
f378454c92 | |||
c8c8436e58 | |||
b31c8b6063 | |||
897261efdc | |||
35d70ff265 | |||
fc0a7959a4 | |||
182c08bee1 | |||
e2bc0ad6c2 | |||
73333ee3e5 | |||
c819859598 | |||
79aefa7659 | |||
e1990a5a80 | |||
4cff5b2964 | |||
459758231b | |||
f29b218060 | |||
39a67548ac | |||
bc88f318f6 | |||
a5b7008c8e | |||
0aafbac99b | |||
ac5aa8f46d | |||
1fb3c4ffee | |||
3c8aa0b301 | |||
15a2b8f622 | |||
d19108531c | |||
354d1944bb | |||
eaccd03ed7 | |||
343df337f4 | |||
9b14483824 | |||
bd42caf1c7 | |||
7db8111973 | |||
74fef157e6 | |||
9661bed3ba | |||
95168e4de0 | |||
13c3e241c8 | |||
8d098d389a | |||
79a2567aa3 | |||
5649acd03f | |||
ebd01e8e79 | |||
e08955b557 | |||
04dfca41f4 | |||
129f69c3bc | |||
c7e2930f25 | |||
6a62ed2245 | |||
482e12c940 | |||
0c344715e5 | |||
cf095d982d | |||
23ec88ef23 | |||
2bd767c4a6 | |||
1e02cd9961 | |||
bc02e19831 | |||
47eb2122c0 | |||
b8422b41bb | |||
8ac4dd6447 | |||
206ae7a233 | |||
79b6256789 | |||
72dce34f42 | |||
7d39bc68fb | |||
32ad2438ca | |||
fc4b993d98 | |||
ff028f0b39 | |||
fef9cebed0 | |||
c08549ae38 | |||
3808416479 | |||
cf8ad24dcf | |||
cee7448efc | |||
7f1cace2a2 | |||
56c86c7e79 | |||
82a14dc107 | |||
a880686081 | |||
026b60cd70 | |||
cea2e0477c | |||
96f9f03d25 | |||
9931bd7576 | |||
48094835bf | |||
e42c1b0da8 | |||
e7ade38731 | |||
d5f47d6b71 | |||
64aa6701f6 | |||
12ccf57340 | |||
c634176035 | |||
4bb10d224c | |||
5d689469f6 | |||
855ad8804e | |||
29d3f3f6dd | |||
33101359c6 | |||
44eef5c343 | |||
68b7847b4c | |||
2b2e841e5b | |||
549de1e21a | |||
48e73c1558 | |||
41ac58ab7d | |||
f37cf52b4c | |||
927323f24e | |||
b94436d86c | |||
bc5cb8153e | |||
34b848ad51 | |||
d7e5bbf2d0 | |||
a9a81f91cf | |||
07c10e2844 | |||
df5999a739 | |||
3fb0da2de5 | |||
8f0fcc3f71 |
2
.bazelignore
Normal file
2
.bazelignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
@ -57,6 +57,6 @@ test --experimental_ui
|
||||
################################
|
||||
# Temporary Settings for Ivy #
|
||||
################################
|
||||
# to determine if the compiler used should be Ivy or ViewEngine one can use `--define=compile=local` on
|
||||
# to determine if the compiler used should be Ivy or ViewEngine one can use `--define=compile=local` on
|
||||
# any bazel target. This is a temporary flag until codebase is permanently switched to Ivy.
|
||||
build --define=compile=legacy
|
||||
build --define=compile=legacy
|
@ -12,8 +12,8 @@
|
||||
## IMPORTANT
|
||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
||||
var_1: &docker_image angular/ngcontainer:0.6.0
|
||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.6.0
|
||||
var_1: &docker_image angular/ngcontainer:0.7.0
|
||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.7.0
|
||||
|
||||
# Define common ENV vars
|
||||
var_3: &define_env_vars
|
||||
@ -113,6 +113,7 @@ jobs:
|
||||
paths:
|
||||
- "node_modules"
|
||||
- "~/bazel_repository_cache"
|
||||
|
||||
# Temporary job to test what will happen when we flip the Ivy flag to true
|
||||
test_ivy_jit:
|
||||
<<: *job_defaults
|
||||
@ -140,6 +141,7 @@ jobs:
|
||||
- *setup_bazel_remote_execution
|
||||
- run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local
|
||||
|
||||
# This job should only be run on PR builds, where `CIRCLE_PR_NUMBER` is defined.
|
||||
aio_preview:
|
||||
<<: *job_defaults
|
||||
environment:
|
||||
@ -150,13 +152,28 @@ jobs:
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- run: yarn install --frozen-lockfile --non-interactive
|
||||
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH
|
||||
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH $CIRCLE_PR_NUMBER $CIRCLE_SHA1
|
||||
- store_artifacts:
|
||||
path: *aio_preview_artifact_path
|
||||
# The `destination` needs to be kept in synch with the value of
|
||||
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
|
||||
destination: aio/dist/aio-snapshot.tgz
|
||||
|
||||
# This job should only be run on PR builds, where `CIRCLE_PR_NUMBER` is defined.
|
||||
test_aio_preview:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- run: yarn install --cwd aio --frozen-lockfile --non-interactive
|
||||
- run:
|
||||
name: Wait for preview and run tests
|
||||
command: |
|
||||
source "./scripts/ci/env.sh" print
|
||||
xvfb-run --auto-servernum node aio/scripts/test-preview.js $CIRCLE_PR_NUMBER $CIRCLE_SHA1 $AIO_MIN_PWA_SCORE
|
||||
|
||||
# This job exists only for backwards-compatibility with old scripts and tests
|
||||
# that rely on the pre-Bazel dist/packages-dist layout.
|
||||
# It duplicates some work with the job above: we build the bazel packages
|
||||
@ -251,7 +268,14 @@ workflows:
|
||||
- test_ivy_jit
|
||||
- test_ivy_aot
|
||||
- build-packages-dist
|
||||
- aio_preview
|
||||
- aio_preview:
|
||||
# Only run on PR builds. (There can be no previews for non-PR builds.)
|
||||
filters:
|
||||
branches:
|
||||
only: /pull\/\d+/
|
||||
- test_aio_preview:
|
||||
requires:
|
||||
- aio_preview
|
||||
- integration_test:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
@ -280,6 +304,7 @@ workflows:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
notify:
|
||||
webhooks:
|
||||
- url: https://ngbuilds.io/circle-build
|
||||
|
9
.github/angular-robot.yml
vendored
9
.github/angular-robot.yml
vendored
@ -3,11 +3,8 @@
|
||||
#options for the size plugin
|
||||
size:
|
||||
disabled: false
|
||||
maxSizeIncrease: 1000
|
||||
circleCiStatusName: "ci/circleci: build-packages-dist"
|
||||
status:
|
||||
disabled: false
|
||||
context: "ci/angular: size"
|
||||
maxSizeIncrease: 2000
|
||||
circleCiStatusName: "ci/circleci: test"
|
||||
|
||||
# options for the merge plugin
|
||||
merge:
|
||||
@ -68,7 +65,7 @@ merge:
|
||||
# This enables us to request reviews from both eng and tech writers, or multiple eng folks, and prevents accidental merges.
|
||||
# Rather than merging PRs with pending reviews, if all PullApprove requirements are satisfied and additional reviews are not needed pending reviewers should be removed via GitHub UI (this also leaves an audit trail behind these decisions).
|
||||
requireReviews: true,
|
||||
|
||||
|
||||
# whether the PR shouldn't have a conflict with the base branch
|
||||
noConflict: true
|
||||
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,7 +14,6 @@ pubspec.lock
|
||||
.settings/
|
||||
*.swo
|
||||
modules/.settings
|
||||
.bazelrc
|
||||
.vscode
|
||||
modules/.vscode
|
||||
|
||||
|
@ -87,10 +87,10 @@ groups:
|
||||
files:
|
||||
include:
|
||||
- "WORKSPACE"
|
||||
- ".bazel*"
|
||||
- "*.bazel"
|
||||
- "*.bzl"
|
||||
- "packages/bazel/*"
|
||||
- "tools/bazel.rc"
|
||||
- "/docs/BAZEL.md"
|
||||
users:
|
||||
- alexeagle #primary
|
||||
@ -108,7 +108,6 @@ groups:
|
||||
- "*.lock"
|
||||
- "tools/*"
|
||||
exclude:
|
||||
- "tools/bazel.rc"
|
||||
- "tools/public_api_guard/*"
|
||||
- "aio/*"
|
||||
users:
|
||||
|
42
BUILD.bazel
42
BUILD.bazel
@ -15,19 +15,14 @@ alias(
|
||||
actual = "@nodejs//:yarn",
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "node_modules",
|
||||
actual = "@angular_deps//:node_modules",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "web_test_bootstrap_scripts",
|
||||
# do not sort
|
||||
srcs = [
|
||||
"@angular_deps//:node_modules/reflect-metadata/Reflect.js",
|
||||
"@angular_deps//:node_modules/zone.js/dist/zone.js",
|
||||
"@angular_deps//:node_modules/zone.js/dist/zone-testing.js",
|
||||
"@angular_deps//:node_modules/zone.js/dist/task-tracking.js",
|
||||
"@ngdeps//node_modules/reflect-metadata:Reflect.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/zone.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/zone-testing.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/task-tracking.js",
|
||||
"//:test-events.js",
|
||||
],
|
||||
)
|
||||
@ -35,11 +30,28 @@ filegroup(
|
||||
filegroup(
|
||||
name = "angularjs_scripts",
|
||||
srcs = [
|
||||
"@angular_deps//:node_modules/angular-1.5/angular.js",
|
||||
"@angular_deps//:node_modules/angular-1.6/angular.js",
|
||||
"@angular_deps//:node_modules/angular-mocks-1.5/angular-mocks.js",
|
||||
"@angular_deps//:node_modules/angular-mocks-1.6/angular-mocks.js",
|
||||
"@angular_deps//:node_modules/angular-mocks/angular-mocks.js",
|
||||
"@angular_deps//:node_modules/angular/angular.js",
|
||||
"@ngdeps//node_modules/angular:angular.js",
|
||||
"@ngdeps//node_modules/angular-1.5:angular.js",
|
||||
"@ngdeps//node_modules/angular-1.6:angular.js",
|
||||
"@ngdeps//node_modules/angular-mocks:angular-mocks.js",
|
||||
"@ngdeps//node_modules/angular-mocks-1.5:angular-mocks.js",
|
||||
"@ngdeps//node_modules/angular-mocks-1.6:angular-mocks.js",
|
||||
],
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
|
||||
|
||||
# A nodejs_binary for @angular/bazel/ngc-wrapped to use by default in
|
||||
# ng_module that depends on @npm//@angular/bazel instead of the
|
||||
# output of the //packages/bazel/src/ngc-wrapped ts_library rule. This
|
||||
# default is for downstream users that depend on the @angular/bazel npm
|
||||
# package. The generated @npm//@angular/bazel/ngc-wrapped target
|
||||
# does not work because it does not have the node `--expose-gc` flag
|
||||
# set which is required to support the call to `global.gc()`.
|
||||
nodejs_binary(
|
||||
name = "@angular/bazel/ngc-wrapped",
|
||||
data = ["@npm//@angular/bazel"],
|
||||
entry_point = "@angular/bazel/src/ngc-wrapped/index.js",
|
||||
install_source_map_support = False,
|
||||
templated_args = ["--node_options=--expose-gc"],
|
||||
)
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,3 +1,40 @@
|
||||
<a name="7.0.0-rc.0"></a>
|
||||
# [7.0.0-rc.0](https://github.com/angular/angular/compare/7.0.0-beta.7...7.0.0-rc.0) (2018-09-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for TypeScript 3.1 ([#26151](https://github.com/angular/angular/issues/26151)) ([9993c72](https://github.com/angular/angular/commit/9993c72))
|
||||
|
||||
|
||||
|
||||
<a name="7.0.0-beta.7"></a>
|
||||
# [7.0.0-beta.7](https://github.com/angular/angular/compare/7.0.0-beta.6...7.0.0-beta.7) (2018-09-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** add missing `peerDependency ` to `[@angular](https://github.com/angular)/compiler` ([#26033](https://github.com/angular/angular/issues/26033)) ([549de1e](https://github.com/angular/angular/commit/549de1e)), closes [/github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e#diff-58563046c4439699f2e6a89187099a54](https://github.com//github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e/issues/diff-58563046c4439699f2e6a89187099a54)
|
||||
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([2bd767c](https://github.com/angular/angular/commit/2bd767c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler-cli:** add support to extend `angularCompilerOptions` ([#22717](https://github.com/angular/angular/issues/22717)) ([d7e5bbf](https://github.com/angular/angular/commit/d7e5bbf)), closes [#22684](https://github.com/angular/angular/issues/22684)
|
||||
* **platform-server:** update domino to v2.1.0 ([#25564](https://github.com/angular/angular/issues/25564)) ([3fb0da2](https://github.com/angular/angular/commit/3fb0da2))
|
||||
|
||||
|
||||
|
||||
<a name="6.1.9"></a>
|
||||
## [6.1.9](https://github.com/angular/angular/compare/6.1.8...6.1.9) (2018-09-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([a169743](https://github.com/angular/angular/commit/a169743))
|
||||
|
||||
|
||||
|
||||
<a name="7.0.0-beta.6"></a>
|
||||
# [7.0.0-beta.6](https://github.com/angular/angular/compare/7.0.0-beta.5...7.0.0-beta.6) (2018-09-19)
|
||||
|
||||
|
107
WORKSPACE
107
WORKSPACE
@ -1,89 +1,24 @@
|
||||
workspace(name = "angular")
|
||||
|
||||
#
|
||||
# Download Bazel toolchain dependencies as needed by build actions
|
||||
#
|
||||
http_archive(
|
||||
name = "build_bazel_rules_typescript",
|
||||
url = "https://github.com/bazelbuild/rules_typescript/archive/0.17.0.zip",
|
||||
strip_prefix = "rules_typescript-0.17.0",
|
||||
sha256 = "1626ee2cc9770af6950bfc77dffa027f9aedf330fe2ea2ee7e504428927bd95d",
|
||||
)
|
||||
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
|
||||
rules_typescript_dependencies()
|
||||
|
||||
http_archive(
|
||||
name = "bazel_toolchains",
|
||||
urls = [
|
||||
"https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
|
||||
"https://github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz",
|
||||
],
|
||||
strip_prefix = "bazel-toolchains-5124557861ebf4c0b67f98180bff1f8551e0b421",
|
||||
sha256 = "c3b08805602cd1d2b67ebe96407c1e8c6ed3d4ce55236ae2efe2f1948f38168d",
|
||||
load(
|
||||
"//packages/bazel:package.bzl",
|
||||
"rules_angular_dependencies",
|
||||
"rules_angular_dev_dependencies",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
url = "https://github.com/bazelbuild/rules_sass/archive/1.11.0.zip",
|
||||
strip_prefix = "rules_sass-1.11.0",
|
||||
sha256 = "dbe9fb97d5a7833b2a733eebc78c9c1e3880f676ac8af16e58ccf2139cbcad03",
|
||||
)
|
||||
|
||||
# This commit matches the version of buildifier in angular/ngcontainer
|
||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
||||
# version in /.circleci/config.yml
|
||||
BAZEL_BUILDTOOLS_VERSION = "49a6c199e3fbf5d94534b2771868677d3f9c6de9"
|
||||
|
||||
http_archive(
|
||||
name = "com_github_bazelbuild_buildtools",
|
||||
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
||||
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
||||
sha256 = "edf39af5fc257521e4af4c40829fffe8fba6d0ebff9f4dd69a6f8f1223ae047b",
|
||||
)
|
||||
|
||||
# Fetching the Bazel source code allows us to compile the Skylark linter
|
||||
http_archive(
|
||||
name = "io_bazel",
|
||||
url = "https://github.com/bazelbuild/bazel/archive/0.17.1.zip",
|
||||
strip_prefix = "bazel-0.17.1",
|
||||
sha256 = "ace8cced3b21e64a8fdad68508e9b0644201ec848ad583651719841d567fc66d",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_skydoc",
|
||||
# TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged
|
||||
url = "https://github.com/alexeagle/skydoc/archive/fe2e9f888d28e567fef62ec9d4a93c425526d701.zip",
|
||||
strip_prefix = "skydoc-fe2e9f888d28e567fef62ec9d4a93c425526d701",
|
||||
sha256 = "7bfb5545f59792a2745f2523b9eef363f9c3e7274791c030885e7069f8116016",
|
||||
)
|
||||
|
||||
# We have a source dependency on the Devkit repository, because it's built with
|
||||
# Bazel.
|
||||
# This allows us to edit sources and have the effect appear immediately without
|
||||
# re-packaging or "npm link"ing.
|
||||
# Even better, things like aspects will visit the entire graph including
|
||||
# ts_library rules in the devkit repository.
|
||||
http_archive(
|
||||
name = "angular_cli",
|
||||
url = "https://github.com/angular/angular-cli/archive/v6.1.0-rc.0.zip",
|
||||
strip_prefix = "angular-cli-6.1.0-rc.0",
|
||||
sha256 = "8cf320ea58c321e103f39087376feea502f20eaf79c61a4fdb05c7286c8684fd",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "org_brotli",
|
||||
url = "https://github.com/google/brotli/archive/v1.0.5.zip",
|
||||
strip_prefix = "brotli-1.0.5",
|
||||
sha256 = "774b893a0700b0692a76e2e5b7e7610dbbe330ffbe3fe864b4b52ca718061d5a",
|
||||
)
|
||||
# Angular Bazel users will call this function
|
||||
rules_angular_dependencies()
|
||||
# These are the dependencies only for us
|
||||
rules_angular_dev_dependencies()
|
||||
|
||||
#
|
||||
# Point Bazel to WORKSPACEs that live in subdirectories
|
||||
#
|
||||
|
||||
local_repository(
|
||||
http_archive(
|
||||
name = "rxjs",
|
||||
path = "node_modules/rxjs/src",
|
||||
url = "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz",
|
||||
strip_prefix = "package/src",
|
||||
sha256 = "72b0b4e517f43358f554c125e40e39f67688cd2738a8998b4a266981ed32f403",
|
||||
)
|
||||
|
||||
# Point to the integration test workspace just so that Bazel doesn't descend into it
|
||||
@ -96,29 +31,37 @@ local_repository(
|
||||
#
|
||||
# Load and install our dependencies downloaded above.
|
||||
#
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||
|
||||
check_bazel_version("0.17.0", """
|
||||
check_bazel_version("0.18.0", """
|
||||
If you are on a Mac and using Homebrew, there is a breaking change to the installation in Bazel 0.16
|
||||
See https://blog.bazel.build/2018/08/22/bazel-homebrew.html
|
||||
|
||||
""")
|
||||
|
||||
node_repositories(
|
||||
node_version = "10.9.0",
|
||||
package_json = ["//:package.json"],
|
||||
preserve_symlinks = True,
|
||||
node_version = "10.9.0",
|
||||
yarn_version = "1.9.2",
|
||||
)
|
||||
|
||||
yarn_install(
|
||||
name = "npm",
|
||||
package_json = "//tools:npm/package.json",
|
||||
yarn_lock = "//tools:npm/yarn.lock",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
||||
|
||||
go_rules_dependencies()
|
||||
|
||||
go_register_toolchains()
|
||||
|
||||
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
|
||||
|
||||
web_test_repositories()
|
||||
|
||||
browser_repositories(
|
||||
chromium = True,
|
||||
firefox = True,
|
||||
@ -136,7 +79,9 @@ ng_setup_workspace()
|
||||
# Skylark documentation generation
|
||||
|
||||
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
|
||||
|
||||
sass_repositories()
|
||||
|
||||
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
|
||||
|
||||
skydoc_repositories()
|
||||
|
@ -1,2 +1,2 @@
|
||||
# Periodically clean up builds that do not correspond to currently open PRs
|
||||
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||
0 12 * * * /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||
|
@ -66,6 +66,21 @@ server {
|
||||
return 200 '';
|
||||
}
|
||||
|
||||
# Check PRs previewability
|
||||
location "~^/can-have-public-preview/\d+/?$" {
|
||||
if ($request_method != "GET") {
|
||||
add_header Allow "GET";
|
||||
return 405;
|
||||
}
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_redirect off;
|
||||
proxy_method GET;
|
||||
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Notify about CircleCI builds
|
||||
location "~^/circle-build/?$" {
|
||||
if ($request_method != "POST") {
|
||||
|
@ -5,12 +5,12 @@ import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {GithubApi} from '../common/github-api';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty, createLogger, getPrInfoFromDownloadPath} from '../common/utils';
|
||||
import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
|
||||
|
||||
// Classes
|
||||
export class BuildCleaner {
|
||||
|
||||
private logger = createLogger('BuildCleaner');
|
||||
private logger = new Logger('BuildCleaner');
|
||||
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
|
||||
@ -122,6 +122,6 @@ export class BuildCleaner {
|
||||
this.logger.log(`Existing downloads: ${existingDownloads.length}`);
|
||||
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
|
||||
|
||||
toRemove.forEach(filePath => shell.rm(filePath));
|
||||
toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath)));
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export class CircleCiApi {
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
return response.json<BuildInfo>();
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`CircleCI build info request failed (${error.message})`);
|
||||
}
|
||||
@ -77,7 +77,7 @@ export class CircleCiApi {
|
||||
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
||||
const artifacts = await response.json<ArtifactResponse>();
|
||||
const artifacts = await response.json() as ArtifactResponse;
|
||||
const artifact = artifacts.find(item => item.path === artifactPath);
|
||||
if (!artifact) {
|
||||
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
||||
|
@ -38,7 +38,8 @@ export class GithubApi {
|
||||
return this.request<T>('post', path, data);
|
||||
}
|
||||
|
||||
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
||||
// In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
|
||||
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
|
||||
const perPage = 100;
|
||||
const params = {
|
||||
...baseParams,
|
||||
|
@ -74,6 +74,6 @@ export class GithubPullRequests {
|
||||
*/
|
||||
public fetchFiles(pr: number): Promise<FileInfo[]> {
|
||||
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||
return this.api.get<FileInfo[]>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
||||
return this.api.getPaginated<FileInfo>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
export const runTests = (specFiles: string[], helpers?: string[]) => {
|
||||
// We can't use `import` here, because of the following mess:
|
||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
// tslint:disable-next-line: no-var-requires variable-name
|
||||
const Jasmine = require('jasmine');
|
||||
// We can't use `import...from` here, because of the following mess:
|
||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
import Jasmine = require('jasmine');
|
||||
import 'source-map-support/register';
|
||||
|
||||
export const runTests = (specFiles: string[]) => {
|
||||
const config = {
|
||||
helpers,
|
||||
random: true,
|
||||
spec_files: specFiles,
|
||||
stopSpecOnExpectationFailure: true,
|
||||
@ -16,7 +16,7 @@ export const runTests = (specFiles: string[], helpers?: string[]) => {
|
||||
|
||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||
|
||||
const runner = new Jasmine();
|
||||
const runner = new Jasmine({});
|
||||
runner.loadConfig(config);
|
||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||
runner.execute();
|
||||
|
@ -74,12 +74,25 @@ export const getEnvVar = (name: string, isOptional = false): string => {
|
||||
return value || '';
|
||||
};
|
||||
|
||||
export function createLogger(scope: string) {
|
||||
const padding = ' '.repeat(20 - scope.length);
|
||||
return {
|
||||
error: (...args: any[]) => console.error(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
info: (...args: any[]) => console.info(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
log: (...args: any[]) => console.log(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
warn: (...args: any[]) => console.warn(`[${new Date()}]`, `${scope}:${padding}`, ...args),
|
||||
};
|
||||
/**
|
||||
* A basic logger implementation.
|
||||
* Delegates to `console`, but prepends each message with the current date and specified scope (i.e caller).
|
||||
*/
|
||||
export class Logger {
|
||||
private padding = ' '.repeat(20 - this.scope.length);
|
||||
|
||||
/**
|
||||
* Create a new `Logger` instance for the specified `scope`.
|
||||
* @param scope The logger's scope (added to all messages).
|
||||
*/
|
||||
constructor(private scope: string) {}
|
||||
|
||||
public error(...args: any[]) { this.callMethod('error', args); }
|
||||
public info(...args: any[]) { this.callMethod('info', args); }
|
||||
public log(...args: any[]) { this.callMethod('log', args); }
|
||||
public warn(...args: any[]) { this.callMethod('warn', args); }
|
||||
|
||||
private callMethod(method: 'error' | 'info' | 'log' | 'warn', args: any[]) {
|
||||
console[method](`[${new Date()}]`, `${this.scope}:${this.padding}`, ...args);
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,14 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {assertNotMissingOrEmpty, computeShortSha, createLogger} from '../common/utils';
|
||||
import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {PreviewServerError} from './preview-error';
|
||||
|
||||
// Classes
|
||||
export class BuildCreator extends EventEmitter {
|
||||
|
||||
private logger = createLogger('BuildCreator');
|
||||
private logger = new Logger('BuildCreator');
|
||||
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string) {
|
||||
|
@ -4,7 +4,7 @@ import {dirname} from 'path';
|
||||
import {mkdir} from 'shelljs';
|
||||
import {promisify} from 'util';
|
||||
import {CircleCiApi} from '../common/circle-ci-api';
|
||||
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, createLogger} from '../common/utils';
|
||||
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils';
|
||||
import {PreviewServerError} from './preview-error';
|
||||
|
||||
export interface GithubInfo {
|
||||
@ -19,7 +19,7 @@ export interface GithubInfo {
|
||||
* A helper that can get information about builds and download build artifacts.
|
||||
*/
|
||||
export class BuildRetriever {
|
||||
private logger = createLogger('BuildRetriever');
|
||||
private logger = new Logger('BuildRetriever');
|
||||
constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
|
||||
assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
|
||||
assertNotMissingOrEmpty('downloadDir', downloadDir);
|
||||
@ -34,7 +34,7 @@ export class BuildRetriever {
|
||||
const buildInfo = await this.api.getBuildInfo(buildNum);
|
||||
const githubInfo: GithubInfo = {
|
||||
org: buildInfo.username,
|
||||
pr: getPrfromBranch(buildInfo.branch),
|
||||
pr: getPrFromBranch(buildInfo.branch),
|
||||
repo: buildInfo.reponame,
|
||||
sha: buildInfo.vcs_revision,
|
||||
success: !buildInfo.failed,
|
||||
@ -73,7 +73,7 @@ export class BuildRetriever {
|
||||
}
|
||||
}
|
||||
|
||||
function getPrfromBranch(branch: string): number {
|
||||
function getPrFromBranch(branch: string): number {
|
||||
// CircleCI only exposes PR numbers via the `branch` field :-(
|
||||
const match = /^pull\/(\d+)$/.exec(branch);
|
||||
if (!match) {
|
||||
|
@ -2,11 +2,12 @@
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {AddressInfo} from 'net';
|
||||
import {CircleCiApi} from '../common/circle-ci-api';
|
||||
import {GithubApi} from '../common/github-api';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {GithubTeams} from '../common/github-teams';
|
||||
import {assert, assertNotMissingOrEmpty, createLogger} from '../common/utils';
|
||||
import {assert, assertNotMissingOrEmpty, Logger} from '../common/utils';
|
||||
import {BuildCreator} from './build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {BuildRetriever} from './build-retriever';
|
||||
@ -31,7 +32,7 @@ export interface PreviewServerConfig {
|
||||
trustedPrLabel: string;
|
||||
}
|
||||
|
||||
const logger = createLogger('PreviewServer');
|
||||
const logger = new Logger('PreviewServer');
|
||||
|
||||
// Classes
|
||||
export class PreviewServerFactory {
|
||||
@ -52,7 +53,7 @@ export class PreviewServerFactory {
|
||||
const httpServer = http.createServer(middleware as any);
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const info = httpServer.address();
|
||||
const info = httpServer.address() as AddressInfo;
|
||||
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
||||
});
|
||||
|
||||
@ -63,10 +64,36 @@ export class PreviewServerFactory {
|
||||
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
|
||||
const middleware = express();
|
||||
const jsonParser = bodyParser.json();
|
||||
const significantFilesRe = new RegExp(cfg.significantFilesPattern);
|
||||
|
||||
// RESPOND TO IS-ALIVE PING
|
||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||
|
||||
// RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
|
||||
const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/;
|
||||
middleware.get(canHavePublicPreviewRe, async (req, res) => {
|
||||
try {
|
||||
const pr = +canHavePublicPreviewRe.exec(req.url)![1];
|
||||
|
||||
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||
// Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
|
||||
res.send({canHavePublicPreview: false, reason: 'No significant files touched.'});
|
||||
logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`);
|
||||
} else if (!await buildVerifier.getPrIsTrusted(pr)) {
|
||||
// Cannot have preview: PR not automatically verifiable as "trusted".
|
||||
res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'});
|
||||
logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`);
|
||||
} else {
|
||||
// Can have preview.
|
||||
res.send({canHavePublicPreview: true, reason: null});
|
||||
logger.log(`PR:${pr} - Can have a public preview.`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Previewability check error', err);
|
||||
respondWithError(res, err);
|
||||
}
|
||||
});
|
||||
|
||||
// CIRCLE_CI BUILD COMPLETE WEBHOOK
|
||||
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
|
||||
try {
|
||||
@ -107,7 +134,7 @@ export class PreviewServerFactory {
|
||||
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
|
||||
|
||||
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
|
||||
if (!await buildVerifier.getSignificantFilesChanged(pr, new RegExp(cfg.significantFilesPattern))) {
|
||||
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||
res.sendStatus(204);
|
||||
logger.log(`PR:${pr}, Build:${buildNum} - ` +
|
||||
`Skipping preview processing because this PR did not touch any significant files.`);
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
AIO_NGINX_PORT_HTTPS,
|
||||
AIO_WWW_USER,
|
||||
} from '../common/env-variables';
|
||||
import {computeShortSha, createLogger} from '../common/utils';
|
||||
import {computeShortSha, Logger} from '../common/utils';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
||||
@ -31,7 +31,7 @@ class Helper {
|
||||
https: AIO_NGINX_PORT_HTTPS,
|
||||
};
|
||||
|
||||
private logger = createLogger('TestHelper');
|
||||
private logger = new Logger('TestHelper');
|
||||
|
||||
// Constructor
|
||||
constructor() {
|
||||
@ -105,7 +105,7 @@ class Helper {
|
||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||
}
|
||||
|
||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
||||
public verifyResponse(status: number | [number, string], regex: string | RegExp = /^/): VerifyCmdResultFn {
|
||||
let statusCode: number;
|
||||
let statusText: string;
|
||||
|
||||
@ -180,26 +180,42 @@ class Helper {
|
||||
}
|
||||
}
|
||||
|
||||
interface DefaultCurlOptions {
|
||||
defaultMethod?: CurlOptions['method'];
|
||||
defaultOptions?: CurlOptions['options'];
|
||||
defaultHeaders?: CurlOptions['headers'];
|
||||
defaultData?: CurlOptions['data'];
|
||||
defaultExtraPath?: CurlOptions['extraPath'];
|
||||
}
|
||||
|
||||
interface CurlOptions {
|
||||
method?: string;
|
||||
options?: string;
|
||||
headers?: string[];
|
||||
data?: any;
|
||||
url?: string;
|
||||
extraPath?: string;
|
||||
}
|
||||
|
||||
export function makeCurl(baseUrl: string) {
|
||||
export function makeCurl(baseUrl: string, {
|
||||
defaultMethod = 'POST',
|
||||
defaultOptions = '',
|
||||
defaultHeaders = ['Content-Type: application/json'],
|
||||
defaultData = {},
|
||||
defaultExtraPath = '',
|
||||
}: DefaultCurlOptions = {}) {
|
||||
return function curl({
|
||||
method = 'POST',
|
||||
options = '',
|
||||
data = {},
|
||||
method = defaultMethod,
|
||||
options = defaultOptions,
|
||||
headers = defaultHeaders,
|
||||
data = defaultData,
|
||||
url = baseUrl,
|
||||
extraPath = '',
|
||||
extraPath = defaultExtraPath,
|
||||
}: CurlOptions) {
|
||||
const dataString = data ? JSON.stringify(data) : '';
|
||||
const cmd = `curl -iLX ${method} ` +
|
||||
`${options} ` +
|
||||
`--header "Content-Type: application/json" ` +
|
||||
headers.map(header => `--header "${header}" `).join('') +
|
||||
`--data '${dataString}' ` +
|
||||
`${url}${extraPath}`;
|
||||
return helper.runCmd(cmd);
|
||||
|
@ -2,7 +2,7 @@
|
||||
import * as nock from 'nock';
|
||||
import * as tar from 'tar-stream';
|
||||
import {gzipSync} from 'zlib';
|
||||
import {createLogger, getEnvVar} from '../common/utils';
|
||||
import {getEnvVar, Logger} from '../common/utils';
|
||||
import {BuildNums, PrNums, SHA} from './constants';
|
||||
|
||||
// We are using the `nock` library to fake responses from REST requests, when testing.
|
||||
@ -14,7 +14,7 @@ import {BuildNums, PrNums, SHA} from './constants';
|
||||
// below and return a suitable response. This is quite complicated to setup since the
|
||||
// response from, say, CircleCI will affect what request is made to, say, Github.
|
||||
|
||||
const logger = createLogger('NOCK');
|
||||
const logger = new Logger('mock-external-apis');
|
||||
|
||||
const log = (...args: any[]) => {
|
||||
// Filter out non-matching URL checks
|
||||
@ -76,7 +76,7 @@ const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/p
|
||||
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
|
||||
|
||||
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
|
||||
const getFilesUrl = (prNum: number) => `${GITHUB_PULLS_URL}/${prNum}/files`;
|
||||
const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum}/files?page=${pageNum}&per_page=100`;
|
||||
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
|
||||
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
|
||||
|
||||
@ -97,7 +97,7 @@ const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authoriz
|
||||
//////////////////////////////
|
||||
|
||||
// GENERAL responses
|
||||
githubApi.get(GITHUB_TEAMS_URL + '?page=0&per_page=100').reply(200, TEST_TEAM_INFO);
|
||||
githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO);
|
||||
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
|
||||
|
||||
// BUILD_INFO errors
|
||||
|
@ -3,6 +3,7 @@ import * as path from 'path';
|
||||
import {rm} from 'shelljs';
|
||||
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
|
||||
import {computeShortSha} from '../common/utils';
|
||||
import {PrNums} from './constants';
|
||||
import {helper as h} from './helper';
|
||||
import {customMatchers} from './jasmine-custom-matchers';
|
||||
|
||||
@ -252,6 +253,42 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/can-have-public-preview`, () => {
|
||||
const baseUrl = `${scheme}://${host}/can-have-public-preview`;
|
||||
|
||||
|
||||
it('should disallow non-GET requests', async () => {
|
||||
await Promise.all([
|
||||
h.runCmd(`curl -iLX POST ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should pass requests through to the preview server', async () => {
|
||||
await h.runCmd(`curl -iLX GET ${baseUrl}/${PrNums.CHANGED_FILES_ERROR}`).
|
||||
then(h.verifyResponse(500, /CHANGED_FILES_ERROR/));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
const cmdPrefix = `curl -iLX GET ${baseUrl}`;
|
||||
|
||||
await Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/foo/42`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}-foo/42`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}nfoo/42`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/42/foo`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/f00`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/`).then(h.verifyResponse(404)),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/circle-build`, () => {
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
@ -287,6 +324,7 @@ describe(`nginx`, () => {
|
||||
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -18,6 +18,92 @@ describe('preview-server', () => {
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
describe(`${host}/can-have-public-preview`, () => {
|
||||
const curl = makeCurl(`${host}/can-have-public-preview`, {
|
||||
defaultData: null,
|
||||
defaultExtraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
|
||||
defaultHeaders: [],
|
||||
defaultMethod: 'GET',
|
||||
});
|
||||
|
||||
|
||||
it('should disallow non-GET requests', async () => {
|
||||
const bodyRegex = /^Unknown resource in request/;
|
||||
|
||||
await Promise.all([
|
||||
curl({method: 'POST'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths', async () => {
|
||||
const bodyRegex = /^Unknown resource in request/;
|
||||
|
||||
await Promise.all([
|
||||
curl({extraPath: `/foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: `-foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: `nfoo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}/foo`}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: '/f00'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
curl({extraPath: '/'}).then(h.verifyResponse(404, bodyRegex)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 500 if checking for significant file changes fails', async () => {
|
||||
await Promise.all([
|
||||
curl({extraPath: `/${PrNums.CHANGED_FILES_404}`}).then(h.verifyResponse(500, /CHANGED_FILES_404/)),
|
||||
curl({extraPath: `/${PrNums.CHANGED_FILES_ERROR}`}).then(h.verifyResponse(500, /CHANGED_FILES_ERROR/)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (false) if no significant files were touched', async () => {
|
||||
const expectedResponse = JSON.stringify({
|
||||
canHavePublicPreview: false,
|
||||
reason: 'No significant files touched.',
|
||||
});
|
||||
|
||||
await curl({extraPath: `/${PrNums.CHANGED_FILES_NONE}`}).then(h.verifyResponse(200, expectedResponse));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 500 if checking "trusted" status fails', async () => {
|
||||
await curl({extraPath: `/${PrNums.TRUST_CHECK_ERROR}`}).then(h.verifyResponse(500, 'TRUST_CHECK_ERROR'));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (false) if the PR is not automatically verifiable as "trusted"', async () => {
|
||||
const expectedResponse = JSON.stringify({
|
||||
canHavePublicPreview: false,
|
||||
reason: 'Not automatically verifiable as \\"trusted\\".',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_UNTRUSTED}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (true) if the PR can have a public preview', async () => {
|
||||
const expectedResponse = JSON.stringify({
|
||||
canHavePublicPreview: true,
|
||||
reason: null,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
curl({extraPath: `/${PrNums.TRUST_CHECK_TRUSTED_LABEL}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/circle-build`, () => {
|
||||
|
||||
const curl = makeCurl(`${host}/circle-build`);
|
||||
|
@ -7,43 +7,49 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn clean-dist",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn build --watch",
|
||||
"build": "yarn ~~build",
|
||||
"prebuild-watch": "yarn prebuild",
|
||||
"build-watch": "yarn ~~build-watch",
|
||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
||||
"predev": "yarn build || true",
|
||||
"dev": "run-p ~~build-watch ~~test-watch",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn build",
|
||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
||||
"pretest-watch": "yarn pretest",
|
||||
"test-watch": "yarn ~~test-watch",
|
||||
"~~build": "tsc",
|
||||
"~~build-watch": "yarn ~~build --watch",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"body-parser": "^1.18.3",
|
||||
"delete-empty": "^2.0.0",
|
||||
"express": "^4.15.4",
|
||||
"jasmine": "^2.8.0",
|
||||
"nock": "^9.2.5",
|
||||
"node-fetch": "^2.1.2",
|
||||
"shelljs": "^0.8.1",
|
||||
"tar-stream": "^1.6.0",
|
||||
"tslib": "^1.7.1"
|
||||
"express": "^4.16.3",
|
||||
"jasmine": "^3.2.0",
|
||||
"nock": "^9.6.1",
|
||||
"node-fetch": "^2.2.0",
|
||||
"shelljs": "^0.8.2",
|
||||
"source-map-support": "^0.5.9",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.16.5",
|
||||
"@types/express": "^4.0.37",
|
||||
"@types/jasmine": "^2.6.0",
|
||||
"@types/nock": "^9.1.3",
|
||||
"@types/node": "^8.0.30",
|
||||
"@types/node-fetch": "^1.6.8",
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/jasmine": "^2.8.8",
|
||||
"@types/nock": "^9.3.0",
|
||||
"@types/node": "^10.9.2",
|
||||
"@types/node-fetch": "^2.1.2",
|
||||
"@types/shelljs": "^0.8.0",
|
||||
"@types/supertest": "^2.0.3",
|
||||
"concurrently": "^3.5.0",
|
||||
"nodemon": "^1.12.1",
|
||||
"supertest": "^3.0.0",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.8",
|
||||
"typescript": "^2.5.2"
|
||||
"@types/supertest": "^2.0.5",
|
||||
"nodemon": "^1.18.3",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"supertest": "^3.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||
"typescript": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,28 @@ import * as shell from 'shelljs';
|
||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
|
||||
const EXISTING_BUILDS = [10, 20, 30, 40];
|
||||
const EXISTING_DOWNLOADS = [
|
||||
'downloads/10-ABCDEF0-build.zip',
|
||||
'downloads/10-1234567-build.zip',
|
||||
'downloads/20-ABCDEF0-build.zip',
|
||||
'downloads/20-1234567-build.zip',
|
||||
'10-ABCDEF0-build.zip',
|
||||
'10-1234567-build.zip',
|
||||
'20-ABCDEF0-build.zip',
|
||||
'20-1234567-build.zip',
|
||||
];
|
||||
const OPEN_PRS = [10, 40];
|
||||
const ANY_DATE = jasmine.any(String);
|
||||
|
||||
// Tests
|
||||
describe('BuildCleaner', () => {
|
||||
let loggerErrorSpy: jasmine.Spy;
|
||||
let loggerLogSpy: jasmine.Spy;
|
||||
let cleaner: BuildCleaner;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'error');
|
||||
spyOn(console, 'log');
|
||||
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', 'build.zip');
|
||||
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip');
|
||||
});
|
||||
|
||||
describe('constructor()', () => {
|
||||
@ -51,11 +54,13 @@ describe('BuildCleaner', () => {
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'downloadsDir\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
|
||||
toThrowError('Missing or empty required parameter \'downloadsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'artifactPath\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
|
||||
toThrowError('Missing or empty required parameter \'artifactPath\'!');
|
||||
@ -85,9 +90,12 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
it('should return a promise', async () => {
|
||||
const promise = cleaner.cleanUp();
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
|
||||
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
|
||||
await promise;
|
||||
});
|
||||
|
||||
|
||||
@ -160,6 +168,7 @@ describe('BuildCleaner', () => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
||||
try {
|
||||
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
@ -168,6 +177,7 @@ describe('BuildCleaner', () => {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -277,11 +287,14 @@ describe('BuildCleaner', () => {
|
||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of open PRs', () => {
|
||||
promise.then(prNumbers => {
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -301,9 +314,9 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should get the contents of the builds directory', () => {
|
||||
it('should get the contents of the downloads directory', () => {
|
||||
expect(fsReaddirSpy).toHaveBeenCalled();
|
||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('downloads');
|
||||
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads');
|
||||
});
|
||||
|
||||
|
||||
@ -317,7 +330,7 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the returned files (as numbers)', done => {
|
||||
it('should resolve with the returned file names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual(EXISTING_DOWNLOADS);
|
||||
done();
|
||||
@ -383,8 +396,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
jasmine.any(String), 'BuildCleaner: ', 'ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -401,8 +413,8 @@ describe('BuildCleaner', () => {
|
||||
it('should log the number of existing builds and builds to be removed', () => {
|
||||
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing builds: 3');
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Removing 2 build(s): 1, 2');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||
});
|
||||
|
||||
|
||||
@ -454,25 +466,36 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
describe('removeUnnecessaryDownloads()', () => {
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(shell, 'rm');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing downloads and downloads to be removed', () => {
|
||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Existing downloads: 4');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 download(s): 20-ABCDEF0-build.zip, 20-1234567-build.zip');
|
||||
});
|
||||
|
||||
|
||||
it('should construct full paths to directories (by prepending \'downloadsDir\')', () => {
|
||||
cleaner.removeUnnecessaryDownloads(['dl-1', 'dl-2', 'dl-3'], []);
|
||||
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-1'));
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-2'));
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-3'));
|
||||
});
|
||||
|
||||
|
||||
it('should remove the downloads that do not correspond to open PRs', () => {
|
||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||
expect(shell.rm).toHaveBeenCalledTimes(2);
|
||||
expect(shell.rm).toHaveBeenCalledWith('downloads/20-ABCDEF0-build.zip');
|
||||
expect(shell.rm).toHaveBeenCalledWith('downloads/20-1234567-build.zip');
|
||||
expect(shellRmSpy).toHaveBeenCalledTimes(2);
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip'));
|
||||
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip'));
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing builds and builds to be removed', () => {
|
||||
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ', 'Existing downloads: 4');
|
||||
expect(console.log).toHaveBeenCalledWith(ANY_DATE, 'BuildCleaner: ',
|
||||
'Removing 2 download(s): downloads/20-ABCDEF0-build.zip, downloads/20-1234567-build.zip');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -126,8 +126,8 @@ describe('GithubApi', () => {
|
||||
(api as any).getPaginated('/foo/bar');
|
||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
|
||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
|
||||
});
|
||||
|
||||
|
||||
@ -162,9 +162,9 @@ describe('GithubApi', () => {
|
||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||
|
||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||
|
||||
expect(data).toEqual(allItems);
|
||||
|
||||
|
@ -4,13 +4,13 @@ import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('GithubPullRequests', () => {
|
||||
|
||||
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||
});
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||
@ -95,16 +95,14 @@ describe('GithubPullRequests', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
@ -131,8 +129,10 @@ describe('GithubPullRequests', () => {
|
||||
githubApi.getPaginated.and.returnValue('Test');
|
||||
expect(prs.fetchAll() as any).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fetchFiles()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
|
||||
@ -141,21 +141,21 @@ describe('GithubPullRequests', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should make a GET request to GitHub with the correct pathname', () => {
|
||||
it('should make a paginated GET request to GitHub with the correct pathname', () => {
|
||||
prs.fetchFiles(42);
|
||||
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
||||
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the data returned from GitHub', done => {
|
||||
const expected: any = [{ sha: 'ABCDE', filename: 'a/b/c'}, { sha: '12345', filename: 'x/y/z' }];
|
||||
githubApi.get.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetch(42).then(data => {
|
||||
const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
|
||||
githubApi.getPaginated.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetchFiles(42).then(data => {
|
||||
expect(data).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Imports
|
||||
import {resolve as resolvePath} from 'path';
|
||||
import {
|
||||
assert,
|
||||
assertNotMissingOrEmpty,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
computeShortSha,
|
||||
getEnvVar,
|
||||
getPrInfoFromDownloadPath,
|
||||
Logger,
|
||||
} from '../../lib/common/utils';
|
||||
|
||||
// Tests
|
||||
@ -19,6 +21,7 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('assert', () => {
|
||||
it('should throw if passed a false value', () => {
|
||||
expect(() => assert(false, 'error message')).toThrowError('error message');
|
||||
@ -29,6 +32,7 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('computeArtifactDownloadPath', () => {
|
||||
it('should compute an absolute path based on the artifact info provided', () => {
|
||||
const downloadDir = '/a/b/c';
|
||||
@ -36,10 +40,11 @@ describe('utils', () => {
|
||||
const sha = 'ABCDEF1234567';
|
||||
const artifactPath = 'a/path/to/file.zip';
|
||||
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
|
||||
expect(path).toEqual('/a/b/c/123-ABCDEF1-file.zip');
|
||||
expect(path).toBe(resolvePath('/a/b/c/123-ABCDEF1-file.zip'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('getPrInfoFromDownloadPath', () => {
|
||||
it('should extract the PR and SHA from the file path', () => {
|
||||
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
|
||||
@ -48,6 +53,7 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('assertNotMissingOrEmpty()', () => {
|
||||
|
||||
it('should throw if passed an empty value', () => {
|
||||
@ -122,4 +128,79 @@ describe('utils', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('Logger', () => {
|
||||
let consoleErrorSpy: jasmine.Spy;
|
||||
let consoleInfoSpy: jasmine.Spy;
|
||||
let consoleLogSpy: jasmine.Spy;
|
||||
let consoleWarnSpy: jasmine.Spy;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = spyOn(console, 'error');
|
||||
consoleInfoSpy = spyOn(console, 'info');
|
||||
consoleLogSpy = spyOn(console, 'log');
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
|
||||
logger = new Logger('TestScope');
|
||||
});
|
||||
|
||||
|
||||
it('should delegate to `console`', () => {
|
||||
logger.error('foo');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleErrorSpy.calls.argsFor(0)).toContain('foo');
|
||||
|
||||
logger.info('bar');
|
||||
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleInfoSpy.calls.argsFor(0)).toContain('bar');
|
||||
|
||||
logger.log('baz');
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy.calls.argsFor(0)).toContain('baz');
|
||||
|
||||
logger.warn('qux');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(consoleWarnSpy.calls.argsFor(0)).toContain('qux');
|
||||
});
|
||||
|
||||
|
||||
it('should prepend messages with the current date and logger\'s scope', () => {
|
||||
const mockDate = new Date(1337);
|
||||
const expectedDateStr = `[${mockDate}]`;
|
||||
const expectedScopeStr = 'TestScope: ';
|
||||
|
||||
jasmine.clock().mockDate(mockDate);
|
||||
jasmine.clock().withMock(() => {
|
||||
logger.error();
|
||||
logger.info();
|
||||
logger.log();
|
||||
logger.warn();
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||
});
|
||||
|
||||
|
||||
it('should pass all arguments to `console`', () => {
|
||||
const someString = jasmine.any(String);
|
||||
|
||||
logger.error('foo1', 'foo2');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(someString, someString, 'foo1', 'foo2');
|
||||
|
||||
logger.info('bar1', 'bar2');
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(someString, someString, 'bar1', 'bar2');
|
||||
|
||||
logger.log('baz1', 'baz2');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(someString, someString, 'baz1', 'baz2');
|
||||
|
||||
logger.warn('qux1', 'qux2');
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(someString, someString, 'qux1', 'qux2');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,6 +0,0 @@
|
||||
declare namespace jasmine {
|
||||
export interface DoneFn extends Function {
|
||||
(): void;
|
||||
fail: (message: Error | string) => void;
|
||||
}
|
||||
}
|
@ -3,5 +3,4 @@ import {runTests} from '../lib/common/run-tests';
|
||||
|
||||
// Run
|
||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||
const helpers = [`${__dirname}/helpers.js`];
|
||||
runTests(specFiles, helpers);
|
||||
runTests(specFiles);
|
||||
|
@ -5,6 +5,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||
@ -491,7 +492,7 @@ describe('BuildCreator', () => {
|
||||
beforeEach(() => {
|
||||
cpExecCbs = [];
|
||||
|
||||
consoleWarnSpy = spyOn(console, 'warn');
|
||||
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||
@ -513,8 +514,7 @@ describe('BuildCreator', () => {
|
||||
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(consoleWarnSpy)
|
||||
.toHaveBeenCalledWith(jasmine.any(String), 'BuildCreator: ', 'This is stderr')).
|
||||
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||
|
@ -1,11 +1,13 @@
|
||||
import * as fs from 'fs';
|
||||
import * as nock from 'nock';
|
||||
import {resolve as resolvePath} from 'path';
|
||||
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
|
||||
|
||||
describe('BuildRetriever', () => {
|
||||
const MAX_DOWNLOAD_SIZE = 10000;
|
||||
const DOWNLOAD_DIR = '/DOWNLOAD/DIR';
|
||||
const DOWNLOAD_DIR = resolvePath('/DOWNLOAD/DIR');
|
||||
const BASE_URL = 'http://test.com';
|
||||
const ARTIFACT_PATH = '/some/path/build.zip';
|
||||
|
||||
@ -29,10 +31,6 @@ describe('BuildRetriever', () => {
|
||||
vcs_revision: 'COMMIT',
|
||||
};
|
||||
|
||||
spyOn(console, 'log');
|
||||
spyOn(console, 'warn');
|
||||
spyOn(console, 'error');
|
||||
|
||||
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
|
||||
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
|
||||
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
|
||||
@ -91,6 +89,7 @@ describe('BuildRetriever', () => {
|
||||
let retriever: BuildRetriever;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Logger.prototype, 'warn');
|
||||
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||
});
|
||||
|
||||
@ -133,11 +132,14 @@ describe('BuildRetriever', () => {
|
||||
|
||||
it('should write the artifact file to disk', async () => {
|
||||
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||
const downloadPath = resolvePath(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`);
|
||||
|
||||
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||
expect(writeFileSpy)
|
||||
.toHaveBeenCalledWith(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`, jasmine.any(Buffer), jasmine.any(Function));
|
||||
expect(writeFileSpy).toHaveBeenCalledWith(downloadPath, jasmine.any(Buffer), jasmine.any(Function));
|
||||
|
||||
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
|
||||
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
|
||||
|
||||
artifactRequest.done();
|
||||
});
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as supertest from 'supertest';
|
||||
import {promisify} from 'util';
|
||||
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
|
||||
@ -38,15 +38,18 @@ describe('PreviewServerFactory', () => {
|
||||
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
|
||||
trustedPrLabel: 'trusted: pr-label',
|
||||
};
|
||||
let loggerErrorSpy: jasmine.Spy;
|
||||
let loggerInfoSpy: jasmine.Spy;
|
||||
let loggerLogSpy: jasmine.Spy;
|
||||
|
||||
// Helpers
|
||||
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
|
||||
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'error');
|
||||
spyOn(console, 'info');
|
||||
spyOn(console, 'log');
|
||||
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||
loggerInfoSpy = spyOn(Logger.prototype, 'info');
|
||||
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||
});
|
||||
|
||||
describe('create()', () => {
|
||||
@ -140,11 +143,10 @@ describe('PreviewServerFactory', () => {
|
||||
const server = createPreviewServer();
|
||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||
|
||||
expect(console.info).not.toHaveBeenCalled();
|
||||
expect(loggerInfoSpy).not.toHaveBeenCalled();
|
||||
|
||||
server.emit('listening');
|
||||
expect(console.info).toHaveBeenCalledWith(
|
||||
jasmine.any(String), 'PreviewServer: ', 'Up and running (and listening on foo:1337)...');
|
||||
expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
||||
});
|
||||
|
||||
});
|
||||
@ -241,10 +243,6 @@ describe('PreviewServerFactory', () => {
|
||||
let buildCreator: BuildCreator;
|
||||
let agent: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
// Helpers
|
||||
const promisifyRequest = async (req: supertest.Request) => await promisify(req.end.bind(req))();
|
||||
const verifyRequests = async (reqs: supertest.Request[]) => await Promise.all(reqs.map(promisifyRequest));
|
||||
|
||||
beforeEach(() => {
|
||||
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
|
||||
defaultConfig.circleCiToken);
|
||||
@ -257,14 +255,15 @@ describe('PreviewServerFactory', () => {
|
||||
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
||||
|
||||
const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator,
|
||||
defaultConfig);
|
||||
defaultConfig);
|
||||
agent = supertest.agent(middleware);
|
||||
});
|
||||
|
||||
|
||||
describe('GET /health-check', () => {
|
||||
|
||||
it('should respond with 200', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get('/health-check').expect(200),
|
||||
agent.get('/health-check/').expect(200),
|
||||
]);
|
||||
@ -272,7 +271,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 404 for non-GET requests', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.put('/health-check').expect(404),
|
||||
agent.post('/health-check').expect(404),
|
||||
agent.patch('/health-check').expect(404),
|
||||
@ -282,7 +281,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get('/health-check/foo').expect(404),
|
||||
agent.get('/health-check-foo').expect(404),
|
||||
agent.get('/health-checknfoo').expect(404),
|
||||
@ -294,7 +293,104 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('/circle-build', () => {
|
||||
|
||||
describe('GET /can-have-public-preview/<pr>', () => {
|
||||
const baseUrl = '/can-have-public-preview';
|
||||
const pr = 777;
|
||||
const url = `${baseUrl}/${pr}`;
|
||||
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').
|
||||
and.returnValue(Promise.resolve(true));
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for non-GET requests', async () => {
|
||||
await Promise.all([
|
||||
agent.put(url).expect(404),
|
||||
agent.post(url).expect(404),
|
||||
agent.patch(url).expect(404),
|
||||
agent.delete(url).expect(404),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the path does not match exactly', async () => {
|
||||
await Promise.all([
|
||||
agent.get('/can-have-public-preview/42/foo').expect(404),
|
||||
agent.get('/can-have-public-preview-foo/42').expect(404),
|
||||
agent.get('/can-have-public-previewnfoo/42').expect(404),
|
||||
agent.get('/foo/can-have-public-preview/42').expect(404),
|
||||
agent.get('/foo-can-have-public-preview/42').expect(404),
|
||||
agent.get('/fooncan-have-public-preview/42').expect(404),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should respond appropriately if the PR did not touch any significant files', async () => {
|
||||
bvGetSignificantFilesChangedSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
|
||||
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
|
||||
|
||||
await agent.get(url).expect(200, expectedResponse);
|
||||
|
||||
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||
});
|
||||
|
||||
|
||||
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValue(Promise.resolve(false));
|
||||
|
||||
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
|
||||
const expectedLog =
|
||||
`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`;
|
||||
|
||||
await agent.get(url).expect(200, expectedResponse);
|
||||
|
||||
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||
});
|
||||
|
||||
|
||||
it('should respond appropriately if the PR can have a preview', async () => {
|
||||
const expectedResponse = {canHavePublicPreview: true, reason: null};
|
||||
const expectedLog = `PR:${pr} - Can have a public preview.`;
|
||||
|
||||
await agent.get(url).expect(200, expectedResponse);
|
||||
|
||||
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
|
||||
bvGetSignificantFilesChangedSpy.and.callFake(() => Promise.reject('getSignificantFilesChanged error'));
|
||||
|
||||
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
|
||||
});
|
||||
|
||||
|
||||
it('should respond with error if `getPrIsTrusted()` fails', async () => {
|
||||
const error = new Error('getPrIsTrusted error');
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => { throw error; });
|
||||
|
||||
await agent.get(url).expect(500, 'getPrIsTrusted error');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', error);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('POST /circle-build', () => {
|
||||
let getGithubInfoSpy: jasmine.Spy;
|
||||
let getSignificantFilesChangedSpy: jasmine.Spy;
|
||||
let downloadBuildArtifactSpy: jasmine.Spy;
|
||||
@ -359,8 +455,8 @@ describe('PreviewServerFactory', () => {
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||
expect(getGithubInfoSpy).not.toHaveBeenCalled();
|
||||
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
|
||||
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
|
||||
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
|
||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||
@ -371,7 +467,7 @@ describe('PreviewServerFactory', () => {
|
||||
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
|
||||
expect(console.log).toHaveBeenCalledWith(jasmine.any(String), 'PreviewServer: ',
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
|
||||
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
@ -467,7 +563,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should respond with 404 for non-POST requests', async () => {
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get(url).expect(404),
|
||||
agent.put(url).expect(404),
|
||||
agent.patch(url).expect(404),
|
||||
@ -482,7 +578,7 @@ describe('PreviewServerFactory', () => {
|
||||
const request1 = agent.post(url);
|
||||
const request2 = agent.post(url).send();
|
||||
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
request1.expect(400, responseBody),
|
||||
request2.expect(400, responseBody),
|
||||
]);
|
||||
@ -495,7 +591,7 @@ describe('PreviewServerFactory', () => {
|
||||
const request1 = agent.post(url).send({});
|
||||
const request2 = agent.post(url).send({number: null});
|
||||
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||
]);
|
||||
@ -503,7 +599,7 @@ describe('PreviewServerFactory', () => {
|
||||
|
||||
|
||||
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
|
||||
await promisifyRequest(createRequest(+pr));
|
||||
await createRequest(+pr);
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||
});
|
||||
|
||||
@ -511,9 +607,8 @@ describe('PreviewServerFactory', () => {
|
||||
it('should propagate errors from BuildVerifier', async () => {
|
||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
await createRequest(+pr).expect(500, 'Test');
|
||||
|
||||
await promisifyRequest(req);
|
||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -522,19 +617,17 @@ describe('PreviewServerFactory', () => {
|
||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
|
||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||
|
||||
await promisifyRequest(createRequest(24));
|
||||
await createRequest(24);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
|
||||
|
||||
await promisifyRequest(createRequest(42));
|
||||
await createRequest(42);
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
|
||||
});
|
||||
|
||||
|
||||
it('should propagate errors from BuildCreator', async () => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||
|
||||
const req = createRequest(+pr).expect(500, 'Test');
|
||||
await verifyRequests([req]);
|
||||
await createRequest(+pr).expect(500, 'Test');
|
||||
});
|
||||
|
||||
|
||||
@ -544,7 +637,7 @@ describe('PreviewServerFactory', () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||
await verifyRequests(reqs);
|
||||
await Promise.all(reqs);
|
||||
});
|
||||
|
||||
|
||||
@ -552,7 +645,7 @@ describe('PreviewServerFactory', () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||
await verifyRequests(reqs);
|
||||
await Promise.all(reqs);
|
||||
});
|
||||
|
||||
|
||||
@ -560,14 +653,13 @@ describe('PreviewServerFactory', () => {
|
||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
|
||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||
await verifyRequests(reqs);
|
||||
await Promise.all(reqs);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
|
||||
const promises = ['foo', 'notlabeled'].
|
||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
||||
map(promisifyRequest);
|
||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200]));
|
||||
|
||||
await Promise.all(promises);
|
||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||
@ -584,7 +676,7 @@ describe('PreviewServerFactory', () => {
|
||||
it('should respond with 404', async () => {
|
||||
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||
|
||||
await verifyRequests([
|
||||
await Promise.all([
|
||||
agent.get('/some/url').expect(404, responseFor('get')),
|
||||
agent.put('/some/url').expect(404, responseFor('put')),
|
||||
agent.post('/some/url').expect(404, responseFor('post')),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
||||
set -eu -o pipefail
|
||||
|
||||
# Set up env variables
|
||||
export AIO_CIRCLE_CI_TOKEN=UNUSED_CIRCLE_CI_TOKEN
|
||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||
|
||||
# Run the clean-up
|
||||
|
@ -5,4 +5,5 @@ TODO (gkalpak): Add docs. Mention:
|
||||
- Testing on CI.
|
||||
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
|
||||
- Deploying from CI.
|
||||
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-to-firebase.sh`
|
||||
Relevant files: `.circleci/config.yml`, `scripts/ci/deploy.sh`, `aio/scripts/build-artifacts.sh`,
|
||||
`aio/scripts/deploy-to-firebase.sh`
|
||||
|
@ -34,34 +34,31 @@ container:
|
||||
|
||||
|
||||
### On CI (CircleCI)
|
||||
- Build job completes successfully.
|
||||
- The CI script checks whether the build job was initiated by a PR against the angular/angular
|
||||
master branch.
|
||||
- The CI script checks whether the PR has touched any files that might affect the angular.io app
|
||||
(currently the `aio/` or `packages/` directories, ignoring spec files).
|
||||
- The CI script builds the angular.io project.
|
||||
- The CI script gzips and stores the build artifacts in the CI infrastructure.
|
||||
- When the build completes CircleCI triggers a webhook on the preview-server.
|
||||
- When the build completes, CircleCI triggers a webhook on the preview-server.
|
||||
|
||||
More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md).
|
||||
|
||||
|
||||
### Hosting build artifacts
|
||||
|
||||
- nginx receives the webhook trigger and passes it through to the preview server.
|
||||
- The preview-server runs several preliminary checks to determine whether the request is valid and
|
||||
whether the corresponding PR can have a (public or non-public) preview (more details can be found
|
||||
[here](overview--security-model.md)).
|
||||
- The preview-server makes a request to CircleCI for the URL of the AIO build artifacts.
|
||||
- The preview-server makes a request to this URL to receive the artifact - failing if the size
|
||||
exceeds the specified max file size - and stores it in a temporary location.
|
||||
- The preview-server runs several checks to determine whether the request should be accepted and
|
||||
whether it should be publicly accessible or stored for later verification (more details can be
|
||||
found [here](overview--security-model.md)).
|
||||
- The preview-server runs more checks to determine whether the preview should be publicly accessible
|
||||
or stored for later verification (more details can be found [here](overview--security-model.md)).
|
||||
- The preview-server changes the "visibility" of the associated PR, if necessary. For example, if
|
||||
builds for the same PR had been previously deployed as non-public and the current build has been
|
||||
automatically verified, all previous builds are made public as well.
|
||||
If the PR transitions from "non-public" to "public", the preview-server posts a comment on the
|
||||
corresponding PR on GitHub mentioning the SHAs and the links where the previews can be found.
|
||||
- The preview-server verifies that it is not trying to overwrite an existing build.
|
||||
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the first
|
||||
few characters of the SHA: `<PR>/<SHA>/`
|
||||
- The preview-server deploys the artifacts to a sub-directory named after the PR number and the
|
||||
first few characters of the SHA: `<PR>/<SHA>/`
|
||||
(Non-publicly accessible PRs will be stored in a different location, but again derived from the PR
|
||||
number and SHA.)
|
||||
- If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on
|
||||
@ -101,8 +98,8 @@ More info on the possible HTTP status codes and their meaning can be found
|
||||
|
||||
### Removing obsolete artifacts
|
||||
In order to avoid flooding the disk with unnecessary build artifacts, there is a cronjob that runs a
|
||||
clean-up tasks once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||
that do not correspond with an open PR.
|
||||
clean-up task once a day. The task retrieves all open PRs from GitHub and removes all directories
|
||||
that do not correspond to an open PR.
|
||||
|
||||
|
||||
### Health-check
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Overview - HTTP Status Codes
|
||||
|
||||
|
||||
This is a list of all the possible HTTP status codes returned by the nginx and preview servers, along
|
||||
with a brief explanation of what they mean:
|
||||
This is a list of all the possible HTTP status codes returned by the nginx and preview servers,
|
||||
along with a brief explanation of what they mean:
|
||||
|
||||
|
||||
## `http://*.ngbuilds.io/*`
|
||||
@ -25,6 +25,23 @@ with a brief explanation of what they mean:
|
||||
File not found.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/can-have-public-preview/<pr>`
|
||||
|
||||
- **200 (OK)**:
|
||||
Whether the PR can have a public preview (based on its author, label, changed files).
|
||||
_Response type:_ JSON
|
||||
_Response format:_
|
||||
```ts
|
||||
{
|
||||
canHavePublicPreview: boolean,
|
||||
reason: string | null,
|
||||
}
|
||||
```
|
||||
|
||||
- **405 (Method Not Allowed)**:
|
||||
Request method other than GET.
|
||||
|
||||
|
||||
## `https://ngbuilds.io/circle-build`
|
||||
|
||||
- **201 (Created)**:
|
||||
|
@ -11,8 +11,8 @@ part of the CI process and serving them publicly.
|
||||
|
||||
## Security objectives
|
||||
|
||||
- **Prevent hosting arbitrary content to on servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow arbitrary untrusted PRs'
|
||||
- **Prevent hosting arbitrary content on our servers.**
|
||||
Since there is no restriction on who can submit a PR, we cannot allow arbitrary, untrusted PRs'
|
||||
build artifacts to be hosted.
|
||||
|
||||
- **Prevent overwriting other people's hosted build artifacts.**
|
||||
@ -40,40 +40,49 @@ part of the CI process and serving them publicly.
|
||||
### In a nutshell
|
||||
The implemented approach can be broken up to the following sub-tasks:
|
||||
|
||||
0. Receive notification from CircleCI of a completed build.
|
||||
1. Verify that the build is valid and download the artifact.
|
||||
2. Fetch the PR's metadata, including author and labels.
|
||||
3. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||
4. If necessary, update the corresponding PR's verification status.
|
||||
5. Deploy the artifacts to the corresponding PR's directory.
|
||||
6. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
1. Receive notification from CircleCI of a completed build.
|
||||
2. Verify that the build is valid and can have a preview.
|
||||
3. Download the build artifact.
|
||||
4. Fetch the PR's metadata, including author and labels.
|
||||
5. Check whether the PR can be automatically verified as "trusted" (based on its author or labels).
|
||||
6. If necessary, update the corresponding PR's verification status.
|
||||
7. Deploy the artifacts to the corresponding PR's directory.
|
||||
8. Prevent overwriting previously deployed artifacts (which ensures that the guarantees established
|
||||
during deployment will remain valid until the artifacts are removed).
|
||||
7. Prevent hosted preview files from accessing anything outside their directory.
|
||||
9. Prevent hosted preview files from accessing anything outside their directory.
|
||||
|
||||
|
||||
### Implementation details
|
||||
This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
|
||||
0. **Receive notification from CircleCI of a completed build**
|
||||
1. **Receive notification from CircleCI of a completed build**
|
||||
|
||||
CircleCI is configured to trigger a webhook on our preview-server whenever a build completes.
|
||||
The payload contains the number of the build that completed.
|
||||
|
||||
1. **Verify that the build is valid and download the artifact.**
|
||||
2. **Verify that the build is valid and can have a preview.**
|
||||
|
||||
We cannot trust that the data in the webhook trigger is authentic, so we only extract the build
|
||||
number and then run a direct query against the CircleCI API to get hold of the real data for
|
||||
the given build number.
|
||||
|
||||
If the build was not successful then we ignore this trigger. Otherwise we check that the
|
||||
associated github organisation and repository are what we expect (e.g. angular/angular).
|
||||
We perform a number of preliminary checks:
|
||||
- Was the webhook triggered by the designated CircleCI job (currently `aio_preview`)?
|
||||
- Was the build successful?
|
||||
- Are the associated GitHub organisation and repository what we expect (e.g. `angular/angular`)?
|
||||
- Has the PR touched any files that might affect the angular.io app (currently the `aio/` or
|
||||
`packages/` directories, ignoring spec files)?
|
||||
|
||||
Next we make another call to the CircleCI API to get a list of the URLS for artifacts of that
|
||||
If any of the preliminary checks fails, the process is aborted and not preview is generated.
|
||||
|
||||
3. **Download the build artifact.**
|
||||
|
||||
Next we make another call to the CircleCI API to get a list of the URLs for artifacts of that
|
||||
build. If there is one that matches the configured artifact path, we download the contents of the
|
||||
build artifact and store it in a local folder. This download has a maximum size limit to prevent
|
||||
PRs from producing artifacts that are so large they would cause the preview server to crash.
|
||||
|
||||
2. **Fetch the PR's metadata, including author and labels**.
|
||||
4. **Fetch the PR's metadata, including author and labels**.
|
||||
|
||||
Once we have securely downloaded the artifact for a build, we retrieve the PR's metadata -
|
||||
including the author's username and the labels - using the
|
||||
@ -81,7 +90,7 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
To avoid rate-limit restrictions, we use a Personal Access Token (issued by
|
||||
[@mary-poppins](https://github.com/mary-poppins)).
|
||||
|
||||
3. **Check whether the PR can be automatically verified as "trusted"**.
|
||||
5. **Check whether the PR can be automatically verified as "trusted"**.
|
||||
|
||||
"Trusted" means that we are confident that the build artifacts are suitable for being deployed
|
||||
and publicly accessible on the preview server. There are two ways to check that:
|
||||
@ -93,31 +102,32 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
`read:org` scope issued by a user that can "see" the specified GitHub organization.
|
||||
Here too, we use the token by @mary-poppins.
|
||||
|
||||
4. **If necessary update the corresponding PR's verification status**.
|
||||
6. **If necessary update the corresponding PR's verification status**.
|
||||
|
||||
Once we have determined whether the PR is considered "trusted", we update its "visibility" (i.e.
|
||||
whether it is publicly accessible or not), based on the new verification status. For example, if
|
||||
a PR was initially considered "not trusted" but the check triggered by a new build determined
|
||||
otherwise, the PR (and all the previously hosted previews) are made public. It works the same
|
||||
otherwise, the PR (and all the previously downloaded previews) are made public. It works the same
|
||||
way if a PR has gone from "trusted" to "not trusted".
|
||||
|
||||
5. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
7. **Deploy the artifacts to the corresponding PR's directory.**
|
||||
|
||||
With the preceding steps, we have verified that the build artifacts are valid.
|
||||
Additionally, we have determined whether the PR can be trusted to have its previews
|
||||
publicly accessible or whether further verification is necessary. The artifacts will be stored to
|
||||
the PR's directory, but will not be publicly accessible unless the PR has been verified.
|
||||
Essentially, as long as sub-tasks 1, 2 and 3 can be securely accomplished, it is possible to
|
||||
"project" the trust we have in a team's members through the PR to the build artifacts.
|
||||
With the preceding steps, we have verified that the build artifacts are valid. Additionally, we
|
||||
have determined whether the PR can be trusted to have its previews publicly accessible or whether
|
||||
further verification is necessary.
|
||||
|
||||
6. **Prevent overwriting previously deployed artifacts**.
|
||||
The artifacts will be stored to the PR's directory, but will not be publicly accessible unless
|
||||
the PR has been verified. Essentially, as long as sub-tasks 2, 3, 4 and 5 can be securely
|
||||
accomplished, it is possible to "project" the trust we have in a team's members through the PR to
|
||||
the build artifacts.
|
||||
|
||||
8. **Prevent overwriting previously deployed artifacts**.
|
||||
|
||||
In order to enforce this restriction (and ensure that the deployed artifacts' validity is
|
||||
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js
|
||||
Express server) rejects builds that have already been handled.
|
||||
preserved throughout their "lifetime"), the server that handles the artifacts (currently a Node.js Express server) rejects builds that have already been handled.
|
||||
_Note: A PR can contain multiple builds; one for each SHA that was built on CircleCI._
|
||||
|
||||
7. **Prevent hosted preview files from accessing anything outside their directory.**
|
||||
9. **Prevent hosted preview files from accessing anything outside their directory.**
|
||||
|
||||
Nginx (which is used to serve the hosted preview) has been configured to not follow symlinks
|
||||
outside of the directory where the preview files are stored.
|
||||
@ -130,10 +140,10 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
|
||||
This means that any secret access keys need only be stored on the preview-server and not on any of
|
||||
the CI build infrastructure (e.g. CircleCI).
|
||||
|
||||
- Each trusted PR author has full control over the content that is hosted as a preview for their PRs.
|
||||
Part of the security model relies on the trustworthiness of these authors.
|
||||
- Each trusted PR author has full control over the content that is hosted as a preview for their
|
||||
PRs. Part of the security model relies on the trustworthiness of these authors.
|
||||
|
||||
- Adding the specified label on a PR to mark it as trusted, gives the author full control over
|
||||
the content that is hosted for the specific PR preview (e.g. by pushing more commits to it).
|
||||
The user adding the label is responsible for ensuring that this control is not abused and that
|
||||
the PR is either closed (one way of another) or the access is revoked.
|
||||
- Adding the specified label on a PR to mark it as trusted, gives the author full control over the
|
||||
content that is hosted for the specific PR preview (e.g. by pushing more commits to it). The user
|
||||
adding the label is responsible for ensuring that this control is not abused and that the PR is
|
||||
either closed (one way of another) or the access is revoked.
|
||||
|
@ -8,7 +8,7 @@ Necessary secrets:
|
||||
1. `GITHUB_TOKEN`
|
||||
- Used for:
|
||||
- Retrieving open PRs without rate-limiting.
|
||||
- Retrieving PR author.
|
||||
- Retrieving PR info, such as author, labels, changed files.
|
||||
- Retrieving members of the trusted GitHub teams.
|
||||
- Posting comments with preview links on PRs.
|
||||
|
||||
@ -25,8 +25,9 @@ Necessary secrets:
|
||||
- Generate new token with the `public_repo` scope.
|
||||
|
||||
2. `CIRCLE_CI_TOKEN`
|
||||
- Visit https://circleci.com/gh/angular/angular/edit#api
|
||||
- Create an API token with `Build Artifacts` scope
|
||||
- Visit https://circleci.com/gh/angular/angular/edit#api.
|
||||
- Create an API token with `Build Artifacts` scope.
|
||||
|
||||
|
||||
## Save secrets on the VM
|
||||
|
||||
|
1
aio/content/cli-src/.gitignore
vendored
Normal file
1
aio/content/cli-src/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
yarn.lock
|
5
aio/content/cli-src/package.json
Normal file
5
aio/content/cli-src/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@angular/cli": "https://github.com/angular/cli-builds#master"
|
||||
}
|
||||
}
|
83
aio/content/cli/index.md
Normal file
83
aio/content/cli/index.md
Normal file
@ -0,0 +1,83 @@
|
||||
<h1 class="no-toc">CLI Command Reference</h1>
|
||||
|
||||
The Angular CLI is a command-line tool that you use to initialize, develop, scaffold, and maintain Angular applications.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installing Angular CLI
|
||||
|
||||
The current version of Angular CLI is 6.x.
|
||||
|
||||
* Both the CLI and the projects that you generate with the tool have dependencies that require Node 8.9 or higher, together with NPM 5.5.1 or higher.
|
||||
* Install the CLI using npm:
|
||||
`npm install -g @angular/cli`
|
||||
* The CLI is an open-source tool:
|
||||
https://github.com/angular/angular-cli/tree/master/packages/angular/cli
|
||||
|
||||
For details about changes between versions, and information about updating from previous releases, see the Releases tab on GitHub.
|
||||
|
||||
### Basic workflow
|
||||
|
||||
Invoke the tool on the command line through the ng executable. Online help is available on the command line:
|
||||
|
||||
```
|
||||
> ng help Lists commands with short descriptions
|
||||
> ng <command> --help Lists options for a command.
|
||||
```
|
||||
|
||||
To create, build, and serve a new, basic Angular project on a development server, use the following commands:
|
||||
|
||||
```
|
||||
cd <parent of new workspace>
|
||||
ng new my-project
|
||||
cd my-project
|
||||
ng serve
|
||||
```
|
||||
|
||||
In your browser, open http://localhost:4200/ to see the new app run.
|
||||
|
||||
### Workspaces and project files
|
||||
|
||||
Angular 6 introduced the workspace directory structure for Angular apps. A workspace defines a project. A project can contain multiple apps, as well as libraries that can be used in any of the apps.
|
||||
|
||||
Some commands (such as build) must be executed from within a workspace folder, and others (such as new) must be executed from outside any workspace. This requirement is called out in the description of each command where it applies.The `new` command creates a [workspace](guide/glossary#workspace) to contain [projects](guide/glossary#project). A project can be an app or a library, and a workspace can contain multiple apps and libraries.
|
||||
|
||||
A newly generated app project contains the source files for a root module, with a root component and template, which you can edit directly, or add to and modify using CLI commands. Use the generate command to add new files for additional components and services, and code for new pipes, directives, and so on.
|
||||
|
||||
* Commands such as `add` and `generate`, that create or operate on apps and libraries, must be executed from within a workspace folder.
|
||||
* Apps in a workspace can use libraries in the same workspace.
|
||||
* Each project has a `src` folder that contains the logic, data, and assets.
|
||||
See an example of the [file structure](guide/quickstart#project-file-review) in [Getting Started](guide/quickstart).
|
||||
|
||||
When you use the `serve` command to build an app, the server automatically rebuilds the app and reloads the page when you change any of the source files.
|
||||
|
||||
### Configuring the CLI
|
||||
|
||||
Configuration files let you customize your project. The CLI configuration file, angular.json, is created at the top level of the project folder. This is where you can set CLI defaults for your project, and specify which files to include when the CLI builds the project.
|
||||
|
||||
The CLI config command lets you set and retrieve configuration values from the command line, or you can edit the angular.json file directly.
|
||||
|
||||
* See the complete schema for angular.json.
|
||||
* Learn more about configuration options for Angular (link to new guide?)
|
||||
|
||||
### Command options and arguments
|
||||
|
||||
All commands and some options have aliases, as listed in the descriptions. Option names are prefixed with a double dash (--), but arguments and option aliases are not.
|
||||
|
||||
Typically, the name of a generated artifact can be given as an argument to the command or specified with the --name option. Most commands have additional options.
|
||||
|
||||
Command syntax is shown as follows:
|
||||
|
||||
```
|
||||
ng commandNameOrAlias <arg> [options]
|
||||
```
|
||||
|
||||
Options take either string or Boolean arguments. Defaults are shown in bold for Boolean or enumerated values, and are given with the description. For example:
|
||||
|
||||
```
|
||||
--optionNameOrAlias=<filename>
|
||||
--optionNameOrAlias=true|false
|
||||
--optionNameOrAlias=allowedValue1|allowedValue2|allowedValue3
|
||||
```
|
||||
|
||||
Boolean options can also be expressed with a prefix `no-` to indicate a value of false. For example, `--no-prod` is equivalent to `--prod=false`.
|
10
aio/content/examples/forms-overview/e2e/src/app.e2e-spec.ts
Normal file
10
aio/content/examples/forms-overview/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
|
||||
describe('Forms Overview Tests', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -0,0 +1,10 @@
|
||||
<!--The content below is only a placeholder and can be replaced.-->
|
||||
<h1>Forms Overview</h1>
|
||||
|
||||
<h2>Reactive</h2>
|
||||
|
||||
<app-reactive-favorite-color></app-reactive-favorite-color>
|
||||
|
||||
<h2>Template-Driven</h2>
|
||||
|
||||
<app-template-favorite-color></app-template-favorite-color>
|
@ -0,0 +1,31 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { TemplateModule } from './template/template.module';
|
||||
import { ReactiveModule } from './reactive/reactive.module';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ReactiveModule, TemplateModule],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should render title in a h1 tag', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Forms Overview');
|
||||
}));
|
||||
});
|
10
aio/content/examples/forms-overview/src/app/app.component.ts
Normal file
10
aio/content/examples/forms-overview/src/app/app.component.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'forms-intro';
|
||||
}
|
19
aio/content/examples/forms-overview/src/app/app.module.ts
Normal file
19
aio/content/examples/forms-overview/src/app/app.module.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { ReactiveModule } from './reactive/reactive.module';
|
||||
import { TemplateModule } from './template/template.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
ReactiveModule,
|
||||
TemplateModule
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
@ -0,0 +1,50 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { FavoriteColorComponent } from './favorite-color.component';
|
||||
import { createNewEvent } from '../../shared/utils';
|
||||
|
||||
describe('Favorite Color Component', () => {
|
||||
let component: FavoriteColorComponent;
|
||||
let fixture: ComponentFixture<FavoriteColorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ ReactiveFormsModule ],
|
||||
declarations: [ FavoriteColorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FavoriteColorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// #docregion view-to-model
|
||||
it('should update the value of the input field', () => {
|
||||
const input = fixture.nativeElement.querySelector('input');
|
||||
const event = createNewEvent('input');
|
||||
|
||||
input.value = 'Red';
|
||||
input.dispatchEvent(event);
|
||||
|
||||
expect(fixture.componentInstance.favoriteColorControl.value).toEqual('Red');
|
||||
});
|
||||
// #enddocregion view-to-model
|
||||
|
||||
// #docregion model-to-view
|
||||
it('should update the value in the control', () => {
|
||||
component.favoriteColorControl.setValue('Blue');
|
||||
|
||||
const input = fixture.nativeElement.querySelector('input');
|
||||
|
||||
expect(input.value).toBe('Blue');
|
||||
});
|
||||
// #enddocregion model-to-view
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reactive-favorite-color',
|
||||
template: `
|
||||
Favorite Color: <input type="text" [formControl]="favoriteColorControl">
|
||||
`
|
||||
})
|
||||
export class FavoriteColorComponent {
|
||||
favoriteColorControl = new FormControl('');
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { ReactiveModule } from './reactive.module';
|
||||
|
||||
describe('ReactiveModule', () => {
|
||||
let reactiveModule: ReactiveModule;
|
||||
|
||||
beforeEach(() => {
|
||||
reactiveModule = new ReactiveModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(reactiveModule).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { FavoriteColorComponent } from './favorite-color/favorite-color.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
declarations: [FavoriteColorComponent],
|
||||
exports: [FavoriteColorComponent],
|
||||
})
|
||||
export class ReactiveModule { }
|
@ -0,0 +1,5 @@
|
||||
export function createNewEvent(eventName: string, bubbles = false, cancelable = false) {
|
||||
let evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent(eventName, bubbles, cancelable, null);
|
||||
return evt;
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { FavoriteColorComponent } from './favorite-color.component';
|
||||
import { createNewEvent } from '../../shared/utils';
|
||||
|
||||
describe('FavoriteColorComponent', () => {
|
||||
let component: FavoriteColorComponent;
|
||||
let fixture: ComponentFixture<FavoriteColorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ FormsModule ],
|
||||
declarations: [ FavoriteColorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FavoriteColorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// #docregion model-to-view
|
||||
it('should update the favorite color on the input field', fakeAsync(() => {
|
||||
component.favoriteColor = 'Blue';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
tick();
|
||||
|
||||
const input = fixture.nativeElement.querySelector('input');
|
||||
|
||||
expect(input.value).toBe('Blue');
|
||||
}));
|
||||
// #enddocregion model-to-view
|
||||
|
||||
// #docregion view-to-model
|
||||
it('should update the favorite color in the component', fakeAsync(() => {
|
||||
const input = fixture.nativeElement.querySelector('input');
|
||||
const event = createNewEvent('input');
|
||||
|
||||
input.value = 'Red';
|
||||
input.dispatchEvent(event);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.favoriteColor).toEqual('Red');
|
||||
}));
|
||||
// #enddocregion view-to-model
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-template-favorite-color',
|
||||
template: `
|
||||
Favorite Color: <input type="text" [(ngModel)]="favoriteColor">
|
||||
`
|
||||
})
|
||||
export class FavoriteColorComponent {
|
||||
favoriteColor = '';
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { TemplateModule } from './template.module';
|
||||
|
||||
describe('TemplateModule', () => {
|
||||
let templateModule: TemplateModule;
|
||||
|
||||
beforeEach(() => {
|
||||
templateModule = new TemplateModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(templateModule).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FavoriteColorComponent } from './favorite-color/favorite-color.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
],
|
||||
declarations: [FavoriteColorComponent],
|
||||
exports: [FavoriteColorComponent]
|
||||
})
|
||||
export class TemplateModule { }
|
14
aio/content/examples/forms-overview/src/index.html
Normal file
14
aio/content/examples/forms-overview/src/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forms Overview</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/forms-overview/src/main.ts
Normal file
12
aio/content/examples/forms-overview/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));
|
7
aio/content/examples/forms-overview/stackblitz.json
Normal file
7
aio/content/examples/forms-overview/stackblitz.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Forms Overview",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js"
|
||||
]
|
||||
}
|
@ -4,7 +4,7 @@ import { browser, element, by, ExpectedConditions } from 'protractor';
|
||||
|
||||
const numDashboardTabs = 5;
|
||||
const numCrises = 4;
|
||||
const numHeroes = 6;
|
||||
const numHeroes = 10;
|
||||
const EC = ExpectedConditions;
|
||||
|
||||
describe('Router', () => {
|
||||
@ -13,33 +13,34 @@ describe('Router', () => {
|
||||
|
||||
function getPageStruct() {
|
||||
const hrefEles = element.all(by.css('app-root > nav a'));
|
||||
const crisisDetail = element.all(by.css('app-root > ng-component > ng-component > ng-component > div')).first();
|
||||
const heroDetail = element(by.css('app-root > ng-component > div'));
|
||||
const crisisDetail = element.all(by.css('app-root > div > app-crisis-center > app-crisis-list > app-crisis-detail > div')).first();
|
||||
const heroDetail = element(by.css('app-root > div > app-hero-detail'));
|
||||
|
||||
return {
|
||||
hrefs: hrefEles,
|
||||
activeHref: element(by.css('app-root > nav a.active')),
|
||||
|
||||
crisisHref: hrefEles.get(0),
|
||||
crisisList: element.all(by.css('app-root > ng-component > ng-component li')),
|
||||
crisisList: element.all(by.css('app-root > div > app-crisis-center > app-crisis-list li')),
|
||||
crisisDetail: crisisDetail,
|
||||
crisisDetailTitle: crisisDetail.element(by.xpath('*[1]')),
|
||||
|
||||
heroesHref: hrefEles.get(1),
|
||||
heroesList: element.all(by.css('app-root > ng-component li')),
|
||||
heroesList: element.all(by.css('app-root > div > app-hero-list li')),
|
||||
heroDetail: heroDetail,
|
||||
heroDetailTitle: heroDetail.element(by.xpath('*[1]')),
|
||||
heroDetailTitle: heroDetail.element(by.xpath('*[2]')),
|
||||
|
||||
adminHref: hrefEles.get(2),
|
||||
adminPreloadList: element.all(by.css('app-root > ng-component > ng-component > ul > li')),
|
||||
adminPreloadList: element.all(by.css('app-root > div > app-admin > app-admin-dashboard > ul > li')),
|
||||
|
||||
loginHref: hrefEles.get(3),
|
||||
loginButton: element.all(by.css('app-root > ng-component > p > button')),
|
||||
loginButton: element.all(by.css('app-root > div > app-login > p > button')),
|
||||
|
||||
contactHref: hrefEles.get(4),
|
||||
contactCancelButton: element.all(by.buttonText('Cancel')),
|
||||
|
||||
outletComponents: element.all(by.css('app-root > ng-component'))
|
||||
primaryOutlet: element.all(by.css('app-root > div > app-hero-list')),
|
||||
secondaryOutlet: element.all(by.css('app-root > app-compose-message'))
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,6 +99,7 @@ describe('Router', () => {
|
||||
it('saves changed hero details', async () => {
|
||||
const page = getPageStruct();
|
||||
await page.heroesHref.click();
|
||||
await browser.sleep(600);
|
||||
const heroEle = page.heroesList.get(4);
|
||||
let text = await heroEle.getText();
|
||||
expect(text.length).toBeGreaterThan(0, 'hero item text length');
|
||||
@ -105,6 +107,7 @@ describe('Router', () => {
|
||||
const heroText = text.substr(text.indexOf(' ')).trim();
|
||||
|
||||
await heroEle.click();
|
||||
await browser.sleep(600);
|
||||
expect(page.heroesList.count()).toBe(0, 'hero list count');
|
||||
expect(page.heroDetail.isPresent()).toBe(true, 'hero detail');
|
||||
expect(page.heroDetailTitle.getText()).toContain(heroText);
|
||||
@ -114,6 +117,7 @@ describe('Router', () => {
|
||||
|
||||
let buttonEle = page.heroDetail.element(by.css('button'));
|
||||
await buttonEle.click();
|
||||
await browser.sleep(600);
|
||||
expect(heroEle.getText()).toContain(heroText + '-foo');
|
||||
});
|
||||
|
||||
@ -130,7 +134,8 @@ describe('Router', () => {
|
||||
const page = getPageStruct();
|
||||
await page.heroesHref.click();
|
||||
await page.contactHref.click();
|
||||
expect(page.outletComponents.count()).toBe(2, 'route count');
|
||||
expect(page.primaryOutlet.count()).toBe(1, 'primary outlet');
|
||||
expect(page.secondaryOutlet.count()).toBe(1, 'secondary outlet');
|
||||
});
|
||||
|
||||
async function crisisCenterEdit(index: number, save: boolean) {
|
||||
|
@ -1,9 +0,0 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<p>Dashboard</p>
|
||||
`
|
||||
})
|
||||
export class AdminDashboardComponent { }
|
@ -0,0 +1 @@
|
||||
<p>Dashboard</p>
|
@ -5,13 +5,9 @@ import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<p>Dashboard</p>
|
||||
|
||||
<p>Session ID: {{ sessionId | async }}</p>
|
||||
<a id="anchor"></a>
|
||||
<p>Token: {{ token | async }}</p>
|
||||
`
|
||||
selector: 'app-admin-dashboard',
|
||||
templateUrl: './admin-dashboard.component.html',
|
||||
styleUrls: ['./admin-dashboard.component.css']
|
||||
})
|
||||
export class AdminDashboardComponent implements OnInit {
|
||||
sessionId: Observable<string>;
|
@ -0,0 +1,5 @@
|
||||
<p>Dashboard</p>
|
||||
|
||||
<p>Session ID: {{ sessionId | async }}</p>
|
||||
<a id="anchor"></a>
|
||||
<p>Token: {{ token | async }}</p>
|
@ -0,0 +1,10 @@
|
||||
<p>Dashboard</p>
|
||||
|
||||
<p>Session ID: {{ sessionId | async }}</p>
|
||||
<a id="anchor"></a>
|
||||
<p>Token: {{ token | async }}</p>
|
||||
|
||||
Preloaded Modules
|
||||
<ul>
|
||||
<li *ngFor="let module of modules">{{ module }}</li>
|
||||
</ul>
|
@ -4,22 +4,12 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { SelectivePreloadingStrategy } from '../selective-preloading-strategy';
|
||||
|
||||
import { SelectivePreloadingStrategyService } from '../../selective-preloading-strategy.service';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<p>Dashboard</p>
|
||||
|
||||
<p>Session ID: {{ sessionId | async }}</p>
|
||||
<a id="anchor"></a>
|
||||
<p>Token: {{ token | async }}</p>
|
||||
|
||||
Preloaded Modules
|
||||
<ul>
|
||||
<li *ngFor="let module of modules">{{ module }}</li>
|
||||
</ul>
|
||||
`
|
||||
selector: 'app-admin-dashboard',
|
||||
templateUrl: './admin-dashboard.component.html',
|
||||
styleUrls: ['./admin-dashboard.component.css']
|
||||
})
|
||||
export class AdminDashboardComponent implements OnInit {
|
||||
sessionId: Observable<string>;
|
||||
@ -28,7 +18,7 @@ export class AdminDashboardComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private preloadStrategy: SelectivePreloadingStrategy
|
||||
preloadStrategy: SelectivePreloadingStrategyService
|
||||
) {
|
||||
this.modules = preloadStrategy.preloadedModules;
|
||||
}
|
@ -3,10 +3,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes.component';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises/manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component';
|
||||
|
||||
// #docregion admin-routes
|
||||
const adminRoutes: Routes = [
|
||||
|
@ -3,13 +3,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes.component';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises/manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component';
|
||||
|
||||
// #docregion admin-route
|
||||
import { AuthGuard } from '../auth-guard.service';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
|
||||
const adminRoutes: Routes = [
|
||||
{
|
||||
|
@ -3,13 +3,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes.component';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises/manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component';
|
||||
|
||||
// #docregion admin-route
|
||||
import { AuthGuard } from '../auth-guard.service';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
|
||||
// #docregion can-activate-child
|
||||
const adminRoutes: Routes = [
|
||||
|
@ -3,12 +3,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes.component';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises/manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component';
|
||||
|
||||
import { AuthGuard } from '../auth-guard.service';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
|
||||
const adminRoutes: Routes = [
|
||||
{
|
||||
|
@ -1,17 +0,0 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<h3>ADMIN</h3>
|
||||
<nav>
|
||||
<a routerLink="./" routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
|
||||
<a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
|
||||
<a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
})
|
||||
export class AdminComponent {
|
||||
}
|
@ -2,10 +2,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes.component';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
|
||||
import { ManageCrisesComponent } from './manage-crises/manage-crises.component';
|
||||
import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component';
|
||||
|
||||
import { AdminRoutingModule } from './admin-routing.module';
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
<h3>ADMIN</h3>
|
||||
<nav>
|
||||
<a routerLink="./" routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
|
||||
<a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
|
||||
<a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
@ -0,0 +1,10 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrls: ['./admin.component.css']
|
||||
})
|
||||
export class AdminComponent {
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<p>Manage your crises here</p>
|
||||
`
|
||||
})
|
||||
export class ManageCrisesComponent { }
|
@ -0,0 +1 @@
|
||||
<p>Manage your crises here</p>
|
@ -0,0 +1,9 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-crises',
|
||||
templateUrl: './manage-crises.component.html',
|
||||
styleUrls: ['./manage-crises.component.css']
|
||||
})
|
||||
export class ManageCrisesComponent { }
|
@ -1,9 +0,0 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<p>Manage your heroes here</p>
|
||||
`
|
||||
})
|
||||
export class ManageHeroesComponent { }
|
@ -0,0 +1 @@
|
||||
<p>Manage your heroes here</p>
|
@ -0,0 +1,9 @@
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-hereos',
|
||||
templateUrl: './manage-heroes.component.html',
|
||||
styleUrls: ['./manage-heroes.component.css']
|
||||
})
|
||||
export class ManageHeroesComponent { }
|
@ -1,26 +1,35 @@
|
||||
// #docregion
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import {
|
||||
trigger, animateChild, group,
|
||||
transition, animate, style, query
|
||||
} from '@angular/animations';
|
||||
|
||||
// Component transition animations
|
||||
export const slideInDownAnimation =
|
||||
|
||||
// Routable animations
|
||||
export const slideInAnimation =
|
||||
trigger('routeAnimation', [
|
||||
state('*',
|
||||
style({
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)'
|
||||
})
|
||||
),
|
||||
transition(':enter', [
|
||||
style({
|
||||
opacity: 0,
|
||||
transform: 'translateX(-100%)'
|
||||
}),
|
||||
animate('0.2s ease-in')
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate('0.5s ease-out', style({
|
||||
opacity: 0,
|
||||
transform: 'translateY(100%)'
|
||||
}))
|
||||
transition('heroes <=> hero', [
|
||||
style({ position: 'relative' }),
|
||||
query(':enter, :leave', [
|
||||
style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
})
|
||||
]),
|
||||
query(':enter', [
|
||||
style({ left: '-100%'})
|
||||
]),
|
||||
query(':leave', animateChild()),
|
||||
group([
|
||||
query(':leave', [
|
||||
animate('300ms ease-out', style({ left: '100%'}))
|
||||
]),
|
||||
query(':enter', [
|
||||
animate('300ms ease-out', style({ left: '0%'}))
|
||||
])
|
||||
]),
|
||||
query(':enter', animateChild()),
|
||||
])
|
||||
]);
|
||||
|
@ -2,9 +2,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CrisisListComponent } from './crisis-list.component';
|
||||
import { HeroListComponent } from './hero-list.component';
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
import { CrisisListComponent } from './crisis-list/crisis-list.component';
|
||||
import { HeroListComponent } from './hero-list/hero-list.component';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
// #docregion appRoutes
|
||||
const appRoutes: Routes = [
|
||||
|
@ -1,14 +1,19 @@
|
||||
// #docregion
|
||||
// #docregion milestone3
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CrisisListComponent } from './crisis-list.component';
|
||||
// import { HeroListComponent } from './hero-list.component'; // <-- delete this line
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
import { CrisisListComponent } from './crisis-list/crisis-list.component';
|
||||
// #enddocregion milestone3
|
||||
// import { HeroListComponent } from './hero-list/hero-list.component'; // <-- delete this line
|
||||
// #docregion milestone3
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{ path: 'crisis-center', component: CrisisListComponent },
|
||||
// #enddocregion milestone3
|
||||
// { path: 'heroes', component: HeroListComponent }, // <-- delete this line
|
||||
// #docregion milestone3
|
||||
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
];
|
||||
@ -25,3 +30,4 @@ const appRoutes: Routes = [
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
// #enddocregion milestone3
|
||||
|
@ -3,8 +3,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ComposeMessageComponent } from './compose-message.component';
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
// #enddocregion v3
|
||||
import { ComposeMessageComponent } from './compose-message/compose-message.component';
|
||||
// #docregion v3
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
// #enddocregion v3
|
||||
|
@ -2,9 +2,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ComposeMessageComponent } from './compose-message.component';
|
||||
import { CanDeactivateGuard } from './can-deactivate-guard.service';
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
import { ComposeMessageComponent } from './compose-message/compose-message.component';
|
||||
import { CanDeactivateGuard } from './can-deactivate.guard';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
@ -25,9 +25,6 @@ const appRoutes: Routes = [
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
],
|
||||
providers: [
|
||||
CanDeactivateGuard
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
@ -5,11 +5,10 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
// #enddocregion import-router
|
||||
|
||||
import { ComposeMessageComponent } from './compose-message.component';
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
import { ComposeMessageComponent } from './compose-message/compose-message.component';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
import { CanDeactivateGuard } from './can-deactivate-guard.service';
|
||||
import { AuthGuard } from './auth-guard.service';
|
||||
import { AuthGuard } from './auth/auth.guard';
|
||||
|
||||
|
||||
const appRoutes: Routes = [
|
||||
@ -21,7 +20,7 @@ const appRoutes: Routes = [
|
||||
// #docregion admin, admin-1
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: 'app/admin/admin.module#AdminModule',
|
||||
loadChildren: './admin/admin.module#AdminModule',
|
||||
// #enddocregion admin-1
|
||||
canLoad: [AuthGuard]
|
||||
// #docregion admin-1
|
||||
@ -40,9 +39,6 @@ const appRoutes: Routes = [
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
],
|
||||
providers: [
|
||||
CanDeactivateGuard
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
@ -8,11 +8,10 @@ import {
|
||||
// #docregion preload-v1
|
||||
} from '@angular/router';
|
||||
|
||||
import { ComposeMessageComponent } from './compose-message.component';
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
import { ComposeMessageComponent } from './compose-message/compose-message.component';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
import { CanDeactivateGuard } from './can-deactivate-guard.service';
|
||||
import { AuthGuard } from './auth-guard.service';
|
||||
import { AuthGuard } from './auth/auth.guard';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
@ -22,12 +21,12 @@ const appRoutes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: 'app/admin/admin.module#AdminModule',
|
||||
loadChildren: './admin/admin.module#AdminModule',
|
||||
canLoad: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'crisis-center',
|
||||
loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule'
|
||||
loadChildren: './crisis-center/crisis-center.module#CrisisCenterModule'
|
||||
},
|
||||
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
@ -49,9 +48,6 @@ const appRoutes: Routes = [
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
],
|
||||
providers: [
|
||||
CanDeactivateGuard
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
@ -3,12 +3,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ComposeMessageComponent } from './compose-message.component';
|
||||
import { PageNotFoundComponent } from './not-found.component';
|
||||
import { ComposeMessageComponent } from './compose-message/compose-message.component';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
|
||||
import { CanDeactivateGuard } from './can-deactivate-guard.service';
|
||||
import { AuthGuard } from './auth-guard.service';
|
||||
import { SelectivePreloadingStrategy } from './selective-preloading-strategy';
|
||||
import { AuthGuard } from './auth/auth.guard';
|
||||
import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
@ -18,13 +17,13 @@ const appRoutes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: 'app/admin/admin.module#AdminModule',
|
||||
loadChildren: './admin/admin.module#AdminModule',
|
||||
canLoad: [AuthGuard]
|
||||
},
|
||||
// #docregion preload-v2
|
||||
{
|
||||
path: 'crisis-center',
|
||||
loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
|
||||
loadChildren: './crisis-center/crisis-center.module#CrisisCenterModule',
|
||||
data: { preload: true }
|
||||
},
|
||||
// #enddocregion preload-v2
|
||||
@ -37,18 +36,13 @@ const appRoutes: Routes = [
|
||||
RouterModule.forRoot(
|
||||
appRoutes,
|
||||
{
|
||||
enableTracing: true, // <-- debugging purposes only
|
||||
preloadingStrategy: SelectivePreloadingStrategy,
|
||||
|
||||
enableTracing: false, // <-- debugging purposes only
|
||||
preloadingStrategy: SelectivePreloadingStrategyService,
|
||||
}
|
||||
)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
],
|
||||
providers: [
|
||||
CanDeactivateGuard,
|
||||
SelectivePreloadingStrategy
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
7
aio/content/examples/router/src/app/app.component.1.html
Normal file
7
aio/content/examples/router/src/app/app.component.1.html
Normal file
@ -0,0 +1,7 @@
|
||||
<!-- #docregion -->
|
||||
<h1>Angular Router</h1>
|
||||
<nav>
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
@ -1,18 +1,9 @@
|
||||
/* First version */
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
// #docregion template
|
||||
template: `
|
||||
<h1>Angular Router</h1>
|
||||
<nav>
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
// #enddocregion template
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.css']
|
||||
})
|
||||
export class AppComponent { }
|
||||
|
9
aio/content/examples/router/src/app/app.component.2.html
Normal file
9
aio/content/examples/router/src/app/app.component.2.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!-- #docregion -->
|
||||
<h1>Angular Router</h1>
|
||||
<nav>
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
</nav>
|
||||
<div [@routeAnimation]="getAnimationData(routerOutlet)">
|
||||
<router-outlet #routerOutlet="outlet"></router-outlet>
|
||||
</div>
|
@ -1,16 +1,21 @@
|
||||
/* Second Heroes version */
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
// #docregion animation-imports
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { slideInAnimation } from './animations';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<h1>Angular Router</h1>
|
||||
<nav>
|
||||
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
|
||||
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.css'],
|
||||
animations: [ slideInAnimation ]
|
||||
})
|
||||
export class AppComponent { }
|
||||
// #enddocregion animation-imports
|
||||
// #docregion function-binding
|
||||
export class AppComponent {
|
||||
getAnimationData(outlet: RouterOutlet) {
|
||||
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
|
||||
}
|
||||
}
|
||||
// #enddocregion function-binding
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user