diff --git a/.circleci/bazel.rc b/.circleci/bazel.rc index b63cdfecbd..3ab299f8e8 100644 --- a/.circleci/bazel.rc +++ b/.circleci/bazel.rc @@ -20,18 +20,6 @@ build --announce_rc # We use this when uploading artifacts after the build finishes build --symlink_prefix=dist/ -# Enable experimental CircleCI bazel remote cache proxy -# See remote cache documentation in /docs/BAZEL.md -build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643 - -# Prevent unstable environment variables from tainting cache keys -build --experimental_strict_action_env - -# Save downloaded repositories such as the go toolchain -# This directory can then be included in the CircleCI cache -# It should save time running the first build -build --experimental_repository_cache=/home/circleci/bazel_repository_cache - # Workaround https://github.com/bazelbuild/bazel/issues/3645 # Bazel doesn't calculate the memory ceiling correctly when running under Docker. # Limit Bazel to consuming resources that fit in CircleCI "xlarge" class @@ -40,3 +28,6 @@ build --local_resources=14336,8.0,1.0 # Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309 test --flaky_test_attempts=2 + +# More details on failures +build --verbose_failures=true diff --git a/.circleci/config.yml b/.circleci/config.yml index 4ad0d76d94..3f095d52c5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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.3.3 -var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.3.3 +var_1: &docker_image angular/ngcontainer:0.6.0 +var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.6.0 # Define common ENV vars var_3: &define_env_vars @@ -26,6 +26,11 @@ var_4: &setup-bazel-remote-cache command: ~/bazel-remote-proxy -backend circleci:// background: true +var_5: &setup_bazel_remote_execution + run: + name: "Setup bazel RBE remote execution" + command: openssl aes-256-cbc -d -in .circleci/gcp_token -k "${CIRCLE_PROJECT_REPONAME}" -out /home/circleci/.gcp_credentials && echo "export GOOGLE_APPLICATION_CREDENTIALS=/home/circleci/.gcp_credentials" >> $BASH_ENV && sudo bash -c "cat .circleci/rbe-bazel.rc >> /etc/bazel.bazelrc" + # Settings common to each job anchor_1: &job_defaults working_directory: ~/ng @@ -42,19 +47,18 @@ version: 2 jobs: lint: <<: *job_defaults + resource_class: xlarge steps: - checkout: <<: *post_checkout + - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc # Check BUILD.bazel formatting before we have a node_modules directory # Then we don't need any exclude pattern to avoid checking those files - - run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) || + - run: 'yarn buildifier -mode=check || (echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)' # Run the skylark linter to check our Bazel rules - # deprecated-api is disabled because we use actions.new_file(genfiles_dir) - # which has no replacement, see https://github.com/bazelbuild/bazel/issues/4858 - - run: 'find . -type f -name "*.bzl" | - xargs java -jar /usr/local/bin/Skylint_deploy.jar --disable-checks=deprecated-api || + - run: 'yarn skylint || (echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)' - restore_cache: @@ -70,22 +74,20 @@ jobs: - *define_env_vars - checkout: <<: *post_checkout - # See remote cache documentation in /docs/BAZEL.md - - run: .circleci/setup_cache.sh - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - *setup-bazel-remote-cache - - restore_cache: - key: *cache_key - - - run: ls /home/circleci/bazel_repository_cache || true - run: bazel info release - run: bazel run @nodejs//:yarn # Use bazel query so that we explicitly ask for all buildable targets to be built as well # This avoids waiting for the slowest build target to finish before running the first test # See https://github.com/bazelbuild/bazel/issues/4257 # NOTE: Angular developers should typically just bazel build //packages/... or bazel test //packages/... - - run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only --test_tag_filters=-manual,-ivy-only + # Setup remote execution and run RBE-compatible tests. + - *setup_bazel_remote_execution + - run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only --test_tag_filters=-manual,-ivy-only,-local + # Now run RBE incompatible tests locally. + - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc + - run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only,local --test_tag_filters=-manual,-ivy-only,local # CircleCI will allow us to go back and view/download these artifacts from past builds. # Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts. @@ -119,15 +121,10 @@ jobs: - *define_env_vars - checkout: <<: *post_checkout - # See remote cache documentation in /docs/BAZEL.md - - run: .circleci/setup_cache.sh - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - *setup-bazel-remote-cache - - - restore_cache: - key: *cache_key - run: bazel run @yarn//:yarn + - *setup_bazel_remote_execution - run: bazel query --output=label //... | xargs bazel test --define=compile=jit --build_tag_filters=ivy-jit --test_tag_filters=-manual,ivy-jit test_ivy_aot: @@ -137,17 +134,45 @@ jobs: - *define_env_vars - checkout: <<: *post_checkout - # See remote cache documentation in /docs/BAZEL.md - - run: .circleci/setup_cache.sh - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - *setup-bazel-remote-cache - - - restore_cache: - key: *cache_key - run: bazel run @yarn//:yarn + - *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: + AIO_SNAPSHOT_ARTIFACT_PATH: &aio_preview_artifact_path 'aio/tmp/snapshot.tgz' + steps: + - checkout: + <<: *post_checkout + - restore_cache: + key: *cache_key + - run: yarn install --frozen-lockfile --non-interactive + - 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 @@ -162,12 +187,9 @@ jobs: - *define_env_vars - checkout: <<: *post_checkout - # See remote cache documentation in /docs/BAZEL.md - - run: .circleci/setup_cache.sh - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - *setup-bazel-remote-cache - - run: bazel run @nodejs//:yarn + - *setup_bazel_remote_execution - run: scripts/build-packages-dist.sh # Save the npm packages from //packages/... for other workflow jobs to read @@ -234,7 +256,11 @@ jobs: <<: *post_checkout - restore_cache: key: *cache_key - - run: xvfb-run --auto-servernum ./aio/scripts/test-production.sh + - run: + name: Run tests against the deployed apps + command: | + source "./scripts/ci/env.sh" print + xvfb-run --auto-servernum ./aio/scripts/test-production.sh $AIO_MIN_PWA_SCORE workflows: version: 2 @@ -245,6 +271,14 @@ workflows: - test_ivy_jit - test_ivy_aot - build-packages-dist + - 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 @@ -273,3 +307,7 @@ workflows: branches: only: - master + +notify: + webhooks: + - url: https://ngbuilds.io/circle-build diff --git a/.circleci/gcp_token b/.circleci/gcp_token new file mode 100644 index 0000000000..c77bcf6345 Binary files /dev/null and b/.circleci/gcp_token differ diff --git a/.circleci/rbe-bazel.rc b/.circleci/rbe-bazel.rc new file mode 100644 index 0000000000..74608cfc79 --- /dev/null +++ b/.circleci/rbe-bazel.rc @@ -0,0 +1,77 @@ +# These options are enabled when running on CI with Remote Build Execution. + +################################################################ +# Toolchain related flags for remote build execution. # +################################################################ +# Remote Build Execution requires a strong hash function, such as SHA256. +startup --host_jvm_args=-Dbazel.DigestFunction=SHA256 + +# Depending on how many machines are in the remote execution instance, setting +# this higher can make builds faster by allowing more jobs to run in parallel. +# Setting it too high can result in jobs that timeout, however, while waiting +# for a remote machine to execute them. +build --jobs=150 + +# Set several flags related to specifying the platform, toolchain and java +# properties. +# These flags are duplicated rather than imported from (for example) +# %workspace%/configs/ubuntu16_04_clang/1.0/toolchain.bazelrc to make this +# bazelrc a standalone file that can be copied more easily. +# These flags should only be used as is for the rbe-ubuntu16-04 container +# and need to be adapted to work with other toolchain containers. +build --host_javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:jdk8 +build --javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:jdk8 +build --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8 +build --java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8 +build --crosstool_top=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/default:toolchain +build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 +# Platform flags: +# The toolchain container used for execution is defined in the target indicated +# by "extra_execution_platforms", "host_platform" and "platforms". +# If you are using your own toolchain container, you need to create a platform +# target with "constraint_values" that allow for the toolchain specified with +# "extra_toolchains" to be selected (given constraints defined in +# "exec_compatible_with"). +# More about platforms: https://docs.bazel.build/versions/master/platforms.html +build --extra_toolchains=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/cpp:cc-toolchain-clang-x86_64-default +build --extra_execution_platforms=//tools:rbe_ubuntu1604-angular +build --host_platform=//tools:rbe_ubuntu1604-angular +build --platforms=//tools:rbe_ubuntu1604-angular + +# Set various strategies so that all actions execute remotely. Mixing remote +# and local execution will lead to errors unless the toolchain and remote +# machine exactly match the host machine. +build --spawn_strategy=remote +build --strategy=Javac=remote +build --strategy=Closure=remote +build --genrule_strategy=remote +build --define=EXECUTOR=remote + +# Enable the remote cache so action results can be shared across machines, +# developers, and workspaces. +build --remote_cache=remotebuildexecution.googleapis.com + +# Enable remote execution so actions are performed on the remote systems. +build --remote_executor=remotebuildexecution.googleapis.com + +# Remote instance. +build --remote_instance_name=projects/internal-200822/instances/default_instance + +# Enable encryption. +build --tls_enabled=true + +# Enforce stricter environment rules, which eliminates some non-hermetic +# behavior and therefore improves both the remote cache hit rate and the +# correctness and repeatability of the build. +build --experimental_strict_action_env=true + +# Set a higher timeout value, just in case. +build --remote_timeout=3600 + +# Enable authentication. This will pick up application default credentials by +# default. You can use --auth_credentials=some_file.json to use a service +# account credential instead. +build --auth_enabled=true + +# Do not accept remote cache. +build --remote_accept_cached=false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b74eed5b08..ffec1591b6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,17 +10,17 @@ Please check if your PR fulfills the following requirements: What kind of change does this PR introduce? -``` -[ ] Bugfix -[ ] Feature -[ ] Code style update (formatting, local variables) -[ ] Refactoring (no functional changes, no api changes) -[ ] Build related changes -[ ] CI related changes -[ ] Documentation content changes -[ ] angular.io application / infrastructure changes -[ ] Other... Please describe: -``` + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update (formatting, local variables) +- [ ] Refactoring (no functional changes, no api changes) +- [ ] Build related changes +- [ ] CI related changes +- [ ] Documentation content changes +- [ ] angular.io application / infrastructure changes +- [ ] Other... Please describe: + ## What is the current behavior? @@ -32,10 +32,10 @@ Issue Number: N/A ## Does this PR introduce a breaking change? -``` -[ ] Yes -[ ] No -``` + +- [ ] Yes +- [ ] No + diff --git a/.github/angular-robot.yml b/.github/angular-robot.yml index 471cba19d1..f1931ebb06 100644 --- a/.github/angular-robot.yml +++ b/.github/angular-robot.yml @@ -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: @@ -62,6 +59,13 @@ merge: # list of checks that will determine if the merge label can be added checks: + + # require that the PR has reviews from all requested reviewers + # + # 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") @@ -120,3 +124,23 @@ triage: - - "type: RFC / Discussion / question" - "comp: *" + +# options for the triage PR plugin +triagePR: + # set to true to disable + disabled: false + # number of the milestone to apply when the PR has not been triaged yet + needsTriageMilestone: 83, + # number of the milestone to apply when the PR is triaged + defaultMilestone: 82, + # arrays of labels that determine if a PR has been triaged by the caretaker + l1TriageLabels: + - + - "comp: *" + # arrays of labels that determine if a PR has been fully triaged + l2TriageLabels: + - + - "type: *" + - "effort*" + - "risk*" + - "comp: *" diff --git a/.nvmrc b/.nvmrc index fa97ecedc2..fe6d2ac749 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.9 +10.9.0 diff --git a/.pullapprove.yml b/.pullapprove.yml index 1ac9acfb6b..a19bffa035 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -8,6 +8,7 @@ # alexeagle - Alex Eagle # alxhub - Alex Rickabaugh # andrewseguin - Andrew Seguin +# benlesh - Ben Lesh # brandonroberts - Brandon Roberts # brocco - Mike Brocchi # filipesilva - Filipe Silva @@ -15,7 +16,7 @@ # hansl - Hans Larsen # IgorMinar - Igor Minar # jasonaden - Jason Aden -# kapunahelewong - Kapunahele Wong +# jenniferfell - Jennifer Fell # kara - Kara Erickson # kyliau - Keen Yee Liau # matsko - Matias Niemelä @@ -23,7 +24,6 @@ # petebacondarwin - Pete Bacon Darwin # pkozlowski-opensource - Pawel Kozlowski # robwormald - Rob Wormald -# vicb - Victor Berchet # vikerman - Vikram Subramanian @@ -91,6 +91,7 @@ groups: - "*.bzl" - "packages/bazel/*" - "tools/bazel.rc" + - "/docs/BAZEL.md" users: - alexeagle #primary - kyliau @@ -123,49 +124,116 @@ groups: users: - alexeagle - mhevery - - vicb - IgorMinar #fallback core: conditions: files: - "packages/core/*" + - "aio/content/guide/bootstrapping.md" + - "aio/content/examples/bootstrapping/*" + - "aio/content/guide/attribute-directives.md" + - "aio/content/examples/attribute-directives/*" + - "aio/content/images/guide/attribute-directives/*" + - "aio/content/guide/structural-directives.md" + - "aio/content/examples/structural-directives/*" + - "aio/content/images/guide/structural-directives/*" + - "aio/content/guide/dynamic-component-loader.md" + - "aio/content/examples/dynamic-component-loader/*" + - "aio/content/images/guide/dynamic-component-loader/*" + - "aio/content/guide/template-syntax.md" + - "aio/content/examples/template-syntax/*" + - "aio/content/images/guide/template-syntax/*" + - "aio/content/guide/dependency-injection.md" + - "aio/content/examples/dependency-injection/*" + - "aio/content/images/guide/dependency-injection/*" + - "aio/content/guide/dependency-injection-in-action.md" + - "aio/content/examples/dependency-injection-in-action/*" + - "aio/content/images/guide/dependency-injection-in-action/*" + - "aio/content/guide/hierarchical-dependency-injection.md" + - "aio/content/examples/hierarchical-dependency-injection/*" + - "aio/content/guide/singleton-services.md" + - "aio/content/guide/dependency-injection-pattern.md" + - "aio/content/guide/providers.md" + - "aio/content/examples/providers/*" + - "aio/content/guide/component-interaction.md" + - "aio/content/examples/component-interaction/*" + - "aio/content/images/guide/component-interaction/*" + - "aio/content/guide/component-styles.md" + - "aio/content/examples/component-styles/*" + - "aio/content/guide/lifecycle-hooks.md" + - "aio/content/examples/lifecycle-hooks/*" + - "aio/content/images/guide/lifecycle-hooks/*" + - "aio/content/examples/ngcontainer/*" + - "aio/content/images/guide/ngcontainer/*" + - "aio/content/guide/pipes.md" + - "aio/content/examples/pipes/*" + - "aio/content/images/guide/pipes/*" + - "aio/content/guide/entry-components.md" + - "aio/content/guide/set-document-title.md" + - "aio/content/examples/set-document-title/*" + - "aio/content/images/guide/set-document-title/*" + - "aio/content/guide/ngmodules.md" + - "aio/content/examples/ngmodules/*" + - "aio/content/examples/ngmodule/*" + - "aio/content/images/guide/ngmodule/*" + - "aio/content/guide/ngmodule-faq.md" + - "aio/content/examples/ngmodule-faq/*" + - "aio/content/guide/module-types.md" + - "aio/content/guide/sharing-ngmodules.md" + - "aio/content/guide/frequent-ngmodules.md" + - "aio/content/images/guide/frequent-ngmodules/*" + - "aio/content/guide/ngmodule-api.md" + - "aio/content/guide/ngmodule-vs-jsmodule.md" + - "aio/content/guide/feature-modules.md" + - "aio/content/examples/feature-modules/*" + - "aio/content/images/guide/feature-modules/*" + - "aio/content/guide/lazy-loading-ngmodules.md" + - "aio/content/examples/lazy-loading-ngmodules/*" + - "aio/content/images/guide/lazy-loading-ngmodules" users: - mhevery #primary - jasonaden - kara - - vicb - - IgorMinar #fallback + - IgorMinar + - jenniferfell #docs only animations: conditions: files: - "packages/animations/*" - "packages/platform-browser/animations/*" + - "aio/content/guide/animations.md" + - "aio/content/examples/animations/*" + - "aio/content/images/guide/animations/*" users: - matsko #primary - mhevery #fallback - IgorMinar #fallback + - jenniferfell #docs only compiler/i18n: conditions: files: - "packages/compiler/src/i18n/*" + - "aio/content/guide/i18n.md" + - "aio/content/examples/i18n/*" users: - - vicb #primary - - alxhub + - alxhub #primary - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only compiler: conditions: files: - "packages/compiler/*" + - "aio/content/guide/aot-compiler.md" users: - alxhub #primary - - vicb - mhevery - IgorMinar #fallback + - jenniferfell #docs only compiler-cli/ngtools: conditions: @@ -174,7 +242,6 @@ groups: users: - hansl - filipesilva #fallback - - brocco #fallback - IgorMinar #fallback compiler-cli: @@ -188,7 +255,6 @@ groups: users: - alexeagle - alxhub - - vicb - IgorMinar #fallback - mhevery #fallback @@ -201,7 +267,6 @@ groups: - "packages/common/http/*" users: - pkozlowski-opensource #primary - - vicb - IgorMinar #fallback - mhevery #fallback @@ -210,108 +275,162 @@ groups: files: - "packages/forms/*" - "aio/content/guide/forms.md" - - "aio/content/guide/form-validation.md" - - "aio/content/guide/reactive-forms.md" - "aio/content/examples/forms/*" + - "aio/content/images/guide/forms/*" + - "aio/content/guide/forms-overview.md" + - "aio/content/examples/forms-overview/*" + - "aio/content/images/guide/forms-overview/*" + - "aio/content/guide/form-validation.md" - "aio/content/examples/form-validation/*" + - "aio/content/images/guide/form-validation/*" + - "aio/content/guide/dynamic-form.md" + - "aio/content/examples/dynamic-form/*" + - "aio/content/images/guide/dynamic-form/*" + - "aio/content/guide/reactive-forms.md" - "aio/content/examples/reactive-forms/*" + - "aio/content/images/guide/reactive-forms/*" users: - kara #primary - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only http: conditions: files: - "packages/common/http/*" - "packages/http/*" + - "aio/content/guide/http.md" + - "aio/content/examples/http/*" + - "aio/content/images/guide/http/*" users: - alxhub #primary - IgorMinar - mhevery #fallback + - jenniferfell #docs only language-service: conditions: files: - "packages/language-service/*" + - "aio/content/guide/language-service.md" + - "aio/content/images/guide/language-service/*" users: - kyliau #primary # needs secondary - - vicb - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only router: conditions: files: - "packages/router/*" + - "aio/content/guide/router.md" + - "aio/content/examples/router/*" + - "aio/content/images/guide/router/*" users: - jasonaden #primary - - vicb - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only + + testing: + conditions: + files: + - "*/testing/*" + - "aio/content/guide/testing.md" + - "aio/content/examples/testing/*" + - "aio/content/images/guide/testing/*" + users: + - vikerman + - IgorMinar #fallback + - mhevery #fallback + - jenniferfell #docs only upgrade: conditions: files: - "packages/upgrade/*" + - "aio/content/guide/upgrade.md" + - "aio/content/examples/upgrade-module/*" + - "aio/content/images/guide/upgrade/*" + - "aio/content/examples/upgrade-phonecat-1-typescript/*" + - "aio/content/examples/upgrade-phonecat-2-hybrid/*" + - "aio/content/examples/upgrade-phonecat-3-final/*" + - "aio/content/guide/upgrade-performance.md" + - "aio/content/guide/ajs-quick-reference.md" + - "aio/content/examples/ajs-quick-reference/*" users: - petebacondarwin #primary - gkalpak - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only platform-browser: conditions: files: - "packages/platform-browser/*" users: - - vicb #primary + - mhevery #primary # needs secondary - IgorMinar #fallback - - mhevery #fallback platform-server: conditions: files: - "packages/platform-server/*" + - "aio/content/guide/universal.md" + - "aio/content/examples/universal/*" users: - vikerman #primary - alxhub #secondary - - vicb - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only platform-webworker: conditions: files: - "packages/platform-webworker/*" users: - - vicb #primary + - mhevery #primary # needs secondary - IgorMinar #fallback - - mhevery #fallback service-worker: conditions: files: - "packages/service-worker/*" + - "aio/content/guide/service-worker-getting-started.md" + - "aio/content/examples/service-worker-getting-started/*" + - "aio/content/guide/service-worker-communications.md" + - "aio/content/guide/service-worker-config.md" + - "aio/content/guide/service-worker-devops.md" + - "aio/content/guide/service-worker-intro.md" + - "aio/content/images/guide/service-worker/*" users: - - alxhub #primary - - gkalpak - - IgorMinar #fallback + - gkalpak #primary + - alxhub + - IgorMinar - mhevery #fallback + - jenniferfell #docs only elements: conditions: files: - "packages/elements/*" + - "aio/content/examples/elements/*" + - "aio/content/images/guide/elements/*" + - "aio/content/guide/elements.md" users: - andrewseguin #primary - gkalpak - robwormald - IgorMinar #fallback - mhevery #fallback + - jenniferfell #docs only benchpress: conditions: @@ -323,7 +442,7 @@ groups: - IgorMinar #fallback - mhevery #fallback - angular.io: + docs-infra: conditions: files: include: @@ -336,7 +455,7 @@ groups: - gkalpak - mhevery #fallback - angular.io-guide-and-tutorial: + docs/guide-and-tutorial: conditions: files: include: @@ -346,19 +465,20 @@ groups: - "aio/content/navigation.json" - "aio/content/license.md" users: - - kapunahelewong - stephenfluin + - jenniferfell + - brandonroberts - petebacondarwin - gkalpak - IgorMinar - - brandonroberts - mhevery #fallback - angular.io-marketing: + docs/marketing: conditions: files: include: - "aio/content/marketing/*" + - "aio/content/images/marketing/*" - "aio/content/navigation.json" - "aio/content/license.md" users: @@ -368,3 +488,43 @@ groups: - IgorMinar - robwormald - mhevery #fallback + + docs/observables: + conditions: + files: + - "aio/content/examples/observables/*" + - "aio/content/images/guide/observables/*" + - "aio/content/guide/observables.md" + - "aio/content/guide/comparing-observables.md" + - "aio/content/examples/observables-in-angular/*" + - "aio/content/images/guide/observables-in-angular/*" + - "aio/content/guide/observables-in-angular.md" + - "aio/content/examples/practical-observable-usage/*" + - "aio/content/guide/practical-observable-usage.md" + - "aio/content/examples/rx-library/*" + - "aio/content/guide/rx-library.md" + users: + - jasonaden + - benlesh + - IgorMinar + - mhevery + - jenniferfell #docs only + + docs/packaging: + conditions: + files: + - "aio/content/guide/npm-packages.md" + - "aio/content/guide/browser-support.md" + - "aio/content/guide/typescript-configuration.md" + - "aio/content/guide/setup-systemjs-anatomy.md" + - "aio/content/examples/setup/*" + - "aio/content/guide/setup.md" + - "aio/content/guide/deployment.md" + - "aio/content/guide/releases.md" + - "aio/content/guide/updating.md" + users: + - IgorMinar #primary + - alexeagle + - hansl + - mhevery #fallback + - jenniferfell #docs only diff --git a/.travis.yml b/.travis.yml index e06b84fa54..57cda7bfa2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js sudo: false dist: trusty node_js: - - '8.9.1' + - '10.9.0' addons: # firefox: "38.0" @@ -13,11 +13,7 @@ addons: packages: # needed to install g++ that is used by npms's native modules - g++-4.8 - # https://docs.travis-ci.com/user/jwt - jwt: - # SAUCE_ACCESS_KEY<=secret for NGBUILDS_IO_KEY to work around travis-ci/travis-ci#7223, unencrypted value in valentine as NGBUILDS_IO_KEY> - # we alias NGBUILDS_IO_KEY to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there - - secure: "L7nrZwkAtFtYrP2DykPXgZvEKjkv0J/TwQ/r2QGxFTaBq4VZn+2Dw0YS7uCxoMqYzDwH0aAOqxoutibVpk8Z/16nE3tNmU5RzltMd6Xmt3qU2f/JDQLMo6PSlBodnjOUsDHJgmtrcbjhqrx/znA237BkNUu6UZRT7mxhXIZpn0U=" + branches: except: - g3 @@ -53,12 +49,14 @@ env: - CI_MODE=browserstack_optional - CI_MODE=aio_tools_test - CI_MODE=aio + - CI_MODE=aio_local - CI_MODE=aio_e2e AIO_SHARD=0 - CI_MODE=aio_e2e AIO_SHARD=1 matrix: fast_finish: true allow_failures: + - env: "CI_MODE=aio_local" - env: "CI_MODE=saucelabs_optional" - env: "CI_MODE=browserstack_optional" diff --git a/BUILD.bazel b/BUILD.bazel index 74ef2eb41f..ed2e3b7975 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -36,7 +36,9 @@ 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", ], diff --git a/CHANGELOG.md b/CHANGELOG.md index ca772990c6..500b066133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,155 @@ + +# [7.0.0](https://github.com/angular/angular/compare/7.0.0-rc.1...7.0.0) (2018-10-18) + + +### Release Highlights & Update instructions + +To learn about the release highlights and our new CLI-powered update workflow for your projects please check out the [v7 release announcement](https://blog.angular.io/version-7-of-angular-cli-prompts-virtual-scroll-drag-and-drop-and-more-c594e22e7b8c). + + +### Dependency updates + +* @angular/core now depends on + * TypeScript 3.1 + * RxJS 6.3 +* @angular/platform-server now depends on Domino 2.1 + + +### Features + +* **core:** add DoBootstrap interface. ([#24558](https://github.com/angular/angular/issues/24558)) ([732026c](https://github.com/angular/angular/commit/732026c)), closes [#24557](https://github.com/angular/angular/issues/24557) +* **compiler:** add "original" placeholder value on extracted XMB ([#25079](https://github.com/angular/angular/issues/25079)) ([e99d860](https://github.com/angular/angular/commit/e99d860)) +* **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) +* **bazel:** add additional parameters to `ts_api_guardian_test` def ([#25694](https://github.com/angular/angular/issues/25694)) ([2a21ca0](https://github.com/angular/angular/commit/2a21ca0)) +* **elements:** enable Shadow DOM v1 and slots ([#24861](https://github.com/angular/angular/issues/24861)) ([c9844a2](https://github.com/angular/angular/commit/c9844a2)) +* **platform-server:** update domino to v2.1.0 ([#25564](https://github.com/angular/angular/issues/25564)) ([3fb0da2](https://github.com/angular/angular/commit/3fb0da2)) +* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([010e35d](https://github.com/angular/angular/commit/010e35d)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728) +* **router:** add UrlSegment[] to CanLoad interface ([#13127](https://github.com/angular/angular/issues/13127)) ([07d8d39](https://github.com/angular/angular/commit/07d8d39)), closes [#12411](https://github.com/angular/angular/issues/12411) + + + +### Bug Fixes + +* add mappings for ngfactory & ngsummary files to their module names in aot summary resolver ([#25335](https://github.com/angular/angular/issues/25335)) ([02e201a](https://github.com/angular/angular/commit/02e201a)) +* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([f394ba0](https://github.com/angular/angular/commit/f394ba0)) +* **bazel:** allow compile_strategy to be (privately) imported ([#25080](https://github.com/angular/angular/issues/25080)) ([0d1d589](https://github.com/angular/angular/commit/0d1d589)) +* **bazel:** correct type concatenated to devmode_js ([#25467](https://github.com/angular/angular/issues/25467)) ([fb2c524](https://github.com/angular/angular/commit/fb2c524)) +* **bazel:** move bazel managed runtime deps for downstream usage ([#25690](https://github.com/angular/angular/issues/25690)) ([6ed7993](https://github.com/angular/angular/commit/6ed7993)) +* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([42072c4](https://github.com/angular/angular/commit/42072c4)) +* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([3809e0f](https://github.com/angular/angular/commit/3809e0f)) +* **bazel:** specify the package and lock files using the workspace ([#25694](https://github.com/angular/angular/issues/25694)) ([ddc1335](https://github.com/angular/angular/commit/ddc1335)) +* **benchpress:** Use performance.mark() instead of console.time() ([#24114](https://github.com/angular/angular/issues/24114)) ([06d0400](https://github.com/angular/angular/commit/06d0400)) +* **common:** register locale data for all equivalent closure locales ([#25867](https://github.com/angular/angular/issues/25867)) ([d83f9d4](https://github.com/angular/angular/commit/d83f9d4)) +* **compiler-cli:** correct realPath to realpath. ([#25023](https://github.com/angular/angular/issues/25023)) ([01e6dab](https://github.com/angular/angular/commit/01e6dab)) +* **compiler-cli:** use the oldProgram option in watch mode ([#21364](https://github.com/angular/angular/issues/21364)) ([c6e5b97](https://github.com/angular/angular/commit/c6e5b97)), closes [#21361](https://github.com/angular/angular/issues/21361) +* **compiler:** Fix look up of entryComponents in AOT Summaries ([#24892](https://github.com/angular/angular/issues/24892)) ([00d3666](https://github.com/angular/angular/commit/00d3666)) +* **compiler:** add hostVars and support pure functions in host bindings ([#25626](https://github.com/angular/angular/issues/25626)) ([b424b31](https://github.com/angular/angular/commit/b424b31)) +* **compiler:** update compiler to flatten nested template fns ([#24943](https://github.com/angular/angular/issues/24943)) ([fe14f18](https://github.com/angular/angular/commit/fe14f18)) +* **compiler:** update compiler to generate new slot allocations ([#25607](https://github.com/angular/angular/issues/25607)) ([27e2039](https://github.com/angular/angular/commit/27e2039)) +* **core:** In Testability.whenStable update callback, pass more complete ([#25010](https://github.com/angular/angular/issues/25010)) ([16c03c0](https://github.com/angular/angular/commit/16c03c0)) +* **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) +* **core:** allow null value for renderer setElement(…) ([#17065](https://github.com/angular/angular/issues/17065)) ([ff15043](https://github.com/angular/angular/commit/ff15043)), closes [#13686](https://github.com/angular/angular/issues/13686) +* **core:** do not clear element content when using shadow dom ([#24861](https://github.com/angular/angular/issues/24861)) ([6e828bb](https://github.com/angular/angular/commit/6e828bb)) +* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([1f59f2f](https://github.com/angular/angular/commit/1f59f2f)) +* **core:** throw error message when @Output not initialized ([#19116](https://github.com/angular/angular/issues/19116)) ([adf510f](https://github.com/angular/angular/commit/adf510f)), closes [#3664](https://github.com/angular/angular/issues/3664) +* **elements:** add compiler dependency ([#24861](https://github.com/angular/angular/issues/24861)) ([6143da6](https://github.com/angular/angular/commit/6143da6)) +* **elements:** add compiler to integration ([#24861](https://github.com/angular/angular/issues/24861)) ([a080ffc](https://github.com/angular/angular/commit/a080ffc)) +* **elements:** strict null checks ([#24861](https://github.com/angular/angular/issues/24861)) ([a8210d0](https://github.com/angular/angular/commit/a8210d0)) +* **router:** fix regression where navigateByUrl promise didn't resolve on CanLoad failure ([#26455](https://github.com/angular/angular/issues/26455)) ([1c9b065](https://github.com/angular/angular/commit/1c9b065)), closes [#26284](https://github.com/angular/angular/issues/26284) +* **router:** mount correct component if router outlet was not instantiated and if using a route reuse strategy ([#25313](https://github.com/angular/angular/issues/25313)) ([#25314](https://github.com/angular/angular/issues/25314)) ([8dc2b11](https://github.com/angular/angular/commit/8dc2b11)) +* **router:** take base uri into account in `setUpLocationSync()` ([#20244](https://github.com/angular/angular/issues/20244)) ([ba1e25f](https://github.com/angular/angular/commit/ba1e25f)), closes [#20061](https://github.com/angular/angular/issues/20061) +* **service-worker:** clean up caches from old SW versions ([#26319](https://github.com/angular/angular/issues/26319)) ([00b5c7b](https://github.com/angular/angular/commit/00b5c7b)) +* **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)) +* **upgrade:** properly destroy upgraded component elements and descendants ([#26209](https://github.com/angular/angular/issues/26209)) ([071934e](https://github.com/angular/angular/commit/071934e)), closes [#26208](https://github.com/angular/angular/issues/26208) +* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([2a672a9](https://github.com/angular/angular/commit/2a672a9)), closes [#25334](https://github.com/angular/angular/issues/25334) + + + + + +## [6.1.10](https://github.com/angular/angular/compare/6.1.9...6.1.10) (2018-10-10) + + +### Bug Fixes + +* **platform-browser:** fix [#22155](https://github.com/angular/angular/issues/22155), destroy hammer manager when `HammerInstance.off()` is run ([#22156](https://github.com/angular/angular/issues/22156)) ([3b4d9dc](https://github.com/angular/angular/commit/3b4d9dc)) +* **upgrade:** properly destroy upgraded component elements and descendants ([#26209](https://github.com/angular/angular/issues/26209)) ([623adbb](https://github.com/angular/angular/commit/623adbb)), closes [#26208](https://github.com/angular/angular/issues/26208) + + + + + +## [6.1.9](https://github.com/angular/angular/compare/6.1.8...6.1.9) (2018-09-26) + + + + + +## [6.1.7](https://github.com/angular/angular/compare/6.1.6...6.1.7) (2018-09-06) + + +### Bug Fixes + +* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([ed6b68b](https://github.com/angular/angular/commit/ed6b68b)) +* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762)) +* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870)) +* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334) +* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728) + + + + +## [6.1.6](https://github.com/angular/angular/compare/6.1.5...6.1.6) (2018-08-29) + + +### Bug Fixes + +* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([3e690e0](https://github.com/angular/angular/commit/3e690e0)) +* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([7aff364](https://github.com/angular/angular/commit/7aff364)) + + +Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 instead. sorry! :-) + + + + +## [6.1.4](https://github.com/angular/angular/compare/6.1.3...6.1.4) (2018-08-22) + + +### Bug Fixes + +* **router:** default scroll position restoration to disabled ([#25586](https://github.com/angular/angular/issues/25586)) ([7e61645](https://github.com/angular/angular/commit/7e61645)), closes [#25145](https://github.com/angular/angular/issues/25145) + + + + +## [6.1.3](https://github.com/angular/angular/compare/6.1.2...6.1.3) (2018-08-15) + + +### Bug Fixes + +* **service-worker:** `Cache-Control: no-cache` on assets breaks service worker ([#25408](https://github.com/angular/angular/issues/25408)) ([1319ff4](https://github.com/angular/angular/commit/1319ff4)), closes [#25442](https://github.com/angular/angular/issues/25442) + + + + +## [6.1.2](https://github.com/angular/angular/compare/6.1.1...6.1.2) (2018-08-08) + + +### Bug Fixes + +* **router:** take base uri into account in `setUpLocationSync()` ([#20244](https://github.com/angular/angular/issues/20244)) ([ae9b4e6](https://github.com/angular/angular/commit/ae9b4e6)), closes [#20061](https://github.com/angular/angular/issues/20061) +* add mappings for ngfactory & ngsummary files to their module names in aot summary resolver ([#25335](https://github.com/angular/angular/issues/25335)) ([054fbbe](https://github.com/angular/angular/commit/054fbbe)) + + + + +## [6.1.1](https://github.com/angular/angular/compare/6.1.0...6.1.1) (2018-08-02) + +* **compiler-cli:** correct tsickle dependency version to fix typescript 2.9 compatibility ([fec29fa](https://github.com/angular/angular/commit/317c7087c56b72aa74cd6d6a8f719e6e7fec29fa)) + + + # [6.1.0](https://github.com/angular/angular/compare/6.0.0-rc.5...6.1.0) (2018-07-25) @@ -16,26 +168,19 @@ * **common:** format fractional seconds ([#24844](https://github.com/angular/angular/issues/24844)) ([0b4d85e](https://github.com/angular/angular/commit/0b4d85e)), closes [#24831](https://github.com/angular/angular/issues/24831) * **common:** properly update collection reference in NgForOf ([#24684](https://github.com/angular/angular/issues/24684)) ([ff84c5c](https://github.com/angular/angular/commit/ff84c5c)), closes [#24155](https://github.com/angular/angular/issues/24155) * **common:** use correct currency format for locale de-AT ([#24658](https://github.com/angular/angular/issues/24658)) ([dcabb05](https://github.com/angular/angular/commit/dcabb05)), closes [#24609](https://github.com/angular/angular/issues/24609) -* **common:** do not round factional seconds ([#24831](https://github.com/angular/angular/issues/24831)) ([a527c69](https://github.com/angular/angular/commit/a527c69)), closes [#24384](https://github.com/angular/angular/issues/24384) -* **common:** properly update collection reference in NgForOf ([#24684](https://github.com/angular/angular/issues/24684)) ([ff84c5c](https://github.com/angular/angular/commit/ff84c5c)), closes [#24155](https://github.com/angular/angular/issues/24155) -* **common:** use correct currency format for locale de-AT ([#24658](https://github.com/angular/angular/issues/24658)) ([dcabb05](https://github.com/angular/angular/commit/dcabb05)), closes [#24609](https://github.com/angular/angular/issues/24609) * **common:** use correct ICU plural for locale mk ([#24659](https://github.com/angular/angular/issues/24659)) ([64a8584](https://github.com/angular/angular/commit/64a8584)) * **compiler:** fix a few non-tree-shakeable code patterns ([#24677](https://github.com/angular/angular/issues/24677)) ([50d4a4f](https://github.com/angular/angular/commit/50d4a4f)) * **compiler:** i18n_extractor now outputs the correct source file name ([#24885](https://github.com/angular/angular/issues/24885)) ([c8ad965](https://github.com/angular/angular/commit/c8ad965)), closes [#24884](https://github.com/angular/angular/issues/24884) -* **compiler:** fix a few non-tree-shakeable code patterns ([#24677](https://github.com/angular/angular/issues/24677)) ([50d4a4f](https://github.com/angular/angular/commit/50d4a4f)) * **compiler:** support `.` in import statements. ([#20634](https://github.com/angular/angular/issues/20634)) ([d8f7b29](https://github.com/angular/angular/commit/d8f7b29)), closes [#20363](https://github.com/angular/angular/issues/20363) * **compiler:** avoid a crash in ngc-wrapped. ([#23468](https://github.com/angular/angular/issues/23468)) ([e1c4930](https://github.com/angular/angular/commit/e1c4930)) * **compiler:** generate constant array for i18n attributes ([#23837](https://github.com/angular/angular/issues/23837)) ([cfde36d](https://github.com/angular/angular/commit/cfde36d)) * **compiler:** generate core-compliant hostBindings property ([#24087](https://github.com/angular/angular/issues/24087)) ([01b5acd](https://github.com/angular/angular/commit/01b5acd)), closes [#24013](https://github.com/angular/angular/issues/24013) * **compiler:** handle undefined annotation metadata ([#23349](https://github.com/angular/angular/issues/23349)) ([ca776c5](https://github.com/angular/angular/commit/ca776c5)) * **compiler-cli:** Use typescript to resolve modules for metadata ([#22856](https://github.com/angular/angular/issues/22856)) ([0d5f2d3](https://github.com/angular/angular/commit/0d5f2d3)) -* **compiler-cli:** Use typescript to resolve modules for metadata ([#22856](https://github.com/angular/angular/issues/22856)) ([0d5f2d3](https://github.com/angular/angular/commit/0d5f2d3)) * **compiler-cli:** don't rely on incompatible TS method ([#23550](https://github.com/angular/angular/issues/23550)) ([b1f040f](https://github.com/angular/angular/commit/b1f040f)) * **core:** stop reusing provider definitions across NgModuleRef instances ([#25022](https://github.com/angular/angular/issues/25022)) ([6b859da](https://github.com/angular/angular/commit/6b859da)), closes [#25018](https://github.com/angular/angular/issues/25018) * **core:** mark NgModule as not the root if APP_ROOT is set to false ([#24814](https://github.com/angular/angular/issues/24814)) ([1089261](https://github.com/angular/angular/commit/1089261)) * **core:** use addCustomEqualityTester instead of overriding toEqual ([#22983](https://github.com/angular/angular/issues/22983)) ([0922228](https://github.com/angular/angular/commit/0922228)), closes [#22939](https://github.com/angular/angular/issues/22939) -* **core:** mark NgModule as not the root if APP_ROOT is set to false ([#24814](https://github.com/angular/angular/issues/24814)) ([1089261](https://github.com/angular/angular/commit/1089261)) -* **core:** use addCustomEqualityTester instead of overriding toEqual ([#22983](https://github.com/angular/angular/issues/22983)) ([0922228](https://github.com/angular/angular/commit/0922228)), closes [#22939](https://github.com/angular/angular/issues/22939) * **core:** Injector correctly honors the @Self flag ([#24520](https://github.com/angular/angular/issues/24520)) ([ccbda9d](https://github.com/angular/angular/commit/ccbda9d)) * **core:** avoid eager providers re-initialization ([#23559](https://github.com/angular/angular/issues/23559)) ([0c6dc45](https://github.com/angular/angular/commit/0c6dc45)) * **core:** call ngOnDestroy on all services that have it ([#23755](https://github.com/angular/angular/issues/23755)) ([fc03427](https://github.com/angular/angular/commit/fc03427)), closes [#22466](https://github.com/angular/angular/issues/22466) [#22240](https://github.com/angular/angular/issues/22240) [#14818](https://github.com/angular/angular/issues/14818) @@ -44,10 +189,6 @@ * **elements:** prevent closure renaming of platform properties ([#23843](https://github.com/angular/angular/issues/23843)) ([d4b8b24](https://github.com/angular/angular/commit/d4b8b24)) * **forms:** properly handle special properties in FormGroup.get ([#22249](https://github.com/angular/angular/issues/22249)) ([9367e91](https://github.com/angular/angular/commit/9367e91)), closes [#17195](https://github.com/angular/angular/issues/17195) * **language-service:** do not overwrite native `Reflect` ([#24299](https://github.com/angular/angular/issues/24299)) ([6881404](https://github.com/angular/angular/commit/6881404)), closes [#21420](https://github.com/angular/angular/issues/21420) -* **language-service:** do not overwrite native `Reflect` ([#24299](https://github.com/angular/angular/issues/24299)) ([6881404](https://github.com/angular/angular/commit/6881404)), closes [#21420](https://github.com/angular/angular/issues/21420) -* **platform-browser:** add missing deps for HammerGesturesPlugin ([#24682](https://github.com/angular/angular/issues/24682)) ([13d60ea](https://github.com/angular/angular/commit/13d60ea)) -* **platform-browser:** mark Meta and Title services as tree shakable providers ([#24815](https://github.com/angular/angular/issues/24815)) ([197387d](https://github.com/angular/angular/commit/197387d)) -* **platform-browser:** workaround wrong import path generated by ngc for DOCUMENT ([#24830](https://github.com/angular/angular/issues/24830)) ([7d27ecc](https://github.com/angular/angular/commit/7d27ecc)) * **platform-browser:** add missing deps for HammerGesturesPlugin ([#24682](https://github.com/angular/angular/issues/24682)) ([13d60ea](https://github.com/angular/angular/commit/13d60ea)) * **platform-browser:** mark Meta and Title services as tree shakable providers ([#24815](https://github.com/angular/angular/issues/24815)) ([197387d](https://github.com/angular/angular/commit/197387d)) * **platform-browser:** workaround wrong import path generated by ngc for DOCUMENT ([#24830](https://github.com/angular/angular/issues/24830)) ([7d27ecc](https://github.com/angular/angular/commit/7d27ecc)) @@ -57,14 +198,12 @@ * **platform-server:** provide Domino DOM types globally ([#24116](https://github.com/angular/angular/issues/24116)) ([c73196e](https://github.com/angular/angular/commit/c73196e)), closes [#23280](https://github.com/angular/angular/issues/23280) [#23133](https://github.com/angular/angular/issues/23133) * **router:** Fix _lastPathIndex in deeply nested empty paths ([#22394](https://github.com/angular/angular/issues/22394)) ([968f153](https://github.com/angular/angular/commit/968f153)) * **router:** add ability to recover from malformed url ([#23283](https://github.com/angular/angular/issues/23283)) ([86d254d](https://github.com/angular/angular/commit/86d254d)), closes [#21468](https://github.com/angular/angular/issues/21468) -* **router:** add ability to recover from malformed url ([#23283](https://github.com/angular/angular/issues/23283)) ([86d254d](https://github.com/angular/angular/commit/86d254d)), closes [#21468](https://github.com/angular/angular/issues/21468) * **router:** fix lazy loading of aux routes ([#23459](https://github.com/angular/angular/issues/23459)) ([5731d07](https://github.com/angular/angular/commit/5731d07)), closes [#10981](https://github.com/angular/angular/issues/10981) * **router:** avoid freezing queryParams in-place ([#22663](https://github.com/angular/angular/issues/22663)) ([89f64e5](https://github.com/angular/angular/commit/89f64e5)), closes [#22617](https://github.com/angular/angular/issues/22617) * **router:** cache route handle if found ([#22475](https://github.com/angular/angular/issues/22475)) ([4cfa571](https://github.com/angular/angular/commit/4cfa571)), closes [#22474](https://github.com/angular/angular/issues/22474) * **router:** correct the segment parsing so it won't break on ampersand ([#23684](https://github.com/angular/angular/issues/23684)) ([553a680](https://github.com/angular/angular/commit/553a680)) * **service-worker:** don't include sourceMappingURL in ngsw-worker ([#24877](https://github.com/angular/angular/issues/24877)) ([8620373](https://github.com/angular/angular/commit/8620373)), closes [#23596](https://github.com/angular/angular/issues/23596) * **service-worker:** avoid network requests when looking up hashed resources in cache ([#24127](https://github.com/angular/angular/issues/24127)) ([52d43a9](https://github.com/angular/angular/commit/52d43a9)) -* **service-worker:** avoid network requests when looking up hashed resources in cache ([#24127](https://github.com/angular/angular/issues/24127)) ([52d43a9](https://github.com/angular/angular/commit/52d43a9)) * **service-worker:** fix `SwPush.unsubscribe()` ([#24162](https://github.com/angular/angular/issues/24162)) ([3ed2d75](https://github.com/angular/angular/commit/3ed2d75)), closes [#24095](https://github.com/angular/angular/issues/24095) * **service-worker:** add badge to NOTIFICATION_OPTION_NAMES ([#23241](https://github.com/angular/angular/issues/23241)) ([fb59b2d](https://github.com/angular/angular/commit/fb59b2d)), closes [#23196](https://github.com/angular/angular/issues/23196) * **service-worker:** check platformBrowser before accessing navigator.serviceWorker ([#21231](https://github.com/angular/angular/issues/21231)) ([0bdd30e](https://github.com/angular/angular/commit/0bdd30e)) @@ -82,9 +221,6 @@ * **core:** expose a Compiler API for accessing module ids from NgModule types ([#24258](https://github.com/angular/angular/issues/24258)) ([bd02b27](https://github.com/angular/angular/commit/bd02b27)) * **core:** KeyValueDiffer#diff allows null values ([#24319](https://github.com/angular/angular/issues/24319)) ([52ce9d5](https://github.com/angular/angular/commit/52ce9d5)) * **core:** add support for ShadowDOM v1 ([#24718](https://github.com/angular/angular/issues/24718)) ([3553977](https://github.com/angular/angular/commit/3553977)) -* **core:** add support for using async/await with Jasmine ([#24637](https://github.com/angular/angular/issues/24637)) ([71100e6](https://github.com/angular/angular/commit/71100e6)) -* **core:** add support for ShadowDOM v1 ([#24718](https://github.com/angular/angular/issues/24718)) ([3553977](https://github.com/angular/angular/commit/3553977)) -* **core:** add support for using async/await with Jasmine ([#24637](https://github.com/angular/angular/issues/24637)) ([71100e6](https://github.com/angular/angular/commit/71100e6)) (https://github.com/angular/angular/commit/328971f)), closes [#24616](https://github.com/angular/angular/issues/24616) * **platform-browser:** add HammerJS lazy-loader symbols to public API ([#23943](https://github.com/angular/angular/issues/23943)) ([26fbf1d](https://github.com/angular/angular/commit/26fbf1d)) * **platform-browser:** allow lazy-loading HammerJS ([#23906](https://github.com/angular/angular/issues/23906)) ([313bdce](https://github.com/angular/angular/commit/313bdce)) @@ -99,7 +235,7 @@ * **bazel:** turn on preserve-symlinks ([#24881](https://github.com/angular/angular/issues/24881)) ([c438b5e](https://github.com/angular/angular/commit/c438b5e)) -### BREAKING CHANGES +### Angular Labs (experimental feature) breaking change * **bazel:** Use of @angular/bazel rules now requires calling ng_setup_workspace() in your WORKSPACE file. @@ -247,7 +383,6 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **animations:** only use the WA-polyfill alongside AnimationBuilder ([#22143](https://github.com/angular/angular/issues/22143)) ([b2f366b](https://github.com/angular/angular/commit/b2f366b)), closes [#17496](https://github.com/angular/angular/issues/17496) * **animations:** expose `element` and `params` within transition matchers ([#22693](https://github.com/angular/angular/issues/22693)) ([58b94e6](https://github.com/angular/angular/commit/58b94e6)) * **common:** better error message when non-template element used in NgIf ([#22274](https://github.com/angular/angular/issues/22274)) ([67cf11d](https://github.com/angular/angular/commit/67cf11d)), closes [#16410](https://github.com/angular/angular/issues/16410) -* **common:** better error message when non-template element used in NgIf ([#22274](https://github.com/angular/angular/issues/22274)) ([67cf11d](https://github.com/angular/angular/commit/67cf11d)), closes [#16410](https://github.com/angular/angular/issues/16410) * **common:** export functions to format numbers, percents, currencies & dates ([#22423](https://github.com/angular/angular/issues/22423)) ([4180912](https://github.com/angular/angular/commit/4180912)), closes [#20536](https://github.com/angular/angular/issues/20536) * **compiler:** lower @NgModule ids if needed ([#23031](https://github.com/angular/angular/issues/23031)) ([bd024c0](https://github.com/angular/angular/commit/bd024c0)) * **compiler:** implement "enableIvy" compiler option ([#21427](https://github.com/angular/angular/issues/21427)) ([64d16de](https://github.com/angular/angular/commit/64d16de)) @@ -287,7 +422,6 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **animations:** report correct totalTime value even during noOp animations ([#22225](https://github.com/angular/angular/issues/22225)) ([e1bf067](https://github.com/angular/angular/commit/e1bf067)) * **animations:** avoid animation insertions during router back/refresh ([#21977](https://github.com/angular/angular/issues/21977)) ([f88fba0](https://github.com/angular/angular/commit/f88fba0)), closes [#19712](https://github.com/angular/angular/issues/19712) * **animations:** treat numeric state name values as strings ([#22923](https://github.com/angular/angular/issues/22923)) ([e5e1b0d](https://github.com/angular/angular/commit/e5e1b0d)) -* **animations:** report correct totalTime value even during noOp animations ([#22225](https://github.com/angular/angular/issues/22225)) ([e1bf067](https://github.com/angular/angular/commit/e1bf067)) * **animations:** fix increment/decrement aliases example ([#18323](https://github.com/angular/angular/issues/18323)) ([d2aa8ac](https://github.com/angular/angular/commit/d2aa8ac)) * **common:** NgClass should properly take className changes into account ([#21937](https://github.com/angular/angular/issues/21937)) ([4a42669](https://github.com/angular/angular/commit/4a42669)), closes [#21932](https://github.com/angular/angular/issues/21932) * **common:** fix the titlecase pipe ([#22600](https://github.com/angular/angular/issues/22600)) ([7966744](https://github.com/angular/angular/commit/7966744)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44142a7d68..31d5ba6065 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,6 +71,8 @@ Before you submit your Pull Request (PR) consider the following guidelines: 1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate effort. +1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. + Discussing the design up front helps to ensure that we're ready to accept your work. 1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs. We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository. 1. Fork the angular/angular repo. diff --git a/WORKSPACE b/WORKSPACE index ea4eaaa5dd..7664320bbd 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -3,52 +3,60 @@ workspace(name = "angular") # # Download Bazel toolchain dependencies as needed by build actions # - -http_archive( - name = "build_bazel_rules_nodejs", - url = "https://github.com/bazelbuild/rules_nodejs/archive/20ff5892612f8359aec8aaf26dd3902a24976ada.zip", - strip_prefix = "rules_nodejs-20ff5892612f8359aec8aaf26dd3902a24976ada", - sha256 = "07da9d4c3e688a02745d0f50709a87744706d4f5d1959b799b0ac38e97acd622", -) - -http_archive( - name = "io_bazel_rules_webtesting", - url = "https://github.com/bazelbuild/rules_webtesting/archive/7ffe970bbf380891754487f66c3d680c087d67f2.zip", - strip_prefix = "rules_webtesting-7ffe970bbf380891754487f66c3d680c087d67f2", - sha256 = "4fb0dca8c9a90547891b7ef486592775a523330fc4555c88cd8f09270055c2ce", -) - http_archive( name = "build_bazel_rules_typescript", - url = "https://github.com/bazelbuild/rules_typescript/archive/0.15.3.zip", - strip_prefix = "rules_typescript-0.15.3", - sha256 = "a2b26ac3fc13036011196063db1bf7f1eae81334449201dc28087ebfa3708c99", + sha256 = "1626ee2cc9770af6950bfc77dffa027f9aedf330fe2ea2ee7e504428927bd95d", + strip_prefix = "rules_typescript-0.17.0", + url = "https://github.com/bazelbuild/rules_typescript/archive/0.17.0.zip", +) + +load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies") + +rules_typescript_dependencies() + +http_archive( + name = "bazel_toolchains", + sha256 = "c3b08805602cd1d2b67ebe96407c1e8c6ed3d4ce55236ae2efe2f1948f38168d", + strip_prefix = "bazel-toolchains-5124557861ebf4c0b67f98180bff1f8551e0b421", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz", + "https://github.com/bazelbuild/bazel-toolchains/archive/5124557861ebf4c0b67f98180bff1f8551e0b421.tar.gz", + ], ) http_archive( - name = "io_bazel_rules_go", - url = "https://github.com/bazelbuild/rules_go/releases/download/0.10.3/rules_go-0.10.3.tar.gz", - sha256 = "feba3278c13cde8d67e341a837f69a029f698d7a27ddbb2a202be7a10b22142a", + name = "io_bazel_rules_sass", + sha256 = "dbe9fb97d5a7833b2a733eebc78c9c1e3880f676ac8af16e58ccf2139cbcad03", + strip_prefix = "rules_sass-1.11.0", + url = "https://github.com/bazelbuild/rules_sass/archive/1.11.0.zip", ) # 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 = "82b21607e00913b16fe1c51bec80232d9d6de31c" +BAZEL_BUILDTOOLS_VERSION = "49a6c199e3fbf5d94534b2771868677d3f9c6de9" http_archive( name = "com_github_bazelbuild_buildtools", - url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION, + sha256 = "edf39af5fc257521e4af4c40829fffe8fba6d0ebff9f4dd69a6f8f1223ae047b", strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION, - sha256 = "edb24c2f9c55b10a820ec74db0564415c0cf553fa55e9fc709a6332fb6685eff", + url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION, ) # Fetching the Bazel source code allows us to compile the Skylark linter http_archive( name = "io_bazel", - url = "https://github.com/bazelbuild/bazel/archive/968f87900dce45a7af749a965b72dbac51b176b3.zip", - strip_prefix = "bazel-968f87900dce45a7af749a965b72dbac51b176b3", - sha256 = "e373d2ae24955c1254c495c9c421c009d88966565c35e4e8444c082cb1f0f48f", + sha256 = "ace8cced3b21e64a8fdad68508e9b0644201ec848ad583651719841d567fc66d", + strip_prefix = "bazel-0.17.1", + url = "https://github.com/bazelbuild/bazel/archive/0.17.1.zip", +) + +http_archive( + name = "io_bazel_skydoc", + sha256 = "7bfb5545f59792a2745f2523b9eef363f9c3e7274791c030885e7069f8116016", + strip_prefix = "skydoc-fe2e9f888d28e567fef62ec9d4a93c425526d701", + # TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged + url = "https://github.com/alexeagle/skydoc/archive/fe2e9f888d28e567fef62ec9d4a93c425526d701.zip", ) # We have a source dependency on the Devkit repository, because it's built with @@ -59,16 +67,16 @@ http_archive( # 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", + strip_prefix = "angular-cli-6.1.0-rc.0", + url = "https://github.com/angular/angular-cli/archive/v6.1.0-rc.0.zip", ) http_archive( name = "org_brotli", - url = "https://github.com/google/brotli/archive/f9b8c02673c576a3e807edbf3a9328e9e7af6d7c.zip", - strip_prefix = "brotli-f9b8c02673c576a3e807edbf3a9328e9e7af6d7c", - sha256 = "8a517806d2b7c8505ba5c53934e7d7c70d341b68ffd268e9044d35b564a48828", + sha256 = "774b893a0700b0692a76e2e5b7e7610dbbe330ffbe3fe864b4b52ca718061d5a", + strip_prefix = "brotli-1.0.5", + url = "https://github.com/google/brotli/archive/v1.0.5.zip", ) # @@ -91,22 +99,31 @@ 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", """ +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 + +""") -check_bazel_version("0.15.0") node_repositories( - package_json = ["//:package.json"], - preserve_symlinks = True, + node_version = "10.9.0", + package_json = ["//:package.json"], + preserve_symlinks = True, + yarn_version = "1.9.2", ) 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, @@ -120,20 +137,24 @@ load("@angular//:index.bzl", "ng_setup_workspace") ng_setup_workspace() -# -# Ask Bazel to manage these toolchain dependencies for us. -# Bazel will run `yarn install` when one of these toolchains is requested during -# a build. -# +################################## +# Skylark documentation generation -yarn_install( - name = "ts-api-guardian_runtime_deps", - package_json = "//tools/ts-api-guardian:package.json", - yarn_lock = "//tools/ts-api-guardian:yarn.lock", -) +load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories") -yarn_install( - name = "http-server_runtime_deps", - package_json = "//tools/http-server:package.json", - yarn_lock = "//tools/http-server:yarn.lock", +sass_repositories() + +load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories") + +skydoc_repositories() + +################################## +# Prevent Bazel from trying to build rxjs under angular devkit +local_repository( + name = "rxjs_ignore_nested_1", + path = "node_modules/@angular-devkit/core/node_modules/rxjs/src", +) +local_repository( + name = "rxjs_ignore_nested_2", + path = "node_modules/@angular-devkit/schematics/node_modules/rxjs/src", ) diff --git a/aio/README.md b/aio/README.md index 4a183af8cd..a6fef940cd 100644 --- a/aio/README.md +++ b/aio/README.md @@ -8,7 +8,7 @@ Everything in this folder is part of the documentation project. This includes ## Developer tasks -We use `yarn` to manage the dependencies and to run build tasks. +We use [Yarn](https://yarnpkg.com) to manage the dependencies and to run build tasks. You should run all these tasks from the `angular/aio` folder. Here are the most important tasks you might need to use: @@ -22,8 +22,8 @@ Here are the most important tasks you might need to use: * `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary. * `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console. * `yarn lint` - check that the doc-viewer code follows our style rules. -* `yarn test` - run all the unit tests once. -* `yarn test --watch` - watch all the source files, for the doc-viewer, and run all the unit tests when any change. +* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change. +* `yarn test --watch=false` - run all the unit tests once. * `yarn e2e` - run all the e2e tests for the doc-viewer. * `yarn docs` - generate all the docs from the source files. @@ -43,16 +43,22 @@ Here are the most important tasks you might need to use: * `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer. +## Developing on Windows +The `packages/` directory may contain Linux-specific symlinks, which are not recognized by Windows. +These unresolved links cause the docs generation process to fail because it cannot locate certain files. + +> Hint: The following steps require administration rights or [Windows Developer Mode](https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development) enabled! + +To fix this problem, run `scripts/windows/create-symlinks.sh`. This command creates temporary files where the symlinks used to be. Make sure not to commit those files with your documentation changes. +When you are done making and testing your documentation changes, you can restore the original symlinks and delete the temporary files by running `scripts/windows/remove-symlinks.sh`. + +It's necessary to remove the temporary files, because otherwise they're displayed as local changes in your git working copy and certain operations are blocked. + ## Using ServiceWorker locally -Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which -would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible -with webpack serving the files from memory). - -If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/` -with `yarn http-server dist -p 4200`. - -For more details see #16745. +Running `yarn start` (even when explicitly targeting production mode) does not set up the +ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then +serve the files in `dist/` with `yarn http-server dist -p 4200`. ## Guide to authoring diff --git a/aio/aio-builds-setup/dockerbuild/Dockerfile b/aio/aio-builds-setup/dockerbuild/Dockerfile index 206af101ce..1145c41b64 100644 --- a/aio/aio-builds-setup/dockerbuild/Dockerfile +++ b/aio/aio-builds-setup/dockerbuild/Dockerfile @@ -8,17 +8,24 @@ LABEL name="angular.io PR preview" \ VOLUME /aio-secrets VOLUME /var/www/aio-builds +VOLUME /dockerbuild EXPOSE 80 443 # Build-time args and env vars +# The AIO_ARTIFACT_PATH path needs to be kept in synch with the value of +# `aio_preview->steps->store_artifacts->destination` property in `.circleci/config.yml` +ARG AIO_ARTIFACT_PATH=aio/dist/aio-snapshot.tgz +ARG TEST_AIO_ARTIFACT_PATH=$AIO_ARTIFACT_PATH ARG AIO_BUILDS_DIR=/var/www/aio-builds ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds ARG AIO_DOMAIN_NAME=ngbuilds.io ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost ARG AIO_GITHUB_ORGANIZATION=angular -ARG TEST_AIO_GITHUB_ORGANIZATION=angular +ARG TEST_AIO_GITHUB_ORGANIZATION=test-org +ARG AIO_GITHUB_REPO=angular +ARG TEST_AIO_GITHUB_REPO=test-repo ARG AIO_GITHUB_TEAM_SLUGS=team,aio-contributors ARG TEST_AIO_GITHUB_TEAM_SLUGS=team,aio-contributors ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME @@ -27,34 +34,36 @@ ARG AIO_NGINX_PORT_HTTP=80 ARG TEST_AIO_NGINX_PORT_HTTP=8080 ARG AIO_NGINX_PORT_HTTPS=443 ARG TEST_AIO_NGINX_PORT_HTTPS=4433 -ARG AIO_REPO_SLUG=angular/angular -ARG TEST_AIO_REPO_SLUG=test-repo/test-slug +ARG AIO_SIGNIFICANT_FILES_PATTERN='^(?:aio|packages)/(?!.*[._]spec\\.[jt]s$)' +ARG TEST_AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN ARG AIO_TRUSTED_PR_LABEL="aio: preview" ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview" -ARG AIO_UPLOAD_HOSTNAME=upload.localhost -ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost -ARG AIO_UPLOAD_MAX_SIZE=20971520 -ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520 -ARG AIO_UPLOAD_PORT=3000 -ARG TEST_AIO_UPLOAD_PORT=3001 +ARG AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost +ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost +ARG AIO_ARTIFACT_MAX_SIZE=20971520 +ARG TEST_AIO_ARTIFACT_MAX_SIZE=200 +ARG AIO_PREVIEW_SERVER_PORT=3000 +ARG TEST_AIO_PREVIEW_SERVER_PORT=3001 -ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \ - AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \ - AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \ - AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \ - AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \ - AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \ - AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \ - AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \ - AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \ - AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \ - AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \ - AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \ - AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \ - AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \ - AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \ - AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \ - AIO_WWW_USER=www-data \ +ENV AIO_ARTIFACT_PATH=$AIO_ARTIFACT_PATH TEST_AIO_ARTIFACT_PATH=$TEST_AIO_ARTIFACT_PATH \ + AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \ + AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \ + AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \ + AIO_GITHUB_REPO=$AIO_GITHUB_REPO TEST_AIO_GITHUB_REPO=$TEST_AIO_GITHUB_REPO \ + AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \ + AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \ + AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \ + AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \ + AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \ + AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \ + AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \ + AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \ + AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN TEST_AIO_SIGNIFICANT_FILES_PATTERN=$TEST_AIO_SIGNIFICANT_FILES_PATTERN \ + AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \ + AIO_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME TEST_AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME \ + AIO_ARTIFACT_MAX_SIZE=$AIO_ARTIFACT_MAX_SIZE TEST_AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE \ + AIO_PREVIEW_SERVER_PORT=$AIO_PREVIEW_SERVER_PORT TEST_AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT \ + AIO_WWW_USER=www-data \ NODE_ENV=production @@ -64,7 +73,7 @@ RUN mkdir /var/log/aio # Add extra package sources RUN apt-get update -y && apt-get install -y curl -RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash - +RUN curl --silent --show-error --location https://deb.nodesource.com/setup_10.x | bash - RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list @@ -99,9 +108,9 @@ RUN printenv | grep AIO_ >> /etc/environment # Set up dnsmasq COPY dnsmasq/dnsmasq.conf /etc/ RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf -RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf +RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/dnsmasq.conf RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf -RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf +RUN sed -i "s|{{\$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}|$TEST_AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/dnsmasq.conf # Set up SSL/TLS certificates @@ -125,9 +134,9 @@ RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/ RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf -RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf -RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf -RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_ARTIFACT_MAX_SIZE}}|$AIO_ARTIFACT_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf +RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_PORT}}|$AIO_PREVIEW_SERVER_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-test.conf RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf @@ -136,9 +145,9 @@ RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/co RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf -RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf -RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf -RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$TEST_AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_ARTIFACT_MAX_SIZE}}|$TEST_AIO_ARTIFACT_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf +RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_PORT}}|$TEST_AIO_PREVIEW_SERVER_PORT|g" /etc/nginx/conf.d/aio-builds-test.conf # Set up pm2 diff --git a/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup b/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup index 3e6e5117ea..f820811ab2 100644 --- a/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup +++ b/aio/aio-builds-setup/dockerbuild/cronjobs/aio-builds-cleanup @@ -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 diff --git a/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf b/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf index 231625141c..af43feeb2e 100644 --- a/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf +++ b/aio/aio-builds-setup/dockerbuild/dnsmasq/dnsmasq.conf @@ -8,9 +8,9 @@ listen-address=127.0.0.1 # Force an IP address for these domains. address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1 -address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1 +address=/{{$AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1 address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1 -address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1 +address=/{{$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1 # Run as root (required from inside docker container). user=root diff --git a/aio/aio-builds-setup/dockerbuild/logrotate/aio-upload-server b/aio/aio-builds-setup/dockerbuild/logrotate/aio-preview-server similarity index 71% rename from aio/aio-builds-setup/dockerbuild/logrotate/aio-upload-server rename to aio/aio-builds-setup/dockerbuild/logrotate/aio-preview-server index d3f2fc6188..3fe5d31f88 100644 --- a/aio/aio-builds-setup/dockerbuild/logrotate/aio-upload-server +++ b/aio/aio-builds-setup/dockerbuild/logrotate/aio-preview-server @@ -1,4 +1,4 @@ -/var/log/aio/upload-server-*.log { +/var/log/aio/preview-server-*.log { compress copytruncate delaycompress diff --git a/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf b/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf index ec2a244f48..c9aeec032d 100644 --- a/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf +++ b/aio/aio-builds-setup/dockerbuild/nginx/aio-builds.conf @@ -36,6 +36,11 @@ server { access_log {{$AIO_NGINX_LOGS_DIR}}/access.log; error_log {{$AIO_NGINX_LOGS_DIR}}/error.log; + error_page 404 /404.html; + location "=/404.html" { + internal; + } + location "~/[^/]+\.[^/]+$" { try_files $uri $uri/ =404; } @@ -66,24 +71,32 @@ server { return 200 ''; } - # Upload builds - location "~^/create-build/(?[1-9][0-9]*)/(?[0-9a-f]{40})/?$" { + # 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") { add_header Allow "POST"; return 405; } - client_body_temp_path /tmp/aio-create-builds; - client_body_buffer_size 128K; - client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}}; - client_body_in_file_only on; - proxy_pass_request_headers on; - proxy_set_header X-FILE $request_body_file; - proxy_set_body off; proxy_redirect off; - proxy_method GET; - proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri; + proxy_method POST; + proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri; resolver 127.0.0.1; } @@ -98,7 +111,7 @@ server { proxy_pass_request_headers on; proxy_redirect off; proxy_method POST; - proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri; + proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri; resolver 127.0.0.1; } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts index a5fef4bb4b..d52cc6ebbc 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/build-cleaner.ts @@ -3,29 +3,53 @@ import * as fs from 'fs'; import * as path from 'path'; 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} from '../common/utils'; +import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils'; // Classes export class BuildCleaner { + + private logger = new Logger('BuildCleaner'); + // Constructor - constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken: string) { + constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string, + protected githubToken: string, protected downloadsDir: string, protected artifactPath: string) { assertNotMissingOrEmpty('buildsDir', buildsDir); - assertNotMissingOrEmpty('repoSlug', repoSlug); + assertNotMissingOrEmpty('githubOrg', githubOrg); + assertNotMissingOrEmpty('githubRepo', githubRepo); assertNotMissingOrEmpty('githubToken', githubToken); + assertNotMissingOrEmpty('downloadsDir', downloadsDir); + assertNotMissingOrEmpty('artifactPath', artifactPath); } // Methods - Public - public cleanUp(): Promise { - return Promise.all([ - this.getExistingBuildNumbers(), - this.getOpenPrNumbers(), - ]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs)); + public async cleanUp(): Promise { + try { + this.logger.log('Cleaning up builds and downloads'); + const openPrs = await this.getOpenPrNumbers(); + this.logger.log(`Open pull requests: ${openPrs.length}`); + await Promise.all([ + this.cleanBuilds(openPrs), + this.cleanDownloads(openPrs), + ]); + } catch (error) { + this.logger.error('ERROR:', error); + } } - // Methods - Protected - protected getExistingBuildNumbers(): Promise { - return new Promise((resolve, reject) => { + public async cleanBuilds(openPrs: number[]): Promise { + const existingBuilds = await this.getExistingBuildNumbers(); + await this.removeUnnecessaryBuilds(existingBuilds, openPrs); + } + + public async cleanDownloads(openPrs: number[]): Promise { + const existingDownloads = await this.getExistingDownloads(); + await this.removeUnnecessaryDownloads(existingDownloads, openPrs); + } + + public getExistingBuildNumbers(): Promise { + return new Promise((resolve, reject) => { fs.readdir(this.buildsDir, (err, files) => { if (err) { return reject(err); @@ -41,31 +65,29 @@ export class BuildCleaner { }); } - protected getOpenPrNumbers(): Promise { - const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug); - - return githubPullRequests. - fetchAll('open'). - then(prs => prs.map(pr => pr.number)); + public async getOpenPrNumbers(): Promise { + const api = new GithubApi(this.githubToken); + const githubPullRequests = new GithubPullRequests(api, this.githubOrg, this.githubRepo); + const prs = await githubPullRequests.fetchAll('open'); + return prs.map(pr => pr.number); } - protected removeDir(dir: string) { + public removeDir(dir: string): void { try { if (shell.test('-d', dir)) { shell.chmod('-R', 'a+w', dir); shell.rm('-rf', dir); } } catch (err) { - console.error(`ERROR: Unable to remove '${dir}' due to:`, err); + this.logger.error(`ERROR: Unable to remove '${dir}' due to:`, err); } } - protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) { + public removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]): void { const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num)); - console.log(`Existing builds: ${existingBuildNumbers.length}`); - console.log(`Open pull requests: ${openPrNumbers.length}`); - console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`); + this.logger.log(`Existing builds: ${existingBuildNumbers.length}`); + this.logger.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`); // Try removing public dirs. toRemove. @@ -77,4 +99,29 @@ export class BuildCleaner { map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))). forEach(dir => this.removeDir(dir)); } + + public getExistingDownloads(): Promise { + const artifactFile = path.basename(this.artifactPath); + return new Promise((resolve, reject) => { + fs.readdir(this.downloadsDir, (err, files) => { + if (err) { + return reject(err); + } + files = files.filter(file => file.endsWith(artifactFile)); + resolve(files); + }); + }); + } + + public removeUnnecessaryDownloads(existingDownloads: string[], openPrNumbers: number[]): void { + const toRemove = existingDownloads.filter(filePath => { + const {pr} = getPrInfoFromDownloadPath(filePath); + return !openPrNumbers.includes(pr); + }); + + this.logger.log(`Existing downloads: ${existingDownloads.length}`); + this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`); + + toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath))); + } } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts index c9819dd998..5fb6bbec70 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/clean-up/index.ts @@ -1,23 +1,26 @@ // Imports -import {getEnvVar} from '../common/utils'; +import {AIO_DOWNLOADS_DIR} from '../common/constants'; +import { + AIO_ARTIFACT_PATH, + AIO_BUILDS_DIR, + AIO_GITHUB_ORGANIZATION, + AIO_GITHUB_REPO, + AIO_GITHUB_TOKEN, +} from '../common/env-variables'; import {BuildCleaner} from './build-cleaner'; -// Constants -const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR'); -const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true); -const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG'); - // Run _main(); // Functions -function _main() { - console.log(`[${new Date()}] - Cleaning up builds...`); +function _main(): void { + const buildCleaner = new BuildCleaner( + AIO_BUILDS_DIR, + AIO_GITHUB_ORGANIZATION, + AIO_GITHUB_REPO, + AIO_GITHUB_TOKEN, + AIO_DOWNLOADS_DIR, + AIO_ARTIFACT_PATH); - const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN); - - buildCleaner.cleanUp().catch(err => { - console.error('ERROR:', err); - process.exit(1); - }); + buildCleaner.cleanUp().catch(() => process.exit(1)); } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts new file mode 100644 index 0000000000..c416ca4841 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/circle-ci-api.ts @@ -0,0 +1,90 @@ +// Imports +import fetch from 'node-fetch'; +import {assertNotMissingOrEmpty} from './utils'; + +// Constants +const CIRCLE_CI_API_URL = 'https://circleci.com/api/v1.1/project/github'; + +// Interfaces - Types +export interface ArtifactInfo { + path: string; + pretty_path: string; + node_index: number; + url: string; +} + +export type ArtifactResponse = ArtifactInfo[]; + +export interface BuildInfo { + reponame: string; + failed: boolean; + branch: string; + username: string; + build_num: number; + has_artifacts: boolean; + outcome: string; // e.g. 'success' + vcs_revision: string; // HEAD SHA + // there are other fields but they are not used in this code +} + +/** + * A Helper that can interact with the CircleCI API. + */ +export class CircleCiApi { + + private tokenParam = `circle-token=${this.circleCiToken}`; + + /** + * Construct a helper that can interact with the CircleCI REST API. + * @param githubOrg The Github organisation whose repos we want to access in CircleCI (e.g. angular). + * @param githubRepo The Github repo whose builds we want to access in CircleCI (e.g. angular). + * @param circleCiToken The CircleCI API access token (secret). + */ + constructor( + private githubOrg: string, + private githubRepo: string, + private circleCiToken: string, + ) { + assertNotMissingOrEmpty('githubOrg', githubOrg); + assertNotMissingOrEmpty('githubRepo', githubRepo); + assertNotMissingOrEmpty('circleCiToken', circleCiToken); + } + + /** + * Get the info for a build from the CircleCI API + * @param buildNumber The CircleCI build number that generated the artifact. + * @returns A promise to the info about the build + */ + public async getBuildInfo(buildNumber: number): Promise { + try { + const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`; + const response = await fetch(`${baseUrl}?${this.tokenParam}`); + if (response.status !== 200) { + throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`); + } + return response.json(); + } catch (error) { + throw new Error(`CircleCI build info request failed (${error.message})`); + } + } + + /** + * Query the CircleCI API to get a URL for a specified artifact from a specified build. + * @param artifactPath The path, within the build to the artifact. + * @returns A promise to the URL that can be requested to download the actual build artifact file. + */ + public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise { + 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() as ArtifactResponse; + const artifact = artifacts.find(item => item.path === artifactPath); + if (!artifact) { + throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`); + } + return artifact.url; + } catch (error) { + throw new Error(`CircleCI artifact URL request failed (${error.message})`); + } + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/constants.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/constants.ts index c5064c5dfc..186f75cae6 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/constants.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/constants.ts @@ -1,3 +1,4 @@ // Constants +export const AIO_DOWNLOADS_DIR = '/tmp/aio-downloads'; export const HIDDEN_DIR_PREFIX = 'hidden--'; export const SHORT_SHA_LEN = 7; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/env-variables.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/env-variables.ts new file mode 100644 index 0000000000..1383836d1e --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/env-variables.ts @@ -0,0 +1,19 @@ +import {getEnvVar} from './utils'; + +export const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH'); +export const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR'); +export const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN'); +export const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN'); +export const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME'); +export const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION'); +export const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO'); +export const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS'); +export const AIO_NGINX_HOSTNAME = getEnvVar('AIO_NGINX_HOSTNAME'); +export const AIO_NGINX_PORT_HTTP = +getEnvVar('AIO_NGINX_PORT_HTTP'); +export const AIO_NGINX_PORT_HTTPS = +getEnvVar('AIO_NGINX_PORT_HTTPS'); +export const AIO_SIGNIFICANT_FILES_PATTERN = getEnvVar('AIO_SIGNIFICANT_FILES_PATTERN'); +export const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL'); +export const AIO_PREVIEW_SERVER_HOSTNAME = getEnvVar('AIO_PREVIEW_SERVER_HOSTNAME'); +export const AIO_PREVIEW_SERVER_PORT = +getEnvVar('AIO_PREVIEW_SERVER_PORT'); +export const AIO_ARTIFACT_MAX_SIZE = +getEnvVar('AIO_ARTIFACT_MAX_SIZE'); +export const AIO_WWW_USER = getEnvVar('AIO_WWW_USER'); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts index 820944cb5f..fc259cbdb7 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-api.ts @@ -28,29 +28,18 @@ export class GithubApi { } // Methods - Public - public get(pathname: string, params?: RequestParamsOrNull): Promise { + public get(pathname: string, params?: RequestParamsOrNull): Promise { const path = this.buildPath(pathname, params); return this.request('get', path); } - public post(pathname: string, params?: RequestParamsOrNull, data?: any): Promise { + public post(pathname: string, params?: RequestParamsOrNull, data?: any): Promise { const path = this.buildPath(pathname, params); return this.request('post', path, data); } - // Methods - Protected - protected buildPath(pathname: string, params?: RequestParamsOrNull): string { - if (params == null) { - return pathname; - } - - const search = (params === null) ? '' : this.serializeSearchParams(params); - const joiner = search && '?'; - - return `${pathname}${joiner}${search}`; - } - - protected getPaginated(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise { + // In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination) + public getPaginated(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise { const perPage = 100; const params = { ...baseParams, @@ -67,6 +56,18 @@ export class GithubApi { }); } + // Methods - Protected + protected buildPath(pathname: string, params?: RequestParamsOrNull): string { + if (params == null) { + return pathname; + } + + const search = (params === null) ? '' : this.serializeSearchParams(params); + const joiner = search && '?'; + + return `${pathname}${joiner}${search}`; + } + protected request(method: string, path: string, data: any = null): Promise { return new Promise((resolve, reject) => { const options = { @@ -81,7 +82,7 @@ export class GithubApi { reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`); }; const onSuccess = (responseText: string) => { - try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); } + try { resolve(responseText && JSON.parse(responseText)); } catch (err) { reject(err); } }; const onResponse = (res: IncomingMessage) => { const statusCode = res.statusCode || -1; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts index 0075b17f98..f429a3c2c3 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-pull-requests.ts @@ -1,46 +1,79 @@ -// Imports -import {assertNotMissingOrEmpty} from '../common/utils'; import {GithubApi} from './github-api'; +import {assert, assertNotMissingOrEmpty} from './utils'; -// Interfaces - Types -export interface PullRequest { +export interface PullRequest { number: number; user: {login: string}; labels: {name: string}[]; } +export interface FileInfo { + sha: string; + filename: string; +} + export type PullRequestState = 'all' | 'closed' | 'open'; -// Classes -export class GithubPullRequests extends GithubApi { - // Constructor - constructor(githubToken: string, protected repoSlug: string) { - super(githubToken); - assertNotMissingOrEmpty('repoSlug', repoSlug); +/** + * Access pull requests on GitHub. + */ +export class GithubPullRequests { + public repoSlug: string; + + /** + * Create an instance of this helper + * @param api An instance of the Github API helper. + * @param githubOrg The organisation on GitHub whose repo we will interrogate. + * @param githubRepo The repository on Github with whose PRs we will interact. + */ + constructor(private api: GithubApi, githubOrg: string, githubRepo: string) { + assertNotMissingOrEmpty('githubOrg', githubOrg); + assertNotMissingOrEmpty('githubRepo', githubRepo); + this.repoSlug = `${githubOrg}/${githubRepo}`; } - // Methods - Public - public addComment(pr: number, body: string): Promise { - if (!(pr > 0)) { - throw new Error(`Invalid PR number: ${pr}`); - } else if (!body) { - throw new Error(`Invalid or empty comment body: ${body}`); - } - - return this.post(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body}); + /** + * Post a comment on a PR. + * @param pr The number of the PR on which to comment. + * @param body The body of the comment to post. + * @returns A promise that resolves when the comment has been posted. + */ + public addComment(pr: number, body: string): Promise { + assert(pr > 0, `Invalid PR number: ${pr}`); + assert(!!body, `Invalid or empty comment body: ${body}`); + return this.api.post(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body}); } + /** + * Request information about a PR. + * @param pr The number of the PR for which to request info. + * @returns A promise that is resolves with information about the specified PR. + */ public fetch(pr: number): Promise { + assert(pr > 0, `Invalid PR number: ${pr}`); // Using the `/issues/` URL, because the `/pulls/` one does not provide labels. - return this.get(`/repos/${this.repoSlug}/issues/${pr}`); + return this.api.get(`/repos/${this.repoSlug}/issues/${pr}`); } + /** + * Request information about all PRs that match the given state. + * @param state Only retrieve PRs that have this state. + * @returns A promise that is resolved with information about the requested PRs. + */ public fetchAll(state: PullRequestState = 'all'): Promise { - console.log(`Fetching ${state} pull requests...`); - const pathname = `/repos/${this.repoSlug}/pulls`; const params = {state}; - return this.getPaginated(pathname, params); + return this.api.getPaginated(pathname, params); + } + + /** + * Request a list of files for the given PR. + * @param pr The number of the PR for which to request files. + * @returns A promise that resolves to an array of file information + */ + public fetchFiles(pr: number): Promise { + assert(pr > 0, `Invalid PR number: ${pr}`); + return this.api.getPaginated(`/repos/${this.repoSlug}/pulls/${pr}/files`); } } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-teams.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-teams.ts index ab1a2e0f16..e1657ce979 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-teams.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/github-teams.ts @@ -1,45 +1,72 @@ -// Imports -import {assertNotMissingOrEmpty} from '../common/utils'; import {GithubApi} from './github-api'; +import {assertNotMissingOrEmpty} from './utils'; -// Interfaces - Types -interface Team { +export interface Team { id: number; slug: string; } -interface TeamMembership { +export interface TeamMembership { state: string; } -// Classes -export class GithubTeams extends GithubApi { - // Constructor - constructor(githubToken: string, protected organization: string) { - super(githubToken); - assertNotMissingOrEmpty('organization', organization); +export class GithubTeams { + /** + * Create an instance of this helper + * @param api An instance of the Github API helper. + * @param githubOrg The organisation on GitHub whose repo we will interrogate. + */ + constructor(private api: GithubApi, protected githubOrg: string) { + assertNotMissingOrEmpty('githubOrg', githubOrg); } - // Methods - Public + /** + * Request information about all the organisation's teams in GitHub. + * @returns A promise that is resolved with information about the teams. + */ public fetchAll(): Promise { - return this.getPaginated(`/orgs/${this.organization}/teams`); + return this.api.getPaginated(`/orgs/${this.githubOrg}/teams`); } - public isMemberById(username: string, teamIds: number[]): Promise { - const getMembership = (teamId: number) => - this.get(`/teams/${teamId}/memberships/${username}`). - then(membership => membership.state === 'active'). - catch(() => false); - const reduceFn = (promise: Promise, teamId: number) => - promise.then(isMember => isMember || getMembership(teamId)); + /** + * Check whether the specified username is a member of the specified team. + * @param username The usernane to check for in the team. + * @param teamIds The team to check for the username. + * @returns a Promise that resolves to `true` if the username is a member of the team. + */ + public async isMemberById(username: string, teamIds: number[]): Promise { - return teamIds.reduce(reduceFn, Promise.resolve(false)); + const getMembership = async (teamId: number) => { + try { + const {state} = await this.api.get(`/teams/${teamId}/memberships/${username}`); + return state === 'active'; + } catch (error) { + return false; + } + }; + + for (const teamId of teamIds) { + if (await getMembership(teamId)) { + return true; + } + } + + return false; } - public isMemberBySlug(username: string, teamSlugs: string[]): Promise { - return this.fetchAll(). - then(teams => teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id)). - then(teamIds => this.isMemberById(username, teamIds)). - catch(() => false); + /** + * Check whether the given username is a member of the teams specified by the team slugs. + * @param username The username to check for in the teams. + * @param teamSlugs A collection of slugs that represent the teams to check for the the username. + * @returns a Promise that resolves to `true` if the usernane is a member of at least one of the specified teams. + */ + public async isMemberBySlug(username: string, teamSlugs: string[]): Promise { + try { + const teams = await this.fetchAll(); + const teamIds = teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id); + return await this.isMemberById(username, teamIds); + } catch (error) { + return false; + } } } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts index 296b910d39..5ff9083b42 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/run-tests.ts @@ -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(); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts index 50e4ee5b7d..870126ba95 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/common/utils.ts @@ -1,17 +1,98 @@ -// Functions -export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => { +import {basename, resolve as resolvePath} from 'path'; +import {SHORT_SHA_LEN} from './constants'; + +/** + * Shorten a SHA to make it more readable + * @param sha The SHA to shorten. + */ +export function computeShortSha(sha: string) { + return sha.substr(0, SHORT_SHA_LEN); +} + +/** + * Compute the path for a downloaded artifact file. + * @param downloadsDir The directory where artifacts are downloaded + * @param pr The PR associated with this artifact. + * @param sha The SHA associated with the build for this artifact. + * @param artifactPath The path to the artifact on CircleCI. + * @returns The fully resolved location for the specified downloaded artifact. + */ +export function computeArtifactDownloadPath(downloadsDir: string, pr: number, sha: string, artifactPath: string) { + return resolvePath(downloadsDir, `${pr}-${computeShortSha(sha)}-${basename(artifactPath)}`); +} + +/** + * Extract the PR number and latest commit SHA from a downloaded file path. + * @param downloadPath the path to the downloaded file. + * @returns An object whose keys are the PR and SHA extracted from the file path. + */ +export function getPrInfoFromDownloadPath(downloadPath: string) { + const file = basename(downloadPath); + const [pr, sha] = file.split('-'); + return {pr: +pr, sha}; +} + +/** + * Assert that a value is true. + * @param value The value to assert. + * @param message The message if the value is not true. + */ +export function assert(value: boolean, message: string) { if (!value) { - throw new Error(`Missing or empty required parameter '${name}'!`); + throw new Error(message); } +} + +/** + * Assert that a parameter is not equal to "". + * @param name The name of the parameter. + * @param value The value of the parameter. + */ +export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => { + assert(!!value, `Missing or empty required parameter '${name}'!`); }; +/** + * Get an environment variable. + * @param name The name of the environment variable. + * @param isOptional True if the variable is optional. + * @returns The value of the variable or "" if it is optional and falsy. + * @throws `Error` if the variable is falsy and not optional. + */ export const getEnvVar = (name: string, isOptional = false): string => { const value = process.env[name]; if (!isOptional && !value) { - console.error(`ERROR: Missing required environment variable '${name}'!`); - process.exit(1); + try { + throw new Error(`ERROR: Missing required environment variable '${name}'!`); + } catch (error) { + console.error(error.stack); + process.exit(1); + } } return value || ''; }; + +/** + * 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); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-creator.ts similarity index 78% rename from aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts rename to aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-creator.ts index 899b3ab7be..f6c0771b12 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-creator.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-creator.ts @@ -4,13 +4,16 @@ import {EventEmitter} from 'events'; import * as fs from 'fs'; import * as path from 'path'; import * as shell from 'shelljs'; -import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants'; -import {assertNotMissingOrEmpty} from '../common/utils'; +import {HIDDEN_DIR_PREFIX} from '../common/constants'; +import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils'; import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; -import {UploadError} from './upload-error'; +import {PreviewServerError} from './preview-error'; // Classes export class BuildCreator extends EventEmitter { + + private logger = new Logger('BuildCreator'); + // Constructor constructor(protected buildsDir: string) { super(); @@ -18,9 +21,9 @@ export class BuildCreator extends EventEmitter { } // Methods - Public - public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise { + public create(pr: number, sha: string, archivePath: string, isPublic: boolean): Promise { // Use only part of the SHA for more readable URLs. - sha = sha.substr(0, SHORT_SHA_LEN); + sha = computeShortSha(sha); const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic); const shaDir = path.join(prDir, sha); @@ -33,7 +36,7 @@ export class BuildCreator extends EventEmitter { then(([prDirExisted, shaDirExisted]) => { if (shaDirExisted) { const publicOrNot = isPublic ? 'public' : 'non-public'; - throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); + throw new PreviewServerError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); } dirToRemoveOnError = prDirExisted ? shaDir : prDir; @@ -49,15 +52,15 @@ export class BuildCreator extends EventEmitter { shell.rm('-rf', dirToRemoveOnError); } - if (!(err instanceof UploadError)) { - err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`); + if (!(err instanceof PreviewServerError)) { + err = new PreviewServerError(500, `Error while creating preview at: ${shaDir}\n${err}`); } throw err; }); } - public updatePrVisibility(pr: string, makePublic: boolean): Promise { + public updatePrVisibility(pr: number, makePublic: boolean): Promise { const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic); return Promise. @@ -68,7 +71,8 @@ export class BuildCreator extends EventEmitter { return false; } else if (targetVisPrDirExisted) { // Error: Directories for both visibilities exist. - throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`); + throw new PreviewServerError(409, + `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`); } // Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`. @@ -79,8 +83,8 @@ export class BuildCreator extends EventEmitter { then(() => true); }). catch(err => { - if (!(err instanceof UploadError)) { - err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`); + if (!(err instanceof PreviewServerError)) { + err = new PreviewServerError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`); } throw err; @@ -102,7 +106,7 @@ export class BuildCreator extends EventEmitter { } if (stderr) { - console.warn(stderr); + this.logger.warn(stderr); } try { @@ -116,9 +120,9 @@ export class BuildCreator extends EventEmitter { }); } - protected getCandidatePrDirs(pr: string, isPublic: boolean) { + protected getCandidatePrDirs(pr: number, isPublic: boolean): {oldPrDir: string, newPrDir: string} { const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr); - const publicPrDir = path.join(this.buildsDir, pr); + const publicPrDir = path.join(this.buildsDir, `${pr}`); const oldPrDir = isPublic ? hiddenPrDir : publicPrDir; const newPrDir = isPublic ? publicPrDir : hiddenPrDir; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-events.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-events.ts similarity index 100% rename from aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-events.ts rename to aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-events.ts diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts new file mode 100644 index 0000000000..2c29d6cd96 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-retriever.ts @@ -0,0 +1,83 @@ +import * as fs from 'fs'; +import fetch from 'node-fetch'; +import {dirname} from 'path'; +import {mkdir} from 'shelljs'; +import {promisify} from 'util'; +import {CircleCiApi} from '../common/circle-ci-api'; +import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils'; +import {PreviewServerError} from './preview-error'; + +export interface GithubInfo { + org: string; + pr: number; + repo: string; + sha: string; + success: boolean; +} + +/** + * A helper that can get information about builds and download build artifacts. + */ +export class 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); + } + + /** + * Get GitHub information about a build + * @param buildNum The number of the build for which to retrieve the info. + * @returns The Github org, repo, PR and latest SHA for the specified build. + */ + public async getGithubInfo(buildNum: number): Promise { + const buildInfo = await this.api.getBuildInfo(buildNum); + const githubInfo: GithubInfo = { + org: buildInfo.username, + pr: getPrFromBranch(buildInfo.branch), + repo: buildInfo.reponame, + sha: buildInfo.vcs_revision, + success: !buildInfo.failed, + }; + return githubInfo; + } + + /** + * Make a request to the given URL for a build artifact and store it locally. + * @param buildNum the number of the CircleCI build whose artifact we want to download. + * @param pr the number of the PR that triggered the CircleCI build. + * @param sha the commit in the PR that triggered the CircleCI build. + * @param artifactPath the path on CircleCI where the artifact was stored. + * @returns A promise to the file path where the downloaded file was stored. + */ + public async downloadBuildArtifact(buildNum: number, pr: number, sha: string, artifactPath: string): Promise { + try { + const outPath = computeArtifactDownloadPath(this.downloadDir, pr, sha, artifactPath); + const downloadExists = await new Promise(resolve => fs.exists(outPath, exists => resolve(exists))); + if (!downloadExists) { + const url = await this.api.getBuildArtifactUrl(buildNum, artifactPath); + const response = await fetch(url, {size: this.downloadSizeLimit}); + if (response.status !== 200) { + throw new PreviewServerError(response.status, `Error ${response.status} - ${response.statusText}`); + } + const buffer = await response.buffer(); + mkdir('-p', dirname(outPath)); + await promisify(fs.writeFile)(outPath, buffer); + } + return outPath; + } catch (error) { + this.logger.warn(error); + const status = (error.type === 'max-size') ? 413 : 500; + throw new PreviewServerError(status, `CircleCI artifact download failed (${error.message || error})`); + } + } +} + +function getPrFromBranch(branch: string): number { + // CircleCI only exposes PR numbers via the `branch` field :-( + const match = /^pull\/(\d+)$/.exec(branch); + if (!match) { + throw new Error(`No PR found in branch field: ${branch}`); + } + return +match[1]; +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-verifier.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-verifier.ts new file mode 100644 index 0000000000..42a56344d7 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/build-verifier.ts @@ -0,0 +1,46 @@ +import {GithubPullRequests, PullRequest} from '../common/github-pull-requests'; +import {GithubTeams} from '../common/github-teams'; +import {assertNotMissingOrEmpty} from '../common/utils'; + +/** + * A helper to verify whether builds are trusted. + */ +export class BuildVerifier { + /** + * Construct a new BuildVerifier instance. + * @param prs A helper to access PR information. + * @param teams A helper to access Github team information. + * @param allowedTeamSlugs The teams that are trusted. + * @param trustedPrLabel The github label that indicates that a PR is trusted. + */ + constructor(protected prs: GithubPullRequests, protected teams: GithubTeams, + protected allowedTeamSlugs: string[], protected trustedPrLabel: string) { + assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join('')); + assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel); + } + + /** + * Check whether a PR contains files that are significant to the build. + * @param pr The number of the PR to check + * @param significantFilePattern A regex that selects files that are significant. + */ + public async getSignificantFilesChanged(pr: number, significantFilePattern: RegExp): Promise { + const files = await this.prs.fetchFiles(pr); + return files.some(file => significantFilePattern.test(file.filename)); + } + + /** + * Check whether a PR is trusted. + * @param pr The number of the PR to check. + * @returns true if the PR is trusted. + */ + public async getPrIsTrusted(pr: number): Promise { + const prInfo = await this.prs.fetch(pr); + return this.hasLabel(prInfo, this.trustedPrLabel) || + (await this.teams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs)); + } + + protected hasLabel(prInfo: PullRequest, label: string): boolean { + return prInfo.labels.some(labelObj => labelObj.name === label); + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/index.ts new file mode 100644 index 0000000000..90780bc8de --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/index.ts @@ -0,0 +1,41 @@ +// Imports +import {AIO_DOWNLOADS_DIR} from '../common/constants'; +import { + AIO_ARTIFACT_MAX_SIZE, + AIO_ARTIFACT_PATH, + AIO_BUILDS_DIR, + AIO_CIRCLE_CI_TOKEN, + AIO_DOMAIN_NAME, + AIO_GITHUB_ORGANIZATION, + AIO_GITHUB_REPO, + AIO_GITHUB_TEAM_SLUGS, + AIO_GITHUB_TOKEN, + AIO_PREVIEW_SERVER_HOSTNAME, + AIO_PREVIEW_SERVER_PORT, + AIO_SIGNIFICANT_FILES_PATTERN, + AIO_TRUSTED_PR_LABEL, +} from '../common/env-variables'; +import {PreviewServerFactory} from './preview-server-factory'; + +// Run +_main(); + +// Functions +function _main(): void { + PreviewServerFactory + .create({ + buildArtifactPath: AIO_ARTIFACT_PATH, + buildsDir: AIO_BUILDS_DIR, + circleCiToken: AIO_CIRCLE_CI_TOKEN, + domainName: AIO_DOMAIN_NAME, + downloadSizeLimit: AIO_ARTIFACT_MAX_SIZE, + downloadsDir: AIO_DOWNLOADS_DIR, + githubOrg: AIO_GITHUB_ORGANIZATION, + githubRepo: AIO_GITHUB_REPO, + githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','), + githubToken: AIO_GITHUB_TOKEN, + significantFilesPattern: AIO_SIGNIFICANT_FILES_PATTERN, + trustedPrLabel: AIO_TRUSTED_PR_LABEL, + }) + .listen(AIO_PREVIEW_SERVER_PORT, AIO_PREVIEW_SERVER_HOSTNAME); +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-error.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/preview-error.ts similarity index 51% rename from aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-error.ts rename to aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/preview-error.ts index 877a3c2baf..af23b26cd0 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-error.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/preview-error.ts @@ -1,8 +1,8 @@ // Classes -export class UploadError extends Error { +export class PreviewServerError extends Error { // Constructor constructor(public status: number = 500, message?: string) { super(message); - Object.setPrototypeOf(this, UploadError.prototype); + Object.setPrototypeOf(this, PreviewServerError.prototype); } } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/preview-server-factory.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/preview-server-factory.ts new file mode 100644 index 0000000000..21186f4539 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/preview-server-factory.ts @@ -0,0 +1,210 @@ +// Imports +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, Logger} from '../common/utils'; +import {BuildCreator} from './build-creator'; +import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; +import {BuildRetriever} from './build-retriever'; +import {BuildVerifier} from './build-verifier'; +import {respondWithError, throwRequestError} from './utils'; + +const AIO_PREVIEW_JOB = 'aio_preview'; + +// Interfaces - Types +export interface PreviewServerConfig { + downloadsDir: string; + downloadSizeLimit: number; + buildArtifactPath: string; + buildsDir: string; + domainName: string; + githubOrg: string; + githubRepo: string; + githubTeamSlugs: string[]; + circleCiToken: string; + githubToken: string; + significantFilesPattern: string; + trustedPrLabel: string; +} + +const logger = new Logger('PreviewServer'); + +// Classes +export class PreviewServerFactory { + // Methods - Public + public static create(cfg: PreviewServerConfig): http.Server { + assertNotMissingOrEmpty('domainName', cfg.domainName); + + const circleCiApi = new CircleCiApi(cfg.githubOrg, cfg.githubRepo, cfg.circleCiToken); + const githubApi = new GithubApi(cfg.githubToken); + const prs = new GithubPullRequests(githubApi, cfg.githubOrg, cfg.githubRepo); + const teams = new GithubTeams(githubApi, cfg.githubOrg); + + const buildRetriever = new BuildRetriever(circleCiApi, cfg.downloadSizeLimit, cfg.downloadsDir); + const buildVerifier = new BuildVerifier(prs, teams, cfg.githubTeamSlugs, cfg.trustedPrLabel); + const buildCreator = PreviewServerFactory.createBuildCreator(prs, cfg.buildsDir, cfg.domainName); + + const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator, cfg); + const httpServer = http.createServer(middleware as any); + + httpServer.on('listening', () => { + const info = httpServer.address() as AddressInfo; + logger.info(`Up and running (and listening on ${info.address}:${info.port})...`); + }); + + return httpServer; + } + + public static createMiddleware(buildRetriever: BuildRetriever, buildVerifier: BuildVerifier, + 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 { + if (!( + req.is('json') && + req.body && + req.body.payload && + req.body.payload.build_num > 0 && + req.body.payload.build_parameters && + req.body.payload.build_parameters.CIRCLE_JOB + )) { + throwRequestError(400, `Incorrect body content. Expected JSON`, req); + } + + const job = req.body.payload.build_parameters.CIRCLE_JOB; + const buildNum = req.body.payload.build_num; + + logger.log(`Build:${buildNum}, Job:${job} - processing web-hook trigger`); + + if (job !== AIO_PREVIEW_JOB) { + res.sendStatus(204); + logger.log(`Build:${buildNum}, Job:${job} -`, + `Skipping preview processing because this is not the "${AIO_PREVIEW_JOB}" job.`); + return; + } + + const { pr, sha, org, repo, success } = await buildRetriever.getGithubInfo(buildNum); + + if (!success) { + res.sendStatus(204); + logger.log(`PR:${pr}, Build:${buildNum} - Skipping preview processing because this build did not succeed.`); + return; + } + + assert(cfg.githubOrg === org, + `Invalid webhook: expected "githubOrg" property to equal "${cfg.githubOrg}" but got "${org}".`); + assert(cfg.githubRepo === repo, + `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, significantFilesRe)) { + res.sendStatus(204); + logger.log(`PR:${pr}, Build:${buildNum} - ` + + `Skipping preview processing because this PR did not touch any significant files.`); + return; + } + + const artifactPath = await buildRetriever.downloadBuildArtifact(buildNum, pr, sha, cfg.buildArtifactPath); + const isPublic = await buildVerifier.getPrIsTrusted(pr); + await buildCreator.create(pr, sha, artifactPath, isPublic); + res.sendStatus(isPublic ? 201 : 202); + } catch (err) { + logger.error('CircleCI webhook error', err); + respondWithError(res, err); + } + }); + + // GITHUB PR UPDATED WEBHOOK + middleware.post(/^\/pr-updated\/?$/, jsonParser, async (req, res) => { + const { action, number: prNo }: { action?: string, number?: number } = req.body; + const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled'); + + try { + if (!visMayHaveChanged) { + res.sendStatus(200); + } else if (!prNo) { + throwRequestError(400, `Missing or empty 'number' field`, req); + } else { + const isPublic = await buildVerifier.getPrIsTrusted(prNo); + await buildCreator.updatePrVisibility(prNo, isPublic); + res.sendStatus(200); + } + } catch (err) { + logger.error('PR update hook error', err); + respondWithError(res, err); + } + }); + + // ALL OTHER REQUESTS + middleware.all('*', req => throwRequestError(404, 'Unknown resource', req)); + middleware.use((err: any, _req: any, res: express.Response, _next: any) => { + const statusText = http.STATUS_CODES[err.status] || '???'; + logger.error(`Preview server error: ${err.status} - ${statusText}:`, err.message); + respondWithError(res, err); + }); + + return middleware; + } + + public static createBuildCreator(prs: GithubPullRequests, buildsDir: string, domainName: string): BuildCreator { + const buildCreator = new BuildCreator(buildsDir); + const postPreviewsComment = (pr: number, shas: string[]) => { + const body = shas. + map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`). + join('\n'); + + return prs.addComment(pr, body); + }; + + buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => { + if (isPublic) { + postPreviewsComment(pr, [sha]); + } + }); + + buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => { + if (isPublic && shas.length) { + postPreviewsComment(pr, shas); + } + }); + + return buildCreator; + } +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/utils.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/utils.ts new file mode 100644 index 0000000000..00f1df0aba --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/preview-server/utils.ts @@ -0,0 +1,29 @@ +import * as express from 'express'; +import {promisify} from 'util'; +import {PreviewServerError} from './preview-error'; + +/** + * Update the response to report that an error has occurred. + * @param res The response to configure as an error. + * @param err The error that needs to be reported. + */ +export async function respondWithError(res: express.Response, err: any): Promise { + if (!(err instanceof PreviewServerError)) { + err = new PreviewServerError(500, String((err && err.message) || err)); + } + + res.status(err.status); + await promisify(res.end.bind(res))(err.message); +} + +/** + * Throw an exception that describes the given error information. + * @param status The HTTP status code include in the error. + * @param error The error message to include in the error. + * @param req The request that triggered this error. + */ +export function throwRequestError(status: number, error: string, req: express.Request): never { + const message = `${error} in request: ${req.method} ${req.originalUrl}` + + (!req.body ? '' : ` ${JSON.stringify(req.body)}`); + throw new PreviewServerError(status, message); +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-verifier.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-verifier.ts deleted file mode 100644 index bfabb47525..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/build-verifier.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Imports -import * as jwt from 'jsonwebtoken'; -import {GithubPullRequests, PullRequest} from '../common/github-pull-requests'; -import {GithubTeams} from '../common/github-teams'; -import {assertNotMissingOrEmpty} from '../common/utils'; -import {UploadError} from './upload-error'; - -// Interfaces - Types -interface JwtPayload { - slug: string; - 'pull-request': number; -} - -// Enums -export enum BUILD_VERIFICATION_STATUS { - verifiedAndTrusted, - verifiedNotTrusted, -} - -// Classes -export class BuildVerifier { - // Properties - Protected - protected githubPullRequests: GithubPullRequests; - protected githubTeams: GithubTeams; - - // Constructor - constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string, - protected allowedTeamSlugs: string[], protected trustedPrLabel: string) { - assertNotMissingOrEmpty('secret', secret); - assertNotMissingOrEmpty('githubToken', githubToken); - assertNotMissingOrEmpty('repoSlug', repoSlug); - assertNotMissingOrEmpty('organization', organization); - assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join('')); - assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel); - - this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug); - this.githubTeams = new GithubTeams(githubToken, organization); - } - - // Methods - Public - public getPrIsTrusted(pr: number): Promise { - return Promise.resolve(). - then(() => this.githubPullRequests.fetch(pr)). - then(prInfo => this.hasLabel(prInfo, this.trustedPrLabel) || - this.githubTeams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs)); - } - - public verify(expectedPr: number, authHeader: string): Promise { - return Promise.resolve(). - then(() => this.extractJwtString(authHeader)). - then(jwtString => this.verifyJwt(expectedPr, jwtString)). - then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])). - catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); }); - } - - // Methods - Protected - protected extractJwtString(input: string): string { - return input.replace(/^token +/i, ''); - } - - protected hasLabel(prInfo: PullRequest, label: string) { - return prInfo.labels.some(labelObj => labelObj.name === label); - } - - protected verifyJwt(expectedPr: number, token: string): Promise { - return new Promise((resolve, reject) => { - jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload: JwtPayload) => { - if (err) { - reject(err.message || err); - } else if (payload.slug !== this.repoSlug) { - reject(`jwt slug invalid. expected: ${this.repoSlug}`); - } else if (payload['pull-request'] !== expectedPr) { - reject(`jwt pull-request invalid. expected: ${expectedPr}`); - } else { - resolve(payload); - } - }); - }); - } - - protected verifyPr(pr: number): Promise { - return this.getPrIsTrusted(pr). - then(isTrusted => Promise.resolve(isTrusted ? - BUILD_VERIFICATION_STATUS.verifiedAndTrusted : - BUILD_VERIFICATION_STATUS.verifiedNotTrusted)); - } -} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-preverify-pr.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-preverify-pr.ts deleted file mode 100644 index 6cc058e61b..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index-preverify-pr.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Imports -import {getEnvVar} from '../common/utils'; -import {BuildVerifier} from './build-verifier'; - -// Run -_main(); - -// Functions -function _main() { - const secret = 'unused'; - const githubToken = getEnvVar('AIO_GITHUB_TOKEN'); - const repoSlug = getEnvVar('AIO_REPO_SLUG'); - const organization = getEnvVar('AIO_GITHUB_ORGANIZATION'); - const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(','); - const trustedPrLabel = getEnvVar('AIO_TRUSTED_PR_LABEL'); - const pr = +getEnvVar('AIO_PREVERIFY_PR'); - - const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs, - trustedPrLabel); - - // Exit codes: - // - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label). - // - 1: An error occurred. - // - 2: The PR cannot be automatically trusted. - buildVerifier.getPrIsTrusted(pr). - then(isTrusted => { - if (!isTrusted) { - console.warn( - `The PR cannot be automatically verified, because it doesn't have the "${trustedPrLabel}" label and the ` + - `the author is not an active member of any of the following teams: ${allowedTeamSlugs.join(', ')}`); - } - - process.exit(isTrusted ? 0 : 2); - }). - catch(err => { - console.error(err); - process.exit(1); - }); -} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts deleted file mode 100644 index e7b705c0c2..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Imports -import {getEnvVar} from '../common/utils'; -import {uploadServerFactory} from './upload-server-factory'; - -// Constants -const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR'); -const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME'); -const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION'); -const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS'); -const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN'); -const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN'); -const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG'); -const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL'); -const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME'); -const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT'); - -// Run -_main(); - -// Functions -function _main() { - uploadServerFactory. - create({ - buildsDir: AIO_BUILDS_DIR, - domainName: AIO_DOMAIN_NAME, - githubOrganization: AIO_GITHUB_ORGANIZATION, - githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','), - githubToken: AIO_GITHUB_TOKEN, - repoSlug: AIO_REPO_SLUG, - secret: AIO_PREVIEW_DEPLOYMENT_TOKEN, - trustedPrLabel: AIO_TRUSTED_PR_LABEL, - }). - listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME); -} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts deleted file mode 100644 index 48e60e3cc1..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/upload-server/upload-server-factory.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Imports -import * as bodyParser from 'body-parser'; -import * as express from 'express'; -import * as http from 'http'; -import {GithubPullRequests} from '../common/github-pull-requests'; -import {assertNotMissingOrEmpty} from '../common/utils'; -import {BuildCreator} from './build-creator'; -import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; -import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier'; -import {UploadError} from './upload-error'; - -// Constants -const AUTHORIZATION_HEADER = 'AUTHORIZATION'; -const X_FILE_HEADER = 'X-FILE'; - -// Interfaces - Types -interface UploadServerConfig { - buildsDir: string; - domainName: string; - githubOrganization: string; - githubTeamSlugs: string[]; - githubToken: string; - repoSlug: string; - secret: string; - trustedPrLabel: string; -} - -// Classes -class UploadServerFactory { - // Methods - Public - public create({ - buildsDir, - domainName, - githubOrganization, - githubTeamSlugs, - githubToken, - repoSlug, - secret, - trustedPrLabel, - }: UploadServerConfig): http.Server { - assertNotMissingOrEmpty('domainName', domainName); - - const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs, - trustedPrLabel); - const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName); - - const middleware = this.createMiddleware(buildVerifier, buildCreator); - const httpServer = http.createServer(middleware as any); - - httpServer.on('listening', () => { - const info = httpServer.address(); - console.info(`Up and running (and listening on ${info.address}:${info.port})...`); - }); - - return httpServer; - } - - // Methods - Protected - protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string, - domainName: string): BuildCreator { - const buildCreator = new BuildCreator(buildsDir); - const githubPullRequests = new GithubPullRequests(githubToken, repoSlug); - const postPreviewsComment = (pr: number, shas: string[]) => { - const body = shas. - map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`). - join('\n'); - - return githubPullRequests.addComment(pr, body); - }; - - buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => { - if (isPublic) { - postPreviewsComment(pr, [sha]); - } - }); - - buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => { - if (isPublic && shas.length) { - postPreviewsComment(pr, shas); - } - }); - - return buildCreator; - } - - protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express { - const middleware = express(); - const jsonParser = bodyParser.json(); - - middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => { - const pr = req.params[0]; - const sha = req.params[1]; - const archive = req.header(X_FILE_HEADER); - const authHeader = req.header(AUTHORIZATION_HEADER); - - if (!authHeader) { - this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req); - } else if (!archive) { - this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req); - } else { - Promise.resolve(). - then(() => buildVerifier.verify(+pr, authHeader)). - then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted). - then(isPublic => buildCreator.create(pr, sha, archive, isPublic). - then(() => res.sendStatus(isPublic ? 201 : 202))). - catch(err => this.respondWithError(res, err)); - } - }); - middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200)); - middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => { - const {action, number: prNo}: {action?: string, number?: number} = req.body; - const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled'); - - if (!visMayHaveChanged) { - res.sendStatus(200); - } else if (!prNo) { - this.throwRequestError(400, `Missing or empty 'number' field`, req); - } else { - Promise.resolve(). - then(() => buildVerifier.getPrIsTrusted(prNo)). - then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)). - then(() => res.sendStatus(200)). - catch(err => this.respondWithError(res, err)); - } - }); - middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req)); - middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err)); - - return middleware; - } - - protected respondWithError(res: express.Response, err: any) { - if (!(err instanceof UploadError)) { - err = new UploadError(500, String((err && err.message) || err)); - } - - const statusText = http.STATUS_CODES[err.status] || '???'; - console.error(`Upload error: ${err.status} - ${statusText}`); - console.error(err.message); - - res.status(err.status).end(err.message); - } - - protected throwRequestError(status: number, error: string, req: express.Request) { - const message = `${error} in request: ${req.method} ${req.originalUrl}` + - (!req.body ? '' : ` ${JSON.stringify(req.body)}`); - - throw new UploadError(status, message); - } -} - -// Exports -export const uploadServerFactory = new UploadServerFactory(); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts index 92e7f15a31..19ce093879 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/constants.ts @@ -1,16 +1,37 @@ -// Using the values below, we can fake the response of the corresponding methods in tests. This is -// necessary, because the test upload-server will be running as a separate node process, so we will -// not have direct access to the code (e.g. for mocking). -// (See also 'lib/verify-setup/start-test-upload-server.ts'.) +export const enum BuildNums { + BUILD_INFO_ERROR = 1, + BUILD_INFO_404, + BUILD_INFO_BUILD_FAILED, + BUILD_INFO_INVALID_GH_ORG, + BUILD_INFO_INVALID_GH_REPO, + CHANGED_FILES_ERROR, + CHANGED_FILES_404, + CHANGED_FILES_NONE, + BUILD_ARTIFACTS_ERROR, + BUILD_ARTIFACTS_404, + BUILD_ARTIFACTS_EMPTY, + BUILD_ARTIFACTS_MISSING, + DOWNLOAD_ARTIFACT_ERROR, + DOWNLOAD_ARTIFACT_404, + DOWNLOAD_ARTIFACT_TOO_BIG, + TRUST_CHECK_ERROR, + TRUST_CHECK_UNTRUSTED, + TRUST_CHECK_TRUSTED_LABEL, + TRUST_CHECK_ACTIVE_TRUSTED_USER, + TRUST_CHECK_INACTIVE_TRUSTED_USER, +} -/* tslint:disable: variable-name */ +export const enum PrNums { + CHANGED_FILES_ERROR = 1, + CHANGED_FILES_404, + CHANGED_FILES_NONE, + TRUST_CHECK_ERROR, + TRUST_CHECK_UNTRUSTED, + TRUST_CHECK_TRUSTED_LABEL, + TRUST_CHECK_ACTIVE_TRUSTED_USER, + TRUST_CHECK_INACTIVE_TRUSTED_USER, +} -// Special values to be used as `authHeader` in `BuildVerifier#verify()`. -export const BV_verify_error = 'FAKE_VERIFICATION_ERROR'; -export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED'; - -// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`. -export const BV_getPrIsTrusted_error = 32203; -export const BV_getPrIsTrusted_notTrusted = 72457; - -/* tslint:enable: variable-name */ +export const SHA = '1234567890'.repeat(4); +export const ALT_SHA = 'abcde'.repeat(8); +export const SIMILAR_SHA = SHA.slice(0, -1) + 'A'; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/delete-empty.d.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/delete-empty.d.ts new file mode 100644 index 0000000000..19b96d7255 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/delete-empty.d.ts @@ -0,0 +1,10 @@ +declare module 'delete-empty' { + interface Options { + dryRun: boolean; + verbose: boolean; + filter: (filePath: string) => boolean; + } + export default function deleteEmpty(cwd: string, options?: Options): Promise; + export default function deleteEmpty(cwd: string, options?: Options, callback?: (err: any, deleted: string[]) => void): void; + export function sync(cwd: string, options?: Options): string[]; +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts index e5b2322494..f267e608dd 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/helper.ts @@ -4,18 +4,14 @@ import * as fs from 'fs'; import * as http from 'http'; import * as path from 'path'; import * as shell from 'shelljs'; -import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants'; -import {getEnvVar} from '../common/utils'; - -// Constans -const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR'); -const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME'); -const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP'); -const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS'); -const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME'); -const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE'); -const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT'); -const WWW_USER = getEnvVar('AIO_WWW_USER'); +import {AIO_DOWNLOADS_DIR, HIDDEN_DIR_PREFIX} from '../common/constants'; +import { + AIO_BUILDS_DIR, + AIO_NGINX_PORT_HTTP, + AIO_NGINX_PORT_HTTPS, + AIO_WWW_USER, +} from '../common/env-variables'; +import {computeShortSha, Logger} from '../common/utils'; // Interfaces - Types export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; } @@ -27,61 +23,50 @@ export type VerifyCmdResultFn = (result: CmdResult) => void; // Classes class Helper { - // Properties - Public - public get buildsDir() { return TEST_AIO_BUILDS_DIR; } - public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; } - public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; } - public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; } - public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; } - public get uploadPort() { return TEST_AIO_UPLOAD_PORT; } - public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; } - public get wwwUser() { return WWW_USER; } // Properties - Protected protected cleanUpFns: CleanUpFn[] = []; protected portPerScheme: {[scheme: string]: number} = { - http: this.nginxPortHttp, - https: this.nginxPortHttps, + http: AIO_NGINX_PORT_HTTP, + https: AIO_NGINX_PORT_HTTPS, }; + private logger = new Logger('TestHelper'); + // Constructor constructor() { - shell.mkdir('-p', this.buildsDir); - shell.exec(`chown -R ${this.wwwUser} ${this.buildsDir}`); + shell.mkdir('-p', AIO_BUILDS_DIR); + shell.exec(`chown -R ${AIO_WWW_USER} ${AIO_BUILDS_DIR}`); + shell.mkdir('-p', AIO_DOWNLOADS_DIR); + shell.exec(`chown -R ${AIO_WWW_USER} ${AIO_DOWNLOADS_DIR}`); } // Methods - Public - public buildExists(pr: string, sha = '', isPublic = true, legacy = false): boolean { - const prDir = this.getPrDir(pr, isPublic); - const dir = !sha ? prDir : this.getShaDir(prDir, sha, legacy); - return fs.existsSync(dir); - } - - public cleanUp() { + public cleanUp(): void { while (this.cleanUpFns.length) { // Clean-up fns remove themselves from the list. this.cleanUpFns[0](); } - if (fs.readdirSync(this.buildsDir).length) { - throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`); + const leftoverDownloads = fs.readdirSync(AIO_DOWNLOADS_DIR); + const leftoverBuilds = fs.readdirSync(AIO_BUILDS_DIR); + + if (leftoverDownloads.length) { + this.logger.log(`Downloads directory '${AIO_DOWNLOADS_DIR}' is not empty after clean-up.`, leftoverDownloads); + shell.rm('-rf', `${AIO_DOWNLOADS_DIR}/*`); + } + + if (leftoverBuilds.length) { + this.logger.log(`Builds directory '${AIO_BUILDS_DIR}' is not empty after clean-up.`, leftoverBuilds); + shell.rm('-rf', `${AIO_BUILDS_DIR}/*`); + } + + if (leftoverBuilds.length || leftoverDownloads.length) { + throw new Error(`Unexpected test files not cleaned up.`); } } - public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn { - const inputDir = this.getShaDir(this.getPrDir(`uploaded/${pr}`, true), sha); - const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`; - const cmd2 = `chown ${this.wwwUser} ${archivePath}`; - - const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true, true); - shell.exec(cmd1); - shell.exec(cmd2); - cleanUpTemp(); - - return this.createCleanUpFn(() => shell.rm('-rf', archivePath)); - } - - public createDummyBuild(pr: string, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn { + public createDummyBuild(pr: number, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn { const prDir = this.getPrDir(pr, isPublic); const shaDir = this.getShaDir(prDir, sha, legacy); const idxPath = path.join(shaDir, 'index.html'); @@ -89,34 +74,21 @@ class Helper { this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force); this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force); - shell.exec(`chown -R ${this.wwwUser} ${prDir}`); + shell.exec(`chown -R ${AIO_WWW_USER} ${prDir}`); return this.createCleanUpFn(() => shell.rm('-rf', prDir)); } - public deletePrDir(pr: string, isPublic = true) { - const prDir = this.getPrDir(pr, isPublic); - - if (fs.existsSync(prDir)) { - shell.chmod('-R', 'a+w', prDir); - shell.rm('-rf', prDir); - } - } - - public getPrDir(pr: string, isPublic: boolean): string { - const prDirName = isPublic ? pr : HIDDEN_DIR_PREFIX + pr; - return path.join(this.buildsDir, prDirName); + public getPrDir(pr: number, isPublic: boolean): string { + const prDirName = isPublic ? '' + pr : HIDDEN_DIR_PREFIX + pr; + return path.join(AIO_BUILDS_DIR, prDirName); } public getShaDir(prDir: string, sha: string, legacy = false): string { - return path.join(prDir, legacy ? sha : this.getShordSha(sha)); + return path.join(prDir, legacy ? sha : computeShortSha(sha)); } - public getShordSha(sha: string): string { - return sha.substr(0, SHORT_SHA_LEN); - } - - public readBuildFile(pr: string, sha: string, relFilePath: string, isPublic = true, legacy = false): string { + public readBuildFile(pr: number, sha: string, relFilePath: string, isPublic = true, legacy = false): string { const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy); const absFilePath = path.join(shaDir, relFilePath); return fs.readFileSync(absFilePath, 'utf8'); @@ -129,11 +101,11 @@ class Helper { }); } - public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) { + public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory): void { 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; @@ -153,9 +125,9 @@ class Helper { // Only keep the last to sections (final headers and body). if (!result.success) { - console.log('Stdout:', result.stdout); - console.log('Stderr:', result.stderr); - console.log('Error:', result.err); + this.logger.log('Stdout:', result.stdout); + this.logger.error('Stderr:', result.stderr); + this.logger.error('Error:', result.err); } expect(result.success).toBe(true); @@ -164,14 +136,14 @@ class Helper { }; } - public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string, isPublic = true, - legacy = false): CleanUpFn { + public writeBuildFile(pr: number, sha: string, relFilePath: string, content: string, isPublic = true, + legacy = false): void { const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy); const absFilePath = path.join(shaDir, relFilePath); - return this.writeFile(absFilePath, {content}, true); + this.writeFile(absFilePath, {content}, true); } - public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn { + public writeFile(filePath: string, {content, size}: FileSpecs, force = false): void { if (!force && fs.existsSync(filePath)) { throw new Error(`Refusing to overwrite existing file '${filePath}'.`); } @@ -189,9 +161,7 @@ class Helper { // Create a file with the specified content. fs.writeFileSync(filePath, content || ''); } - shell.exec(`chown ${this.wwwUser} ${filePath}`); - - return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget)); + shell.exec(`chown ${AIO_WWW_USER} ${filePath}`); } // Methods - Protected @@ -210,5 +180,70 @@ 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, { + defaultMethod = 'POST', + defaultOptions = '', + defaultHeaders = ['Content-Type: application/json'], + defaultData = {}, + defaultExtraPath = '', +}: DefaultCurlOptions = {}) { + return function curl({ + method = defaultMethod, + options = defaultOptions, + headers = defaultHeaders, + data = defaultData, + url = baseUrl, + extraPath = defaultExtraPath, + }: CurlOptions) { + const dataString = data ? JSON.stringify(data) : ''; + const cmd = `curl -iLX ${method} ` + + `${options} ` + + headers.map(header => `--header "${header}" `).join('') + + `--data '${dataString}' ` + + `${url}${extraPath}`; + return helper.runCmd(cmd); + }; +} + +export interface PayloadData { + data: { + payload: { + build_num: number, + build_parameters: { + CIRCLE_JOB: string; + }; + }; + }; +} + +export function payload(buildNum: number): PayloadData { + return { + data: { + payload: { + build_num: buildNum, + build_parameters: { CIRCLE_JOB: 'aio_preview' }, + }, + }, + }; +} + + // Exports export const helper = new Helper(); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/jasmine-custom-matchers-types.d.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/jasmine-custom-matchers-types.d.ts new file mode 100644 index 0000000000..8788325666 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/jasmine-custom-matchers-types.d.ts @@ -0,0 +1,7 @@ +declare module jasmine { + interface Matchers { + toExistAsAFile(remove = true): boolean; + toExistAsABuild(remove = true): boolean; + toExistAsAnArtifact(remove = true): boolean; + } +} \ No newline at end of file diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/jasmine-custom-matchers.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/jasmine-custom-matchers.ts new file mode 100644 index 0000000000..4767254b2d --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/jasmine-custom-matchers.ts @@ -0,0 +1,88 @@ +import {sync as deleteEmpty} from 'delete-empty'; +import {existsSync, unlinkSync} from 'fs'; +import {join} from 'path'; +import {AIO_DOWNLOADS_DIR} from '../common/constants'; +import {computeShortSha} from '../common/utils'; +import {SHA} from './constants'; +import {helper} from './helper'; + +function checkFile(filePath: string, remove: boolean): boolean { + const exists = existsSync(filePath); + if (exists && remove) { + // if we expected the file to exist then we remove it to prevent leftover file errors + unlinkSync(filePath); + } + return exists; +} + +function getArtifactPath(prNum: number, sha: string = SHA): string { + return `${AIO_DOWNLOADS_DIR}/${prNum}-${computeShortSha(sha)}-aio-snapshot.tgz`; +} + +function checkFiles(prNum: number, isPublic: boolean, sha: string, isLegacy: boolean, remove: boolean) { + const files = ['/index.html', '/foo/bar.js']; + const prPath = helper.getPrDir(prNum, isPublic); + const shaPath = helper.getShaDir(prPath, sha, isLegacy); + + const existingFiles: string[] = []; + const missingFiles: string[] = []; + files + .map(file => join(shaPath, file)) + .forEach(file => (checkFile(file, remove) ? existingFiles : missingFiles).push(file)); + + deleteEmpty(prPath); + + return { existingFiles, missingFiles }; +} + +class ToExistAsAFile implements jasmine.CustomMatcher { + public compare(actual: string, remove = true): jasmine.CustomMatcherResult { + const pass = checkFile(actual, remove); + return { + message: `Expected file at "${actual}" ${pass ? 'not' : ''} to exist`, + pass, + }; + } +} + +class ToExistAsAnArtifact implements jasmine.CustomMatcher { + public compare(actual: {prNum: number, sha?: string}, remove = true): jasmine.CustomMatcherResult { + const { prNum, sha = SHA } = actual; + const filePath = getArtifactPath(prNum, sha); + const pass = checkFile(filePath, remove); + return { + message: `Expected artifact "PR:${prNum}, SHA:${sha}, FILE:${filePath}" ${pass ? 'not' : '\b'} to exist`, + pass, + }; + } +} + +class ToExistAsABuild implements jasmine.CustomMatcher { + public compare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}, remove = true): + jasmine.CustomMatcherResult { + const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual; + const {missingFiles} = checkFiles(prNum, isPublic, sha, isLegacy, remove); + return { + message: `Expected files for build "PR:${prNum}, SHA:${sha}" to exist:\n` + + missingFiles.map(file => ` - ${file}`).join('\n'), + pass: missingFiles.length === 0, + }; + } + public negativeCompare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}): + jasmine.CustomMatcherResult { + const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual; + const { existingFiles } = checkFiles(prNum, isPublic, sha, isLegacy, false); + return { + message: `Expected files for build "PR:${prNum}, SHA:${sha}" not to exist:\n` + + existingFiles.map(file => ` - ${file}`).join('\n'), + pass: existingFiles.length === 0, + }; + } + +} + +export const customMatchers = { + toExistAsABuild: () => new ToExistAsABuild(), + toExistAsAFile: () => new ToExistAsAFile(), + toExistAsAnArtifact: () => new ToExistAsAnArtifact(), +}; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts new file mode 100644 index 0000000000..34875e22a0 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/mock-external-apis.ts @@ -0,0 +1,171 @@ +/* tslint:disable:max-line-length */ +import * as nock from 'nock'; +import * as tar from 'tar-stream'; +import {gzipSync} from 'zlib'; +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. +// This is necessary, because the test preview-server runs as a separate node process to +// the test harness, so we do not have direct access to the code (e.g. for mocking). +// (See also 'lib/verify-setup/start-test-preview-server.ts'.) + +// Each of the potential requests to an external API (e.g. Github or CircleCI) are mocked +// 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 = new Logger('mock-external-apis'); + +const log = (...args: any[]) => { + // Filter out non-matching URL checks + if (!/^matching.+: false$/.test(args[0])) { + logger.log(...args); + } +}; + +const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN'); +const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN'); + +const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH'); +const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION'); +const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO'); +const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL'); +const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(','); + +const ACTIVE_TRUSTED_USER = 'active-trusted-user'; +const INACTIVE_TRUSTED_USER = 'inactive-trusted-user'; +const UNTRUSTED_USER = 'untrusted-user'; + +const BASIC_BUILD_INFO = { + branch: `pull/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`, + failed: false, + reponame: AIO_GITHUB_REPO, + username: AIO_GITHUB_ORGANIZATION, + vcs_revision: SHA, +}; + +const ISSUE_INFO_TRUSTED_LABEL = { labels: [{ name: AIO_TRUSTED_PR_LABEL }], user: { login: UNTRUSTED_USER } }; +const ISSUE_INFO_ACTIVE_TRUSTED_USER = { labels: [], user: { login: ACTIVE_TRUSTED_USER } }; +const ISSUE_INFO_INACTIVE_TRUSTED_USER = { labels: [], user: { login: INACTIVE_TRUSTED_USER } }; +const ISSUE_INFO_UNTRUSTED = { labels: [], user: { login: UNTRUSTED_USER } }; +const ACTIVE_STATE = { state: 'active' }; +const INACTIVE_STATE = { state: 'inactive' }; + +const TEST_TEAM_INFO = AIO_GITHUB_TEAM_SLUGS.map((slug, index) => ({ slug, id: index })); + +const CIRCLE_CI_API_HOST = 'https://circleci.com'; +const CIRCLE_CI_TOKEN_PARAM = `circle-token=${AIO_CIRCLE_CI_TOKEN}`; +const ARTIFACT_1 = { path: 'artifact-1', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-1`, _urlPath: '/artifacts/artifact-1' }; +const ARTIFACT_2 = { path: 'artifact-2', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-2`, _urlPath: '/artifacts/artifact-2' }; +const ARTIFACT_3 = { path: 'artifact-3', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-3`, _urlPath: '/artifacts/artifact-3' }; +const ARTIFACT_ERROR = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/error`, _urlPath: '/artifacts/error' }; +const ARTIFACT_404 = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/404`, _urlPath: '/artifacts/404' }; +const ARTIFACT_VALID_TRUSTED_USER = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/user`, _urlPath: '/artifacts/valid/user' }; +const ARTIFACT_VALID_TRUSTED_LABEL = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/label`, _urlPath: '/artifacts/valid/label' }; +const ARTIFACT_VALID_UNTRUSTED = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/untrusted`, _urlPath: '/artifacts/valid/untrusted' }; + +const CIRCLE_CI_BUILD_INFO_URL = `/api/v1.1/project/github/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}`; + +const buildInfoUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}?${CIRCLE_CI_TOKEN_PARAM}`; +const buildArtifactsUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}/artifacts?${CIRCLE_CI_TOKEN_PARAM}`; +const buildInfo = (prNum: number) => ({ ...BASIC_BUILD_INFO, branch: `pull/${prNum}` }); + +const GITHUB_API_HOST = 'https://api.github.com'; +const GITHUB_ISSUES_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/issues`; +const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/pulls`; +const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`; + +const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`; +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}`; + +const createArchive = (buildNum: number, prNum: number, sha: string) => { + logger.log('createArchive', buildNum, prNum, sha); + const pack = tar.pack(); + pack.entry({name: 'index.html'}, `BUILD: ${buildNum} | PR: ${prNum} | SHA: ${sha} | File: /index.html`); + pack.entry({name: 'foo/bar.js'}, `BUILD: ${buildNum} | PR: ${prNum} | SHA: ${sha} | File: /foo/bar.js`); + pack.finalize(); + const zip = gzipSync(pack.read()); + return zip; +}; + +// Create request scopes +const circleCiApi = nock(CIRCLE_CI_API_HOST).log(log).persist(); +const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`); + +////////////////////////////// + +// GENERAL responses +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 +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_ERROR)).replyWithError('BUILD_INFO_ERROR'); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_404)).reply(404, 'BUILD_INFO_404'); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_BUILD_FAILED)).reply(200, { ...BASIC_BUILD_INFO, failed: true }); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_ORG)).reply(200, { ...BASIC_BUILD_INFO, username: 'bad' }); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_REPO)).reply(200, { ...BASIC_BUILD_INFO, reponame: 'bad' }); + +// CHANGED FILE errors +circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_ERROR)).reply(200, buildInfo(PrNums.CHANGED_FILES_ERROR)); +githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_ERROR)).replyWithError('CHANGED_FILES_ERROR'); +circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_404)).reply(200, buildInfo(PrNums.CHANGED_FILES_404)); +githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_404)).reply(404, 'CHANGED_FILES_404'); +circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_NONE)).reply(200, buildInfo(PrNums.CHANGED_FILES_NONE)); +githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_NONE)).reply(200, []); + +// ARTIFACT URL errors +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).replyWithError('BUILD_ARTIFACTS_ERROR'); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(404, 'BUILD_ARTIFACTS_ERROR'); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, []); +circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, [ARTIFACT_1, ARTIFACT_2, ARTIFACT_3]); + +// ARTIFACT DOWNLOAD errors +circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, [ARTIFACT_ERROR]); +circleCiApi.get(ARTIFACT_ERROR._urlPath).replyWithError(ARTIFACT_ERROR._urlPath); +circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)); +circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, [ARTIFACT_404]); +circleCiApi.get(ARTIFACT_ERROR._urlPath).reply(404, ARTIFACT_ERROR._urlPath); + +// TRUST CHECK errors +circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ERROR)); +githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ERROR)).reply(200, [{ filename: 'aio/a' }]); +circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, [ARTIFACT_VALID_TRUSTED_USER]); +githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ERROR)).replyWithError('TRUST_CHECK_ERROR'); + +// ACTIVE TRUSTED USER response +circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, BASIC_BUILD_INFO); +githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, [{ filename: 'aio/a' }]); +circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, [ARTIFACT_VALID_TRUSTED_USER]); +circleCiApi.get(ARTIFACT_VALID_TRUSTED_USER._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA)); +githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, ISSUE_INFO_ACTIVE_TRUSTED_USER); +githubApi.get(getTeamMembershipUrl(0, ACTIVE_TRUSTED_USER)).reply(200, ACTIVE_STATE); + +// TRUSTED LABEL response +circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, BASIC_BUILD_INFO); +githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, [{ filename: 'aio/a' }]); +circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, [ARTIFACT_VALID_TRUSTED_LABEL]); +circleCiApi.get(ARTIFACT_VALID_TRUSTED_LABEL._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_TRUSTED_LABEL, PrNums.TRUST_CHECK_TRUSTED_LABEL, SHA)); +githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, ISSUE_INFO_TRUSTED_LABEL); +githubApi.get(getTeamMembershipUrl(0, ACTIVE_TRUSTED_USER)).reply(200, ACTIVE_STATE); + +// INACTIVE TRUSTED USER response +circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, BASIC_BUILD_INFO); +githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, [{ filename: 'aio/a' }]); +circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, [ARTIFACT_VALID_TRUSTED_USER]); +githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, ISSUE_INFO_INACTIVE_TRUSTED_USER); +githubApi.get(getTeamMembershipUrl(0, INACTIVE_TRUSTED_USER)).reply(200, INACTIVE_STATE); + +// UNTRUSTED reponse +circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, buildInfo(PrNums.TRUST_CHECK_UNTRUSTED)); +githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, [{ filename: 'aio/a' }]); +circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, [ARTIFACT_VALID_UNTRUSTED]); +circleCiApi.get(ARTIFACT_VALID_UNTRUSTED._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_UNTRUSTED, PrNums.TRUST_CHECK_UNTRUSTED, SHA)); +githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, ISSUE_INFO_UNTRUSTED); +githubApi.get(getTeamMembershipUrl(0, UNTRUSTED_USER)).reply(404); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts index a3e15992f3..656973b205 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/nginx.e2e.ts @@ -1,17 +1,23 @@ // Imports 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'; // Tests describe(`nginx`, () => { - beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); + beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000); + beforeEach(() => jasmine.addMatchers(customMatchers)); afterEach(() => h.cleanUp()); it('should redirect HTTP to HTTPS', done => { - const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`; - const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`; + const httpHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTP}`; + const httpsHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTPS}`; const urlMap = { [`http://${httpHost}/`]: `https://${httpsHost}/`, [`http://${httpHost}/foo`]: `https://${httpsHost}/foo`, @@ -32,13 +38,13 @@ describe(`nginx`, () => { h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => { - const hostname = h.nginxHostname; + const hostname = AIO_NGINX_HOSTNAME; const host = `${hostname}:${port}`; - const pr = '9'; + const pr = 9; const sha9 = '9'.repeat(40); const sha0 = '0'.repeat(40); - const shortSha9 = h.getShordSha(sha9); - const shortSha0 = h.getShordSha(sha0); + const shortSha9 = computeShortSha(sha9); + const shortSha0 = computeShortSha(sha0); describe(`pr-.${host}/*`, () => { @@ -50,6 +56,11 @@ describe(`nginx`, () => { h.createDummyBuild(pr, sha0); }); + afterEach(() => { + expect({ prNum: pr, sha: sha9 }).toExistAsABuild(); + expect({ prNum: pr, sha: sha0 }).toExistAsABuild(); + }); + it('should return /index.html', done => { const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`; @@ -63,17 +74,19 @@ describe(`nginx`, () => { }); - it('should return /index.html (for legacy builds)', done => { + it('should return /index.html (for legacy builds)', async () => { const origin = `${scheme}://pr${pr}-${sha9}.${host}`; const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`); h.createDummyBuild(pr, sha9, true, false, true); - Promise.all([ + await Promise.all([ h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)), h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)), h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)), - ]).then(done); + ]); + + expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild(); }); @@ -86,15 +99,15 @@ describe(`nginx`, () => { }); - it('should return /foo/bar.js (for legacy builds)', done => { + it('should return /foo/bar.js (for legacy builds)', async () => { const origin = `${scheme}://pr${pr}-${sha9}.${host}`; const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`); h.createDummyBuild(pr, sha9, true, false, true); - h.runCmd(`curl -iL ${origin}/foo/bar.js`). - then(h.verifyResponse(200, bodyRegex)). - then(done); + await h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(h.verifyResponse(200, bodyRegex)); + + expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild(); }); @@ -126,7 +139,7 @@ describe(`nginx`, () => { it('should respond with 404 for unknown PRs/SHAs', done => { const otherPr = 54321; - const otherShortSha = h.getShordSha('8'.repeat(40)); + const otherShortSha = computeShortSha('8'.repeat(40)); Promise.all([ h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)), @@ -174,39 +187,41 @@ describe(`nginx`, () => { describe('(for hidden builds)', () => { - it('should respond with 404 for any file or directory', done => { + it('should respond with 404 for any file or directory', async () => { const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`; const assert404 = h.verifyResponse(404); h.createDummyBuild(pr, sha9, false); - expect(h.buildExists(pr, sha9, false)).toBe(true); - Promise.all([ + await Promise.all([ h.runCmd(`curl -iL ${origin}/index.html`).then(assert404), h.runCmd(`curl -iL ${origin}/`).then(assert404), h.runCmd(`curl -iL ${origin}`).then(assert404), h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404), h.runCmd(`curl -iL ${origin}/foo/`).then(assert404), h.runCmd(`curl -iL ${origin}/foo`).then(assert404), - ]).then(done); + ]); + + expect({ prNum: pr, sha: sha9, isPublic: false }).toExistAsABuild(); }); - it('should respond with 404 for any file or directory (for legacy builds)', done => { + it('should respond with 404 for any file or directory (for legacy builds)', async () => { const origin = `${scheme}://pr${pr}-${sha9}.${host}`; const assert404 = h.verifyResponse(404); h.createDummyBuild(pr, sha9, false, false, true); - expect(h.buildExists(pr, sha9, false, true)).toBe(true); - Promise.all([ + await Promise.all([ h.runCmd(`curl -iL ${origin}/index.html`).then(assert404), h.runCmd(`curl -iL ${origin}/`).then(assert404), h.runCmd(`curl -iL ${origin}`).then(assert404), h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404), h.runCmd(`curl -iL ${origin}/foo/`).then(assert404), h.runCmd(`curl -iL ${origin}/foo`).then(assert404), - ]).then(done); + ]); + + expect({ prNum: pr, sha: sha9, isPublic: false, isLegacy: true }).toExistAsABuild(); }); }); @@ -238,10 +253,46 @@ describe(`nginx`, () => { }); - describe(`${host}/create-build//`, () => { + 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 => { - const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; + const url = `${scheme}://${host}/circle-build`; Promise.all([ h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])), @@ -252,31 +303,9 @@ describe(`nginx`, () => { }); - it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => { - const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`; - const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; - - h.runCmd(`curl -iLX POST ${headers} ${url}`). - then(h.verifyResponse([413, 'Request Entity Too Large'])). - then(done); - }); - - - it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => { - const filePath = path.join(h.buildsDir, 'snapshot.tar.gz'); - const url = `${scheme}://${host}/create-build/${pr}/${sha9}`; - - h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize}); - - h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`). - then(h.verifyResponse([413, 'Request Entity Too Large'])). - then(done); - }); - - - it('should pass requests through to the upload server', done => { - h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)). + it('should pass requests through to the preview server', done => { + h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`). + then(h.verifyResponse(400, /Incorrect body content. Expected JSON/)). then(done); }); @@ -285,32 +314,14 @@ describe(`nginx`, () => { const cmdPrefix = `curl -iLX POST ${scheme}://${host}`; Promise.all([ - h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)), - ]).then(done); - }); - - - it('should reject PRs with leading zeros', done => { - h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`). - then(h.verifyResponse(404)). - then(done); - }); - - - it('should accept SHAs with leading zeros (but not trim the zeros)', done => { - const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`; - const bodyRegex = /Missing or empty 'AUTHORIZATION' header/; - - Promise.all([ - h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)), + h.runCmd(`${cmdPrefix}/foo/circle-build/`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/foo-circle-build/`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/fooncircle-build/`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/circle-build/foo/`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/circle-build-foo/`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/circle-buildnfoo/`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/circle-build/pr`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)), ]).then(done); }); @@ -331,17 +342,13 @@ describe(`nginx`, () => { }); - it('should pass requests through to the upload server', done => { + it('should pass requests through to the preview server', done => { const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`; const cmd1 = `${cmdPrefix} ${url}`; - const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`; - const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`; Promise.all([ h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)), - h.runCmd(cmd2).then(h.verifyResponse(200)), - h.runCmd(cmd3).then(h.verifyResponse(200)), ]).then(done); }); @@ -364,13 +371,15 @@ describe(`nginx`, () => { describe(`${host}/*`, () => { - it('should respond with 404 for unknown URLs (even if the resource exists)', done => { + beforeEach(() => { ['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => { - const absFilePath = path.join(h.buildsDir, relFilePath); - h.writeFile(absFilePath, {content: `File: /${relFilePath}`}); + const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath); + return h.writeFile(absFilePath, {content: `File: /${relFilePath}`}); }); + }); - Promise.all([ + it('should respond with 404 for unknown URLs (even if the resource exists)', async () => { + await Promise.all([ h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)), @@ -379,7 +388,14 @@ describe(`nginx`, () => { h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)), h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)), - ]).then(done); + ]); + }); + + afterEach(() => { + ['index.html', 'foo.js', 'foo/index.html', 'foo'].forEach(relFilePath => { + const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath); + rm('-r', absFilePath); + }); }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts new file mode 100644 index 0000000000..f9667a9d2d --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/preview-server.e2e.ts @@ -0,0 +1,569 @@ +// Imports +import * as fs from 'fs'; +import {join} from 'path'; +import {AIO_PREVIEW_SERVER_HOSTNAME, AIO_PREVIEW_SERVER_PORT, AIO_WWW_USER} from '../common/env-variables'; +import {computeShortSha} from '../common/utils'; +import {ALT_SHA, BuildNums, PrNums, SHA, SIMILAR_SHA} from './constants'; +import {helper as h, makeCurl, payload} from './helper'; +import {customMatchers} from './jasmine-custom-matchers'; + +// Tests +describe('preview-server', () => { + const hostname = AIO_PREVIEW_SERVER_HOSTNAME; + const port = AIO_PREVIEW_SERVER_PORT; + const host = `http://${hostname}:${port}`; + + beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000); + beforeEach(() => jasmine.addMatchers(customMatchers)); + 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`); + + it('should disallow non-POST requests', async () => { + const bodyRegex = /^Unknown resource/; + + await Promise.all([ + curl({method: 'GET'}).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 () => { + await Promise.all([ + curl({url: `${host}/foo/circle-build`}).then(h.verifyResponse(404)), + curl({url: `${host}/foo-circle-build`}).then(h.verifyResponse(404)), + curl({url: `${host}/fooncircle-build`}).then(h.verifyResponse(404)), + curl({url: `${host}/circle-build/foo`}).then(h.verifyResponse(404)), + curl({url: `${host}/circle-build-foo`}).then(h.verifyResponse(404)), + curl({url: `${host}/circle-buildnfoo`}).then(h.verifyResponse(404)), + curl({url: `${host}/circle-build/pr`}).then(h.verifyResponse(404)), + curl({url: `${host}/circle-build42`}).then(h.verifyResponse(404)), + ]); + }); + + it('should respond with 400 if the body is not valid', async () => { + await Promise.all([ + curl({ data: '' }).then(h.verifyResponse(400)), + curl({ data: {} }).then(h.verifyResponse(400)), + curl({ data: { payload: {} } }).then(h.verifyResponse(400)), + curl({ data: { payload: { build_num: 1 } } }).then(h.verifyResponse(400)), + curl({ data: { payload: { build_num: 1, build_parameters: {} } } }).then(h.verifyResponse(400)), + curl(payload(0)).then(h.verifyResponse(400)), + curl(payload(-1)).then(h.verifyResponse(400)), + ]); + }); + + it('should respond with 500 if the CircleCI API request errors', async () => { + await curl(payload(BuildNums.BUILD_INFO_ERROR)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.BUILD_INFO_404)).then(h.verifyResponse(500)); + }); + + it('should respond with 204 if the build on CircleCI failed', async () => { + await curl(payload(BuildNums.BUILD_INFO_BUILD_FAILED)).then(h.verifyResponse(204)); + }); + + it('should respond with 500 if the github org from CircleCI does not match what is configured', async () => { + await curl(payload(BuildNums.BUILD_INFO_INVALID_GH_ORG)).then(h.verifyResponse(500)); + }); + + it('should respond with 500 if the github repo from CircleCI does not match what is configured', async () => { + await curl(payload(BuildNums.BUILD_INFO_INVALID_GH_REPO)).then(h.verifyResponse(500)); + }); + + it('should respond with 500 if the github files API errors', async () => { + await curl(payload(BuildNums.CHANGED_FILES_ERROR)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.CHANGED_FILES_404)).then(h.verifyResponse(500)); + }); + + it('should respond with 204 if no significant files are changed by the PR', async () => { + await curl(payload(BuildNums.CHANGED_FILES_NONE)).then(h.verifyResponse(204)); + }); + + it('should respond with 500 if the CircleCI artifact API fails', async () => { + await curl(payload(BuildNums.BUILD_ARTIFACTS_ERROR)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.BUILD_ARTIFACTS_404)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.BUILD_ARTIFACTS_EMPTY)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.BUILD_ARTIFACTS_MISSING)).then(h.verifyResponse(500)); + }); + + it('should respond with 500 if fetching the artifact errors', async () => { + await curl(payload(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).then(h.verifyResponse(500)); + await curl(payload(BuildNums.DOWNLOAD_ARTIFACT_404)).then(h.verifyResponse(500)); + }); + + it('should respond with 500 if the GH trusted API fails', async () => { + await curl(payload(BuildNums.TRUST_CHECK_ERROR)).then(h.verifyResponse(500)); + expect({ prNum: PrNums.TRUST_CHECK_ERROR }).toExistAsAnArtifact(); + }); + + it('should respond with 201 if a new public build is created', async () => { + await curl(payload(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)) + .then(h.verifyResponse(201)); + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER }).toExistAsABuild(); + }); + + it('should respond with 202 if a new private build is created', async () => { + await curl(payload(BuildNums.TRUST_CHECK_UNTRUSTED)).then(h.verifyResponse(202)); + expect({ prNum: PrNums.TRUST_CHECK_UNTRUSTED, isPublic: false }).toExistAsABuild(); + }); + + [true].forEach(isPublic => { + const build = isPublic ? BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : BuildNums.TRUST_CHECK_UNTRUSTED; + const prNum = isPublic ? PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : PrNums.TRUST_CHECK_UNTRUSTED; + const label = isPublic ? 'public' : 'non-public'; + const overwriteRe = RegExp(`^Request to overwrite existing ${label} directory`); + const statusCode = isPublic ? 201 : 202; + + describe(`for ${label} builds`, () => { + + it('should extract the contents of the build artifact', async () => { + await curl(payload(build)) + .then(h.verifyResponse(statusCode)); + expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic)) + .toContain(`PR: ${prNum} | SHA: ${SHA} | File: /index.html`); + expect(h.readBuildFile(prNum, SHA, 'foo/bar.js', isPublic)) + .toContain(`PR: ${prNum} | SHA: ${SHA} | File: /foo/bar.js`); + expect({ prNum, isPublic }).toExistAsABuild(); + }); + + it(`should create files/directories owned by '${AIO_WWW_USER}'`, async () => { + await curl(payload(build)) + .then(h.verifyResponse(statusCode)); + + const shaDir = h.getShaDir(h.getPrDir(prNum, isPublic), SHA); + const { stdout: allFiles } = await h.runCmd(`find ${shaDir}`); + const { stdout: userFiles } = await h.runCmd(`find ${shaDir} -user ${AIO_WWW_USER}`); + + expect(userFiles).toBe(allFiles); + expect(userFiles).toContain(shaDir); + expect(userFiles).toContain(join(shaDir, 'index.html')); + expect(userFiles).toContain(join(shaDir, 'foo', 'bar.js')); + + expect({ prNum, isPublic }).toExistAsABuild(); + }); + + it('should delete the build artifact file', async () => { + await curl(payload(build)) + .then(h.verifyResponse(statusCode)); + expect({ prNum, SHA }).not.toExistAsAnArtifact(); + expect({ prNum, isPublic }).toExistAsABuild(); + }); + + it('should make the build directory non-writable', async () => { + await curl(payload(build)) + .then(h.verifyResponse(statusCode)); + + // See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588. + const isNotWritable = (fileOrDir: string) => { + const mode = fs.statSync(fileOrDir).mode; + // tslint:disable-next-line: no-bitwise + return !(mode & parseInt('222', 8)); + }; + + const shaDir = h.getShaDir(h.getPrDir(prNum, isPublic), SHA); + expect(isNotWritable(shaDir)).toBe(true); + expect(isNotWritable(join(shaDir, 'index.html'))).toBe(true); + expect(isNotWritable(join(shaDir, 'foo', 'bar.js'))).toBe(true); + + expect({ prNum, isPublic }).toExistAsABuild(); + }); + + it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)', + async () => { + // It is possible that 40-chars long build directories exist, if they had been deployed + // before implementing the shorter build directory names. In that case, we don't want the + // second (shorter) name to be considered the same as the old one (even if they originate + // from the same SHA). + + h.createDummyBuild(prNum, SHA, isPublic, false, true); + h.writeBuildFile(prNum, SHA, 'index.html', 'My content', isPublic, true); + expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, true)).toBe('My content'); + + await curl(payload(build)) + .then(h.verifyResponse(statusCode)); + + expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, false)).toContain('index.html'); + expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, true)).toBe('My content'); + + expect({ prNum, isPublic, sha: SHA, isLegacy: false }).toExistAsABuild(); + expect({ prNum, isPublic, sha: SHA, isLegacy: true }).toExistAsABuild(); + }); + + it(`should not overwrite existing builds`, async () => { + // setup a build already in place + h.createDummyBuild(prNum, SHA, isPublic); + // distinguish this build from the downloaded one + h.writeBuildFile(prNum, SHA, 'index.html', 'My content', isPublic); + await curl(payload(build)).then(h.verifyResponse(409, overwriteRe)); + expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic)).toBe('My content'); + expect({ prNum, isPublic }).toExistAsABuild(); + expect({ prNum }).toExistAsAnArtifact(); + }); + + it(`should not overwrite existing builds (even if the SHA is different)`, async () => { + // Since only the first few characters of the SHA are used, it is possible for two different + // SHAs to correspond to the same directory. In that case, we don't want the second SHA to + // overwrite the first. + expect(SIMILAR_SHA).not.toEqual(SHA); + expect(computeShortSha(SIMILAR_SHA)).toEqual(computeShortSha(SHA)); + h.createDummyBuild(prNum, SIMILAR_SHA, isPublic); + expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toContain('index.html'); + h.writeBuildFile(prNum, SIMILAR_SHA, 'index.html', 'My content', isPublic); + expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toBe('My content'); + + await curl(payload(build)).then(h.verifyResponse(409, overwriteRe)); + expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toBe('My content'); + expect({ prNum, isPublic, sha: SIMILAR_SHA }).toExistAsABuild(); + expect({ prNum, sha: SIMILAR_SHA }).toExistAsAnArtifact(); + }); + + it('should only delete the SHA directory on error (for existing PR)', async () => { + h.createDummyBuild(prNum, ALT_SHA, isPublic); + await curl(payload(BuildNums.TRUST_CHECK_ERROR)).then(h.verifyResponse(500)); + expect({ prNum: PrNums.TRUST_CHECK_ERROR }).toExistAsAnArtifact(); + expect({ prNum, isPublic, sha: SHA }).not.toExistAsABuild(); + expect({ prNum, isPublic, sha: ALT_SHA }).toExistAsABuild(); + }); + + describe('when the PR\'s visibility has changed', () => { + + it('should update the PR\'s visibility', async () => { + h.createDummyBuild(prNum, ALT_SHA, !isPublic); + await curl(payload(build)).then(h.verifyResponse(statusCode)); + expect({ prNum, isPublic }).toExistAsABuild(); + expect({ prNum, isPublic, sha: ALT_SHA }).toExistAsABuild(); + }); + + + it('should not overwrite existing builds (but keep the updated visibility)', async () => { + h.createDummyBuild(prNum, SHA, !isPublic); + await curl(payload(build)).then(h.verifyResponse(409)); + expect({ prNum, isPublic }).toExistAsABuild(); + expect({ prNum, isPublic: !isPublic }).not.toExistAsABuild(); + // since it errored we didn't clear up the downloaded artifact - perhaps we should? + expect({ prNum }).toExistAsAnArtifact(); + }); + + + it('should reject the request if it fails to update the PR\'s visibility', async () => { + // One way to cause an error is to have both a public and a hidden directory for the same PR. + h.createDummyBuild(prNum, ALT_SHA, isPublic); + h.createDummyBuild(prNum, ALT_SHA, !isPublic); + + const errorRegex = new RegExp(`^Request to move '${h.getPrDir(prNum, !isPublic)}' ` + + `to existing directory '${h.getPrDir(prNum, isPublic)}'.`); + + await curl(payload(build)).then(h.verifyResponse(409, errorRegex)); + + expect({ prNum, isPublic }).not.toExistAsABuild(); + + // The bad folders should have been deleted + expect({ prNum, sha: ALT_SHA, isPublic }).toExistAsABuild(); + expect({ prNum, sha: ALT_SHA, isPublic: !isPublic }).toExistAsABuild(); + + // since it errored we didn't clear up the downloaded artifact - perhaps we should? + expect({ prNum }).toExistAsAnArtifact(); + }); + }); + }); + }); + }); + + + describe(`${host}/health-check`, () => { + + it('should respond with 200', done => { + Promise.all([ + h.runCmd(`curl -iL ${host}/health-check`).then(h.verifyResponse(200)), + h.runCmd(`curl -iL ${host}/health-check/`).then(h.verifyResponse(200)), + ]).then(done); + }); + + + it('should respond with 404 if the path does not match exactly', done => { + Promise.all([ + h.runCmd(`curl -iL ${host}/health-check/foo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${host}/health-check-foo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${host}/health-checknfoo`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${host}/foo/health-check`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${host}/foo-health-check`).then(h.verifyResponse(404)), + h.runCmd(`curl -iL ${host}/foonhealth-check`).then(h.verifyResponse(404)), + ]).then(done); + }); + + }); + + + describe(`${host}/pr-updated`, () => { + const curl = makeCurl(`${host}/pr-updated`); + + it('should disallow non-POST requests', async () => { + const bodyRegex = /^Unknown resource in request/; + + await Promise.all([ + curl({method: 'GET'}).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 400 for requests without a payload', async () => { + const bodyRegex = /^Missing or empty 'number' field in request/; + + await Promise.all([ + curl({ data: '' }).then(h.verifyResponse(400, bodyRegex)), + curl({ data: {} }).then(h.verifyResponse(400, bodyRegex)), + ]); + }); + + + it('should respond with 400 for requests without a \'number\' field', async () => { + const bodyRegex = /^Missing or empty 'number' field in request/; + + await Promise.all([ + curl({ data: {} }).then(h.verifyResponse(400, bodyRegex)), + curl({ data: { number: null} }).then(h.verifyResponse(400, bodyRegex)), + ]); + }); + + + it('should reject requests for which checking the PR visibility fails', async () => { + await curl({ data: { number: PrNums.TRUST_CHECK_ERROR } }).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/)); + }); + + + it('should respond with 404 for unknown paths', done => { + const mockPayload = JSON.stringify({number: 1}); // MockExternalApiFlags.TRUST_CHECK_ACTIVE_TRUSTED_USER }); + const cmdPrefix = `curl -iLX POST --data "${mockPayload}" ${host}`; + + Promise.all([ + h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)), + h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)), + ]).then(done); + }); + + + it('should do nothing if PR\'s visibility is already up-to-date', async () => { + const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED; + + const checkVisibilities = (remove: boolean) => { + // Public build is already public. + expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild(remove); + expect({ prNum: publicPr, isPublic: true }).toExistAsABuild(remove); + // Hidden build is already hidden. + expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild(remove); + expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild(remove); + }; + + h.createDummyBuild(publicPr, SHA, true); + h.createDummyBuild(hiddenPr, SHA, false); + checkVisibilities(false); + + await Promise.all([ + curl({ data: {number: +publicPr, action: 'foo' } }).then(h.verifyResponse(200)), + curl({ data: {number: +hiddenPr, action: 'foo' } }).then(h.verifyResponse(200)), + ]); + + // Visibilities should not have changed, because the specified action could not have triggered a change. + checkVisibilities(true); + }); + + + it('should do nothing if \'action\' implies no visibility change', async () => { + const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED; + + const checkVisibilities = (remove: boolean) => { + // Public build is hidden atm. + expect({ prNum: publicPr, isPublic: false }).toExistAsABuild(remove); + expect({ prNum: publicPr, isPublic: true }).not.toExistAsABuild(remove); + // Hidden build is public atm. + expect({ prNum: hiddenPr, isPublic: false }).not.toExistAsABuild(remove); + expect({ prNum: hiddenPr, isPublic: true }).toExistAsABuild(remove); + }; + + h.createDummyBuild(publicPr, SHA, false); + h.createDummyBuild(hiddenPr, SHA, true); + checkVisibilities(false); + + await Promise.all([ + curl({ data: {number: +publicPr, action: 'foo' } }).then(h.verifyResponse(200)), + curl({ data: {number: +hiddenPr, action: 'foo' } }).then(h.verifyResponse(200)), + ]); + // Visibilities should not have changed, because the specified action could not have triggered a change. + checkVisibilities(true); + }); + + + describe('when the visiblity has changed', () => { + const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED; + + beforeEach(() => { + // Create initial PR builds with opposite visibilities as the ones that will be reported: + // - The now public PR was previously hidden. + // - The now hidden PR was previously public. + h.createDummyBuild(publicPr, SHA, false); + h.createDummyBuild(hiddenPr, SHA, true); + + expect({ prNum: publicPr, isPublic: false }).toExistAsABuild(false); + expect({ prNum: publicPr, isPublic: true }).not.toExistAsABuild(false); + expect({ prNum: hiddenPr, isPublic: false }).not.toExistAsABuild(false); + expect({ prNum: hiddenPr, isPublic: true }).toExistAsABuild(false); + }); + afterEach(() => { + // Expect PRs' visibility to have been updated: + // - The public PR should be actually public (previously it was hidden). + // - The hidden PR should be actually hidden (previously it was public). + expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild(); + expect({ prNum: publicPr, isPublic: true }).toExistAsABuild(); + expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild(); + expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild(); + }); + + + it('should update the PR\'s visibility (action: undefined)', async () => { + await Promise.all([ + curl({ data: {number: +publicPr } }).then(h.verifyResponse(200)), + curl({ data: {number: +hiddenPr } }).then(h.verifyResponse(200)), + ]); + }); + + + it('should update the PR\'s visibility (action: labeled)', async () => { + await Promise.all([ + curl({ data: {number: +publicPr, action: 'labeled' } }).then(h.verifyResponse(200)), + curl({ data: {number: +hiddenPr, action: 'labeled' } }).then(h.verifyResponse(200)), + ]); + }); + + + it('should update the PR\'s visibility (action: unlabeled)', async () => { + await Promise.all([ + curl({ data: {number: +publicPr, action: 'unlabeled' } }).then(h.verifyResponse(200)), + curl({ data: {number: +hiddenPr, action: 'unlabeled' } }).then(h.verifyResponse(200)), + ]); + }); + + }); + + }); + + + describe(`${host}/*`, () => { + + it('should respond with 404 for requests to unknown URLs', done => { + const bodyRegex = /^Unknown resource/; + + Promise.all([ + h.runCmd(`curl -iL ${host}/index.html`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iL ${host}/`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iL ${host}`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iLX PUT ${host}`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iLX POST ${host}`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iLX PATCH ${host}`).then(h.verifyResponse(404, bodyRegex)), + h.runCmd(`curl -iLX DELETE ${host}`).then(h.verifyResponse(404, bodyRegex)), + ]).then(done); + }); + + }); +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts index 6e9467e837..83755dda3a 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/server-integration.e2e.ts @@ -1,101 +1,80 @@ // Imports -import * as path from 'path'; -import * as c from './constants'; -import {helper as h} from './helper'; +import {AIO_NGINX_HOSTNAME} from '../common/env-variables'; +import {computeShortSha} from '../common/utils'; +import {ALT_SHA, BuildNums, PrNums, SHA} from './constants'; +import {helper as h, makeCurl, payload} from './helper'; +import {customMatchers} from './jasmine-custom-matchers'; // Tests h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => { - const hostname = h.nginxHostname; + const hostname = AIO_NGINX_HOSTNAME; const host = `${hostname}:${port}`; - const pr9 = '9'; - const sha9 = '9'.repeat(40); - const sha0 = '0'.repeat(40); - const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); + const curlPrUpdated = makeCurl(`${scheme}://${host}/pr-updated`); - const getFile = (pr: string, sha: string, file: string) => - h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`); - const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => { - const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`; - return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`); - }; - const prUpdated = (pr: number, action?: string) => { - const url = `${scheme}://${host}/pr-updated`; - const payloadStr = JSON.stringify({number: pr, action}); - return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`); - }; + const getFile = (pr: number, sha: string, file: string) => + h.runCmd(`curl -iL ${scheme}://pr${pr}-${computeShortSha(sha)}.${host}/${file}`); + const prUpdated = (prNum: number, action?: string) => curlPrUpdated({ data: { number: prNum, action } }); + const circleBuild = makeCurl(`${scheme}://${host}/circle-build`); - beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); - afterEach(() => { - h.deletePrDir(pr9); - h.deletePrDir(pr9, false); - h.cleanUp(); + beforeEach(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.addMatchers(customMatchers); }); + afterEach(() => h.cleanUp()); describe('for a new/non-existing PR', () => { - it('should be able to upload and serve a public build', done => { - const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + it('should be able to create and serve a public preview', async () => { + const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; - h.createDummyArchive(pr9, sha9, archivePath); + const regexPrefix = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`; + const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`); + const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`); - uploadBuild(pr9, sha9, archivePath). - then(() => Promise.all([ - getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), - getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), - ])). - then(done); + await circleBuild(payload(BUILD)).then(h.verifyResponse(201)); + await Promise.all([ + getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)), + getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)), + ]); + + expect({ prNum: PR }).toExistAsABuild(); + expect({ prNum: PR, isPublic: false }).not.toExistAsABuild(); }); - it('should be able to upload but not serve a hidden build', done => { - const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + it('should be able to create but not serve a hidden preview', async () => { + const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED; + const PR = PrNums.TRUST_CHECK_UNTRUSTED; - h.createDummyArchive(pr9, sha9, archivePath); + await circleBuild(payload(BUILD)).then(h.verifyResponse(202)); + await Promise.all([ + getFile(PR, SHA, 'index.html').then(h.verifyResponse(404)), + getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(404)), + ]); - uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted). - then(() => Promise.all([ - getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)), - getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)), - ])). - then(() => { - expect(h.buildExists(pr9, sha9)).toBe(false); - expect(h.buildExists(pr9, sha9, false)).toBe(true); - expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9); - expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9); - }). - then(done); + expect({ prNum: PR }).not.toExistAsABuild(); + expect({ prNum: PR, isPublic: false }).toExistAsABuild(); }); - it('should reject an upload if verification fails', done => { - const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`); + it('should reject if verification fails', async () => { + const BUILD = BuildNums.TRUST_CHECK_ERROR; + const PR = PrNums.TRUST_CHECK_ERROR; - h.createDummyArchive(pr9, sha9, archivePath); - - uploadBuild(pr9, sha9, archivePath, c.BV_verify_error). - then(h.verifyResponse(403, errorRegex9)). - then(() => { - expect(h.buildExists(pr9)).toBe(false); - expect(h.buildExists(pr9, '', false)).toBe(false); - }). - then(done); + await circleBuild(payload(BUILD)).then(h.verifyResponse(500)); + expect({ prNum: PR }).toExistAsAnArtifact(); + expect({ prNum: PR }).not.toExistAsABuild(); + expect({ prNum: PR, isPublic: false }).not.toExistAsABuild(); }); - it('should be able to notify that a PR has been updated (and do nothing)', done => { - prUpdated(+pr9). - then(h.verifyResponse(200)). - then(() => { - // The PR should still not exist. - expect(h.buildExists(pr9, '', false)).toBe(false); - expect(h.buildExists(pr9, '', true)).toBe(false); - }). - then(done); + it('should be able to notify that a PR has been updated (and do nothing)', async () => { + await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(200)); + // The PR should still not exist. + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).not.toExistAsABuild(); + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).not.toExistAsABuild(); }); }); @@ -103,215 +82,186 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme describe('for an existing PR', () => { - it('should be able to upload and serve a public build', done => { - const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`; - const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`); - const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`); + it('should be able to create and serve a public preview', async () => { + const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; - const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + const regexPrefix1 = `^PR: ${PR} \\| SHA: ${ALT_SHA} \\| File:`; + const idxContentRegex1 = new RegExp(`${regexPrefix1} \\/index\\.html$`); + const barContentRegex1 = new RegExp(`${regexPrefix1} \\/foo\\/bar\\.js$`); - h.createDummyBuild(pr9, sha0); - h.createDummyArchive(pr9, sha9, archivePath); + const regexPrefix2 = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`; + const idxContentRegex2 = new RegExp(`${regexPrefix2} \\/index\\.html$`); + const barContentRegex2 = new RegExp(`${regexPrefix2} \\/foo\\/bar\\.js$`); - uploadBuild(pr9, sha9, archivePath). - then(() => Promise.all([ - getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)), - getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)), - getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), - getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), - ])). - then(done); + h.createDummyBuild(PR, ALT_SHA); + await circleBuild(payload(BUILD)).then(h.verifyResponse(201)); + await Promise.all([ + getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex1)), + getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex1)), + getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex2)), + getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex2)), + ]); + + expect({ prNum: PR, sha: SHA }).toExistAsABuild(); + expect({ prNum: PR, sha: ALT_SHA }).toExistAsABuild(); }); - it('should be able to upload but not serve a hidden build', done => { - const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`; - const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`); - const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`); + it('should be able to create but not serve a hidden preview', async () => { + const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED; + const PR = PrNums.TRUST_CHECK_UNTRUSTED; - const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + h.createDummyBuild(PR, ALT_SHA, false); + await circleBuild(payload(BUILD)).then(h.verifyResponse(202)); - h.createDummyBuild(pr9, sha0, false); - h.createDummyArchive(pr9, sha9, archivePath); + await Promise.all([ + getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(404)), + getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(404)), + getFile(PR, SHA, 'index.html').then(h.verifyResponse(404)), + getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(404)), + ]); - uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted). - then(() => Promise.all([ - getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)), - getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)), - getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)), - getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)), - ])). - then(() => { - expect(h.buildExists(pr9, sha9)).toBe(false); - expect(h.buildExists(pr9, sha9, false)).toBe(true); - expect(h.readBuildFile(pr9, sha0, 'index.html', false)).toMatch(idxContentRegex0); - expect(h.readBuildFile(pr9, sha0, 'foo/bar.js', false)).toMatch(barContentRegex0); - expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9); - expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9); - }). - then(done); + expect({ prNum: PR, sha: SHA }).not.toExistAsABuild(); + expect({ prNum: PR, sha: SHA, isPublic: false }).toExistAsABuild(); + expect({ prNum: PR, sha: ALT_SHA }).not.toExistAsABuild(); + expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild(); }); - it('should reject an upload if verification fails', done => { - const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`); + it('should reject if verification fails', async () => { + const BUILD = BuildNums.TRUST_CHECK_ERROR; + const PR = PrNums.TRUST_CHECK_ERROR; - h.createDummyBuild(pr9, sha0); - h.createDummyArchive(pr9, sha9, archivePath); + h.createDummyBuild(PR, ALT_SHA, false); - uploadBuild(pr9, sha9, archivePath, c.BV_verify_error). - then(h.verifyResponse(403, errorRegex9)). - then(() => { - expect(h.buildExists(pr9)).toBe(true); - expect(h.buildExists(pr9, sha0)).toBe(true); - expect(h.buildExists(pr9, sha9)).toBe(false); - }). - then(done); + await circleBuild(payload(BUILD)).then(h.verifyResponse(500)); + expect({ prNum: PR }).toExistAsAnArtifact(); + expect({ prNum: PR }).not.toExistAsABuild(); + expect({ prNum: PR, isPublic: false }).not.toExistAsABuild(); + expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild(); }); - it('should not be able to overwrite an existing public build', done => { - const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + it('should not be able to overwrite an existing public preview', async () => { + const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; - h.createDummyBuild(pr9, sha9); - h.createDummyArchive(pr9, sha9, archivePath); + const regexPrefix = `^PR: ${PR} \\| SHA: ${SHA} \\| File:`; + const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`); + const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`); - uploadBuild(pr9, sha9, archivePath). - then(h.verifyResponse(409)). - then(() => Promise.all([ - getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)), - getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)), - ])). - then(done); + h.createDummyBuild(PR, SHA); + + await circleBuild(payload(BUILD)).then(h.verifyResponse(409)); + await Promise.all([ + getFile(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)), + getFile(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)), + ]); + + expect({ prNum: PR }).toExistAsAnArtifact(); + expect({ prNum: PR }).toExistAsABuild(); }); - it('should not be able to overwrite an existing hidden build', done => { - const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`; - const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`); - const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`); + it('should not be able to overwrite an existing hidden preview', async () => { + const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED; + const PR = PrNums.TRUST_CHECK_UNTRUSTED; + h.createDummyBuild(PR, SHA, false); - h.createDummyBuild(pr9, sha9, false); - h.createDummyArchive(pr9, sha9, archivePath); + await circleBuild(payload(BUILD)).then(h.verifyResponse(409)); - uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted). - then(h.verifyResponse(409)). - then(() => { - expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9); - expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9); - }). - then(done); + expect({ prNum: PR }).toExistAsAnArtifact(); + expect({ prNum: PR, isPublic: false }).toExistAsABuild(); }); - it('should be able to request re-checking visibility (if outdated)', done => { - const publicPr = pr9; - const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); + it('should be able to request re-checking visibility (if outdated)', async () => { + const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED; - h.createDummyBuild(publicPr, sha9, false); - h.createDummyBuild(hiddenPr, sha9, true); + h.createDummyBuild(publicPr, SHA, false); + h.createDummyBuild(hiddenPr, SHA, true); // PR visibilities are outdated (i.e. the opposte of what the should). - expect(h.buildExists(publicPr, '', false)).toBe(true); - expect(h.buildExists(publicPr, '', true)).toBe(false); - expect(h.buildExists(hiddenPr, '', false)).toBe(false); - expect(h.buildExists(hiddenPr, '', true)).toBe(true); + expect({ prNum: publicPr, sha: SHA, isPublic: false }).toExistAsABuild(false); + expect({ prNum: publicPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false); + expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false); + expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).toExistAsABuild(false); - Promise. - all([ - prUpdated(+publicPr).then(h.verifyResponse(200)), - prUpdated(+hiddenPr).then(h.verifyResponse(200)), - ]). - then(() => { - // PR visibilities should have been updated. - expect(h.buildExists(publicPr, '', false)).toBe(false); - expect(h.buildExists(publicPr, '', true)).toBe(true); - expect(h.buildExists(hiddenPr, '', false)).toBe(true); - expect(h.buildExists(hiddenPr, '', true)).toBe(false); - }). - then(() => { - h.deletePrDir(publicPr, true); - h.deletePrDir(hiddenPr, false); - }). - then(done); + await Promise.all([ + prUpdated(publicPr).then(h.verifyResponse(200)), + prUpdated(hiddenPr).then(h.verifyResponse(200)), + ]); + + // PR visibilities should have been updated. + expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild(); + expect({ prNum: publicPr, isPublic: true }).toExistAsABuild(); + expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild(); + expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild(); }); - it('should be able to request re-checking visibility (if up-to-date)', done => { - const publicPr = pr9; - const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); + it('should be able to request re-checking visibility (if up-to-date)', async () => { + const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER; + const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED; - h.createDummyBuild(publicPr, sha9, true); - h.createDummyBuild(hiddenPr, sha9, false); + h.createDummyBuild(publicPr, SHA, true); + h.createDummyBuild(hiddenPr, SHA, false); // PR visibilities are already up-to-date. - expect(h.buildExists(publicPr, '', false)).toBe(false); - expect(h.buildExists(publicPr, '', true)).toBe(true); - expect(h.buildExists(hiddenPr, '', false)).toBe(true); - expect(h.buildExists(hiddenPr, '', true)).toBe(false); + expect({ prNum: publicPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false); + expect({ prNum: publicPr, sha: SHA, isPublic: true }).toExistAsABuild(false); + expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).toExistAsABuild(false); + expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false); - Promise. - all([ - prUpdated(+publicPr).then(h.verifyResponse(200)), - prUpdated(+hiddenPr).then(h.verifyResponse(200)), - ]). - then(() => { - // PR visibilities are still up-to-date. - expect(h.buildExists(publicPr, '', false)).toBe(false); - expect(h.buildExists(publicPr, '', true)).toBe(true); - expect(h.buildExists(hiddenPr, '', false)).toBe(true); - expect(h.buildExists(hiddenPr, '', true)).toBe(false); - }). - then(done); + await Promise.all([ + prUpdated(publicPr).then(h.verifyResponse(200)), + prUpdated(hiddenPr).then(h.verifyResponse(200)), + ]); + + // PR visibilities are still up-to-date. + expect({ prNum: publicPr, isPublic: true }).toExistAsABuild(); + expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild(); + expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild(); + expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild(); }); - it('should reject a request if re-checking visibility fails', done => { - const errorPr = String(c.BV_getPrIsTrusted_error); + it('should reject a request if re-checking visibility fails', async () => { + const errorPr = PrNums.TRUST_CHECK_ERROR; - h.createDummyBuild(errorPr, sha9, true); + h.createDummyBuild(errorPr, SHA, true); - expect(h.buildExists(errorPr, '', false)).toBe(false); - expect(h.buildExists(errorPr, '', true)).toBe(true); + expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild(false); + expect({ prNum: errorPr, isPublic: true }).toExistAsABuild(false); - prUpdated(+errorPr). - then(h.verifyResponse(500, /Test/)). - then(() => { - // PR visibility should not have been updated. - expect(h.buildExists(errorPr, '', false)).toBe(false); - expect(h.buildExists(errorPr, '', true)).toBe(true); - }). - then(done); + await prUpdated(errorPr).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/)); + + // PR visibility should not have been updated. + expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild(); + expect({ prNum: errorPr, isPublic: true }).toExistAsABuild(); }); - it('should reject a request if updating visibility fails', done => { + it('should reject a request if updating visibility fails', async () => { // One way to cause an error is to have both a public and a hidden directory for the same PR. - h.createDummyBuild(pr9, sha9, false); - h.createDummyBuild(pr9, sha9, true); + h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, false); + h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, true); - const hiddenPrDir = h.getPrDir(pr9, false); - const publicPrDir = h.getPrDir(pr9, true); + const hiddenPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, false); + const publicPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, true); const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`); - expect(h.buildExists(pr9, '', false)).toBe(true); - expect(h.buildExists(pr9, '', true)).toBe(true); + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild(false); + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild(false); - prUpdated(+pr9). - then(h.verifyResponse(409, bodyRegex)). - then(() => { - // PR visibility should not have been updated. - expect(h.buildExists(pr9, '', false)).toBe(true); - expect(h.buildExists(pr9, '', true)).toBe(true); - }). - then(done); + await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(409, bodyRegex)); + + // PR visibility should not have been updated. + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild(); + expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild(); }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/start-test-preview-server.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/start-test-preview-server.ts new file mode 100644 index 0000000000..cc1cfd19de --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/start-test-preview-server.ts @@ -0,0 +1,2 @@ +import '../preview-server'; +import './mock-external-apis'; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/start-test-upload-server.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/start-test-upload-server.ts deleted file mode 100644 index 3450d1bc0c..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/start-test-upload-server.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Imports -import {GithubPullRequests} from '../common/github-pull-requests'; -import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier'; -import {UploadError} from '../upload-server/upload-error'; -import * as c from './constants'; - -// Run -// TODO(gkalpak): Add e2e tests to cover these interactions as well. -GithubPullRequests.prototype.addComment = () => Promise.resolve(); -BuildVerifier.prototype.getPrIsTrusted = (pr: number) => { - switch (pr) { - case c.BV_getPrIsTrusted_error: - // For e2e tests, fake an error. - return Promise.reject('Test'); - case c.BV_getPrIsTrusted_notTrusted: - // For e2e tests, fake an untrusted PR (`false`). - return Promise.resolve(false); - default: - // For e2e tests, default to trusted PRs (`true`). - return Promise.resolve(true); - } -}; -BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => { - switch (authHeader) { - case c.BV_verify_error: - // For e2e tests, fake a verification error. - return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`)); - case c.BV_verify_verifiedNotTrusted: - // For e2e tests, fake a `verifiedNotTrusted` verification status. - return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted); - default: - // For e2e tests, default to `verifiedAndTrusted` verification status. - return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted); - } -}; - -// tslint:disable-next-line: no-var-requires -require('../upload-server/index'); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/tar-stream.d.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/tar-stream.d.ts new file mode 100644 index 0000000000..99a50ab35d --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/tar-stream.d.ts @@ -0,0 +1,30 @@ +declare module 'tar-stream' { + + import {Readable, Writable} from 'stream'; + + export interface Pack extends Readable { + entry(header: Header, callback?: (err?: any) => {}): Writable; + entry(header: Header, contents: string, callback?: (err?: any) => {}): Writable; + entry(header: Header, buffer: Buffer, callback?: (err?: any) => {}): Writable; + entry(header: Header, buffer: string|Buffer, callback?: (err?: any) => {}): Writable; + finalize(); + destroy(err: any); + } + + export interface Header { + name: string; + mode?: number; + uid?: number; + gid?: number; + size?: number; + mtime?: Date; + type?: type; + linkname?: string; + uname?: string; + gname?: string; + devmajor?: number; + devminor?: number; + } + + export function pack(): Pack; +} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts deleted file mode 100644 index 002d93de3e..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/upload-server.e2e.ts +++ /dev/null @@ -1,571 +0,0 @@ -// Imports -import * as fs from 'fs'; -import * as path from 'path'; -import * as c from './constants'; -import {CmdResult, helper as h} from './helper'; - -// Tests -describe('upload-server (on HTTP)', () => { - const hostname = h.uploadHostname; - const port = h.uploadPort; - const host = `${hostname}:${port}`; - const pr = '9'; - const sha9 = '9'.repeat(40); - const sha0 = '0'.repeat(40); - - beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000); - afterEach(() => h.cleanUp()); - - - describe(`${host}/create-build//`, () => { - const authorizationHeader = `--header "Authorization: Token FOO"`; - const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`; - const defaultHeaders = `${authorizationHeader} ${xFileHeader}`; - const curl = (url: string, headers = defaultHeaders) => `curl -iL ${headers} ${url}`; - - - it('should disallow non-GET requests', done => { - const url = `http://${host}/create-build/${pr}/${sha9}`; - const bodyRegex = /^Unknown resource/; - - Promise.all([ - h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)), - ]).then(done); - }); - - - it('should reject requests without an \'AUTHORIZATION\' header', done => { - const headers1 = ''; - const headers2 = '--header "AUTHORIXATION: "'; - const url = `http://${host}/create-build/${pr}/${sha9}`; - const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/; - - Promise.all([ - h.runCmd(curl(url, headers1)).then(h.verifyResponse(401, bodyRegex)), - h.runCmd(curl(url, headers2)).then(h.verifyResponse(401, bodyRegex)), - ]).then(done); - }); - - - it('should reject requests without an \'X-FILE\' header', done => { - const headers1 = authorizationHeader; - const headers2 = `${authorizationHeader} --header "X-FILE: "`; - const url = `http://${host}/create-build/${pr}/${sha9}`; - const bodyRegex = /^Missing or empty 'X-FILE' header/; - - Promise.all([ - h.runCmd(curl(url, headers1)).then(h.verifyResponse(400, bodyRegex)), - h.runCmd(curl(url, headers2)).then(h.verifyResponse(400, bodyRegex)), - ]).then(done); - }); - - - it('should reject requests for which the PR verification fails', done => { - const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`; - const url = `http://${host}/create-build/${pr}/${sha9}`; - const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`); - - h.runCmd(curl(url, headers)). - then(h.verifyResponse(403, bodyRegex)). - then(done); - }); - - - it('should respond with 404 for unknown paths', done => { - const cmdPrefix = curl(`http://${host}`); - - Promise.all([ - h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)), - ]).then(done); - }); - - - it('should reject PRs with leading zeros', done => { - h.runCmd(curl(`http://${host}/create-build/0${pr}/${sha9}`)). - then(h.verifyResponse(404)). - then(done); - }); - - - it('should accept SHAs with leading zeros (but not trim the zeros)', done => { - Promise.all([ - h.runCmd(curl(`http://${host}/create-build/${pr}/0${sha9}`)).then(h.verifyResponse(404)), - h.runCmd(curl(`http://${host}/create-build/${pr}/${sha9}`)).then(h.verifyResponse(500)), - h.runCmd(curl(`http://${host}/create-build/${pr}/${sha0}`)).then(h.verifyResponse(500)), - ]).then(done); - }); - - - [true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => { - const authorizationHeader2 = isPublic ? - authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`; - const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`); - const overwriteRe = RegExp(`^Request to overwrite existing ${isPublic ? 'public' : 'non-public'} directory`); - - - it('should not overwrite existing builds', done => { - h.createDummyBuild(pr, sha9, isPublic); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html'); - - h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content'); - - h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(409, overwriteRe)). - then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')). - then(done); - }); - - - it('should not overwrite existing builds (even if the SHA is different)', done => { - // Since only the first few characters of the SHA are used, it is possible for two different - // SHAs to correspond to the same directory. In that case, we don't want the second SHA to - // overwrite the first. - - const sha9Almost = sha9.replace(/.$/, '8'); - expect(sha9Almost).not.toBe(sha9); - - h.createDummyBuild(pr, sha9, isPublic); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html'); - - h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content'); - - h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9Almost}`). - then(h.verifyResponse(409, overwriteRe)). - then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')). - then(done); - }); - - - it('should delete the PR directory on error (for new PR)', done => { - h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(500)). - then(() => expect(h.buildExists(pr, '', isPublic)).toBe(false)). - then(done); - }); - - - it('should only delete the SHA directory on error (for existing PR)', done => { - h.createDummyBuild(pr, sha0, isPublic); - - h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(500)). - then(() => { - expect(h.buildExists(pr, sha9, isPublic)).toBe(false); - expect(h.buildExists(pr, '', isPublic)).toBe(true); - }). - then(done); - }); - - - describe('on successful upload', () => { - const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); - const statusCode = isPublic ? 201 : 202; - let uploadPromise: Promise; - - beforeEach(() => { - h.createDummyArchive(pr, sha9, archivePath); - uploadPromise = h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`); - }); - afterEach(() => h.deletePrDir(pr, isPublic)); - - - it(`should respond with ${statusCode}`, done => { - uploadPromise.then(h.verifyResponse(statusCode)).then(done); - }); - - - it('should extract the contents of the uploaded file', done => { - uploadPromise. - then(() => { - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`); - expect(h.readBuildFile(pr, sha9, 'foo/bar.js', isPublic)).toContain(`uploaded/${pr}`); - }). - then(done); - }); - - - it(`should create files/directories owned by '${h.wwwUser}'`, done => { - const prDir = h.getPrDir(pr, isPublic); - const shaDir = h.getShaDir(prDir, sha9); - const idxPath = path.join(shaDir, 'index.html'); - const barPath = path.join(shaDir, 'foo', 'bar.js'); - - uploadPromise. - then(() => Promise.all([ - h.runCmd(`find ${shaDir}`), - h.runCmd(`find ${shaDir} -user ${h.wwwUser}`), - ])). - then(([{stdout: allFiles}, {stdout: userFiles}]) => { - expect(userFiles).toBe(allFiles); - expect(userFiles).toContain(shaDir); - expect(userFiles).toContain(idxPath); - expect(userFiles).toContain(barPath); - }). - then(done); - }); - - - it('should delete the uploaded file', done => { - expect(fs.existsSync(archivePath)).toBe(true); - uploadPromise. - then(() => expect(fs.existsSync(archivePath)).toBe(false)). - then(done); - }); - - - it('should make the build directory non-writable', done => { - const prDir = h.getPrDir(pr, isPublic); - const shaDir = h.getShaDir(prDir, sha9); - const idxPath = path.join(shaDir, 'index.html'); - const barPath = path.join(shaDir, 'foo', 'bar.js'); - - // See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588. - const isNotWritable = (fileOrDir: string) => { - const mode = fs.statSync(fileOrDir).mode; - // tslint:disable-next-line: no-bitwise - return !(mode & parseInt('222', 8)); - }; - - uploadPromise. - then(() => { - expect(isNotWritable(shaDir)).toBe(true); - expect(isNotWritable(idxPath)).toBe(true); - expect(isNotWritable(barPath)).toBe(true); - }). - then(done); - }); - - - it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)', done => { - // It is possible that 40-chars long build directories exist, if they had been deployed - // before implementing the shorter build directory names. In that case, we don't want the - // second (shorter) name to be considered the same as the old one (even if they originate - // from the same SHA). - - h.createDummyBuild(pr, sha9, isPublic, false, true); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toContain('index.html'); - - h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic, true); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content'); - - h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`). - then(h.verifyResponse(statusCode)). - then(() => { - expect(h.buildExists(pr, sha9, isPublic)).toBe(true); - expect(h.buildExists(pr, sha9, isPublic, true)).toBe(true); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html'); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content'); - }). - then(done); - }); - - }); - - - describe('when the PR\'s visibility has changed', () => { - const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz'); - const statusCode = isPublic ? 201 : 202; - - const checkPrVisibility = (isPublic2: boolean) => { - expect(h.buildExists(pr, '', isPublic2)).toBe(true); - expect(h.buildExists(pr, '', !isPublic2)).toBe(false); - expect(h.buildExists(pr, sha0, isPublic2)).toBe(true); - expect(h.buildExists(pr, sha0, !isPublic2)).toBe(false); - }; - const uploadBuild = (sha: string) => h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha}`); - - beforeEach(() => { - h.createDummyBuild(pr, sha0, !isPublic); - h.createDummyArchive(pr, sha9, archivePath); - checkPrVisibility(!isPublic); - }); - afterEach(() => h.deletePrDir(pr, isPublic)); - - - it('should update the PR\'s visibility', done => { - uploadBuild(sha9). - then(h.verifyResponse(statusCode)). - then(() => { - checkPrVisibility(isPublic); - expect(h.buildExists(pr, sha9, isPublic)).toBe(true); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`); - expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(sha9); - }). - then(done); - }); - - - it('should not overwrite existing builds (but keep the updated visibility)', done => { - expect(h.buildExists(pr, sha0, isPublic)).toBe(false); - - uploadBuild(sha0). - then(h.verifyResponse(409, overwriteRe)). - then(() => { - checkPrVisibility(isPublic); - expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr); - expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(`uploaded/${pr}`); - expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(sha0); - expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(sha9); - }). - then(done); - }); - - - it('should reject the request if it fails to update the PR\'s visibility', done => { - // One way to cause an error is to have both a public and a hidden directory for the same PR. - h.createDummyBuild(pr, sha0, isPublic); - - expect(h.buildExists(pr, sha0, isPublic)).toBe(true); - expect(h.buildExists(pr, sha0, !isPublic)).toBe(true); - - const errorRegex = new RegExp(`^Request to move '${h.getPrDir(pr, !isPublic)}' ` + - `to existing directory '${h.getPrDir(pr, isPublic)}'.`); - - uploadBuild(sha9). - then(h.verifyResponse(409, errorRegex)). - then(() => { - expect(h.buildExists(pr, sha0, isPublic)).toBe(true); - expect(h.buildExists(pr, sha0, !isPublic)).toBe(true); - expect(h.buildExists(pr, sha9, isPublic)).toBe(false); - expect(h.buildExists(pr, sha9, !isPublic)).toBe(false); - }). - then(done); - }); - - }); - - })); - - }); - - - describe(`${host}/health-check`, () => { - - it('should respond with 200', done => { - Promise.all([ - h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)), - h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)), - ]).then(done); - }); - - - it('should respond with 404 if the path does not match exactly', done => { - Promise.all([ - h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)), - h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)), - ]).then(done); - }); - - }); - - - describe(`${host}/pr-updated`, () => { - const url = `http://${host}/pr-updated`; - - // Helpers - const curl = (payload?: {number: number, action?: string}) => { - const payloadStr = payload && JSON.stringify(payload) || ''; - return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`; - }; - - - it('should disallow non-POST requests', done => { - const bodyRegex = /^Unknown resource in request/; - - Promise.all([ - h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)), - ]).then(done); - }); - - - it('should respond with 400 for requests without a payload', done => { - const bodyRegex = /^Missing or empty 'number' field in request/; - - h.runCmd(curl()). - then(h.verifyResponse(400, bodyRegex)). - then(done); - }); - - - it('should respond with 400 for requests without a \'number\' field', done => { - const bodyRegex = /^Missing or empty 'number' field in request/; - - Promise.all([ - h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)), - h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)), - ]).then(done); - }); - - - it('should reject requests for which checking the PR visibility fails', done => { - h.runCmd(curl({number: c.BV_getPrIsTrusted_error})). - then(h.verifyResponse(500, /Test/)). - then(done); - }); - - - it('should respond with 404 for unknown paths', done => { - const mockPayload = JSON.stringify({number: +pr}); - const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`; - - Promise.all([ - h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)), - h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)), - ]).then(done); - }); - - - it('should do nothing if PR\'s visibility is already up-to-date', done => { - const publicPr = pr; - const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); - const checkVisibilities = () => { - // Public build is already public. - expect(h.buildExists(publicPr, '', false)).toBe(false); - expect(h.buildExists(publicPr, '', true)).toBe(true); - // Hidden build is already hidden. - expect(h.buildExists(hiddenPr, '', false)).toBe(true); - expect(h.buildExists(hiddenPr, '', true)).toBe(false); - }; - - h.createDummyBuild(publicPr, sha9, true); - h.createDummyBuild(hiddenPr, sha9, false); - checkVisibilities(); - - Promise. - all([ - h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)), - h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)), - ]). - // Visibilities should not have changed, because the specified action could not have triggered a change. - then(checkVisibilities). - then(done); - }); - - - it('should do nothing if \'action\' implies no visibility change', done => { - const publicPr = pr; - const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); - const checkVisibilities = () => { - // Public build is hidden atm. - expect(h.buildExists(publicPr, '', false)).toBe(true); - expect(h.buildExists(publicPr, '', true)).toBe(false); - // Hidden build is public atm. - expect(h.buildExists(hiddenPr, '', false)).toBe(false); - expect(h.buildExists(hiddenPr, '', true)).toBe(true); - }; - - h.createDummyBuild(publicPr, sha9, false); - h.createDummyBuild(hiddenPr, sha9, true); - checkVisibilities(); - - Promise. - all([ - h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)), - h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)), - ]). - // Visibilities should not have changed, because the specified action could not have triggered a change. - then(checkVisibilities). - then(done); - }); - - - describe('when the visiblity has changed', () => { - const publicPr = pr; - const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted); - - beforeEach(() => { - // Create initial PR builds with opposite visibilities as the ones that will be reported: - // - The now public PR was previously hidden. - // - The now hidden PR was previously public. - h.createDummyBuild(publicPr, sha9, false); - h.createDummyBuild(hiddenPr, sha9, true); - - expect(h.buildExists(publicPr, '', false)).toBe(true); - expect(h.buildExists(publicPr, '', true)).toBe(false); - expect(h.buildExists(hiddenPr, '', false)).toBe(false); - expect(h.buildExists(hiddenPr, '', true)).toBe(true); - }); - afterEach(() => { - // Expect PRs' visibility to have been updated: - // - The public PR should be actually public (previously it was hidden). - // - The hidden PR should be actually hidden (previously it was public). - expect(h.buildExists(publicPr, '', false)).toBe(false); - expect(h.buildExists(publicPr, '', true)).toBe(true); - expect(h.buildExists(hiddenPr, '', false)).toBe(true); - expect(h.buildExists(hiddenPr, '', true)).toBe(false); - - h.deletePrDir(publicPr, true); - h.deletePrDir(hiddenPr, false); - }); - - - it('should update the PR\'s visibility (action: undefined)', done => { - Promise.all([ - h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)), - h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)), - ]).then(done); - }); - - - it('should update the PR\'s visibility (action: labeled)', done => { - Promise.all([ - h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)), - h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)), - ]).then(done); - }); - - - it('should update the PR\'s visibility (action: unlabeled)', done => { - Promise.all([ - h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)), - h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)), - ]).then(done); - }); - - }); - - }); - - - describe(`${host}/*`, () => { - - it('should respond with 404 for requests to unknown URLs', done => { - const bodyRegex = /^Unknown resource/; - - Promise.all([ - h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)), - h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)), - ]).then(done); - }); - - }); - -}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/package.json b/aio/aio-builds-setup/dockerbuild/scripts-js/package.json index 05ba8a807c..fcb6ee94dc 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/package.json +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/package.json @@ -7,39 +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", - "express": "^4.15.4", - "jasmine": "^2.8.0", - "jsonwebtoken": "^8.0.1", - "shelljs": "^0.7.8", - "tslib": "^1.7.1" + "body-parser": "^1.18.3", + "delete-empty": "^2.0.0", + "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/jsonwebtoken": "^7.2.3", - "@types/node": "^8.0.30", + "@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" } } diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts index df9b7e074a..398d52f5ff 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/clean-up/build-cleaner.spec.ts @@ -1,135 +1,186 @@ // Imports import * as fs from 'fs'; -import * as path from 'path'; +import {normalize} from 'path'; 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 = [ + '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(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345')); - + beforeEach(() => { + loggerErrorSpy = spyOn(Logger.prototype, 'error'); + loggerLogSpy = spyOn(Logger.prototype, 'log'); + cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip'); + }); describe('constructor()', () => { it('should throw if \'buildsDir\' is empty', () => { - expect(() => new BuildCleaner('', '/baz/qux', '12345')). + expect(() => new BuildCleaner('', 'baz', 'qux', '12345', 'downloads', 'build.zip')). toThrowError('Missing or empty required parameter \'buildsDir\'!'); }); - it('should throw if \'repoSlug\' is empty', () => { - expect(() => new BuildCleaner('/foo/bar', '', '12345')). - toThrowError('Missing or empty required parameter \'repoSlug\'!'); + it('should throw if \'githubOrg\' is empty', () => { + expect(() => new BuildCleaner('/foo/bar', '', 'qux', '12345', 'downloads', 'build.zip')). + toThrowError('Missing or empty required parameter \'githubOrg\'!'); + }); + + + it('should throw if \'githubRepo\' is empty', () => { + expect(() => new BuildCleaner('/foo/bar', 'baz', '', '12345', 'downloads', 'build.zip')). + toThrowError('Missing or empty required parameter \'githubRepo\'!'); }); it('should throw if \'githubToken\' is empty', () => { - expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')). + expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '', 'downloads', 'build.zip')). 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\'!'); + }); + }); describe('cleanUp()', () => { let cleanerGetExistingBuildNumbersSpy: jasmine.Spy; let cleanerGetOpenPrNumbersSpy: jasmine.Spy; + let cleanerGetExistingDownloadsSpy: jasmine.Spy; let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy; - let existingBuildsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void}; - let openPrsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void}; - let promise: Promise; + let cleanerRemoveUnnecessaryDownloadsSpy: jasmine.Spy; beforeEach(() => { - cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => { - return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject}); - }); - cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => { - return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject}); - }); - cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds'); + cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers') + .and.callFake(() => Promise.resolve(EXISTING_BUILDS)); + cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers') + .and.callFake(() => Promise.resolve(OPEN_PRS)); + cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads') + .and.callFake(() => Promise.resolve(EXISTING_DOWNLOADS)); + + cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner, 'removeUnnecessaryBuilds'); + cleanerRemoveUnnecessaryDownloadsSpy = spyOn(cleaner, 'removeUnnecessaryDownloads'); - promise = cleaner.cleanUp(); }); - 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; }); - it('should get the existing builds', () => { - expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled(); - }); - - - it('should get the open PRs', () => { + it('should get the open PRs', async () => { + await cleaner.cleanUp(); expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled(); }); - it('should reject if \'getExistingBuildNumbers()\' rejects', done => { - promise.catch(err => { + it('should get the existing builds', async () => { + await cleaner.cleanUp(); + expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled(); + }); + + + it('should get the existing downloads', async () => { + await cleaner.cleanUp(); + expect(cleanerGetExistingDownloadsSpy).toHaveBeenCalled(); + }); + + + it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', async () => { + await cleaner.cleanUp(); + expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith(EXISTING_BUILDS, OPEN_PRS); + }); + + + it('should pass existing downloads and open PRs to \'removeUnnecessaryDownloads()\'', async () => { + await cleaner.cleanUp(); + expect(cleanerRemoveUnnecessaryDownloadsSpy).toHaveBeenCalledWith(EXISTING_DOWNLOADS, OPEN_PRS); + }); + + + it('should reject if \'getOpenPrNumbers()\' rejects', async () => { + try { + cleanerGetOpenPrNumbersSpy.and.callFake(() => Promise.reject('Test')); + await cleaner.cleanUp(); + } catch (err) { expect(err).toBe('Test'); - done(); - }); - - existingBuildsDeferred.reject('Test'); + } }); - it('should reject if \'getOpenPrNumbers()\' rejects', done => { - promise.catch(err => { + it('should reject if \'getExistingBuildNumbers()\' rejects', async () => { + try { + cleanerGetExistingBuildNumbersSpy.and.callFake(() => Promise.reject('Test')); + await cleaner.cleanUp(); + } catch (err) { expect(err).toBe('Test'); - done(); - }); - - openPrsDeferred.reject('Test'); + } }); - it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => { - promise.catch(err => { + it('should reject if \'getExistingDownloads()\' rejects', async () => { + try { + cleanerGetExistingDownloadsSpy.and.callFake(() => Promise.reject('Test')); + await cleaner.cleanUp(); + } catch (err) { expect(err).toBe('Test'); - done(); - }); - - cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test')); - existingBuildsDeferred.resolve(); - openPrsDeferred.resolve(); + } }); - it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => { - promise.then(() => { - expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar'); - done(); - }); - - existingBuildsDeferred.resolve('foo'); - openPrsDeferred.resolve('bar'); + it('should reject if \'removeUnnecessaryBuilds()\' rejects', async () => { + try { + cleanerRemoveUnnecessaryBuildsSpy.and.callFake(() => Promise.reject('Test')); + await cleaner.cleanUp(); + } catch (err) { + expect(err).toBe('Test'); + } }); - it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => { - promise.then(result => { - expect(result as any).toBe('Test'); - done(); - }); - - cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test')); - existingBuildsDeferred.resolve(); - openPrsDeferred.resolve(); + it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => { + try { + cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test')); + await cleaner.cleanUp(); + } catch (err) { + expect(err).toBe('Test'); + } }); }); - // Protected methods - describe('getExistingBuildNumbers()', () => { let fsReaddirSpy: jasmine.Spy; let readdirCb: (err: any, files?: string[]) => void; @@ -137,7 +188,7 @@ describe('BuildCleaner', () => { beforeEach(() => { fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb); - promise = (cleaner as any).getExistingBuildNumbers(); + promise = cleaner.getExistingBuildNumbers(); }); @@ -203,7 +254,7 @@ describe('BuildCleaner', () => { return new Promise((resolve, reject) => prDeferred = {resolve, reject}); }); - promise = (cleaner as any).getOpenPrNumbers(); + promise = cleaner.getOpenPrNumbers(); }); @@ -236,6 +287,68 @@ 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(loggerLogSpy).toHaveBeenCalledWith( + ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`); + }); + }); + + }); + + + describe('getExistingDownloads()', () => { + let fsReaddirSpy: jasmine.Spy; + let readdirCb: (err: any, files?: string[]) => void; + let promise: Promise; + + beforeEach(() => { + fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb); + promise = cleaner.getExistingDownloads(); + }); + + + it('should return a promise', () => { + expect(promise).toEqual(jasmine.any(Promise)); + }); + + + it('should get the contents of the downloads directory', () => { + expect(fsReaddirSpy).toHaveBeenCalled(); + expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads'); + }); + + + it('should reject if an error occurs while getting the files', done => { + promise.catch(err => { + expect(err).toBe('Test'); + done(); + }); + + readdirCb('Test'); + }); + + + it('should resolve with the returned file names', done => { + promise.then(result => { + expect(result).toEqual(EXISTING_DOWNLOADS); + done(); + }); + + readdirCb(null, EXISTING_DOWNLOADS); + }); + + + it('should ignore files that do not match the artifactPath', done => { + promise.then(result => { + expect(result).toEqual(['10-ABCDEF-build.zip', '30-FFFFFFF-build.zip']); + done(); + }); + + readdirCb(null, ['10-ABCDEF-build.zip', '20-AAAAAAA-otherfile.zip', '30-FFFFFFF-build.zip']); + }); + }); @@ -253,7 +366,7 @@ describe('BuildCleaner', () => { it('should test if the directory exists (and return if is does not)', () => { shellTestSpy.and.returnValue(false); - (cleaner as any).removeDir('/foo/bar'); + cleaner.removeDir('/foo/bar'); expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar'); expect(shellChmodSpy).not.toHaveBeenCalled(); @@ -262,99 +375,127 @@ describe('BuildCleaner', () => { it('should remove the specified directory and its content', () => { - (cleaner as any).removeDir('/foo/bar'); + cleaner.removeDir('/foo/bar'); expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar'); }); it('should make the directory and its content writable before removing', () => { shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar')); - (cleaner as any).removeDir('/foo/bar'); + cleaner.removeDir('/foo/bar'); expect(shellRmSpy).toHaveBeenCalled(); }); it('should catch errors and log them', () => { - const consoleErrorSpy = spyOn(console, 'error'); shellRmSpy.and.callFake(() => { // tslint:disable-next-line: no-string-throw throw 'Test'; }); - (cleaner as any).removeDir('/foo/bar'); + cleaner.removeDir('/foo/bar'); - expect(consoleErrorSpy).toHaveBeenCalled(); - expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\''); - expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test'); + expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test'); }); }); describe('removeUnnecessaryBuilds()', () => { - let consoleLogSpy: jasmine.Spy; let cleanerRemoveDirSpy: jasmine.Spy; beforeEach(() => { - consoleLogSpy = spyOn(console, 'log'); - cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir'); + cleanerRemoveDirSpy = spyOn(cleaner, 'removeDir'); }); - it('should log the number of existing builds, open PRs and builds to be removed', () => { - (cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]); + 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('Existing builds: 3'); - expect(console.log).toHaveBeenCalledWith('Open pull requests: 4'); - expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2'); + expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3'); + expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2'); }); it('should construct full paths to directories (by prepending \'buildsDir\')', () => { - (cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []); + cleaner.removeUnnecessaryBuilds([1, 2, 3], []); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3')); }); it('should try removing hidden directories as well', () => { - (cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []); + cleaner.removeUnnecessaryBuilds([1, 2, 3], []); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`)); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); }); it('should remove the builds that do not correspond to open PRs', () => { - (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]); + cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]); expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); cleanerRemoveDirSpy.calls.reset(); - (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]); + cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]); expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0); cleanerRemoveDirSpy.calls.reset(); (cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []); expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/4')); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`)); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); - expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/4')); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`)); + expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`)); cleanerRemoveDirSpy.calls.reset(); }); }); + + describe('removeUnnecessaryDownloads()', () => { + let shellRmSpy: jasmine.Spy; + + beforeEach(() => { + 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(shellRmSpy).toHaveBeenCalledTimes(2); + expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip')); + expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip')); + }); + + }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts new file mode 100644 index 0000000000..7bd3b5b820 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/circleci-api.spec.ts @@ -0,0 +1,134 @@ +import * as nock from 'nock'; +import {CircleCiApi} from '../../lib/common/circle-ci-api'; + +const ORG = 'testorg'; +const REPO = 'testrepo'; +const TOKEN = 'xxxx'; +const BASE_URL = `https://circleci.com/api/v1.1/project/github/${ORG}/${REPO}`; + +describe('CircleCIApi', () => { + describe('constructor()', () => { + it('should throw if \'githubOrg\' is missing or empty', () => { + expect(() => new CircleCiApi('', REPO, TOKEN)). + toThrowError('Missing or empty required parameter \'githubOrg\'!'); + }); + + it('should throw if \'githubRepo\' is missing or empty', () => { + expect(() => new CircleCiApi(ORG, '', TOKEN)). + toThrowError('Missing or empty required parameter \'githubRepo\'!'); + }); + + it('should throw if \'circleCiToken\' is missing or empty', () => { + expect(() => new CircleCiApi(ORG, REPO, '')). + toThrowError('Missing or empty required parameter \'circleCiToken\'!'); + }); + }); + + describe('getBuildInfo', () => { + it('should make a request to the CircleCI API for the given build number', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const buildNum = 12345; + const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum }; + + const request = nock(BASE_URL) + .get(`/${buildNum}?circle-token=${TOKEN}`) + .reply(200, expectedBuildInfo); + + const buildInfo = await api.getBuildInfo(buildNum); + expect(buildInfo).toEqual(expectedBuildInfo); + request.done(); + }); + + it('should throw an error if the request fails', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const buildNum = 12345; + const errorMessage = 'Invalid request'; + const request = nock(BASE_URL).get(`/${buildNum}?circle-token=${TOKEN}`); + + try { + request.replyWithError(errorMessage); + await api.getBuildInfo(buildNum); + throw new Error('Exception Expected'); + } catch (err) { + expect(err.message).toEqual( + `CircleCI build info request failed ` + + `(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`); + } + + try { + request.reply(404, errorMessage); + await api.getBuildInfo(buildNum); + throw new Error('Exception Expected'); + } catch (err) { + expect(err.message).toEqual( + `CircleCI build info request failed ` + + `(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`); + } + }); + }); + + describe('getBuildArtifactUrl', () => { + it('should make a request to the CircleCI API for the given build number', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const buildNum = 12345; + const artifact0: any = { path: 'some/path/0', url: 'https://url/0' }; + const artifact1: any = { path: 'some/path/1', url: 'https://url/1' }; + const artifact2: any = { path: 'some/path/2', url: 'https://url/2' }; + const request = nock(BASE_URL) + .get(`/${buildNum}/artifacts?circle-token=${TOKEN}`) + .reply(200, [artifact0, artifact1, artifact2]); + + const artifactUrl = await api.getBuildArtifactUrl(buildNum, 'some/path/1'); + expect(artifactUrl).toEqual('https://url/1'); + request.done(); + }); + + + it('should throw an error if the request fails', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const buildNum = 12345; + const errorMessage = 'Invalid request'; + const request = nock(BASE_URL).get(`/${buildNum}/artifacts?circle-token=${TOKEN}`); + + try { + request.replyWithError(errorMessage); + await api.getBuildArtifactUrl(buildNum, 'some/path/1'); + throw new Error('Exception Expected'); + } catch (err) { + expect(err.message).toEqual( + `CircleCI artifact URL request failed ` + + `(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`); + } + + try { + request.reply(404, errorMessage); + await api.getBuildArtifactUrl(buildNum, 'some/path/1'); + throw new Error('Exception Expected'); + } catch (err) { + expect(err.message).toEqual( + `CircleCI artifact URL request failed ` + + `(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`); + } + }); + + it('should throw an error if the response does not contain the specified artifact', async () => { + const api = new CircleCiApi(ORG, REPO, TOKEN); + const buildNum = 12345; + const artifact0: any = { path: 'some/path/0', url: 'https://url/0' }; + const artifact1: any = { path: 'some/path/1', url: 'https://url/1' }; + const artifact2: any = { path: 'some/path/2', url: 'https://url/2' }; + nock(BASE_URL) + .get(`/${buildNum}/artifacts?circle-token=${TOKEN}`) + .reply(200, [artifact0, artifact1, artifact2]); + + try { + await api.getBuildArtifactUrl(buildNum, 'some/path/3'); + throw new Error('Exception Expected'); + } catch (err) { + expect(err.message).toEqual( + `CircleCI artifact URL request failed ` + + `(Missing artifact (some/path/3) for CircleCI build: ${buildNum})`); + } + }); + }); +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts index 65ff54f61a..b512c1507e 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-api.spec.ts @@ -1,7 +1,5 @@ // Imports -import {EventEmitter} from 'events'; -import {ClientRequest, IncomingMessage} from 'http'; -import * as https from 'https'; +import * as nock from 'nock'; import {GithubApi} from '../../lib/common/github-api'; // Tests @@ -110,39 +108,6 @@ describe('GithubApi', () => { }); - // Protected methods - - describe('buildPath()', () => { - - it('should return the pathname if no params', () => { - expect((api as any).buildPath('/foo')).toBe('/foo'); - expect((api as any).buildPath('/foo', undefined)).toBe('/foo'); - expect((api as any).buildPath('/foo', null)).toBe('/foo'); - }); - - - it('should append the params to the pathname', () => { - expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz'); - }); - - - it('should join the params with \'&\'', () => { - expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2'); - }); - - - it('should ignore undefined/null params', () => { - expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo'); - }); - - - it('should encode param values as URI components', () => { - expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z'); - }); - - }); - - describe('getPaginated()', () => { let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[]; @@ -161,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}); }); @@ -197,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); @@ -218,191 +183,162 @@ describe('GithubApi', () => { }); - describe('request()', () => { - let httpsRequestSpy: jasmine.Spy; - let latestRequest: ClientRequest; + // Protected methods - beforeEach(() => { - const originalRequest = https.request; + describe('buildPath()', () => { - httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => { - latestRequest = originalRequest.apply(https, args); - - spyOn(latestRequest, 'on').and.callThrough(); - spyOn(latestRequest, 'end'); - - return latestRequest; - }); + it('should return the pathname if no params', () => { + expect((api as any).buildPath('/foo')).toBe('/foo'); + expect((api as any).buildPath('/foo', undefined)).toBe('/foo'); + expect((api as any).buildPath('/foo', null)).toBe('/foo'); }); + it('should append the params to the pathname', () => { + expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz'); + }); + + + it('should join the params with \'&\'', () => { + expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2'); + }); + + + it('should ignore undefined/null params', () => { + expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo'); + }); + + + it('should encode param values as URI components', () => { + expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z'); + }); + + }); + + describe('request()', () => { it('should return a promise', () => { + nock('https://api.github.com').get('').reply(200); expect((api as any).request()).toEqual(jasmine.any(Promise)); }); it('should call \'https.request()\' with the correct options', () => { - (api as any).request('method', 'path'); + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(200); - expect(httpsRequestSpy).toHaveBeenCalled(); - expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ - headers: jasmine.objectContaining({ - 'User-Agent': `Node/${process.versions.node}`, - }), - host: 'api.github.com', - method: 'method', - path: 'path', - })); + (api as any).request('method', '/path'); + requestHandler.done(); }); - it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => { - (api as any).request('method', 'path'); - - expect(httpsRequestSpy).toHaveBeenCalled(); - expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({ - Authorization: 'token 12345', - })); + it('should add the \'Authorization\' header containing the \'githubToken\'', () => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method', undefined, { + reqheaders: {Authorization: 'token 12345'}, + }) + .reply(200); + (api as any).request('method', '/path'); + requestHandler.done(); }); - it('should reject on request error', done => { - (api as any).request('method', 'path').catch((err: any) => { - expect(err).toBe('Test'); - done(); - }); - - latestRequest.emit('error', 'Test'); - }); - - - it('should send the request (i.e. call \'end()\')', () => { - (api as any).request('method', 'path'); - expect(latestRequest.end).toHaveBeenCalled(); + it('should reject on request error', async () => { + nock('https://api.github.com') + .intercept('/path', 'method') + .replyWithError('Test'); + let message = 'Failed to reject error'; + await (api as any).request('method', '/path').catch((err: any) => message = err.message); + expect(message).toEqual('Test'); }); it('should \'JSON.stringify\' and send the data along with the request', () => { - (api as any).request('method', 'path'); - expect(latestRequest.end).toHaveBeenCalledWith(null); - - (api as any).request('method', 'path', {key: 'value'}); - expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}'); + const data = {key: 'value'}; + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method', JSON.stringify(data)) + .reply(200); + (api as any).request('method', '/path', data); + requestHandler.done(); }); - describe('onResponse', () => { - let promise: Promise; - let respond: (statusCode: number) => IncomingMessage; + it('should reject if response statusCode is <200', done => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(199); - beforeEach(() => { - promise = (api as any).request('method', 'path'); - - respond = (statusCode: number) => { - const mockResponse = new EventEmitter() as IncomingMessage; - mockResponse.statusCode = statusCode; - - const onResponse = httpsRequestSpy.calls.argsFor(0)[1]; - onResponse(mockResponse); - - return mockResponse; - }; - }); - - - it('should reject on response error', done => { - promise.catch(err => { - expect(err).toBe('Test'); - done(); - }); - - const res = respond(200); - res.emit('error', 'Test'); - }); - - - it('should reject if returned statusCode is <200', done => { - promise.catch(err => { + (api as any).request('method', '/path') + .catch((err: string) => { expect(err).toContain('failed'); expect(err).toContain('status: 199'); done(); }); - - const res = respond(199); - res.emit('end'); - }); + requestHandler.done(); + }); - it('should reject if returned statusCode is >=400', done => { - promise.catch(err => { + it('should reject if response statusCode is >=400', done => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(400); + + (api as any).request('method', '/path') + .catch((err: string) => { expect(err).toContain('failed'); expect(err).toContain('status: 400'); done(); }); - - const res = respond(400); - res.emit('end'); - }); + requestHandler.done(); + }); - it('should include the response text in the rejection message', done => { - promise.catch(err => { + it('should include the response text in the rejection message', done => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(500, 'Test'); + + (api as any).request('method', '/path') + .catch((err: string) => { expect(err).toContain('Test'); done(); }); + requestHandler.done(); + }); - const res = respond(500); - res.emit('data', 'Test'); - res.emit('end'); + + it('should resolve if returned statusCode is >=200 and <400', done => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(200); + + (api as any).request('method', '/path').then(done); + requestHandler.done(); + }); + + + it('should parse the response body into an object using \'JSON.parse\'', done => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(300, '{"foo": "bar"}'); + + (api as any).request('method', '/path').then((data: any) => { + expect(data).toEqual({foo: 'bar'}); + done(); }); + requestHandler.done(); + }); + it('should reject if the response text is malformed JSON', done => { + const requestHandler = nock('https://api.github.com') + .intercept('/path', 'method') + .reply(300, '}'); - it('should resolve if returned statusCode is <=200 <400', done => { - promise.then(done); - - const res = respond(200); - res.emit('data', '{}'); - res.emit('end'); + (api as any).request('method', '/path').catch((err: any) => { + expect(err).toEqual(jasmine.any(SyntaxError)); + done(); }); - - - it('should resolve with the response text \'JSON.parsed\'', done => { - promise.then(data => { - expect(data).toEqual({foo: 'bar'}); - done(); - }); - - const res = respond(300); - res.emit('data', '{"foo":"bar"}'); - res.emit('end'); - }); - - - it('should collect and concatenate the whole response text', done => { - promise.then(data => { - expect(data).toEqual({foo: 'bar', baz: 'qux'}); - done(); - }); - - const res = respond(300); - res.emit('data', '{"foo":'); - res.emit('data', '"bar","baz"'); - res.emit('data', ':"qux"}'); - res.emit('end'); - }); - - - it('should reject if the response text is malformed JSON', done => { - promise.catch(err => { - expect(err).toEqual(jasmine.any(SyntaxError)); - done(); - }); - - const res = respond(300); - res.emit('data', '}'); - res.emit('end'); - }); - + requestHandler.done(); }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts index db0f70dd03..27e265b9f1 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-pull-requests.spec.ts @@ -1,20 +1,27 @@ // Imports +import {GithubApi} from '../../lib/common/github-api'; import {GithubPullRequests} from '../../lib/common/github-pull-requests'; // Tests describe('GithubPullRequests', () => { + let githubApi: jasmine.SpyObj; + + beforeEach(() => { + githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']); + }); + describe('constructor()', () => { - it('should throw if \'githubToken\' is missing or empty', () => { - expect(() => new GithubPullRequests('', 'foo/bar')). - toThrowError('Missing or empty required parameter \'githubToken\'!'); + it('should throw if \'githubOrg\' is missing or empty', () => { + expect(() => new GithubPullRequests(githubApi, '', 'bar')). + toThrowError('Missing or empty required parameter \'githubOrg\'!'); }); - it('should throw if \'repoSlug\' is missing or empty', () => { - expect(() => new GithubPullRequests('12345', '')). - toThrowError('Missing or empty required parameter \'repoSlug\'!'); + it('should throw if \'githubRepo\' is missing or empty', () => { + expect(() => new GithubPullRequests(githubApi, 'foo', '')). + toThrowError('Missing or empty required parameter \'githubRepo\'!'); }); }); @@ -22,17 +29,9 @@ describe('GithubPullRequests', () => { describe('addComment()', () => { let prs: GithubPullRequests; - let deferred: {resolve: (v: any) => void, reject: (v: any) => void}; beforeEach(() => { - prs = new GithubPullRequests('12345', 'foo/bar'); - - spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject})); - }); - - - it('should return a promise', () => { - expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise)); + prs = new GithubPullRequests(githubApi, 'foo', 'bar'); }); @@ -47,30 +46,28 @@ describe('GithubPullRequests', () => { }); - it('should call \'post()\' with the correct pathname, params and data', () => { + it('should make a POST request to Github with the correct pathname, params and data', () => { + githubApi.post.and.callFake(() => Promise.resolve()); prs.addComment(42, 'body'); - - expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'}); + expect(githubApi.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'}); }); it('should reject if the request fails', done => { + githubApi.post.and.callFake(() => Promise.reject('Test')); prs.addComment(42, 'body').catch(err => { expect(err).toBe('Test'); done(); }); - - deferred.reject('Test'); }); - it('should resolve with the returned response', done => { + it('should resolve with the data from the Github POST', done => { + githubApi.post.and.callFake(() => Promise.resolve('Test')); prs.addComment(42, 'body').then(data => { - expect(data as any).toBe('Test'); + expect(data).toBe('Test'); done(); }); - - deferred.resolve('Test'); }); }); @@ -78,23 +75,25 @@ describe('GithubPullRequests', () => { describe('fetch()', () => { let prs: GithubPullRequests; - let prsGetSpy: jasmine.Spy; beforeEach(() => { - prs = new GithubPullRequests('12345', 'foo/bar'); - prsGetSpy = spyOn(prs as any, 'get'); + prs = new GithubPullRequests(githubApi, 'foo', 'bar'); }); - it('should call \'get()\' with the correct pathname', () => { + it('should make a GET request to GitHub with the correct pathname', () => { prs.fetch(42); - expect(prsGetSpy).toHaveBeenCalledWith('/repos/foo/bar/issues/42'); + expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/issues/42'); }); - it('should forward the value returned by \'get()\'', () => { - prsGetSpy.and.returnValue('Test'); - expect(prs.fetch(42) as any).toBe('Test'); + it('should resolve with the data returned from GitHub', done => { + const expected: any = {number: 42}; + githubApi.get.and.callFake(() => Promise.resolve(expected)); + prs.fetch(42).then(data => { + expect(data).toEqual(expected); + done(); + }); }); }); @@ -102,13 +101,8 @@ describe('GithubPullRequests', () => { describe('fetchAll()', () => { let prs: GithubPullRequests; - let prsGetPaginatedSpy: jasmine.Spy; - beforeEach(() => { - prs = new GithubPullRequests('12345', 'foo/bar'); - prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated'); - spyOn(console, 'log'); - }); + beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar')); it('should call \'getPaginated()\' with the correct pathname and params', () => { @@ -118,24 +112,50 @@ describe('GithubPullRequests', () => { prs.fetchAll('closed'); prs.fetchAll('open'); - expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3); - expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]); - expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]); - expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]); + expect(githubApi.getPaginated).toHaveBeenCalledTimes(3); + expect(githubApi.getPaginated.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]); + expect(githubApi.getPaginated.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]); + expect(githubApi.getPaginated.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]); }); it('should default to \'all\' if no state is specified', () => { prs.fetchAll(); - expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'}); + expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'}); }); it('should forward the value returned by \'getPaginated()\'', () => { - prsGetPaginatedSpy.and.returnValue('Test'); + githubApi.getPaginated.and.returnValue('Test'); expect(prs.fetchAll() as any).toBe('Test'); }); }); + + describe('fetchFiles()', () => { + let prs: GithubPullRequests; + + beforeEach(() => { + prs = new GithubPullRequests(githubApi, 'foo', 'bar'); + }); + + + it('should make a paginated GET request to GitHub with the correct pathname', () => { + prs.fetchFiles(42); + 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.getPaginated.and.callFake(() => Promise.resolve(expected)); + prs.fetchFiles(42).then(data => { + expect(data).toEqual(expected); + done(); + }); + }); + + }); + }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-teams.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-teams.spec.ts index a9b03517c0..2a089c92b9 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-teams.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/github-teams.spec.ts @@ -1,43 +1,40 @@ -// Imports +import {GithubApi} from '../../lib/common/github-api'; import {GithubTeams} from '../../lib/common/github-teams'; // Tests describe('GithubTeams', () => { + let githubApi: jasmine.SpyObj; + + beforeEach(() => { + githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']); + }); + describe('constructor()', () => { - it('should throw if \'githubToken\' is missing or empty', () => { - expect(() => new GithubTeams('', 'org')). - toThrowError('Missing or empty required parameter \'githubToken\'!'); + it('should throw if \'githubOrg\' is missing or empty', () => { + expect(() => new GithubTeams(githubApi, '')). + toThrowError('Missing or empty required parameter \'githubOrg\'!'); }); - - - it('should throw if \'organization\' is missing or empty', () => { - expect(() => new GithubTeams('12345', '')). - toThrowError('Missing or empty required parameter \'organization\'!'); - }); - }); describe('fetchAll()', () => { let teams: GithubTeams; - let teamsGetPaginatedSpy: jasmine.Spy; beforeEach(() => { - teams = new GithubTeams('12345', 'foo'); - teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated'); + teams = new GithubTeams(githubApi, 'foo'); }); it('should call \'getPaginated()\' with the correct pathname and params', () => { teams.fetchAll(); - expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams'); + expect(githubApi.getPaginated).toHaveBeenCalledWith('/orgs/foo/teams'); }); it('should forward the value returned by \'getPaginated()\'', () => { - teamsGetPaginatedSpy.and.returnValue('Test'); + githubApi.getPaginated.and.returnValue('Test'); expect(teams.fetchAll() as any).toBe('Test'); }); @@ -46,19 +43,15 @@ describe('GithubTeams', () => { describe('isMemberById()', () => { let teams: GithubTeams; - let teamsGetSpy: jasmine.Spy; beforeEach(() => { - teams = new GithubTeams('12345', 'foo'); - teamsGetSpy = spyOn(teams, 'get').and.returnValue(Promise.resolve(null)); + teams = new GithubTeams(githubApi, 'foo'); }); - it('should return a promise', done => { + it('should return a promise', () => { + githubApi.get.and.callFake(() => Promise.resolve()); const promise = teams.isMemberById('user', [1]); - promise.then(done); // Do not complete the test (and release the spies) synchronously - // to avoid running the actual `get()`. - expect(promise).toEqual(jasmine.any(Promise)); }); @@ -66,42 +59,43 @@ describe('GithubTeams', () => { it('should resolve with false if called with an empty array', done => { teams.isMemberById('user', []).then(isMember => { expect(isMember).toBe(false); - expect(teamsGetSpy).not.toHaveBeenCalled(); + expect(githubApi.get).not.toHaveBeenCalled(); done(); }); }); it('should call \'get()\' with the correct pathname', done => { + githubApi.get.and.callFake(() => Promise.resolve()); teams.isMemberById('user', [1]).then(() => { - expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user'); + expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user'); done(); }); }); it('should resolve with false if \'get()\' rejects', done => { - teamsGetSpy.and.returnValue(Promise.reject(null)); + githubApi.get.and.callFake(() => Promise.reject(null)); teams.isMemberById('user', [1]).then(isMember => { expect(isMember).toBe(false); - expect(teamsGetSpy).toHaveBeenCalled(); + expect(githubApi.get).toHaveBeenCalled(); done(); }); }); it('should resolve with false if the membership is not active', done => { - teamsGetSpy.and.returnValue(Promise.resolve({state: 'pending'})); + githubApi.get.and.callFake(() => Promise.resolve({state: 'pending'})); teams.isMemberById('user', [1]).then(isMember => { expect(isMember).toBe(false); - expect(teamsGetSpy).toHaveBeenCalled(); + expect(githubApi.get).toHaveBeenCalled(); done(); }); }); it('should resolve with true if the membership is active', done => { - teamsGetSpy.and.returnValue(Promise.resolve({state: 'active'})); + githubApi.get.and.callFake(() => Promise.resolve({state: 'active'})); teams.isMemberById('user', [1]).then(isMember => { expect(isMember).toBe(true); done(); @@ -115,15 +109,15 @@ describe('GithubTeams', () => { '/teams/2/memberships/user': Promise.reject(null), '/teams/3/memberships/user': Promise.resolve({state: 'active'}), }; - teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]); + githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]); teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => { expect(isMember).toBe(true); - expect(teamsGetSpy).toHaveBeenCalledTimes(3); - expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user'); - expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user'); - expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user'); + expect(githubApi.get).toHaveBeenCalledTimes(3); + expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user'); + expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user'); + expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user'); done(); }); @@ -137,16 +131,16 @@ describe('GithubTeams', () => { '/teams/3/memberships/user': Promise.resolve({state: 'not active'}), '/teams/4/memberships/user': Promise.reject(null), }; - teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]); + githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]); teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => { expect(isMember).toBe(false); - expect(teamsGetSpy).toHaveBeenCalledTimes(4); - expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user'); - expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user'); - expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user'); - expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user'); + expect(githubApi.get).toHaveBeenCalledTimes(4); + expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user'); + expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user'); + expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user'); + expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user'); done(); }); @@ -161,7 +155,7 @@ describe('GithubTeams', () => { let teamsIsMemberByIdSpy: jasmine.Spy; beforeEach(() => { - teams = new GithubTeams('12345', 'foo'); + teams = new GithubTeams(githubApi, 'foo'); const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]); teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse); @@ -181,7 +175,7 @@ describe('GithubTeams', () => { it('should resolve with false if \'fetchAll()\' rejects', done => { - teamsFetchAllSpy.and.returnValue(Promise.reject(null)); + teamsFetchAllSpy.and.callFake(() => Promise.reject(null)); teams.isMemberBySlug('user', ['team-slug']).then(isMember => { expect(isMember).toBe(false); done(); @@ -209,7 +203,7 @@ describe('GithubTeams', () => { it('should resolve with false if \'isMemberById()\' rejects', done => { - teamsIsMemberByIdSpy.and.returnValue(Promise.reject(null)); + teamsIsMemberByIdSpy.and.callFake(() => Promise.reject(null)); teams.isMemberBySlug('user', ['team1']).then(isMember => { expect(isMember).toBe(false); expect(teamsIsMemberByIdSpy).toHaveBeenCalled(); @@ -218,16 +212,17 @@ describe('GithubTeams', () => { }); - it('should resolve with the value \'isMemberById()\' resolves with', done => { - teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true)); + it('should resolve with the value \'isMemberById()\' resolves with', async () => { - Promise.all([ - teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)), - teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)), - ]).then(() => { - expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2); - done(); - }); + teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(true)); + const isMember1 = await teams.isMemberBySlug('user', ['team1']); + expect(isMember1).toBe(true); + expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]); + + teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(false)); + const isMember2 = await teams.isMemberBySlug('user', ['team1']); + expect(isMember2).toBe(false); + expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]); }); }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts index 872fa2fdd8..063268cce5 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/common/utils.spec.ts @@ -1,9 +1,59 @@ // Imports -import {assertNotMissingOrEmpty, getEnvVar} from '../../lib/common/utils'; +import {resolve as resolvePath} from 'path'; +import { + assert, + assertNotMissingOrEmpty, + computeArtifactDownloadPath, + computeShortSha, + getEnvVar, + getPrInfoFromDownloadPath, + Logger, +} from '../../lib/common/utils'; // Tests describe('utils', () => { + describe('computeShortSha', () => { + it('should return only the first SHORT_SHA_LEN characters of the SHA', () => { + expect(computeShortSha('0123456789')).toEqual('0123456'); + expect(computeShortSha('ABC')).toEqual('ABC'); + expect(computeShortSha('')).toEqual(''); + }); + }); + + + describe('assert', () => { + it('should throw if passed a false value', () => { + expect(() => assert(false, 'error message')).toThrowError('error message'); + }); + + it('should not throw if passed a true value', () => { + expect(() => assert(true, 'error message')).not.toThrow(); + }); + }); + + + describe('computeArtifactDownloadPath', () => { + it('should compute an absolute path based on the artifact info provided', () => { + const downloadDir = '/a/b/c'; + const pr = 123; + const sha = 'ABCDEF1234567'; + const artifactPath = 'a/path/to/file.zip'; + const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath); + 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'); + expect(pr).toEqual(12345); + expect(sha).toEqual('ABCDE'); + }); + }); + + describe('assertNotMissingOrEmpty()', () => { it('should throw if passed an empty value', () => { @@ -78,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'); + }); + + }); + }); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/helpers.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/helpers.ts deleted file mode 100644 index 044af2b207..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/helpers.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace jasmine { - export interface DoneFn extends Function { - (): void; - fail: (message: Error | string) => void; - } -} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts index 487536920c..3470b5541b 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/index.ts @@ -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); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-creator.spec.ts similarity index 89% rename from aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts rename to aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-creator.spec.ts index 469beb73a0..b09293b48a 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-creator.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-creator.spec.ts @@ -5,20 +5,21 @@ import * as fs from 'fs'; import * as path from 'path'; import * as shell from 'shelljs'; import {SHORT_SHA_LEN} from '../../lib/common/constants'; -import {BuildCreator} from '../../lib/upload-server/build-creator'; -import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events'; -import {UploadError} from '../../lib/upload-server/upload-error'; -import {expectToBeUploadError} from './helpers'; +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'; +import {expectToBePreviewServerError} from './helpers'; // Tests describe('BuildCreator', () => { - const pr = '9'; + const pr = 9; const sha = '9'.repeat(40); const shortSha = sha.substr(0, SHORT_SHA_LEN); const archive = 'snapshot.tar.gz'; const buildsDir = 'builds/dir'; const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`); - const publicPrDir = path.join(buildsDir, pr); + const publicPrDir = path.join(buildsDir, `${pr}`); const hiddenShaDir = path.join(hiddenPrDir, shortSha); const publicShaDir = path.join(publicPrDir, shortSha); let bc: BuildCreator; @@ -134,8 +135,8 @@ describe('BuildCreator', () => { it('should abort and skip further operations if changing the PR\'s visibility fails', done => { - const mockError = new UploadError(543, 'Test'); - bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError)); + const mockError = new PreviewServerError(543, 'Test'); + bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject(mockError)); bc.create(pr, sha, archive, isPublic).catch(err => { expect(err).toBe(mockError); @@ -154,7 +155,7 @@ describe('BuildCreator', () => { existsValues[shaDir] = true; bc.create(pr, sha, archive, isPublic).catch(err => { const publicOrNot = isPublic ? 'public' : 'non-public'; - expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); + expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); expect(shellMkdirSpy).not.toHaveBeenCalled(); expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled(); @@ -171,7 +172,7 @@ describe('BuildCreator', () => { bc.create(pr, sha, archive, isPublic).catch(err => { const publicOrNot = isPublic ? 'public' : 'non-public'; - expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); + expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); expect(shellMkdirSpy).not.toHaveBeenCalled(); expect(bcExtractArchiveSpy).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled(); @@ -222,20 +223,20 @@ describe('BuildCreator', () => { }); - it('should reject with an UploadError', done => { + it('should reject with an PreviewServerError', done => { // tslint:disable-next-line: no-string-throw shellMkdirSpy.and.callFake(() => { throw 'Test'; }); bc.create(pr, sha, archive, isPublic).catch(err => { - expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`); + expectToBePreviewServerError(err, 500, `Error while creating preview at: ${shaDir}\nTest`); done(); }); }); - it('should pass UploadError instances unmodified', done => { - shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); }); + it('should pass PreviewServerError instances unmodified', done => { + shellMkdirSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); }); bc.create(pr, sha, archive, isPublic).catch(err => { - expectToBeUploadError(err, 543, 'Test'); + expectToBePreviewServerError(err, 543, 'Test'); done(); }); }); @@ -324,7 +325,7 @@ describe('BuildCreator', () => { const shas = ['foo', 'bar', 'baz']; let emitted = false; - bcListShasByDate.and.returnValue(Promise.resolve(shas)); + bcListShasByDate.and.callFake(() => Promise.resolve(shas)); bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => { expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir); @@ -376,7 +377,8 @@ describe('BuildCreator', () => { it('should abort and skip further operations if both directories exist', done => { bcExistsSpy.and.returnValue(true); bc.updatePrVisibility(pr, makePublic).catch(err => { - expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`); + expectToBePreviewServerError(err, 409, + `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`); expect(shellMvSpy).not.toHaveBeenCalled(); expect(bcListShasByDate).not.toHaveBeenCalled(); expect(bcEmitSpy).not.toHaveBeenCalled(); @@ -407,20 +409,21 @@ describe('BuildCreator', () => { }); - it('should reject with an UploadError', done => { + it('should reject with an PreviewServerError', done => { // tslint:disable-next-line: no-string-throw shellMvSpy.and.callFake(() => { throw 'Test'; }); bc.updatePrVisibility(pr, makePublic).catch(err => { - expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`); + expectToBePreviewServerError(err, 500, + `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`); done(); }); }); - it('should pass UploadError instances unmodified', done => { - shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); }); + it('should pass PreviewServerError instances unmodified', done => { + shellMvSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); }); bc.updatePrVisibility(pr, makePublic).catch(err => { - expectToBeUploadError(err, 543, 'Test'); + expectToBePreviewServerError(err, 543, 'Test'); done(); }); }); @@ -451,7 +454,7 @@ describe('BuildCreator', () => { it('should call \'fs.access()\' with the specified argument', () => { (bc as any).exists('foo'); - expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function)); + expect(fsAccessSpy).toHaveBeenCalledWith('foo', jasmine.any(Function)); }); @@ -489,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)); @@ -527,7 +530,7 @@ describe('BuildCreator', () => { }); - it('should delete the uploaded file on success', done => { + it('should delete the build artifact file on success', done => { (bc as any).extractArchive('input/file', 'output/dir'). then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')). then(done); @@ -567,7 +570,7 @@ describe('BuildCreator', () => { }); - it('should abort and reject if it fails to remove the uploaded file', done => { + it('should abort and reject if it fails to remove the build artifact file', done => { (bc as any).extractArchive('foo', 'bar').catch((err: any) => { expect(shellChmodSpy).toHaveBeenCalled(); expect(shellRmSpy).toHaveBeenCalled(); @@ -618,7 +621,7 @@ describe('BuildCreator', () => { it('should reject if listing files fails', done => { - shellLsSpy.and.returnValue(Promise.reject('Test')); + shellLsSpy.and.callFake(() => Promise.reject('Test')); (bc as any).listShasByDate('input/dir').catch((err: string) => { expect(err).toBe('Test'); done(); @@ -627,7 +630,7 @@ describe('BuildCreator', () => { it('should return the filenames', done => { - shellLsSpy.and.returnValue(Promise.resolve([ + shellLsSpy.and.callFake(() => Promise.resolve([ lsResult('foo', 100), lsResult('bar', 200), lsResult('baz', 300), @@ -640,7 +643,7 @@ describe('BuildCreator', () => { it('should sort by date', done => { - shellLsSpy.and.returnValue(Promise.resolve([ + shellLsSpy.and.callFake(() => Promise.resolve([ lsResult('foo', 300), lsResult('bar', 100), lsResult('baz', 200), @@ -660,7 +663,7 @@ describe('BuildCreator', () => { ]; mockArray.sort = jasmine.createSpy('sort'); - shellLsSpy.and.returnValue(Promise.resolve(mockArray)); + shellLsSpy.and.callFake(() => Promise.resolve(mockArray)); (bc as any).listShasByDate('input/dir'). then((shas: string[]) => { expect(shas).toEqual(['bar', 'baz', 'foo']); @@ -671,7 +674,7 @@ describe('BuildCreator', () => { it('should only include directories', done => { - shellLsSpy.and.returnValue(Promise.resolve([ + shellLsSpy.and.callFake(() => Promise.resolve([ lsResult('foo', 100), lsResult('bar', 200, false), lsResult('baz', 300), diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-events.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-events.spec.ts similarity index 97% rename from aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-events.spec.ts rename to aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-events.spec.ts index 73214f3d9e..7c6af3fcad 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-events.spec.ts +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-events.spec.ts @@ -1,5 +1,5 @@ // Imports -import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events'; +import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events'; // Tests describe('ChangedPrVisibilityEvent', () => { diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts new file mode 100644 index 0000000000..6f8746712b --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-retriever.spec.ts @@ -0,0 +1,193 @@ +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 = resolvePath('/DOWNLOAD/DIR'); + const BASE_URL = 'http://test.com'; + const ARTIFACT_PATH = '/some/path/build.zip'; + + let api: CircleCiApi; + let BUILD_INFO: BuildInfo; + let WRITEFILE_RESULT: any; + let writeFileSpy: jasmine.Spy; + let EXISTS_RESULT: boolean; + let existsSpy: jasmine.Spy; + let getBuildArtifactUrlSpy: jasmine.Spy; + + beforeEach(() => { + BUILD_INFO = { + branch: 'pull/777', + build_num: 12345, + failed: false, + has_artifacts: true, + outcome: 'success', + reponame: 'REPO', + username: 'ORG', + vcs_revision: 'COMMIT', + }; + + api = new CircleCiApi('ORG', 'REPO', 'TOKEN'); + spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO)); + getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl') + .and.callFake(() => Promise.resolve(BASE_URL + ARTIFACT_PATH)); + + WRITEFILE_RESULT = undefined; + writeFileSpy = spyOn(fs, 'writeFile').and.callFake( + (_path: string, _buffer: Buffer, callback: (err?: any) => {}) => callback(WRITEFILE_RESULT), + ); + + EXISTS_RESULT = false; + existsSpy = spyOn(fs, 'exists').and.callFake( + (_path: string, callback: (exists: boolean) => {}) => callback(EXISTS_RESULT), + ); + }); + + describe('constructor', () => { + it('should fail if the "downloadSizeLimit" is invalid', () => { + expect(() => new BuildRetriever(api, NaN, DOWNLOAD_DIR)) + .toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`); + expect(() => new BuildRetriever(api, 0, DOWNLOAD_DIR)) + .toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`); + expect(() => new BuildRetriever(api, -1, DOWNLOAD_DIR)) + .toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`); + }); + it('should fail if the "downloadDir" is missing', () => { + expect(() => new BuildRetriever(api, MAX_DOWNLOAD_SIZE, '')) + .toThrowError(`Missing or empty required parameter 'downloadDir'!`); + }); + }); + + + describe('getGithubInfo', () => { + it('should request the info from CircleCI', async () => { + const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR); + const info = await retriever.getGithubInfo(12345); + expect(api.getBuildInfo).toHaveBeenCalledWith(12345); + expect(info).toEqual({org: 'ORG', pr: 777, repo: 'REPO', sha: 'COMMIT', success: true}); + }); + + it('should error if it is not possible to extract the PR number from the branch', async () => { + const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR); + try { + BUILD_INFO.branch = 'master'; + await retriever.getGithubInfo(12345); + throw new Error('Exception Expected'); + } catch (error) { + expect(error.message).toEqual('No PR found in branch field: master'); + } + }); + }); + + + describe('downloadBuildArtifact', () => { + const ARTIFACT_CONTENTS = 'ARTIFACT CONTENTS'; + let retriever: BuildRetriever; + + beforeEach(() => { + spyOn(Logger.prototype, 'warn'); + retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR); + }); + + it('should get the artifact URL from the CircleCI API', async () => { + const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS); + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + expect(api.getBuildArtifactUrl).toHaveBeenCalledWith(12345, ARTIFACT_PATH); + artifactRequest.done(); + }); + + it('should download the artifact from its URL', async () => { + const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS); + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + // The following line proves that the artifact URL fetch occurred. + artifactRequest.done(); + }); + + it('should fail if the artifact is too large', async () => { + const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS); + retriever = new BuildRetriever(api, 10, DOWNLOAD_DIR); + try { + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + throw new Error('Exception Expected'); + } catch (error) { + expect(error.status).toEqual(413); + } + artifactRequest.done(); + }); + + it('should not download the artifact if it already exists', async () => { + const artifactRequestInterceptor = nock(BASE_URL).get(ARTIFACT_PATH); + const artifactRequest = artifactRequestInterceptor.reply(200, ARTIFACT_CONTENTS); + EXISTS_RESULT = true; + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + expect(existsSpy).toHaveBeenCalled(); + expect(getBuildArtifactUrlSpy).not.toHaveBeenCalled(); + expect(artifactRequest.isDone()).toEqual(false); + nock.removeInterceptor(artifactRequestInterceptor); + }); + + 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(downloadPath, jasmine.any(Buffer), jasmine.any(Function)); + + const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1]; + expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS); + + artifactRequest.done(); + }); + + it('should fail if the CircleCI API fails', async () => { + try { + getBuildArtifactUrlSpy.and.callFake(() => Promise.reject('getBuildArtifactUrl failed')); + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + throw new Error('Exception Expected'); + } catch (error) { + expect(error.message).toEqual('CircleCI artifact download failed (getBuildArtifactUrl failed)'); + } + }); + + it('should fail if the URL fetch errors', async () => { + // create a new handler that errors + const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).replyWithError('Artifact Request Failed'); + try { + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + throw new Error('Exception Expected'); + } catch (error) { + expect(error.message).toEqual('CircleCI artifact download failed ' + + '(request to http://test.com/some/path/build.zip failed, reason: Artifact Request Failed)'); + } + artifactRequest.done(); + }); + + it('should fail if the URL fetch 404s', async () => { + // create a new handler that errors + const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact'); + try { + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + throw new Error('Exception Expected'); + } catch (error) { + expect(error.message).toEqual('CircleCI artifact download failed (Error 404 - Not Found)'); + } + artifactRequest.done(); + }); + + it('should fail if file write fails', async () => { + const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS); + try { + WRITEFILE_RESULT = 'Test Error'; + await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH); + throw new Error('Exception Expected'); + } catch (error) { + expect(error.message).toEqual('CircleCI artifact download failed (Test Error)'); + } + artifactRequest.done(); + }); + }); +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-verifier.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-verifier.spec.ts new file mode 100644 index 0000000000..07494f7ff5 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/build-verifier.spec.ts @@ -0,0 +1,180 @@ +// Imports +import {GithubApi} from '../../lib/common/github-api'; +import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests'; +import {GithubTeams} from '../../lib/common/github-teams'; +import {BuildVerifier} from '../../lib/preview-server/build-verifier'; + +// Tests +describe('BuildVerifier', () => { + const defaultConfig = { + allowedTeamSlugs: ['team1', 'team2'], + githubOrg: 'organization', + githubRepo: 'repo', + githubToken: 'githubToken', + secret: 'secret', + trustedPrLabel: 'trusted: pr-label', + }; + let prs: GithubPullRequests; + let bv: BuildVerifier; + + // Helpers + const createBuildVerifier = (partialConfig: Partial = {}) => { + const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig; + const api = new GithubApi(cfg.githubToken); + prs = new GithubPullRequests(api, cfg.githubOrg, cfg.githubRepo); + const teams = new GithubTeams(api, cfg.githubOrg); + return new BuildVerifier(prs, teams, cfg.allowedTeamSlugs, cfg.trustedPrLabel); + }; + + beforeEach(() => bv = createBuildVerifier()); + + + describe('constructor()', () => { + + ['githubToken', 'githubRepo', 'githubOrg', 'allowedTeamSlugs', 'trustedPrLabel']. + forEach(param => { + it(`should throw if '${param}' is missing or empty`, () => { + expect(() => createBuildVerifier({[param]: ''})). + toThrowError(`Missing or empty required parameter '${param}'!`); + }); + }); + + + it('should throw if \'allowedTeamSlugs\' is an empty array', () => { + expect(() => createBuildVerifier({allowedTeamSlugs: []})). + toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!'); + }); + + }); + + + describe('getSignificantFilesChanged', () => { + it('should return false if none of the fetched files match the given pattern', async () => { + const fetchFilesSpy = spyOn(prs, 'fetchFiles'); + fetchFilesSpy.and.callFake(() => Promise.resolve([{filename: 'a/b/c'}, {filename: 'd/e/f'}])); + expect(await bv.getSignificantFilesChanged(777, /^x/)).toEqual(false); + expect(fetchFilesSpy).toHaveBeenCalledWith(777); + + fetchFilesSpy.calls.reset(); + expect(await bv.getSignificantFilesChanged(777, /^a/)).toEqual(true); + expect(fetchFilesSpy).toHaveBeenCalledWith(777); + }); + }); + + + describe('getPrIsTrusted()', () => { + const pr = 9; + let mockPrInfo: PullRequest; + let prsFetchSpy: jasmine.Spy; + let teamsIsMemberBySlugSpy: jasmine.Spy; + + beforeEach(() => { + mockPrInfo = { + labels: [ + {name: 'foo'}, + {name: 'bar'}, + ], + number: 9, + user: {login: 'username'}, + }; + + prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch'). + and.callFake(() => Promise.resolve(mockPrInfo)); + + teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug'). + and.callFake(() => Promise.resolve(true)); + }); + + + it('should return a promise', done => { + const promise = bv.getPrIsTrusted(pr); + promise.then(done); // Do not complete the test (and release the spies) synchronously + // to avoid running the actual `GithubTeams#isMemberBySlug()`. + + expect(promise).toEqual(jasmine.any(Promise)); + }); + + + it('should fetch the corresponding PR', done => { + bv.getPrIsTrusted(pr).then(() => { + expect(prsFetchSpy).toHaveBeenCalledWith(pr); + done(); + }); + }); + + + it('should fail if fetching the PR errors', done => { + prsFetchSpy.and.callFake(() => Promise.reject('Test')); + bv.getPrIsTrusted(pr).catch(err => { + expect(err).toBe('Test'); + done(); + }); + }); + + + describe('when the PR has the "trusted PR" label', () => { + + beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'})); + + + it('should resolve to true', done => { + bv.getPrIsTrusted(pr).then(isTrusted => { + expect(isTrusted).toBe(true); + done(); + }); + }); + + + it('should not try to verify the author\'s membership status', done => { + bv.getPrIsTrusted(pr).then(() => { + expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + }); + + + describe('when the PR does not have the "trusted PR" label', () => { + + it('should verify the PR author\'s membership in the specified teams', done => { + bv.getPrIsTrusted(pr).then(() => { + expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']); + done(); + }); + }); + + + it('should fail if verifying membership errors', done => { + teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test')); + bv.getPrIsTrusted(pr).catch(err => { + expect(err).toBe('Test'); + done(); + }); + }); + + + it('should resolve to true if the PR\'s author is a member', done => { + teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(true)); + + bv.getPrIsTrusted(pr).then(isTrusted => { + expect(isTrusted).toBe(true); + done(); + }); + }); + + + it('should resolve to false if the PR\'s author is not a member', done => { + teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(false)); + + bv.getPrIsTrusted(pr).then(isTrusted => { + expect(isTrusted).toBe(false); + done(); + }); + }); + + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/helpers.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/helpers.ts new file mode 100644 index 0000000000..190774dbfb --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/helpers.ts @@ -0,0 +1,11 @@ +import {PreviewServerError} from '../../lib/preview-server/preview-error'; + +export const expectToBePreviewServerError = (actual: PreviewServerError, status?: number, message?: string) => { + expect(actual).toEqual(jasmine.any(PreviewServerError)); + if (status != null) { + expect(actual.status).toBe(status); + } + if (message != null) { + expect(actual.message).toBe(message); + } +}; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/preview-error.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/preview-error.spec.ts new file mode 100644 index 0000000000..11a1e0eabc --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/preview-error.spec.ts @@ -0,0 +1,39 @@ +// Imports +import {PreviewServerError} from '../../lib/preview-server/preview-error'; + +// Tests +describe('PreviewServerError', () => { + let err: PreviewServerError; + + beforeEach(() => err = new PreviewServerError(999, 'message')); + + + it('should extend Error', () => { + expect(err).toEqual(jasmine.any(PreviewServerError)); + expect(err).toEqual(jasmine.any(Error)); + + expect(Object.getPrototypeOf(err)).toBe(PreviewServerError.prototype); + }); + + + it('should have a \'status\' property', () => { + expect(err.status).toBe(999); + }); + + + it('should have a \'message\' property', () => { + expect(err.message).toBe('message'); + }); + + + it('should have a 500 \'status\' by default', () => { + expect(new PreviewServerError().status).toBe(500); + }); + + + it('should have an empty \'message\' by default', () => { + expect(new PreviewServerError().message).toBe(''); + expect(new PreviewServerError(999).message).toBe(''); + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/preview-server-factory.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/preview-server-factory.spec.ts new file mode 100644 index 0000000000..d647c6e03c --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/preview-server-factory.spec.ts @@ -0,0 +1,692 @@ +// Imports +import * as express from 'express'; +import * as http from 'http'; +import * as supertest from 'supertest'; +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'; +import {BuildVerifier} from '../../lib/preview-server/build-verifier'; +import {PreviewServerConfig, PreviewServerFactory} from '../../lib/preview-server/preview-server-factory'; + +interface CircleCiWebHookPayload { + payload: { + build_num: number; + build_parameters: { + CIRCLE_JOB: string; + } + }; +} + +// Tests +describe('PreviewServerFactory', () => { + const defaultConfig: PreviewServerConfig = { + buildArtifactPath: 'artifact/path.zip', + buildsDir: 'builds/dir', + circleCiToken: 'CIRCLE_CI_TOKEN', + domainName: 'domain.name', + downloadSizeLimit: 999, + downloadsDir: '/tmp/aio-create-builds', + githubOrg: 'organisation', + githubRepo: 'repo', + githubTeamSlugs: ['team1', 'team2'], + githubToken: '12345', + 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 = {}) => + PreviewServerFactory.create({...defaultConfig, ...partialConfig}); + + beforeEach(() => { + loggerErrorSpy = spyOn(Logger.prototype, 'error'); + loggerInfoSpy = spyOn(Logger.prototype, 'info'); + loggerLogSpy = spyOn(Logger.prototype, 'log'); + }); + + describe('create()', () => { + let usfCreateMiddlewareSpy: jasmine.Spy; + + beforeEach(() => { + usfCreateMiddlewareSpy = spyOn(PreviewServerFactory, 'createMiddleware').and.callThrough(); + }); + + + it('should throw if \'buildsDir\' is missing or empty', () => { + expect(() => createPreviewServer({buildsDir: ''})). + toThrowError('Missing or empty required parameter \'buildsDir\'!'); + }); + + + it('should throw if \'domainName\' is missing or empty', () => { + expect(() => createPreviewServer({domainName: ''})). + toThrowError('Missing or empty required parameter \'domainName\'!'); + }); + + + it('should throw if \'githubToken\' is missing or empty', () => { + expect(() => createPreviewServer({githubToken: ''})). + toThrowError('Missing or empty required parameter \'githubToken\'!'); + }); + + + it('should throw if \'githubOrg\' is missing or empty', () => { + expect(() => createPreviewServer({githubOrg: ''})). + toThrowError('Missing or empty required parameter \'githubOrg\'!'); + }); + + + it('should throw if \'githubTeamSlugs\' is missing or empty', () => { + expect(() => createPreviewServer({githubTeamSlugs: []})). + toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!'); + }); + + + it('should throw if \'githubRepo\' is missing or empty', () => { + expect(() => createPreviewServer({githubRepo: ''})). + toThrowError('Missing or empty required parameter \'githubRepo\'!'); + }); + + + it('should throw if \'trustedPrLabel\' is missing or empty', () => { + expect(() => createPreviewServer({trustedPrLabel: ''})). + toThrowError('Missing or empty required parameter \'trustedPrLabel\'!'); + }); + + + it('should return an http.Server', () => { + const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); + const server = createPreviewServer(); + + expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue); + }); + + + it('should create and use an appropriate BuildCreator', () => { + const usfCreateBuildCreatorSpy = spyOn(PreviewServerFactory, 'createBuildCreator').and.callThrough(); + + createPreviewServer(); + const buildRetriever = jasmine.any(BuildRetriever); + const buildVerifier = jasmine.any(BuildVerifier); + const prs = jasmine.any(GithubPullRequests); + const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue; + + expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig); + expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith(prs, 'builds/dir', 'domain.name'); + }); + + + it('should create and use an appropriate middleware', () => { + const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); + + createPreviewServer(); + + const buildRetriever = jasmine.any(BuildRetriever); + const buildVerifier = jasmine.any(BuildVerifier); + const buildCreator = jasmine.any(BuildCreator); + expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig); + + const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue; + expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware); + }); + + + it('should log the server address info on \'listening\'', () => { + const server = createPreviewServer(); + server.address = () => ({address: 'foo', family: '', port: 1337}); + + expect(loggerInfoSpy).not.toHaveBeenCalled(); + + server.emit('listening'); + expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...'); + }); + + }); + + + // Protected methods + + describe('createBuildCreator()', () => { + let buildCreator: BuildCreator; + + beforeEach(() => { + const api = new GithubApi(defaultConfig.githubToken); + const prs = new GithubPullRequests(api, defaultConfig.githubOrg, defaultConfig.githubRepo); + buildCreator = PreviewServerFactory.createBuildCreator(prs, defaultConfig.buildsDir, defaultConfig.domainName); + }); + + it('should pass the \'buildsDir\' to the BuildCreator', () => { + expect((buildCreator as any).buildsDir).toBe('builds/dir'); + }); + + + describe('on \'build.created\'', () => { + let prsAddCommentSpy: jasmine.Spy; + + beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment')); + + + it('should post a comment on GitHub for public previews', () => { + const commentBody = 'You can preview 1234567890 at https://pr42-1234567890.domain.name/.'; + + buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true}); + expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody); + }); + + + it('should not post a comment on GitHub for non-public previews', () => { + buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: false}); + expect(prsAddCommentSpy).not.toHaveBeenCalled(); + }); + + }); + + + describe('on \'pr.changedVisibility\'', () => { + let prsAddCommentSpy: jasmine.Spy; + + beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment')); + + + it('should post a comment on GitHub (for all SHAs) for PRs made public', () => { + const commentBody = 'You can preview 12345 at https://pr42-12345.domain.name/.\n' + + 'You can preview 67890 at https://pr42-67890.domain.name/.'; + + buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true}); + expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody); + }); + + + it('should not post a comment on GitHub if no SHAs were affected', () => { + buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: [], isPublic: true}); + expect(prsAddCommentSpy).not.toHaveBeenCalled(); + }); + + + it('should not post a comment on GitHub for PRs made non-public', () => { + buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: false}); + expect(prsAddCommentSpy).not.toHaveBeenCalled(); + }); + + }); + + + it('should pass the correct parameters to GithubPullRequests', () => { + const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'); + + buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true}); + buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true}); + + const allCalls = prsAddCommentSpy.calls.all(); + const prs: GithubPullRequests = allCalls[0].object; + + expect(prsAddCommentSpy).toHaveBeenCalledTimes(2); + expect(prs).toBe(allCalls[1].object); + expect(prs).toEqual(jasmine.any(GithubPullRequests)); + expect(prs.repoSlug).toBe('organisation/repo'); + }); + + }); + + + describe('createMiddleware()', () => { + let buildRetriever: BuildRetriever; + let buildVerifier: BuildVerifier; + let buildCreator: BuildCreator; + let agent: supertest.SuperTest; + + beforeEach(() => { + const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo, + defaultConfig.circleCiToken); + const githubApi = new GithubApi(defaultConfig.githubToken); + const prs = new GithubPullRequests(githubApi, defaultConfig.githubOrg, defaultConfig.githubRepo); + const teams = new GithubTeams(githubApi, defaultConfig.githubOrg); + + buildRetriever = new BuildRetriever(circleCiApi, defaultConfig.downloadSizeLimit, defaultConfig.downloadsDir); + buildVerifier = new BuildVerifier(prs, teams, defaultConfig.githubTeamSlugs, defaultConfig.trustedPrLabel); + buildCreator = new BuildCreator(defaultConfig.buildsDir); + + const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator, + defaultConfig); + agent = supertest.agent(middleware); + }); + + + describe('GET /health-check', () => { + + it('should respond with 200', async () => { + await Promise.all([ + agent.get('/health-check').expect(200), + agent.get('/health-check/').expect(200), + ]); + }); + + + it('should respond with 404 for non-GET requests', async () => { + await Promise.all([ + agent.put('/health-check').expect(404), + agent.post('/health-check').expect(404), + agent.patch('/health-check').expect(404), + agent.delete('/health-check').expect(404), + ]); + }); + + + it('should respond with 404 if the path does not match exactly', async () => { + await Promise.all([ + agent.get('/health-check/foo').expect(404), + agent.get('/health-check-foo').expect(404), + agent.get('/health-checknfoo').expect(404), + agent.get('/foo/health-check').expect(404), + agent.get('/foo-health-check').expect(404), + agent.get('/foonhealth-check').expect(404), + ]); + }); + + }); + + + describe('GET /can-have-public-preview/', () => { + 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; + let getPrIsTrustedSpy: jasmine.Spy; + let createBuildSpy: jasmine.Spy; + let IS_PUBLIC: boolean; + let BUILD_INFO: GithubInfo; + let AFFECTS_SIGNIFICANT_FILES: boolean; + let BASIC_PAYLOAD: CircleCiWebHookPayload; + const URL = '/circle-build'; + const BUILD_NUM = 12345; + const PR = 777; + const SHA = 'COMMIT'; + const DOWNLOADED_ARTIFACT_PATH = 'downloads/777-COMMIT-build.zip'; + + beforeEach(() => { + IS_PUBLIC = true; + BUILD_INFO = { + org: defaultConfig.githubOrg, + pr: PR, + repo: defaultConfig.githubRepo, + sha: SHA, + success: true, + }; + BASIC_PAYLOAD = { payload: { build_num: BUILD_NUM, build_parameters: { CIRCLE_JOB: 'aio_preview' } } }; + AFFECTS_SIGNIFICANT_FILES = true; + getGithubInfoSpy = spyOn(buildRetriever, 'getGithubInfo') + .and.callFake(() => Promise.resolve(BUILD_INFO)); + getSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged') + .and.callFake(() => Promise.resolve(AFFECTS_SIGNIFICANT_FILES)); + downloadBuildArtifactSpy = spyOn(buildRetriever, 'downloadBuildArtifact') + .and.callFake(() => Promise.resolve(DOWNLOADED_ARTIFACT_PATH)); + getPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted') + .and.callFake(() => Promise.resolve(IS_PUBLIC)); + createBuildSpy = spyOn(buildCreator, 'create'); + }); + + it('should respond with 400 if the request body is not in the correct format', async () => { + await Promise.all([ + agent.post(URL).expect(400), + agent.post(URL).send().expect(400), + agent.post(URL).send({}).expect(400), + agent.post(URL).send({ payload: {} }).expect(400), + agent.post(URL).send({ payload: { build_num: -1 } }).expect(400), + agent.post(URL).send({ payload: { build_num: 4000 } }).expect(400), + agent.post(URL).send({ payload: { build_num: 4000, build_parameters: { } } }).expect(400), + agent.post(URL).send({ payload: { build_num: 4000, build_parameters: { CIRCLE_JOB: '' } } }).expect(400), + ]); + }); + + it('should create a preview if everything is good and the build succeeded', async () => { + await agent.post(URL).send(BASIC_PAYLOAD).expect(201); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp)); + expect(downloadBuildArtifactSpy).toHaveBeenCalledWith(BUILD_NUM, PR, SHA, defaultConfig.buildArtifactPath); + expect(getPrIsTrustedSpy).toHaveBeenCalledWith(PR); + expect(createBuildSpy).toHaveBeenCalledWith(PR, SHA, DOWNLOADED_ARTIFACT_PATH, IS_PUBLIC); + }); + + it('should respond with 204 if the reported build is not the "AIO preview" job', async () => { + BASIC_PAYLOAD.payload.build_parameters.CIRCLE_JOB = 'lint'; + await agent.post(URL).send(BASIC_PAYLOAD).expect(204); + expect(getGithubInfoSpy).not.toHaveBeenCalled(); + expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled(); + 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(); + }); + + it('should respond with 204 if the build did not affect any significant files', async () => { + AFFECTS_SIGNIFICANT_FILES = false; + await agent.post(URL).send(BASIC_PAYLOAD).expect(204); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp)); + 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(); + expect(createBuildSpy).not.toHaveBeenCalled(); + }); + + it('should respond with 201 if the build is trusted', async () => { + IS_PUBLIC = true; + await agent.post(URL).send(BASIC_PAYLOAD).expect(201); + }); + + it('should respond with 202 if the build is not trusted', async () => { + IS_PUBLIC = false; + await agent.post(URL).send(BASIC_PAYLOAD).expect(202); + }); + + it('should not create a preview if the build was not successful', async () => { + BUILD_INFO.success = false; + await agent.post(URL).send(BASIC_PAYLOAD).expect(204); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(downloadBuildArtifactSpy).not.toHaveBeenCalled(); + expect(getPrIsTrustedSpy).not.toHaveBeenCalled(); + expect(createBuildSpy).not.toHaveBeenCalled(); + }); + + it('should fail if the CircleCI request fails', async () => { + // Note it is important to put the `reject` into `and.callFake`; + // If you just `and.returnValue` the rejected promise + // then you get an "unhandled rejection" message in the console. + getGithubInfoSpy.and.callFake(() => Promise.reject('Test Error')); + await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error'); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(downloadBuildArtifactSpy).not.toHaveBeenCalled(); + expect(getPrIsTrustedSpy).not.toHaveBeenCalled(); + expect(createBuildSpy).not.toHaveBeenCalled(); + }); + + it('should fail if the Github organisation of the build does not match the configured organisation', async () => { + BUILD_INFO.org = 'bad'; + await agent.post(URL).send(BASIC_PAYLOAD) + .expect(500, `Invalid webhook: expected "githubOrg" property to equal "organisation" but got "bad".`); + }); + + it('should fail if the Github repo of the build does not match the configured repo', async () => { + BUILD_INFO.repo = 'bad'; + await agent.post(URL).send(BASIC_PAYLOAD) + .expect(500, `Invalid webhook: expected "githubRepo" property to equal "repo" but got "bad".`); + }); + + it('should fail if the artifact fetch request fails', async () => { + downloadBuildArtifactSpy.and.callFake(() => Promise.reject('Test Error')); + await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error'); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(downloadBuildArtifactSpy).toHaveBeenCalled(); + expect(getPrIsTrustedSpy).not.toHaveBeenCalled(); + expect(createBuildSpy).not.toHaveBeenCalled(); + }); + + it('should fail if verifying the PR fails', async () => { + getPrIsTrustedSpy.and.callFake(() => Promise.reject('Test Error')); + await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error'); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(downloadBuildArtifactSpy).toHaveBeenCalled(); + expect(getPrIsTrustedSpy).toHaveBeenCalled(); + expect(createBuildSpy).not.toHaveBeenCalled(); + }); + + it('should fail if creating the preview build fails', async () => { + createBuildSpy.and.callFake(() => Promise.reject('Test Error')); + await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error'); + expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM); + expect(downloadBuildArtifactSpy).toHaveBeenCalled(); + expect(getPrIsTrustedSpy).toHaveBeenCalled(); + expect(createBuildSpy).toHaveBeenCalled(); + }); + }); + + + describe('POST /pr-updated', () => { + const pr = '9'; + const url = '/pr-updated'; + let bvGetPrIsTrustedSpy: jasmine.Spy; + let bcUpdatePrVisibilitySpy: jasmine.Spy; + + // Helpers + const createRequest = (num: number, action?: string) => + agent.post(url).send({number: num, action}); + + beforeEach(() => { + bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted'); + bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility'); + }); + + + it('should respond with 404 for non-POST requests', async () => { + await Promise.all([ + agent.get(url).expect(404), + agent.put(url).expect(404), + agent.patch(url).expect(404), + agent.delete(url).expect(404), + ]); + }); + + + it('should respond with 400 for requests without a payload', async () => { + const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`; + + const request1 = agent.post(url); + const request2 = agent.post(url).send(); + + await Promise.all([ + request1.expect(400, responseBody), + request2.expect(400, responseBody), + ]); + }); + + + it('should respond with 400 for requests without a \'number\' field', async () => { + const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`; + + const request1 = agent.post(url).send({}); + const request2 = agent.post(url).send({number: null}); + + await Promise.all([ + request1.expect(400, `${responseBodyPrefix} {}`), + request2.expect(400, `${responseBodyPrefix} {"number":null}`), + ]); + }); + + + it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => { + await createRequest(+pr); + expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9); + }); + + + it('should propagate errors from BuildVerifier', async () => { + bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test')); + + await createRequest(+pr).expect(500, 'Test'); + + expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9); + expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled(); + }); + + + it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => { + bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42)); + + await createRequest(24); + expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false); + + await createRequest(42); + expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true); + }); + + + it('should propagate errors from BuildCreator', async () => { + bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test')); + await createRequest(+pr).expect(500, 'Test'); + }); + + + describe('on success', () => { + + it('should respond with 200 (action: undefined)', async () => { + bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); + + const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200])); + await Promise.all(reqs); + }); + + + it('should respond with 200 (action: labeled)', async () => { + 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 Promise.all(reqs); + }); + + + it('should respond with 200 (action: unlabeled)', async () => { + 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 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])); + + await Promise.all(promises); + expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled(); + expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled(); + }); + + }); + + }); + + + describe('ALL *', () => { + + it('should respond with 404', async () => { + const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`; + + 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')), + agent.patch('/some/url').expect(404, responseFor('patch')), + agent.delete('/some/url').expect(404, responseFor('delete')), + ]); + }); + + }); + + }); + +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/utils.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/utils.spec.ts new file mode 100644 index 0000000000..8be6ce6b49 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/test/preview-server/utils.spec.ts @@ -0,0 +1,49 @@ +import * as express from 'express'; +import {PreviewServerError} from '../../lib/preview-server/preview-error'; +import {respondWithError, throwRequestError} from '../../lib/preview-server/utils'; + +describe('preview-server/utils', () => { + describe('respondWithError', () => { + let endSpy: jasmine.Spy; + let statusSpy: jasmine.Spy; + let response: express.Response; + + beforeEach(() => { + endSpy = jasmine.createSpy('end'); + statusSpy = jasmine.createSpy('status').and.callFake(() => response); + response = {status: statusSpy, end: endSpy} as any; + }); + + it('should set the status on the response', () => { + respondWithError(response, new PreviewServerError(505, 'TEST MESSAGE')); + expect(statusSpy).toHaveBeenCalledWith(505); + expect(endSpy).toHaveBeenCalledWith('TEST MESSAGE', jasmine.any(Function)); + }); + + it('should convert non-PreviewServerError errors to 500 PreviewServerErrors', () => { + respondWithError(response, new Error('OTHER MESSAGE')); + expect(statusSpy).toHaveBeenCalledWith(500); + expect(endSpy).toHaveBeenCalledWith('OTHER MESSAGE', jasmine.any(Function)); + }); + }); + + describe('throwRequestError', () => { + it('should throw a suitable error', () => { + let caught = false; + try { + const request = { + body: 'The request body', + method: 'POST', + originalUrl: 'some.domain.com/path', + } as express.Request; + throwRequestError(505, 'ERROR MESSAGE', request); + } catch (error) { + caught = true; + expect(error).toEqual(jasmine.any(PreviewServerError)); + expect(error.status).toEqual(505); + expect(error.message).toEqual(`ERROR MESSAGE in request: POST some.domain.com/path "The request body"`); + } + expect(caught).toEqual(true); + }); + }); +}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-verifier.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-verifier.spec.ts deleted file mode 100644 index aa8f501aa3..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/build-verifier.spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -// Imports -import * as jwt from 'jsonwebtoken'; -import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests'; -import {GithubTeams} from '../../lib/common/github-teams'; -import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier'; -import {expectToBeUploadError} from './helpers'; - -// Tests -describe('BuildVerifier', () => { - const defaultConfig = { - allowedTeamSlugs: ['team1', 'team2'], - githubToken: 'githubToken', - organization: 'organization', - repoSlug: 'repo/slug', - secret: 'secret', - trustedPrLabel: 'trusted: pr-label', - }; - let bv: BuildVerifier; - - // Helpers - const createBuildVerifier = (partialConfig: Partial = {}) => { - const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig; - return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization, - cfg.allowedTeamSlugs, cfg.trustedPrLabel); - }; - - beforeEach(() => bv = createBuildVerifier()); - - - describe('constructor()', () => { - - ['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs', 'trustedPrLabel']. - forEach(param => { - it(`should throw if '${param}' is missing or empty`, () => { - expect(() => createBuildVerifier({[param]: ''})). - toThrowError(`Missing or empty required parameter '${param}'!`); - }); - }); - - - it('should throw if \'allowedTeamSlugs\' is an empty array', () => { - expect(() => createBuildVerifier({allowedTeamSlugs: []})). - toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!'); - }); - - }); - - - describe('getPrIsTrusted()', () => { - const pr = 9; - let mockPrInfo: PullRequest; - let prsFetchSpy: jasmine.Spy; - let teamsIsMemberBySlugSpy: jasmine.Spy; - - beforeEach(() => { - mockPrInfo = { - labels: [ - {name: 'foo'}, - {name: 'bar'}, - ], - number: 9, - user: {login: 'username'}, - }; - - prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch'). - and.returnValue(Promise.resolve(mockPrInfo)); - - teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug'). - and.returnValue(Promise.resolve(true)); - }); - - - it('should return a promise', done => { - const promise = bv.getPrIsTrusted(pr); - promise.then(done); // Do not complete the test (and release the spies) synchronously - // to avoid running the actual `GithubTeams#isMemberBySlug()`. - - expect(promise).toEqual(jasmine.any(Promise)); - }); - - - it('should fetch the corresponding PR', done => { - bv.getPrIsTrusted(pr).then(() => { - expect(prsFetchSpy).toHaveBeenCalledWith(pr); - done(); - }); - }); - - - it('should fail if fetching the PR errors', done => { - prsFetchSpy.and.callFake(() => Promise.reject('Test')); - bv.getPrIsTrusted(pr).catch(err => { - expect(err).toBe('Test'); - done(); - }); - }); - - - describe('when the PR has the "trusted PR" label', () => { - - beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'})); - - - it('should resolve to true', done => { - bv.getPrIsTrusted(pr).then(isTrusted => { - expect(isTrusted).toBe(true); - done(); - }); - }); - - - it('should not try to verify the author\'s membership status', done => { - bv.getPrIsTrusted(pr).then(() => { - expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled(); - done(); - }); - }); - - }); - - - describe('when the PR does not have the "trusted PR" label', () => { - - it('should verify the PR author\'s membership in the specified teams', done => { - bv.getPrIsTrusted(pr).then(() => { - expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']); - done(); - }); - }); - - - it('should fail if verifying membership errors', done => { - teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test')); - bv.getPrIsTrusted(pr).catch(err => { - expect(err).toBe('Test'); - done(); - }); - }); - - - it('should resolve to true if the PR\'s author is a member', done => { - teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(true)); - - bv.getPrIsTrusted(pr).then(isTrusted => { - expect(isTrusted).toBe(true); - done(); - }); - }); - - - it('should resolve to false if the PR\'s author is not a member', done => { - teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(false)); - - bv.getPrIsTrusted(pr).then(isTrusted => { - expect(isTrusted).toBe(false); - done(); - }); - }); - - }); - - }); - - - describe('verify()', () => { - const pr = 9; - const defaultJwt = { - 'exp': Math.floor(Date.now() / 1000) + 30, - 'iat': Math.floor(Date.now() / 1000) - 30, - 'iss': 'Travis CI, GmbH', - 'pull-request': pr, - 'slug': defaultConfig.repoSlug, - }; - let bvGetPrIsTrusted: jasmine.Spy; - - // Heleprs - const createAuthHeader = (partialJwt: Partial = {}, secret: string = defaultConfig.secret) => - `Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`; - - beforeEach(() => { - bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true)); - }); - - - it('should return a promise', done => { - const promise = bv.verify(pr, createAuthHeader()); - promise.then(done); // Do not complete the test (and release the spies) synchronously - // to avoid running the actual `bvGetPrIsTrusted()`. - - expect(promise).toEqual(jasmine.any(Promise)); - }); - - - it('should fail if the authorization header is invalid', done => { - bv.verify(pr, 'foo').catch(err => { - const errorMessage = 'Error while verifying upload for PR 9: jwt malformed'; - - expectToBeUploadError(err, 403, errorMessage); - done(); - }); - }); - - - it('should fail if the secret is invalid', done => { - bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => { - const errorMessage = 'Error while verifying upload for PR 9: invalid signature'; - - expectToBeUploadError(err, 403, errorMessage); - done(); - }); - }); - - - it('should fail if the issuer is invalid', done => { - bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => { - const errorMessage = 'Error while verifying upload for PR 9: ' + - `jwt issuer invalid. expected: ${defaultJwt.iss}`; - - expectToBeUploadError(err, 403, errorMessage); - done(); - }); - }); - - - it('should fail if the token has expired', done => { - bv.verify(pr, createAuthHeader({exp: 0})).catch(err => { - const errorMessage = 'Error while verifying upload for PR 9: jwt expired'; - - expectToBeUploadError(err, 403, errorMessage); - done(); - }); - }); - - - it('should fail if the repo slug does not match', done => { - bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => { - const errorMessage = 'Error while verifying upload for PR 9: ' + - `jwt slug invalid. expected: ${defaultConfig.repoSlug}`; - - expectToBeUploadError(err, 403, errorMessage); - done(); - }); - }); - - - it('should fail if the PR does not match', done => { - bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => { - const errorMessage = 'Error while verifying upload for PR 9: ' + - `jwt pull-request invalid. expected: ${pr}`; - - expectToBeUploadError(err, 403, errorMessage); - done(); - }); - }); - - - it('should not fail if the token is valid', done => { - bv.verify(pr, createAuthHeader()).then(done); - }); - - - it('should not fail even if the token has been issued in the future', done => { - const in30s = Math.floor(Date.now() / 1000) + 30; - bv.verify(pr, createAuthHeader({iat: in30s})).then(done); - }); - - - it('should call \'getPrIsTrusted()\' if the token is valid', done => { - bv.verify(pr, createAuthHeader()).then(() => { - expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr); - done(); - }); - }); - - - it('should fail if \'getPrIsTrusted()\' rejects', done => { - bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test')); - bv.verify(pr, createAuthHeader()).catch(err => { - expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`); - done(); - }); - }); - - - it('should resolve to `verifiedNotTrusted` if \'getPrIsTrusted()\' returns false', done => { - bvGetPrIsTrusted.and.returnValue(Promise.resolve(false)); - bv.verify(pr, createAuthHeader()).then(value => { - expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedNotTrusted); - done(); - }); - }); - - - it('should resolve to `verifiedAndTrusted` if \'getPrIsTrusted()\' returns true', done => { - bv.verify(pr, createAuthHeader()).then(value => { - expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedAndTrusted); - done(); - }); - }); - - }); - -}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/helpers.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/helpers.ts deleted file mode 100644 index 9213d7c1a6..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {UploadError} from '../../lib/upload-server/upload-error'; - -export const expectToBeUploadError = (actual: UploadError, status?: number, message?: string) => { - expect(actual).toEqual(jasmine.any(UploadError)); - if (status != null) { - expect(actual.status).toBe(status); - } - if (message != null) { - expect(actual.message).toBe(message); - } -}; diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-error.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-error.spec.ts deleted file mode 100644 index 9a20925932..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-error.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Imports -import {UploadError} from '../../lib/upload-server/upload-error'; - -// Tests -describe('UploadError', () => { - let err: UploadError; - - beforeEach(() => err = new UploadError(999, 'message')); - - - it('should extend Error', () => { - expect(err).toEqual(jasmine.any(UploadError)); - expect(err).toEqual(jasmine.any(Error)); - - expect(Object.getPrototypeOf(err)).toBe(UploadError.prototype); - }); - - - it('should have a \'status\' property', () => { - expect(err.status).toBe(999); - }); - - - it('should have a \'message\' property', () => { - expect(err.message).toBe('message'); - }); - - - it('should have a 500 \'status\' by default', () => { - expect(new UploadError().status).toBe(500); - }); - - - it('should have an empty \'message\' by default', () => { - expect(new UploadError().message).toBe(''); - expect(new UploadError(999).message).toBe(''); - }); - -}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts b/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts deleted file mode 100644 index d0db5552e4..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/test/upload-server/upload-server-factory.spec.ts +++ /dev/null @@ -1,603 +0,0 @@ -// Imports -import * as express from 'express'; -import * as http from 'http'; -import * as supertest from 'supertest'; -import {GithubPullRequests} from '../../lib/common/github-pull-requests'; -import {BuildCreator} from '../../lib/upload-server/build-creator'; -import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events'; -import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier'; -import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory'; - -// Tests -describe('uploadServerFactory', () => { - const defaultConfig = { - buildsDir: 'builds/dir', - domainName: 'domain.name', - githubOrganization: 'organization', - githubTeamSlugs: ['team1', 'team2'], - githubToken: '12345', - repoSlug: 'repo/slug', - secret: 'secret', - trustedPrLabel: 'trusted: pr-label', - }; - - // Helpers - const createUploadServer = (partialConfig: Partial = {}) => - usf.create({...defaultConfig, ...partialConfig} as typeof defaultConfig); - - - describe('create()', () => { - let usfCreateMiddlewareSpy: jasmine.Spy; - - beforeEach(() => { - usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough(); - }); - - - it('should throw if \'buildsDir\' is missing or empty', () => { - expect(() => createUploadServer({buildsDir: ''})). - toThrowError('Missing or empty required parameter \'buildsDir\'!'); - }); - - - it('should throw if \'domainName\' is missing or empty', () => { - expect(() => createUploadServer({domainName: ''})). - toThrowError('Missing or empty required parameter \'domainName\'!'); - }); - - - it('should throw if \'githubToken\' is missing or empty', () => { - expect(() => createUploadServer({githubToken: ''})). - toThrowError('Missing or empty required parameter \'githubToken\'!'); - }); - - - it('should throw if \'githubOrganization\' is missing or empty', () => { - expect(() => createUploadServer({githubOrganization: ''})). - toThrowError('Missing or empty required parameter \'organization\'!'); - }); - - - it('should throw if \'githubTeamSlugs\' is missing or empty', () => { - expect(() => createUploadServer({githubTeamSlugs: []})). - toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!'); - }); - - - it('should throw if \'repoSlug\' is missing or empty', () => { - expect(() => createUploadServer({repoSlug: ''})). - toThrowError('Missing or empty required parameter \'repoSlug\'!'); - }); - - - it('should throw if \'secret\' is missing or empty', () => { - expect(() => createUploadServer({secret: ''})). - toThrowError('Missing or empty required parameter \'secret\'!'); - }); - - - it('should throw if \'trustedPrLabel\' is missing or empty', () => { - expect(() => createUploadServer({trustedPrLabel: ''})). - toThrowError('Missing or empty required parameter \'trustedPrLabel\'!'); - }); - - - it('should return an http.Server', () => { - const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); - const server = createUploadServer(); - - expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue); - }); - - - it('should create and use an appropriate BuildCreator', () => { - const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough(); - - createUploadServer(); - const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue; - - expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator); - expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug', 'domain.name'); - }); - - - it('should create and use an appropriate middleware', () => { - const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough(); - - createUploadServer(); - const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue; - const buildVerifier = jasmine.any(BuildVerifier); - const buildCreator = jasmine.any(BuildCreator); - - expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware); - expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator); - }); - - - it('should log the server address info on \'listening\'', () => { - const consoleInfoSpy = spyOn(console, 'info'); - const server = createUploadServer(); - server.address = () => ({address: 'foo', family: '', port: 1337}); - - expect(consoleInfoSpy).not.toHaveBeenCalled(); - - server.emit('listening'); - expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...'); - }); - - }); - - - // Protected methods - - describe('createBuildCreator()', () => { - let buildCreator: BuildCreator; - - beforeEach(() => { - buildCreator = (usf as any).createBuildCreator( - defaultConfig.buildsDir, - defaultConfig.githubToken, - defaultConfig.repoSlug, - defaultConfig.domainName, - ); - }); - - - it('should pass the \'buildsDir\' to the BuildCreator', () => { - expect((buildCreator as any).buildsDir).toBe('builds/dir'); - }); - - - describe('on \'build.created\'', () => { - let prsAddCommentSpy: jasmine.Spy; - - beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment')); - - - it('should post a comment on GitHub for public previews', () => { - const commentBody = 'You can preview 1234567890 at https://pr42-1234567890.domain.name/.'; - - buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true}); - expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody); - }); - - - it('should not post a comment on GitHub for non-public previews', () => { - buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: false}); - expect(prsAddCommentSpy).not.toHaveBeenCalled(); - }); - - }); - - - describe('on \'pr.changedVisibility\'', () => { - let prsAddCommentSpy: jasmine.Spy; - - beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment')); - - - it('should post a comment on GitHub (for all SHAs) for PRs made public', () => { - const commentBody = 'You can preview 12345 at https://pr42-12345.domain.name/.\n' + - 'You can preview 67890 at https://pr42-67890.domain.name/.'; - - buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true}); - expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody); - }); - - - it('should not post a comment on GitHub if no SHAs were affected', () => { - buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: [], isPublic: true}); - expect(prsAddCommentSpy).not.toHaveBeenCalled(); - }); - - - it('should not post a comment on GitHub for PRs made non-public', () => { - buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: false}); - expect(prsAddCommentSpy).not.toHaveBeenCalled(); - }); - - }); - - - it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => { - const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'); - - buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true}); - buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true}); - - const allCalls = prsAddCommentSpy.calls.all(); - const prs = allCalls[0].object; - - expect(prsAddCommentSpy).toHaveBeenCalledTimes(2); - expect(prs).toBe(allCalls[1].object); - expect(prs).toEqual(jasmine.any(GithubPullRequests)); - expect(prs.repoSlug).toBe('repo/slug'); - expect(prs.requestHeaders.Authorization).toContain('12345'); - }); - - }); - - - describe('createMiddleware()', () => { - let buildVerifier: BuildVerifier; - let buildCreator: BuildCreator; - let agent: supertest.SuperTest; - - // Helpers - const promisifyRequest = (req: supertest.Request) => - new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve())); - const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) => - Promise.all(reqs.map(promisifyRequest)).then(done, done.fail); - - beforeEach(() => { - buildVerifier = new BuildVerifier( - defaultConfig.secret, - defaultConfig.githubToken, - defaultConfig.repoSlug, - defaultConfig.githubOrganization, - defaultConfig.githubTeamSlugs, - defaultConfig.trustedPrLabel, - ); - buildCreator = new BuildCreator(defaultConfig.buildsDir); - agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator)); - - spyOn(console, 'error'); - }); - - - describe('GET /create-build//', () => { - const pr = '9'; - const sha = '9'.repeat(40); - let buildVerifierVerifySpy: jasmine.Spy; - let buildCreatorCreateSpy: jasmine.Spy; - - beforeEach(() => { - const verStatus = BUILD_VERIFICATION_STATUS.verifiedAndTrusted; - buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve(verStatus)); - buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve()); - }); - - - it('should respond with 404 for non-GET requests', done => { - verifyRequests([ - agent.put(`/create-build/${pr}/${sha}`).expect(404), - agent.post(`/create-build/${pr}/${sha}`).expect(404), - agent.patch(`/create-build/${pr}/${sha}`).expect(404), - agent.delete(`/create-build/${pr}/${sha}`).expect(404), - ], done); - }); - - - it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => { - const url = `/create-build/${pr}/${sha}`; - const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`; - - verifyRequests([ - agent.get(url).expect(401, responseBody), - agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody), - ], done); - }); - - - it('should respond with 400 for requests without an \'X-FILE\' header', done => { - const url = `/create-build/${pr}/${sha}`; - const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`; - - const request1 = agent.get(url).set('AUTHORIZATION', 'foo'); - const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', ''); - - verifyRequests([ - request1.expect(400, responseBody), - request2.expect(400, responseBody), - ], done); - }); - - - it('should respond with 404 for unknown paths', done => { - verifyRequests([ - agent.get(`/foo/create-build/${pr}/${sha}`).expect(404), - agent.get(`/foo-create-build/${pr}/${sha}`).expect(404), - agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404), - agent.get(`/create-build/foo/${pr}/${sha}`).expect(404), - agent.get(`/create-build-foo/${pr}/${sha}`).expect(404), - agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404), - agent.get(`/create-build/pr${pr}/${sha}`).expect(404), - agent.get(`/create-build/${pr}/${sha}42`).expect(404), - ], done); - }); - - - it('should call \'BuildVerifier#verify()\' with the correct arguments', done => { - const req = agent. - get(`/create-build/${pr}/${sha}`). - set('AUTHORIZATION', 'foo'). - set('X-FILE', 'bar'); - - promisifyRequest(req). - then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')). - then(done, done.fail); - }); - - - it('should propagate errors from BuildVerifier', done => { - buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test')); - - const req = agent. - get(`/create-build/${pr}/${sha}`). - set('AUTHORIZATION', 'foo'). - set('X-FILE', 'bar'). - expect(500, 'Test'); - - promisifyRequest(req). - then(() => { - expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo'); - expect(buildCreatorCreateSpy).not.toHaveBeenCalled(); - }). - then(done, done.fail); - }); - - - it('should call \'BuildCreator#create()\' with the correct arguments', done => { - buildVerifierVerifySpy.and.returnValues( - Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted), - Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted)); - - const req1 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar'); - const req2 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar'); - - Promise.all([ - promisifyRequest(req1).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', true)), - promisifyRequest(req2).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', false)), - ]).then(done, done.fail); - }); - - - it('should propagate errors from BuildCreator', done => { - buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test')); - const req = agent. - get(`/create-build/${pr}/${sha}`). - set('AUTHORIZATION', 'foo'). - set('X-FILE', 'bar'). - expect(500, 'Test'); - - verifyRequests([req], done); - }); - - - it('should respond with 201 on successful upload (for public builds)', done => { - const req = agent. - get(`/create-build/${pr}/${sha}`). - set('AUTHORIZATION', 'foo'). - set('X-FILE', 'bar'). - expect(201, http.STATUS_CODES[201]); - - verifyRequests([req], done); - }); - - - it('should respond with 202 on successful upload (for hidden builds)', done => { - buildVerifierVerifySpy.and.returnValue(Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted)); - const req = agent. - get(`/create-build/${pr}/${sha}`). - set('AUTHORIZATION', 'foo'). - set('X-FILE', 'bar'). - expect(202, http.STATUS_CODES[202]); - - verifyRequests([req], done); - }); - - - it('should reject PRs with leading zeros', done => { - verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done); - }); - - - it('should accept SHAs with leading zeros (but not trim the zeros)', done => { - const sha40 = '0'.repeat(40); - const sha41 = `0${sha40}`; - - const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar'); - const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux'); - - Promise.all([ - promisifyRequest(request40.expect(201)), - promisifyRequest(request41.expect(404)), - ]).then(done, done.fail); - }); - - }); - - - describe('GET /health-check', () => { - - it('should respond with 200', done => { - verifyRequests([ - agent.get('/health-check').expect(200), - agent.get('/health-check/').expect(200), - ], done); - }); - - - it('should respond with 404 for non-GET requests', done => { - verifyRequests([ - agent.put('/health-check').expect(404), - agent.post('/health-check').expect(404), - agent.patch('/health-check').expect(404), - agent.delete('/health-check').expect(404), - ], done); - }); - - - it('should respond with 404 if the path does not match exactly', done => { - verifyRequests([ - agent.get('/health-check/foo').expect(404), - agent.get('/health-check-foo').expect(404), - agent.get('/health-checknfoo').expect(404), - agent.get('/foo/health-check').expect(404), - agent.get('/foo-health-check').expect(404), - agent.get('/foonhealth-check').expect(404), - ], done); - }); - - }); - - - describe('POST /pr-updated', () => { - const pr = '9'; - const url = '/pr-updated'; - let bvGetPrIsTrustedSpy: jasmine.Spy; - let bcUpdatePrVisibilitySpy: jasmine.Spy; - - // Helpers - const createRequest = (num: number, action?: string) => - agent.post(url).send({number: num, action}); - - beforeEach(() => { - bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted'); - bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility'); - }); - - - it('should respond with 404 for non-POST requests', done => { - verifyRequests([ - agent.get(url).expect(404), - agent.put(url).expect(404), - agent.patch(url).expect(404), - agent.delete(url).expect(404), - ], done); - }); - - - it('should respond with 400 for requests without a payload', done => { - const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`; - - const request1 = agent.post(url); - const request2 = agent.post(url).send(); - - verifyRequests([ - request1.expect(400, responseBody), - request2.expect(400, responseBody), - ], done); - }); - - - it('should respond with 400 for requests without a \'number\' field', done => { - const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`; - - const request1 = agent.post(url).send({}); - const request2 = agent.post(url).send({number: null}); - - verifyRequests([ - request1.expect(400, `${responseBodyPrefix} {}`), - request2.expect(400, `${responseBodyPrefix} {"number":null}`), - ], done); - }); - - - it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => { - const req = createRequest(+pr); - - promisifyRequest(req). - then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)). - then(done, done.fail); - }); - - - it('should propagate errors from BuildVerifier', done => { - bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test')); - - const req = createRequest(+pr).expect(500, 'Test'); - - promisifyRequest(req). - then(() => { - expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9); - expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled(); - }). - then(done, done.fail); - }); - - - it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => { - bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42)); - - const req1 = createRequest(24); - const req2 = createRequest(42); - - Promise.all([ - promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)), - promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)), - ]).then(done, done.fail); - }); - - - it('should propagate errors from BuildCreator', done => { - bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test')); - - const req = createRequest(+pr).expect(500, 'Test'); - verifyRequests([req], done); - }); - - - describe('on success', () => { - - it('should respond with 200 (action: undefined)', done => { - bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); - - const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200])); - verifyRequests(reqs, done); - }); - - - it('should respond with 200 (action: labeled)', done => { - bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); - - const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200])); - verifyRequests(reqs, done); - }); - - - it('should respond with 200 (action: unlabeled)', done => { - bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false)); - - const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200])); - verifyRequests(reqs, done); - }); - - - it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => { - const promises = ['foo', 'notlabeled']. - map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])). - map(promisifyRequest); - - Promise.all(promises). - then(() => { - expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled(); - expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled(); - }). - then(done, done.fail); - }); - - }); - - }); - - - describe('ALL *', () => { - - it('should respond with 404', done => { - const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`; - - verifyRequests([ - agent.get('/some/url').expect(404, responseFor('get')), - agent.put('/some/url').expect(404, responseFor('put')), - agent.post('/some/url').expect(404, responseFor('post')), - agent.patch('/some/url').expect(404, responseFor('patch')), - agent.delete('/some/url').expect(404, responseFor('delete')), - ], done); - }); - - }); - - }); - -}); diff --git a/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock b/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock index 73ef310751..587addbbb1 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock +++ b/aio/aio-builds-setup/dockerbuild/scripts-js/yarn.lock @@ -2,11 +2,17 @@ # yarn lockfile v1 -"@types/body-parser@^1.16.5": - version "1.16.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.5.tgz#d2b7daefab84e0afa9d3fae0935bc7355b6320af" +"@types/body-parser@*", "@types/body-parser@^1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" dependencies: - "@types/express" "*" "@types/node" "*" "@types/express-serve-static-core@*": @@ -15,17 +21,11 @@ dependencies: "@types/node" "*" -"@types/express@*": - version "4.0.36" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2" - dependencies: - "@types/express-serve-static-core" "*" - "@types/serve-static" "*" - -"@types/express@^4.0.37": - version "4.0.37" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.37.tgz#625ac3765169676e01897ca47011c26375784971" +"@types/express@^4.16.0": + version "4.16.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19" dependencies: + "@types/body-parser" "*" "@types/express-serve-static-core" "*" "@types/serve-static" "*" @@ -36,15 +36,9 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/jasmine@^2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.6.0.tgz#997b41a27752b4850af2683bc4a8d8222c25bd02" - -"@types/jsonwebtoken@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.3.tgz#483c8f39945e1e6d308dcc51fd4aeca5208d4dca" - dependencies: - "@types/node" "*" +"@types/jasmine@^2.8.8": + version "2.8.8" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.8.tgz#bf53a7d193ea8b03867a38bfdb4fbb0e0bf066c9" "@types/mime@*": version "1.3.0" @@ -54,13 +48,25 @@ version "3.0.1" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.1.tgz#b683eb60be358304ef146f5775db4c0e3696a550" +"@types/nock@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@types/nock/-/nock-9.3.0.tgz#9d34358fdcc08afd07144e0784ac9e951d412dd4" + dependencies: + "@types/node" "*" + +"@types/node-fetch@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.1.2.tgz#8c5da14d70321e4c4ecd5db668e3f93cf6c7399f" + dependencies: + "@types/node" "*" + "@types/node@*": version "7.0.31" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.31.tgz#80ea4d175599b2a00149c29a10a4eb2dff592e86" -"@types/node@^8.0.30": - version "8.0.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.30.tgz#aa3c42946fc6357737eb215349fe728b38679d05" +"@types/node@^10.9.2": + version "10.9.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.2.tgz#f0ab8dced5cd6c56b26765e1c0d9e4fdcc9f2a00" "@types/serve-static@*": version "1.7.31" @@ -82,9 +88,9 @@ dependencies: "@types/node" "*" -"@types/supertest@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.3.tgz#edcae925c427dec6a7abe2697ee4b06fb29664c1" +"@types/supertest@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.5.tgz#18d082a667eaed22759be98f4923e0061ae70c62" dependencies: "@types/superagent" "*" @@ -92,48 +98,53 @@ abbrev@1: version "1.1.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" -accepts@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" +accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" dependencies: - mime-types "~2.1.11" + mime-types "~2.1.18" negotiator "0.6.1" -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" dependencies: string-width "^2.0.0" -ansi-regex@^0.2.0, ansi-regex@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" +ansi-green@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-green/-/ansi-green-0.1.1.tgz#8a5d9a979e458d57c40e33580b37390b8e10d0f7" + dependencies: + ansi-wrap "0.1.0" ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" -ansi-styles@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -anymatch@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" dependencies: - arrify "^1.0.0" - micromatch "^2.1.5" + color-convert "^1.9.0" + +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" aproba@^1.0.3: version "1.1.2" @@ -146,39 +157,51 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" dependencies: - arr-flatten "^1.0.1" + sprintf-js "~1.0.2" -arr-flatten@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" -assert-plus@1.0.0, assert-plus@^1.0.0: +assertion-error@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + +assign-symbols@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" async-each@^1.0.0: version "1.0.1" @@ -188,13 +211,9 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" babel-code-frame@^6.22.0: version "6.26.0" @@ -208,27 +227,30 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" -base64url@2.0.0, base64url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" dependencies: - tweetnacl "^0.14.3" + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" binary-extensions@^1.0.0: version "1.8.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" +bl@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" dependencies: - inherits "~2.0.0" + readable-stream "^2.3.5" + safe-buffer "^5.1.1" -body-parser@^1.18.2: +body-parser@1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" dependencies: @@ -243,23 +265,32 @@ body-parser@^1.18.2: raw-body "2.3.2" type-is "~1.6.15" -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" +body-parser@^1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" dependencies: - hoek "2.x.x" + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" -boxen@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.1.0.tgz#b1b69dd522305e807a99deee777dbd6e5167b102" +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" dependencies: ansi-align "^2.0.0" camelcase "^4.0.0" - chalk "^1.1.1" + chalk "^2.0.1" cli-boxes "^1.0.0" string-width "^2.0.0" - term-size "^0.1.0" - widest-line "^1.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" brace-expansion@^1.1.7: version "1.1.8" @@ -268,22 +299,62 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" +braces@^2.3.0, braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" +buffer-alloc-unsafe@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a" + +buffer-alloc@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303" + dependencies: + buffer-alloc-unsafe "^0.1.0" + buffer-fill "^0.1.0" + +buffer-fill@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.1.tgz#76d825c4d6e50e06b7a31eb520c04d08cc235071" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + +builtin-modules@^1.0.0, builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + camelcase@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -292,21 +363,18 @@ capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -chalk@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" +chai@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" dependencies: - ansi-styles "^1.1.0" - escape-string-regexp "^1.0.0" - has-ansi "^0.1.0" - strip-ansi "^0.3.0" - supports-color "^0.2.0" + assertion-error "^1.0.1" + check-error "^1.0.1" + deep-eql "^3.0.0" + get-func-name "^2.0.0" + pathval "^1.0.0" + type-detect "^4.0.0" -chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -316,52 +384,90 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chokidar@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" +chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" dependencies: - anymatch "^1.3.0" + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +check-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + +chokidar@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" + dependencies: + anymatch "^2.0.0" async-each "^1.0.0" - glob-parent "^2.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" inherits "^2.0.1" is-binary-path "^1.0.0" - is-glob "^2.0.0" + is-glob "^4.0.0" + lodash.debounce "^4.0.8" + normalize-path "^2.1.1" path-is-absolute "^1.0.0" readdirp "^2.0.0" + upath "^1.0.5" optionalDependencies: - fsevents "^1.0.0" + fsevents "^1.2.2" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +ci-info@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.4.0.tgz#4841d53cad49f11b827b648ebde27a6e189b412f" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -colors@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" +color-convert@^1.9.0: + version "1.9.2" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147" + dependencies: + color-name "1.1.1" + +color-name@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" + +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" -commander@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" +commander@^2.12.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" -commander@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - -component-emitter@^1.2.0: +component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -369,19 +475,6 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concurrently@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.5.0.tgz#8cf1b7707a6916a78a4ff5b77bb04dec54b379b2" - dependencies: - chalk "0.5.1" - commander "2.6.0" - date-fns "^1.23.0" - lodash "^4.5.1" - rx "2.3.24" - spawn-command "^0.0.2-1" - supports-color "^3.2.3" - tree-kill "^1.1.0" - configstore@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.0.tgz#45df907073e26dfa1cf4b2d52f5b60545eaa11d1" @@ -401,10 +494,6 @@ content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" -content-type@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" - content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -417,9 +506,13 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" -cookiejar@^2.0.6: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" +cookiejar@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" core-util-is@~1.0.0: version "1.0.2" @@ -431,40 +524,29 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -cross-spawn-async@^2.1.1: - version "2.2.5" - resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: - lru-cache "^4.0.0" - which "^1.2.8" + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" +cross-spawn@^6.0.4: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" dependencies: - boom "2.x.x" + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -date-fns@^1.23.0: - version "1.28.5" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" - -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - dependencies: - ms "2.0.0" - -debug@2.6.9, debug@^2.6.8: +debug@2.6.9, debug@^2.1.2, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -476,10 +558,59 @@ debug@^2.2.0: dependencies: ms "2.0.0" +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-eql@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + dependencies: + type-detect "^4.0.0" + +deep-equal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -488,14 +619,30 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +delete-empty@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/delete-empty/-/delete-empty-2.0.0.tgz#dcf7c4f93a98445119acd57b137d13e7af78fa39" + dependencies: + log-ok "^0.1.1" + relative "^3.0.2" + rimraf "^2.6.2" + depd@1.1.1, depd@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + diff@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" @@ -514,46 +661,63 @@ duplexer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -ecdsa-sig-formatter@1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" - dependencies: - base64url "^2.0.0" - safe-buffer "^5.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -encodeurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" -es6-promise@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" +end-of-stream@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.4.3: + version "1.12.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -etag@~1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" event-stream@~3.3.0: version "3.3.4" @@ -567,179 +731,174 @@ event-stream@~3.3.0: stream-combiner "~0.0.4" through "~2.3.1" -execa@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" dependencies: - cross-spawn-async "^2.1.1" + cross-spawn "^5.0.1" + get-stream "^3.0.0" is-stream "^1.1.0" - npm-run-path "^1.0.0" - object-assign "^4.0.1" - path-key "^1.0.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" strip-eof "^1.0.0" -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" dependencies: - is-posix-bracket "^0.1.0" + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" +express@^4.16.3: + version "4.16.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" dependencies: - fill-range "^2.1.0" - -express@^4.15.4: - version "4.15.4" - resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" - dependencies: - accepts "~1.3.3" + accepts "~1.3.5" array-flatten "1.1.1" + body-parser "1.18.2" content-disposition "0.5.2" - content-type "~1.0.2" + content-type "~1.0.4" cookie "0.3.1" cookie-signature "1.0.6" - debug "2.6.8" - depd "~1.1.1" - encodeurl "~1.0.1" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" escape-html "~1.0.3" - etag "~1.8.0" - finalhandler "~1.0.4" - fresh "0.5.0" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" - parseurl "~1.3.1" + parseurl "~1.3.2" path-to-regexp "0.1.7" - proxy-addr "~1.1.5" - qs "6.5.0" + proxy-addr "~2.0.3" + qs "6.5.1" range-parser "~1.2.0" - send "0.15.4" - serve-static "1.12.4" - setprototypeof "1.0.3" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.0" - vary "~1.1.1" + safe-buffer "5.1.1" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" -extend@^3.0.0, extend@~3.0.0: +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" dependencies: - is-extglob "^1.0.0" + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - -fill-range@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^1.1.3" - repeat-element "^1.1.2" - repeat-string "^1.5.2" + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" -finalhandler@~1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.6.tgz#007aea33d1a4d3e42017f624848ad58d212f814f" +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" dependencies: debug "2.6.9" - encodeurl "~1.0.1" + encodeurl "~1.0.2" escape-html "~1.0.3" on-finished "~2.3.0" parseurl "~1.3.2" - statuses "~1.3.1" + statuses "~1.4.0" unpipe "~1.0.0" -for-in@^1.0.1: +for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - dependencies: - for-in "^1.0.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.2.0.tgz#9a5e3b9295f980b2623cf64fa238b14cebca707b" +form-data@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" dependencies: asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" + combined-stream "1.0.6" mime-types "^2.1.12" formidable@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" -forwarded@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" -fresh@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" +fsevents@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.36" + nan "^2.9.2" + node-pre-gyp "^0.10.0" -fstream-ignore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: - version "1.0.11" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" +function-bind@^1.0.2, function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" gauge@~2.7.3: version "2.7.4" @@ -754,28 +913,24 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - dependencies: - assert-plus "^1.0.0" +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - dependencies: - is-glob "^2.0.0" + is-glob "^3.1.0" + path-dirname "^1.0.0" glob@^7.0.0, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: version "7.1.2" @@ -788,6 +943,12 @@ glob@^7.0.0, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + dependencies: + ini "^1.3.4" + got@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -808,49 +969,56 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -has-ansi@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" - dependencies: - ansi-regex "^0.2.0" - has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" dependencies: ansi-regex "^2.0.0" -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" @@ -861,22 +1029,41 @@ http-errors@1.6.2, http-errors@~1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" +http-errors@1.6.3, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -892,10 +1079,14 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +ini@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -904,9 +1095,25 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -ipaddr.js@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" +ipaddr.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" is-binary-path@^1.0.0: version "1.0.1" @@ -918,23 +1125,67 @@ is-buffer@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" dependencies: - is-primitive "^2.0.0" + builtin-modules "^1.0.0" -is-extendable@^0.1.1: +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + +is-ci@^1.0.10: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.0.tgz#3f4a08d6303a09882cef3f0fb97439c5f5ce2d53" + dependencies: + ci-info "^1.3.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" is-fullwidth-code-point@^1.0.0: version "1.0.0" @@ -946,22 +1197,29 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" dependencies: - is-extglob "^1.0.0" + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - dependencies: - kind-of "^3.0.2" - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -972,18 +1230,28 @@ is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + is-retry-allowed@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" @@ -992,9 +1260,13 @@ is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" isarray@1.0.0, isarray@~1.0.0: version "1.0.0" @@ -1010,41 +1282,37 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" -jasmine-core@~2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" +jasmine-core@~3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.2.1.tgz#8e4ff5b861603ee83343f2b49eee6a0ffe9650ce" -jasmine@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" +jasmine@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.2.0.tgz#b3a018454781805650e46578803d08e7cfdd7b3d" dependencies: - exit "^0.1.2" glob "^7.0.6" - jasmine-core "~2.8.0" + jasmine-core "~3.2.0" js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" +js-yaml@^3.7.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: - jsonify "~0.0.0" + argparse "^1.0.7" + esprima "^4.0.0" -json-stringify-safe@~5.0.1: +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + +json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -1052,48 +1320,7 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" -jsonwebtoken@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.0.1.tgz#50daef8d0a8c7de2cd06bc1013b75b04ccf3f0cf" - dependencies: - jws "^3.1.4" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.0.0" - xtend "^4.0.1" - -jsprim@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" - dependencies: - assert-plus "1.0.0" - extsprintf "1.0.2" - json-schema "0.2.3" - verror "1.3.6" - -jwa@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" - dependencies: - base64url "2.0.0" - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.9" - safe-buffer "^5.0.1" - -jws@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" - dependencies: - base64url "^2.0.0" - jwa "^1.1.4" - safe-buffer "^5.0.1" - -kind-of@^3.0.2: +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" dependencies: @@ -1105,117 +1332,51 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + latest-version@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" dependencies: package-json "^4.0.0" -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" +lodash@^4.17.5: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" -lodash._createassigner@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" +log-ok@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" dependencies: - lodash._bindcallback "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.restparam "^3.0.0" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - -lodash.assign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" - dependencies: - lodash._baseassign "^3.0.0" - lodash._createassigner "^3.0.0" - lodash.keys "^3.0.0" - -lodash.defaults@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" - dependencies: - lodash.assign "^3.0.0" - lodash.restparam "^3.0.0" - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - -lodash@^4.5.1: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + ansi-green "^0.1.1" + success-symbol "^0.1.0" lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" +lru-cache@^4.0.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" dependencies: pseudomap "^1.0.2" yallist "^2.1.2" @@ -1226,14 +1387,28 @@ make-dir@^1.0.0: dependencies: pify "^2.3.0" +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -1242,39 +1417,53 @@ methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" +micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: +mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + +mime-types@^2.1.12, mime-types@~2.1.15: version "2.1.15" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" dependencies: mime-db "~1.27.0" -mime@1.3.4, mime@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mime-types@~2.1.18: + version "2.1.20" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + dependencies: + mime-db "~1.36.0" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -1288,52 +1477,119 @@ minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -"mkdirp@>=0.5 0", mkdirp@^0.5.1: +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -ms@2.0.0, ms@^2.0.0: +ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -nan@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" +nan@^2.9.2: + version "2.11.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +needle@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.2.tgz#1120ca4c41f2fcc6976fd28a8968afe239929418" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" -node-pre-gyp@^0.6.36: - version "0.6.36" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + +nock@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-9.6.1.tgz#d96e099be9bc1d0189a77f4490bbbb265c381b49" dependencies: + chai "^4.1.2" + debug "^3.1.0" + deep-equal "^1.0.0" + json-stringify-safe "^5.0.1" + lodash "^4.17.5" + mkdirp "^0.5.0" + propagate "^1.0.0" + qs "^6.5.1" + semver "^5.5.0" + +node-fetch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" + +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + dependencies: + detect-libc "^1.0.2" mkdirp "^0.5.1" + needle "^2.2.1" nopt "^4.0.1" + npm-packlist "^1.1.6" npmlog "^4.0.2" - rc "^1.1.7" - request "^2.81.0" + rc "^1.2.7" rimraf "^2.6.1" semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" + tar "^4" -nodemon@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.12.1.tgz#996a56dc49d9f16bbf1b78a4de08f13634b3878d" +nodemon@^1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.3.tgz#46e681ee0dd1b590562e03019b4c5df234f906f9" dependencies: - chokidar "^1.7.0" - debug "^2.6.8" - es6-promise "^3.3.1" + chokidar "^2.0.2" + debug "^3.1.0" ignore-by-default "^1.0.1" - lodash.defaults "^3.1.2" minimatch "^3.0.4" - ps-tree "^1.1.0" + pstree.remy "^1.1.0" + semver "^5.5.0" + supports-color "^5.2.0" touch "^3.1.0" - undefsafe "0.0.3" - update-notifier "^2.2.0" + undefsafe "^2.0.2" + update-notifier "^2.3.0" nopt@^4.0.1: version "4.0.1" @@ -1348,17 +1604,51 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-path@^2.0.1: +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: remove-trailing-separator "^1.0.1" -npm-run-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" +npm-bundled@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" + +npm-packlist@^1.1.6: + version "1.1.11" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" dependencies: - path-key "^1.0.0" + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-all@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.3.tgz#49f15b55a66bb4101664ce270cb18e7103f8f185" + dependencies: + ansi-styles "^3.2.0" + chalk "^2.1.0" + cross-spawn "^6.0.4" + memorystream "^0.3.1" + minimatch "^3.0.4" + ps-tree "^1.1.0" + read-pkg "^3.0.0" + shell-quote "^1.6.1" + string.prototype.padend "^3.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" npmlog@^4.0.2: version "4.1.0" @@ -1373,20 +1663,33 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" on-finished@~2.3.0: version "2.3.0" @@ -1394,7 +1697,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.3: +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -1415,6 +1718,10 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -1424,30 +1731,36 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parseurl@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-key@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" path-parse@^1.0.5: version "1.0.5" @@ -1457,38 +1770,56 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + +pathval@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" dependencies: through "~2.3" -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -proxy-addr@~1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +propagate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709" + +proxy-addr@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" dependencies: - forwarded "~0.1.0" - ipaddr.js "1.4.0" + forwarded "~0.1.2" + ipaddr.js "1.8.0" ps-tree@^1.1.0: version "1.1.0" @@ -1500,28 +1831,19 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +pstree.remy@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.0.tgz#f2af27265bd3e5b32bbfcc10e80bac55ba78688b" + dependencies: + ps-tree "^1.1.0" -qs@6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" - -qs@6.5.1: +qs@6.5.1, qs@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" -qs@^6.1.0, qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -randomatic@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" range-parser@~1.2.0: version "1.2.0" @@ -1536,7 +1858,16 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +rc@^1.0.1, rc@^1.1.6: version "1.2.1" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" dependencies: @@ -1545,7 +1876,24 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4: +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6: version "2.2.11" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72" dependencies: @@ -1557,6 +1905,18 @@ readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable string_decoder "~1.0.0" util-deprecate "~1.0.1" +readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -1572,12 +1932,12 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -regex-cache@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" dependencies: - is-equal-shallow "^0.1.3" - is-primitive "^2.0.0" + extend-shallow "^3.0.2" + safe-regex "^1.1.0" registry-auth-token@^3.0.1: version "3.3.1" @@ -1592,6 +1952,12 @@ registry-url@^3.0.3: dependencies: rc "^1.0.1" +relative@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" + dependencies: + isobject "^2.0.0" + remove-trailing-separator@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511" @@ -1600,36 +1966,13 @@ repeat-element@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" -repeat-string@^1.5.2: +repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" -request@^2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolve@^1.1.6: version "1.3.3" @@ -1643,60 +1986,96 @@ resolve@^1.3.2: dependencies: path-parse "^1.0.5" -rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + +rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" -rx@2.3.24: - version "2.3.24" - resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" +rimraf@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +safe-buffer@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" safe-buffer@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" +safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" dependencies: semver "^5.0.3" +"semver@2 || 3 || 4 || 5": + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" -send@0.15.4: - version "0.15.4" - resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" +semver@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" dependencies: - debug "2.6.8" - depd "~1.1.1" + debug "2.6.9" + depd "~1.1.2" destroy "~1.0.4" - encodeurl "~1.0.1" + encodeurl "~1.0.2" escape-html "~1.0.3" - etag "~1.8.0" - fresh "0.5.0" + etag "~1.8.1" + fresh "0.5.2" http-errors "~1.6.2" - mime "1.3.4" + mime "1.4.1" ms "2.0.0" on-finished "~2.3.0" range-parser "~1.2.0" - statuses "~1.3.1" + statuses "~1.4.0" -serve-static@1.12.4: - version "1.12.4" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" dependencies: - encodeurl "~1.0.1" + encodeurl "~1.0.2" escape-html "~1.0.3" - parseurl "~1.3.1" - send "0.15.4" + parseurl "~1.3.2" + send "0.16.2" set-blocking@~2.0.0: version "2.0.0" @@ -1706,13 +2085,54 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" -shelljs@^0.7.8: - version "0.7.8" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +shelljs@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -1726,15 +2146,89 @@ slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" dependencies: - hoek "2.x.x" + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" -spawn-command@^0.0.2-1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" split@0.3: version "0.3.3" @@ -1742,24 +2236,29 @@ split@0.3: dependencies: through "2" -sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" -"statuses@>= 1.3.1 < 2", statuses@~1.3.1: +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.3.1 < 2": version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -1781,21 +2280,32 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string.prototype.padend@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.4.3" + function-bind "^1.0.2" + string_decoder@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.2.tgz#b29e1f4e1125fa97a10382b8a533737b7491e179" dependencies: safe-buffer "~5.0.1" -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" dependencies: - ansi-regex "^0.2.1" + safe-buffer "~5.1.0" strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" @@ -1803,6 +2313,16 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -1811,68 +2331,71 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -superagent@^3.0.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.5.2.tgz#3361a3971567504c351063abeaae0faa23dbf3f8" +success-symbol@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" + +superagent@3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" dependencies: component-emitter "^1.2.0" - cookiejar "^2.0.6" - debug "^2.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" extend "^3.0.0" - form-data "^2.1.1" + form-data "^2.3.1" formidable "^1.1.1" methods "^1.1.1" - mime "^1.3.4" - qs "^6.1.0" + mime "^1.4.1" + qs "^6.5.1" readable-stream "^2.0.5" -supertest@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" +supertest@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" dependencies: methods "~1.1.2" - superagent "^3.0.0" - -supports-color@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + superagent "3.8.2" supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" +supports-color@^5.2.0, supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" dependencies: - has-flag "^1.0.0" + has-flag "^3.0.0" -tar-pack@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" +tar-stream@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395" dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" + bl "^1.0.0" + buffer-alloc "^1.1.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.0" + xtend "^4.0.0" -tar@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" +tar@^4: + version "4.4.6" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" dependencies: - block-stream "*" - fstream "^1.0.2" - inherits "2" + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" -term-size@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-0.1.1.tgz#87360b96396cab5760963714cda0d0cbeecad9ca" +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" dependencies: - execa "^0.4.0" + execa "^0.7.0" through@2, through@~2.3, through@~2.3.1: version "2.3.8" @@ -1882,62 +2405,74 @@ timed-out@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" +to-buffer@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" dependencies: nopt "~1.0.10" -tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" - dependencies: - punycode "^1.4.1" +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" -tree-kill@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.1.0.tgz#c963dcf03722892ec59cba569e940b71954d1729" - -tslib@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec" - -tslint-jasmine-noSkipOrFocus@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/tslint-jasmine-noSkipOrFocus/-/tslint-jasmine-noSkipOrFocus-1.0.8.tgz#91ebe63d71625c5f00a13f060a2b1945c407923b" +tslint-jasmine-noSkipOrFocus@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/tslint-jasmine-noSkipOrFocus/-/tslint-jasmine-noSkipOrFocus-1.0.9.tgz#a562d3a6b3aa5c51e5ad29211a6eeb06ad1a082f" dependencies: rimraf "^2.6.1" -tslint@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552" +tslint@^5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" dependencies: babel-code-frame "^6.22.0" - colors "^1.1.2" - commander "^2.9.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" diff "^3.2.0" glob "^7.1.1" + js-yaml "^3.7.0" minimatch "^3.0.4" resolve "^1.3.2" semver "^5.3.0" - tslib "^1.7.1" - tsutils "^2.8.1" + tslib "^1.8.0" + tsutils "^2.27.2" -tsutils@^2.8.1: - version "2.8.2" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a" +tsutils@^2.27.2: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" dependencies: - tslib "^1.7.1" + tslib "^1.8.1" -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +type-detect@^4.0.0: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" type-is@~1.6.15: version "1.6.15" @@ -1946,17 +2481,31 @@ type-is@~1.6.15: media-typer "0.3.0" mime-types "~2.1.15" -typescript@^2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34" +type-is@~1.6.16: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" -uid-number@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +typescript@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb" -undefsafe@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" +undefsafe@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" + dependencies: + debug "^2.2.0" + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" unique-string@^1.0.0: version "1.0.0" @@ -1968,54 +2517,72 @@ unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -update-notifier@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.2.0.tgz#1b5837cf90c0736d88627732b661c138f86de72f" +upath@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" + +update-notifier@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" dependencies: - boxen "^1.0.0" - chalk "^1.0.0" + boxen "^1.2.1" + chalk "^2.0.1" configstore "^3.0.0" import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" is-npm "^1.0.0" latest-version "^3.0.0" semver-diff "^2.0.0" xdg-basedir "^3.0.0" +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" dependencies: prepend-http "^1.0.1" +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -utils-merge@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" -uuid@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - -vary@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" - -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" dependencies: - extsprintf "1.0.2" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" -which@^1.2.8: - version "1.2.14" - resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" dependencies: isexe "^2.0.0" @@ -2025,11 +2592,11 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2" -widest-line@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c" +widest-line@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273" dependencies: - string-width "^1.0.1" + string-width "^2.1.1" wrappy@1: version "1.0.2" @@ -2047,10 +2614,14 @@ xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" -xtend@^4.0.1: +xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh old mode 100755 new mode 100644 index 951f88f1c1..49525ea46d --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh @@ -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 diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/dev-mode.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/dev-mode.sh new file mode 100644 index 0000000000..64ed6b7377 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/dev-mode.sh @@ -0,0 +1,12 @@ +# Link the scripts on the host to the scripts in the container +# - the host scripts are mounted as a volume at `/dockerbuild`) +# - the original scripts are moved to `..._prod` in case they are needed later +# See `aio/aio-builds-setup/docs/misc--debug-docker-container.md` for more info + +mv $AIO_SCRIPTS_SH_DIR ${AIO_SCRIPTS_SH_DIR}_prod +ln -s /dockerbuild/scripts-sh $AIO_SCRIPTS_SH_DIR +chmod a+x $AIO_SCRIPTS_SH_DIR/* + +mv $AIO_SCRIPTS_JS_DIR ${AIO_SCRIPTS_JS_DIR}_prod +ln -s /dockerbuild/scripts-js $AIO_SCRIPTS_JS_DIR + diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh index d61ddada65..6a50c22175 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/health-check.sh @@ -30,7 +30,7 @@ done # Check servers origins=( - http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT + http://$AIO_PREVIEW_SERVER_HOSTNAME:$AIO_PREVIEW_SERVER_PORT http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS ) diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh old mode 100755 new mode 100644 index 9e888e0c3d..b9f949e7f0 --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh @@ -14,5 +14,5 @@ service cron start service dnsmasq start service nginx start service pm2-root start -aio-upload-server-prod start +aio-preview-server-prod start echo [`date`] - Services started successfully. diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/preview-server-prod.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/preview-server-prod.sh new file mode 100644 index 0000000000..091384ee48 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/preview-server-prod.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -eu -o pipefail + +# Set up env variables for production +export AIO_CIRCLE_CI_TOKEN=$(head -c -1 /aio-secrets/CIRCLE_CI_TOKEN 2>/dev/null || echo "MISSING_CIRCLE_CI_TOKEN") +export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN") + +# Start the preview-server instance +action=$([ "$1" == "stop" ] && echo "stop" || echo "start") +pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/preview-server \ + --uid $AIO_WWW_USER \ + --log /var/log/aio/preview-server-prod.log \ + --name aio-preview-server-prod \ + ${@:2} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/preview-server-test.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/preview-server-test.sh new file mode 100644 index 0000000000..8a94550441 --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/preview-server-test.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -eu -o pipefail + +# Start the preview-server instance +appName=aio-preview-server-test +if [[ "$1" == "stop" ]]; then + pm2 delete $appName +else + source aio-test-env + pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-preview-server.js \ + --uid $AIO_WWW_USER \ + --log /var/log/aio/preview-server-test.log \ + --name $appName \ + --no-autorestart \ + ${@:2} +fi diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/test-env.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/test-env.sh new file mode 100644 index 0000000000..00826bab4d --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/test-env.sh @@ -0,0 +1,19 @@ + # Set up env variables for testing + export AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME + export AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP + export AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS + + export AIO_ARTIFACT_PATH=$TEST_AIO_ARTIFACT_PATH + export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR + export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME + export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION + export AIO_GITHUB_REPO=$TEST_AIO_GITHUB_REPO + export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS + export AIO_SIGNIFICANT_FILES_PATTERN=$TEST_AIO_SIGNIFICANT_FILES_PATTERN + export AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL + export AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME + export AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT + export AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE + + export AIO_CIRCLE_CI_TOKEN=TEST_CIRCLE_CI_TOKEN + export AIO_GITHUB_TOKEN=TEST_GITHUB_TOKEN diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh deleted file mode 100755 index 3e31be22c6..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-prod.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -eu -o pipefail - -# Set up env variables for production -export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN") -export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "MISSING_PREVIEW_DEPLOYMENT_TOKEN") - -# Start the upload-server instance -action=$([ "$1" == "stop" ] && echo "stop" || echo "start") -pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \ - --uid $AIO_WWW_USER \ - --log /var/log/aio/upload-server-prod.log \ - --name aio-upload-server-prod \ - ${@:2} diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh deleted file mode 100644 index 1aeb4e2fa0..0000000000 --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/upload-server-test.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -eu -o pipefail - -# Set up env variables for testing -export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR -export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME -export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION -export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS -export AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG -export AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL -export AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME -export AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT - -export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null || echo "TEST_GITHUB_TOKEN") -export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/TEST_PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "TEST_PREVIEW_DEPLOYMENT_TOKEN") - -# Start the upload-server instance -appName=aio-upload-server-test -if [[ "$1" == "stop" ]]; then - pm2 delete $appName -else - pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-upload-server.js \ - --uid $AIO_WWW_USER \ - --log /var/log/aio/upload-server-test.log \ - --name $appName \ - --no-autorestart \ - ${@:2} -fi diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup-and-log.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup-and-log.sh new file mode 100644 index 0000000000..af8f876eca --- /dev/null +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup-and-log.sh @@ -0,0 +1,2 @@ +aio-verify-setup +ls -t /var/log/aio/preview-server-verify* | head -1 | xargs cat diff --git a/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh b/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh index 0011d7fcd9..a83fcca693 100644 --- a/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh +++ b/aio/aio-builds-setup/dockerbuild/scripts-sh/verify-setup.sh @@ -2,7 +2,7 @@ set -eu -o pipefail logFile=/var/log/aio/verify-setup.log -uploadServerLogFile=/var/log/aio/upload-server-verify-setup.log +previewServerLogFile=/var/log/aio/preview-server-verify-setup.log exec 3>&1 exec >> $logFile @@ -23,18 +23,22 @@ function countdown { } function onExit { - aio-upload-server-test stop + echo -e "Stopping Test Server" + aio-preview-server-test stop echo -e "Full logs in '$logFile'.\n" > /dev/fd/3 } # Setup EXIT trap trap 'onExit' EXIT -# Start an upload-server instance for testing -aio-upload-server-test start --log $uploadServerLogFile +# Start an preview-server instance for testing +echo -e "Starting Test Server" +aio-preview-server-test start --log $previewServerLogFile -# Give the upload-server some time to start :( +# Give the preview-server some time to start :( countdown "Starting" 5 > /dev/fd/3 # Run the tests +echo Running the tests +source aio-test-env node $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup | tee /dev/fd/3 diff --git a/aio/aio-builds-setup/docs/image-config--environment-variables.md b/aio/aio-builds-setup/docs/image-config--environment-variables.md index 4584b15596..43ba7e70a5 100644 --- a/aio/aio-builds-setup/docs/image-config--environment-variables.md +++ b/aio/aio-builds-setup/docs/image-config--environment-variables.md @@ -10,18 +10,26 @@ environment variables and their default values can be found in the Each variable has a `TEST_` prefixed counterpart, which is used for testing purposes. In most cases you don't need to specify values for those. +- `AIO_ARTIFACT_PATH`: + The path used to identify the AIO build artifact on the CircleCI servers. This should be equal to + the path given in the `.circleci/config.yml` file for the + `aio_preview->steps->store_artifacts->destination` key. + - `AIO_BUILDS_DIR`: - The directory (inside the container) where the uploaded build artifacts are kept. + The directory (inside the container) where the hosted build artifacts are kept. - `AIO_DOMAIN_NAME`: The domain name of the server. - `AIO_GITHUB_ORGANIZATION`: - The GitHub organization whose teams are whitelisted for accepting uploads. + The GitHub organization whose teams are whitelisted for accepting build artifacts. See also `AIO_GITHUB_TEAM_SLUGS`. +- `AIO_GITHUB_REPO`: + The Github repository for which PRs will be hosted. + - `AIO_GITHUB_TEAM_SLUGS`: - A comma-separated list of teams, whose authors are allowed to upload PRs. + A comma-separated list of teams, whose authors are allowed to preview PRs. See also `AIO_GITHUB_ORGANIZATION`. - `AIO_NGINX_HOSTNAME`: @@ -36,22 +44,24 @@ you don't need to specify values for those. The port number on which nginx listens for HTTPS connections. This should be mapped to the corresponding port on the host VM (as described [here](vm-setup--start-docker-container.md)). -- `AIO_REPO_SLUG`: - The repository slug (in the form `/`) for which PRs will be uploaded. +- `AIO_SIGNIFICANT_FILES_PATTERN`: + The RegExp that determines whether a changed file indicates that a new preview needs to + be deployed. For example, if there is a changed file in the `/packages` directory then + some of the API docs might have changed, so we need to create a new preview. - `AIO_TRUSTED_PR_LABEL`: The PR whose presence indicates the PR has been manually verified and is allowed to have its build artifacts publicly served. This is useful for enabling previews for any PR (not only those from trusted authors). -- `AIO_UPLOAD_HOSTNAME`: - The internal hostname for accessing the Node.js upload-server. This is used by nginx for - delegating upload requests and also for performing a periodic health-check. +- `AIO_PREVIEW_SERVER_HOSTNAME`: + The internal hostname for accessing the Node.js preview-server. This is used by nginx for + delegating web-hook requests and also for performing a periodic health-check. -- `AIO_UPLOAD_MAX_SIZE`: - The maximum allowed size for the uploaded gzip archive containing the build artifacts. Files - larger than this will be rejected. +- `AIO_ARTIFACT_MAX_SIZE`: + The maximum allowed size for the gzip archive containing the build artifacts. + Files larger than this will be rejected. -- `AIO_UPLOAD_PORT`: - The port number on which the Node.js upload-server listens for HTTP connections. This is used by - nginx for delegating upload requests and also for performing a periodic health-check. +- `AIO_PREVIEW_SERVER_PORT`: + The port number on which the Node.js preview-server listens for HTTP connections. This is used by + nginx for delegating web-hook requests and also for performing a periodic health-check. diff --git a/aio/aio-builds-setup/docs/misc--debug-docker-container.md b/aio/aio-builds-setup/docs/misc--debug-docker-container.md index 4b6aa265c5..9f024bca22 100644 --- a/aio/aio-builds-setup/docs/misc--debug-docker-container.md +++ b/aio/aio-builds-setup/docs/misc--debug-docker-container.md @@ -5,8 +5,44 @@ TODO (gkalpak): Add docs. Mention: - `aio-health-check` - `aio-verify-setup` - Test nginx accessible at: - - `http://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTP` - - `https://$TEST_AIO_NGINX_HOTNAME:$TEST_AIO_NGINX_PORT_HTTPS` -- Test upload-server accessible at: - - `http://$TEST_AIO_UPLOAD_HOTNAME:$TEST_AIO_UPLOAD_PORT` + - `http://$TEST_AIO_NGINX_HOSTNAME:$TEST_AIO_NGINX_PORT_HTTP` + - `https://$TEST_AIO_NGINX_HOSTNAME:$TEST_AIO_NGINX_PORT_HTTPS` +- Test preview-server accessible at: + - `http://$TEST_AIO_PREVIEW_SERVER_HOSTNAME:$TEST_AIO_PREVIEW_SERVER_PORT` - Local DNS (via dnsmasq) maps the above hostnames to 127.0.0.1 + + +## Developing the preview server TypeScript files + +If you are running Docker on OS/X then you can benefit from linking the built TypeScript +files (i.e. `script-js/dist`) to the JavaScript files inside the Docker container. + +First start watching and building the TypeScript files (in the host): + +```bash +yarn build-watch +``` + +Now build, start and attach to the Docker container. See "Setting up the VM" +section in [TOC](_TOC.md). Then link the JavaScript folders (in the container): + +```bash +aio-dev-mode +``` + +Now whenever you make changes to the TypeScript, it will be automatically built +in the host, and the changes are automatically available in the container. +You can then run the unit tests (in the container): + +```bash +aio-verify-setup +``` + +Sometimes, the errors in the unit test log are not enough to tell you what went wrong. +In that case you can also look at the log of the preview-server itself. +A helper script that runs the unit tests (i.e. `aio-verify-setup`) and displays the +last relevant test-preview-server log is: + +```bash +aio-verify-setup-and-log +``` diff --git a/aio/aio-builds-setup/docs/misc--integrate-with-ci.md b/aio/aio-builds-setup/docs/misc--integrate-with-ci.md index 352c977876..8b98924d55 100644 --- a/aio/aio-builds-setup/docs/misc--integrate-with-ci.md +++ b/aio/aio-builds-setup/docs/misc--integrate-with-ci.md @@ -2,10 +2,8 @@ TODO (gkalpak): Add docs. Mention: -- Travis' JWT addon (+ limitations). - Relevant files: `.travis.yml`, `scripts/ci/env.sh` - 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-preview.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` diff --git a/aio/aio-builds-setup/docs/overview--general.md b/aio/aio-builds-setup/docs/overview--general.md index 0637e5235c..e6d4e376c6 100644 --- a/aio/aio-builds-setup/docs/overview--general.md +++ b/aio/aio-builds-setup/docs/overview--general.md @@ -2,9 +2,10 @@ ## Objective -Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to -a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview -the changes without having to checkout and build the app locally. +Whenever a PR job is run on the CI infrastructure (e.g. CircleCI), we want to build `angular.io` +and host the build artifacts on a publicly accessible server so that collaborators (developers, +designers, authors, etc) can preview the changes without having to checkout and build the app +locally. ## Source code @@ -32,48 +33,35 @@ This section gives a brief summary of the several operations performed on CI and container: -### On CI (Travis) -- 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). -- Optionally, the CI script can check whether the PR can be automatically verified (i.e. if the - author of the PR is a member of one of the whitelisted GitHub teams or the PR has the specified - "trusted PR" label). - **Note:** - For security reasons, the same checks will be performed on the server as well. This is an optional - step that can be used in case one wants to apply special logic depending on the outcome of the - pre-verification. For example: - 1. One might want to deploy automatically verified PRs only. In that case, the pre-verification - helps avoid the wasted overhead associated with uploads that are going to be rejected (e.g. - building the artifacts, sending them to the server, running checks on the server, detecting the - reasons of deployment failure and whether to fail the build, etc). - 2. One might want to apply additional logic (e.g. different tests) depending on whether the PR is - automatically verified or not). -- The CI script gzips and uploads the build artifacts to the server. +### On CI (CircleCI) +- 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. More info on how to set things up on CI can be found [here](misc--integrate-with-ci.md). -### Uploading build artifacts -- nginx receives the upload request. -- nginx checks that the uploaded gzip archive does not exceed the specified max file size, stores it - in a temporary location and passes the filepath to the Node.js upload-server. -- The upload-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 upload-server changes the "visibility" of the associated PR, if necessary. For example, if +### 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 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 upload-server posts a comment on the + 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 upload-server verifies that the uploaded file is not trying to overwrite an existing build. -- The upload-server deploys the artifacts to a sub-directory named after the PR number and the first - few characters of the SHA: `//` +- 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: `//` (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 upload-server posts a comment on the corresponding PR on +- If the PR is publicly accessible, the preview-server posts a comment on the corresponding PR on GitHub mentioning the SHA and the link where the preview can be found. More info on the possible HTTP status codes and their meaning can be found @@ -82,24 +70,24 @@ More info on the possible HTTP status codes and their meaning can be found ### Updating PR visibility - nginx receives a natification that a PR has been updated and passes it through to the - upload-server. This could, for example, be sent by a GitHub webhook every time a PR's labels + preview-server. This could, for example, be sent by a GitHub webhook every time a PR's labels change. E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":,"action":"labeled"}`) - The request contains the PR number (as `number`) and optionally the action that triggered the request (as `action`) in the payload. -- The upload-server verifies the payload and determines whether the `action` (if specified) could +- The preview-server verifies the payload and determines whether the `action` (if specified) could have led to PR visibility changes. Only requests that omit the `action` field altogether or specify an action that can affect visibility are further processed. (Currently, the only actions that are considered capable of affecting visibility are `labeled` and `unlabeled`.) -- The upload-server re-checks and if necessary updates the PR's visibility. +- The preview-server re-checks and if necessary updates the PR's visibility. More info on the possible HTTP status codes and their meaning can be found [here](overview--http-status-codes.md). ### Serving build artifacts -- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA. +- nginx receives a request for a hosted preview resource on a subdomain corresponding to the PR and SHA. E.g.: `pr-.ngbuilds.io/path/to/resource` - nginx maps the subdomain to the correct sub-directory and serves the resource. E.g.: `///path/to/resource` @@ -110,11 +98,11 @@ 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 The docker service runs a periodic health-check that verifies the running conditions of the container. This includes verifying the status of specific system services, the responsiveness of -nginx and the upload-server and internet connectivity. +nginx and the preview-server and internet connectivity. diff --git a/aio/aio-builds-setup/docs/overview--http-status-codes.md b/aio/aio-builds-setup/docs/overview--http-status-codes.md index 1a041ffbcf..170e66a865 100644 --- a/aio/aio-builds-setup/docs/overview--http-status-codes.md +++ b/aio/aio-builds-setup/docs/overview--http-status-codes.md @@ -1,8 +1,8 @@ # Overview - HTTP Status Codes -This is a list of all the possible HTTP status codes returned by the nginx anf upload servers, along -with a bried 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,7 +25,24 @@ with a bried explanation of what they mean: File not found. -## `https://ngbuilds.io/create-build//` +## `https://ngbuilds.io/can-have-public-preview/` + +- **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)**: Build deployed successfully and is publicly available. @@ -33,14 +50,14 @@ with a bried explanation of what they mean: - **202 (Accepted)**: Build not automatically verifiable. Stored for later deployment (after re-verification). -- **400 (Bad Request)**: - No payload. +- **204 (No Content)**: + Build was not successful, so no further action is being taken. -- **401 (Unauthorized)**: - No `AUTHORIZATION` header. +- **400 (Bad Request)**: + Invalid payload. - **403 (Forbidden)**: - Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc). + Unable to talk to 3rd-party APIs. - **405 (Method Not Allowed)**: Request method other than POST. @@ -49,9 +66,6 @@ with a bried explanation of what they mean: Request to overwrite existing (public or non-public) directory (e.g. deploy existing build or change PR visibility when the destination directory does already exist). -- **413 (Payload Too Large)**: - Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`. - ## `https://ngbuilds.io/health-check` diff --git a/aio/aio-builds-setup/docs/overview--scripts-and-commands.md b/aio/aio-builds-setup/docs/overview--scripts-and-commands.md index 4260fe3b96..84c1b683c3 100644 --- a/aio/aio-builds-setup/docs/overview--scripts-and-commands.md +++ b/aio/aio-builds-setup/docs/overview--scripts-and-commands.md @@ -21,7 +21,7 @@ available: from a git repository. See [here](vm-setup--update-docker-container.md) for more info. -## Commands +## Production Commands The following commands are available globally from inside the docker container. They are either used by the container to perform its various operations or can be used ad-hoc, mainly for testing purposes. Each command is backed by a corresponding script inside @@ -40,14 +40,27 @@ purposes. Each command is backed by a corresponding script inside Initializes the container (mainly by starting the necessary services). _It is run (by default) when starting the container._ -- `aio-upload-server-prod`: - Spins up a Node.js upload-server instance. +- `aio-preview-server-prod`: + Spins up a Node.js preview-server instance. _It is used in `aio-init` (see above) during initialization._ -- `aio-upload-server-test`: - Spins up a Node.js upload-server instance for tests. + +## Developer Commands + +- `aio-preview-server-test`: + Spins up a Node.js preview-server instance for tests. _It is used in `aio-verify-setup` (see below) for running tests._ - `aio-verify-setup`: Runs a suite of e2e-like tests, mainly verifying the correct (inter)operation of nginx and the - Node.js upload-server. + Node.js preview-server. + +- `aio-verify-setup-and-log`: + Runs the `aio-verify-setup` command but also then dumps the logs from the preview server, which + gives additional useful debugging information. See the [debugging docs](misc--debug-docker-container.md) + for more info. + +- `aio-dev-mode`: + Links external source files (from the Docker host) to interal source files (in the Docker + container). This makes it easier to use an IDE to edit files in the host that are then + tested in the container. See the [debugging docs](misc--debug-docker-container.md) for more info. \ No newline at end of file diff --git a/aio/aio-builds-setup/docs/overview--security-model.md b/aio/aio-builds-setup/docs/overview--security-model.md index c616958f4a..453ad412b9 100644 --- a/aio/aio-builds-setup/docs/overview--security-model.md +++ b/aio/aio-builds-setup/docs/overview--security-model.md @@ -1,27 +1,27 @@ # Overview - Security model -Whenever a PR job is run on Travis, we want to build `angular.io` and upload the build artifacts to +Whenever a PR job is run on CircleCI, we want to build `angular.io` and host the build artifacts on a publicly accessible server so that collaborators (developers, designers, authors, etc) can preview the changes without having to checkout and build the app locally. -This document discusses the security considerations associated with uploading build artifacts as -part of the CI setup and serving them publicly. +This document discusses the security considerations associated with moving build artifacts as +part of the CI process and serving them publicly. ## Security objectives -- **Prevent uploading arbitrary content to our servers.** - Since there is no restriction on who can submit a PR, we cannot allow any PR's build artifacts to - be uploaded. +- **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 peoples uploaded content.** - There needs to be a mechanism in place to ensure that the uploaded content does indeed correspond +- **Prevent overwriting other people's hosted build artifacts.** + There needs to be a mechanism in place to ensure that the hosted content does indeed correspond to the PR indicated by its URL. - **Prevent arbitrary access on the server.** - Since the PR author has full access over the build artifacts that would be uploaded, we must - ensure that the uploaded files will not enable arbitrary access to the server or expose sensitive + Since the PR author has full access over the build artifacts that would be hosted, we must + ensure that the build artifacts will not have arbitrary access to the server or expose sensitive info. @@ -30,7 +30,7 @@ part of the CI setup and serving them publicly. - Because the PR author can change the scripts run on CI, any security mechanisms must be immune to such changes. -- For security reasons, encrypted Travis variables are not available to PRs, so we can't rely on +- For security reasons, encrypted CircleCI variables are not available to PRs, so we can't rely on them to implement security. @@ -40,41 +40,57 @@ part of the CI setup and serving them publicly. ### In a nutshell The implemented approach can be broken up to the following sub-tasks: -1. Verify which PR the uploaded artifacts correspond to. -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 uploaded 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: -1. **Verify which PR the uploaded artifacts correspond to.** +1. **Receive notification from CircleCI of a completed build** - We are taking advantage of Travis' [JWT addon](https://docs.travis-ci.com/user/jwt). By sharing - a secret between Travis (which keeps it private but uses it to sign a JWT) and the server (which - uses it to verify the authenticity of the JWT), we can accomplish the following: - a. Verify that the upload request comes from Travis. - b. Determine the PR that these artifacts correspond to (since Travis puts that information into - the JWT, without the PR author being able to modify it). + 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. - _Note:_ - _There are currently certain limitation in the implementation of the JWT addon._ - _See the next section for more details._ +2. **Verify that the build is valid and can have a preview.** -2. **Fetch the PR's metadata, including author and labels**. + 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. - Once we have securely associated the uploaded artifacts to a PR, we retrieve the PR's metadata - + 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)? + + 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. + +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 [GitHub API](https://developer.github.com/v3/). 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: @@ -86,53 +102,48 @@ 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 uploaded 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 uploaded artifacts have been uploaded by - Travis. 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 and Travis 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 upload (currently a Node.js - Express server) rejects uploads that target an existing directory. - _Note: A PR can contain multiple uploads; one for each SHA that was built on Travis._ + 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 uploaded 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 uploaded artifacts) has been configured to not follow symlinks - outside of the directory where the build artifacts are stored. + 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. ## Assumptions / Things to keep in mind -- Each trusted PR author has full control over the content that is uploaded for their PRs. Part of - the security model relies on the trustworthiness of these authors. +- Other than the initial webhook trigger, which provides a build number, all requests for data come + from the preview-server making requests to well defined API endpoints (e.g. CircleCI and Github). + 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). -- Adding the specified label on a PR and marking it as trusted, gives the author full control over - the content that is uploaded for the specific PR (e.g. by pushing more commits to it). The user +- 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. - -- If anyone gets access to the `PREVIEW_DEPLOYMENT_TOKEN` (a.k.a. `NGBUILDS_IO_KEY` on - angular/angular) variable generated for each Travis job, they will be able to impersonate the - corresponding PR's author on the preview server for as long as the token is valid (currently 90 - mins). Because of this, the value of the `PREVIEW_DEPLOYMENT_TOKEN` should not be made publicly - accessible (e.g. by printing it on the Travis job log). - -- Travis does only allow specific whitelisted property names to be used with the JWT addon. The only - known such property at the time is `SAUCE_ACCESS_KEY` (used for integration with SauceLabs). In - order to be able to actually use the JWT addon we had to name the encrypted variable - `SAUCE_ACCESS_KEY` (which we later re-assign to `NGBUILDS_IO_KEY`). diff --git a/aio/aio-builds-setup/docs/vm-setup--create-docker-image.md b/aio/aio-builds-setup/docs/vm-setup--create-docker-image.md index 273cb7ba1c..f46a953d04 100644 --- a/aio/aio-builds-setup/docs/vm-setup--create-docker-image.md +++ b/aio/aio-builds-setup/docs/vm-setup--create-docker-image.md @@ -1,6 +1,12 @@ # VM setup - Create docker image +## Install node and yarn +- Install [nvm](https://github.com/creationix/nvm#installation). +- Install node.js: `nvm install 8` +- Install yarn: `npm -g install yarn` + + ## Checkout repository - `git clone ` @@ -21,7 +27,7 @@ The following commands would create a docker image from GitHub repo `foo/bar` to - `git clone https://github.com/foo/bar.git foobar` - Run: ``` - ./foobar/aio-builds-setup/scripts/build.sh foobar-builds \ + ./foobar/aio-builds-setup/scripts/create-image.sh foobar-builds \ --build-arg AIO_REPO_SLUG=foo/bar \ --build-arg AIO_DOMAIN_NAME=foobar-builds.io \ --build-arg AIO_GITHUB_ORGANIZATION=foo \ diff --git a/aio/aio-builds-setup/docs/vm-setup--create-host-dirs-and-files.md b/aio/aio-builds-setup/docs/vm-setup--create-host-dirs-and-files.md index 423904954b..b38a02b9ed 100644 --- a/aio/aio-builds-setup/docs/vm-setup--create-host-dirs-and-files.md +++ b/aio/aio-builds-setup/docs/vm-setup--create-host-dirs-and-files.md @@ -12,8 +12,8 @@ More info on how to create `secrets` directory and files can be found ## Create directory for build artifacts -The uploaded build artifacts should be kept on a directory outside the docker container, so it is -easier to replace the container without losing the uploaded builds. For portability across VMs a +The build artifacts should be kept on a directory outside the docker container, so it is +easier to replace the container without losing the builds. For portability across VMs a persistent disk can be used (as described [here](vm-setup--attach-persistent-disk.md)). **Note:** The directories created inside that directory will be owned by user `www-data`. @@ -21,7 +21,7 @@ persistent disk can be used (as described [here](vm-setup--attach-persistent-dis ## Create SSL certificates (Optional for dev) The host VM can attach a directory containing the SSL certificate and key to be used by the nginx -server for serving the uploaded build artifacts. More info on how to attach the directory when +server for serving the hosted previews. More info on how to attach the directory when starting the container can be found [here](vm-setup--start-docker-container.md). In order for the container to be able to find the certificate and key, they should be named @@ -61,15 +61,15 @@ The following log files are kept in this directory: used when running tests locally from inside the container, e.g. with the `aio-verify-setup` command. (See [here](overview--scripts-and-commands.md) for more info.) -- `upload-server-{prod,test,verify-setup}-*.log`: - The logs produced by the Node.js upload-server while serving either: +- `preview-server-{prod,test,verify-setup}-*.log`: + The logs produced by the Node.js preview-server while serving either: - `-prod`: "Production" files (g.g during normal operation). - - `-test`: "Test" files (e.g. when a test instance is started with the `aio-upload-server-test` + - `-test`: "Test" files (e.g. when a test instance is started with the `aio-preview-server-test` command). - `-verify-setup`: "Test" files, but while running `aio-verify-setup`. (See [here](overview--scripts-and-commands.md) for more info the commands mentioned above.) - `verify-setup.log`: - The output of the `aio-verify-setup` command (e.g. Jasmine output), except for upload-server - output which is logged to `upload-server-verify-setup-*.log` (see above). + The output of the `aio-verify-setup` command (e.g. Jasmine output), except for preview-server + output which is logged to `preview-server-verify-setup-*.log` (see above). diff --git a/aio/aio-builds-setup/docs/vm-setup--set-up-secrets.md b/aio/aio-builds-setup/docs/vm-setup--set-up-secrets.md index 8b9ab9a7c7..48e890401c 100644 --- a/aio/aio-builds-setup/docs/vm-setup--set-up-secrets.md +++ b/aio/aio-builds-setup/docs/vm-setup--set-up-secrets.md @@ -8,18 +8,14 @@ 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. -2. `PREVIEW_DEPLOYMENT_TOKEN` +2. `CIRCLE_CI_TOKEN` - Used for: - - Decoding the JWT tokens received with `/create-build` requests. - -**Note:** -`TEST_GITHUB_TOKEN` and `TEST_PREVIEW_DEPLOYMENT_TOKEN` can also be created similar to their -non-TEST counterparts and they will be loaded when running `aio-verify-setup`, but it is currently -not clear if/how they can be used in tests. + - Retrieving build information. + - Downloading build artifacts. ## Create secrets @@ -28,18 +24,9 @@ not clear if/how they can be used in tests. - Visit https://github.com/settings/tokens. - Generate new token with the `public_repo` scope. -2. `PREVIEW_DEPLOYMENT_TOKEN` - - Just generate a hard-to-guess character sequence. - - Add it to `.travis.yml` under `addons -> jwt -> secure`. - Can be added automatically with: `travis encrypt --add addons.jwt PREVIEW_DEPLOYMENT_TOKEN=` - -**Note:** -Due to [travis-ci/travis-ci#7223](https://github.com/travis-ci/travis-ci/issues/7223) it is not -currently possible to use the JWT addon (as described above) for anything other than the -`SAUCE_ACCESS_KEY` variable. You can get creative, though... - -**WARNING** -TO avoid arbitrary uploads, make sure the `PREVIEW_DEPLOYMENT_TOKEN` is NOT printed in the Travis log. +2. `CIRCLE_CI_TOKEN` + - Visit https://circleci.com/gh/angular/angular/edit#api. + - Create an API token with `Build Artifacts` scope. ## Save secrets on the VM @@ -47,6 +34,6 @@ TO avoid arbitrary uploads, make sure the `PREVIEW_DEPLOYMENT_TOKEN` is NOT prin - `sudo mkdir /aio-secrets` - `sudo touch /aio-secrets/GITHUB_TOKEN` - Insert `` into `/aio-secrets/GITHUB_TOKEN`. -- `sudo touch /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN` -- Insert `` into `/aio-secrets/PREVIEW_DEPLOYMENT_TOKEN`. +- `sudo touch /aio-secrets/CIRCLE_CI_TOKEN` +- Insert `` into `/aio-secrets/CIRCLE_CI_TOKEN`. - `sudo chmod 400 /aio-secrets/*` diff --git a/aio/aio-builds-setup/docs/vm-setup--start-docker-container.md b/aio/aio-builds-setup/docs/vm-setup--start-docker-container.md index d223e97621..cdbe53be88 100644 --- a/aio/aio-builds-setup/docs/vm-setup--start-docker-container.md +++ b/aio/aio-builds-setup/docs/vm-setup--start-docker-container.md @@ -13,14 +13,15 @@ sudo docker run \ --publish 80:80 \ --publish 443:443 \ --restart unless-stopped \ - [--volume :/etc/ssl/localcerts:ro] \ --volume :/aio-secrets:ro \ --volume :/var/www/aio-builds \ + [--volume :/etc/ssl/localcerts:ro] \ [--volume :/var/log/aio] \ + [--volume :/dockerbuild] \ [:] ``` -Below is the same command with inline comments explaining each option. The aPI docs for `docker run` +Below is the same command with inline comments explaining each option. The API docs for `docker run` can be found [here](https://docs.docker.com/engine/reference/run/). ``` @@ -30,7 +31,7 @@ sudo docker run \ --detach \ # Use the local DNS server. - # (This is necessary for mapping internal URLs, e.g. for the Node.js upload-server.) + # (This is necessary for mapping internal URLs, e.g. for the Node.js preview-server.) --dns 127.0.0.1 \ # USe `` as an alias for the container. @@ -45,28 +46,32 @@ sudo docker run \ # (This ensures that the container will be automatically started on boot.) --restart unless-stopped \ - # The directory the contains the SSL certificates. - # (See [here](vm-setup--create-host-dirs-and-files.md) for more info.) - # If not provided, the container will use self-signed certificates. - [--volume :/etc/ssl/localcerts:ro] \ - # The directory the contains the secrets (e.g. GitHub token, JWT secret, etc). # (See [here](vm-setup--set-up-secrets.md) for more info.) --volume :/aio-secrets:ro \ - # The uploaded build artifacts will stored to and served from this directory. + # The build artifacts and hosted previews will stored to and served from this directory. # (If you are using a persistent disk - as described [here](vm-setup--attach-persistent-disk.md) - # this will be a directory inside the disk.) --volume :/var/www/aio-builds \ + # The directory the contains the SSL certificates. + # (See [here](vm-setup--create-host-dirs-and-files.md) for more info.) + # If not provided, the container will use self-signed certificates. + [--volume :/etc/ssl/localcerts:ro] \ + # The directory where the logs are being kept. # (See [here](vm-setup--create-host-dirs-and-files.md) for more info.) # If not provided, the logs will be kept inside the container, which means they will be lost # whenever a new container is created. [--volume :/var/log/aio] \ + # This directory allows you to share the source scripts between the host and the container when + # debugging. (See [here](misc--debug-docker-container.md) for how to set this up.) + [--volume :/dockerbuild] \ + # The name of the docker image to use (and an optional tag; defaults to `latest`). - # (See [here](vm-setup--create-docker-image.md) for instructions on how to create the iamge.) + # (See [here](vm-setup--create-docker-image.md) for instructions on how to create the image.) [:] ``` @@ -74,7 +79,8 @@ sudo docker run \ ## Example The following command would start a docker container based on the previously created `foobar-builds` docker image, alias it as 'foobar-builds-1' and map predefined directories on the host VM to be used -by the container for accessing secrets and SSL certificates and keeping the build artifacts and logs. +by the container for accessing secrets and SSL certificates and keeping the build artifacts and logs; +and will map the source scripts from the host to the container. ``` sudo docker run \ @@ -84,9 +90,10 @@ sudo docker run \ --publish 80:80 \ --publish 443:443 \ --restart unless-stopped \ - --volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \ --volume /foobar-secrets:/aio-secrets:ro \ --volume /mnt/disks/foobar-builds:/var/www/aio-builds \ + --volume /etc/ssl/localcerts:/etc/ssl/localcerts:ro \ --volume /foobar-logs:/var/log/aio \ + --volume ~/angular/aio/aio-builds-setup/dockerbuild:/dockerbuild \ foobar-builds ``` diff --git a/aio/angular.json b/aio/angular.json index 4e80b492fa..67b7aa750d 100644 --- a/aio/angular.json +++ b/aio/angular.json @@ -33,7 +33,6 @@ "src/assets", "src/generated", "src/app/search/search-worker.js", - "src/favicon.ico", "src/pwa-manifest.json", "src/google44a5c950a9a9b940.html", "src/baidu_verify_AD8wvnfKCx.html", @@ -63,7 +62,8 @@ "src": "src/environments/environment.ts", "replaceWith": "src/environments/environment.next.ts" } - ] + ], + "serviceWorker": true }, "stable": { "fileReplacements": [ @@ -71,7 +71,8 @@ "src": "src/environments/environment.ts", "replaceWith": "src/environments/environment.stable.ts" } - ] + ], + "serviceWorker": true }, "archive": { "fileReplacements": [ @@ -79,7 +80,8 @@ "src": "src/environments/environment.ts", "replaceWith": "src/environments/environment.archive.ts" } - ] + ], + "serviceWorker": true } } }, @@ -124,7 +126,6 @@ "src/assets", "src/generated", "src/app/search/search-worker.js", - "src/favicon.ico", "src/pwa-manifest.json", "src/google44a5c950a9a9b940.html", "src/baidu_verify_AD8wvnfKCx.html", diff --git a/aio/content/cli-src/.gitignore b/aio/content/cli-src/.gitignore new file mode 100644 index 0000000000..c5245735cb --- /dev/null +++ b/aio/content/cli-src/.gitignore @@ -0,0 +1,3 @@ +/node_modules +package.json +yarn.lock diff --git a/aio/content/cli/index.md b/aio/content/cli/index.md new file mode 100644 index 0000000000..0656cb827c --- /dev/null +++ b/aio/content/cli/index.md @@ -0,0 +1,102 @@ +

CLI Command Reference

+ +The Angular CLI is a command-line interface tool that you use to initialize, develop, scaffold, and maintain Angular applications. You can use the tool directly in a command shell, or indirectly through an interactive UI such as [Angular Console](https://angularconsole.com). + +## Installing Angular CLI + +Major versions of Angular CLI follow the supported major version of Angular, but minor versions can be released separately. + +Install the CLI using the `npm` package manager: + +npm install -g @angular/cli + + +For details about changes between versions, and information about updating from previous releases, +see the Releases tab on GitHub: https://github.com/angular/angular-cli/releases + +## Basic workflow + +Invoke the tool on the command line through the `ng` executable. +Online help is available on the command line. +Enter the following to list commands or options for a given command (such as [generate](cli/generate)) with a short description. + + +ng help +ng generate --help + + +To create, build, and serve a new, basic Angular project on a development server, go to the parent directory of your new workspace use the following commands: + + +ng new my-first-project +cd my-first-project +ng serve + + +In your browser, open http://localhost:4200/ to see the new app run. + +## Workspaces and project files + +The [ng new](cli/new) command creates an *Angular workspace* folder and generates a new app skeleton. +A workspace can contain multiple apps and libraries. +The initial app created by the [ng new](cli/new) command is at the top level of the workspace. +When you generate an additional app or library in a workspace, it goes into a `projects/` subfolder. + +A newly generated app contains the source files for a root module, with a root component and template. +Each app has a `src` folder that contains the logic, data, and assets. + +You can edit the generated files directly, or add to and modify them using CLI commands. +Use the [ng generate](cli/generate) command to add new files for additional components and services, and code for new pipes, directives, and so on. +Commands such as [add](cli/add) and [generate](cli/generate), which create or operate on apps and libraries, must be executed from within a workspace or project folder. + +When you use the [ng serve](cli/serve) command to build an app and serve it locally, the server automatically rebuilds the app and reloads the page when you change any of the source files. + +* See more about the [Workspace file structure](guide/file-structure). + +When you use the [ng serve](cli/serve) command to build an app and serve it locally, the server automatically rebuilds the app and reloads the page when you change any of the source files. + +A single workspace configuration file, `angular.json`, is created at the top level of the workspace. +This is where you can set workspace-wide defaults, and specify configurations to use when the CLI builds a project for different targets. + +The [ng config](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](https://github.com/angular/angular-cli/wiki/angular-workspace) for `angular.json`. + + + +## CLI command-language syntax + +Command syntax is shown as follows: + +`ng` *commandNameOrAlias* *requiredArg* [*optionalArg*] `[options]` + +* Most commands, and some options, have aliases. Aliases are shown in the syntax statement for each command. + +* Option names are prefixed with a double dash (--). + Option aliases are prefixed with a single dash (-). + Arguments are not prefixed. + For example: `ng build my-app -c production` + +* Typically, the name of a generated artifact can be given as an argument to the command or specified with the --name option. + +* Argument and option names can be given in either +[camelCase or dash-case](guide/glossary#case-types). +`--myOptionName` is equivalent to `--my-option-name`. + +### Boolean and enumerated options + +Boolean options have two forms: `--thisOption` sets the flag, `--noThisOption` clears it. +If neither option is supplied, the flag remains in its default state, as listed in the reference documentation. + +Allowed values are given with each enumerated option description, with the default value in **bold**. + +### Relative paths + +Options that specify files can be given as absolute paths, or as paths relative to the current working directory, which is generally either the workspace or project root. + +### Schematics + +The [ng generate](cli/generate) and [ng add](cli/add) commands take as an argument the artifact or library to be generated or added to the current project. +In addition to any general options, each artifact or library defines its own options in a *schematic*. +Schematic options are supplied to the command in the same format as immediate command options. + diff --git a/aio/content/examples/animations/e2e/src/app.e2e-spec.ts b/aio/content/examples/animations/e2e/src/app.e2e-spec.ts index 524335223d..8632f944a8 100644 --- a/aio/content/examples/animations/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/animations/e2e/src/app.e2e-spec.ts @@ -1,351 +1,259 @@ 'use strict'; // necessary for es6 output in node -import { browser, element, by, ElementFinder } from 'protractor'; -import { logging, promise } from 'selenium-webdriver'; +import { browser } from 'protractor'; +import { logging } from 'selenium-webdriver'; +import * as openClose from './open-close.po'; +import * as statusSlider from './status-slider.po'; +import * as toggle from './toggle.po'; +import * as enterLeave from './enter-leave.po'; +import * as auto from './auto.po'; +import * as filterStagger from './filter-stagger.po'; +import * as heroGroups from './hero-groups'; +import { getLinkById, sleepFor } from './util'; -/** - * The tests here basically just checking that the end styles - * of each animation are in effect. - * - * Relies on the Angular testability only becoming stable once - * animation(s) have finished. - * - * Ideally we'd use https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations - * but they're not supported in Chrome at the moment. The upcoming nganimate polyfill - * may also add some introspection support. - */ describe('Animation Tests', () => { + const openCloseHref = getLinkById('open-close'); + const statusSliderHref = getLinkById('status'); + const toggleHref = getLinkById('toggle'); + const enterLeaveHref = getLinkById('enter-leave'); + const autoHref = getLinkById('auto'); + const filterHref = getLinkById('heroes'); + const heroGroupsHref = getLinkById('hero-groups'); - const INACTIVE_COLOR = 'rgba(238, 238, 238, 1)'; - const ACTIVE_COLOR = 'rgba(207, 216, 220, 1)'; - const NO_TRANSFORM_MATRIX_REGEX = /matrix\(1,\s*0,\s*0,\s*1,\s*0,\s*0\)/; - - beforeEach(() => { + beforeAll(() => { browser.get(''); }); - describe('basic states', () => { + describe('Open/Close Component', () => { - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-basic')); + beforeAll(async () => { + await openCloseHref.click(); + sleepFor(); }); - it('animates between active and inactive', () => { - addInactiveHero(); + it('should be open', async () => { + let text = await openClose.getComponentText(); + const toggleButton = openClose.getToggleButton(); + const container = openClose.getComponentContainer(); - let li = host.element(by.css('li')); + if (text.includes('Closed')) { + await toggleButton.click(); + sleepFor(); + } - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); + text = await openClose.getComponentText(); + const containerHeight = await container.getCssValue('height'); - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.1); - expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); + expect(text).toContain('The box is now Open!'); + expect(containerHeight).toBe('200px'); }); - }); + it('should be closed', async () => { + let text = await openClose.getComponentText(); + const toggleButton = openClose.getToggleButton(); + const container = openClose.getComponentContainer(); - describe('styles inline in transitions', () => { + if (text.includes('Open')) { + await toggleButton.click(); + sleepFor(); + } - let host: ElementFinder; + text = await openClose.getComponentText(); + const containerHeight = await container.getCssValue('height'); - beforeEach(function() { - host = element(by.css('app-hero-list-inline-styles')); + expect(text).toContain('The box is now Closed!'); + expect(containerHeight).toBe('100px'); }); - it('are not kept after animation', () => { - addInactiveHero(); + it('should log animation events', async () => { + const toggleButton = openClose.getToggleButton(); + const loggingCheckbox = openClose.getLoggingCheckbox(); + await loggingCheckbox.click(); + await toggleButton.click(); - let li = host.element(by.css('li')); + const logs = await browser.manage().logs().get(logging.Type.BROWSER); - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); - }); + const animationMessages = logs.filter(({ message }) => message.indexOf('Animation') !== -1 ? true : false); - }); - - describe('combined transition syntax', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-combined-transitions')); - }); - - it('animates between active and inactive', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.1); - expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); - }); - - }); - - describe('two-way transition syntax', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-twoway')); - }); - - it('animates between active and inactive', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.1); - expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); - }); - - }); - - describe('enter & leave', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-enter-leave')); - }); - - it('adds and removes element', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX); - - removeHero(); - expect(li.isPresent()).toBe(false); - }); - - }); - - describe('enter & leave & states', () => { - - let host: ElementFinder; - - beforeEach(function() { - host = element(by.css('app-hero-list-enter-leave-states')); - }); - - it('adds and removes and animates between active and inactive', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - - expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.1); - - li.click(); - browser.driver.sleep(300); - expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX); - - removeHero(); - expect(li.isPresent()).toBe(false); - }); - - }); - - describe('auto style calc', () => { - - let host: ElementFinder; - - beforeEach(function() { - host = element(by.css('app-hero-list-auto')); - }); - - it('adds and removes element', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - expect(li.getCssValue('height')).toBe('50px'); - - removeHero(); - expect(li.isPresent()).toBe(false); - }); - - }); - - describe('different timings', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-timings')); - }); - - it('adds and removes element', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX); - expect(li.getCssValue('opacity')).toMatch('1'); - - removeHero(); - expect(li.isPresent()).toBe(false); - }); - - }); - - describe('multiple keyframes', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-multistep')); - }); - - it('adds and removes element', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX); - expect(li.getCssValue('opacity')).toMatch('1'); - - removeHero(); - expect(li.isPresent()).toBe(false); - }); - - }); - - describe('parallel groups', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-groups')); - }); - - it('adds and removes element', () => { - addInactiveHero(); - - let li = host.element(by.css('li')); - expect(li.getCssValue('transform')).toMatch(NO_TRANSFORM_MATRIX_REGEX); - expect(li.getCssValue('opacity')).toMatch('1'); - - removeHero(700); - expect(li.isPresent()).toBe(false); - }); - - }); - - describe('adding active heroes', () => { - - let host: ElementFinder; - - beforeEach(() => { - host = element(by.css('app-hero-list-basic')); - }); - - it('animates between active and inactive', () => { - addActiveHero(); - - let li = host.element(by.css('li')); - - expect(getScaleX(li)).toBe(1.1); - expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.0); - expect(li.getCssValue('backgroundColor')).toBe(INACTIVE_COLOR); - - li.click(); - browser.driver.sleep(300); - expect(getScaleX(li)).toBe(1.1); - expect(li.getCssValue('backgroundColor')).toBe(ACTIVE_COLOR); + expect(animationMessages.length).toBeGreaterThan(0); }); }); - describe('callbacks', () => { - it('fires a callback on start and done', () => { - addActiveHero(); - browser.manage().logs().get(logging.Type.BROWSER) - .then((logs: logging.Entry[]) => { - const animationMessages = logs.filter((log) => { - return log.message.indexOf('Animation') !== -1 ? true : false; - }); + describe('Status Slider Component', () => { + const activeColor = 'rgba(255, 165, 0, 1)'; + const inactiveColor = 'rgba(0, 0, 255, 1)'; - expect(animationMessages.length).toBeGreaterThan(0); - }); + beforeAll(async () => { + await statusSliderHref.click(); + sleepFor(2000); + }); + + it('should be inactive with an orange background', async () => { + let text = await statusSlider.getComponentText(); + const toggleButton = statusSlider.getToggleButton(); + const container = statusSlider.getComponentContainer(); + + if (text === 'Active') { + await toggleButton.click(); + sleepFor(2000); + } + + text = await statusSlider.getComponentText(); + const bgColor = await container.getCssValue('backgroundColor'); + + expect(text).toBe('Inactive'); + expect(bgColor).toBe(inactiveColor); + }); + + it('should be active with a blue background', async () => { + let text = await statusSlider.getComponentText(); + const toggleButton = statusSlider.getToggleButton(); + const container = statusSlider.getComponentContainer(); + + if (text === 'Inactive') { + await toggleButton.click(); + sleepFor(2000); + } + + text = await statusSlider.getComponentText(); + const bgColor = await container.getCssValue('backgroundColor'); + + expect(text).toBe('Active'); + expect(bgColor).toBe(activeColor); }); }); - function addActiveHero(sleep?: number) { - sleep = sleep || 500; - element(by.buttonText('Add active hero')).click(); - browser.driver.sleep(sleep); - } - - function addInactiveHero(sleep?: number) { - sleep = sleep || 500; - element(by.buttonText('Add inactive hero')).click(); - browser.driver.sleep(sleep); - } - - function removeHero(sleep?: number) { - sleep = sleep || 500; - element(by.buttonText('Remove hero')).click(); - browser.driver.sleep(sleep); - } - - function getScaleX(el: ElementFinder) { - return Promise.all([ - getBoundingClientWidth(el), - getOffsetWidth(el) - ]).then(function(promiseResolutions) { - let clientWidth = promiseResolutions[0]; - let offsetWidth = promiseResolutions[1]; - return clientWidth / offsetWidth; + describe('Toggle Animations Component', () => { + beforeAll(async () => { + await toggleHref.click(); + sleepFor(); }); - } - function getBoundingClientWidth(el: ElementFinder) { - return browser.executeScript( - 'return arguments[0].getBoundingClientRect().width', - el.getWebElement() - ) as PromiseLike; - } + it('should disabled animations on the child element', async () => { + const toggleButton = toggle.getToggleAnimationsButton(); - function getOffsetWidth(el: ElementFinder) { - return browser.executeScript( - 'return arguments[0].offsetWidth', - el.getWebElement() - ) as PromiseLike; - } + await toggleButton.click(); + + const container = toggle.getComponentContainer(); + const cssClasses = await container.getAttribute('class'); + + expect(cssClasses).toContain('ng-animate-disabled'); + }); + }); + + describe('Enter/Leave Component', () => { + beforeAll(async () => { + await enterLeaveHref.click(); + sleepFor(100); + }); + + it('should attach a flyInOut trigger to the list of items', async () => { + const heroesList = enterLeave.getHeroesList(); + const hero = heroesList.get(0); + const cssClasses = await hero.getAttribute('class'); + const transform = await hero.getCssValue('transform'); + + expect(cssClasses).toContain('ng-trigger-flyInOut'); + expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)'); + }); + + it('should remove the hero from the list when clicked', async () => { + const heroesList = enterLeave.getHeroesList(); + const total = await heroesList.count(); + const hero = heroesList.get(0); + + await hero.click(); + await sleepFor(100); + const newTotal = await heroesList.count(); + + expect(newTotal).toBeLessThan(total); + }); + }); + + describe('Auto Calculation Component', () => { + beforeAll(async () => { + await autoHref.click(); + sleepFor(0); + }); + + it('should attach a shrinkOut trigger to the list of items', async () => { + const heroesList = auto.getHeroesList(); + const hero = heroesList.get(0); + const cssClasses = await hero.getAttribute('class'); + + expect(cssClasses).toContain('ng-trigger-shrinkOut'); + }); + + it('should remove the hero from the list when clicked', async () => { + const heroesList = auto.getHeroesList(); + const total = await heroesList.count(); + const hero = heroesList.get(0); + + await hero.click(); + await sleepFor(250); + const newTotal = await heroesList.count(); + + expect(newTotal).toBeLessThan(total); + }); + }); + + describe('Filter/Stagger Component', () => { + beforeAll(async () => { + await filterHref.click(); + sleepFor(); + }); + + it('should attach a filterAnimations trigger to the list container', async () => { + const heroesList = filterStagger.getComponentContainer(); + const cssClasses = await heroesList.getAttribute('class'); + + expect(cssClasses).toContain('ng-trigger-filterAnimation'); + }); + + it('should filter down the list when a search is performed', async () => { + const heroesList = filterStagger.getHeroesList(); + const total = await heroesList.count(); + const formInput = filterStagger.getFormInput(); + + await formInput.sendKeys('Mag'); + await sleepFor(500); + const newTotal = await heroesList.count(); + + expect(newTotal).toBeLessThan(total); + expect(newTotal).toBe(2); + }); + }); + + describe('Hero Groups Component', () => { + beforeAll(async () => { + await heroGroupsHref.click(); + sleepFor(300); + }); + + it('should attach a flyInOut trigger to the list of items', async () => { + const heroesList = heroGroups.getHeroesList(); + const hero = heroesList.get(0); + const cssClasses = await hero.getAttribute('class'); + const transform = await hero.getCssValue('transform'); + const opacity = await hero.getCssValue('opacity'); + + expect(cssClasses).toContain('ng-trigger-flyInOut'); + expect(transform).toBe('matrix(1, 0, 0, 1, 0, 0)'); + expect(opacity).toBe('1'); + }); + + it('should remove the hero from the list when clicked', async () => { + const heroesList = heroGroups.getHeroesList(); + const total = await heroesList.count(); + const hero = heroesList.get(0); + + await hero.click(); + await sleepFor(300); + const newTotal = await heroesList.count(); + + expect(newTotal).toBeLessThan(total); + }); + }); }); + + diff --git a/aio/content/examples/animations/e2e/src/auto.po.ts b/aio/content/examples/animations/e2e/src/auto.po.ts new file mode 100644 index 0000000000..fbaa38e85b --- /dev/null +++ b/aio/content/examples/animations/e2e/src/auto.po.ts @@ -0,0 +1,19 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-hero-list-auto-page'); +} + +export function getComponent() { + return by.css('app-hero-list-auto'); +} + +export function getComponentContainer() { + const findContainer = () => by.css('ul'); + return locate(getComponent(), findContainer()); +} + +export function getHeroesList() { + return getComponentContainer().all(by.css('li')); +} diff --git a/aio/content/examples/animations/e2e/src/enter-leave.po.ts b/aio/content/examples/animations/e2e/src/enter-leave.po.ts new file mode 100644 index 0000000000..62ac5858c2 --- /dev/null +++ b/aio/content/examples/animations/e2e/src/enter-leave.po.ts @@ -0,0 +1,19 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-hero-list-enter-leave-page'); +} + +export function getComponent() { + return by.css('app-hero-list-enter-leave'); +} + +export function getComponentContainer() { + const findContainer = () => by.css('ul'); + return locate(getComponent(), findContainer()); +} + +export function getHeroesList() { + return getComponentContainer().all(by.css('li')); +} diff --git a/aio/content/examples/animations/e2e/src/filter-stagger.po.ts b/aio/content/examples/animations/e2e/src/filter-stagger.po.ts new file mode 100644 index 0000000000..235b95483a --- /dev/null +++ b/aio/content/examples/animations/e2e/src/filter-stagger.po.ts @@ -0,0 +1,20 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-hero-list-page'); +} + +export function getComponentContainer() { + const findContainer = () => by.css('ul'); + return locate(getPage(), findContainer()); +} + +export function getHeroesList() { + return getComponentContainer().all(by.css('li')); +} + +export function getFormInput() { + const formInput = () => by.css('form > input'); + return locate(getPage(), formInput()); +} diff --git a/aio/content/examples/animations/e2e/src/hero-groups.ts b/aio/content/examples/animations/e2e/src/hero-groups.ts new file mode 100644 index 0000000000..69700adcc6 --- /dev/null +++ b/aio/content/examples/animations/e2e/src/hero-groups.ts @@ -0,0 +1,19 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-hero-list-groups-page'); +} + +export function getComponent() { + return by.css('app-hero-list-groups'); +} + +export function getComponentContainer() { + const findContainer = () => by.css('ul'); + return locate(getComponent(), findContainer()); +} + +export function getHeroesList() { + return getComponentContainer().all(by.css('li')); +} diff --git a/aio/content/examples/animations/e2e/src/open-close.po.ts b/aio/content/examples/animations/e2e/src/open-close.po.ts new file mode 100644 index 0000000000..cc34558db6 --- /dev/null +++ b/aio/content/examples/animations/e2e/src/open-close.po.ts @@ -0,0 +1,33 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-open-close-page'); +} + +export function getComponent() { + return by.css('app-open-close'); +} + +export function getToggleButton() { + const toggleButton = () => by.buttonText('Toggle Open/Close'); + return locate(getComponent(), toggleButton()); +} + +export function getLoggingCheckbox() { + const loggingCheckbox = () => by.css('section > input[type="checkbox"]'); + return locate(getPage(), loggingCheckbox()); +} + +export function getComponentContainer() { + const findContainer = () => by.css('div'); + return locate(getComponent(), findContainer()); +} + +export async function getComponentText() { + const findContainerText = () => by.css('div'); + const contents = locate(getComponent(), findContainerText()); + const componentText = await contents.getText(); + + return componentText; +} diff --git a/aio/content/examples/animations/e2e/src/status-slider.po.ts b/aio/content/examples/animations/e2e/src/status-slider.po.ts new file mode 100644 index 0000000000..81d0b285cd --- /dev/null +++ b/aio/content/examples/animations/e2e/src/status-slider.po.ts @@ -0,0 +1,28 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-status-slider-page'); +} + +export function getComponent() { + return by.css('app-status-slider'); +} + +export function getToggleButton() { + const toggleButton = () => by.buttonText('Toggle Status'); + return locate(getComponent(), toggleButton()); +} + +export function getComponentContainer() { + const findContainer = () => by.css('div'); + return locate(getComponent(), findContainer()); +} + +export async function getComponentText() { + const findContainerText = () => by.css('div'); + const contents = locate(getComponent(), findContainerText()); + const componentText = await contents.getText(); + + return componentText; +} diff --git a/aio/content/examples/animations/e2e/src/toggle.po.ts b/aio/content/examples/animations/e2e/src/toggle.po.ts new file mode 100644 index 0000000000..28575a55a2 --- /dev/null +++ b/aio/content/examples/animations/e2e/src/toggle.po.ts @@ -0,0 +1,25 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getPage() { + return by.css('app-toggle-animations-child-page'); +} + +export function getComponent() { + return by.css('app-open-close-toggle'); +} + +export function getToggleButton() { + const toggleButton = () => by.buttonText('Toggle Open/Closed'); + return locate(getComponent(), toggleButton()); +} + +export function getToggleAnimationsButton() { + const toggleAnimationsButton = () => by.buttonText('Toggle Animations'); + return locate(getComponent(), toggleAnimationsButton()); +} + +export function getComponentContainer() { + const findContainer = () => by.css('div'); + return locate(getComponent()).all(findContainer()).get(0); +} diff --git a/aio/content/examples/animations/e2e/src/util.ts b/aio/content/examples/animations/e2e/src/util.ts new file mode 100644 index 0000000000..b6c0ef1be7 --- /dev/null +++ b/aio/content/examples/animations/e2e/src/util.ts @@ -0,0 +1,19 @@ +import { Locator, ElementFinder, browser, by, element } from 'protractor'; + +/** + * + * locate(finder1, finder2) => element(finder1).element(finder2).element(finderN); + */ +export function locate(locator: Locator, ...locators: Locator[]) { + return locators.reduce((current: ElementFinder, next: Locator) => { + return current.element(next); + }, element(locator)) as ElementFinder; +} + +export async function sleepFor(time = 1000) { + return await browser.sleep(time); +} + +export function getLinkById(id: string) { + return element(by.css(`a[id=${id}]`)); +} diff --git a/aio/content/examples/animations/src/app/about.component.css b/aio/content/examples/animations/src/app/about.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/animations/src/app/about.component.html b/aio/content/examples/animations/src/app/about.component.html new file mode 100644 index 0000000000..6ef2412703 --- /dev/null +++ b/aio/content/examples/animations/src/app/about.component.html @@ -0,0 +1,3 @@ +

+ Angular's animations library makes it easy to define and apply animation effects such as page and list transitions. +

diff --git a/aio/content/examples/animations/src/app/about.component.ts b/aio/content/examples/animations/src/app/about.component.ts new file mode 100644 index 0000000000..d8a4231199 --- /dev/null +++ b/aio/content/examples/animations/src/app/about.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-about', + templateUrl: './about.component.html', + styleUrls: ['./about.component.css'] +}) +export class AboutComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/aio/content/examples/animations/src/app/animations.1.ts b/aio/content/examples/animations/src/app/animations.1.ts new file mode 100644 index 0000000000..ae7558f53d --- /dev/null +++ b/aio/content/examples/animations/src/app/animations.1.ts @@ -0,0 +1,11 @@ +// #docregion +import { animation, style, animate } from '@angular/animations'; + +export const transAnimation = animation([ + style({ + height: '{{ height }}', + opacity: '{{ opacity }}', + backgroundColor: '{{ backgroundColor }}' + }), + animate('{{ time }}') +]); diff --git a/aio/content/examples/animations/src/app/animations.ts b/aio/content/examples/animations/src/app/animations.ts new file mode 100644 index 0000000000..d525647354 --- /dev/null +++ b/aio/content/examples/animations/src/app/animations.ts @@ -0,0 +1,74 @@ +// #docregion reusable +import { + animation, trigger, animateChild, group, + transition, animate, style, query +} from '@angular/animations'; + +export const transAnimation = animation([ + style({ + height: '{{ height }}', + opacity: '{{ opacity }}', + backgroundColor: '{{ backgroundColor }}' + }), + animate('{{ time }}') +]); +// #enddocregion reusable + +// Routable animations +// #docregion route-animations +export const slideInAnimation = +// #docregion style-view + trigger('routeAnimations', [ + transition('HomePage <=> AboutPage', [ + style({ position: 'relative' }), + query(':enter, :leave', [ + style({ + position: 'absolute', + top: 0, + left: 0, + width: '100%' + }) + ]), +// #enddocregion style-view +// #docregion query + 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()), + ]), + transition('* <=> FilterPage', [ + 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('200ms ease-out', style({ left: '100%'})) + ]), + query(':enter', [ + animate('300ms ease-out', style({ left: '0%'})) + ]) + ]), + query(':enter', animateChild()), + ]) + // #enddocregion query + ]); + // #enddocregion route-animations diff --git a/aio/content/examples/animations/src/app/app.component.1.ts b/aio/content/examples/animations/src/app/app.component.1.ts new file mode 100644 index 0000000000..39e2022dad --- /dev/null +++ b/aio/content/examples/animations/src/app/app.component.1.ts @@ -0,0 +1,35 @@ +// #docplaster +// #docregion imports +import { Component, HostBinding } from '@angular/core'; +import { + trigger, + state, + style, + animate, + transition, + // ... +} from '@angular/animations'; + +// #enddocregion imports + +// #docregion decorator, toggle-app-animations +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'], + animations: [ + // animation triggers go here + ] +}) +// #enddocregion decorator +export class AppComponent { + @HostBinding('@.disabled') + public animationsDisabled = false; +// #enddocregion toggle-app-animations + + toggleAnimations() { + this.animationsDisabled = !this.animationsDisabled; + } +// #docregion toggle-app-animations +} +// #enddocregion toggle-app-animations diff --git a/aio/content/examples/animations/src/app/app.component.css b/aio/content/examples/animations/src/app/app.component.css new file mode 100644 index 0000000000..04d8866ac2 --- /dev/null +++ b/aio/content/examples/animations/src/app/app.component.css @@ -0,0 +1,7 @@ +:host { + display: block; +} + +section { + margin-top: 100px; +} diff --git a/aio/content/examples/animations/src/app/app.component.html b/aio/content/examples/animations/src/app/app.component.html new file mode 100644 index 0000000000..c2d1188f70 --- /dev/null +++ b/aio/content/examples/animations/src/app/app.component.html @@ -0,0 +1,21 @@ +

Animations

+ +Toggle All Animations + + + + +
+ +
+ \ No newline at end of file diff --git a/aio/content/examples/animations/src/app/app.component.ts b/aio/content/examples/animations/src/app/app.component.ts new file mode 100644 index 0000000000..4a0b559939 --- /dev/null +++ b/aio/content/examples/animations/src/app/app.component.ts @@ -0,0 +1,47 @@ +// #docplaster +// #docregion imports +import { Component, HostBinding } from '@angular/core'; +import { + trigger, + state, + style, + animate, + transition, + // ... +} from '@angular/animations'; + +// #enddocregion imports +import { RouterOutlet } from '@angular/router'; +import { slideInAnimation } from './animations'; + +// #docregion decorator, toggle-app-animations, define +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'], + animations: [ +// #enddocregion decorator + slideInAnimation +// #docregion decorator + // animation triggers go here + ] +}) +// #enddocregion decorator, define +export class AppComponent { + @HostBinding('@.disabled') + public animationsDisabled = false; +// #enddocregion toggle-app-animations + +// #docregion prepare-router-outlet + prepareRoute(outlet: RouterOutlet) { + return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']; + } + +// #enddocregion prepare-router-outlet + + toggleAnimations() { + this.animationsDisabled = !this.animationsDisabled; + } +// #docregion toggle-app-animations +} +// #enddocregion toggle-app-animations diff --git a/aio/content/examples/animations/src/app/app.module.1.ts b/aio/content/examples/animations/src/app/app.module.1.ts new file mode 100644 index 0000000000..d2ba898f78 --- /dev/null +++ b/aio/content/examples/animations/src/app/app.module.1.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +@NgModule({ + imports: [ + BrowserModule, + BrowserAnimationsModule + ], + declarations: [ ], + bootstrap: [ ] +}) +export class AppModule { } diff --git a/aio/content/examples/animations/src/app/app.module.ts b/aio/content/examples/animations/src/app/app.module.ts index b1ac87cca2..8109d6269b 100644 --- a/aio/content/examples/animations/src/app/app.module.ts +++ b/aio/content/examples/animations/src/app/app.module.ts @@ -1,43 +1,63 @@ -// #docplaster +// #docregion route-animation-data import { NgModule } from '@angular/core'; -// #docregion animations-module import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -// #enddocregion animations-module - -import { HeroTeamBuilderComponent } from './hero-team-builder.component'; -import { HeroListBasicComponent } from './hero-list-basic.component'; -import { HeroListInlineStylesComponent } from './hero-list-inline-styles.component'; -import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component'; -import { HeroListEnterLeaveStatesComponent } from './hero-list-enter-leave-states.component'; -import { HeroListCombinedTransitionsComponent } from './hero-list-combined-transitions.component'; -import { HeroListTwowayComponent } from './hero-list-twoway.component'; -import { HeroListAutoComponent } from './hero-list-auto.component'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; +import { OpenCloseComponent } from './open-close.component'; +import { OpenClosePageComponent } from './open-close-page.component'; +import { OpenCloseChildComponent } from './open-close.component.4'; +import { ToggleAnimationsPageComponent } from './toggle-animations-page.component'; +import { StatusSliderComponent } from './status-slider.component'; +import { StatusSliderPageComponent } from './status-slider-page.component'; +import { HeroListPageComponent } from './hero-list-page.component'; +import { HeroListGroupPageComponent } from './hero-list-group-page.component'; import { HeroListGroupsComponent } from './hero-list-groups.component'; -import { HeroListMultistepComponent } from './hero-list-multistep.component'; -import { HeroListTimingsComponent } from './hero-list-timings.component'; -// #docregion animations-module +import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component'; +import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component'; +import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component'; +import { HeroListAutoComponent } from './hero-list-auto.component'; +import { HomeComponent } from './home.component'; +import { AboutComponent } from './about.component'; + @NgModule({ - imports: [ BrowserModule, BrowserAnimationsModule ], - // ... more stuff ... -// #enddocregion animations-module - declarations: [ - HeroTeamBuilderComponent, - HeroListBasicComponent, - HeroListInlineStylesComponent, - HeroListCombinedTransitionsComponent, - HeroListTwowayComponent, - HeroListEnterLeaveComponent, - HeroListEnterLeaveStatesComponent, - HeroListAutoComponent, - HeroListTimingsComponent, - HeroListMultistepComponent, - HeroListGroupsComponent + imports: [ + BrowserModule, + BrowserAnimationsModule, + RouterModule.forRoot([ + { path: '', pathMatch: 'full', redirectTo: '/enter-leave' }, + { path: 'open-close', component: OpenClosePageComponent }, + { path: 'status', component: StatusSliderPageComponent }, + { path: 'toggle', component: ToggleAnimationsPageComponent }, + { path: 'heroes', component: HeroListPageComponent, data: {animation: 'FilterPage'} }, + { path: 'hero-groups', component: HeroListGroupPageComponent }, + { path: 'enter-leave', component: HeroListEnterLeavePageComponent }, + { path: 'auto', component: HeroListAutoCalcPageComponent }, + { path: 'home', component: HomeComponent, data: {animation: 'HomePage'} }, + { path: 'about', component: AboutComponent, data: {animation: 'AboutPage'} }, + + ]) ], - bootstrap: [ HeroTeamBuilderComponent ] -// #docregion animations-module + // #enddocregion route-animation-data + declarations: [ + AppComponent, + StatusSliderComponent, + OpenCloseComponent, + OpenCloseChildComponent, + OpenClosePageComponent, + StatusSliderPageComponent, + ToggleAnimationsPageComponent, + HeroListPageComponent, + HeroListGroupsComponent, + HeroListGroupPageComponent, + HeroListEnterLeavePageComponent, + HeroListEnterLeaveComponent, + HeroListAutoCalcPageComponent, + HeroListAutoComponent, + HomeComponent, + AboutComponent + ], + bootstrap: [AppComponent] }) export class AppModule { } -// #enddocregion animations-module - diff --git a/aio/content/examples/animations/src/app/hero-list-auto-page.component.ts b/aio/content/examples/animations/src/app/hero-list-auto-page.component.ts new file mode 100644 index 0000000000..78f17da64b --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-auto-page.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { HEROES } from './mock-heroes'; + +@Component({ + selector: 'app-hero-list-auto-page', + template: ` +
+

Automatic Calculation

+ + +
+ ` +}) +export class HeroListAutoCalcPageComponent { + heroes = HEROES.slice(); + + onRemove(id: number) { + this.heroes = this.heroes.filter(hero => hero.id !== id); + } +} diff --git a/aio/content/examples/animations/src/app/hero-list-auto.component.html b/aio/content/examples/animations/src/app/hero-list-auto.component.html new file mode 100644 index 0000000000..bb99794f0c --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-auto.component.html @@ -0,0 +1,9 @@ +
    +
  • +
    + {{ hero.id }} + {{ hero.name }} +
    +
  • +
\ No newline at end of file diff --git a/aio/content/examples/animations/src/app/hero-list-auto.component.ts b/aio/content/examples/animations/src/app/hero-list-auto.component.ts index 97a5ff99de..a4489bafd0 100644 --- a/aio/content/examples/animations/src/app/hero-list-auto.component.ts +++ b/aio/content/examples/animations/src/app/hero-list-auto.component.ts @@ -1,6 +1,8 @@ import { Component, - Input + Input, + Output, + EventEmitter } from '@angular/core'; import { trigger, @@ -10,38 +12,30 @@ import { transition } from '@angular/animations'; -import { Hero } from './hero.service'; +import { Hero } from './hero'; @Component({ selector: 'app-hero-list-auto', - // #docregion template - template: ` -
    -
  • - {{hero.name}} -
  • -
- `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - - /* When the element leaves (transition "in => void" occurs), - * get the element's current computed height and animate - * it down to 0. - */ - // #docregion animationdef + templateUrl: 'hero-list-auto.component.html', + styleUrls: ['./hero-list-page.component.css'], + // #docregion auto-calc animations: [ trigger('shrinkOut', [ - state('in', style({height: '*'})), + state('in', style({ height: '*' })), transition('* => void', [ - style({height: '*'}), - animate(250, style({height: 0})) + style({ height: '*' }), + animate(250, style({ height: 0 })) ]) ]) ] - // #enddocregion animationdef + // #enddocregion auto-calc }) export class HeroListAutoComponent { @Input() heroes: Hero[]; + + @Output() remove = new EventEmitter(); + + removeHero(id: number) { + this.remove.emit(id); + } } diff --git a/aio/content/examples/animations/src/app/hero-list-basic.component.ts b/aio/content/examples/animations/src/app/hero-list-basic.component.ts deleted file mode 100644 index ce23628d6f..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-basic.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -// #docplaster -// #docregion -// #docregion imports -import { - Component, - Input -} from '@angular/core'; -import { - trigger, - state, - style, - animate, - transition -} from '@angular/animations'; -// #enddocregion imports - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-basic', - // #enddocregion - /* The click event calls hero.toggleState(), which - * causes the state of that hero to switch from - * active to inactive or vice versa. - */ - // #docregion - // #docregion template - template: ` -
    -
  • - {{hero.name}} -
  • -
- `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - // #enddocregion - /** - * Define two states, "inactive" and "active", and the end - * styles that apply whenever the element is in those states. - * Then define animations for transitioning between the states, - * one in each direction - */ - // #docregion - // #docregion animationdef - animations: [ - trigger('heroState', [ - // #docregion states - state('inactive', style({ - backgroundColor: '#eee', - transform: 'scale(1)' - })), - state('active', style({ - backgroundColor: '#cfd8dc', - transform: 'scale(1.1)' - })), - // #enddocregion states - // #docregion transitions - transition('inactive => active', animate('100ms ease-in')), - transition('active => inactive', animate('100ms ease-out')) - // #enddocregion transitions - ]) - ] - // #enddocregion animationdef -}) -export class HeroListBasicComponent { - @Input() heroes: Hero[]; -} diff --git a/aio/content/examples/animations/src/app/hero-list-combined-transitions.component.ts b/aio/content/examples/animations/src/app/hero-list-combined-transitions.component.ts deleted file mode 100644 index 2811254c81..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-combined-transitions.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -// #docregion -// #docregion imports -import { - Component, - Input -} from '@angular/core'; -import { - trigger, - state, - style, - animate, - transition -} from '@angular/animations'; -// #enddocregion imports - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-combined-transitions', - // #docregion template - template: ` -
    -
  • - {{hero.name}} -
  • -
- `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - /* - * Define two states, "inactive" and "active", and the end - * styles that apply whenever the element is in those states. - * Then define an animated transition between these two - * states, in *both* directions. - */ - // #docregion animationdef - animations: [ - trigger('heroState', [ - state('inactive', style({ - backgroundColor: '#eee', - transform: 'scale(1)' - })), - state('active', style({ - backgroundColor: '#cfd8dc', - transform: 'scale(1.1)' - })), - // #docregion transitions - transition('inactive => active, active => inactive', - animate('100ms ease-out')) - // #enddocregion transitions - ]) - ] - // #enddocregion animationdef -}) -export class HeroListCombinedTransitionsComponent { - @Input() heroes: Hero[]; -} diff --git a/aio/content/examples/animations/src/app/hero-list-enter-leave-page.component.ts b/aio/content/examples/animations/src/app/hero-list-enter-leave-page.component.ts new file mode 100644 index 0000000000..ea81624527 --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-enter-leave-page.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { HEROES } from './mock-heroes'; + +@Component({ + selector: 'app-hero-list-enter-leave-page', + template: ` +
+

Enter/Leave

+ + +
+ ` +}) +export class HeroListEnterLeavePageComponent { + heroes = HEROES.slice(); + + onRemove(id: number) { + this.heroes = this.heroes.filter(hero => hero.id !== id); + } +} diff --git a/aio/content/examples/animations/src/app/hero-list-enter-leave-states.component.ts b/aio/content/examples/animations/src/app/hero-list-enter-leave-states.component.ts deleted file mode 100644 index 291234dab9..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-enter-leave-states.component.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - Component, - Input -} from '@angular/core'; -import { - trigger, - state, - style, - animate, - transition -} from '@angular/animations'; - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-enter-leave-states', - // #docregion template - template: ` -
    -
  • - {{hero.name}} -
  • -
- `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - /* The elements here have two possible states based - * on the hero state, "active", or "inactive". We animate - * six transitions: Between the two states in both directions, - * and between each state and void. With this we can animate - * the enter and leave of elements differently based on which - * state they are in when they are added and removed. - */ - // #docregion animationdef - animations: [ - trigger('heroState', [ - state('inactive', style({transform: 'translateX(0) scale(1)'})), - state('active', style({transform: 'translateX(0) scale(1.1)'})), - transition('inactive => active', animate('100ms ease-in')), - transition('active => inactive', animate('100ms ease-out')), - transition('void => inactive', [ - style({transform: 'translateX(-100%) scale(1)'}), - animate(100) - ]), - transition('inactive => void', [ - animate(100, style({transform: 'translateX(100%) scale(1)'})) - ]), - transition('void => active', [ - style({transform: 'translateX(0) scale(0)'}), - animate(200) - ]), - transition('active => void', [ - animate(200, style({transform: 'translateX(0) scale(0)'})) - ]) - ]) - ] - // #enddocregion animationdef -}) -export class HeroListEnterLeaveStatesComponent { - @Input() heroes: Hero[]; -} diff --git a/aio/content/examples/animations/src/app/hero-list-enter-leave.component.ts b/aio/content/examples/animations/src/app/hero-list-enter-leave.component.ts index 0a888698c3..164adb8ab9 100644 --- a/aio/content/examples/animations/src/app/hero-list-enter-leave.component.ts +++ b/aio/content/examples/animations/src/app/hero-list-enter-leave.component.ts @@ -1,6 +1,8 @@ import { Component, - Input + Input, + Output, + EventEmitter } from '@angular/core'; import { trigger, @@ -10,42 +12,45 @@ import { transition } from '@angular/animations'; -import { Hero } from './hero.service'; +import { Hero } from './hero'; @Component({ selector: 'app-hero-list-enter-leave', // #docregion template template: ` -
    +
    • - {{hero.name}} + [@flyInOut]="'in'" (click)="removeHero(hero.id)"> +
      + {{ hero.id }} + {{ hero.name }} +
    `, // #enddocregion template - styleUrls: ['./hero-list.component.css'], - /* The element here always has the state "in" when it - * is present. We animate two transitions: From void - * to in and from in to void, to achieve an animated - * enter and leave transition. The element enters from - * the left and leaves to the right using translateX. - */ + styleUrls: ['./hero-list-page.component.css'], // #docregion animationdef animations: [ trigger('flyInOut', [ - state('in', style({transform: 'translateX(0)'})), + state('in', style({ transform: 'translateX(0)' })), transition('void => *', [ - style({transform: 'translateX(-100%)'}), + style({ transform: 'translateX(-100%)' }), animate(100) ]), transition('* => void', [ - animate(100, style({transform: 'translateX(100%)'})) + animate(100, style({ transform: 'translateX(100%)' })) ]) ]) ] // #enddocregion animationdef }) export class HeroListEnterLeaveComponent { - @Input() heroes: Hero[]; + @Input() heroes: Hero[]; + + @Output() remove = new EventEmitter(); + + removeHero(id: number) { + this.remove.emit(id); + } } diff --git a/aio/content/examples/animations/src/app/hero-list-group-page.component.ts b/aio/content/examples/animations/src/app/hero-list-group-page.component.ts new file mode 100644 index 0000000000..b400126ff0 --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-group-page.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { HEROES } from './mock-heroes'; + +@Component({ + selector: 'app-hero-list-groups-page', + template: ` +
    +

    Hero List Group

    + + +
    + ` +}) +export class HeroListGroupPageComponent { + heroes = HEROES.slice(); + + onRemove(id: number) { + this.heroes = this.heroes.filter(hero => hero.id !== id); + } +} diff --git a/aio/content/examples/animations/src/app/hero-list-groups.component.ts b/aio/content/examples/animations/src/app/hero-list-groups.component.ts index d17b47d213..2893b91e81 100644 --- a/aio/content/examples/animations/src/app/hero-list-groups.component.ts +++ b/aio/content/examples/animations/src/app/hero-list-groups.component.ts @@ -1,6 +1,8 @@ import { Component, - Input + Input, + Output, + EventEmitter } from '@angular/core'; import { trigger, @@ -11,45 +13,31 @@ import { group } from '@angular/animations'; -import { Hero } from './hero.service'; +import { Hero } from './hero'; @Component({ selector: 'app-hero-list-groups', template: ` -
      +
      • - {{hero.name}} + [@flyInOut]="'in'" (click)="removeHero(hero.id)"> +
        + {{ hero.id }} + {{ hero.name }} +
      `, - styleUrls: ['./hero-list.component.css'], - styles: [` - li { - padding: 0 !important; - text-align: center; - } - `], - /* The element here always has the state "in" when it - * is present. We animate two transitions: From void - * to in and from in to void, to achieve an animated - * enter and leave transition. - * - * The transitions have *parallel group* that allow - * animating several properties at the same time but - * with different timing configurations. On enter - * (void => *) we start the opacity animation 0.1s - * earlier than the translation/width animation. - * On leave (* => void) we do the opposite - - * the translation/width animation begins immediately - * and the opacity animation 0.1s later. - */ + styleUrls: ['./hero-list-page.component.css'], // #docregion animationdef animations: [ trigger('flyInOut', [ - state('in', style({width: 120, transform: 'translateX(0)', opacity: 1})), + state('in', style({ + width: 120, + transform: 'translateX(0)', opacity: 1 + })), transition('void => *', [ - style({width: 10, transform: 'translateX(50px)', opacity: 0}), + style({ width: 10, transform: 'translateX(50px)', opacity: 0 }), group([ animate('0.3s 0.1s ease', style({ transform: 'translateX(0)', @@ -77,4 +65,10 @@ import { Hero } from './hero.service'; }) export class HeroListGroupsComponent { @Input() heroes: Hero[]; + + @Output() remove = new EventEmitter(); + + removeHero(id: number) { + this.remove.emit(id); + } } diff --git a/aio/content/examples/animations/src/app/hero-list-inline-styles.component.ts b/aio/content/examples/animations/src/app/hero-list-inline-styles.component.ts deleted file mode 100644 index 43fe2b7ce3..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-inline-styles.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -// #docregion -// #docregion imports -import { - Component, - Input, -} from '@angular/core'; -import { - trigger, - style, - animate, - transition -} from '@angular/animations'; -// #enddocregion imports - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-inline-styles', - // #docregion template - template: ` -
        -
      • - {{hero.name}} -
      • -
      - `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - /** - * Define two states, "inactive" and "active", and the end - * styles that apply whenever the element is in those states. - * Then define an animation for the inactive => active transition. - * This animation has no end styles, but only styles that are - * defined inline inside the transition and thus are only kept - * as long as the animation is running. - */ - // #docregion animationdef - animations: [ - trigger('heroState', [ - // #docregion transitions - transition('inactive => active', [ - style({ - backgroundColor: '#cfd8dc', - transform: 'scale(1.3)' - }), - animate('80ms ease-in', style({ - backgroundColor: '#eee', - transform: 'scale(1)' - })) - ]), - // #enddocregion transitions - ]) - ] - // #enddocregion animationdef -}) -export class HeroListInlineStylesComponent { - @Input() heroes: Hero[]; -} diff --git a/aio/content/examples/animations/src/app/hero-list-multistep.component.ts b/aio/content/examples/animations/src/app/hero-list-multistep.component.ts deleted file mode 100644 index 7e77bff1a4..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-multistep.component.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - Component, - Input, -} from '@angular/core'; -import { - trigger, - state, - style, - animate, - transition, - keyframes, - AnimationEvent -} from '@angular/animations'; - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-multistep', - // #docregion template - template: ` -
        -
      • - {{hero.name}} -
      • -
      - `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - /* The element here always has the state "in" when it - * is present. We animate two transitions: From void - * to in and from in to void, to achieve an animated - * enter and leave transition. Each transition is - * defined in terms of multiple keyframes, to give it - * a bounce effect. - */ - // #docregion animationdef - animations: [ - trigger('flyInOut', [ - state('in', style({transform: 'translateX(0)'})), - transition('void => *', [ - animate(300, keyframes([ - style({opacity: 0, transform: 'translateX(-100%)', offset: 0}), - style({opacity: 1, transform: 'translateX(15px)', offset: 0.3}), - style({opacity: 1, transform: 'translateX(0)', offset: 1.0}) - ])) - ]), - transition('* => void', [ - animate(300, keyframes([ - style({opacity: 1, transform: 'translateX(0)', offset: 0}), - style({opacity: 1, transform: 'translateX(-15px)', offset: 0.7}), - style({opacity: 0, transform: 'translateX(100%)', offset: 1.0}) - ])) - ]) - ]) - ] - // #enddocregion animationdef -}) -export class HeroListMultistepComponent { - @Input() heroes: Hero[]; - - animationStarted(event: AnimationEvent) { - console.warn('Animation started: ', event); - } - - animationDone(event: AnimationEvent) { - console.warn('Animation done: ', event); - } -} diff --git a/aio/content/examples/animations/src/app/hero-list-page.component.css b/aio/content/examples/animations/src/app/hero-list-page.component.css new file mode 100644 index 0000000000..67a3fb0c9c --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-page.component.css @@ -0,0 +1,94 @@ +.heroes { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; +} + +.heroes li { + position: relative; + height: 2.3em; + overflow:hidden; + margin: .5em; +} + +.heroes li > .inner { + cursor: pointer; + background-color: #EEE; + padding: .3em 0; + height: 1.6em; + border-radius: 4px; + width: 19em; +} + +.heroes li:hover > .inner { + color: #607D8B; + background-color: #DDD; + transform: translateX(.1em); +} + +.heroes a { + color: #888; + text-decoration: none; + position: relative; + display: block; + width: 250px; +} + +.heroes a:hover { + color:#607D8B; +} + +.heroes .badge { + display: inline-block; + font-size: small; + color: white; + padding: 0.8em 0.7em 0 0.7em; + background-color: #607D8B; + line-height: 1em; + position: relative; + left: -1px; + top: -4px; + height: 1.8em; + min-width: 16px; + text-align: right; + margin-right: .8em; + border-radius: 4px 0 0 4px; +} + +.button { + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; + font-family: Arial; +} + +button:hover { + background-color: #cfd8dc; +} + +button.delete { + position: relative; + left: 24em; + top: -32px; + background-color: gray !important; + color: white; + display: inherit; + padding: 5px 8px; + width: 2em; +} + +input { + font-size: 100%; + margin-bottom: 2px; + width: 11em; +} + +.heroes input { + position: relative; + top: -3px; + width: 12em; +} diff --git a/aio/content/examples/animations/src/app/hero-list-page.component.html b/aio/content/examples/animations/src/app/hero-list-page.component.html new file mode 100644 index 0000000000..9e07b62581 --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-page.component.html @@ -0,0 +1,19 @@ + +

      Filter/Stagger

      + +
      + +
      + + +
        + +
      • +
        + {{ hero.id }} + {{ hero.name }} +
        +
      • + +
      + \ No newline at end of file diff --git a/aio/content/examples/animations/src/app/hero-list-page.component.ts b/aio/content/examples/animations/src/app/hero-list-page.component.ts new file mode 100644 index 0000000000..746c195d19 --- /dev/null +++ b/aio/content/examples/animations/src/app/hero-list-page.component.ts @@ -0,0 +1,81 @@ +// #docplaster +import { Component, HostBinding, OnInit } from '@angular/core'; +import { trigger, transition, animate, style, query, stagger } from '@angular/animations'; +import { HEROES } from './mock-heroes'; + +// #docregion filter-animations +@Component({ +// #enddocregion filter-animations + selector: 'app-hero-list-page', + templateUrl: 'hero-list-page.component.html', + styleUrls: ['hero-list-page.component.css'], +// #docregion page-animations, filter-animations + animations: [ +// #enddocregion filter-animations + trigger('pageAnimations', [ + transition(':enter', [ + query('.hero, form', [ + style({opacity: 0, transform: 'translateY(-100px)'}), + stagger(-30, [ + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity: 1, transform: 'none' })) + ]) + ]) + ]) + ]), +// #enddocregion page-animations +// #docregion increment +// #docregion filter-animations + trigger('filterAnimation', [ + transition(':enter, * => 0, * => -1', []), + transition(':increment', [ + query(':enter', [ + style({ opacity: 0, width: '0px' }), + stagger(50, [ + animate('300ms ease-out', style({ opacity: 1, width: '*' })), + ]), + ], { optional: true }) + ]), + transition(':decrement', [ + query(':leave', [ + stagger(50, [ + animate('300ms ease-out', style({ opacity: 0, width: '0px' })), + ]), + ]) + ]), + ]), + // #enddocregion increment +// #docregion page-animations + ] +}) +export class HeroListPageComponent implements OnInit { +// #enddocregion filter-animations + @HostBinding('@pageAnimations') + public animatePage = true; + + _heroes = []; +// #docregion filter-animations + heroTotal = -1; +// #enddocregion filter-animations + get heroes() { + return this._heroes; + } + + ngOnInit() { + this._heroes = HEROES; + } + + updateCriteria(criteria: string) { + criteria = criteria ? criteria.trim() : ''; + + this._heroes = HEROES.filter(hero => hero.name.toLowerCase().includes(criteria.toLowerCase())); + const newTotal = this.heroes.length; + + if (this.heroTotal !== newTotal) { + this.heroTotal = newTotal; + } else if (!criteria) { + this.heroTotal = -1; + } + } +// #docregion filter-animations +} +// #enddocregion filter-animations diff --git a/aio/content/examples/animations/src/app/hero-list-timings.component.ts b/aio/content/examples/animations/src/app/hero-list-timings.component.ts deleted file mode 100644 index b410c22cb9..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-timings.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Component, - Input -} from '@angular/core'; -import { - trigger, - state, - style, - animate, - transition -} from '@angular/animations'; - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-timings', - template: ` -
        -
      • - {{hero.name}} -
      • -
      - `, - styleUrls: ['./hero-list.component.css'], - /* The element here always has the state "in" when it - * is present. We animate two transitions: From void - * to in and from in to void, to achieve an animated - * enter and leave transition. The element enters from - * the left and leaves to the right using translateX, - * and fades in/out using opacity. We use different easings - * for enter and leave. - */ - // #docregion animationdef - animations: [ - trigger('flyInOut', [ - state('in', style({opacity: 1, transform: 'translateX(0)'})), - transition('void => *', [ - style({ - opacity: 0, - transform: 'translateX(-100%)' - }), - animate('0.2s ease-in') - ]), - transition('* => void', [ - animate('0.2s 0.1s ease-out', style({ - opacity: 0, - transform: 'translateX(100%)' - })) - ]) - ]) - ] - // #enddocregion animationdef -}) -export class HeroListTimingsComponent { - @Input() heroes: Hero[]; -} diff --git a/aio/content/examples/animations/src/app/hero-list-twoway.component.ts b/aio/content/examples/animations/src/app/hero-list-twoway.component.ts deleted file mode 100644 index ef0be5fce7..0000000000 --- a/aio/content/examples/animations/src/app/hero-list-twoway.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -// #docregion -// #docregion imports -import { - Component, - Input -} from '@angular/core'; -import { - trigger, - state, - style, - animate, - transition -} from '@angular/animations'; -// #enddocregion imports - -import { Hero } from './hero.service'; - -@Component({ - selector: 'app-hero-list-twoway', - // #docregion template - template: ` -
        -
      • - {{hero.name}} -
      • -
      - `, - // #enddocregion template - styleUrls: ['./hero-list.component.css'], - /* - * Define two states, "inactive" and "active", and the end - * styles that apply whenever the element is in those states. - * Then define an animated transition between these two - * states, in *both* directions. - */ - // #docregion animationdef - animations: [ - trigger('heroState', [ - state('inactive', style({ - backgroundColor: '#eee', - transform: 'scale(1)' - })), - state('active', style({ - backgroundColor: '#cfd8dc', - transform: 'scale(1.1)' - })), - // #docregion transitions - transition('inactive <=> active', animate('100ms ease-out')) - // #enddocregion transitions - ]) - ] - // #enddocregion animationdef -}) -export class HeroListTwowayComponent { - @Input() heroes: Hero[]; -} diff --git a/aio/content/examples/animations/src/app/hero-team-builder.component.ts b/aio/content/examples/animations/src/app/hero-team-builder.component.ts deleted file mode 100644 index 8a1904ea4f..0000000000 --- a/aio/content/examples/animations/src/app/hero-team-builder.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Component } from '@angular/core'; - -import { Hero, HeroService } from './hero.service'; - -@Component({ - selector: 'app-root', - template: ` -
      - - - -
      - -
      -
      -

      Basic State

      -

      Switch between active/inactive on click.

      - -
      -
      -

      Styles inline in transitions

      -

      Animated effect on click, no persistend end styles.

      - -
      -
      -

      Combined transition syntax

      -

      Switch between active/inactive on click. Define just one transition used in both directions.

      - -
      -
      -

      Two-way transition syntax

      -

      Switch between active/inactive on click. Define just one transition used in both directions using the <=> syntax.

      - -
      -
      -

      Enter & Leave

      -

      Enter and leave animations using the void state.

      - -
      -
      -
      -
      -

      Enter & Leave & States

      -

      - Enter and leave animations combined with active/inactive state animations. - Different enter and leave transitions depending on state. -

      - -
      -
      -

      Auto Style Calc

      -

      Leave animation from the current computed height using the auto-style value *.

      - -
      -
      -

      Different Timings

      -

      Enter and leave animations with different easings, ease-in for enter, ease-out for leave.

      - -
      -
      -

      Multiple Keyframes

      -

      Enter and leave animations with three keyframes in each, to give the transition some bounce.

      - -
      -
      -

      Parallel Groups

      -

      Enter and leave animations with multiple properties animated in parallel with different timings.

      - -
      -
      - `, - styles: [` - .buttons { - text-align: center; - } - button { - padding: 1.5em 3em; - } - .columns { - display: flex; - flex-direction: row; - } - .column { - flex: 1; - padding: 10px; - } - .column p { - min-height: 6em; - } - `], - providers: [HeroService] -}) -export class HeroTeamBuilderComponent { - heroes: Hero[]; - - constructor(private heroService: HeroService) { - this.heroes = heroService.heroes; - } -} diff --git a/aio/content/examples/animations/src/app/hero.service.ts b/aio/content/examples/animations/src/app/hero.service.ts deleted file mode 100644 index c0053d0185..0000000000 --- a/aio/content/examples/animations/src/app/hero.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Injectable } from '@angular/core'; - -// #docregion hero -export class Hero { - constructor(public name: string, public state = 'inactive') { } - - toggleState() { - this.state = this.state === 'active' ? 'inactive' : 'active'; - } -} -// #enddocregion hero - -const ALL_HEROES = [ - 'Windstorm', - 'RubberMan', - 'Bombasto', - 'Magneta', - 'Dynama', - 'Narco', - 'Celeritas', - 'Dr IQ', - 'Magma', - 'Tornado', - 'Mr. Nice' -].map(name => new Hero(name)); - -@Injectable() -export class HeroService { - - heroes: Hero[] = []; - - canAdd() { - return this.heroes.length < ALL_HEROES.length; - } - - canRemove() { - return this.heroes.length > 0; - } - - addActive(active = true) { - let hero = ALL_HEROES[this.heroes.length]; - hero.state = active ? 'active' : 'inactive'; - this.heroes.push(hero); - } - - addInactive() { - this.addActive(false); - } - - remove() { - this.heroes.length -= 1; - } - -} diff --git a/aio/content/examples/animations/src/app/hero.ts b/aio/content/examples/animations/src/app/hero.ts new file mode 100644 index 0000000000..e3eac516da --- /dev/null +++ b/aio/content/examples/animations/src/app/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/aio/content/examples/animations/src/app/home.component.css b/aio/content/examples/animations/src/app/home.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/animations/src/app/home.component.html b/aio/content/examples/animations/src/app/home.component.html new file mode 100644 index 0000000000..0fb44758d8 --- /dev/null +++ b/aio/content/examples/animations/src/app/home.component.html @@ -0,0 +1,3 @@ +

      + Welcome to Animations in Angular! +

      diff --git a/aio/content/examples/animations/src/app/home.component.ts b/aio/content/examples/animations/src/app/home.component.ts new file mode 100644 index 0000000000..33fd77077e --- /dev/null +++ b/aio/content/examples/animations/src/app/home.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'] +}) +export class HomeComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/aio/content/examples/animations/src/app/insert-remove.component.css b/aio/content/examples/animations/src/app/insert-remove.component.css new file mode 100644 index 0000000000..bc0cb127de --- /dev/null +++ b/aio/content/examples/animations/src/app/insert-remove.component.css @@ -0,0 +1,12 @@ +:host { + display: block; +} + +.insert-remove-container { + border: 1px solid #dddddd; + margin-top: 1em; + padding: 20px 20px 0px 20px; + color: #000000; + font-weight: bold; + font-size: 20px; +} diff --git a/aio/content/examples/animations/src/app/insert-remove.component.html b/aio/content/examples/animations/src/app/insert-remove.component.html new file mode 100644 index 0000000000..f40238dbea --- /dev/null +++ b/aio/content/examples/animations/src/app/insert-remove.component.html @@ -0,0 +1,10 @@ + + + + +
      +

      The box is inserted

      +
      + diff --git a/aio/content/examples/animations/src/app/insert-remove.component.ts b/aio/content/examples/animations/src/app/insert-remove.component.ts new file mode 100644 index 0000000000..596f8e7168 --- /dev/null +++ b/aio/content/examples/animations/src/app/insert-remove.component.ts @@ -0,0 +1,29 @@ +// #docplaster +import { Component } from '@angular/core'; +import { trigger, transition, animate, style } from '@angular/animations'; + +@Component({ + selector: 'app-insert-remove', + animations: [ +// #docregion enter-leave-trigger + trigger('myInsertRemoveTrigger', [ + transition(':enter', [ + style({ opacity: 0 }), + animate('5s', style({ opacity: 1 })), + ]), + transition(':leave', [ + animate('5s', style({ opacity: 0 })) + ]) + ]), +// #enddocregion enter-leave-trigger + ], + templateUrl: 'insert-remove.component.html', + styleUrls: ['insert-remove.component.css'] +}) +export class InsertRemoveComponent { + isShown = false; + + toggle() { + this.isShown = !this.isShown; + } +} diff --git a/aio/content/examples/animations/src/app/mock-heroes.ts b/aio/content/examples/animations/src/app/mock-heroes.ts new file mode 100644 index 0000000000..1771a7103b --- /dev/null +++ b/aio/content/examples/animations/src/app/mock-heroes.ts @@ -0,0 +1,15 @@ +// #docregion +import { Hero } from './hero'; + +export const HEROES: Hero[] = [ + { id: 11, name: 'Mr. Nice' }, + { id: 12, name: 'Narco' }, + { id: 13, name: 'Bombasto' }, + { id: 14, name: 'Celeritas' }, + { id: 15, name: 'Magneta' }, + { id: 16, name: 'RubberMan' }, + { id: 17, name: 'Dynama' }, + { id: 18, name: 'Dr IQ' }, + { id: 19, name: 'Magma' }, + { id: 20, name: 'Tornado' } +]; diff --git a/aio/content/examples/animations/src/app/open-close-page.component.ts b/aio/content/examples/animations/src/app/open-close-page.component.ts new file mode 100644 index 0000000000..72bc45caf6 --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close-page.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-open-close-page', + template: ` +
      +

      Open Close Component

      + Console Log Animation Events + + +
      + ` +}) +export class OpenClosePageComponent { + logging = false; + + toggleLogging() { + this.logging = !this.logging; + } +} diff --git a/aio/content/examples/animations/src/app/open-close.component.1.html b/aio/content/examples/animations/src/app/open-close.component.1.html new file mode 100644 index 0000000000..4edd5000fa --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.1.html @@ -0,0 +1,10 @@ + + + + +
      +

      The box is now {{ isOpen ? 'Open' : 'Closed' }}!

      +
      + diff --git a/aio/content/examples/animations/src/app/open-close.component.1.ts b/aio/content/examples/animations/src/app/open-close.component.1.ts new file mode 100644 index 0000000000..77e33258ea --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.1.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { trigger, transition, state, animate, style, keyframes } from '@angular/animations'; + +@Component({ + selector: 'app-open-close', + animations: [ +// #docregion trigger + trigger('openClose', [ + state('open', style({ + height: '200px', + opacity: 1, + backgroundColor: 'yellow' + })), + state('close', style({ + height: '100px', + opacity: 0.5, + backgroundColor: 'green' + })), + // ... + transition('* => *', [ + animate('1s', keyframes ( [ + style({ opacity: 0.1, offset: 0.1 }), + style({ opacity: 0.6, offset: 0.2 }), + style({ opacity: 1, offset: 0.5 }), + style({ opacity: 0.2, offset: 0.7 }) + ])) + ]) + ]) +// #enddocregion trigger + ], + templateUrl: 'open-close.component.html', + styleUrls: ['open-close.component.css'] +}) +export class OpenCloseKeyframeComponent { + isOpen = false; + + toggle() { + this.isOpen = !this.isOpen; + } +} diff --git a/aio/content/examples/animations/src/app/open-close.component.2.html b/aio/content/examples/animations/src/app/open-close.component.2.html new file mode 100644 index 0000000000..2b1e1cd70c --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.2.html @@ -0,0 +1,12 @@ + + + + +
      + +

      The box is now {{ isOpen ? 'Open' : 'Closed' }}!

      + +
      + diff --git a/aio/content/examples/animations/src/app/open-close.component.2.ts b/aio/content/examples/animations/src/app/open-close.component.2.ts new file mode 100644 index 0000000000..5f13d0d838 --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.2.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { trigger, transition, state, animate, style } from '@angular/animations'; + +@Component({ + selector: 'app-open-close-boolean', +// #docregion trigger-boolean + animations: [ + trigger('openClose', [ + state('true', style({ height: '*' })), + state('false', style({ height: '0px' })), + transition('false <=> true', animate(500)) + ]) + ], +// #enddocregion trigger-boolean + templateUrl: 'open-close.component.2.html', + styleUrls: ['open-close.component.css'] +}) +export class OpenCloseBooleanComponent { + isOpen = false; + + toggle() { + this.isOpen = !this.isOpen; + } +} diff --git a/aio/content/examples/animations/src/app/open-close.component.3.html b/aio/content/examples/animations/src/app/open-close.component.3.html new file mode 100644 index 0000000000..4097a6034e --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.3.html @@ -0,0 +1,15 @@ + + + + +
      + +

      The box is now {{ isOpen ? 'Open' : 'Closed' }}!

      + +
      + diff --git a/aio/content/examples/animations/src/app/open-close.component.3.ts b/aio/content/examples/animations/src/app/open-close.component.3.ts new file mode 100644 index 0000000000..ddd6f09f2b --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.3.ts @@ -0,0 +1,48 @@ +// #docplaster +// #docregion reusable +import { Component } from '@angular/core'; +import { useAnimation, transition, trigger, style, animate } from '@angular/animations'; +import { transAnimation } from './animations'; + +@Component({ +// #enddocregion reusable + selector: 'app-open-close-reusable', +// #docregion runtime + animations: [ + transition('open => closed', [ + style({ + height: '200 px', + opacity: '{{ opacity }}', + backgroundcolor: 'yelow' + }), + animate('{{ time }}'), + ], { + params: { + time: '1s', + opacity: '1' + } + }), +// #enddocregion runtime +// #docregion reusable + trigger('openClose', [ + transition('open => closed', [ + useAnimation(transAnimation, { + params: { + height: 0, + opacity: 1, + backgroundColor: 'red', + time: '1s' + } + }) + ]) + ]) +// #docregion runtime + ], +// #enddocregion runtime +// #enddocregion reusable + templateUrl: 'open-close.component.html', + styleUrls: ['open-close.component.css'] +// #docregion reusable +}) +// #enddocregion reusable +export class OpenCloseReusableComponent { } diff --git a/aio/content/examples/animations/src/app/open-close.component.4.html b/aio/content/examples/animations/src/app/open-close.component.4.html new file mode 100644 index 0000000000..36efb2be57 --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.4.html @@ -0,0 +1,12 @@ + + +
      +
      +

      The box is now {{ isOpen ? 'Open' : 'Closed' }}!

      +
      +
      + \ No newline at end of file diff --git a/aio/content/examples/animations/src/app/open-close.component.4.ts b/aio/content/examples/animations/src/app/open-close.component.4.ts new file mode 100644 index 0000000000..8d03512faf --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.4.ts @@ -0,0 +1,47 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; +import { trigger, transition, state, animate, style } from '@angular/animations'; + +// #docregion toggle-animation +@Component({ +// #enddocregion toggle-animation + selector: 'app-open-close-toggle', + templateUrl: 'open-close.component.4.html', + styleUrls: ['open-close.component.css'], + // #docregion toggle-animation + animations: [ + trigger('childAnimation', [ + // ... +// #enddocregion toggle-animation + state('open', style({ + width: '250px', + opacity: 1, + backgroundColor: 'yellow' + })), + state('closed', style({ + width: '100px', + opacity: 0.5, + backgroundColor: 'green' + })), + transition('* => *', [ + animate('1s') + ]), +// #docregion toggle-animation + ]), + ], +}) +export class OpenCloseChildComponent { + isDisabled = false; + isOpen = false; +// #enddocregion toggle-animation + toggleAnimations() { + this.isDisabled = !this.isDisabled; + } + + toggle() { + this.isOpen = !this.isOpen; + } +// #docregion toggle-animation +} +// #enddocregion toggle-animation diff --git a/aio/content/examples/animations/src/app/open-close.component.css b/aio/content/examples/animations/src/app/open-close.component.css new file mode 100644 index 0000000000..0ba9d678f8 --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.css @@ -0,0 +1,12 @@ +:host { + display: block; +} + +.open-close-container { + border: 1px solid #dddddd; + margin-top: 1em; + padding: 20px 20px 0px 20px; + color: #000000; + font-weight: bold; + font-size: 20px; +} diff --git a/aio/content/examples/animations/src/app/open-close.component.html b/aio/content/examples/animations/src/app/open-close.component.html new file mode 100644 index 0000000000..b58a2e812d --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.html @@ -0,0 +1,13 @@ + + + + +
      +

      The box is now {{ isOpen ? 'Open' : 'Closed' }}!

      +
      + diff --git a/aio/content/examples/animations/src/app/open-close.component.ts b/aio/content/examples/animations/src/app/open-close.component.ts new file mode 100644 index 0000000000..f5ca38e47f --- /dev/null +++ b/aio/content/examples/animations/src/app/open-close.component.ts @@ -0,0 +1,110 @@ +// #docplaster +import { Component, Input } from '@angular/core'; +import { trigger, transition, state, animate, style, AnimationEvent } from '@angular/animations'; + +// #docregion component, events1 +@Component({ + selector: 'app-open-close', +// #docregion trigger, trigger-wildcard1, trigger-transition + animations: [ + trigger('openClose', [ +// #enddocregion events1 +// #docregion state1, events1 + // ... +// #enddocregion events1 + state('open', style({ + height: '200px', + opacity: 1, + backgroundColor: 'yellow' + })), +// #enddocregion state1 +// #docregion state2 + state('closed', style({ + height: '100px', + opacity: 0.5, + backgroundColor: 'green' + })), +// #enddocregion state2, trigger-wildcard1 +// #docregion transition1 + transition('open => closed', [ + animate('1s') + ]), +// #enddocregion transition1 +// #docregion transition2 + transition('closed => open', [ + animate('0.5s') + ]), +// #enddocregion trigger, component +// #enddocregion transition2 +// #docregion trigger-wildcard1 + transition('* => closed', [ + animate('1s') + ]), + transition('* => open', [ + animate('0.5s') + ]), +// #enddocregion trigger-wildcard1 +// #docregion trigger-wildcard2 + transition('open <=> closed', [ + animate('0.5s') + ]), +// #enddocregion trigger-wildcard2 +// #docregion transition4 + transition ('* => open', [ + animate ('1s', + style ({ opacity: '*' }), + ), + ]), +// #enddocregion transition4 +// #docregion transition3 + transition('* => *', [ + animate('1s') + ]), +// #enddocregion transition3, trigger-transition +// #docregion trigger, component, trigger-wildcard1, events1 + ]), + ], +// #enddocregion trigger, trigger-wildcard1 + templateUrl: 'open-close.component.html', + styleUrls: ['open-close.component.css'] +}) +// #docregion events +export class OpenCloseComponent { +// #enddocregion events1, events + isOpen = true; + + toggle() { + this.isOpen = !this.isOpen; + } + +// #enddocregion component + @Input() logging = false; +// #docregion events1, events + onAnimationEvent ( event: AnimationEvent ) { +// #enddocregion events1, events + if (!this.logging) { + return; + } +// #docregion events + // openClose is trigger name in this example + console.warn(`Animation Trigger: ${event.triggerName}`); + + // phaseName is start or done + console.warn(`Phase: ${event.phaseName}`); + + // in our example, totalTime is 1000 or 1 second + console.warn(`Total time: ${event.totalTime}`); + + // in our example, fromState is either open or closed + console.warn(`From: ${event.fromState}`); + + // in our example, toState either open or closed + console.warn(`To: ${event.toState}`); + + // the HTML element itself, the button in this case + console.warn(`Element: ${event.element}`); +// #docregion events1 + } +// #docregion component +} +// #enddocregion component diff --git a/aio/content/examples/animations/src/app/status-slider-page.component.ts b/aio/content/examples/animations/src/app/status-slider-page.component.ts new file mode 100644 index 0000000000..93f60e254d --- /dev/null +++ b/aio/content/examples/animations/src/app/status-slider-page.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-status-slider-page', + template: ` +
      +

      Status Slider

      + +
      + ` +}) +export class StatusSliderPageComponent {} diff --git a/aio/content/examples/animations/src/app/status-slider.component.css b/aio/content/examples/animations/src/app/status-slider.component.css new file mode 100644 index 0000000000..b29dcd2113 --- /dev/null +++ b/aio/content/examples/animations/src/app/status-slider.component.css @@ -0,0 +1,13 @@ +:host { + display: block; +} + +.box { + width: 300px; + border: 5px solid black; + display: block; + line-height: 300px; + text-align: center; + font-size: 50px; + color: white; +} diff --git a/aio/content/examples/animations/src/app/status-slider.component.html b/aio/content/examples/animations/src/app/status-slider.component.html new file mode 100644 index 0000000000..bf6d092889 --- /dev/null +++ b/aio/content/examples/animations/src/app/status-slider.component.html @@ -0,0 +1,7 @@ + + +
      + {{ status == 'active' ? 'Active' : 'Inactive' }} +
      diff --git a/aio/content/examples/animations/src/app/status-slider.component.ts b/aio/content/examples/animations/src/app/status-slider.component.ts new file mode 100644 index 0000000000..fa39b9e688 --- /dev/null +++ b/aio/content/examples/animations/src/app/status-slider.component.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { trigger, transition, state, animate, style, keyframes } from '@angular/animations'; + +@Component({ + selector: 'app-status-slider', + templateUrl: 'status-slider.component.html', + styleUrls: ['status-slider.component.css'], + animations: [ + trigger('slideStatus', [ + state('inactive', style({ backgroundColor: 'blue' })), + state('active', style({ backgroundColor: 'orange' })), + +// #docregion keyframesWithOffsets + transition('* => active', [ + animate('2s', keyframes([ + style({ backgroundColor: 'blue', offset: 0}), + style({ backgroundColor: 'red', offset: 0.8}), + style({ backgroundColor: 'orange', offset: 1.0}) + ])), + ]), + transition('* => inactive', [ + animate('2s', keyframes([ + style({ backgroundColor: 'orange', offset: 0}), + style({ backgroundColor: 'red', offset: 0.2}), + style({ backgroundColor: 'blue', offset: 1.0}) + ])) + ]), +// #enddocregion keyframesWithOffsets + +// #docregion keyframes + transition('* => active', [ + animate('2s', keyframes([ + style({ backgroundColor: 'blue' }), + style({ backgroundColor: 'red' }), + style({ backgroundColor: 'orange' }) + ])) +// #enddocregion keyframes + ]), + ]) + ] +}) +export class StatusSliderComponent { + status: 'active' | 'inactive' = 'inactive'; + + toggle() { + if (this.status === 'active') { + this.status = 'inactive'; + } else { + this.status = 'active'; + } + } +} diff --git a/aio/content/examples/animations/src/app/toggle-animations-page.component.ts b/aio/content/examples/animations/src/app/toggle-animations-page.component.ts new file mode 100644 index 0000000000..443960155e --- /dev/null +++ b/aio/content/examples/animations/src/app/toggle-animations-page.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-toggle-animations-child-page', + template: ` +
      +

      Toggle Animations

      + + +
      + ` +}) +export class ToggleAnimationsPageComponent {} diff --git a/aio/content/examples/animations/src/index.html b/aio/content/examples/animations/src/index.html index fcd0ca2a3c..c1078687e7 100644 --- a/aio/content/examples/animations/src/index.html +++ b/aio/content/examples/animations/src/index.html @@ -8,12 +8,7 @@ -

      External H1 Title for E2E test

      - -
        -
      • External list for E2E test
      • -
      diff --git a/aio/content/examples/animations/stackblitz.json b/aio/content/examples/animations/stackblitz.json index 4fface319e..bc7b0bc2f1 100644 --- a/aio/content/examples/animations/stackblitz.json +++ b/aio/content/examples/animations/stackblitz.json @@ -1,7 +1,9 @@ { - "description": "Angular Animations", + "description": "Angular Animations Guide", "files":[ "!**/*.d.ts", - "!**/*.js" - ] + "!**/*.js", + "!**/*.[1,2,3].*" + ], + "tags": ["animations"] } diff --git a/aio/content/examples/cli-quickstart/src/app/app.component.ts b/aio/content/examples/cli-quickstart/src/app/app.component.ts index d977bbe40a..1c462d321b 100644 --- a/aio/content/examples/cli-quickstart/src/app/app.component.ts +++ b/aio/content/examples/cli-quickstart/src/app/app.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; // #enddocregion import -// #docregion metadata +// #docregion metadata, component @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -13,4 +13,4 @@ import { Component } from '@angular/core'; export class AppComponent { title = 'My First Angular App!'; } -// #enddocregion title, class +// #enddocregion title, class, component diff --git a/aio/content/examples/dependency-injection-in-action/src/app/app.component.html b/aio/content/examples/dependency-injection-in-action/src/app/app.component.html index 45cc12bc2d..e69f4bed42 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/app.component.html +++ b/aio/content/examples/dependency-injection-in-action/src/app/app.component.html @@ -36,3 +36,7 @@
      + +
      + +
      diff --git a/aio/content/examples/dependency-injection-in-action/src/app/app.component.ts b/aio/content/examples/dependency-injection-in-action/src/app/app.component.ts index b29f3b4126..a1992e0892 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/app.component.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/app.component.ts @@ -9,9 +9,6 @@ import { UserService } from './user.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', -// #docregion providers - providers: [ LoggerService, UserContextService, UserService ] -// #enddocregion providers }) export class AppComponent { // #enddocregion import-services diff --git a/aio/content/examples/dependency-injection-in-action/src/app/app.module.ts b/aio/content/examples/dependency-injection-in-action/src/app/app.module.ts index 490670a71c..5c48d4f5f5 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/app.module.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/app.module.ts @@ -31,6 +31,7 @@ import { ParentFinderComponent, BarryComponent, BethComponent, BobComponent } from './parent-finder.component'; +import { StorageComponent } from './storage.component'; const declarations = [ AppComponent, @@ -63,6 +64,7 @@ const c_components = [ a_components, b_components, c_components, + StorageComponent, ], bootstrap: [ AppComponent ], // #docregion providers diff --git a/aio/content/examples/dependency-injection-in-action/src/app/date-logger.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/date-logger.service.ts index 465e16cf9b..d792018c56 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/date-logger.service.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/date-logger.service.ts @@ -5,7 +5,9 @@ import { Injectable } from '@angular/core'; import { LoggerService } from './logger.service'; // #docregion date-logger-service -@Injectable() +@Injectable({ + providedIn: 'root' +}) // #docregion date-logger-service-signature export class DateLoggerService extends LoggerService // #enddocregion date-logger-service-signature diff --git a/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts index 6eb5ffa14b..44db98ee7a 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { Hero } from './hero'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class HeroService { // TODO: move to database diff --git a/aio/content/examples/dependency-injection-in-action/src/app/logger.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/logger.service.ts index df8ee6b9c7..824d84b672 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/logger.service.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/logger.service.ts @@ -1,7 +1,9 @@ // #docregion import { Injectable } from '@angular/core'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class LoggerService { logs: string[] = []; diff --git a/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts b/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts new file mode 100644 index 0000000000..d0fb0ca54e --- /dev/null +++ b/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts @@ -0,0 +1,38 @@ +// #docregion +import { Component, OnInit, Self, SkipSelf } from '@angular/core'; +import { BROWSER_STORAGE, BrowserStorageService } from './storage.service'; + +@Component({ + selector: 'app-storage', + template: ` + Open the inspector to see the local/session storage keys: + +

      Session Storage

      + + +

      Local Storage

      + + `, + providers: [ + BrowserStorageService, + { provide: BROWSER_STORAGE, useFactory: () => sessionStorage } + ] +}) +export class StorageComponent implements OnInit { + + constructor( + @Self() private sessionStorageService: BrowserStorageService, + @SkipSelf() private localStorageService: BrowserStorageService, + ) { } + + ngOnInit() { + } + + setSession() { + this.sessionStorageService.set('hero', 'Mr. Nice - Session'); + } + + setLocal() { + this.localStorageService.set('hero', 'Mr. Nice - Local'); + } +} diff --git a/aio/content/examples/dependency-injection-in-action/src/app/storage.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/storage.service.ts new file mode 100644 index 0000000000..f90bbe01bc --- /dev/null +++ b/aio/content/examples/dependency-injection-in-action/src/app/storage.service.ts @@ -0,0 +1,34 @@ +// #docregion +import { Inject, Injectable, InjectionToken } from '@angular/core'; + +// #docregion storage-token +export const BROWSER_STORAGE = new InjectionToken('Browser Storage', { + providedIn: 'root', + factory: () => localStorage +}); +// #enddocregion storage-token + +// #docregion inject-storage-token +@Injectable({ + providedIn: 'root' +}) +export class BrowserStorageService { + constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {} + + get(key: string) { + this.storage.getItem(key); + } + + set(key: string, value: string) { + this.storage.setItem(key, value); + } + + remove(key: string) { + this.storage.removeItem(key); + } + + clear() { + this.storage.clear(); + } +} +// #enddocregion inject-storage-token diff --git a/aio/content/examples/dependency-injection-in-action/src/app/user-context.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/user-context.service.ts index ed394fc734..45f24105b3 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/user-context.service.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/user-context.service.ts @@ -6,7 +6,9 @@ import { LoggerService } from './logger.service'; import { UserService } from './user.service'; // #docregion injectables, injectable -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class UserContextService { // #enddocregion injectables, injectable name: string; diff --git a/aio/content/examples/dependency-injection-in-action/src/app/user.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/user.service.ts index c48b025a08..09c1c7b4cd 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/user.service.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/user.service.ts @@ -1,7 +1,9 @@ // #docregion import { Injectable } from '@angular/core'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class UserService { getUserById(userId: number): any { diff --git a/aio/content/examples/dependency-injection/src/app/car/car-injector.ts b/aio/content/examples/dependency-injection/src/app/car/car-injector.ts index 4f7498ee4e..931efefc0a 100644 --- a/aio/content/examples/dependency-injection/src/app/car/car-injector.ts +++ b/aio/content/examples/dependency-injection/src/app/car/car-injector.ts @@ -1,26 +1,38 @@ -import { ReflectiveInjector } from '@angular/core'; +import { Injector } from '@angular/core'; import { Car, Engine, Tires } from './car'; import { Logger } from '../logger.service'; // #docregion injector export function useInjector() { - let injector: ReflectiveInjector; + let injector: Injector; // #enddocregion injector /* // #docregion injector-no-new - // Cannot instantiate an ReflectiveInjector like this! - let injector = new ReflectiveInjector([Car, Engine, Tires]); + // Cannot instantiate an Injector like this! + let injector = new Injector([ + { provide: Car, deps: [Engine, Tires] }, + { provide: Engine, deps: [] }, + { provide: Tires, deps: [] } + ]); // #enddocregion injector-no-new */ // #docregion injector, injector-create-and-call - injector = ReflectiveInjector.resolveAndCreate([Car, Engine, Tires]); + injector = Injector.create({ + providers: [ + { provide: Car, deps: [Engine, Tires] }, + { provide: Engine, deps: [] }, + { provide: Tires, deps: [] } + ] + }); // #docregion injector-call let car = injector.get(Car); // #enddocregion injector-call, injector-create-and-call car.description = 'Injector'; - injector = ReflectiveInjector.resolveAndCreate([Logger]); + injector = Injector.create({ + providers: [{ provide: Logger, deps: [] }] + }); let logger = injector.get(Logger); logger.log('Injector car.drive() said: ' + car.drive()); return car; diff --git a/aio/content/examples/dependency-injection/src/app/heroes/hero.service.3.ts b/aio/content/examples/dependency-injection/src/app/heroes/hero.service.3.ts index 2d43704de0..9008bf15c3 100644 --- a/aio/content/examples/dependency-injection/src/app/heroes/hero.service.3.ts +++ b/aio/content/examples/dependency-injection/src/app/heroes/hero.service.3.ts @@ -1,11 +1,10 @@ // #docregion import { Injectable } from '@angular/core'; -import { HEROES } from './mock-heroes'; +import { HEROES } from './mock-heroes'; @Injectable({ // we declare that this service should be created // by the root application injector. - providedIn: 'root', }) export class HeroService { diff --git a/aio/content/examples/dependency-injection/src/app/heroes/hero.service.4.ts b/aio/content/examples/dependency-injection/src/app/heroes/hero.service.4.ts index 0b77c78545..121f94ef02 100644 --- a/aio/content/examples/dependency-injection/src/app/heroes/hero.service.4.ts +++ b/aio/content/examples/dependency-injection/src/app/heroes/hero.service.4.ts @@ -1,12 +1,11 @@ // #docregion import { Injectable } from '@angular/core'; import { HeroModule } from './hero.module'; -import { HEROES } from './mock-heroes'; +import { HEROES } from './mock-heroes'; @Injectable({ // we declare that this service should be created // by any injector that includes HeroModule. - providedIn: HeroModule, }) export class HeroService { diff --git a/aio/content/examples/dependency-injection/src/app/heroes/heroes.component.1.ts b/aio/content/examples/dependency-injection/src/app/heroes/heroes.component.1.ts index 962f95fcb3..a9e40e902c 100644 --- a/aio/content/examples/dependency-injection/src/app/heroes/heroes.component.1.ts +++ b/aio/content/examples/dependency-injection/src/app/heroes/heroes.component.1.ts @@ -1,6 +1,7 @@ // #docplaster // #docregion, v1 import { Component } from '@angular/core'; + // #enddocregion v1 import { HeroService } from './hero.service'; diff --git a/aio/content/examples/dependency-injection/src/app/logger.service.ts b/aio/content/examples/dependency-injection/src/app/logger.service.ts index e943523ad2..3dd172c5f3 100644 --- a/aio/content/examples/dependency-injection/src/app/logger.service.ts +++ b/aio/content/examples/dependency-injection/src/app/logger.service.ts @@ -1,7 +1,9 @@ // #docregion import { Injectable } from '@angular/core'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class Logger { logs: string[] = []; // capture logs for testing diff --git a/aio/content/examples/dependency-injection/src/app/user.service.ts b/aio/content/examples/dependency-injection/src/app/user.service.ts index 03e23b7687..b665f7c5fa 100644 --- a/aio/content/examples/dependency-injection/src/app/user.service.ts +++ b/aio/content/examples/dependency-injection/src/app/user.service.ts @@ -11,7 +11,9 @@ export class User { let alice = new User('Alice', true); let bob = new User('Bob', false); -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class UserService { user = bob; // initial user is Bob diff --git a/aio/content/examples/elements/stackblitz.json b/aio/content/examples/elements/stackblitz.json new file mode 100644 index 0000000000..512e5aca34 --- /dev/null +++ b/aio/content/examples/elements/stackblitz.json @@ -0,0 +1,9 @@ +{ + "description": "Angular Elements", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1].*" + ], + "tags":["cookbook"] +} diff --git a/aio/content/examples/feature-modules/src/app/app.module.ts b/aio/content/examples/feature-modules/src/app/app.module.ts index 503d6a46a1..dd55e2ea04 100644 --- a/aio/content/examples/feature-modules/src/app/app.module.ts +++ b/aio/content/examples/feature-modules/src/app/app.module.ts @@ -1,15 +1,14 @@ // #docplaster // #docregion app-module -import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; +import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; // import the feature module here so you can add it to the imports array below import { CustomerDashboardModule } from './customer-dashboard/customer-dashboard.module'; - @NgModule({ declarations: [ AppComponent @@ -17,7 +16,7 @@ import { CustomerDashboardModule } from './customer-dashboard/customer-dashboard imports: [ BrowserModule, FormsModule, - HttpModule, + HttpClientModule, CustomerDashboardModule // add the feature module here ], providers: [], diff --git a/aio/content/examples/form-validation/e2e/src/app.e2e-spec.ts b/aio/content/examples/form-validation/e2e/src/app.e2e-spec.ts index e6b8bbf605..e6f5e52951 100644 --- a/aio/content/examples/form-validation/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/form-validation/e2e/src/app.e2e-spec.ts @@ -16,6 +16,7 @@ describe('Form Validation Tests', function () { tests('Template-Driven Form'); bobTests(); + asyncValidationTests(); crossValidationTests(); }); @@ -26,6 +27,7 @@ describe('Form Validation Tests', function () { tests('Reactive Form'); bobTests(); + asyncValidationTests(); crossValidationTests(); }); }); @@ -45,6 +47,7 @@ let page: { errorMessages: ElementArrayFinder, heroFormButtons: ElementArrayFinder, heroSubmitted: ElementFinder, + alterEgoErrors: ElementFinder, crossValidationErrorMessage: ElementFinder, }; @@ -63,6 +66,7 @@ function getPage(sectionTag: string) { errorMessages: section.all(by.css('div.alert')), heroFormButtons: buttons, heroSubmitted: section.element(by.css('.submitted-message')), + alterEgoErrors: section.element(by.css('.alter-ego-errors')), crossValidationErrorMessage: section.element(by.css('.cross-validation-error-message')), }; } @@ -156,6 +160,16 @@ function expectFormIsInvalid() { expect(page.form.getAttribute('class')).toMatch('ng-invalid'); } +function triggerAlterEgoValidation() { + // alterEgo has updateOn set to 'blur', click outside of the input to trigger the blur event + element(by.css('app-root')).click() +} + +function waitForAlterEgoValidation() { + // alterEgo async validation will be performed in 400ms + browser.sleep(400); +} + function bobTests() { const emsg = 'Name cannot be Bob.'; @@ -177,6 +191,32 @@ function bobTests() { }); } +function asyncValidationTests() { + const emsg = 'Alter ego is already taken.'; + + it(`should produce "${emsg}" error after setting alterEgo to Eric`, function () { + page.alterEgoInput.clear(); + page.alterEgoInput.sendKeys('Eric'); + + triggerAlterEgoValidation(); + waitForAlterEgoValidation(); + + expectFormIsInvalid(); + expect(page.alterEgoErrors.getText()).toBe(emsg); + }); + + it('should be ok again with different values', function () { + page.alterEgoInput.clear(); + page.alterEgoInput.sendKeys('John'); + + triggerAlterEgoValidation(); + waitForAlterEgoValidation(); + + expectFormIsValid(); + expect(page.alterEgoErrors.isPresent()).toBe(false); + }); +} + function crossValidationTests() { const emsg = 'Name cannot match alter ego.'; @@ -187,6 +227,9 @@ function crossValidationTests() { page.alterEgoInput.clear(); page.alterEgoInput.sendKeys('Batman'); + triggerAlterEgoValidation(); + waitForAlterEgoValidation(); + expectFormIsInvalid(); expect(page.crossValidationErrorMessage.getText()).toBe(emsg); }); @@ -198,6 +241,9 @@ function crossValidationTests() { page.alterEgoInput.clear(); page.alterEgoInput.sendKeys('Superman'); + triggerAlterEgoValidation(); + waitForAlterEgoValidation(); + expectFormIsValid(); expect(page.crossValidationErrorMessage.isPresent()).toBe(false); }); diff --git a/aio/content/examples/form-validation/src/app/app.module.ts b/aio/content/examples/form-validation/src/app/app.module.ts index f25504e6ea..bf08854159 100644 --- a/aio/content/examples/form-validation/src/app/app.module.ts +++ b/aio/content/examples/form-validation/src/app/app.module.ts @@ -8,6 +8,7 @@ import { HeroFormTemplateComponent } from './template/hero-form-template.compone import { HeroFormReactiveComponent } from './reactive/hero-form-reactive.component'; import { ForbiddenValidatorDirective } from './shared/forbidden-name.directive'; import { IdentityRevealedValidatorDirective } from './shared/identity-revealed.directive'; +import { UniqueAlterEgoValidatorDirective } from './shared/alter-ego.directive'; @NgModule({ imports: [ @@ -20,7 +21,8 @@ import { IdentityRevealedValidatorDirective } from './shared/identity-revealed.d HeroFormTemplateComponent, HeroFormReactiveComponent, ForbiddenValidatorDirective, - IdentityRevealedValidatorDirective + IdentityRevealedValidatorDirective, + UniqueAlterEgoValidatorDirective ], bootstrap: [ AppComponent ] }) diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.2.ts b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.2.ts new file mode 100644 index 0000000000..1bd4ca8ab5 --- /dev/null +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.2.ts @@ -0,0 +1,48 @@ +/* tslint:disable: member-ordering forin */ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { forbiddenNameValidator } from '../shared/forbidden-name.directive'; +import { UniqueAlterEgoValidator } from '../shared/alter-ego.directive'; + +@Component({ + selector: 'app-hero-form-reactive', + templateUrl: './hero-form-reactive.component.html', + styleUrls: ['./hero-form-reactive.component.css'], +}) +export class HeroFormReactiveComponent implements OnInit { + + powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; + + hero = { name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0] }; + + heroForm: FormGroup; + + ngOnInit(): void { + // #docregion async-validation + this.heroForm = new FormGroup({ + 'name': new FormControl(this.hero.name, [ + Validators.required, + Validators.minLength(4), + forbiddenNameValidator(/bob/i) + ]), + 'alterEgo': new FormControl(this.hero.alterEgo, { + asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)], + updateOn: 'blur' + }), + 'power': new FormControl(this.hero.power, Validators.required) + }); + // #enddocregion async-validation + } + + get name() { return this.heroForm.get('name'); } + + get power() { return this.heroForm.get('power'); } + + get alterEgo() { return this.heroForm.get('alterEgo'); } + + // #docregion async-validation + constructor(private alterEgoValidator: UniqueAlterEgoValidator) {} + // #enddocregion async-validation +} diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html index b1bc5a4150..0878c76073 100644 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.html @@ -35,6 +35,13 @@ + +
      Validating...
      +
      +
      + Alter ego is already taken. +
      +
      diff --git a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts index 1c1956b51f..41541e7460 100644 --- a/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts +++ b/aio/content/examples/form-validation/src/app/reactive/hero-form-reactive.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { forbiddenNameValidator } from '../shared/forbidden-name.directive'; import { identityRevealedValidator } from '../shared/identity-revealed.directive'; +import { UniqueAlterEgoValidator } from '../shared/alter-ego.directive'; @Component({ selector: 'app-hero-form-reactive', @@ -25,7 +26,10 @@ export class HeroFormReactiveComponent implements OnInit { Validators.minLength(4), forbiddenNameValidator(/bob/i) ]), - 'alterEgo': new FormControl(this.hero.alterEgo), + 'alterEgo': new FormControl(this.hero.alterEgo, { + asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)], + updateOn: 'blur' + }), 'power': new FormControl(this.hero.power, Validators.required) }, { validators: identityRevealedValidator }); // <-- add custom validator at the FormGroup level } @@ -33,4 +37,8 @@ export class HeroFormReactiveComponent implements OnInit { get name() { return this.heroForm.get('name'); } get power() { return this.heroForm.get('power'); } + + get alterEgo() { return this.heroForm.get('alterEgo'); } + + constructor(private alterEgoValidator: UniqueAlterEgoValidator) { } } diff --git a/aio/content/examples/form-validation/src/app/shared/alter-ego.directive.ts b/aio/content/examples/form-validation/src/app/shared/alter-ego.directive.ts new file mode 100644 index 0000000000..f8230e3195 --- /dev/null +++ b/aio/content/examples/form-validation/src/app/shared/alter-ego.directive.ts @@ -0,0 +1,46 @@ +import { Directive, forwardRef, Injectable } from '@angular/core'; +import { + AsyncValidator, + AbstractControl, + NG_ASYNC_VALIDATORS, + ValidationErrors +} from '@angular/forms'; +import { catchError, map } from 'rxjs/operators'; +import { HeroesService } from './heroes.service'; +import { Observable } from 'rxjs'; + +// #docregion async-validator +@Injectable({ providedIn: 'root' }) +export class UniqueAlterEgoValidator implements AsyncValidator { + constructor(private heroesService: HeroesService) {} + + validate( + ctrl: AbstractControl + ): Promise | Observable { + return this.heroesService.isAlterEgoTaken(ctrl.value).pipe( + map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)), + catchError(() => null) + ); + } +} +// #enddocregion async-validator + +// #docregion async-validator-directive +@Directive({ + selector: '[appUniqueAlterEgo]', + providers: [ + { + provide: NG_ASYNC_VALIDATORS, + useExisting: forwardRef(() => UniqueAlterEgoValidator), + multi: true + } + ] +}) +export class UniqueAlterEgoValidatorDirective { + constructor(private validator: UniqueAlterEgoValidator) {} + + validate(control: AbstractControl) { + this.validator.validate(control); + } +} +// #enddocregion async-validator-directive diff --git a/aio/content/examples/form-validation/src/app/shared/heroes.service.ts b/aio/content/examples/form-validation/src/app/shared/heroes.service.ts new file mode 100644 index 0000000000..1bacdec57f --- /dev/null +++ b/aio/content/examples/form-validation/src/app/shared/heroes.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +const ALTER_EGOS = ['Eric']; + +@Injectable({ providedIn: 'root' }) +export class HeroesService { + isAlterEgoTaken(alterEgo: string): Observable { + const isTaken = ALTER_EGOS.includes(alterEgo); + + return of(isTaken).pipe(delay(400)); + } +} diff --git a/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html index 573fa060d4..14668dcbcd 100644 --- a/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html +++ b/aio/content/examples/form-validation/src/app/template/hero-form-template.component.html @@ -11,7 +11,7 @@ - @@ -35,8 +35,20 @@
      - + + + + +
      Validating...
      +
      +
      + Alter ego is already taken. +
      +
      diff --git a/aio/content/examples/forms-overview/e2e/src/app.e2e-spec.ts b/aio/content/examples/forms-overview/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..b15faa2a4c --- /dev/null +++ b/aio/content/examples/forms-overview/e2e/src/app.e2e-spec.ts @@ -0,0 +1,10 @@ +import { browser, element, by } from 'protractor'; + +describe('Forms Overview Tests', function () { + + beforeEach(function () { + browser.get(''); + }); + +}); + diff --git a/aio/content/examples/forms-overview/example-config.json b/aio/content/examples/forms-overview/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/forms-overview/src/app/app.component.css b/aio/content/examples/forms-overview/src/app/app.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/forms-overview/src/app/app.component.html b/aio/content/examples/forms-overview/src/app/app.component.html new file mode 100644 index 0000000000..a274b28459 --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/app.component.html @@ -0,0 +1,10 @@ + +

      Forms Overview

      + +

      Reactive

      + + + +

      Template-Driven

      + + \ No newline at end of file diff --git a/aio/content/examples/forms-overview/src/app/app.component.spec.ts b/aio/content/examples/forms-overview/src/app/app.component.spec.ts new file mode 100644 index 0000000000..1ab69b0184 --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/app.component.spec.ts @@ -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'); + })); +}); diff --git a/aio/content/examples/forms-overview/src/app/app.component.ts b/aio/content/examples/forms-overview/src/app/app.component.ts new file mode 100644 index 0000000000..26f42471ce --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/app.component.ts @@ -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'; +} diff --git a/aio/content/examples/forms-overview/src/app/app.module.ts b/aio/content/examples/forms-overview/src/app/app.module.ts new file mode 100644 index 0000000000..fbce0eff3e --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/app.module.ts @@ -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 { } diff --git a/aio/content/examples/forms-overview/src/app/reactive/favorite-color/favorite-color.component.spec.ts b/aio/content/examples/forms-overview/src/app/reactive/favorite-color/favorite-color.component.spec.ts new file mode 100644 index 0000000000..5b676c08b8 --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/reactive/favorite-color/favorite-color.component.spec.ts @@ -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; + + 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 +}); diff --git a/aio/content/examples/forms-overview/src/app/reactive/favorite-color/favorite-color.component.ts b/aio/content/examples/forms-overview/src/app/reactive/favorite-color/favorite-color.component.ts new file mode 100644 index 0000000000..282df4d8db --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/reactive/favorite-color/favorite-color.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +@Component({ + selector: 'app-reactive-favorite-color', + template: ` + Favorite Color: + ` +}) +export class FavoriteColorComponent { + favoriteColorControl = new FormControl(''); +} diff --git a/aio/content/examples/forms-overview/src/app/reactive/reactive.module.spec.ts b/aio/content/examples/forms-overview/src/app/reactive/reactive.module.spec.ts new file mode 100644 index 0000000000..3add422b83 --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/reactive/reactive.module.spec.ts @@ -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(); + }); +}); diff --git a/aio/content/examples/forms-overview/src/app/reactive/reactive.module.ts b/aio/content/examples/forms-overview/src/app/reactive/reactive.module.ts new file mode 100644 index 0000000000..3b85430dda --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/reactive/reactive.module.ts @@ -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 { } diff --git a/aio/content/examples/forms-overview/src/app/shared/utils.ts b/aio/content/examples/forms-overview/src/app/shared/utils.ts new file mode 100644 index 0000000000..a7cb3ce69c --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/shared/utils.ts @@ -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; +} diff --git a/aio/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.spec.ts b/aio/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.spec.ts new file mode 100644 index 0000000000..60e1830b4d --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.spec.ts @@ -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; + + 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 +}); diff --git a/aio/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.ts b/aio/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.ts new file mode 100644 index 0000000000..7a965b7c33 --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/template/favorite-color/favorite-color.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-template-favorite-color', + template: ` + Favorite Color: + ` +}) +export class FavoriteColorComponent { + favoriteColor = ''; +} diff --git a/aio/content/examples/forms-overview/src/app/template/template.module.spec.ts b/aio/content/examples/forms-overview/src/app/template/template.module.spec.ts new file mode 100644 index 0000000000..cb28c36acd --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/template/template.module.spec.ts @@ -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(); + }); +}); diff --git a/aio/content/examples/forms-overview/src/app/template/template.module.ts b/aio/content/examples/forms-overview/src/app/template/template.module.ts new file mode 100644 index 0000000000..dc2ade4f25 --- /dev/null +++ b/aio/content/examples/forms-overview/src/app/template/template.module.ts @@ -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 { } diff --git a/aio/content/examples/forms-overview/src/index.html b/aio/content/examples/forms-overview/src/index.html new file mode 100644 index 0000000000..a4dc0c2ba3 --- /dev/null +++ b/aio/content/examples/forms-overview/src/index.html @@ -0,0 +1,14 @@ + + + + + Forms Overview + + + + + + + + + diff --git a/aio/content/examples/forms-overview/src/main.ts b/aio/content/examples/forms-overview/src/main.ts new file mode 100644 index 0000000000..91ec6da5f0 --- /dev/null +++ b/aio/content/examples/forms-overview/src/main.ts @@ -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)); diff --git a/aio/content/examples/forms-overview/stackblitz.json b/aio/content/examples/forms-overview/stackblitz.json new file mode 100644 index 0000000000..b3eefdff14 --- /dev/null +++ b/aio/content/examples/forms-overview/stackblitz.json @@ -0,0 +1,7 @@ +{ + "description": "Forms Overview", + "files":[ + "!**/*.d.ts", + "!**/*.js" + ] +} diff --git a/aio/content/examples/hierarchical-dependency-injection/src/app/app.module.ts b/aio/content/examples/hierarchical-dependency-injection/src/app/app.module.ts index 6ea18655af..6445bb0f11 100644 --- a/aio/content/examples/hierarchical-dependency-injection/src/app/app.module.ts +++ b/aio/content/examples/hierarchical-dependency-injection/src/app/app.module.ts @@ -3,23 +3,18 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; -import { AppComponent } from './app.component'; -import { HeroTaxReturnComponent } from './hero-tax-return.component'; -import { HeroesListComponent } from './heroes-list.component'; -import { HeroesService } from './heroes.service'; -import { VillainsListComponent } from './villains-list.component'; +import { AppComponent } from './app.component'; +import { HeroTaxReturnComponent } from './hero-tax-return.component'; +import { HeroesListComponent } from './heroes-list.component'; +import { VillainsListComponent } from './villains-list.component'; -import { carComponents, carServices } from './car.components'; +import { carComponents } from './car.components'; @NgModule({ imports: [ BrowserModule, FormsModule ], - providers: [ - carServices, - HeroesService - ], declarations: [ AppComponent, carComponents, diff --git a/aio/content/examples/hierarchical-dependency-injection/src/app/car.services.ts b/aio/content/examples/hierarchical-dependency-injection/src/app/car.services.ts index 03c79270b0..9ce98d1c5f 100644 --- a/aio/content/examples/hierarchical-dependency-injection/src/app/car.services.ts +++ b/aio/content/examples/hierarchical-dependency-injection/src/app/car.services.ts @@ -21,13 +21,17 @@ export class Tires { } //// Engine services /// -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class EngineService { id = 'E1'; getEngine() { return new Engine(); } } -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class EngineService2 { id = 'E2'; getEngine() { @@ -38,14 +42,18 @@ export class EngineService2 { } //// Tire services /// -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class TiresService { id = 'T1'; getTires() { return new Tires(); } } /// Car Services /// -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CarService { id = 'C1'; constructor( @@ -63,7 +71,9 @@ export class CarService { } } -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CarService2 extends CarService { id = 'C2'; constructor( @@ -78,7 +88,9 @@ export class CarService2 extends CarService { } } -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CarService3 extends CarService2 { id = 'C3'; constructor( diff --git a/aio/content/examples/hierarchical-dependency-injection/src/app/hero-tax-return.component.ts b/aio/content/examples/hierarchical-dependency-injection/src/app/hero-tax-return.component.ts index 0696e5f96b..e1a8daf656 100644 --- a/aio/content/examples/hierarchical-dependency-injection/src/app/hero-tax-return.component.ts +++ b/aio/content/examples/hierarchical-dependency-injection/src/app/hero-tax-return.component.ts @@ -13,17 +13,19 @@ import { HeroTaxReturnService } from './hero-tax-return.service'; }) export class HeroTaxReturnComponent { message = ''; + @Output() close = new EventEmitter(); get taxReturn(): HeroTaxReturn { return this.heroTaxReturnService.taxReturn; } + @Input() set taxReturn (htr: HeroTaxReturn) { this.heroTaxReturnService.taxReturn = htr; } - constructor(private heroTaxReturnService: HeroTaxReturnService ) { } + constructor(private heroTaxReturnService: HeroTaxReturnService) { } onCanceled() { this.flashMessage('Canceled'); diff --git a/aio/content/examples/hierarchical-dependency-injection/src/app/heroes.service.ts b/aio/content/examples/hierarchical-dependency-injection/src/app/heroes.service.ts index c4d08c7b8f..76b8ec09bb 100644 --- a/aio/content/examples/hierarchical-dependency-injection/src/app/heroes.service.ts +++ b/aio/content/examples/hierarchical-dependency-injection/src/app/heroes.service.ts @@ -4,7 +4,9 @@ import { Observable, Observer } from 'rxjs'; import { Hero, HeroTaxReturn } from './hero'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class HeroesService { heroes: Hero[] = [ { id: 1, name: 'RubberMan', tid: '082-27-5678'}, diff --git a/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts b/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts index dc2f71be29..073bf73dd6 100644 --- a/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts +++ b/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts @@ -8,11 +8,11 @@ import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'customers', - loadChildren: 'app/customers/customers.module#CustomersModule' + loadChildren: './customers/customers.module#CustomersModule' }, { path: 'orders', - loadChildren: 'app/orders/orders.module#OrdersModule' + loadChildren: './orders/orders.module#OrdersModule' }, { path: '', diff --git a/aio/content/examples/lazy-loading-ngmodules/src/app/app.module.ts b/aio/content/examples/lazy-loading-ngmodules/src/app/app.module.ts index fa3ee4def3..7848845cef 100644 --- a/aio/content/examples/lazy-loading-ngmodules/src/app/app.module.ts +++ b/aio/content/examples/lazy-loading-ngmodules/src/app/app.module.ts @@ -1,9 +1,9 @@ -import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; -import { AppRoutingModule } from './app-routing.module'; +import { BrowserModule } from '@angular/platform-browser'; +import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ @@ -13,7 +13,7 @@ import { AppComponent } from './app.component'; imports: [ BrowserModule, FormsModule, - HttpModule, + HttpClientModule, AppRoutingModule ], providers: [], diff --git a/aio/content/examples/ngmodule-faq/src/app/app-routing.module.3.ts b/aio/content/examples/ngmodule-faq/src/app/app-routing.module.3.ts index f3c68f5050..573e30ed11 100644 --- a/aio/content/examples/ngmodule-faq/src/app/app-routing.module.3.ts +++ b/aio/content/examples/ngmodule-faq/src/app/app-routing.module.3.ts @@ -5,8 +5,8 @@ import { ContactModule } from './contact/contact.module.3'; const routes: Routes = [ { path: '', redirectTo: 'contact', pathMatch: 'full'}, - { path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' }, - { path: 'heroes', loadChildren: 'app/hero/hero.module.3#HeroModule' } + { path: 'crisis', loadChildren: './crisis/crisis.module#CrisisModule' }, + { path: 'heroes', loadChildren: './hero/hero.module.3#HeroModule' } ]; @NgModule({ diff --git a/aio/content/examples/ngmodule-faq/src/app/app-routing.module.ts b/aio/content/examples/ngmodule-faq/src/app/app-routing.module.ts index 4ce7438bc4..de16e8e6e6 100644 --- a/aio/content/examples/ngmodule-faq/src/app/app-routing.module.ts +++ b/aio/content/examples/ngmodule-faq/src/app/app-routing.module.ts @@ -8,8 +8,8 @@ import { ContactModule } from './contact/contact.module'; const routes: Routes = [ { path: '', redirectTo: 'contact', pathMatch: 'full'}, // #docregion lazy-routes - { path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' }, - { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' } + { path: 'crisis', loadChildren: './crisis/crisis.module#CrisisModule' }, + { path: 'heroes', loadChildren: './hero/hero.module#HeroModule' } // #enddocregion lazy-routes ]; // #enddocregion routes diff --git a/aio/content/examples/ngmodules/e2e/src/app.e2e-spec.ts b/aio/content/examples/ngmodules/e2e/src/app.e2e-spec.ts index e0f080e70f..42207ea22c 100644 --- a/aio/content/examples/ngmodules/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/ngmodules/e2e/src/app.e2e-spec.ts @@ -173,51 +173,4 @@ describe('NgModule-example', function () { }); }); - // describe('index.0.html', function() { - // beforeEach(function () { - // browser.get('index.0.html'); - // }); - - // it('has a title', function () { - // const title = element.all(by.tagName('h1')).get(0); - // expect(title.getText()).toBe('Minimal NgModule'); - // }); - // }); - - // describe('index.1.html', function () { - // beforeEach(function () { - // browser.get('index.1.html'); - // }); - - // describe('app-title', appTitleTests(powderblue)); - // }); - - // describe('index.1b.html', function () { - // beforeEach(function () { - // browser.get('index.1b.html'); - // }); - - // describe('app-title', appTitleTests(powderblue)); - - // describe('contact', contactTests(powderblue)); - // }); - - // describe('index.2.html', function () { - // beforeEach(function () { - // browser.get('index.2.html'); - // }); - - // describe('app-title', appTitleTests(gold)); - - // describe('contact', contactTests(powderblue)); - // }); - - // describe('index.3.html', function () { - // beforeEach(function () { - // browser.get('index.3.html'); - // }); - - // describe('app-title', appTitleTests(gold)); - // }); - }); diff --git a/aio/content/examples/ngmodules/src/app/app-routing.module.ts b/aio/content/examples/ngmodules/src/app/app-routing.module.ts index f4cd4ba03a..2fea32093a 100644 --- a/aio/content/examples/ngmodules/src/app/app-routing.module.ts +++ b/aio/content/examples/ngmodules/src/app/app-routing.module.ts @@ -3,8 +3,8 @@ import { Routes, RouterModule } from '@angular/router'; export const routes: Routes = [ { path: '', redirectTo: 'contact', pathMatch: 'full'}, - { path: 'items', loadChildren: 'app/items/items.module#ItemsModule' }, - { path: 'customers', loadChildren: 'app/customers/customers.module#CustomersModule' } + { path: 'items', loadChildren: './items/items.module#ItemsModule' }, + { path: 'customers', loadChildren: './customers/customers.module#CustomersModule' } ]; @NgModule({ diff --git a/aio/content/examples/observables/src/subscribing.ts b/aio/content/examples/observables/src/subscribing.ts index 4712580b33..06a21575d1 100644 --- a/aio/content/examples/observables/src/subscribing.ts +++ b/aio/content/examples/observables/src/subscribing.ts @@ -4,7 +4,7 @@ import { Observable, of } from 'rxjs'; // #docregion observer // Create simple observable that emits three values -const myObservable = Observable.of(1, 2, 3); +const myObservable = of(1, 2, 3); // Create observer object const myObserver = { diff --git a/aio/content/examples/pipes/src/app/app.module.ts b/aio/content/examples/pipes/src/app/app.module.ts index f1d75597fc..8dd502c702 100644 --- a/aio/content/examples/pipes/src/app/app.module.ts +++ b/aio/content/examples/pipes/src/app/app.module.ts @@ -1,26 +1,21 @@ // #docregion -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; -import { - FlyingHeroesComponent, - FlyingHeroesImpureComponent -} from './flying-heroes.component'; +import { ExponentialStrengthPipe } from './exponential-strength.pipe'; +import { FetchJsonPipe } from './fetch-json.pipe'; +import { FlyingHeroesComponent, FlyingHeroesImpureComponent } from './flying-heroes.component'; +import { FlyingHeroesImpurePipe, FlyingHeroesPipe } from './flying-heroes.pipe'; import { HeroAsyncMessageComponent } from './hero-async-message.component'; import { HeroBirthdayComponent } from './hero-birthday1.component'; import { HeroBirthday2Component } from './hero-birthday2.component'; import { HeroListComponent } from './hero-list.component'; -import { PowerBoosterComponent } from './power-booster.component'; import { PowerBoostCalculatorComponent } from './power-boost-calculator.component'; -import { - FlyingHeroesPipe, - FlyingHeroesImpurePipe -} from './flying-heroes.pipe'; -import { FetchJsonPipe } from './fetch-json.pipe'; -import { ExponentialStrengthPipe } from './exponential-strength.pipe'; +import { PowerBoosterComponent } from './power-booster.component'; + @NgModule({ imports: [ @@ -43,6 +38,6 @@ import { ExponentialStrengthPipe } from './exponential-strength.pipe'; FetchJsonPipe, ExponentialStrengthPipe ], - bootstrap: [ AppComponent ] + bootstrap: [AppComponent] }) export class AppModule { } diff --git a/aio/content/examples/pipes/src/app/fetch-json.pipe.ts b/aio/content/examples/pipes/src/app/fetch-json.pipe.ts index 9fcdf341e1..a6696d3b1d 100644 --- a/aio/content/examples/pipes/src/app/fetch-json.pipe.ts +++ b/aio/content/examples/pipes/src/app/fetch-json.pipe.ts @@ -1,13 +1,14 @@ // #docregion -import { Pipe, PipeTransform } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { Pipe, PipeTransform } from '@angular/core'; + // #docregion pipe-metadata @Pipe({ name: 'fetch', pure: false }) // #enddocregion pipe-metadata -export class FetchJsonPipe implements PipeTransform { +export class FetchJsonPipe implements PipeTransform { private cachedData: any = null; private cachedUrl = ''; @@ -17,7 +18,7 @@ export class FetchJsonPipe implements PipeTransform { if (url !== this.cachedUrl) { this.cachedData = null; this.cachedUrl = url; - this.http.get(url).subscribe( result => this.cachedData = result ); + this.http.get(url).subscribe(result => this.cachedData = result); } return this.cachedData; diff --git a/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts b/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts index 25022d2290..c895a39bcc 100644 --- a/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts @@ -35,14 +35,14 @@ describe('Reactive forms', function () { it('should update the name control when the Update Name button is clicked', async () => { await nameInput.sendKeys(nameText); - const value = await nameInput.getAttribute('value'); + const value1 = await nameInput.getAttribute('value'); - expect(value).toBe(nameText); + expect(value1).toBe(nameText); await updateButton.click(); - const value = await nameInput.getAttribute('value'); + const value2 = await nameInput.getAttribute('value'); - expect(value).toBe('Nancy'); + expect(value2).toBe('Nancy'); }); it('should update the displayed control value when the name control updated', async () => { diff --git a/aio/content/examples/router/e2e/src/app.e2e-spec.ts b/aio/content/examples/router/e2e/src/app.e2e-spec.ts index 4343806859..41d3a49c9a 100644 --- a/aio/content/examples/router/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/router/e2e/src/app.e2e-spec.ts @@ -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) { diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard.component.1.ts b/aio/content/examples/router/src/app/admin/admin-dashboard.component.1.ts deleted file mode 100644 index ffa3e3cb8f..0000000000 --- a/aio/content/examples/router/src/app/admin/admin-dashboard.component.1.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      Dashboard

      - ` -}) -export class AdminDashboardComponent { } diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.1.html b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.1.html new file mode 100644 index 0000000000..754a262154 --- /dev/null +++ b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.1.html @@ -0,0 +1 @@ +

      Dashboard

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard.component.2.ts b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.1.ts similarity index 82% rename from aio/content/examples/router/src/app/admin/admin-dashboard.component.2.ts rename to aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.1.ts index 19406bea9b..8806d7cea8 100644 --- a/aio/content/examples/router/src/app/admin/admin-dashboard.component.2.ts +++ b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.1.ts @@ -5,13 +5,9 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ - template: ` -

      Dashboard

      - -

      Session ID: {{ sessionId | async }}

      - -

      Token: {{ token | async }}

      - ` + selector: 'app-admin-dashboard', + templateUrl: './admin-dashboard.component.html', + styleUrls: ['./admin-dashboard.component.css'] }) export class AdminDashboardComponent implements OnInit { sessionId: Observable; diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.2.html b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.2.html new file mode 100644 index 0000000000..9fedba9793 --- /dev/null +++ b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.2.html @@ -0,0 +1,5 @@ +

      Dashboard

      + +

      Session ID: {{ sessionId | async }}

      + +

      Token: {{ token | async }}

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.css b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.html b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.html new file mode 100644 index 0000000000..9c14d5b266 --- /dev/null +++ b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.html @@ -0,0 +1,10 @@ +

      Dashboard

      + +

      Session ID: {{ sessionId | async }}

      + +

      Token: {{ token | async }}

      + +Preloaded Modules +
        +
      • {{ module }}
      • +
      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/admin/admin-dashboard.component.ts b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.ts similarity index 67% rename from aio/content/examples/router/src/app/admin/admin-dashboard.component.ts rename to aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.ts index 11be8fd029..2447f3ad9c 100644 --- a/aio/content/examples/router/src/app/admin/admin-dashboard.component.ts +++ b/aio/content/examples/router/src/app/admin/admin-dashboard/admin-dashboard.component.ts @@ -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: ` -

      Dashboard

      - -

      Session ID: {{ sessionId | async }}

      - -

      Token: {{ token | async }}

      - - Preloaded Modules -
        -
      • {{ module }}
      • -
      - ` + selector: 'app-admin-dashboard', + templateUrl: './admin-dashboard.component.html', + styleUrls: ['./admin-dashboard.component.css'] }) export class AdminDashboardComponent implements OnInit { sessionId: Observable; @@ -28,7 +18,7 @@ export class AdminDashboardComponent implements OnInit { constructor( private route: ActivatedRoute, - private preloadStrategy: SelectivePreloadingStrategy + preloadStrategy: SelectivePreloadingStrategyService ) { this.modules = preloadStrategy.preloadedModules; } diff --git a/aio/content/examples/router/src/app/admin/admin-routing.module.1.ts b/aio/content/examples/router/src/app/admin/admin-routing.module.1.ts index e7d83f113f..6c3f6cbfc4 100644 --- a/aio/content/examples/router/src/app/admin/admin-routing.module.1.ts +++ b/aio/content/examples/router/src/app/admin/admin-routing.module.1.ts @@ -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 = [ diff --git a/aio/content/examples/router/src/app/admin/admin-routing.module.2.ts b/aio/content/examples/router/src/app/admin/admin-routing.module.2.ts index d945201afe..9b80c24390 100644 --- a/aio/content/examples/router/src/app/admin/admin-routing.module.2.ts +++ b/aio/content/examples/router/src/app/admin/admin-routing.module.2.ts @@ -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 = [ { diff --git a/aio/content/examples/router/src/app/admin/admin-routing.module.3.ts b/aio/content/examples/router/src/app/admin/admin-routing.module.3.ts index 63f1c9aaf4..b337cab0b5 100644 --- a/aio/content/examples/router/src/app/admin/admin-routing.module.3.ts +++ b/aio/content/examples/router/src/app/admin/admin-routing.module.3.ts @@ -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 = [ diff --git a/aio/content/examples/router/src/app/admin/admin-routing.module.ts b/aio/content/examples/router/src/app/admin/admin-routing.module.ts index 2b1048d110..6faa557b16 100644 --- a/aio/content/examples/router/src/app/admin/admin-routing.module.ts +++ b/aio/content/examples/router/src/app/admin/admin-routing.module.ts @@ -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 = [ { diff --git a/aio/content/examples/router/src/app/admin/admin.component.ts b/aio/content/examples/router/src/app/admin/admin.component.ts deleted file mode 100644 index 30abfa4524..0000000000 --- a/aio/content/examples/router/src/app/admin/admin.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      ADMIN

      - - - ` -}) -export class AdminComponent { -} diff --git a/aio/content/examples/router/src/app/admin/admin.module.ts b/aio/content/examples/router/src/app/admin/admin.module.ts index 2736f00e1d..d276b0efd5 100644 --- a/aio/content/examples/router/src/app/admin/admin.module.ts +++ b/aio/content/examples/router/src/app/admin/admin.module.ts @@ -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'; diff --git a/aio/content/examples/router/src/app/admin/admin/admin.component.css b/aio/content/examples/router/src/app/admin/admin/admin.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/admin/admin/admin.component.html b/aio/content/examples/router/src/app/admin/admin/admin.component.html new file mode 100644 index 0000000000..6e924c35d6 --- /dev/null +++ b/aio/content/examples/router/src/app/admin/admin/admin.component.html @@ -0,0 +1,8 @@ +

      ADMIN

      + + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/admin/admin/admin.component.ts b/aio/content/examples/router/src/app/admin/admin/admin.component.ts new file mode 100644 index 0000000000..beb6b89fcf --- /dev/null +++ b/aio/content/examples/router/src/app/admin/admin/admin.component.ts @@ -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 { +} diff --git a/aio/content/examples/router/src/app/admin/manage-crises.component.ts b/aio/content/examples/router/src/app/admin/manage-crises.component.ts deleted file mode 100644 index d3176563eb..0000000000 --- a/aio/content/examples/router/src/app/admin/manage-crises.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      Manage your crises here

      - ` -}) -export class ManageCrisesComponent { } diff --git a/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.css b/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.html b/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.html new file mode 100644 index 0000000000..4edfa72133 --- /dev/null +++ b/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.html @@ -0,0 +1 @@ +

      Manage your crises here

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.ts b/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.ts new file mode 100644 index 0000000000..c3fe3bb92d --- /dev/null +++ b/aio/content/examples/router/src/app/admin/manage-crises/manage-crises.component.ts @@ -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 { } diff --git a/aio/content/examples/router/src/app/admin/manage-heroes.component.ts b/aio/content/examples/router/src/app/admin/manage-heroes.component.ts deleted file mode 100644 index 7f3a39893d..0000000000 --- a/aio/content/examples/router/src/app/admin/manage-heroes.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      Manage your heroes here

      - ` -}) -export class ManageHeroesComponent { } diff --git a/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.css b/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.html b/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.html new file mode 100644 index 0000000000..3e5256527d --- /dev/null +++ b/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.html @@ -0,0 +1 @@ +

      Manage your heroes here

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.ts b/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.ts new file mode 100644 index 0000000000..cb68fb3711 --- /dev/null +++ b/aio/content/examples/router/src/app/admin/manage-heroes/manage-heroes.component.ts @@ -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 { } diff --git a/aio/content/examples/router/src/app/animations.ts b/aio/content/examples/router/src/app/animations.ts index 39a0b1840a..c1cf63f75d 100644 --- a/aio/content/examples/router/src/app/animations.ts +++ b/aio/content/examples/router/src/app/animations.ts @@ -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()), ]) ]); diff --git a/aio/content/examples/router/src/app/app-routing.module.1.ts b/aio/content/examples/router/src/app/app-routing.module.1.ts index 436291f499..181384575f 100644 --- a/aio/content/examples/router/src/app/app-routing.module.1.ts +++ b/aio/content/examples/router/src/app/app-routing.module.1.ts @@ -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 = [ diff --git a/aio/content/examples/router/src/app/app-routing.module.2.ts b/aio/content/examples/router/src/app/app-routing.module.2.ts index c4df89d7f8..01192901f6 100644 --- a/aio/content/examples/router/src/app/app-routing.module.2.ts +++ b/aio/content/examples/router/src/app/app-routing.module.2.ts @@ -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 diff --git a/aio/content/examples/router/src/app/app-routing.module.3.ts b/aio/content/examples/router/src/app/app-routing.module.3.ts index 354ca6e740..4351476a4e 100644 --- a/aio/content/examples/router/src/app/app-routing.module.3.ts +++ b/aio/content/examples/router/src/app/app-routing.module.3.ts @@ -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 diff --git a/aio/content/examples/router/src/app/app-routing.module.4.ts b/aio/content/examples/router/src/app/app-routing.module.4.ts index 9c0d7bb928..5943ad7d05 100644 --- a/aio/content/examples/router/src/app/app-routing.module.4.ts +++ b/aio/content/examples/router/src/app/app-routing.module.4.ts @@ -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 {} diff --git a/aio/content/examples/router/src/app/app-routing.module.5.ts b/aio/content/examples/router/src/app/app-routing.module.5.ts index a12bd2cc7e..a120771875 100644 --- a/aio/content/examples/router/src/app/app-routing.module.5.ts +++ b/aio/content/examples/router/src/app/app-routing.module.5.ts @@ -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 {} diff --git a/aio/content/examples/router/src/app/app-routing.module.6.ts b/aio/content/examples/router/src/app/app-routing.module.6.ts index 83a6ab3521..e1c81f2498 100644 --- a/aio/content/examples/router/src/app/app-routing.module.6.ts +++ b/aio/content/examples/router/src/app/app-routing.module.6.ts @@ -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 {} diff --git a/aio/content/examples/router/src/app/app-routing.module.ts b/aio/content/examples/router/src/app/app-routing.module.ts index be5dd1d5c9..ffad588287 100644 --- a/aio/content/examples/router/src/app/app-routing.module.ts +++ b/aio/content/examples/router/src/app/app-routing.module.ts @@ -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 { } diff --git a/aio/content/examples/router/src/app/app.component.1.html b/aio/content/examples/router/src/app/app.component.1.html new file mode 100644 index 0000000000..255232483e --- /dev/null +++ b/aio/content/examples/router/src/app/app.component.1.html @@ -0,0 +1,7 @@ + +

      Angular Router

      + + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/app.component.1.ts b/aio/content/examples/router/src/app/app.component.1.ts index c6fe6d2a77..68dc114661 100644 --- a/aio/content/examples/router/src/app/app.component.1.ts +++ b/aio/content/examples/router/src/app/app.component.1.ts @@ -1,18 +1,9 @@ -/* First version */ // #docregion import { Component } from '@angular/core'; @Component({ selector: 'app-root', - // #docregion template - template: ` -

      Angular Router

      - - - ` - // #enddocregion template + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'] }) export class AppComponent { } diff --git a/aio/content/examples/router/src/app/app.component.2.html b/aio/content/examples/router/src/app/app.component.2.html new file mode 100644 index 0000000000..51e62b5bd1 --- /dev/null +++ b/aio/content/examples/router/src/app/app.component.2.html @@ -0,0 +1,9 @@ + +

      Angular Router

      + +
      + +
      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/app.component.2.ts b/aio/content/examples/router/src/app/app.component.2.ts index e705183911..79b5689cc8 100644 --- a/aio/content/examples/router/src/app/app.component.2.ts +++ b/aio/content/examples/router/src/app/app.component.2.ts @@ -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: ` -

      Angular Router

      - - - ` + 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 diff --git a/aio/content/examples/router/src/app/app.component.4.html b/aio/content/examples/router/src/app/app.component.4.html new file mode 100644 index 0000000000..4a32aedb34 --- /dev/null +++ b/aio/content/examples/router/src/app/app.component.4.html @@ -0,0 +1,15 @@ + +

      Angular Router

      + + +
      + +
      + + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/app.component.4.ts b/aio/content/examples/router/src/app/app.component.4.ts deleted file mode 100644 index a51b792a78..0000000000 --- a/aio/content/examples/router/src/app/app.component.4.ts +++ /dev/null @@ -1,23 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-root', - // #docregion template - template: ` -

      Angular Router

      - - // #docregion outlets - - - // #enddocregion outlets - ` - // #enddocregion template -}) -export class AppComponent { } diff --git a/aio/content/examples/router/src/app/app.component.5.html b/aio/content/examples/router/src/app/app.component.5.html new file mode 100644 index 0000000000..4cc8c91ffc --- /dev/null +++ b/aio/content/examples/router/src/app/app.component.5.html @@ -0,0 +1,12 @@ + +

      Angular Router

      + +
      + +
      + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/app.component.5.ts b/aio/content/examples/router/src/app/app.component.5.ts deleted file mode 100644 index dc7ebcf58b..0000000000 --- a/aio/content/examples/router/src/app/app.component.5.ts +++ /dev/null @@ -1,20 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-root', - // #docregion template - template: ` -

      Angular Router

      - - - - ` - // #enddocregion template -}) -export class AppComponent { } diff --git a/aio/content/examples/router/src/app/app.component.6.html b/aio/content/examples/router/src/app/app.component.6.html new file mode 100644 index 0000000000..b082558ecb --- /dev/null +++ b/aio/content/examples/router/src/app/app.component.6.html @@ -0,0 +1,13 @@ + +

      Angular Router

      + +
      + +
      + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/app.component.6.ts b/aio/content/examples/router/src/app/app.component.6.ts deleted file mode 100644 index d67ddfb728..0000000000 --- a/aio/content/examples/router/src/app/app.component.6.ts +++ /dev/null @@ -1,23 +0,0 @@ -// #docplaster -// #docregion -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-root', - // #docregion template - template: ` -

      Angular Router

      - - - - ` - // #enddocregion template -}) -export class AppComponent { -} diff --git a/aio/content/examples/router/src/app/app.component.css b/aio/content/examples/router/src/app/app.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/app.component.html b/aio/content/examples/router/src/app/app.component.html new file mode 100644 index 0000000000..70e0c7aeb2 --- /dev/null +++ b/aio/content/examples/router/src/app/app.component.html @@ -0,0 +1,13 @@ + +

      Angular Router

      + +
      + +
      + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/app.component.ts b/aio/content/examples/router/src/app/app.component.ts index 70cdf8cba0..6747d50278 100644 --- a/aio/content/examples/router/src/app/app.component.ts +++ b/aio/content/examples/router/src/app/app.component.ts @@ -1,23 +1,17 @@ // #docplaster // #docregion import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { slideInAnimation } from './animations'; @Component({ selector: 'app-root', - // #docregion template - template: ` -

      Angular Router

      - - - - ` - // #enddocregion template + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'], + animations: [ slideInAnimation ] }) export class AppComponent { + getAnimationData(outlet: RouterOutlet) { + return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']; + } } diff --git a/aio/content/examples/router/src/app/app.module.0.ts b/aio/content/examples/router/src/app/app.module.0.ts index 5d14073b1b..ca3c93cb6b 100644 --- a/aio/content/examples/router/src/app/app.module.0.ts +++ b/aio/content/examples/router/src/app/app.module.0.ts @@ -3,10 +3,10 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { HeroListComponent } from './hero-list.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { PageNotFoundComponent } from './not-found.component'; -import { PageNotFoundComponent as HeroDetailComponent } from './not-found.component'; +import { HeroListComponent } from './hero-list/hero-list.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; +import { PageNotFoundComponent as HeroDetailComponent } from './page-not-found/page-not-found.component'; // #docregion const appRoutes: Routes = [ diff --git a/aio/content/examples/router/src/app/app.module.1.ts b/aio/content/examples/router/src/app/app.module.1.ts index e83e299a16..42a1b546fc 100644 --- a/aio/content/examples/router/src/app/app.module.1.ts +++ b/aio/content/examples/router/src/app/app.module.1.ts @@ -9,10 +9,10 @@ import { RouterModule, Routes } from '@angular/router'; // #enddocregion import-router import { AppComponent } from './app.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { HeroListComponent } from './hero-list.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { HeroListComponent } from './hero-list/hero-list.component'; // #enddocregion first-config -import { PageNotFoundComponent } from './not-found.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; // #docregion first-config // #docregion appRoutes diff --git a/aio/content/examples/router/src/app/app.module.2.ts b/aio/content/examples/router/src/app/app.module.2.ts index 2ba739168c..5bb925b76c 100644 --- a/aio/content/examples/router/src/app/app.module.2.ts +++ b/aio/content/examples/router/src/app/app.module.2.ts @@ -8,9 +8,9 @@ import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; -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'; @NgModule({ imports: [ diff --git a/aio/content/examples/router/src/app/app.module.3.ts b/aio/content/examples/router/src/app/app.module.3.ts index 862faf1c51..3277a6ded6 100644 --- a/aio/content/examples/router/src/app/app.module.3.ts +++ b/aio/content/examples/router/src/app/app.module.3.ts @@ -7,8 +7,8 @@ import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; -import { CrisisListComponent } from './crisis-list.component'; -import { PageNotFoundComponent } from './not-found.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; @NgModule({ // #docregion module-imports @@ -27,3 +27,15 @@ import { PageNotFoundComponent } from './not-found.component'; bootstrap: [ AppComponent ] }) export class AppModule { } +// #enddocregion + +/* +// #docregion module-imports-2 + imports: [ + RouterModule.forChild([ + // Heroes Routes + ]), + AppRoutingModule + ], +// #enddocregion module-imports-2 +*/ diff --git a/aio/content/examples/router/src/app/app.module.4.ts b/aio/content/examples/router/src/app/app.module.4.ts index 4825572361..104a352e5d 100644 --- a/aio/content/examples/router/src/app/app.module.4.ts +++ b/aio/content/examples/router/src/app/app.module.4.ts @@ -6,19 +6,17 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; -import { PageNotFoundComponent } from './not-found.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; +import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; -// #enddocregion crisis-center-module, admin-module -import { ComposeMessageComponent } from './compose-message.component'; -// #docregion admin-module +// #enddocregion crisis-center-module + import { AdminModule } from './admin/admin.module'; // #docregion crisis-center-module -import { DialogService } from './dialog.service'; - @NgModule({ imports: [ CommonModule, @@ -32,14 +30,11 @@ import { DialogService } from './dialog.service'; ], declarations: [ AppComponent, -// #enddocregion admin-module, crisis-center-module +// #enddocregion crisis-center-module ComposeMessageComponent, -// #docregion admin-module, crisis-center-module +// #docregion crisis-center-module PageNotFoundComponent ], - providers: [ - DialogService - ], bootstrap: [ AppComponent ] }) export class AppModule { } diff --git a/aio/content/examples/router/src/app/app.module.5.ts b/aio/content/examples/router/src/app/app.module.5.ts index ad34668cea..593c513e04 100644 --- a/aio/content/examples/router/src/app/app.module.5.ts +++ b/aio/content/examples/router/src/app/app.module.5.ts @@ -10,11 +10,10 @@ import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; -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 { AdminModule } from './admin/admin.module'; -import { DialogService } from './dialog.service'; @NgModule({ imports: [ @@ -30,9 +29,6 @@ import { DialogService } from './dialog.service'; ComposeMessageComponent, PageNotFoundComponent ], - providers: [ - DialogService - ], bootstrap: [ AppComponent ] }) export class AppModule { } diff --git a/aio/content/examples/router/src/app/app.module.6.ts b/aio/content/examples/router/src/app/app.module.6.ts index 4cb0b1fdd5..6875f40d5e 100644 --- a/aio/content/examples/router/src/app/app.module.6.ts +++ b/aio/content/examples/router/src/app/app.module.6.ts @@ -5,7 +5,7 @@ import { FormsModule } from '@angular/forms'; import { Routes, RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; -import { PageNotFoundComponent } from './not-found.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const routes: Routes = [ diff --git a/aio/content/examples/router/src/app/app.module.7.ts b/aio/content/examples/router/src/app/app.module.7.ts index b6ca81ddea..2e0428583d 100644 --- a/aio/content/examples/router/src/app/app.module.7.ts +++ b/aio/content/examples/router/src/app/app.module.7.ts @@ -2,18 +2,16 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; -import { AppComponent } from './app.component'; -import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; +import { ComposeMessageComponent } from './compose-message/compose-message.component'; +import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; -import { ComposeMessageComponent } from './compose-message.component'; -import { LoginRoutingModule } from './login-routing.module'; -import { LoginComponent } from './login.component'; -import { PageNotFoundComponent } from './not-found.component'; - -import { DialogService } from './dialog.service'; +import { AuthModule } from './auth/auth.module'; @NgModule({ imports: [ @@ -21,18 +19,24 @@ import { DialogService } from './dialog.service'; FormsModule, HeroesModule, CrisisCenterModule, - LoginRoutingModule, + AuthModule, AppRoutingModule ], declarations: [ AppComponent, ComposeMessageComponent, - LoginComponent, PageNotFoundComponent ], - providers: [ - DialogService - ], bootstrap: [ AppComponent ] }) -export class AppModule { } +// #docregion inspect-config +export class AppModule { + // Diagnostic only: inspect router configuration + constructor(router: Router) { + // Use a custom replacer to display function names in the route configs + const replacer = (key, value) => (typeof value === 'function') ? value.name : value; + + console.log('Routes: ', JSON.stringify(router.config, replacer, 2)); + } +} +// #enddocregion inspect-config diff --git a/aio/content/examples/router/src/app/app.module.ts b/aio/content/examples/router/src/app/app.module.ts index dcf3401ded..38168eb011 100644 --- a/aio/content/examples/router/src/app/app.module.ts +++ b/aio/content/examples/router/src/app/app.module.ts @@ -1,56 +1,58 @@ // #docplaster -// #docregion +// #docregion auth, preload import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; // #docregion animations-module import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -// #enddocregion animations-module +// #enddocregion auth, animations-module // #docregion inspect-config import { Router } from '@angular/router'; // #enddocregion inspect-config +// #docregion auth import { AppComponent } from './app.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; +import { ComposeMessageComponent } from './compose-message/compose-message.component'; + import { AppRoutingModule } from './app-routing.module'; - import { HeroesModule } from './heroes/heroes.module'; -import { ComposeMessageComponent } from './compose-message.component'; -import { LoginRoutingModule } from './login-routing.module'; -import { LoginComponent } from './login.component'; -import { PageNotFoundComponent } from './not-found.component'; - -import { DialogService } from './dialog.service'; +import { AuthModule } from './auth/auth.module'; // #docregion animations-module @NgModule({ imports: [ // #enddocregion animations-module BrowserModule, + // #docregion animations-module + BrowserAnimationsModule, + // #enddocregion animations-module FormsModule, HeroesModule, - LoginRoutingModule, + AuthModule, AppRoutingModule, // #docregion animations-module - BrowserAnimationsModule - // #enddocregion animations-module ], + // #enddocregion animations-module declarations: [ AppComponent, ComposeMessageComponent, - LoginComponent, PageNotFoundComponent ], - providers: [ - DialogService - ], bootstrap: [ AppComponent ] +// #docregion animations-module }) -// #docregion inspect-config +// #enddocregion animations-module export class AppModule { +// #enddocregion preload, auth // Diagnostic only: inspect router configuration constructor(router: Router) { - console.log('Routes: ', JSON.stringify(router.config, undefined, 2)); + // Use a custom replacer to display function names in the route configs + // const replacer = (key, value) => (typeof value === 'function') ? value.name : value; + + // console.log('Routes: ', JSON.stringify(router.config, replacer, 2)); } +// #docregion preload, auth } -// #enddocregion inspect-config +// #enddocregion preload, auth diff --git a/aio/content/examples/router/src/app/auth-guard.service.1.ts b/aio/content/examples/router/src/app/auth-guard.service.1.ts deleted file mode 100644 index c824bcb208..0000000000 --- a/aio/content/examples/router/src/app/auth-guard.service.1.ts +++ /dev/null @@ -1,11 +0,0 @@ -// #docregion -import { Injectable } from '@angular/core'; -import { CanActivate } from '@angular/router'; - -@Injectable() -export class AuthGuard implements CanActivate { - canActivate() { - console.log('AuthGuard#canActivate called'); - return true; - } -} diff --git a/aio/content/examples/router/src/app/login-routing.module.ts b/aio/content/examples/router/src/app/auth/auth-routing.module.ts similarity index 52% rename from aio/content/examples/router/src/app/login-routing.module.ts rename to aio/content/examples/router/src/app/auth/auth-routing.module.ts index 96d05e7972..78d28ddfc8 100644 --- a/aio/content/examples/router/src/app/login-routing.module.ts +++ b/aio/content/examples/router/src/app/auth/auth-routing.module.ts @@ -1,24 +1,20 @@ // #docregion import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { AuthGuard } from './auth-guard.service'; +import { AuthGuard } from './auth.guard'; import { AuthService } from './auth.service'; -import { LoginComponent } from './login.component'; +import { LoginComponent } from './login/login.component'; -const loginRoutes: Routes = [ +const authRoutes: Routes = [ { path: 'login', component: LoginComponent } ]; @NgModule({ imports: [ - RouterModule.forChild(loginRoutes) + RouterModule.forChild(authRoutes) ], exports: [ RouterModule - ], - providers: [ - AuthGuard, - AuthService ] }) -export class LoginRoutingModule {} +export class AuthRoutingModule {} diff --git a/aio/content/examples/router/src/app/auth/auth.guard.1.ts b/aio/content/examples/router/src/app/auth/auth.guard.1.ts new file mode 100644 index 0000000000..1caa4ef0e7 --- /dev/null +++ b/aio/content/examples/router/src/app/auth/auth.guard.1.ts @@ -0,0 +1,15 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot): boolean { + console.log('AuthGuard#canActivate called'); + return true; + } +} diff --git a/aio/content/examples/router/src/app/auth-guard.service.2.ts b/aio/content/examples/router/src/app/auth/auth.guard.2.ts similarity index 71% rename from aio/content/examples/router/src/app/auth-guard.service.2.ts rename to aio/content/examples/router/src/app/auth/auth.guard.2.ts index 8fd00e151a..2ce03c8c9f 100644 --- a/aio/content/examples/router/src/app/auth-guard.service.2.ts +++ b/aio/content/examples/router/src/app/auth/auth.guard.2.ts @@ -1,17 +1,18 @@ // #docregion -import { Injectable } from '@angular/core'; -import { - CanActivate, Router, - ActivatedRouteSnapshot, - RouterStateSnapshot -} from '@angular/router'; +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; + import { AuthService } from './auth.service'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); diff --git a/aio/content/examples/router/src/app/auth-guard.service.3.ts b/aio/content/examples/router/src/app/auth/auth.guard.3.ts similarity index 79% rename from aio/content/examples/router/src/app/auth-guard.service.3.ts rename to aio/content/examples/router/src/app/auth/auth.guard.3.ts index dd89006411..97dc4546b4 100644 --- a/aio/content/examples/router/src/app/auth-guard.service.3.ts +++ b/aio/content/examples/router/src/app/auth/auth.guard.3.ts @@ -1,4 +1,3 @@ -// #docregion // #docregion can-activate-child import { Injectable } from '@angular/core'; import { @@ -9,17 +8,23 @@ import { } from '@angular/router'; import { AuthService } from './auth.service'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } - canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + canActivateChild( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } diff --git a/aio/content/examples/router/src/app/auth-guard.service.4.ts b/aio/content/examples/router/src/app/auth/auth.guard.4.ts similarity index 97% rename from aio/content/examples/router/src/app/auth-guard.service.4.ts rename to aio/content/examples/router/src/app/auth/auth.guard.4.ts index 5d239a8432..feca8d2eb0 100644 --- a/aio/content/examples/router/src/app/auth-guard.service.4.ts +++ b/aio/content/examples/router/src/app/auth/auth.guard.4.ts @@ -10,7 +10,9 @@ import { } from '@angular/router'; import { AuthService } from './auth.service'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} diff --git a/aio/content/examples/router/src/app/auth-guard.service.ts b/aio/content/examples/router/src/app/auth/auth.guard.ts similarity index 97% rename from aio/content/examples/router/src/app/auth-guard.service.ts rename to aio/content/examples/router/src/app/auth/auth.guard.ts index a32b5cc2b8..8f7d863cc1 100644 --- a/aio/content/examples/router/src/app/auth-guard.service.ts +++ b/aio/content/examples/router/src/app/auth/auth.guard.ts @@ -10,7 +10,9 @@ import { } from '@angular/router'; import { AuthService } from './auth.service'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class AuthGuard implements CanActivate, CanActivateChild, CanLoad { constructor(private authService: AuthService, private router: Router) {} diff --git a/aio/content/examples/router/src/app/auth/auth.module.ts b/aio/content/examples/router/src/app/auth/auth.module.ts new file mode 100644 index 0000000000..f81d867862 --- /dev/null +++ b/aio/content/examples/router/src/app/auth/auth.module.ts @@ -0,0 +1,25 @@ +// #docplaster +// #docregion +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { LoginComponent } from './login/login.component'; +import { AuthRoutingModule } from './auth-routing.module'; + +// #docregion v1 +@NgModule({ + imports: [ + CommonModule, + FormsModule, +// #enddocregion v1 + AuthRoutingModule +// #docregion v1 + ], + declarations: [ + LoginComponent + ] +}) +export class AuthModule {} +// #enddocregion v1 +// #enddocregion diff --git a/aio/content/examples/router/src/app/auth.service.ts b/aio/content/examples/router/src/app/auth/auth.service.ts similarity index 92% rename from aio/content/examples/router/src/app/auth.service.ts rename to aio/content/examples/router/src/app/auth/auth.service.ts index 9978541065..280caa14da 100644 --- a/aio/content/examples/router/src/app/auth.service.ts +++ b/aio/content/examples/router/src/app/auth/auth.service.ts @@ -4,7 +4,9 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { tap, delay } from 'rxjs/operators'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class AuthService { isLoggedIn = false; diff --git a/aio/content/examples/router/src/app/login.component.1.ts b/aio/content/examples/router/src/app/auth/login/login.component.1.ts similarity index 76% rename from aio/content/examples/router/src/app/login.component.1.ts rename to aio/content/examples/router/src/app/auth/login/login.component.1.ts index ddee339011..2986631aa1 100644 --- a/aio/content/examples/router/src/app/login.component.1.ts +++ b/aio/content/examples/router/src/app/auth/login/login.component.1.ts @@ -1,16 +1,12 @@ // #docregion import { Component } from '@angular/core'; import { Router } from '@angular/router'; -import { AuthService } from './auth.service'; +import { AuthService } from '../auth.service'; @Component({ - template: ` -

      LOGIN

      -

      {{message}}

      -

      - - -

      ` + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] }) export class LoginComponent { message: string; diff --git a/aio/content/examples/router/src/app/auth/login/login.component.css b/aio/content/examples/router/src/app/auth/login/login.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/auth/login/login.component.html b/aio/content/examples/router/src/app/auth/login/login.component.html new file mode 100644 index 0000000000..adac1fb788 --- /dev/null +++ b/aio/content/examples/router/src/app/auth/login/login.component.html @@ -0,0 +1,6 @@ +

      LOGIN

      +

      {{message}}

      +

      + + +

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/login.component.ts b/aio/content/examples/router/src/app/auth/login/login.component.ts similarity index 81% rename from aio/content/examples/router/src/app/login.component.ts rename to aio/content/examples/router/src/app/auth/login/login.component.ts index 1a6fae162f..5ebfd58b21 100644 --- a/aio/content/examples/router/src/app/login.component.ts +++ b/aio/content/examples/router/src/app/auth/login/login.component.ts @@ -2,16 +2,12 @@ import { Component } from '@angular/core'; import { Router, NavigationExtras } from '@angular/router'; -import { AuthService } from './auth.service'; +import { AuthService } from '../auth.service'; @Component({ - template: ` -

      LOGIN

      -

      {{message}}

      -

      - - -

      ` + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] }) export class LoginComponent { message: string; diff --git a/aio/content/examples/router/src/app/can-deactivate-guard.service.1.ts b/aio/content/examples/router/src/app/can-deactivate.guard.1.ts similarity index 93% rename from aio/content/examples/router/src/app/can-deactivate-guard.service.1.ts rename to aio/content/examples/router/src/app/can-deactivate.guard.1.ts index 35af6226fb..b490093d42 100644 --- a/aio/content/examples/router/src/app/can-deactivate-guard.service.1.ts +++ b/aio/content/examples/router/src/app/can-deactivate.guard.1.ts @@ -5,9 +5,11 @@ import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { CrisisDetailComponent } from './crisis-center/crisis-detail.component'; +import { CrisisDetailComponent } from './crisis-center/crisis-detail/crisis-detail.component'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class CanDeactivateGuard implements CanDeactivate { canDeactivate( diff --git a/aio/content/examples/router/src/app/can-deactivate-guard.service.ts b/aio/content/examples/router/src/app/can-deactivate.guard.ts similarity index 92% rename from aio/content/examples/router/src/app/can-deactivate-guard.service.ts rename to aio/content/examples/router/src/app/can-deactivate.guard.ts index e001d95ed9..41a93a2af4 100644 --- a/aio/content/examples/router/src/app/can-deactivate-guard.service.ts +++ b/aio/content/examples/router/src/app/can-deactivate.guard.ts @@ -7,7 +7,9 @@ export interface CanComponentDeactivate { canDeactivate: () => Observable | Promise | boolean; } -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class CanDeactivateGuard implements CanDeactivate { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; diff --git a/aio/content/examples/router/src/app/compose-message/compose-message.component.css b/aio/content/examples/router/src/app/compose-message/compose-message.component.css new file mode 100644 index 0000000000..c436ef4952 --- /dev/null +++ b/aio/content/examples/router/src/app/compose-message/compose-message.component.css @@ -0,0 +1,3 @@ +:host { + position: relative; bottom: 10%; +} \ No newline at end of file diff --git a/aio/content/examples/router/src/app/compose-message.component.html b/aio/content/examples/router/src/app/compose-message/compose-message.component.html similarity index 100% rename from aio/content/examples/router/src/app/compose-message.component.html rename to aio/content/examples/router/src/app/compose-message/compose-message.component.html diff --git a/aio/content/examples/router/src/app/compose-message.component.ts b/aio/content/examples/router/src/app/compose-message/compose-message.component.ts similarity index 70% rename from aio/content/examples/router/src/app/compose-message.component.ts rename to aio/content/examples/router/src/app/compose-message/compose-message.component.ts index f2d95de745..445eb8e396 100644 --- a/aio/content/examples/router/src/app/compose-message.component.ts +++ b/aio/content/examples/router/src/app/compose-message/compose-message.component.ts @@ -2,18 +2,12 @@ import { Component, HostBinding } from '@angular/core'; import { Router } from '@angular/router'; -import { slideInDownAnimation } from './animations'; - @Component({ + selector: 'app-compose-message', templateUrl: './compose-message.component.html', - styles: [ ':host { position: relative; bottom: 10%; }' ], - animations: [ slideInDownAnimation ] + styleUrls: ['./compose-message.component.css'] }) export class ComposeMessageComponent { - @HostBinding('@routeAnimation') routeAnimation = true; - @HostBinding('style.display') display = 'block'; - @HostBinding('style.position') position = 'absolute'; - details: string; message: string; sending = false; diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-home.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-home.component.ts deleted file mode 100644 index 0edc35bc6e..0000000000 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center-home.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      Welcome to the Crisis Center

      - ` -}) -export class CrisisCenterHomeComponent { } diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.css b/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.html b/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.html new file mode 100644 index 0000000000..f3c7d4e50c --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.html @@ -0,0 +1 @@ +

      Welcome to the Crisis Center

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.ts new file mode 100644 index 0000000000..c2b7cde814 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-home/crisis-center-home.component.ts @@ -0,0 +1,9 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-crisis-center-home', + templateUrl: './crisis-center-home.component.html', + styleUrls: ['./crisis-center-home.component.css'] +}) +export class CrisisCenterHomeComponent { } diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.1.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.1.ts index 8ef60e68a1..c002b2ebb3 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.1.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.1.ts @@ -3,10 +3,10 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; +import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; +import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; // #docregion routes const crisisCenterRoutes: Routes = [ diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.2.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.2.ts index 9e9b514968..5bc0dfee02 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.2.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.2.ts @@ -3,30 +3,23 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; +import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; +import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; // #enddocregion routes // #docregion can-deactivate-guard -import { CanDeactivateGuard } from '../can-deactivate-guard.service'; +import { CanDeactivateGuard } from '../can-deactivate.guard'; // #enddocregion can-deactivate-guard // #docregion crisis-detail-resolver -import { CrisisDetailResolver } from './crisis-detail-resolver.service'; +import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; // #enddocregion crisis-detail-resolver // #docregion routes const crisisCenterRoutes: Routes = [ // #enddocregion routes - // #docregion redirect, routes - { - path: '', - redirectTo: '/crisis-center', - pathMatch: 'full' - }, - // #enddocregion redirect, routes // #docregion routes { path: 'crisis-center', @@ -45,7 +38,7 @@ const crisisCenterRoutes: Routes = [ // #enddocregion can-deactivate-guard // #docregion crisis-detail-resolver resolve: { - crisis: CrisisDetailResolver + crisis: CrisisDetailResolverService } // #enddocregion crisis-detail-resolver // #docregion routes diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.3.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.3.ts index 6d605dbe84..f134bd34e9 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.3.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.3.ts @@ -3,20 +3,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; +import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; +import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; // #docregion can-deactivate-guard -import { CanDeactivateGuard } from '../can-deactivate-guard.service'; +import { CanDeactivateGuard } from '../can-deactivate.guard'; const crisisCenterRoutes: Routes = [ - { - path: '', - redirectTo: '/crisis-center', - pathMatch: 'full' - }, { path: 'crisis-center', component: CrisisCenterComponent, diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.4.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.4.ts index b7ac88e852..03ffed2ec2 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.4.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.4.ts @@ -3,25 +3,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; +import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; +import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; -import { CanDeactivateGuard } from '../can-deactivate-guard.service'; +import { CanDeactivateGuard } from '../can-deactivate.guard'; +import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; -// #docregion crisis-detail-resolver -import { CrisisDetailResolver } from './crisis-detail-resolver.service'; - -// #enddocregion crisis-detail-resolver const crisisCenterRoutes: Routes = [ - // #docregion redirect - { - path: '', - redirectTo: '/crisis-center', - pathMatch: 'full' - }, - // #enddocregion redirect { path: 'crisis-center', component: CrisisCenterComponent, @@ -35,7 +25,7 @@ const crisisCenterRoutes: Routes = [ component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { - crisis: CrisisDetailResolver + crisis: CrisisDetailResolverService } }, { @@ -48,18 +38,13 @@ const crisisCenterRoutes: Routes = [ } ]; -// #docregion crisis-detail-resolver @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule - ], - providers: [ - CrisisDetailResolver ] }) export class CrisisCenterRoutingModule { } -// #enddocregion crisis-detail-resolver // #enddocregion diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.ts index c01d592455..393deb7d8d 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center-routing.module.ts @@ -3,13 +3,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; +import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; +import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; -import { CanDeactivateGuard } from '../can-deactivate-guard.service'; -import { CrisisDetailResolver } from './crisis-detail-resolver.service'; +import { CanDeactivateGuard } from '../can-deactivate.guard'; +import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { @@ -25,7 +25,7 @@ const crisisCenterRoutes: Routes = [ component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { - crisis: CrisisDetailResolver + crisis: CrisisDetailResolverService } }, { @@ -44,9 +44,6 @@ const crisisCenterRoutes: Routes = [ ], exports: [ RouterModule - ], - providers: [ - CrisisDetailResolver ] }) export class CrisisCenterRoutingModule { } diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center.component.ts deleted file mode 100644 index c7d7fe412d..0000000000 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      CRISIS CENTER

      - - ` -}) -export class CrisisCenterComponent { } diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center.module.1.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center.module.1.ts deleted file mode 100644 index 5a3e45f58f..0000000000 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center.module.1.ts +++ /dev/null @@ -1,36 +0,0 @@ -// #docplaster -// #docregion -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { CommonModule } from '@angular/common'; - -import { CrisisService } from './crisis.service'; - -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; - -import { CrisisCenterRoutingModule } from './crisis-center-routing.module'; - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - CrisisCenterRoutingModule - ], - declarations: [ - CrisisCenterComponent, - CrisisListComponent, - CrisisCenterHomeComponent, - CrisisDetailComponent - ], - - // #docregion providers - providers: [ - CrisisService - ] - // #enddocregion providers -}) -export class CrisisCenterModule {} -// #enddocregion diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center.module.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center.module.ts index 4061ceac60..fb6753011e 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-center.module.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center.module.ts @@ -1,15 +1,12 @@ -// #docplaster // #docregion import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import { CrisisService } from './crisis.service'; - -import { CrisisCenterComponent } from './crisis-center.component'; -import { CrisisListComponent } from './crisis-list.component'; -import { CrisisCenterHomeComponent } from './crisis-center-home.component'; -import { CrisisDetailComponent } from './crisis-detail.component'; +import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; +import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CrisisCenterRoutingModule } from './crisis-center-routing.module'; @@ -24,12 +21,6 @@ import { CrisisCenterRoutingModule } from './crisis-center-routing.module'; CrisisListComponent, CrisisCenterHomeComponent, CrisisDetailComponent - ], - providers: [ - CrisisService ] }) -// #docregion crisis-center-module-export export class CrisisCenterModule {} -// #enddocregion crisis-center-module-export -// #enddocregion diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.css b/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.html b/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.html new file mode 100644 index 0000000000..f208bcd790 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.html @@ -0,0 +1,2 @@ +

      CRISIS CENTER

      + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.ts new file mode 100644 index 0000000000..3e0ac92cb8 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-center/crisis-center.component.ts @@ -0,0 +1,9 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-crisis-center', + templateUrl: './crisis-center.component.html', + styleUrls: ['./crisis-center.component.css'] +}) +export class CrisisCenterComponent { } diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.1.ts b/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.1.ts new file mode 100644 index 0000000000..c87b9343fc --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.1.ts @@ -0,0 +1,11 @@ +// #docregion +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class CrisisDetailResolverService { + + constructor() { } + +} diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.ts b/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.ts index a861c3bb97..eed3b2a190 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-detail-resolver.service.ts @@ -1,27 +1,34 @@ + // #docregion import { Injectable } from '@angular/core'; -import { Router, Resolve, RouterStateSnapshot, - ActivatedRouteSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import { Observable, of, EMPTY } from 'rxjs'; +import { mergeMap, take } from 'rxjs/operators'; -import { Crisis, CrisisService } from './crisis.service'; +import { CrisisService } from './crisis.service'; +import { Crisis } from './crisis'; -@Injectable() -export class CrisisDetailResolver implements Resolve { +@Injectable({ + providedIn: 'root', +}) +export class CrisisDetailResolverService implements Resolve { constructor(private cs: CrisisService, private router: Router) {} - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Observable { let id = route.paramMap.get('id'); return this.cs.getCrisis(id).pipe( take(1), - map(crisis => { + mergeMap(crisis => { if (crisis) { - return crisis; + return of(crisis); } else { // id not found this.router.navigate(['/crisis-center']); - return null; + return EMPTY; } }) ); diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-detail.component.1.ts b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.1.ts similarity index 68% rename from aio/content/examples/router/src/app/crisis-center/crisis-detail.component.1.ts rename to aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.1.ts index 45e8d9c95b..8a70cd9b86 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-detail.component.1.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.1.ts @@ -1,38 +1,20 @@ // #docplaster // #docregion -import { Component, OnInit, HostBinding } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { slideInDownAnimation } from '../animations'; -import { Crisis, CrisisService } from './crisis.service'; -import { DialogService } from '../dialog.service'; +import { CrisisService } from '../crisis.service'; +import { Crisis } from '../crisis'; +import { DialogService } from '../../dialog.service'; @Component({ - template: ` -
      -

      "{{ editName }}"

      -
      - {{ crisis.id }}
      -
      - - -
      -

      - - -

      -
      - `, - styles: ['input {width: 20em}'], - animations: [ slideInDownAnimation ] + selector: 'app-crisis-detail', + templateUrl: './crisis-detail.component.html', + styleUrls: ['./crisis-detail.component.css'] }) export class CrisisDetailComponent implements OnInit { - @HostBinding('@routeAnimation') routeAnimation = true; - @HostBinding('style.display') display = 'block'; - @HostBinding('style.position') position = 'absolute'; - crisis: Crisis; editName: string; diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.css b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.css new file mode 100644 index 0000000000..1300202b35 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.css @@ -0,0 +1,3 @@ +input { + width: 20em +} \ No newline at end of file diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.html b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.html new file mode 100644 index 0000000000..524f839df1 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.html @@ -0,0 +1,13 @@ +
      +

      "{{ editName }}"

      +
      + {{ crisis.id }}
      +
      + + +
      +

      + + +

      +
      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-detail.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.ts similarity index 70% rename from aio/content/examples/router/src/app/crisis-center/crisis-detail.component.ts rename to aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.ts index 2b5150686c..1aeeff58aa 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-detail.component.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-detail/crisis-detail.component.ts @@ -4,34 +4,15 @@ import { Component, OnInit, HostBinding } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { slideInDownAnimation } from '../animations'; -import { Crisis } from './crisis.service'; -import { DialogService } from '../dialog.service'; +import { Crisis } from '../crisis'; +import { DialogService } from '../../dialog.service'; @Component({ - template: ` -
      -

      "{{ editName }}"

      -
      - {{ crisis.id }}
      -
      - - -
      -

      - - -

      -
      - `, - styles: ['input {width: 20em}'], - animations: [ slideInDownAnimation ] + selector: 'app-crisis-detail', + templateUrl: './crisis-detail.component.html', + styleUrls: ['./crisis-detail.component.css'] }) export class CrisisDetailComponent implements OnInit { - @HostBinding('@routeAnimation') routeAnimation = true; - @HostBinding('style.display') display = 'block'; - @HostBinding('style.position') position = 'absolute'; - crisis: Crisis; editName: string; diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-list.component.1.ts b/aio/content/examples/router/src/app/crisis-center/crisis-list.component.1.ts deleted file mode 100644 index 5bd95158bb..0000000000 --- a/aio/content/examples/router/src/app/crisis-center/crisis-list.component.1.ts +++ /dev/null @@ -1,44 +0,0 @@ - -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, ParamMap } from '@angular/router'; - -import { Crisis, CrisisService } from './crisis.service'; -import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; - -@Component({ - // #docregion relative-navigation-router-link - template: ` - - - - ` - // #enddocregion relative-navigation-router-link -}) -export class CrisisListComponent implements OnInit { - crises$: Observable; - selectedId: number; - - - constructor( - private service: CrisisService, - private route: ActivatedRoute - ) {} - - - ngOnInit() { - this.crises$ = this.route.paramMap.pipe( - switchMap((params: ParamMap) => { - this.selectedId = +params.get('id'); - return this.service.getCrises(); - }) - ); - } -} diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-list.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.1.ts similarity index 59% rename from aio/content/examples/router/src/app/crisis-center/crisis-list.component.ts rename to aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.1.ts index 3219f10212..eac3ba228e 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis-list.component.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.1.ts @@ -1,35 +1,26 @@ -// #docregion + import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { Crisis, CrisisService } from './crisis.service'; +import { CrisisService } from '../crisis.service'; +import { Crisis } from '../crisis'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @Component({ - template: ` - - - - ` + selector: 'app-crisis-list', + templateUrl: './crisis-list.component.html', + styleUrls: ['./crisis-list.component.css'] }) export class CrisisListComponent implements OnInit { crises$: Observable; selectedId: number; - // #docregion ctor constructor( private service: CrisisService, private route: ActivatedRoute ) {} - // #enddocregion ctor + ngOnInit() { this.crises$ = this.route.paramMap.pipe( diff --git a/aio/content/examples/router/src/assets/app.css b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.css similarity index 54% rename from aio/content/examples/router/src/assets/app.css rename to aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.css index 8da7fa6567..65ce4b1e70 100644 --- a/aio/content/examples/router/src/assets/app.css +++ b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.css @@ -1,41 +1,37 @@ -/* items class */ -.items { +/* CrisisListComponent's private CSS styles */ +.crises { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 24em; } -.items li { - cursor: pointer; +.crises li { position: relative; - left: 0; + cursor: pointer; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } -.items li a { - display: block; - text-decoration: none; -} -.items li:hover { + +.crises li:hover { color: #607D8B; background-color: #DDD; left: .1em; } -.items li.selected { - background-color: #CFD8DC; - color: white; + +.crises a { + color: #888; + text-decoration: none; + display: block; } -.items li.selected:hover { - background-color: #BBD8DC; + +.crises a:hover { + color:#607D8B; } -.items .text { - position: relative; - top: -3px; -} -.items .badge { + +.crises .badge { display: inline-block; font-size: small; color: white; @@ -46,6 +42,38 @@ left: -1px; top: -4px; height: 1.8em; + min-width: 16px; + text-align: right; margin-right: .8em; border-radius: 4px 0 0 4px; } + +button { + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; + font-family: Arial; +} + +button:hover { + background-color: #cfd8dc; +} + +button.delete { + position: relative; + left: 194px; + top: -32px; + background-color: gray !important; + color: white; +} + +.crises li.selected { + background-color: #CFD8DC; + color: white; +} +.crises li.selected:hover { + background-color: #BBD8DC; +} \ No newline at end of file diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.html b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.html new file mode 100644 index 0000000000..062bd6b319 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.html @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.ts b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.ts new file mode 100644 index 0000000000..21a6e63fb0 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis-list/crisis-list.component.ts @@ -0,0 +1,34 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { CrisisService } from '../crisis.service'; +import { Crisis } from '../crisis'; +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +@Component({ + selector: 'app-crisis-list', + templateUrl: './crisis-list.component.html', + styleUrls: ['./crisis-list.component.css'] +}) +export class CrisisListComponent implements OnInit { + crises$: Observable; + selectedId: number; + + // #docregion ctor + constructor( + private service: CrisisService, + private route: ActivatedRoute + ) {} + // #enddocregion ctor + + ngOnInit() { + this.crises$ = this.route.paramMap.pipe( + switchMap(params => { + this.selectedId = +params.get('id'); + return this.service.getCrises(); + }) + ); + } +} diff --git a/aio/content/examples/router/src/app/crisis-center/crisis.service.ts b/aio/content/examples/router/src/app/crisis-center/crisis.service.ts index 72b51316c7..a1ebc42256 100644 --- a/aio/content/examples/router/src/app/crisis-center/crisis.service.ts +++ b/aio/content/examples/router/src/app/crisis-center/crisis.service.ts @@ -1,27 +1,22 @@ // #docplaster -// #docregion , mock-crises +// #docregion import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; -export class Crisis { - constructor(public id: number, public name: string) { } -} - -const CRISES = [ - new Crisis(1, 'Dragon Burning Cities'), - new Crisis(2, 'Sky Rains Great White Sharks'), - new Crisis(3, 'Giant Asteroid Heading For Earth'), - new Crisis(4, 'Procrastinators Meeting Delayed Again'), -]; -// #enddocregion mock-crises - import { Injectable } from '@angular/core'; +import { MessageService } from '../message.service'; +import { Crisis } from './crisis'; +import { CRISES } from './mock-crises'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class CrisisService { static nextCrisisId = 100; private crises$: BehaviorSubject = new BehaviorSubject(CRISES); + constructor(private messageService: MessageService) { } + getCrises() { return this.crises$; } getCrisis(id: number | string) { @@ -34,7 +29,7 @@ export class CrisisService { addCrisis(name: string) { name = name.trim(); if (name) { - let crisis = new Crisis(CrisisService.nextCrisisId++, name); + let crisis = { id: CrisisService.nextCrisisId++, name }; CRISES.push(crisis); this.crises$.next(CRISES); } diff --git a/aio/content/examples/router/src/app/crisis-center/crisis.ts b/aio/content/examples/router/src/app/crisis-center/crisis.ts new file mode 100644 index 0000000000..3b89ceec79 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/crisis.ts @@ -0,0 +1,4 @@ +export class Crisis { + id: number; + name: string; +} diff --git a/aio/content/examples/router/src/app/crisis-center/mock-crises.ts b/aio/content/examples/router/src/app/crisis-center/mock-crises.ts new file mode 100644 index 0000000000..1870a86170 --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-center/mock-crises.ts @@ -0,0 +1,9 @@ +// #docregion +import { Crisis } from './crisis'; + +export const CRISES: Crisis[] = [ + { id: 1, name: 'Dragon Burning Cities' }, + { id: 2, name: 'Sky Rains Great White Sharks' }, + { id: 3, name: 'Giant Asteroid Heading For Earth' }, + { id: 4, name: 'Procrastinators Meeting Delayed Again' }, +] diff --git a/aio/content/examples/router/src/app/crisis-list/crisis-list.component.css b/aio/content/examples/router/src/app/crisis-list/crisis-list.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/crisis-list/crisis-list.component.html b/aio/content/examples/router/src/app/crisis-list/crisis-list.component.html new file mode 100644 index 0000000000..aabd2a641e --- /dev/null +++ b/aio/content/examples/router/src/app/crisis-list/crisis-list.component.html @@ -0,0 +1,2 @@ +

      CRISIS CENTER

      +

      Get your crisis here

      diff --git a/aio/content/examples/router/src/app/crisis-list.component.ts b/aio/content/examples/router/src/app/crisis-list/crisis-list.component.ts similarity index 52% rename from aio/content/examples/router/src/app/crisis-list.component.ts rename to aio/content/examples/router/src/app/crisis-list/crisis-list.component.ts index 6caa3653b5..2fa2e05e49 100644 --- a/aio/content/examples/router/src/app/crisis-list.component.ts +++ b/aio/content/examples/router/src/app/crisis-list/crisis-list.component.ts @@ -3,8 +3,8 @@ import { Component } from '@angular/core'; @Component({ - template: ` -

      CRISIS CENTER

      -

      Get your crisis here

      ` + selector: 'app-crisis-list', + templateUrl: './crisis-list.component.html', + styleUrls: ['./crisis-list.component.css'] }) export class CrisisListComponent { } diff --git a/aio/content/examples/router/src/app/dialog.service.ts b/aio/content/examples/router/src/app/dialog.service.ts index d9f7f1e163..e0ed6760cb 100644 --- a/aio/content/examples/router/src/app/dialog.service.ts +++ b/aio/content/examples/router/src/app/dialog.service.ts @@ -7,7 +7,9 @@ import { Observable, of } from 'rxjs'; * DialogService makes this app easier to test by faking this service. * TODO: better modal implementation that doesn't use window.confirm */ -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class DialogService { /** * Ask user to confirm an action. `message` explains the action and choices. diff --git a/aio/content/examples/router/src/app/hero-list.component.ts b/aio/content/examples/router/src/app/hero-list.component.ts deleted file mode 100644 index 7a8f97ca1e..0000000000 --- a/aio/content/examples/router/src/app/hero-list.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -/// Initial empty version -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: ` -

      HEROES

      -

      Get your heroes here

      - - - ` -}) -export class HeroListComponent { } diff --git a/aio/content/examples/router/src/app/hero-list/hero-list.component.css b/aio/content/examples/router/src/app/hero-list/hero-list.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/hero-list/hero-list.component.html b/aio/content/examples/router/src/app/hero-list/hero-list.component.html new file mode 100644 index 0000000000..58747e208a --- /dev/null +++ b/aio/content/examples/router/src/app/hero-list/hero-list.component.html @@ -0,0 +1,6 @@ + +

      HEROES

      +

      Get your heroes here

      + + + diff --git a/aio/content/examples/router/src/app/hero-list/hero-list.component.ts b/aio/content/examples/router/src/app/hero-list/hero-list.component.ts new file mode 100644 index 0000000000..5ee06903c0 --- /dev/null +++ b/aio/content/examples/router/src/app/hero-list/hero-list.component.ts @@ -0,0 +1,10 @@ + +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-hero-list', + templateUrl: './hero-list.component.html', + styleUrls: ['./hero-list.component.css'] +}) +export class HeroListComponent { } diff --git a/aio/content/examples/router/src/app/heroes/hero-detail.component.1.ts b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.1.ts similarity index 70% rename from aio/content/examples/router/src/app/heroes/hero-detail.component.1.ts rename to aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.1.ts index 511b12d8a7..42cb60e1f8 100644 --- a/aio/content/examples/router/src/app/heroes/hero-detail.component.1.ts +++ b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.1.ts @@ -9,24 +9,13 @@ import { Observable } from 'rxjs'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; // #enddocregion imports -import { Hero, HeroService } from './hero.service'; +import { HeroService } from '../hero.service'; +import { Hero } from '../hero'; @Component({ - template: ` -

      HEROES

      -
      -

      "{{ hero.name }}"

      -
      - {{ hero.id }}
      -
      - - -
      -

      - -

      -
      - ` + selector: 'app-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { hero$: Observable; diff --git a/aio/content/examples/router/src/app/heroes/hero-detail.component.2.ts b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.2.ts similarity index 61% rename from aio/content/examples/router/src/app/heroes/hero-detail.component.2.ts rename to aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.2.ts index 0affccda9a..23d7e4fdc9 100644 --- a/aio/content/examples/router/src/app/heroes/hero-detail.component.2.ts +++ b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.2.ts @@ -4,24 +4,13 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { Hero, HeroService } from './hero.service'; +import { HeroService } from '../hero.service'; +import { Hero } from '../hero'; @Component({ - template: ` -

      HEROES

      -
      -

      "{{ hero.name }}"

      -
      - {{ hero.id }}
      -
      - - -
      -

      - -

      -
      - ` + selector: 'app-hero-detaill', + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { hero$: Observable; diff --git a/aio/content/examples/router/src/app/heroes/hero-detail.component.ts b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.3.ts similarity index 57% rename from aio/content/examples/router/src/app/heroes/hero-detail.component.ts rename to aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.3.ts index 2170686864..52d670637e 100644 --- a/aio/content/examples/router/src/app/heroes/hero-detail.component.ts +++ b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.3.ts @@ -3,39 +3,19 @@ // #docregion rxjs-operator-import import { switchMap } from 'rxjs/operators'; // #enddocregion rxjs-operator-import -import { Component, OnInit, HostBinding } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; -import { slideInDownAnimation } from '../animations'; - -import { Hero, HeroService } from './hero.service'; +import { HeroService } from '../hero.service'; +import { Hero } from '../hero'; @Component({ - template: ` -

      HEROES

      -
      -

      "{{ hero.name }}"

      -
      - {{ hero.id }}
      -
      - - -
      -

      - -

      -
      - `, - animations: [ slideInDownAnimation ] + selector: 'app-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { -// #docregion host-bindings - @HostBinding('@routeAnimation') routeAnimation = true; - @HostBinding('style.display') display = 'block'; - @HostBinding('style.position') position = 'absolute'; -// #enddocregion host-bindings - hero$: Observable; // #docregion ctor @@ -65,3 +45,9 @@ export class HeroDetailComponent implements OnInit { } // #enddocregion gotoHeroes } + +/* +// #docregion redirect + this.router.navigate(['/superheroes', { id: heroId, foo: 'foo' }]); +// #enddocregion redirect +*/ diff --git a/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.css b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.html b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.html new file mode 100644 index 0000000000..52321987b4 --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.html @@ -0,0 +1,13 @@ +

      HEROES

      +
      +

      "{{ hero.name }}"

      +
      + {{ hero.id }}
      +
      + + +
      +

      + +

      +
      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.ts b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.ts new file mode 100644 index 0000000000..720301d192 --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/hero-detail/hero-detail.component.ts @@ -0,0 +1,41 @@ +// #docplaster +// #docregion +import { switchMap } from 'rxjs/operators'; +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute, ParamMap } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { HeroService } from '../hero.service'; +import { Hero } from '../hero'; + +@Component({ + selector: 'app-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.css'] +}) +export class HeroDetailComponent implements OnInit { + hero$: Observable; + + constructor( + private route: ActivatedRoute, + private router: Router, + private service: HeroService + ) {} + + ngOnInit() { + this.hero$ = this.route.paramMap.pipe( + switchMap((params: ParamMap) => + this.service.getHero(params.get('id'))) + ); + } + + // #docregion redirect + gotoHeroes(hero: Hero) { + let heroId = hero ? hero.id : null; + // Pass along the hero id if available + // so that the HeroList component can select that hero. + // Include a junk 'foo' property for fun. + this.router.navigate(['/superheroes', { id: heroId, foo: 'foo' }]); + } + // #enddocregion redirect +} diff --git a/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.1.html b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.1.html new file mode 100644 index 0000000000..579719d5f4 --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.1.html @@ -0,0 +1,14 @@ + +

      HEROES

      + + + + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/heroes/hero-list.component.1.ts b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.1.ts similarity index 57% rename from aio/content/examples/router/src/app/heroes/hero-list.component.1.ts rename to aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.1.ts index 50ad9b74e1..c34e5ff678 100644 --- a/aio/content/examples/router/src/app/heroes/hero-list.component.1.ts +++ b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.1.ts @@ -5,25 +5,13 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { Hero, HeroService } from './hero.service'; +import { HeroService } from '../hero.service'; +import { Hero } from '../hero'; @Component({ - // #docregion template - template: ` -

      HEROES

      - - - - ` - // #enddocregion template + selector: 'app-hero-list', + templateUrl: './hero-list.component.1.html', + styleUrls: ['./hero-list.component.css'] }) export class HeroListComponent implements OnInit { heroes$: Observable; diff --git a/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.css b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.css new file mode 100644 index 0000000000..a5e62b257b --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.css @@ -0,0 +1,82 @@ +/* HeroListComponent's private CSS styles */ +.heroes { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; +} +.heroes li { + position: relative; + cursor: pointer; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + height: 1.6em; + border-radius: 4px; +} + +.heroes li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; +} + +.heroes a { + color: #888; + text-decoration: none; + position: relative; + display: block; +} + +.heroes a:hover { + color:#607D8B; +} + +.heroes .badge { + display: inline-block; + font-size: small; + color: white; + padding: 0.8em 0.7em 0 0.7em; + background-color: #607D8B; + line-height: 1em; + position: relative; + left: -1px; + top: -4px; + height: 1.8em; + min-width: 16px; + text-align: right; + margin-right: .8em; + border-radius: 4px 0 0 4px; +} + +button { + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; + font-family: Arial; +} + +button:hover { + background-color: #cfd8dc; +} + +button.delete { + position: relative; + left: 194px; + top: -32px; + background-color: gray !important; + color: white; +} + +/* #docregion selected */ +.heroes li.selected { + background-color: #CFD8DC; + color: white; +} +.heroes li.selected:hover { + background-color: #BBD8DC; +} +/* #enddocregion selected */ \ No newline at end of file diff --git a/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.html b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.html new file mode 100644 index 0000000000..282a82e174 --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.html @@ -0,0 +1,11 @@ +

      HEROES

      + + + \ No newline at end of file diff --git a/aio/content/examples/router/src/app/heroes/hero-list.component.ts b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.ts similarity index 57% rename from aio/content/examples/router/src/app/heroes/hero-list.component.ts rename to aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.ts index cbb97c4cf0..b367f7b76b 100644 --- a/aio/content/examples/router/src/app/heroes/hero-list.component.ts +++ b/aio/content/examples/router/src/app/heroes/hero-list/hero-list.component.ts @@ -7,33 +7,21 @@ import { switchMap } from 'rxjs/operators'; // #enddocregion rxjs-imports import { Component, OnInit } from '@angular/core'; // #docregion import-router -import { ActivatedRoute, ParamMap } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; // #enddocregion import-router -import { Hero, HeroService } from './hero.service'; +import { HeroService } from '../hero.service'; +import { Hero } from '../hero'; @Component({ - // #docregion template - template: ` -

      HEROES

      - - - - ` - // #enddocregion template + selector: 'app-hero-list', + templateUrl: './hero-list.component.html', + styleUrls: ['./hero-list.component.css'] }) // #docregion ctor export class HeroListComponent implements OnInit { heroes$: Observable; - - private selectedId: number; + selectedId: number; constructor( private service: HeroService, @@ -42,7 +30,7 @@ export class HeroListComponent implements OnInit { ngOnInit() { this.heroes$ = this.route.paramMap.pipe( - switchMap((params: ParamMap) => { + switchMap(params => { // (+) before `params.get()` turns the string into a number this.selectedId = +params.get('id'); return this.service.getHeroes(); diff --git a/aio/content/examples/router/src/app/heroes/hero.service.ts b/aio/content/examples/router/src/app/heroes/hero.service.ts index 51518a4ae0..2a85f7511c 100644 --- a/aio/content/examples/router/src/app/heroes/hero.service.ts +++ b/aio/content/examples/router/src/app/heroes/hero.service.ts @@ -1,29 +1,31 @@ // #docregion import { Injectable } from '@angular/core'; -import { of } from 'rxjs'; + +import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; -export class Hero { - constructor(public id: number, public name: string) { } -} +import { Hero } from './hero'; +import { HEROES } from './mock-heroes'; +import { MessageService } from '../message.service'; -const HEROES = [ - new Hero(11, 'Mr. Nice'), - new Hero(12, 'Narco'), - new Hero(13, 'Bombasto'), - new Hero(14, 'Celeritas'), - new Hero(15, 'Magneta'), - new Hero(16, 'RubberMan') -]; - -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class HeroService { - getHeroes() { return of(HEROES); } + + constructor(private messageService: MessageService) { } + + getHeroes(): Observable { + // TODO: send the message _after_ fetching the heroes + this.messageService.add('HeroService: fetched heroes'); + return of(HEROES); + } getHero(id: number | string) { return this.getHeroes().pipe( // (+) before `id` turns the string into a number - map(heroes => heroes.find(hero => hero.id === +id)) + map((heroes: Hero[]) => heroes.find(hero => hero.id === +id)) ); } } + diff --git a/aio/content/examples/router/src/app/heroes/hero.ts b/aio/content/examples/router/src/app/heroes/hero.ts new file mode 100644 index 0000000000..e3eac516da --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/aio/content/examples/router/src/app/heroes/heroes-routing.module.1.ts b/aio/content/examples/router/src/app/heroes/heroes-routing.module.1.ts index dbee521793..7a85522fb3 100644 --- a/aio/content/examples/router/src/app/heroes/heroes-routing.module.1.ts +++ b/aio/content/examples/router/src/app/heroes/heroes-routing.module.1.ts @@ -2,8 +2,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { HeroListComponent } from './hero-list.component'; -import { HeroDetailComponent } from './hero-detail.component'; +import { HeroListComponent } from './hero-list/hero-list.component'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, @@ -20,5 +20,5 @@ const heroesRoutes: Routes = [ RouterModule ] }) -export class HeroRoutingModule { } +export class HeroesRoutingModule { } // #enddocregion diff --git a/aio/content/examples/router/src/app/heroes/heroes-routing.module.2.ts b/aio/content/examples/router/src/app/heroes/heroes-routing.module.2.ts new file mode 100644 index 0000000000..d9997c873e --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/heroes-routing.module.2.ts @@ -0,0 +1,22 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { HeroListComponent } from './hero-list/hero-list.component'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; + +const heroesRoutes: Routes = [ + { path: 'heroes', component: HeroListComponent, data: { animation: 'heroes' } }, + { path: 'hero/:id', component: HeroDetailComponent, data: { animation: 'hero' } } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(heroesRoutes) + ], + exports: [ + RouterModule + ] +}) +export class HeroesRoutingModule { } +// #enddocregion diff --git a/aio/content/examples/router/src/app/heroes/heroes-routing.module.ts b/aio/content/examples/router/src/app/heroes/heroes-routing.module.ts index 43558907b0..b059e5d496 100644 --- a/aio/content/examples/router/src/app/heroes/heroes-routing.module.ts +++ b/aio/content/examples/router/src/app/heroes/heroes-routing.module.ts @@ -2,14 +2,14 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { HeroListComponent } from './hero-list.component'; -import { HeroDetailComponent } from './hero-detail.component'; +import { HeroListComponent } from './hero-list/hero-list.component'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', redirectTo: '/superheroes' }, { path: 'hero/:id', redirectTo: '/superhero/:id' }, - { path: 'superheroes', component: HeroListComponent }, - { path: 'superhero/:id', component: HeroDetailComponent } + { path: 'superheroes', component: HeroListComponent, data: { animation: 'heroes' } }, + { path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } } ]; @NgModule({ @@ -20,5 +20,5 @@ const heroesRoutes: Routes = [ RouterModule ] }) -export class HeroRoutingModule { } +export class HeroesRoutingModule { } // #enddocregion diff --git a/aio/content/examples/router/src/app/heroes/heroes.module.ts b/aio/content/examples/router/src/app/heroes/heroes.module.ts index 95ee64a182..13091398ab 100644 --- a/aio/content/examples/router/src/app/heroes/heroes.module.ts +++ b/aio/content/examples/router/src/app/heroes/heroes.module.ts @@ -5,13 +5,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { HeroListComponent } from './hero-list.component'; -import { HeroDetailComponent } from './hero-detail.component'; - -import { HeroService } from './hero.service'; +import { HeroListComponent } from './hero-list/hero-list.component'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; // #enddocregion v1 -import { HeroRoutingModule } from './heroes-routing.module'; +import { HeroesRoutingModule } from './heroes-routing.module'; // #docregion v1 @NgModule({ @@ -19,14 +17,13 @@ import { HeroRoutingModule } from './heroes-routing.module'; CommonModule, FormsModule, // #enddocregion v1 - HeroRoutingModule + HeroesRoutingModule // #docregion v1 ], declarations: [ HeroListComponent, HeroDetailComponent - ], - providers: [ HeroService ] + ] }) export class HeroesModule {} // #enddocregion v1 diff --git a/aio/content/examples/router/src/app/heroes/mock-heroes.ts b/aio/content/examples/router/src/app/heroes/mock-heroes.ts new file mode 100644 index 0000000000..e84c2fd2b0 --- /dev/null +++ b/aio/content/examples/router/src/app/heroes/mock-heroes.ts @@ -0,0 +1,14 @@ +import { Hero } from './hero'; + +export const HEROES: Hero[] = [ + { id: 11, name: 'Mr. Nice' }, + { id: 12, name: 'Narco' }, + { id: 13, name: 'Bombasto' }, + { id: 14, name: 'Celeritas' }, + { id: 15, name: 'Magneta' }, + { id: 16, name: 'RubberMan' }, + { id: 17, name: 'Dynama' }, + { id: 18, name: 'Dr IQ' }, + { id: 19, name: 'Magma' }, + { id: 20, name: 'Tornado' } +]; diff --git a/aio/content/examples/router/src/app/message.service.ts b/aio/content/examples/router/src/app/message.service.ts new file mode 100644 index 0000000000..d72412e115 --- /dev/null +++ b/aio/content/examples/router/src/app/message.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class MessageService { + messages: string[] = []; + + add(message: string) { + this.messages.push(message); + } + + clear() { + this.messages = []; + } +} diff --git a/aio/content/examples/router/src/app/not-found.component.ts b/aio/content/examples/router/src/app/not-found.component.ts deleted file mode 100644 index 2e74544e17..0000000000 --- a/aio/content/examples/router/src/app/not-found.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; - -@Component({ - template: '

      Page not found

      ' -}) -export class PageNotFoundComponent {} diff --git a/aio/content/examples/router/src/app/page-not-found/page-not-found.component.css b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router/src/app/page-not-found/page-not-found.component.html b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.html new file mode 100644 index 0000000000..6c581c4fc8 --- /dev/null +++ b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.html @@ -0,0 +1 @@ +

      Page not found

      \ No newline at end of file diff --git a/aio/content/examples/router/src/app/page-not-found/page-not-found.component.spec.ts b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.spec.ts new file mode 100644 index 0000000000..697a946572 --- /dev/null +++ b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageNotFoundComponent } from './page-not-found.component'; + +describe('PageNotFoundComponent', () => { + let component: PageNotFoundComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PageNotFoundComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PageNotFoundComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/content/examples/router/src/app/page-not-found/page-not-found.component.ts b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.ts new file mode 100644 index 0000000000..c5c55a794d --- /dev/null +++ b/aio/content/examples/router/src/app/page-not-found/page-not-found.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-page-not-found', + templateUrl: './page-not-found.component.html', + styleUrls: ['./page-not-found.component.css'] +}) +export class PageNotFoundComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/aio/content/examples/router/src/app/selective-preloading-strategy.ts b/aio/content/examples/router/src/app/selective-preloading-strategy.service.ts similarity index 82% rename from aio/content/examples/router/src/app/selective-preloading-strategy.ts rename to aio/content/examples/router/src/app/selective-preloading-strategy.service.ts index c2192ec12b..a22ac0227e 100644 --- a/aio/content/examples/router/src/app/selective-preloading-strategy.ts +++ b/aio/content/examples/router/src/app/selective-preloading-strategy.service.ts @@ -3,8 +3,10 @@ import { Injectable } from '@angular/core'; import { PreloadingStrategy, Route } from '@angular/router'; import { Observable, of } from 'rxjs'; -@Injectable() -export class SelectivePreloadingStrategy implements PreloadingStrategy { +@Injectable({ + providedIn: 'root', +}) +export class SelectivePreloadingStrategyService implements PreloadingStrategy { preloadedModules: string[] = []; preload(route: Route, load: () => Observable): Observable { diff --git a/aio/content/examples/router/src/index.html b/aio/content/examples/router/src/index.html index 0e5ca721fa..edb919d1ec 100644 --- a/aio/content/examples/router/src/index.html +++ b/aio/content/examples/router/src/index.html @@ -9,7 +9,6 @@ Angular Router - diff --git a/aio/content/examples/router/stackblitz.json b/aio/content/examples/router/stackblitz.json index c1f330ae39..4ffcac610b 100644 --- a/aio/content/examples/router/stackblitz.json +++ b/aio/content/examples/router/stackblitz.json @@ -4,8 +4,8 @@ "!**/*.d.ts", "!**/*.js", "!**/*.[0-9].*", - "!src/app/crisis-list.component.ts", - "!src/app/hero-list.component.ts" + "!src/app/crisis-list/*.*", + "!src/app/hero-list/*.*" ], "tags": ["router"] } diff --git a/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.avoid.ts b/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.avoid.ts index 8bce611048..89e5d1b010 100644 --- a/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.avoid.ts +++ b/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.avoid.ts @@ -23,7 +23,10 @@ import { Hero, HeroService } from './shared'; `, styles: [` .heroes { - margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; } .heroes li { cursor: pointer; diff --git a/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.css b/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.css index 82f0c1d0ab..f042cd85f7 100644 --- a/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.css +++ b/aio/content/examples/styleguide/src/05-04/app/heroes/heroes.component.css @@ -1,6 +1,9 @@ /* #docregion */ .heroes { - margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; } .heroes li { cursor: pointer; diff --git a/aio/content/examples/testing/src/app/app-routing.module.ts b/aio/content/examples/testing/src/app/app-routing.module.ts index f9fd0bdc83..7630c216f6 100644 --- a/aio/content/examples/testing/src/app/app-routing.module.ts +++ b/aio/content/examples/testing/src/app/app-routing.module.ts @@ -8,7 +8,7 @@ import { AboutComponent } from './about/about.component'; RouterModule.forRoot([ { path: '', redirectTo: 'dashboard', pathMatch: 'full'}, { path: 'about', component: AboutComponent }, - { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'} + { path: 'heroes', loadChildren: './hero/hero.module#HeroModule'} ]) ], exports: [ RouterModule ] // re-export the module declarations diff --git a/aio/content/examples/testing/src/app/demo/async-helper.spec.ts b/aio/content/examples/testing/src/app/demo/async-helper.spec.ts index 9bf42ed33f..9c9c3cb7d8 100644 --- a/aio/content/examples/testing/src/app/demo/async-helper.spec.ts +++ b/aio/content/examples/testing/src/app/demo/async-helper.spec.ts @@ -1,8 +1,7 @@ // tslint:disable-next-line:no-unused-variable import { async, fakeAsync, tick } from '@angular/core/testing'; - -import { of } from 'rxjs'; -import { delay } from 'rxjs/operators'; +import { interval, of } from 'rxjs'; +import { delay, take } from 'rxjs/operators'; describe('Angular async helper', () => { let actuallyDone = false; @@ -21,49 +20,120 @@ describe('Angular async helper', () => { }); it('should run async test with task', - async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); + async(() => { setTimeout(() => { actuallyDone = true; }, 0); })); + + it('should run async test with task', async(() => { + const id = setInterval(() => { + actuallyDone = true; + clearInterval(id); + }, 100); + })); it('should run async test with successful promise', async(() => { - const p = new Promise(resolve => { setTimeout(resolve, 10); }); - p.then(() => { actuallyDone = true; }); - })); + const p = new Promise(resolve => { setTimeout(resolve, 10); }); + p.then(() => { actuallyDone = true; }); + })); it('should run async test with failed promise', async(() => { - const p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); - p.catch(() => { actuallyDone = true; }); - })); + const p = new Promise((resolve, reject) => { setTimeout(reject, 10); }); + p.catch(() => { actuallyDone = true; }); + })); - // Use done. Cannot use setInterval with async or fakeAsync - // See https://github.com/angular/angular/issues/10127 + // Use done. Can also use async or fakeAsync. it('should run async test with successful delayed Observable', (done: DoneFn) => { - const source = of(true).pipe(delay(10)); - source.subscribe( - val => actuallyDone = true, - err => fail(err), - done - ); + const source = of (true).pipe(delay(10)); + source.subscribe(val => actuallyDone = true, err => fail(err), done); }); - // Cannot use setInterval from within an async zone test - // See https://github.com/angular/angular/issues/10127 - // xit('should run async test with successful delayed Observable', async(() => { - // const source = of(true).pipe(delay(10)); - // source.subscribe( - // val => actuallyDone = true, - // err => fail(err) - // ); - // })); + // #docregion fake-async-test-tick + it('should run timeout callback with delay after call tick with millis', fakeAsync(() => { + let called = false; + setTimeout(() => { called = true; }, 100); + tick(100); + expect(called).toBe(true); + })); + // #enddocregion fake-async-test-tick - // // Fail message: Error: 1 periodic timer(s) still in the queue - // // See https://github.com/angular/angular/issues/10127 - // xit('should run async test with successful delayed Observable', fakeAsync(() => { - // const source = of(true).pipe(delay(10)); - // source.subscribe( - // val => actuallyDone = true, - // err => fail(err) - // ); + // #docregion fake-async-test-date + it('should get Date diff correctly in fakeAsync', fakeAsync(() => { + const start = Date.now(); + tick(100); + const end = Date.now(); + expect(end - start).toBe(100); + })); + // #enddocregion fake-async-test-date - // tick(); - // })); + // #docregion fake-async-test-rxjs + it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => { + // need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async' + // to patch rxjs scheduler + let result = null; + of ('hello').pipe(delay(1000)).subscribe(v => { result = v; }); + expect(result).toBeNull(); + tick(1000); + expect(result).toBe('hello'); + + const start = new Date().getTime(); + let dateDiff = 0; + interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start)); + + tick(1000); + expect(dateDiff).toBe(1000); + tick(1000); + expect(dateDiff).toBe(2000); + })); + // #enddocregion fake-async-test-rxjs + + // #docregion fake-async-test-clock + describe('use jasmine.clock()', () => { + // need to config __zone_symbol__fakeAsyncPatchLock flag + // before loading zone.js/dist/zone-testing + beforeEach(() => { jasmine.clock().install(); }); + afterEach(() => { jasmine.clock().uninstall(); }); + it('should auto enter fakeAsync', () => { + // is in fakeAsync now, don't need to call fakeAsync(testFn) + let called = false; + setTimeout(() => { called = true; }, 100); + jasmine.clock().tick(100); + expect(called).toBe(true); + }); + }); + // #enddocregion fake-async-test-clock + + // #docregion async-test-promise-then + describe('test jsonp', () => { + function jsonp(url: string, callback: Function) { + // do a jsonp call which is not zone aware + } + // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag + // before loading zone.js/dist/zone-testing + it('should wait until promise.then is called', async(() => { + let finished = false; + new Promise((res, rej) => { + jsonp('localhost:8080/jsonp', () => { + // success callback and resolve the promise + finished = true; + res(); + }); + }).then(() => { + // async will wait until promise.then is called + // if __zone_symbol__supportWaitUnResolvedChainedPromise is set + expect(finished).toBe(true); + }); + })); + }); + // #enddocregion async-test-promise-then + + it('should run async test with successful delayed Observable', async(() => { + const source = of (true).pipe(delay(10)); + source.subscribe(val => actuallyDone = true, err => fail(err)); + })); + + it('should run async test with successful delayed Observable', fakeAsync(() => { + const source = of (true).pipe(delay(10)); + source.subscribe(val => actuallyDone = true, err => fail(err)); + + tick(10); + })); }); diff --git a/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts b/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts new file mode 100644 index 0000000000..eacb67eb82 --- /dev/null +++ b/aio/content/examples/testing/src/app/shared/canvas.component.spec.ts @@ -0,0 +1,27 @@ +import { TestBed, async, tick, fakeAsync } from '@angular/core/testing'; +import { CanvasComponent } from './canvas.component'; +describe('CanvasComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + CanvasComponent + ], + }).compileComponents(); + })); + beforeEach(() => { + window['__zone_symbol__FakeAsyncTestMacroTask'] = [ + { + source: 'HTMLCanvasElement.toBlob', + callbackArgs: [{ size: 200 }] + } + ]; + }); + it('should be able to generate blob data from canvas', fakeAsync(() => { + const fixture = TestBed.createComponent(CanvasComponent); + fixture.detectChanges(); + tick(); + const app = fixture.debugElement.componentInstance; + expect(app.blobSize).toBeGreaterThan(0); + })); +}); + diff --git a/aio/content/examples/testing/src/app/shared/canvas.component.ts b/aio/content/examples/testing/src/app/shared/canvas.component.ts new file mode 100644 index 0000000000..0f32dbeeb6 --- /dev/null +++ b/aio/content/examples/testing/src/app/shared/canvas.component.ts @@ -0,0 +1,25 @@ +import { Component, AfterViewInit, ViewChild } from '@angular/core'; + +@Component({ + selector: 'sample-canvas', + template: '' +}) +export class CanvasComponent implements AfterViewInit { + blobSize: number; + @ViewChild('sampleCanvas') sampleCanvas; + + constructor() { } + + ngAfterViewInit() { + const canvas = this.sampleCanvas.nativeElement; + const context = canvas.getContext('2d'); + if (context) { + context.clearRect(0, 0, 200, 200); + context.fillStyle = '#FF1122'; + context.fillRect(0, 0, 200, 200); + canvas.toBlob((blob: any) => { + this.blobSize = blob.size; + }); + } + } +} diff --git a/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.ts b/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.ts index bee5856586..f367a9baa0 100644 --- a/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.ts +++ b/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.ts @@ -12,16 +12,17 @@ import { HEROES } from '../mock-heroes'; styleUrls: ['./heroes.component.css'] }) // #enddocregion metadata + +// #docregion component export class HeroesComponent implements OnInit { // #docregion heroes heroes = HEROES; // #enddocregion heroes - + // #enddocregion component // #docregion on-select selectedHero: Hero; - - // #enddocregion on-select + // #enddocregion on-select constructor() { } diff --git a/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.html b/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.html index 946a4d5fdb..ea2387fdc1 100644 --- a/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.html +++ b/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.html @@ -3,9 +3,9 @@ -

      {{hero.name}}

      + diff --git a/aio/content/examples/toh-pt5/src/app/hero-detail/hero-detail.component.ts b/aio/content/examples/toh-pt5/src/app/hero-detail/hero-detail.component.ts index 637ba05f66..43d1082470 100644 --- a/aio/content/examples/toh-pt5/src/app/hero-detail/hero-detail.component.ts +++ b/aio/content/examples/toh-pt5/src/app/hero-detail/hero-detail.component.ts @@ -1,6 +1,6 @@ // #docplaster // #docregion -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; // #docregion added-imports import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; @@ -17,7 +17,7 @@ import { HeroService } from '../hero.service'; styleUrls: [ './hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { - @Input() hero: Hero; + hero: Hero; // #docregion ctor constructor( diff --git a/aio/content/examples/toh-pt6/src/app/app.module.ts b/aio/content/examples/toh-pt6/src/app/app.module.ts index 9770300706..69afe100a8 100644 --- a/aio/content/examples/toh-pt6/src/app/app.module.ts +++ b/aio/content/examples/toh-pt6/src/app/app.module.ts @@ -3,7 +3,9 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; +// #docregion import-http-client import { HttpClientModule } from '@angular/common/http'; +// #enddocregion import-http-client // #docregion import-in-mem-stuff import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; diff --git a/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts b/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts index 005545c19a..9679f21055 100644 --- a/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts +++ b/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts @@ -1,5 +1,6 @@ // #docregion , init import { InMemoryDbService } from 'angular-in-memory-web-api'; +import { Hero } from './hero'; export class InMemoryDataService implements InMemoryDbService { createDb() { @@ -17,4 +18,13 @@ export class InMemoryDataService implements InMemoryDbService { ]; return {heroes}; } + + // Overrides the genId method to ensure that a hero always has an id. + // If the heroes array is empty, + // the method below returns the initial number (11). + // if the heroes array is not empty, the method below returns the highest + // hero id + 1. + genId(heroes: Hero[]): number { + return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11; + } } diff --git a/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts index 5745fb8677..25f8d6d9e5 100644 --- a/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts +++ b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; @@ -11,7 +11,7 @@ import { HeroService } from '../hero.service'; styleUrls: [ './hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { - @Input() hero: Hero; + hero: Hero; constructor( private route: ActivatedRoute, diff --git a/aio/content/guide/ajs-quick-reference.md b/aio/content/guide/ajs-quick-reference.md index 3f639e6783..4a9755f9d8 100644 --- a/aio/content/guide/ajs-quick-reference.md +++ b/aio/content/guide/ajs-quick-reference.md @@ -262,11 +262,10 @@ AngularJS 为模板提供了七十多个内置指令。 ### 引导 - - +
      - + Angular doesn't have a bootstrap directive. To launch the app in code, explicitly bootstrap the application's root module (`AppModule`) diff --git a/aio/content/guide/animations.md b/aio/content/guide/animations.md index 2bf9f4ae04..e266c72f9f 100644 --- a/aio/content/guide/animations.md +++ b/aio/content/guide/animations.md @@ -1,563 +1,334 @@ -# Animations +# Introduction to Angular animations -# 动画 +Animation provides the illusion of motion: HTML elements change styling over time. Well-designed animations can make your application more fun and easier to use, but they aren't just cosmetic. Animations can improve your app and user experience in a number of ways: -Motion is an important aspect in the design of modern web applications. Good -user interfaces transition smoothly between states with engaging animations -that call attention where it's needed. Well-designed animations can make a UI not only -more fun but also easier to use. +* Without animations, web page transitions can seem abrupt and jarring. -动画是现代 Web 应用设计中一个很重要的方面。好的用户界面要能在不同的状态之间更平滑的转场。如果需要,还可以用适当的动画来吸引注意力。 -设计良好的动画不但会让 UI 更有趣,还会让它更容易使用。 +* Motion greatly enhances the user experience, so animations give users a chance to detect the application's response to their actions. -## Overview +* Good animations intuitively call the user's attention to where it is needed. -## 概览 +Typically, animations involve multiple style *transformations* over time. An HTML element can move, change color, grow or shrink, fade, or slide off the page. These changes can occur simultaneously or sequentially. You can control the timing of each transformation. -Angular's animation system lets you build animations that run with the same kind of native -performance found in pure CSS animations. You can also tightly integrate your -animation logic with the rest of your application code, for ease of control. +Angular's animation system is built on CSS functionality, which means you can animate any property that the browser considers animatable. This includes positions, sizes, transforms, colors, borders, and more. The W3C maintains a list of animatable properties on its [CSS Transitions](https://www.w3.org/TR/css-transitions-1/) page. -Angular 的动画系统赋予了制作各种动画效果的能力,以构建出与原生 CSS 动画性能相同的动画。 -你还获得了额外的让动画逻辑与其它应用代码紧紧集成在一起的能力,这让动画可以被更容易的触发与控制。 -
      +## About this guide -Angular animations are built on top of the standard [Web Animations API](https://w3c.github.io/web-animations/) -and run natively on [browsers that support it](http://caniuse.com/#feat=web-animation). +This guide covers the basic Angular animation features to get you started on adding Angular animations to your project. -Angular 动画是基于标准的[Web 动画 API(Web Animations API)](https://w3c.github.io/web-animations/)构建的,它们在[支持此 API 的浏览器中](http://caniuse.com/#feat=web-animation)会用原生方式工作。 +The features described in this guide — and the more advanced features described in the related Angular animations guides — are demonstrated in an example app available as a . -As of Angular 6, If the Web Animations API is not supported natively by the browser, then Angular will use CSS -keyframes as a fallback instead (automatically). This means that the polyfill is no longer required unless any -code uses [AnimationBuilder](/api/animations/AnimationBuilder). If your code does use AnimationBuilder, then -uncomment the `web-animations-js` polyfill from the `polyfills.ts` file generated by Angular CLI. +#### Prerequisites -对于 Angular 6,如果浏览器没有提供对 Web 动画 API 的原生支持,Angular 就会自动改用 CSS 的关键帧动画作为后备实现。这意味,除非要在代码中使用 [AnimationBuilder](/api/animations/AnimationBuilder) ,否则不必使用相关的腻子脚本。 -如果你要在代码中使用 AnimationBuilder ,就要从 Angular CLI 自动生成的 `polyfills.ts` 文件中反注释掉 `web-animations-js` 腻子脚本。 +The guide assumes that you're familiar with building basic Angular apps, as described in the following sections: -
      +* [Tutorial](tutorial) +* [Architecture Overview](guide/architecture) -
      -The examples in this page are available as a . +## Getting started -本章中引用的这个例子可以到去体验。 +The main Angular modules for animations are `@angular/animations` and `@angular/platform-browser`. When you create a new project using the CLI, these dependencies are automatically added to your project. -
      +To get started with adding Angular animations to your project, import the animation-specific modules along with standard Angular functionality. -## Setup +### Step 1: Enabling the animations module -## 准备工作 +Import `BrowserAnimationsModule`, which introduces the animation capabilities into your Angular root application module. -Before you can add animations to your application, you need -to import a few animation-specific modules and functions to the root application module. - -在往应用中添加动画之前,你要首先在应用的根模块中引入一些与动画有关的模块和函数。 - - - -#### Example basics - -#### 基本例子 - -The animations examples in this guide animate a list of heroes. - -这里的动画例子用来给英雄列表添加动画。 - -A `Hero` class has a `name` property, a `state` property that indicates if the hero is active or not, -and a `toggleState()` method to switch between the states. - -`Hero` 类有一个 `name` 属性、一个 `state` 属性(用于表明该英雄是否为激活状态)和一个 `toggleState()` 函数,用来在这两种状态之间切换。 - - - -Across the top of the screen (`app.hero-team-builder.component.ts`) -are a series of buttons that add and remove heroes from the list (via the `HeroService`). -The buttons trigger changes to the list that all of the example components see at the same time. - -在屏幕的顶部(`app.hero-team-builder.component.ts`)是一系列按钮,用于从列表中添加和删除英雄(通过 `HeroService`)。 -这些按钮会引起列表的变化,同时可以看到列表中的所有范例组件。 - -{@a example-transitioning-between-states} - -## Transitioning between two states - -## 快速起步范例:在两个状态间转场 - -A simple transition animation - -You can build a simple animation that transitions an element between two states -driven by a model attribute. - -你可以构建一个简单的动画,它会让一个元素用模型驱动的方式在两个状态之间转场。 - -Animations can be defined inside `@Component` metadata. - -动画会被定义在 `@Component` 元数据中。 - - - -With these, you can define an *animation trigger* called `heroState` in the component -metadata. It uses animations to transition between two states: `active` and `inactive`. When a -hero is active, the element appears in a slightly larger size and lighter color. - -通过这些,可以在组件元数据中定义一个名叫 `heroState` 的*动画触发器*。它在两个状态 `active` 和 `inactive` 之间进行转场。 -当英雄处于激活状态时,它会把该元素显示得稍微大一点、亮一点。 - - - -
      - -In this example, you are defining animation styles (color and transform) inline in the -animation metadata. - -在这个例子中,你在元数据中用内联的方式定义了动画样式(`color` 和 `transform`)。在即将到来的一个 Angular 版本中,还将支持从组件的 CSS 样式表中提取样式。 - -
      - -Now, using the `[@triggerName]` syntax, attach the animation that you just defined to -one or more elements in the component's template. - -现在,使用 `[@triggerName]` 语法来把刚刚定义的动画附加到组件模板中一个或多个元素上。 - - - -Here, the animation trigger applies to every element repeated by an `ngFor`. Each of -the repeated elements animates independently. The value of the -attribute is bound to the expression `hero.state` and is always either `active` or `inactive`. - -这里,动画触发器被添加到了由 `ngFor` 重复出来的每一个元素上。每个重复出来的元素都有独立的动画效果。 -然后把 `@triggerName` 属性(Attribute)的值设置成表达式 `hero.state`。这个值应该是 `inactive` 或 `active` 之一。 - -With this setup, an animated transition appears whenever a hero object changes state. -Here's the full component implementation: - -通过这些设置,一旦英雄对象的状态发生了变化,就会触发一个转场动画。下面是完整的组件实现: - - - -## States and transitions - -## 状态与转场 - -Angular animations are defined as logical **states** and **transitions** -between states. - -Angular 动画是由**状态**和**状态之间的转场效果**所定义的。 - -An animation state is a string value that you define in your application code. In the example -above, the states `'active'` and `'inactive'` are based on the logical state of -hero objects. The source of the state can be a simple object attribute, as it was in this case, -or it can be a value computed in a method. The important thing is that you can read it into the -component's template. - -动画状态是一个由程序代码中定义的字符串值。在上面的例子中,`'active'` 和 `'inactive'` 是基于英雄对象的逻辑状态的。 -状态的来源可以是像本例中这样简单的对象属性,也可以是由方法计算出来的值。重点是,你要能从组件模板中读取它。 - -You can define *styles* for each animation state: - -你可以为每个动画状态定义了*一组样式*: - - - -These `state` definitions specify the *end styles* of each state. -They are applied to the element once it has transitioned to that state, and stay -*as long as it remains in that state*. In effect, you're defining what styles the element has in different states. - -这些 `state` 具体定义了每个状态的*最终样式*。一旦元素转场到那个状态,该样式就会被应用到此元素上,*当它留在此状态时*,这些样式也会一直保持着。 -从这个意义上讲,这里其实并不只是在定义动画,而是在定义该元素在不同状态时应该具有的样式。 - -After you define states, you can define *transitions* between the states. Each transition -controls the timing of switching between one set of styles and the next: - -定义完状态,就能定义在状态之间的各种*转场*了。每个转场都会控制一条在一组样式和下一组样式之间切换的时间线: - - - -
      - In Angular animations you define states and transitions between states -
      - -If several transitions have the same timing configuration, you can combine -them into the same `transition` definition: - -如果多个转场都有同样的时间线配置,就可以把它们合并进同一个 `transition` 定义中: - - - -When both directions of a transition have the same timing, as in the previous -example, you can use the shorthand syntax `<=>`: - -如果要对同一个转场的两个方向都使用相同的时间线(就像前面的例子中那样),就可以使用 `<=>` 这种简写语法: - - - -You can also apply a style during an animation but not keep it around -after the animation finishes. You can define such styles inline, in the `transition`. In this example, -the element receives one set of styles immediately and is then animated to the next. -When the transition finishes, none of these styles are kept because they're not -defined in a `state`. - -有时希望一些样式只在动画期间生效,但在结束后并不保留它们。这时可以把这些样式内联在 `transition` 中进行定义。 -在这个例子中,该元素会立刻获得一组样式,然后动态转场到下一个状态。当转场结束时,这些样式并不会被保留,因为它们并没有被定义在 `state` 中。 - - - -### The wildcard state `*` - -### `*`(通配符)状态 - -The `*` ("wildcard") state matches *any* animation state. This is useful for defining styles and -transitions that apply regardless of which state the animation is in. For example: - -`*`(通配符)状态匹配*任何*动画状态。当定义那些不需要管当前处于什么状态的样式及转场时,这很有用。比如: - -* The `active => *` transition applies when the element's state changes from `active` to anything else. - - 当该元素的状态从 `active` 变成任何其它状态时,`active => *` 转场都会生效。 - -* The `* => *` transition applies when *any* change between two states takes place. - - 当在*任意*两个状态之间切换时,`* => *` 转场都会生效。 - -
      - The wildcard state can be used to match many different transitions at once -
      - -### The `void` state - -### `void` 状态 - -The special state called `void` can apply to any animation. It applies -when the element is *not* attached to a view, perhaps because it has not yet been -added or because it has been removed. The `void` state is useful for defining enter and -leave animations. - -有一种叫做 `void` 的特殊状态,它可以应用在任何动画中。它表示元素*没有*被附加到视图。这种情况可能是由于它尚未被添加进来或者已经被移除了。 -`void` 状态在定义“进场”和“离场”的动画时会非常有用。 - -For example the `* => void` transition applies when the element leaves the view, -regardless of what state it was in before it left. - -比如当一个元素离开视图时,`* => void` 转场就会生效,而不管它在离场以前是什么状态。 - -
      - The void state can be used for enter and leave transitions -
      - -The wildcard state `*` also matches `void`. - -`*` 通配符状态也能匹配 `void`。 - -## Example: Entering and leaving - -## 例子:进场与离场 - -Enter and leave animations - -Using the `void` and `*` states you can define transitions that animate the -entering and leaving of elements: - -使用 `void` 和 `*` 状态,可以定义元素进场与离场时的转场动画: - -* Enter: `void => *` - - 进场:`void => *` - -* Leave: `* => void` - - 离场:`* => void` - -For example, in the `animations` array below there are two transitions that use -the `void => *` and `* => void` syntax to animate the element in and out of the view. - -例如,在下面的 `animations` 数组中,这两个转场语句使用 `void => *` 和 `* => void` 语法来让该元素以动画形式进入和离开当前视图。 - - - -Note that in this case the styles are applied to the void state directly in the -transition definitions, and not in a separate `state(void)` definition. Thus, the transforms -are different on enter and leave: the element enters from the left -and leaves to the right. - -注意,在这个例子中,这些样式在转场定义中被直接应用到了 `void` 状态,但并没有一个单独的 `state(void)` 定义。 -这么做是因为希望在进场与离场时使用不一样的转换效果:元素从左侧进场,从右侧离开。 - -
      - -These two common animations have their own aliases: - -这两个常见的动画有自己的别名: - - - transition(':enter', [ ... ]); // void => * - transition(':leave', [ ... ]); // * => void + +
      + +**Note:** When you use the CLI to create your app, the root application module `app.module.ts` is placed in the `src/app` folder.
      -## Example: Entering and leaving from different states +### Step 2: Importing animation functions into component files -## 范例:从不同的状态下进场和离场 +If you plan to use specific animation functions in component files, import those functions from `@angular/animations`. -Enter and leave animations combined with state animations + + -You can also combine this animation with the earlier state transition animation by -using the hero state as the animation state. This lets you configure -different transitions for entering and leaving based on what the state of the hero -is: +
      -通过把英雄的状态用作动画的状态,还能把该动画跟以前的转场动画组合成一个复合动画。这让你能根据该英雄的当前状态为其配置不同的进场与离场动画: +**Note:** See a [summary of available animation functions](guide/animations#animation-api-summary) at the end of this guide. +
      -* Inactive hero enter: `void => inactive` +### Step 3: Adding the animation metadata property - 非激活英雄进场:`void => inactive` +In the component file, add a metadata property called `animations:` within the `@Component()` decorator. You put the trigger that defines an animation within the `animations` metadata property. -* Active hero enter: `void => active` + + - 激活英雄进场:`void => active` +## Animating a simple transition -* Inactive hero leave: `inactive => void` +Let's animate a simple transition that changes a single HTML element from one state to another. For example, you can specify that a button displays either **Open** or **Closed** based on the user's last action. When the button is in the `open` state, it's visible and yellow. When it's the `closed` state, it's transparent and green. - 非激活英雄离场:`inactive => void` - -* Active hero leave: `active => void` - - 激活英雄离场:`active => void` - -This gives you fine-grained control over each transition: - -现在就对每一种转场都有了细粒度的控制: +In HTML, these attributes are set using ordinary CSS styles such as color and opacity. In Angular, use the `style()` function to specify a set of CSS styles for use with animations. You can collect a set of styles in an animation state, and give the state a name, such as `open` or `closed`.
      - This example transitions between active, inactive, and void states +open and closed states
      - +### Animation state and styles -## Animatable properties and units +Use Angular's `state()` function to define different states to call at the end of each transition. This function takes two arguments: a unique name like `open` or `closed` and a `style()` function. -## 可动的(Animatable)属性与单位 +Use the `style()` function to define a set of styles to associate with a given state name. Note that the style attributes must be in [*camelCase*](guide/glossary#case-conventions). -Since Angular's animation support builds on top of Web Animations, you can animate any property -that the browser considers *animatable*. This includes positions, sizes, transforms, colors, -borders, and many others. The W3C maintains -[a list of animatable properties](https://www.w3.org/TR/css3-transitions/#animatable-properties) -on its [CSS Transitions page](https://www.w3.org/TR/css3-transitions). +Let's see how Angular's `state()` function works with the `style⁣­(⁠)` function to set CSS style attributes. In this code snippet, multiple style attributes are set at the same time for the state. In the `open` state, the button has a height of 200 pixels, an opacity of 1, and a background color of yellow. -由于 Angular 的动画支持是基于 Web Animations 标准的,所以也能支持浏览器认为可以*参与动画*的任何属性。这些属性包括位置(position)、大小(size)、变换(transform)、颜色(color)、边框(border)等很多属性。W3C 维护着 -[一个“可动”属性列表](https://www.w3.org/TR/css3-transitions/#animatable-properties)。 + + -For positional properties that have a numeric value, you can define a unit by providing -the value as a string with the appropriate suffix: +In the `closed` state, shown below, the button has a height of 100 pixels, an opacity of 0.5, and a background color of green. -尺寸类属性(如位置、大小、边框等)包括一个数字值和一个用来定义长度单位的后缀: + + -* `'50px'` +### Transitions and timing -* `'3em'` +In Angular, you can set multiple styles without any animation. However, without further refinement, the button instantly transforms with no fade, no shrinkage, or other visible indicator that a change is occurring. -* `'100%'` +To make the change less abrupt, we need to define an animation *transition* to specify the changes that occur between one state and another over a period of time. The `transition()` function accepts two arguments: the first argument accepts an expression that defines the direction between two transition states, and the second argument accepts an `animate()` function. -If you don't provide a unit when specifying dimension, Angular assumes the default of `px`: -对大多数尺寸类属性而言,还能只定义一个数字,那就表示它使用的是像素(px)数: +Use the `animate()` function to define the length, delay, and easing of a transition, and to designate the style function for defining styles while transitions are taking place. You can also use the `animate()` function to define the `keyframes()` function for multi-step animations. These definitions are placed in the second argument of the `animate()` function. -* `50` is the same as saying `'50px'` +#### Animation metadata: duration, delay, and easing - `50` 相当于 `'50px'` +The `animate()` function (second argument of the transition function) accepts the `timings` and `styles` input parameters. -## Automatic property calculation +The `timings` parameter takes a string defined in three parts. -## 自动属性值计算 +>`animate ('duration delay easing')` -Animation with automated height calculation - -Sometimes you don't know the value of a dimensional style property until runtime. -For example, elements often have widths and heights that -depend on their content and the screen size. These properties are often tricky -to animate with CSS. - -有时候,你在开始运行之前都无法知道某个样式属性的值。比如,元素的宽度和高度往往依赖于它们的内容和屏幕的尺寸。处理这些属性对 CSS 动画而言通常是相当棘手的。 - -In these cases, you can use a special `*` property value so that the value of the -property is computed at runtime and then plugged into the animation. - -如果用 Angular 动画,就可以用一个特殊的 `*` 属性值来处理这种情况。该属性的值将会在运行期被计算出来,然后插入到这个动画中。 - -In this example, the leave animation takes whatever height the element has before it -leaves and animates from that height to zero: - -这个例子中的“离场”动画会取得该元素在离场前的高度,并且把它从这个高度用动画转场到 0 高度: - - - -## Animation timing - -## 动画时间线 - -There are three timing properties you can tune for every animated transition: -the duration, the delay, and the easing function. They are all combined into -a single transition *timing string*. - -对每一个动画转场效果,有三种时间线属性可以调整:持续时间(duration)、延迟(delay)和缓动(easing)函数。它们被合并到了一个单独的*转场时间线字符串*。 - -### Duration - -### 持续时间 - -The duration controls how long the animation takes to run from start to finish. -You can define a duration in three ways: - -持续时间控制动画从开始到结束要花多长时间。可以用三种方式定义持续时间: +The first part, `duration`, is required. The duration can be expressed in milliseconds as a simple number without quotes, or in seconds with quotes and a time specifier. For example, a duration of a tenth of a second can be expressed as follows: * As a plain number, in milliseconds: `100` - 作为一个普通数字,以毫秒为单位,如:`100` - * In a string, as milliseconds: `'100ms'` - 作为一个字符串,以毫秒为单位,如:`'100ms'` - * In a string, as seconds: `'0.1s'` - 作为一个字符串,以秒为单位,如:`'0.1s'` - -### Delay - -### 延迟 - -The delay controls the length of time between the animation trigger and the beginning -of the transition. You can define one by adding it to the same string -following the duration. It also has the same format options as the duration: - -延迟控制的是在动画已经触发但尚未真正开始转场之前要等待多久。可以把它添加到字符串中的持续时间后面,它的选项格式也跟持续时间是一样的: +The second argument, `delay`, has the same syntax as `duration`. For example: * Wait for 100ms and then run for 200ms: `'0.2s 100ms'` - 等待 100 毫秒,然后运行 200 毫秒:`'0.2s 100ms'`。 +The third argument, `easing`, controls how the animation [accelerates and decelerates](http://easings.net/) during its runtime. For example, `ease-in` causes the animation to begin slowly, and to pick up speed as it progresses. -### Easing +* Wait for 100ms, run for 200ms. Use a deceleration curve to start out fast and slowly decelerate to a resting point: `'0.2s 100ms ease-out'` -### 缓动函数 +* Run for 200ms, with no delay. Use a standard curve to start slow, accelerate in the middle, and then decelerate slowly at the end: `'0.2s ease-in-out'` -The [easing function](http://easings.net/) controls how the animation accelerates -and decelerates during its runtime. For example, an `ease-in` function causes -the animation to begin relatively slowly but pick up speed as it progresses. You -can control the easing by adding it as a *third* value in the string after the duration -and the delay (or as the *second* value when there is no delay): +* Start immediately, run for 200ms. Use a acceleration curve to start slow and end at full velocity: `'0.2s ease-in'` -[缓动函数](http://easings.net/)用于控制动画在运行期间如何加速和减速。比如:使用 `ease-in` 函数意味着动画开始时相对缓慢,然后在进行中逐步加速。可以通过在这个字符串中的持续时间和延迟后面添加*第三个*值来控制使用哪个缓动函数(如果没有定义延迟就作为*第二个*值)。 +
      -* Wait for 100ms and then run for 200ms, with easing: `'0.2s 100ms ease-out'` +**Note:** See the Angular Material Design website's topic on [Natural easing curves](https://material.io/design/motion/speed.html#easing) for general information on easing curves. +
      - 等待 100 毫秒,然后运行 200 毫秒,并且带缓动:`'0.2s 100ms ease-out'` +This example provides a state transition from `open` to `closed` with a one second transition between states. -* Run for 200ms, with easing: `'0.2s ease-in-out'` + + - 运行 200 毫秒,并且带缓动:`'0.2s ease-in-out'` +In the code snippet above, the `=>` operator indicates unidirectional transitions, and `<=>` is bidirectional. Within the transition, `animate()` specifies how long the transition takes. In this case, the state change from `open` to `closed` takes one second, expressed here as `1s`. -Animations with specific timings +This example adds a state transition from the `closed` state to the `open` state with a 0.5 second transition animation arc. -### Example + + -### 例子 +
      -Here are a couple of custom timings in action. Both enter and leave last for -200 milliseconds, that is `0.2s`, but they have different easings. The leave begins after a -slight delay of 10 milliseconds as specified in `'0.2s 10 ease-out'`: +**Note:** Some additional notes on using styles within `state` and `transition` functions. -这里是两个自定义时间线的动态演示。“进场”和“离场”都持续 200 毫秒,也就是 `0.2s`,但它们有不同的缓动函数。“离场”动画会在 100 毫秒的延迟之后开始,也就是 `'0.2s 10 ease-out'`: +* Use `state()` to define styles that are applied at the end of each transition, they persist after the animation has completed. - +* Use `transition()` to define intermediate styles, which create the illusion of motion during the animation. -## Multi-step animations with keyframes +* When animations are disabled, `transition()` styles can be skipped, but `state()` styles can't. -## 基于关键帧(Keyframes)的多阶段动画 +* You can include multiple state pairs within the same `transition()` argument:
      `transition( 'on => off, off => void' )`. +
      -Animations with some bounce implemented with keyframes +### Triggering the animation -Animation *keyframes* go beyond a simple transition to a more intricate animation -that goes through one or more intermediate styles when transitioning between two sets of styles. +An animation requires a *trigger*, so that it knows when to start. The `trigger()` function collects the states and transitions, and gives the animation a name, so that you can attach it to the triggering element in the HTML template. -通过定义动画的*关键帧*,可以把两组样式之间的简单转场,升级成一种更复杂的动画,它会在转场期间经历一个或多个中间样式。 +The `trigger()` function describes the property name to watch for changes. When a change occurs, the trigger initiates the actions included in its definition. These actions can be transitions or other functions, as we'll see later on. -For each keyframe, you specify an *offset* that defines at which point -in the animation that keyframe applies. The offset is a number between zero, -which marks the beginning of the animation, and one, which marks the end. +In this example, we'll name the trigger `openClose`, and attach it to the `button` element. The trigger describes the open and closed states, and the timings for the two transitions. -每个关键帧都可以被指定一个*偏移量*,用来定义该关键帧将被用在动画期间的哪个时间点。偏移量是一个介于 0(表示动画起点)和 1(表示动画终点)之间的数组。 +
      +triggering the animation +
      -This example adds some "bounce" to the enter and leave animations with -keyframes: +
      -这个例子使用关键帧来为进场和离场动画添加一些“反弹效果”: +**Note:** Within each `trigger()` function call, an element can only be in one state at any given time. However, it's possible for multiple triggers to be active at once. +
      - +### Defining animations and attaching them to the HTML template -Note that the offsets are *not* defined in terms of absolute time. They are relative -measures from zero to one. The final timeline of the animation is based on the combination -of keyframe offsets, duration, delay, and easing. +Animations are defined in the metadata of the component that controls the HTML element to be animated. Put the code that defines your animations under the `animations:` property within the `@Component()` decorator. -注意,这个偏移量并*不是*用绝对数字定义的时间段,而是在 0 到 1 之间的相对值(百分比)。动画的最终时间线会基于关键帧的偏移量、持续时间、延迟和缓动函数计算出来。 + + -Defining offsets for keyframes is optional. If you omit them, offsets with even -spacing are automatically assigned. For example, three keyframes without predefined -offsets receive offsets `0`, `0.5`, and `1`. +When you've defined an animation trigger for a component, you can attach it to an element in that component's template by wrapping the trigger name in brackets and preceding it with an `@` symbol. Then, you can bind the trigger to a template expression using standard Angular property binding syntax as shown below, where `triggerName` is the name of the trigger, and `expression` evaluates to a defined animation state. -为关键帧定义偏移量是可选的。如果省略它们,偏移量会自动根据帧数平均分布出来。例如,三个未定义过偏移量的关键帧会分别获得偏移量:`0`、`0.5` 和 `1`。 +``` +
      ...
      ; +``` -## Parallel animation groups +The animation is executed or triggered when the expression value changes to a new state. -## 并行动画组(Group) +The following code snippet binds the trigger to the value of the `isOpen` property. -Parallel animations with different timings, implemented with groups + + -You've seen how to animate multiple style properties at the same time: -just put all of them into the same `style()` definition. +In this example, when the `isOpen` expression evaluates to a defined state of `open` or `closed`, it notifies the trigger `openClose` of a state change. Then it's up to the `openClose` code to handle the state change and kick off a state change animation. -你已经知道该如何在同一时间段进行多个样式的动画了:只要把它们都放进同一个 `style()` 定义中就行了! +For elements entering or leaving a page (inserted or removed from the DOM), you can make the animations conditional. For example, use `*ngIf` with the animation trigger in the HTML template. -But you may also want to configure different *timings* for animations that happen -in parallel. For example, you may want to animate two CSS properties but use a -different easing function for each one. +
      -但你也可能会希望为同时发生的几个动画配置不同的*时间线*。比如,同时对两个 CSS 属性做动画,但又得为它们定义不同的缓动函数。 +**Note:** In the component file, set the trigger that defines the animations as the value of the `animations:` property in the `@Component()` decorator. -For this you can use animation *groups*. In this example, using groups both on -enter and leave allows for two different timing configurations. Both -are applied to the same element in parallel, but run independently of each other: +In the HTML template file, use the trigger name to attach the defined animations to the HTML element to be animated. -这种情况下就可以用动画*组*来解决了。在这个例子中,同时在进场和离场时使用了组,以便能让它们使用两种不同的时间线配置。 -它们被同时应用到同一个元素上,但又彼此独立运行: +
      - +### Code review -One group animates the element transform and width; the other group animates the opacity. +Here are the code files discussed in the transition example. -其中一个动画组对元素的 `transform` 和 `width` 做动画,另一个组则对 `opacity` 做动画。 + -## Animation callbacks + + -## 动画回调 + + -A callback is fired when an animation is started and also when it is done. + + -当动画开始和结束时,会触发一个回调。 + -In the keyframes example, you have a `trigger` called `@flyInOut`. You can hook -those callbacks like this: +### Summary -对于例子中的这个关键帧,你有一个叫做 `@flyInOut` 的 `trigger`。在那里你可以挂钩到那些回调,比如: +You learned to add animation to a simple transition between two states, using `style()` and `state()` along with `animate()` for the timing. - +You can learn about more advanced features in Angular animations under the Animation section, beginning with advanced techniques in [transition and triggers](guide/transition-and-triggers). -The callbacks receive an `AnimationEvent` that contains useful properties such as -`fromState`, `toState` and `totalTime`. +{@a animation-api-summary} +## Animations API summary -这些回调接收一个 `AnimationTransitionEvent` 参数,它包含一些有用的属性,例如 `fromState`,`toState` 和 `totalTime`。 +The functional API provided by the `@angular/animations` module provides a domain-specific language (DSL) for creating and controlling animations in Angular applications. See the [API reference](api/animations) for a complete listing and syntax details of the core functions and related data structures. -Those callbacks will fire whether or not an animation is picked up. + -无论动画是否实际执行过,那些回调都会触发。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +Function name + +What it does +
      trigger()Kicks off the animation and serves as a container for all other animation function calls. HTML template binds to triggerName. Use the first argument to declare a unique trigger name. Uses array syntax.
      style()Defines one or more CSS styles to use in animations. Controls the visual appearance of HTML elements during animations. Uses object syntax.
      state()Creates a named set of CSS styles that should be applied on successful transition to a given state. The state can then be referenced by name within other animation functions.
      animate()Specifies the timing information for a transition. Optional values for delay and easing. Can contain style() calls within.
      transition()Defines the animation sequence between two named states. Uses array syntax.
      keyframes()Allows a sequential change between styles within a specified time interval. Use within animate(). Can include multiple style() calls within each keyframe(). Uses array syntax.
      group()Specifies a group of animation steps (inner animations) to be run in parallel. Animation continues only after all inner animation steps have completed. Used within sequence() or transition().
      query()Use to find one or more inner HTML elements within the current element.
      sequence()Specifies a list of animation steps that are run sequentially, one by one.
      stagger()Staggers the starting time for animations for multiple elements.
      animation()Produces a reusable animation that can be invoked from elsewhere. Used together with useAnimation().
      useAnimation()Activates a reusable animation. Used with animation().
      animateChild()Allows animations on child components to be run within the same timeframe as the parent.
      + +## More on Angular animations + +You may also be interested in the following: + +* [Transition and triggers](guide/transition-and-triggers) +* [Complex animation sequences](guide/complex-animation-sequences) +* [Reusable animations](guide/reusable-animations) +* [Route transition animations](guide/route-animations) + +
      + +Check out this full animation [demo](http://animationsftw.in/#/) with accompanying [presentation](https://www.youtube.com/watch?v=JhNo3Wvj6UQ&feature=youtu.be&t=2h47m53s), shown at the AngularConnect conference in November 2017. +
      diff --git a/aio/content/guide/aot-compiler.md b/aio/content/guide/aot-compiler.md index bafc9c1484..583ae7398c 100644 --- a/aio/content/guide/aot-compiler.md +++ b/aio/content/guide/aot-compiler.md @@ -1,18 +1,16 @@ -# The Ahead-of-Time (AOT) Compiler +# The Ahead-of-Time (AOT) compiler -# 预先(AOT)编译 +# 预先(AOT)编译器 -The Angular Ahead-of-Time (AOT) compiler converts your Angular HTML and TypeScript code into efficient JavaScript code during the build phase _before_ the browser downloads and runs that code. +An Angular application consists mainly of components and their HTML templates. Because the components and templates provided by Angular cannot be understood by the browser directly, Angular applications require a compilation process before they can run in a browser. -Angular 的“预先(AOT)编译器”会在构建期间把 Angular 应用的 HTML 和 TypeScript 代码编译成高效的 JavaScript 代码,之后浏览器就可以下载并快速运行这些代码。 +The Angular Ahead-of-Time (AOT) compiler converts your Angular HTML and TypeScript code into efficient JavaScript code during the build phase _before_ the browser downloads and runs that code. Compiling your application during the build process provides a faster rendering in the browser. -This guide explains how to build with the AOT compiler using different compiler options and how to write Angular metadata that AOT can compile. +This guide explains how to specify metadata and apply available compiler options to compile your applications efficiently using the AOT compiler. -本章描述了如何使用 AOT 编译器,以及如何书写能被 AOT 编译的 Angular 元数据。 +
      Watch compiler author Tobias Bosch explain the Angular Compiler at AngularConnect 2016. + Watch compiler author Tobias Bosch explain the Angular compiler at AngularConnect 2016. 观看编译器作者 Tobias Bosch 在 AngularConnect 2016 大会里,对Angular 编译器的演讲。 @@ -22,28 +20,14 @@ This guide explains how to build with the AOT compiler using different compiler ## Angular compilation -## Angular 中的编译 - -An Angular application consists largely of components and their HTML templates. -Before the browser can render the application, -the components and templates must be converted to executable JavaScript by an _Angular compiler_. - -Angular 应用由大量组件及其 HTML 模板组成。 -在浏览器渲染应用之前,组件和模板必须由 *Angular 编译器*转换成可执行的 JavaScript 代码。 - Angular offers two ways to compile your application: -Angular 提供了两种方式来编译你的应用: - -1. **_Just-in-Time_ (JIT)**, which compiles your app in the browser at runtime - - **即时(JIT)编译**,它会在浏览器中运行时编译你的应用 - +1. **_Just-in-Time_ (JIT)**, which compiles your app in the browser at runtime. 1. **_Ahead-of-Time_ (AOT)**, which compiles your app at build time. **预先(AOT)编译**,它会在构建时编译你的应用。 -JIT compilation is the default when you run the _build-only_ or the _build-and-serve-locally_ CLI commands: +JIT compilation is the default when you run the [`ng build`](cli/build) (build only) or [`ng serve`](cli/serve) (build and serve locally) CLI commands: 当你运行 *`build`* 或 *`serve`* 这两个 CLI 命令时 JIT 编译是默认选项: @@ -54,7 +38,7 @@ JIT compilation is the default when you run the _build-only_ or the _build-and-s {@a compile} -For AOT compilation, append the `--aot` flags to the _build-only_ or the _build-and-serve-locally_ CLI commands: +For AOT compilation, include the `--aot` option with the `ng build` or `ng serve` command: 要进行 AOT 编译只要给这两个 CLI 命令添加 `--aot` 标志就行了: @@ -65,11 +49,11 @@ For AOT compilation, append the `--aot` flags to the _build-only_ or the _build-
      -The `--prod` meta-flag compiles with AOT by default. +The `ng build` command with the `--prod` meta-flag (`ng build --prod`) compiles with AOT by default. `--prod` 标志也会默认使用 AOT 编译。 -See the [CLI documentation](https://github.com/angular/angular-cli/wiki) for details, especially the [`build` topic](https://github.com/angular/angular-cli/wiki/build). +See the [CLI command reference](cli) and [Building and serving Angular apps](guide/build) for more information. 要了解更多,请参见[CLI 文档](https://github.com/angular/angular-cli/wiki),特别是[`build` 这个主题](https://github.com/angular/angular-cli/wiki/build)。 @@ -128,293 +112,28 @@ AOT compiles HTML templates and components into JavaScript files long before the With no templates to read and no risky client-side HTML or JavaScript evaluation, there are fewer opportunities for injection attacks. -AOT 编译远在 HTML 模版和组件被服务到客户端之前,将它们编译到 JavaScript 文件。 -没有模版可以阅读,没有高风险客户端 HTML 或 JavaScript 可利用,所以注入攻击的机会较少。 +## Controlling app compilation -{@a compiler-options} +When you use the Angular AOT compiler, you can control your app compilation in two ways: -## Angular Compiler Options +* By providing template compiler options in the `tsconfig.json` file. -## Angular 编译器选项 + For more information, see [Angular template compiler options](#compiler-options). -You can control your app compilation by providing template compiler options in the `tsconfig.json` file along with the options supplied to the TypeScript compiler. The template compiler options are specified as members of -`"angularCompilerOptions"` object as shown below: +* By [specifying Angular metadata](#metadata-aot). -你可以通过在 `tsconfig.json` 文件中随 TypeScript 编译选项一起提供模板编译选项来控制应用的编译方式。 -这些模板编译选项都是作为 `"angularCompilerOptions"` 对象的成员指定的,代码如下: -```json - -{ - "compilerOptions": { - "experimentalDecorators": true, - ... - }, - "angularCompilerOptions": { - "fullTemplateTypeCheck": true, - "preserveWhitespaces": true, - ... - } -} - -``` -### *enableResourceInlining* - -This options tell the compiler to replace the `templateUrl` and `styleUrls` property in all `@Component` decorators with inlined contents in `template` and `styles` properties. -When enabled, the `.js` output of ngc will have no lazy-loaded `templateUrl` or `styleUrls`. - -这个选项告诉编译器把所有 `@Component` 装饰器中的 `templateUrl` 和 `styleUrls` 属性内联成 `template` 和 `styles` 属性。 -当启用时,ngc 的 `.js` 输出中将不会有惰性加载的 `templateUrl` 和 `styleUrls`。 - -### *skipMetadataEmit* - -This option tells the compiler not to produce `.metadata.json` files. -The option is `false` by default. - -这个选项告诉编译器不要生成 `.metadata.json` 文件,它默认是 `false`。 - -`.metadata.json` files contain information needed by the template compiler from a `.ts` -file that is not included in the `.d.ts` file produced by the TypeScript compiler. This information contains, -for example, the content of annotations (such as a component's template) which TypeScript -emits to the `.js` file but not to the `.d.ts` file. - -`.metadata.json` 文件中包含模板编译器所需的信息,这些信息来自于 `.ts` 文件中,但是没有包含在由 TypeScript 编译器生成的 `.d.ts` 文件中。 -比如,这个信息包括 TypeScript 发出的注解内容(如组件的模板),TypeScript 把它生成到了 `.js` 文件中,但是没有生成到 `.d.ts` 文件中。 - -This option should be set to `true` if using TypeScript's `--outFile` option, as the metadata files -are not valid for this style of TypeScript output. It is not recommeded to use `--outFile` with -Angular. Use a bundler, such as [webpack](https://webpack.js.org/), instead. - -如果使用了 TypeScript 的 `--outFile` 选项,那就要同时设置这个选项。因为在 TypeScript 的这种输出方式下,metadata 文件是无效的。 -Angular 中不建议使用 `--outFile`,请改用 [webpack](https://webpack.js.org/) 之类的打包器代替。 - -This option can also be set to `true` when using factory summaries as the factory summaries -include a copy of the information that is in the `.metadata.json` file. - -当使用工厂汇总器(factory summary)时,这个选项也要设置为 `true`,因为工厂汇总器在自己的 `.metadata.json` 中也包含了这些信息的一个副本。 - -### *strictMetadataEmit* - -This option tells the template compiler to report an error to the `.metadata.json` -file if `"skipMetadataEmit"` is `false` . This option is `false` by default. This should only be used when `"skipMetadataEmit"` is `false` and `"skipTemplateCodegen"` is `true`. - -这个选项告诉模板编译器如果 `"skipMetadataEmit"` 为 `false`,那就把错误信息汇报到 `.metadata.json` 中。 -只有当 `"skipMetadataEmit"` 为 `false` 且 `"skipTemplateCodegen"` 为 `true` 时才应该使用这个选项。 - -It is intended to validate the `.metadata.json` files emitted for bundling with an `npm` package. The validation is overly strict and can emit errors for metadata that would never produce an error when used by the template compiler. You can choose to suppress the error emitted by this option for an exported symbol by including `@dynamic` in the comment documenting the symbol. - -它的设计意图是要验证为打包 `npm` 而生成的 `.metadata.json` 文件。 -这种验证过于严格,在使用模板编译器时甚至可能会对那些铁定不会出错的元数据文件报告一些错误。 -你可以用 `@dynamic` 在注释中指定一些要导出的符号,来禁止对它们报告错误。 - -It is valid for `.metadata.json` files to contain errors. The template compiler reports these errors -if the metadata is used to determine the contents of an annotation. The metadata -collector cannot predict the symbols that are designed to use in an annotation, so it will preemptively -include error nodes in the metadata for the exported symbols. The template compiler can then use the error -nodes to report an error if these symbols are used. If the client of a library intends to use a symbol in an annotation, the template compiler will not normally report -this until the client uses the symbol. This option allows detecting these errors during the build phase of -the library and is used, for example, in producing Angular libraries themselves. - -对于 `.metadata.json` 文件来说,包含错误是正常的。如果这些元数据被用来确定注解的内容,模板编译器就会报告这些错误。 -元数据收集器无法判断这些符号的设计目的是用在注解中,所以它将会自作主张,在元数据中为这些导出的符号添加错误节点。 -如果这些符号被用到了,模板编译器就会根据这些错误节点报告错误。 -如果某个库的使用者只是在注解中(而不是普通代码中)使用这些符号,模板编译器通常不会报错。 -这个选项允许在该库(比如 Angular 自身这些库)的构建和使用过程中检测这类错误。 - -### *skipTemplateCodegen* - -This option tells the compiler to suppress emitting `.ngfactory.js` and `.ngstyle.js` files. When set, -this turns off most of the template compiler and disables reporting template diagnostics. -This option can be used to instruct the -template compiler to produce `.metadata.json` files for distribution with an `npm` package while -avoiding the production of `.ngfactory.js` and `.ngstyle.js` files that cannot be distributed to -`npm`. - -这个选项告诉编译器忽略从 `.ngfactory.js` 和 `.ngstyle.js` 文件中发出的错误。 -如果为 `true`,它就会关闭大多数的模板编译器,并禁止汇报模板诊断信息。 -这个选项用于指示模板编译器为通过 `npm` 包分发而生成 `.metadata.json` 文件,同时避免生成无法分发到 `npm` 的 `.ngfactory.js` 和 `.ngstyle.js` 文件。 - -### *strictInjectionParameters* - -When set to `true`, this options tells the compiler to report an error for a parameter supplied -whose injection type cannot be determined. When this value option is not provided or is `false`, constructor parameters of classes marked with `@Injectable` whose type cannot be resolved will -produce a warning. - -当设置为 `true` 时,该选项会告诉编译器为那些无法确定其类型的注入参数报告错误。 -当该值没有提供或未 `false` 时,那些带有 `@Injectable` 的类,如果其构造参数的类型无法解析,就会生成一个警告。 - -*Note*: It is recommended to change this option explicitly to `true` as this option will default to `true` in the future. - -*注意*:建议把该选项显式改为 `true`,因为将来这个选项的默认值会是 `true`。 - -### *flatModuleOutFile* - -When set to `true`, this option tells the template compiler to generate a flat module -index of the given file name and the corresponding flat module metadata. Use this option when creating -flat modules that are packaged similarly to `@angular/core` and `@angular/common`. When this option -is used, the `package.json` for the library should refer -to the generated flat module index instead of the library index file. With this -option only one `.metadata.json` file is produced that contains all the metadata necessary -for symbols exported from the library index. In the generated `.ngfactory.js` files, the flat -module index is used to import symbols that includes both the public API from the library index -as well as shrowded internal symbols. - -当为 `true` 时,该选项告诉模板编译器生成一个指定名字的扁平模块索引和相应的扁平模块元数据。 -当要创建像 `@angular/core` 和 `@angular/common` 这样的扁平模块包时,请使用本选项。 -当使用本选项时,库的 `package.json` 文件就会引用生成的扁平模块索引,而不是库的索引文件。 -当使用本选项时,只会生成一个 `.metadata.json` 文件,其中包含从库索引中导出的符号所需的全部元数据。 -在生成的 `.ngfactory.js` 文件中,扁平模块索会用来导入包括库的公共 API 和隐藏的内部符号在内的全部符号。 - -By default the `.ts` file supplied in the `files` field is assumed to be library index. -If more than one `.ts` file is specified, `libraryIndex` is used to select the file to use. -If more than one `.ts` file is supplied without a `libraryIndex`, an error is produced. A flat module -index `.d.ts` and `.js` will be created with the given `flatModuleOutFile` name in the same -location as the library index `.d.ts` file. For example, if a library uses -`public_api.ts` file as the library index of the module, the `tsconfig.json` `files` field -would be `["public_api.ts"]`. The `flatModuleOutFile` options could then be set to, for -example `"index.js"`, which produces `index.d.ts` and `index.metadata.json` files. The -library's `package.json`'s `module` field would be `"index.js"` and the `typings` field -would be `"index.d.ts"`. - -默认情况下,`files` 字段中提供的 `.ts` 文件会被当做库索引。 -如果指定了多个 `.ts` 文件,就要用 `libraryIndex` 来选择要作为库索引的文件。 -扁平模块索引会用 `flatModuleOutFile` 中给出的名字创建 `.d.ts` 和 `.js` 文件,并放在和库索引的 `.d.ts` 文件相同的位置。 -比如,如果某个库使用 `public_api.ts` 文件作为该模块的库索引,那么 `tsconfig.json` 的 `files` 字段就应该是 `["public_api.ts"]`。 -然后可以把 `flatModuleOutFile` 选项设置为 `"index.js"`,它就会生成 `index.d.ts` 和 `index.metadata.json` 文件。 -该库的 `package.json` 文件的 `module` 字段将会是 `"index.js"`,而 `typings` 字段会是 `"index.d.ts"`。 - -### *flatModuleId* - -This option specifies the preferred module id to use for importing a flat module. -References generated by the template compiler will use this module name when importing symbols -from the flat module. -This is only meaningful when `flatModuleOutFile` is also supplied. Otherwise the compiler ignores -this option. - -该选项指定建议的模块 ID,这个 ID 用于导入扁平模块。 -从扁平模块中导入符号时,由模板编译器生成的引用将使用这个模块名称。 -它仅在同时提供了 `flatModuleOutFile` 选项时才有意义,否则,编译器将忽略此选项。 - -### *generateCodeForLibraries* - -This option tells the template compiler to generate factory files (`.ngfactory.js` and `.ngstyle.js`) -for `.d.ts` files with a corresponding `.metadata.json` file. This option defaults to -`true`. When this option is `false`, factory files are generated only for `.ts` files. - -这个选项告诉模板编译器也为与 `.metadata.json` 文件对应的 `.d.ts` 文件生成工厂文件(`.ngfactory.js` 和 `.ngstyle.js`)。 -这个选项默认为 `true`。当该选项为 `false` 时,只会为 `.ts` 文件生成工厂文件。 - -This option should be set to `false` when using factory summaries. - -当使用工厂汇总器时,这个选项应该设置为 `false`。 - -### *fullTemplateTypeCheck* - -This option tells the compiler to enable the [binding expression validation](#binding-expression-validation) -phase of the template compiler which uses TypeScript to validate binding expressions. - -该选项告诉编译器要为模板编译器启用[绑定表达式验证](#binding-expression-validation)阶段,它会使用 TypeScript 来验证绑定表达式。 - -This option is `false` by default. - -该选项默认是 `false`。 - -*Note*: It is recommended to set this to `true` as this option will default to `true` in the future. - -*注意*:建议把它设置为 `true`,因为将来它会默认为 `true`。 - -### *annotateForClosureCompiler* - -This option tells the compiler to use [Tsickle](https://github.com/angular/tsickle) to annotate the emitted -JavaScript with [JsDoc](http://usejsdoc.org/) comments needed by the -[Closure Compiler](https://github.com/google/closure-compiler). This option defaults to `false`. - -该选项告诉编译器使用 [Tsickle](https://github.com/angular/tsickle) 来为生成的 JavaScript 添加供 [Closure Compiler](https://github.com/google/closure-compiler) 使用的 [JsDoc](http://usejsdoc.org/) 注解。 -该选项默认为 `false`。 - -### *annotationsAs* - -Use this option to modify how the Angular specific annotations are emitted to improve tree-shaking. Non-Angular -annotations and decorators are unaffected. Default is `static fields`. - -使用这个选项来修改生成 Angular 特有注解的方式,以提升摇树优化(tree-shaking)的效果。它对 Angular 自身之外的注解和装饰器无效。 -默认值是 `static fields`。 - -value | description说明 -----------------|------------------------------------------------------------- -`decorators` | Leave the Decorators in-place. This makes compilation faster. TypeScript will emit calls to the `__decorate` helper. Use `--emitDecoratorMetadata` for runtime reflection. However, the resulting code will not properly tree-shake. 原地保留装饰器。这会让编译过程更快。TypeScript 将会生成对 `__decorate` 助手函数的调用。使用 `--emitDecoratorMetadata` 进行运行时反射。不过,生成的代码将无法正常进行摇树优化。 -`static fields` | Replace decorators with a static field in the class. Allows advanced tree-shakers like [Closure Compiler](https://github.com/google/closure-compiler) to remove unused classes.使用类的静态字段代替装饰器。它允许像 [Closure Compiler](https://github.com/google/closure-compiler) 这样的高级摇树优化器移除未使用的类。 - -### *trace* - -This tells the compiler to print extra information while compiling templates. - -它告诉编译器在编译模板时打印额外的信息。 - -### *disableExpressionLowering* - -The Angular template compiler transforms code that is used, or could be used, in an annotation -to allow it to be imported from template factory modules. See -[metadata rewriting](#metadata-rewriting) for more information. - -Angular 的模板编译器会转换注解中使用或可能使用的代码,以便能从模板的工厂模块中导入它。 -参见[元数据重写](#metadata-rewriting)以了解更多信息。 - -Setting this option to `false` disables this rewriting, requiring the rewriting to be -done manually. - -把该选项设置为 `false` 将会禁止这种重写,如果需要重写就去得人工完成了。 - -### *preserveWhitespaces* - -This option tells the compiler whether to remove blank text nodes from compiled templates. -As of v6, this option is `false` by default, which results in smaller emitted template factory modules. - -该选项会告诉编译器是否要从编译后的模板中移除空白的文本节点。 -对于 Angular v6,该选项默认为 `false`,它会移除空白节点,以生成更小的模板工厂模块。 - -### *allowEmptyCodegenFiles* - -Tells the compiler to generate all the possible generated files even if they are empty. This option is -`false` by default. This is an option used by `bazel` build rules and is needed to simplify -how `bazel` rules track file dependencies. It is not recommended to use this option outside of the `bazel` -rules. - -告诉编译器生成所有可能生成的文件 —— 即使是空文件。 -该选项默认为 `false`。 -这是供 `bazel` 构建规则使用的选项,它用于简化 `bazel` 规则跟踪文件依赖的方式。 -除了 `bazel` 规则之外不建议使用该选项。 - -### *enableIvy* - -Tells the compiler to generate definitions using the Render3 style code generation. This option defaults to `false`. - -告诉编译器使用 Render3 风格的代码生成器来来生成各种定义。 -该选项默认为 `false`。 - -Not all features are supported with this option enabled. It is only supported - for experimentation and testing of Render3 style code generation. - -当开启该选项时,有些特性不受支持。它仅仅用来为试验和测试 Render3 风格的代码生成提供支持。 - -*Note*: Is it not recommended to use this option as it is not yet feature complete with the Render2 code generation. - -*注意*:不建议使用该选项,因为它在使用 Render2 的代码生成器时还缺少一些特性。 - -## Angular Metadata and AOT - -## Angular 元数据与 AOT - -The Angular **AOT compiler** extracts and interprets **metadata** about the parts of the application that Angular is supposed to manage. +{@a metadata-aot} +## Specifying Angular metadata Angular 的 **AOT 编译器**会提取并解释应用中由 Angular 管理的各个部件的**元数据**。 Angular metadata tells Angular how to construct instances of your application classes and interact with them at runtime. +The Angular **AOT compiler** extracts **metadata** to interpret the parts of the application that Angular is supposed to manage. Angular 的元数据会告诉 Angular 如何创建应用中类的实例以及如何在运行期间与它们交互。 -You specify the metadata with **decorators** such as `@Component()` and `@Input()`. -You also specify metadata implicitly in the constructor declarations of these decorated classes. +You can specify the metadata with **decorators** such as `@Component()` and `@Input()` or implicitly in the constructor declarations of these decorated classes. 你通过**装饰器**来指定元数据,比如 `@Component()` 和 `@Input()`。 你还可以在这些带装饰器的类的构造函数中隐式指定元数据。 @@ -512,28 +231,84 @@ Angular 的 [schema.ts](https://github.com/angular/angular/blob/master/packages/ The _collector_ only understands a subset of JavaScript. Define metadata objects with the following limited syntax: -这个**收集器**只能理解 JavaScript 的一个子集。 -请使用下列受限语法定义元数据对象: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      SyntaxExample
      Literal object {cherry: true, apple: true, mincemeat: false}
      Literal array ['cherries', 'flour', 'sugar']
      Spread in literal array['apples', 'flour', ...the_rest]
      Callsbake(ingredients)
      Newnew Oven()
      Property accesspie.slice
      Array indexingredients[0]
      Identity referenceComponent
      A template string`pie is ${multiplier} times better than cake`
      Literal stringpi
      Literal number3.14153265
      Literal booleantrue
      Literal nullnull
      Supported prefix operator !cake
      Supported binary operator a+b
      Conditional operatora ? b : c
      Parentheses(a+b)
      -Syntax语法 | Example范例 ------------------------------------ | ----------------------------------- -Literal object对象字面量 | `{cherry: true, apple: true, mincemeat: false}` -Literal array数组字面量 | `['cherries', 'flour', 'sugar']` -Spread in literal array字面量数组展开 | `['apples', 'flour', ...the_rest]` -Calls调用 | `bake(ingredients)` -New创建对象 | `new Oven()` -Property access属性访问 | `pie.slice` -Array index数组索引 | `ingredients[0]` -Identifier reference标识符引用 | `Component` -A template string模板字符串 | `pie is ${multiplier} times better than cake` -Literal string字符串字面量 | `'pi'` -Literal number数字字面量 | `3.14153265` -Literal boolean逻辑字面量 | `true` -Literal null空字面量 | `null` -Supported prefix operator受支持的前缀操作符 | `!cake` -Supported Binary operator受支持的二元操作符 | `a + b` -Conditional operator条件操作符 | `a ? b : c` -Parentheses括号 | `(a + b)` If an expression uses unsupported syntax, the _collector_ writes an error node to the `.metadata.json` file. The compiler later reports the error if it needs that piece of metadata to generate the application code. @@ -740,28 +515,89 @@ The _collector_ reduces this expression to its equivalent _folded_ string: The following table describes which expressions the _collector_ can and cannot fold: -下表中描述了哪些表达式是否能被*收集器*折叠: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      SyntaxFoldable
      Literal object Yes
      Literal array Yes
      Spread in literal arrayno
      Callsno
      Newno
      Property accessyes, if target is foldable
      Array index yes, if target and index are foldable
      Identity referenceyes, if it is a reference to a local
      A template with no substitutionsyes
      A template with substitutionsyes, if the substitutions are foldable
      Literal stringyes
      Literal numberyes
      Literal booleanyes
      Literal nullyes
      Supported prefix operator yes, if operand is foldable
      Supported binary operator yes, if both left and right are foldable
      Conditional operatoryes, if condition is foldable
      Parenthesesyes, if the expression is foldable
      -Syntax语法 | Foldable可折叠的 ------------------------------------ | ----------------------------- -Literal object对象字面量 | yes -Literal array数组字面量 | yes -Spread in literal array字面量数组展开 | no -Calls调用 | no -New创建对象 | no -Property access属性访问 | yes, if target is foldable 是(如果目标也是可折叠的) -Array index数组索引 | yes, if target and index are foldable 是(如果目标和索引也是可折叠的) -Identifier reference标识符引用 | yes, if it is a reference to a local 是(如果引用的是局部变量) -A template with no substitutions没有内嵌表达式的模板 | yes -A template with substitutions带内嵌表达式的模板 | yes, if the substitutions are foldable 是(如果内嵌表达式是可折叠的) -Literal string字符串字面量 | yes -Literal number数字字面量 | yes -Literal boolean逻辑字面量 | yes -Literal null空字面量 | yes -Supported prefix operator受支持的前缀操作符 | yes, if operand is foldable 是(如果运算数是可折叠的) -Supported binary operator受支持的二元操作符 | yes, if both left and right are foldable 是(如果左右运算数都是可折叠的) -Conditional operator条件操作符 | yes, if condition is foldable 是(如果条件是可折叠的) -Parentheses括号 | yes, if the expression is foldable 是(如果表达式是可折叠的) If an expression is not foldable, the collector writes it to `.metadata.json` as an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) for the compiler to resolve. @@ -828,28 +664,90 @@ The compiler only allows metadata that create instances of the class `InjectionT The compiler only supports metadata for these Angular decorators. -编译器只支持下列 Angular 装饰器的元数据。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      DecoratorModule
      Attribute@angular/core
      Component@angular/core
      ContentChild@angular/core
      ContentChildren@angular/core
      Directive@angular/core
      Host@angular/core
      HostBinding@angular/core
      HostListner@angular/core
      Inject@angular/core
      Injectable@angular/core
      Input@angular/core
      NgModule@angular/core
      Optional@angular/core
      Output@angular/core
      Pipe@angular/core
      Self@angular/core
      SkipSelf@angular/core
      ViewChild@angular/core
      -Decorator装饰器 | Module所在模块 -------------------|-------------- -`Attribute` | `@angular/core` -`Component` | `@angular/core` -`ContentChild` | `@angular/core` -`ContentChildren` | `@angular/core` -`Directive` | `@angular/core` -`Host` | `@angular/core` -`HostBinding` | `@angular/core` -`HostListener` | `@angular/core` -`Inject` | `@angular/core` -`Injectable` | `@angular/core` -`Input` | `@angular/core` -`NgModule` | `@angular/core` -`Optional` | `@angular/core` -`Output` | `@angular/core` -`Pipe` | `@angular/core` -`Self` | `@angular/core` -`SkipSelf` | `@angular/core` -`ViewChild` | `@angular/core` ### Macro-functions and macro-static methods @@ -976,7 +874,7 @@ The compiler does the rewriting during the emit of the `.js` file. This doesn't 编译器会在生成 `.js` 文件期间进行这种重写。它不会重写 `.d.ts` 文件,所以 TypeScript 也不会把这个变量当做一项导出,因此也就不会污染 ES 模块中导出的 API。 -## Metadata Errors +## Metadata errors ## 元数据错误 @@ -1684,7 +1582,6 @@ Chuck: After reviewing your PR comment I'm still at a loss. See [comment there]( --> {@a binding-expression-validation} - ## Phase 3: binding expression validation ## 阶段 3:验证绑定表达式 @@ -1810,7 +1707,7 @@ Chuck: After reviewing your PR comment I'm still at a loss. See [comment there]( ### 非空类型断言操作符 Use the [non-null type assertion operator](guide/template-syntax#non-null-assertion-operator) - to suppress the `Object is possibly 'undefined'` error when it is incovienent to use + to suppress the `Object is possibly 'undefined'` error when it is inconvenient to use `*ngIf` or when some constraint in the component ensures that the expression is always non-null when the binding expression is interpolated. @@ -1899,34 +1796,218 @@ Chuck: After reviewing your PR comment I'm still at a loss. See [comment there]( ``` -## Summary +{@a tsconfig-extends} +## Configuration inheritance with extends +Similar to TypeScript Compiler, Angular Compiler also supports `extends` in the `tsconfig.json` on `angularCompilerOptions`. A tsconfig file can inherit configurations from another file using the `extends` property. + The `extends` is a top level property parallel to `compilerOptions` and `angularCompilerOptions`. + The configuration from the base file are loaded first, then overridden by those in the inheriting config file. + Example: +```json +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "experimentalDecorators": true, + ... + }, + "angularCompilerOptions": { + "fullTemplateTypeCheck": true, + "preserveWhitespaces": true, + ... + } +} +``` + More information about tsconfig extends can be found in the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). -## 小结 +{@a compiler-options} +## Angular template compiler options -* What the AOT compiler does and why it is important. +The template compiler options are specified as members of the `"angularCompilerOptions"` object in the `tsconfig.json` file. Specify template compiler options along with the options supplied to the TypeScript compiler as shown here: - 什么是 AOT 编译器,以及它为什么如此重要。 + ```json + { + "compilerOptions": { + "experimentalDecorators": true, + ... + }, + "angularCompilerOptions": { + "fullTemplateTypeCheck": true, + "preserveWhitespaces": true, + ... + } + } + ``` -* Why metadata must be written in a subset of JavaScript. +The following section describes the Angular's template compiler options. - 为何元数据必须使用 JavaScript 的一个子集来书写。 +### *enableResourceInlining* +This option instructs the compiler to replace the `templateUrl` and `styleUrls` property in all `@Component` decorators with inlined contents in `template` and `styles` properties. +When enabled, the `.js` output of `ngc` will have no lazy-loaded `templateUrl` or `styleUrls`. -* What that subset is. +### *skipMetadataEmit* - 这个子集是什么。 +This option tells the compiler not to produce `.metadata.json` files. +The option is `false` by default. -* Other restrictions on metadata definition. +`.metadata.json` files contain information needed by the template compiler from a `.ts` +file that is not included in the `.d.ts` file produced by the TypeScript compiler. This information contains, +for example, the content of annotations (such as a component's template), which TypeScript +emits to the `.js` file but not to the `.d.ts` file. - 定义元数据时的其它限制。 +This option should be set to `true` if you are using TypeScript's `--outFile` option, because the metadata files +are not valid for this style of TypeScript output. It is not recommended to use `--outFile` with +Angular. Use a bundler, such as [webpack](https://webpack.js.org/), instead. -* Macro-functions and macro-static methods. +This option can also be set to `true` when using factory summaries because the factory summaries +include a copy of the information that is in the `.metadata.json` file. - 宏函数和静态宏函数。 +### *strictMetadataEmit* -* Compiler errors related to metadata. +This option tells the template compiler to report an error to the `.metadata.json` +file if `"skipMetadataEmit"` is `false`. This option is `false` by default. This should only be used when `"skipMetadataEmit"` is `false` and `"skipTemplateCodeGen"` is `true`. - 与元数据有关的编译器错误。 +This option is intended to validate the `.metadata.json` files emitted for bundling with an `npm` package. The validation is strict and can emit errors for metadata that would never produce an error when used by the template compiler. You can choose to suppress the error emitted by this option for an exported symbol by including `@dynamic` in the comment documenting the symbol. -* Validation of binding expressions +It is valid for `.metadata.json` files to contain errors. The template compiler reports these errors +if the metadata is used to determine the contents of an annotation. The metadata +collector cannot predict the symbols that are designed for use in an annotation, so it will preemptively +include error nodes in the metadata for the exported symbols. The template compiler can then use the error +nodes to report an error if these symbols are used. If the client of a library intends to use a symbol in an annotation, the template compiler will not normally report +this until the client uses the symbol. This option allows detecting these errors during the build phase of +the library and is used, for example, in producing Angular libraries themselves. - 验证绑定表达式。 +### *skipTemplateCodegen* + +This option tells the compiler to suppress emitting `.ngfactory.js` and `.ngstyle.js` files. When set, +this turns off most of the template compiler and disables reporting template diagnostics. +This option can be used to instruct the +template compiler to produce `.metadata.json` files for distribution with an `npm` package while +avoiding the production of `.ngfactory.js` and `.ngstyle.js` files that cannot be distributed to +`npm`. + +### *strictInjectionParameters* + +When set to `true`, this options tells the compiler to report an error for a parameter supplied +whose injection type cannot be determined. When this option is not provided or is `false`, constructor parameters of classes marked with `@Injectable` whose type cannot be resolved will +produce a warning. + +*Note*: It is recommended to change this option explicitly to `true` as this option will default to `true` in the future. + +### *flatModuleOutFile* + +When set to `true`, this option tells the template compiler to generate a flat module +index of the given file name and the corresponding flat module metadata. Use this option when creating +flat modules that are packaged similarly to `@angular/core` and `@angular/common`. When this option +is used, the `package.json` for the library should refer +to the generated flat module index instead of the library index file. With this +option only one `.metadata.json` file is produced, which contains all the metadata necessary +for symbols exported from the library index. In the generated `.ngfactory.js` files, the flat +module index is used to import symbols that includes both the public API from the library index +as well as shrowded internal symbols. + +By default the `.ts` file supplied in the `files` field is assumed to be the library index. +If more than one `.ts` file is specified, `libraryIndex` is used to select the file to use. +If more than one `.ts` file is supplied without a `libraryIndex`, an error is produced. A flat module +index `.d.ts` and `.js` will be created with the given `flatModuleOutFile` name in the same +location as the library index `.d.ts` file. For example, if a library uses the +`public_api.ts` file as the library index of the module, the `tsconfig.json` `files` field +would be `["public_api.ts"]`. The `flatModuleOutFile` options could then be set to, for +example `"index.js"`, which produces `index.d.ts` and `index.metadata.json` files. The +library's `package.json`'s `module` field would be `"index.js"` and the `typings` field +would be `"index.d.ts"`. + +### *flatModuleId* + +This option specifies the preferred module id to use for importing a flat module. +References generated by the template compiler will use this module name when importing symbols +from the flat module. +This is only meaningful when `flatModuleOutFile` is also supplied. Otherwise the compiler ignores +this option. + +### *generateCodeForLibraries* + +This option tells the template compiler to generate factory files (`.ngfactory.js` and `.ngstyle.js`) +for `.d.ts` files with a corresponding `.metadata.json` file. This option defaults to +`true`. When this option is `false`, factory files are generated only for `.ts` files. + +This option should be set to `false` when using factory summaries. + +### *fullTemplateTypeCheck* + +This option tells the compiler to enable the [binding expression validation](#binding-expression-validation) +phase of the template compiler which uses TypeScript to validate binding expressions. + +This option is `false` by default. + +*Note*: It is recommended to set this to `true` because this option will default to `true` in the future. + +### *annotateForClosureCompiler* + +This option tells the compiler to use [Tsickle](https://github.com/angular/tsickle) to annotate the emitted +JavaScript with [JSDoc](http://usejsdoc.org/) comments needed by the +[Closure Compiler](https://github.com/google/closure-compiler). This option defaults to `false`. + +### *annotationsAs* + +Use this option to modify how the Angular specific annotations are emitted to improve tree-shaking. Non-Angular +annotations and decorators are unaffected. Default is `static fields`. + + + + + + + + + + + + + + + + +
      ValueDescription
      decoratorsLeave the decorators in place. This makes compilation faster. TypeScript will emit calls to the __decorate helper. Use --emitDecoratorMetadata for runtime reflection. However, the resulting code will not properly tree-shake.
      static fieldsReplace decorators with a static field in the class. Allows advanced tree-shakers like + Closure compiler to remove unused classes.
      + + +### *trace* + +This tells the compiler to print extra information while compiling templates. + +### *enableLegacyTemplate* + +Use of the `