mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
388 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 470738c8f7 | |||
| a2cb2b4e2f | |||
| d0fbad97f7 | |||
| 9291bd950f | |||
| ba8a644477 | |||
| aabf8be033 | |||
| 19c94c15a2 | |||
| d181b93405 | |||
| 0f616720a7 | |||
| fccf32cd50 | |||
| f7aa210f5a | |||
| 9e0c464589 | |||
| 531808efdb | |||
| 70b2508d22 | |||
| bdbff8337b | |||
| 6c1064c72f | |||
| 8981db4f99 | |||
| f24e5148f9 | |||
| c23f2860ea | |||
| 8ca95b858b | |||
| d8de15e9be | |||
| f64f73df9a | |||
| 966c7cfe0d | |||
| 5af826a144 | |||
| 1a318155e5 | |||
| f30d18a942 | |||
| ffb2c9ce0f | |||
| 59c6bfb632 | |||
| a3fbc2730a | |||
| 6279fecd52 | |||
| c4d17b92f9 | |||
| 91f82ca5a4 | |||
| 2925c6a46b | |||
| 501ad04a1a | |||
| eea71dea80 | |||
| 0b799220ca | |||
| 126e798edf | |||
| 85d3f6f048 | |||
| c47dd76590 | |||
| 1f3e9d904a | |||
| 4aa4ca8b72 | |||
| 06c918ccb0 | |||
| fca7d6b9d2 | |||
| 6c593e314f | |||
| b6e4e1b399 | |||
| 23cac41589 | |||
| e7707dbf56 | |||
| 5af7ec244d | |||
| 77ba4aac00 | |||
| b55d2dab5d | |||
| f273666e90 | |||
| d81428435f | |||
| 5d5b51599d | |||
| bf44dc234a | |||
| 7df1989940 | |||
| 6b6bf6332b | |||
| cb7365a7bd | |||
| a51e625239 | |||
| e4dcaa513e | |||
| c0d04175ff | |||
| 5e2cd07a0b | |||
| 4c76f94ce0 | |||
| 0e539eb56d | |||
| 39b6cbcf4b | |||
| 9a5d84249a | |||
| 1e58a8c05d | |||
| 40d6a06615 | |||
| fc872b12aa | |||
| fdb2a38a1b | |||
| ac51e5c107 | |||
| 45cc5f49e4 | |||
| 3f9cfae8a4 | |||
| dcab0f48d6 | |||
| 0b04da1fd7 | |||
| a4c82ba25b | |||
| 28b0b89cb2 | |||
| 50b1c2bf52 | |||
| 9214e58d44 | |||
| 0b388a00da | |||
| dc5f436769 | |||
| 7f31e55961 | |||
| 06c34cd99a | |||
| 58e55495f2 | |||
| 1b373067e1 | |||
| 226441fc3a | |||
| 38701ce07b | |||
| 09388eb9ab | |||
| 95886d8aa9 | |||
| 6f79c2b7fd | |||
| 59f2d46a16 | |||
| 30226f08f3 | |||
| 7bc6c2d314 | |||
| dd4fd1f4f4 | |||
| 49af83e5e3 | |||
| 2131c551cd | |||
| 2af638818a | |||
| 2eccfd412e | |||
| f1cb455c1e | |||
| 520946d2e7 | |||
| cd526c1db6 | |||
| ff07194e92 | |||
| c0fe82049f | |||
| fab1af764d | |||
| 5a85e440a8 | |||
| 6921bf210a | |||
| b9ef61906e | |||
| 167bc0d163 | |||
| b620d9b316 | |||
| 73ec0c61fa | |||
| d933b17170 | |||
| db53ce40c5 | |||
| 2a445923ee | |||
| 82756e8a5f | |||
| 60af2d091c | |||
| 92066f162d | |||
| 0a36f4fbc8 | |||
| 8e46fb5676 | |||
| f36cf49330 | |||
| 2409dd6530 | |||
| 2067643896 | |||
| a3bbc6dcd9 | |||
| 3c62c70caf | |||
| fb4f228d3c | |||
| bec0007c82 | |||
| 4df7301093 | |||
| a192dea677 | |||
| d29cbe885c | |||
| 1eef647236 | |||
| 9745fac73f | |||
| 7032bf0780 | |||
| 88aec875c0 | |||
| f2d20d5c50 | |||
| bd59a1a395 | |||
| c0601db72c | |||
| aa920aa89a | |||
| 6eb2f76fee | |||
| ea114a0034 | |||
| 0d88348430 | |||
| ce38759f54 | |||
| 03519968c0 | |||
| 8c8ded8d31 | |||
| f88559eb48 | |||
| c20a7eeba6 | |||
| 621013af2a | |||
| 864a96d7a6 | |||
| 458d62b7b8 | |||
| ebb3f58f4c | |||
| 1e483ebe18 | |||
| dab7198a1f | |||
| 34ccffe9ab | |||
| cfd8702747 | |||
| e56f6d2187 | |||
| 86fdcfcc0f | |||
| 00864665a3 | |||
| 78b66b236b | |||
| 049123cf6c | |||
| 7815acd125 | |||
| ae812e3936 | |||
| e044939fa9 | |||
| 86a21f5569 | |||
| c3ccaa83b4 | |||
| 6e9075e787 | |||
| 85c35cf693 | |||
| b8ed822896 | |||
| 420af8ee47 | |||
| 665d4650cd | |||
| 40c138c13d | |||
| dc23ad4978 | |||
| 3596c44e0c | |||
| 8600b3077d | |||
| 31b2716c1c | |||
| 65712b1d15 | |||
| 4881e75097 | |||
| 2f5fe32b92 | |||
| 0136eb6d34 | |||
| 6efefec55c | |||
| 371a599c8e | |||
| a02d114dae | |||
| df5e1802a3 | |||
| c142511084 | |||
| 9520a029d2 | |||
| afad500f40 | |||
| 4199a8db08 | |||
| 6d9339559c | |||
| 5769eb0bd2 | |||
| da125dbb79 | |||
| 84c1896b6b | |||
| b13783eeb6 | |||
| a908ba7a68 | |||
| 500acfcf98 | |||
| 8df1deaad5 | |||
| 9b169f60a6 | |||
| 71ee5d58cf | |||
| 0e0ec49f58 | |||
| 8d685f6997 | |||
| 32cc63c067 | |||
| eb07e5bbda | |||
| 3fa6ae1ba9 | |||
| 347e0139d5 | |||
| 425035b8fd | |||
| 6c6eaddbe2 | |||
| e43686f4d1 | |||
| ef89eb579e | |||
| 5e8159ca2a | |||
| 6700f93183 | |||
| 9e435956f8 | |||
| a06644b816 | |||
| bd20c71391 | |||
| cc5a35123d | |||
| 47045a5306 | |||
| f183129eab | |||
| b0c110ab3a | |||
| 19fe298d11 | |||
| 57f63bf471 | |||
| c0655e447a | |||
| ec286048c9 | |||
| 7c12ebb50d | |||
| 4cafb67e82 | |||
| 20b81d0647 | |||
| 3603b4dbf3 | |||
| 79554b7a39 | |||
| 6fb3b6f5ba | |||
| 8062e7113f | |||
| 7b5e073a70 | |||
| e20bfda83d | |||
| adffd3fdf7 | |||
| d9a29e426a | |||
| 3f92ea57ea | |||
| fa92cf055d | |||
| 4aab63f31e | |||
| 8d24a91c6a | |||
| a36034a135 | |||
| 12855f3805 | |||
| f62339d1d1 | |||
| d7eb429430 | |||
| 3d6315e6e8 | |||
| 5ece26a6dc | |||
| 1dc641ea30 | |||
| 592eadd7c3 | |||
| 216372316a | |||
| 7cbedba3af | |||
| 0b55b647aa | |||
| 75e623b179 | |||
| 472abd7944 | |||
| 4ff8f27bad | |||
| c9478f78a2 | |||
| 8ab50a13d7 | |||
| f689250605 | |||
| cfc224983f | |||
| ebcca47402 | |||
| 11d916dcfd | |||
| d50fa6b79e | |||
| d0631f8aa0 | |||
| cc8b76ef7c | |||
| fa5528fb5f | |||
| f612fce06f | |||
| 930af9dd26 | |||
| 7e3dad1ad2 | |||
| cf55aa031c | |||
| 079ec8c651 | |||
| 890feb9332 | |||
| a04c5387e5 | |||
| bf724a257b | |||
| e73f5e0cd8 | |||
| a8c33bf931 | |||
| 0c3389ecd9 | |||
| c06911b461 | |||
| a12a120272 | |||
| 9baefd085f | |||
| 02b9d435d1 | |||
| b342e55509 | |||
| c7b1cc0f3c | |||
| e4309d57d8 | |||
| b42ff8009c | |||
| 455e359763 | |||
| 5a72929d09 | |||
| 2e5039bb95 | |||
| 371871a077 | |||
| 449512020a | |||
| b491668207 | |||
| 2551c78cc5 | |||
| bc063b2f86 | |||
| b79ad226b2 | |||
| bc97322641 | |||
| 4cfb06086b | |||
| 933d481a0e | |||
| edea15f2c6 | |||
| a1b6610c3d | |||
| 945432e3fa | |||
| 629ebad8eb | |||
| 154ca82f7b | |||
| 0c0550b536 | |||
| 449d29b701 | |||
| 6a9b1e11c2 | |||
| 6ab9f03baf | |||
| 37a25486cf | |||
| 28f289b825 | |||
| 54d0981871 | |||
| 2efcc26bb6 | |||
| a93a1a5759 | |||
| ae29f98c20 | |||
| 02918c9b5a | |||
| 2625dc1312 | |||
| 3e490fab1b | |||
| 76b64f659c | |||
| fb7318cb86 | |||
| 08ee9b4b26 | |||
| 46c0576efe | |||
| b8a6f7491b | |||
| 8f38bbaf26 | |||
| 80dff09134 | |||
| e7b48da713 | |||
| fe0075bdea | |||
| 1d02e6ad68 | |||
| b4706cf3e8 | |||
| cc4f5e6663 | |||
| f35c46a894 | |||
| f9888d928e | |||
| c097c5fbcb | |||
| a016508768 | |||
| 164a1713d5 | |||
| 604d89eef9 | |||
| 83cec3de51 | |||
| 7a33d395b3 | |||
| 8a19bc63ea | |||
| 173dde7681 | |||
| d4b3c0b47c | |||
| e63574774e | |||
| d5cd79f885 | |||
| e683298be6 | |||
| 99597c8c57 | |||
| f99cc40c5d | |||
| 623602fc3a | |||
| b3ea67e8a7 | |||
| 31a7ce1268 | |||
| c7bc47c509 | |||
| 96c0e42e61 | |||
| f24da6baad | |||
| ea7ff35b30 | |||
| 603194ee20 | |||
| 5dadec56e6 | |||
| d75e3da429 | |||
| ebd2ddbbd1 | |||
| df36537bfd | |||
| ed48887068 | |||
| 2908eba59c | |||
| a8ce8f7fc4 | |||
| 66eb1425e2 | |||
| 61f8522410 | |||
| c0903acbd4 | |||
| 588cd3ed50 | |||
| a99d9d67f3 | |||
| 6471eafde6 | |||
| d1840419d1 | |||
| a014ca1760 | |||
| fb273f6461 | |||
| 3a18398d83 | |||
| 44c1770fc9 | |||
| 79e9e8ab77 | |||
| 3d5df576d1 | |||
| 074b794c50 | |||
| 4f52d4e474 | |||
| 716352c1b0 | |||
| 6e19a12f6d | |||
| 5d3bae283c | |||
| 6476f158aa | |||
| 420d6b3054 | |||
| c2c96639e2 | |||
| fa4798095e | |||
| 6f1a4b8217 | |||
| 23f210c0ab | |||
| a2f41703d1 | |||
| 4d61d8900d | |||
| cf17a8096d | |||
| 309b2cde51 | |||
| d366664007 | |||
| b9553882b8 | |||
| 334a740ec0 | |||
| 0a87176b74 | |||
| b6fd814542 | |||
| 4c95484c0a | |||
| b6e9a50d22 | |||
| 874f3fc4d3 | |||
| 0457b58dfe | |||
| e13d97cf7e | |||
| 955edf2afd | |||
| 1b48e65e89 | |||
| 93cc5b2210 |
+13
-14
@@ -6,36 +6,35 @@
|
||||
node_modules
|
||||
dist
|
||||
aio/node_modules
|
||||
aio/content/example-playground
|
||||
aio/tools/examples/shared/node_modules
|
||||
aio/tools/examples/shared/example-scaffold
|
||||
|
||||
# All integration test node_modules folders
|
||||
integration/animations/node_modules
|
||||
integration/cli-elements-universal/node_modules
|
||||
integration/cli-hello-world/node_modules
|
||||
integration/cli-hello-world-ivy-compat/node_modules
|
||||
integration/cli-hello-world-ivy-i18n/node_modules
|
||||
integration/cli-hello-world-ivy-minimal/node_modules
|
||||
integration/cli-hello-world-lazy/node_modules
|
||||
integration/cli-hello-world-mocha/node_modules
|
||||
integration/cli-signal-inputs/node_modules
|
||||
integration/defer/node_modules
|
||||
integration/cli-hello-world-ts-47/node_modules
|
||||
integration/dynamic-compiler/node_modules
|
||||
integration/forms/node_modules
|
||||
integration/hello_world__closure/node_modules
|
||||
integration/hello_world__systemjs_umd/node_modules
|
||||
integration/i18n/node_modules
|
||||
integration/injectable-def/node_modules
|
||||
integration/ivy-i18n/node_modules
|
||||
integration/ng-add-localize/node_modules
|
||||
integration/trusted-types/node_modules
|
||||
integration/forms/node_modules
|
||||
integration/ng_elements/node_modules
|
||||
integration/ng_elements_schematics/node_modules
|
||||
integration/ng_update/node_modules
|
||||
integration/ng_update_migrations/node_modules
|
||||
integration/nodenext_resolution/node_modules
|
||||
integration/npm_package_archives.bzl/node_modules
|
||||
integration/ngcc/node_modules
|
||||
integration/platform-server/node_modules
|
||||
integration/service-worker-schema/node_modules
|
||||
integration/side-effects/node_modules
|
||||
integration/standalone-bootstrap/node_modules
|
||||
integration/terser/node_modules
|
||||
integration/trusted-types/node_modules
|
||||
integration/typings_test_rxjs7/node_modules
|
||||
integration/typings_test_ts49/node_modules
|
||||
integration/typings_test_ts50/node_modules
|
||||
integration/typings_test_ts51/node_modules
|
||||
integration/typings_test_ts52/node_modules
|
||||
integration/typings_test_ts36/node_modules
|
||||
integration/typings_test_ts37/node_modules
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Enable debugging tests with --config=debug
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results --spawn_strategy=local
|
||||
|
||||
# Do not attempt to de-flake locally.
|
||||
# On CI we might set this to `3` to run with deflaking.
|
||||
test --flaky_test_attempts=1
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results
|
||||
|
||||
###############################
|
||||
# Filesystem interactions #
|
||||
@@ -112,10 +108,9 @@ build:remote --define=EXECUTOR=remote
|
||||
# Set a higher timeout value, just in case.
|
||||
build:remote --remote_timeout=600
|
||||
|
||||
# Bazel detects maximum number of jobs based on host resources.
|
||||
# Since we run remotely, we can increase this number significantly.
|
||||
common:remote --jobs=200
|
||||
|
||||
# Increase the default number of jobs by 50% because our build has lots of
|
||||
# parallelism
|
||||
build:remote --jobs=150
|
||||
build:remote --google_default_credentials
|
||||
|
||||
# Limit the number of test jobs for on an AIO local deps build. The example tests running
|
||||
@@ -135,20 +130,13 @@ build:remote --platforms=@npm//@angular/build-tooling/bazel/remote-execution:pla
|
||||
|
||||
# Remote instance and caching
|
||||
build:remote --remote_instance_name=projects/internal-200822/instances/primary_instance
|
||||
build:remote --bes_instance_name=internal-200822
|
||||
build:remote --project_id=internal-200822
|
||||
build:remote --remote_cache=remotebuildexecution.googleapis.com
|
||||
build:remote --remote_executor=remotebuildexecution.googleapis.com
|
||||
|
||||
# Use HTTP remote cache
|
||||
build:remote-cache --remote_cache=https://storage.googleapis.com/angular-team-cache
|
||||
build:remote-cache --remote_accept_cached=true
|
||||
build:remote-cache --remote_upload_local_results=true
|
||||
build:remote-cache --google_default_credentials
|
||||
|
||||
# Ensure that tags like "no-remote-exec" get propagated to actions created by rules,
|
||||
# even if the rule implementation does not explicitly pass them to the execution requirements.
|
||||
# Ensure that tags like "no-remote-exec" get propagated to actions created by rules.
|
||||
# https://bazel.build/reference/command-line-reference#flag--experimental_allow_tags_propagation
|
||||
common --experimental_allow_tags_propagation
|
||||
build --experimental_allow_tags_propagation
|
||||
|
||||
# Disable network access in the sandbox by default. To enable network access
|
||||
# for a particular target, use:
|
||||
@@ -177,9 +165,6 @@ test:saucelabs --flaky_test_attempts=1
|
||||
|
||||
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
|
||||
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
|
||||
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
|
||||
# for local development.
|
||||
build --flag_alias=fast_adev=//adev:fast_build_mode
|
||||
|
||||
####################################################
|
||||
# User bazel configuration
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
build --announce_rc
|
||||
|
||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||
# By default this is set in the project `.bazelrc` to `1`.
|
||||
test --flaky_test_attempts=3
|
||||
test --flaky_test_attempts=2
|
||||
|
||||
# More details on failures
|
||||
build --verbose_failures=true
|
||||
|
||||
# CI supports colors but Bazel does not detect it.
|
||||
common --color=yes
|
||||
|
||||
+768
-6
@@ -1,16 +1,778 @@
|
||||
# This config is remaining in place to prevent pull requests failing because of CircleCI config missing.
|
||||
# Configuration file for https://circleci.com/gh/angular/angular
|
||||
|
||||
# Note: YAML anchors allow an object to be re-used, reducing duplication.
|
||||
# The ampersand declares an alias for an object, then later the `<<: *name`
|
||||
# syntax dereferences it.
|
||||
# See https://blog.daemonl.com/2016/02/yaml.html
|
||||
# To validate changes, use an online parser, eg.
|
||||
# https://yaml-online-parser.appspot.com/
|
||||
|
||||
# CircleCI configuration version
|
||||
# Version 2.1 allows for extra config reuse features
|
||||
# https://circleci.com/docs/2.0/reusing-config/#getting-started-with-config-reuse
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
pass:
|
||||
# We don't want to include the current branch name in the cache key because that would prevent
|
||||
# PRs from being able to restore the cache since the branch names are always different for PRs.
|
||||
# The cache key should only consist of dynamic values that change whenever something in the
|
||||
# cache changes. For example:
|
||||
# 1) yarn lock file changes --> cached "node_modules" are different.
|
||||
# 2) bazel repository definitions change --> cached bazel repositories are different.
|
||||
# Windows needs its own cache key because binaries in node_modules are different.
|
||||
# **NOTE 1 **: In order to avoid the cache from growing indefinitely and causing slow-downs, we invalidate the cache monthly.
|
||||
# (See https://support.circleci.com/hc/en-us/articles/360012618473-Creating-a-daily-cache.)
|
||||
# **NOTE 2 **: If you change the cache key prefix, also sync the cache_key_fallback to match.
|
||||
# **NOTE 3 **: Keep the static part of the cache key as prefix to enable correct fallbacks.
|
||||
# See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI.
|
||||
var_3: &cache_key v8-angular-node-16-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}-{{ checksum "aio/yarn.lock" }}
|
||||
# We invalidate the cache if the Bazel version changes because otherwise the `bazelisk` cache
|
||||
# folder will contain all previously used versions and ultimately cause the cache restoring to
|
||||
# be slower due to its growing size.
|
||||
var_4: &cache_key_fallback v8-angular-node-16-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
|
||||
|
||||
# Windows needs its own cache key because binaries in node_modules are different.
|
||||
var_3_win: &cache_key_win v10-angular-win-node-16-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}-{{ checksum "yarn.lock" }}-{{ checksum "WORKSPACE" }}
|
||||
var_4_win: &cache_key_win_fallback v10-angular-win-node-16-{{ checksum "month.txt" }}-{{ checksum ".bazelversion" }}
|
||||
|
||||
# Cache key for the `components-repo-unit-tests` job. **Note** when updating the SHA in the
|
||||
# cache keys also update the SHA for the "COMPONENTS_REPO_COMMIT" environment variable.
|
||||
var_5: &components_repo_unit_tests_cache_key v2-angular-components-{{ checksum "month.txt" }}-87eb708d162e89897e66809c371e3a1e079de962
|
||||
var_6: &components_repo_unit_tests_cache_key_fallback v2-angular-components-{{ checksum "month.txt" }}
|
||||
|
||||
# Workspace initially persisted by the `setup` job, and then enhanced by `build-npm-packages`.
|
||||
# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
|
||||
# https://circleci.com/blog/deep-diving-into-circleci-workspaces/
|
||||
var_7: &workspace_location ~/
|
||||
|
||||
# Filter to run a job on builds for pull requests only.
|
||||
var_8: &only_on_pull_requests
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /pull\/\d+/
|
||||
|
||||
# Filter to skip a job on builds for pull requests.
|
||||
var_9: &skip_on_pull_requests
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- /pull\/\d+/
|
||||
|
||||
# Filter to run a job on builds for the main branch only.
|
||||
var_10: &only_on_main_branch
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
|
||||
# Filter to run a job on all releasable branches.
|
||||
var_11: &only_release_branches
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- /\d+\.\d+\.x/
|
||||
|
||||
# CircleCI orbs
|
||||
# https://circleci.com/developer/orbs.
|
||||
orbs:
|
||||
node: circleci/node@5.0.1
|
||||
devinfra: angular/dev-infra@1.0.6
|
||||
|
||||
# Executor Definitions
|
||||
# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-executors
|
||||
# **NOTE 1**: Pin to exact images using an ID (SHA). See https://circleci.com/docs/2.0/circleci-images/#using-a-docker-image-id-to-pin-an-image-to-a-fixed-version.
|
||||
# (Using the tag in not necessary when pinning by ID, but include it anyway for documentation purposes.)
|
||||
# **NOTE 2**: If you change the version of the docker images, also change the `cache_key` suffix.
|
||||
# **NOTE 3**: If you change the version of Node.js provided by the docker images, also update `.devcontainer/recommended-Dockerfile` to match the new version.
|
||||
executors:
|
||||
default-executor:
|
||||
parameters:
|
||||
resource_class:
|
||||
type: string
|
||||
default: medium
|
||||
docker:
|
||||
- image: cimg/base:2022.05
|
||||
- image: cimg/node:16.13.0
|
||||
resource_class: << parameters.resource_class >>
|
||||
working_directory: ~/ng
|
||||
|
||||
test-browser-executor:
|
||||
parameters:
|
||||
resource_class:
|
||||
type: string
|
||||
default: medium
|
||||
docker:
|
||||
- image: cimg/node:16.13.0-browsers
|
||||
resource_class: << parameters.resource_class >>
|
||||
working_directory: ~/ng
|
||||
|
||||
windows-executor:
|
||||
working_directory: ~/ng
|
||||
resource_class: windows.2xlarge
|
||||
shell: bash
|
||||
machine:
|
||||
image: windows-server-2019-vs2019:201908-02
|
||||
|
||||
# Command Definitions
|
||||
# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-commands
|
||||
commands:
|
||||
custom_attach_workspace:
|
||||
description: Attach workspace at a predefined location
|
||||
steps:
|
||||
- run: echo "This too shall pass (always)"
|
||||
- attach_workspace:
|
||||
at: *workspace_location
|
||||
|
||||
# Install java runtime which is required by some integration tests such as
|
||||
# //integration:hello_world__closure_test, //integration:i18n_test and
|
||||
# //integration:ng_elements_test to run the closure compiler
|
||||
install_java:
|
||||
description: Install java
|
||||
steps:
|
||||
- run:
|
||||
name: Install java
|
||||
command: |
|
||||
sudo apt-get update
|
||||
# Install java runtime
|
||||
sudo apt-get install default-jre
|
||||
|
||||
# Initializes the CI environment by setting up common environment variables.
|
||||
init_environment:
|
||||
description: Initializing environment (setting up variables)
|
||||
steps:
|
||||
- run:
|
||||
name: Set up environment
|
||||
environment:
|
||||
CIRCLE_GIT_BASE_REVISION: << pipeline.git.base_revision >>
|
||||
CIRCLE_GIT_REVISION: << pipeline.git.revision >>
|
||||
command: ./.circleci/env.sh
|
||||
- devinfra/setup-bazel-remote-exec
|
||||
- run:
|
||||
# Configure git as the CircleCI `checkout` command does.
|
||||
# This is needed because we only checkout on the setup job.
|
||||
# Add GitHub to known hosts
|
||||
name: Configure git
|
||||
command: |
|
||||
mkdir -p ~/.ssh
|
||||
echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' >> ~/.ssh/known_hosts
|
||||
git config --global url."ssh://git@github.com".insteadOf "https://github.com" || true
|
||||
git config --global gc.auto 0 || true
|
||||
|
||||
init_saucelabs_environment:
|
||||
description: Sets up a domain that resolves to the local host.
|
||||
steps:
|
||||
- run:
|
||||
name: Preparing environment for running tests on Sauce Labs.
|
||||
command: |
|
||||
# For SauceLabs jobs, we set up a domain which resolves to the machine which launched
|
||||
# the tunnel. We do this because devices are sometimes not able to properly resolve
|
||||
# `localhost` or `127.0.0.1` through the SauceLabs tunnel. Using a domain that does not
|
||||
# resolve to anything on SauceLabs VMs ensures that such requests are always resolved
|
||||
# through the tunnel, and resolve to the actual tunnel host machine (i.e. the CircleCI VM).
|
||||
# More context can be found in: https://github.com/angular/angular/pull/35171.
|
||||
setPublicVar SAUCE_LOCALHOST_ALIAS_DOMAIN "angular-ci.local"
|
||||
setSecretVar SAUCE_ACCESS_KEY $(echo $SAUCE_ACCESS_KEY | rev)
|
||||
- run:
|
||||
# Sets up a local domain in the machine's host file that resolves to the local
|
||||
# host. This domain is helpful in Sauce Labs tests where devices are not able to
|
||||
# properly resolve `localhost` or `127.0.0.1` through the sauce-connect tunnel.
|
||||
name: Setting up alias domain for local host.
|
||||
command: echo "127.0.0.1 $SAUCE_LOCALHOST_ALIAS_DOMAIN" | sudo tee -a /etc/hosts
|
||||
|
||||
save_month_to_file:
|
||||
description: Store the current year and month in a file, so that it can be used for computing the cache key.
|
||||
steps:
|
||||
- run:
|
||||
name: Save month to file
|
||||
# Note: Make sure this file is excluded in the `.gitignore` as otherwise the
|
||||
# snapshot stamping would have the `-with-local-changes` suffix.
|
||||
command: date +%Y-%m > month.txt
|
||||
|
||||
yarn_install:
|
||||
description: Install dependencies
|
||||
steps:
|
||||
- run:
|
||||
name: Running Yarn install
|
||||
command: yarn install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
# Yarn's requests sometimes take more than 10mins to complete (especially on Windows).
|
||||
no_output_timeout: 45m
|
||||
|
||||
notify_webhook_on_fail:
|
||||
description: Notify a webhook about failure
|
||||
parameters:
|
||||
# `webhook_url_env_var` are secret env vars defined in CircleCI project settings.
|
||||
# The URLs come from https://angular-team.slack.com/apps/A0F7VRE7N-circleci.
|
||||
webhook_url_env_var:
|
||||
type: env_var_name
|
||||
steps:
|
||||
- run:
|
||||
when: on_fail
|
||||
command: |
|
||||
notificationJson="{\"text\":\":x: \`$CIRCLE_JOB\` job for $CIRCLE_BRANCH branch failed on build $CIRCLE_BUILD_NUM: $CIRCLE_BUILD_URL :scream:\"}"
|
||||
curl --request POST --header "Content-Type: application/json" --data "$notificationJson" ${<< parameters.webhook_url_env_var >>}
|
||||
|
||||
# Job definitions
|
||||
# Jobs can include parameters that are passed in the workflow job invocation.
|
||||
# https://circleci.com/docs/2.0/reusing-config/#authoring-parameterized-jobs
|
||||
jobs:
|
||||
setup:
|
||||
executor: default-executor
|
||||
steps:
|
||||
- checkout
|
||||
- save_month_to_file
|
||||
- init_environment
|
||||
- devinfra/rebase-pr-on-target-branch:
|
||||
base_revision: << pipeline.git.base_revision >>
|
||||
head_revision: << pipeline.git.revision >>
|
||||
# This cache is saved in the build-npm-packages so that Bazel cache is also included.
|
||||
- restore_cache:
|
||||
keys:
|
||||
- *cache_key
|
||||
- *cache_key_fallback
|
||||
- yarn_install
|
||||
- run: yarn --cwd aio install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
# Make the bazel directories and add a file to them if they don't exist already so that
|
||||
# persist_to_workspace does not fail.
|
||||
- run: |
|
||||
if [ ! -d ~/bazel_repository_cache ]; then
|
||||
mkdir ~/bazel_repository_cache
|
||||
touch ~/bazel_repository_cache/MARKER
|
||||
fi
|
||||
# Persist any changes at this point to be reused by further jobs.
|
||||
# **NOTE**: To add new content to the workspace, always persist on the same root.
|
||||
- persist_to_workspace:
|
||||
root: *workspace_location
|
||||
paths:
|
||||
- ./ng
|
||||
- ./bazel_repository_cache
|
||||
|
||||
lint:
|
||||
executor: default-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
|
||||
- run: yarn -s tslint
|
||||
- run: yarn -s --cwd aio lint
|
||||
- run: yarn -s ng-dev format changed $CI_GIT_BASE_REVISION --check
|
||||
- run: yarn -s ts-circular-deps:check
|
||||
- run: yarn -s ng-dev pullapprove verify
|
||||
- run: yarn -s ng-dev ngbot verify
|
||||
- run: yarn -s ng-dev commit-message validate-range $CI_GIT_BASE_REVISION $CI_GIT_REVISION
|
||||
- run: yarn -s check-tooling-setup
|
||||
|
||||
test:
|
||||
executor:
|
||||
name: test-browser-executor
|
||||
# Now that large integration tests are running locally in parallel (they can't run on RBE yet
|
||||
# as they require network access for yarn install), this test is running out of memory
|
||||
# consistently with the xlarge machine.
|
||||
# TODO: switch back to xlarge once integration tests are running on remote-exec
|
||||
resource_class: 2xlarge+
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_java
|
||||
- run:
|
||||
# We need to explicitly specify the --symlink_prefix option because otherwise we would
|
||||
# not be able to easily find the output bin directory when uploading artifacts for size
|
||||
# measurements.
|
||||
command: yarn test:ci
|
||||
no_output_timeout: 20m
|
||||
# Publish bundle artifacts which will be used to calculate the size change. **Note**: Make
|
||||
# sure that the size plugin from the Angular robot fetches the artifacts from this CircleCI
|
||||
# job (see .github/angular-robot.yml). Additionally any artifacts need to be stored with the
|
||||
# following path format: "{projectName}/{context}/{fileName}". This format is necessary
|
||||
# because otherwise the bot is not able to pick up the artifacts from CircleCI. See:
|
||||
# https://github.com/angular/github-robot/blob/master/functions/src/plugins/size.ts#L392-L394
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js
|
||||
destination: core/hello_world/bundle
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js
|
||||
destination: core/todo/bundle
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js.br
|
||||
destination: core/hello_world/bundle.br
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js.br
|
||||
destination: core/todo/bundle.br
|
||||
|
||||
# NOTE: This is currently limited to main builds only. See the `monitoring` configuration.
|
||||
saucelabs:
|
||||
executor:
|
||||
name: default-executor
|
||||
# In order to avoid the bottleneck of having a slow host machine, we acquire a better
|
||||
# container for this job. This is necessary because we launch a lot of browsers concurrently
|
||||
# and therefore the tunnel and Karma need to process a lot of file requests and tests.
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- init_saucelabs_environment
|
||||
- run:
|
||||
name: Run Bazel tests on Saucelabs
|
||||
# See /tools/saucelabs/README.md for more info
|
||||
command: |
|
||||
yarn bazel run //tools/saucelabs:sauce_service_setup
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
yarn bazel test --config=saucelabs ${TESTS}
|
||||
yarn bazel run //tools/saucelabs:sauce_service_stop
|
||||
no_output_timeout: 40m
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
|
||||
|
||||
test_aio:
|
||||
executor:
|
||||
name: test-browser-executor
|
||||
resource_class: 2xlarge+
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
|
||||
# Run all aio tests
|
||||
- run:
|
||||
command: yarn --cwd aio test:ci
|
||||
no_output_timeout: 20m
|
||||
|
||||
# Check the bundle sizes.
|
||||
- run: yarn --cwd aio payload-size
|
||||
|
||||
deploy_aio:
|
||||
executor:
|
||||
name: test-browser-executor
|
||||
resource_class: 2xlarge+
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
# Deploy angular.io to production (if necessary)
|
||||
- run: setPublicVar_CI_STABLE_BRANCH
|
||||
- run: yarn --cwd aio deploy-production
|
||||
|
||||
test_aio_local:
|
||||
executor:
|
||||
name: test-browser-executor
|
||||
resource_class: 2xlarge+
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
# Run all aio tests (with local Angular packages)
|
||||
- run:
|
||||
command: yarn --cwd aio test-local:ci
|
||||
no_output_timeout: 20m
|
||||
|
||||
# Check the bundle sizes.
|
||||
- run: yarn --cwd aio payload-size aio-local
|
||||
|
||||
# The `build-npm-packages` tasks exist for backwards-compatibility with old scripts and
|
||||
# tests that rely on the pre-Bazel `dist/packages-dist` output structure (build.sh).
|
||||
# Having multiple jobs that independently build in this manner duplicates some work; we build
|
||||
# the bazel packages more than once. Even though we have a remote cache, these jobs will
|
||||
# typically run in parallel so up-to-date outputs will not be available at the time the build
|
||||
# starts.
|
||||
build-npm-packages:
|
||||
executor:
|
||||
name: default-executor
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- run: yarn build
|
||||
|
||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||
- persist_to_workspace:
|
||||
root: *workspace_location
|
||||
paths:
|
||||
- ng/dist/packages-dist
|
||||
- ng/dist/angular-in-memory-web-api-dist
|
||||
- ng/dist/zone.js-dist
|
||||
|
||||
# Save dependencies and bazel repository cache to use on subsequent runs.
|
||||
- save_cache:
|
||||
key: *cache_key
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- ~/bazel_repository_cache
|
||||
- ~/.cache/bazelisk
|
||||
|
||||
# This job creates compressed tarballs (`.tgz` files) for all Angular packages and stores them as
|
||||
# build artifacts. This makes it easy to try out changes from a PR build for testing purposes.
|
||||
# More info CircleCI build artifacts: https://circleci.com/docs/2.0/artifacts
|
||||
#
|
||||
# NOTE: Currently, this job only runs for PR builds. See `publish_snapshot` for non-PR builds.
|
||||
publish_packages_as_artifacts:
|
||||
executor: default-executor
|
||||
environment:
|
||||
AIMWA_PACKAGES_DIR: &aimwa_packages_dir 'dist/angular-in-memory-web-api-dist'
|
||||
AIMWA_PACKAGES_ARCHIVES_DIR: &aimwa_packages_archives_dir 'dist/angular-in-memory-web-api-dist-archives'
|
||||
NG_PACKAGES_DIR: &ng_packages_dir 'dist/packages-dist'
|
||||
NG_PACKAGES_ARCHIVES_DIR: &ng_packages_archives_dir 'dist/packages-dist-archives'
|
||||
ZONEJS_PACKAGES_DIR: &zonejs_packages_dir 'dist/zone.js-dist'
|
||||
ZONEJS_PACKAGES_ARCHIVES_DIR: &zonejs_packages_archives_dir 'dist/zone.js-dist-archives'
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
# Publish `@angular/*` packages.
|
||||
- run:
|
||||
name: Create artifacts for @angular/* packages
|
||||
command: ./scripts/ci/create-package-archives.sh $CI_BRANCH $CI_COMMIT $NG_PACKAGES_DIR $NG_PACKAGES_ARCHIVES_DIR
|
||||
- store_artifacts:
|
||||
path: *ng_packages_archives_dir
|
||||
destination: angular
|
||||
# Publish the `angular-in-memory-web-api` package.
|
||||
- run:
|
||||
name: Create artifacts for the `angular-in-memory-web-api` package
|
||||
# Need to remove the zone.js.tgz before archive
|
||||
command: ./scripts/ci/create-package-archives.sh $CI_BRANCH $CI_COMMIT $AIMWA_PACKAGES_DIR $AIMWA_PACKAGES_ARCHIVES_DIR
|
||||
- store_artifacts:
|
||||
path: *aimwa_packages_archives_dir
|
||||
destination: angular-in-memory-web-api
|
||||
# Publish the `zone.js` package.
|
||||
- run:
|
||||
name: Create artifacts for the `zone.js` package
|
||||
# Need to remove the zone.js.tgz before archive
|
||||
command: rm -rf $ZONEJS_PACKAGES_DIR/archive && ./scripts/ci/create-package-archives.sh $CI_BRANCH $CI_COMMIT $ZONEJS_PACKAGES_DIR $ZONEJS_PACKAGES_ARCHIVES_DIR
|
||||
- store_artifacts:
|
||||
path: *zonejs_packages_archives_dir
|
||||
destination: zone.js
|
||||
|
||||
# This job updates the content of repos like github.com/angular/core-builds
|
||||
# for every green build on angular/angular.
|
||||
publish_snapshot:
|
||||
executor: default-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
# CircleCI has a config setting to force SSH for all github connections
|
||||
# This is not compatible with our mechanism of using a Personal Access Token
|
||||
# Clear the global setting
|
||||
- run: git config --global --unset "url.ssh://git@github.com.insteadof"
|
||||
- run:
|
||||
name: Decrypt github credentials
|
||||
# We need ensure that the same default digest is used for encoding and decoding with
|
||||
# OpenSSL. OpenSSL versions might have different default digests which can cause
|
||||
# decryption failures based on the installed OpenSSL version. https://stackoverflow.com/a/39641378/4317734
|
||||
command: 'openssl aes-256-cbc -d -in .circleci/github_token -md md5 -k "${KEY}" -out ~/.git_credentials'
|
||||
- run: ./scripts/ci/publish-build-artifacts.sh
|
||||
|
||||
aio_misc:
|
||||
executor: default-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- run:
|
||||
name: Check website provided in contributors.json file
|
||||
command: yarn node aio/scripts/test-external-urls.js
|
||||
|
||||
aio_monitoring_stable:
|
||||
executor: test-browser-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- run: setPublicVar_CI_STABLE_BRANCH
|
||||
- run:
|
||||
name: Check out `aio/` and yarn from the stable branch
|
||||
command: |
|
||||
git fetch origin $CI_STABLE_BRANCH
|
||||
git checkout --force origin/$CI_STABLE_BRANCH -- aio/ .yarn/ .yarnrc
|
||||
# Ignore yarn's engines check, because we checked out `aio/package.json` from the stable
|
||||
# branch and there could be a node version skew, which is acceptable in this monitoring job.
|
||||
- run: yarn config set ignore-engines true
|
||||
- run:
|
||||
name: Run tests against https://angular.io/
|
||||
command: ./aio/scripts/test-production.sh https://angular.io/ $CI_AIO_MIN_PWA_SCORE
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_CARETAKER_WEBHOOK_URL
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
|
||||
|
||||
aio_monitoring_next:
|
||||
executor: test-browser-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- run:
|
||||
name: Run tests against https://next.angular.io/
|
||||
command: ./aio/scripts/test-production.sh https://next.angular.io/ $CI_AIO_MIN_PWA_SCORE
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_CARETAKER_WEBHOOK_URL
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
|
||||
|
||||
legacy-unit-tests-saucelabs:
|
||||
executor:
|
||||
name: default-executor
|
||||
# In order to avoid the bottleneck of having a slow host machine, we acquire a better
|
||||
# container for this job. This is necessary because we launch a lot of browsers concurrently
|
||||
# and therefore the tunnel and Karma need to process a lot of file requests and tests.
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- init_saucelabs_environment
|
||||
- run:
|
||||
name: Starting Saucelabs tunnel service
|
||||
command: ./tools/saucelabs/sauce-service.sh run
|
||||
background: true
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- run:
|
||||
name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
command: |
|
||||
yarn bazel build \
|
||||
//packages/core/test:downleveled_es5_fixture \
|
||||
//packages/common/locales
|
||||
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
mkdir -p dist/legacy-test-out/core/test/reflection/
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
# Locale files are needed for i18n tests running within Saucelabs. These are added
|
||||
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
|
||||
# can succeed. Note that the base locale and currencies files are checked-in, so
|
||||
# we do not need to re-generate those through Bazel.
|
||||
mkdir -p packages/common/locales/extra
|
||||
cp dist/bin/packages/common/locales/*.ts packages/common/locales
|
||||
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
|
||||
# Build ZoneJS so that it can be loaded globally in the Karma tests.
|
||||
- run: yarn bazel build //packages/zone.js:npm_package
|
||||
# Build the bundle for all tests to run within Saucelabs
|
||||
- run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
|
||||
- run:
|
||||
# Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready.
|
||||
name: Waiting for Saucelabs tunnel to connect
|
||||
command: ./tools/saucelabs/sauce-service.sh ready-wait
|
||||
- run:
|
||||
name: Running tests on Saucelabs.
|
||||
command: |
|
||||
KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
|
||||
- run:
|
||||
name: Stop Saucelabs tunnel service
|
||||
command: ./tools/saucelabs/sauce-service.sh stop
|
||||
|
||||
# Job that runs all unit tests of the `angular/components` repository.
|
||||
components-repo-unit-tests:
|
||||
executor:
|
||||
name: default-executor
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
# Restore the cache before cloning the repository because the clone script re-uses
|
||||
# the restored repository if present. This reduces the amount of times the components
|
||||
# repository needs to be cloned (this is slow and increases based on commits in the repo).
|
||||
- restore_cache:
|
||||
keys:
|
||||
- *components_repo_unit_tests_cache_key
|
||||
# Whenever the `angular/components` SHA is updated, the cache key will no longer
|
||||
# match. The fallback cache will still match, and CircleCI will restore the most
|
||||
# recently cached repository folder. Without the fallback cache, we'd need to download
|
||||
# the repository from scratch and it would slow down the job. This is because we can't
|
||||
# clone the repository with reduced `--depth`, but rather need to clone the whole
|
||||
# repository to be able to support arbitrary SHAs.
|
||||
- *components_repo_unit_tests_cache_key_fallback
|
||||
- run:
|
||||
name: 'Fetching angular/components repository'
|
||||
command: ./scripts/ci/clone_angular_components_repo.sh
|
||||
- run:
|
||||
# Run yarn install to fetch the Bazel binaries as used in the components repo.
|
||||
name: Installing dependencies.
|
||||
command: yarn --cwd ${COMPONENTS_REPO_TMP_DIR} install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
- save_cache:
|
||||
key: *components_repo_unit_tests_cache_key
|
||||
paths:
|
||||
# Temporary directory must be kept in sync with the `$COMPONENTS_REPO_TMP_DIR` env
|
||||
# variable. It needs to be hardcoded here, because env variables interpolation is
|
||||
# not supported.
|
||||
- '/tmp/angular-components-repo'
|
||||
- run:
|
||||
# Copy zone.js package to dist/packages-dist, so `angular/components` can also use the local zone.js build.
|
||||
name: Setting up zone.js release packages.
|
||||
command: cp -r dist/zone.js-dist/zone.js dist/packages-dist/
|
||||
- run:
|
||||
# Updates the `angular/components` `package.json` file to refer to the release output
|
||||
# inside the `packages-dist` directory.
|
||||
name: Setting up framework release packages.
|
||||
command: node scripts/ci/update-framework-deps-to-dist-packages.js ${COMPONENTS_REPO_TMP_DIR}/package.json dist/packages-dist/
|
||||
- run:
|
||||
# Run `yarn install` again to install the Angular packages from `packages-dist/` and update the lockfile.
|
||||
# NOTE: We cannot rely on Bazel to run `yarn install`, because it uses the `--frozen-lockfile` flag and fails.
|
||||
name: Installing local Angular packages.
|
||||
command: yarn --cwd ${COMPONENTS_REPO_TMP_DIR} install --non-interactive --cache-folder ~/.cache/yarn
|
||||
- run:
|
||||
name: 'Running `angular/components` unit tests'
|
||||
command: ./scripts/ci/run_angular_components_unit_tests.sh
|
||||
|
||||
test_zonejs:
|
||||
executor:
|
||||
name: default-executor
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- install_java
|
||||
# Install
|
||||
- run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
# Run zone.js tools tests
|
||||
- run: yarn --cwd packages/zone.js promisetest
|
||||
- run: yarn --cwd packages/zone.js promisefinallytest
|
||||
- run: yarn bazel build //packages/zone.js:npm_package &&
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/ &&
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/ &&
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js ./packages/zone.js/build/test/closure/zone.js
|
||||
- run: yarn --cwd packages/zone.js jest:test
|
||||
- run: yarn --cwd packages/zone.js jest:nodetest
|
||||
- run: yarn --cwd packages/zone.js electrontest
|
||||
- run: yarn --cwd packages/zone.js closuretest
|
||||
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive --cache-folder ~/.cache/yarn
|
||||
- run: yarn --cwd packages/zone.js/test/typings test
|
||||
|
||||
# Windows jobs
|
||||
# Docs: https://circleci.com/docs/2.0/hello-world-windows/
|
||||
test_win:
|
||||
executor: windows-executor
|
||||
steps:
|
||||
- checkout
|
||||
- save_month_to_file
|
||||
- init_environment
|
||||
|
||||
# Setup the proper NodeJS version. The image comes pre-installed with `nvm-windows`.
|
||||
- run:
|
||||
name: Install NodeJS version from `.nvmrc`.
|
||||
command: |
|
||||
nvm install $(cat .nvmrc)
|
||||
nvm use $(cat .nvmrc)
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- *cache_key_win
|
||||
- *cache_key_win_fallback
|
||||
|
||||
- run:
|
||||
name: Debug information for flakiness of Windows job
|
||||
command: |
|
||||
node -v
|
||||
yarn -v
|
||||
which node
|
||||
which yarn
|
||||
echo "Path: $PATH"
|
||||
yarn node -v
|
||||
|
||||
# Install project dependencies, and install Bazelisk globally. This is necessary as
|
||||
# Windows might error when `bazel` is invoked from the project node modules. The Bazel
|
||||
# invocation might modify the symlinked project `node_modules` again, causing failures.
|
||||
- yarn_install
|
||||
- run: yarn global add @bazel/bazelisk@${BAZELISK_VERSION}
|
||||
|
||||
- run:
|
||||
name: Build all windows CI targets
|
||||
command: |
|
||||
$(yarn global bin)/bazelisk build //packages/compiler-cli/...
|
||||
no_output_timeout: 15m
|
||||
- run:
|
||||
name: Test all windows CI targets
|
||||
command: |
|
||||
$(yarn global bin)/bazelisk test --test_tag_filters="-browser:chromium-local" //packages/compiler-cli/...
|
||||
no_output_timeout: 15m
|
||||
|
||||
- save_cache:
|
||||
key: *cache_key_win
|
||||
paths:
|
||||
- node_modules
|
||||
- ~/AppData/Local/bazelisk
|
||||
|
||||
test_angular_devtools:
|
||||
executor: test-browser-executor
|
||||
steps:
|
||||
- custom_attach_workspace
|
||||
- init_environment
|
||||
- run:
|
||||
name: Run unit tests
|
||||
command: yarn devtools:test
|
||||
- run:
|
||||
name: Test production build
|
||||
command: yarn devtools:build:chrome
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
default_workflow:
|
||||
jobs:
|
||||
- pass
|
||||
- setup:
|
||||
filters:
|
||||
branches:
|
||||
ignore: g3
|
||||
- lint:
|
||||
requires:
|
||||
- setup
|
||||
- test:
|
||||
requires:
|
||||
- setup
|
||||
- build-npm-packages:
|
||||
requires:
|
||||
- setup
|
||||
- legacy-unit-tests-saucelabs:
|
||||
requires:
|
||||
- setup
|
||||
- test_aio:
|
||||
requires:
|
||||
- setup
|
||||
- deploy_aio:
|
||||
requires:
|
||||
- test_aio
|
||||
- test_aio_local:
|
||||
requires:
|
||||
- setup
|
||||
- publish_packages_as_artifacts:
|
||||
requires:
|
||||
- build-npm-packages
|
||||
- publish_snapshot:
|
||||
<<: *only_release_branches
|
||||
requires:
|
||||
# Only publish if tests and integration tests pass
|
||||
- test
|
||||
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
|
||||
- test_aio_local
|
||||
# Get the artifacts to publish from the build-packages-dist job
|
||||
# since the publishing script expects the legacy outputs layout.
|
||||
- build-npm-packages
|
||||
- legacy-unit-tests-saucelabs
|
||||
- components-repo-unit-tests:
|
||||
requires:
|
||||
- build-npm-packages
|
||||
- test_zonejs:
|
||||
requires:
|
||||
- setup
|
||||
- test_win:
|
||||
requires:
|
||||
- setup
|
||||
- test_angular_devtools:
|
||||
requires:
|
||||
- setup
|
||||
|
||||
monitoring:
|
||||
jobs:
|
||||
- setup
|
||||
- aio_misc:
|
||||
requires:
|
||||
- setup
|
||||
- aio_monitoring_stable:
|
||||
requires:
|
||||
- setup
|
||||
- aio_monitoring_next:
|
||||
requires:
|
||||
- setup
|
||||
- saucelabs:
|
||||
# Testing saucelabs via Bazel currently taking longer than the legacy saucelabs job as it
|
||||
# each karma_web_test target is provisioning and tearing down browsers which is adding
|
||||
# a lot of overhead. Running once daily in the main branch to avoid wasting resources and
|
||||
# slowing down CI for PRs.
|
||||
# TODO: Run this job on all branches (including PRs) once karma_web_test targets can
|
||||
# share provisioned browsers and we can remove the legacy saucelabs job.
|
||||
requires:
|
||||
- setup
|
||||
triggers:
|
||||
- schedule:
|
||||
<<: *only_on_main_branch
|
||||
# Runs monitoring jobs at 10:00AM every day.
|
||||
cron: '0 10 * * *'
|
||||
|
||||
@@ -61,6 +61,20 @@ setPublicVar SAUCE_TUNNEL_IDENTIFIER "angular-framework-${CIRCLE_BUILD_NUM}-${CI
|
||||
# acquire CircleCI instances for too long if sauceconnect failed, we need a connect timeout.
|
||||
setPublicVar SAUCE_READY_FILE_TIMEOUT 120
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Define environment variables for the `angular/components` repo unit tests job.
|
||||
####################################################################################################
|
||||
# We specifically use a directory within "/tmp" here because we want the cloned repo to be
|
||||
# completely isolated from angular/angular in order to avoid any bad interactions between
|
||||
# their separate build setups. **NOTE**: When updating the temporary directory, also update
|
||||
# the `save_cache` path configuration in `config.yml`
|
||||
setPublicVar COMPONENTS_REPO_TMP_DIR "/tmp/angular-components-repo"
|
||||
setPublicVar COMPONENTS_REPO_URL "https://github.com/angular/components.git"
|
||||
setPublicVar COMPONENTS_REPO_BRANCH "main"
|
||||
# **NOTE**: When updating the commit SHA, also update the cache key in the CircleCI `config.yml`.
|
||||
setPublicVar COMPONENTS_REPO_COMMIT "87eb708d162e89897e66809c371e3a1e079de962"
|
||||
|
||||
####################################################################################################
|
||||
# Create shell script in /tmp for Bazel actions to access CI envs without
|
||||
# busting the cache. Used by payload-size.sh script in integration tests.
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
# Image metadata and config.
|
||||
# Ideally, the Node.js version should match what we use on CI.
|
||||
# See `executors > default-executor` in `.circleci/config.yml`.
|
||||
FROM cimg/node:18.13.0@sha256:45d0ee279f522c4562a464df71108104cc9b2187205333ffd0707d4f05589731
|
||||
FROM cimg/node:14-browsers
|
||||
|
||||
|
||||
LABEL name="Angular dev environment" \
|
||||
|
||||
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
|
||||
- uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # tag=v3
|
||||
with:
|
||||
path: |
|
||||
./node_modules/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Configuration for angular-robot
|
||||
|
||||
# options for the size plugin
|
||||
#options for the size plugin
|
||||
size:
|
||||
disabled: true
|
||||
disabled: false
|
||||
maxSizeIncrease: 2000
|
||||
circleCiStatusName: 'ci/circleci: test'
|
||||
|
||||
@@ -68,6 +68,9 @@ merge:
|
||||
|
||||
# list of PR statuses that need to be successful
|
||||
requiredStatuses:
|
||||
- 'ci/circleci: build'
|
||||
- 'ci/circleci: lint'
|
||||
- 'ci/angular: size'
|
||||
- 'google-internal-tests'
|
||||
- 'pullapprove'
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# This workflow builds the previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build adev for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
adev-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'adev: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/build/browser'
|
||||
@@ -1,50 +0,0 @@
|
||||
# This workflow runs whenever the ADEV build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying adev preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build adev for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
# Needed in order to checkout the repository
|
||||
contents: read
|
||||
# Needed in order to retrieve the artifacts from the previous job
|
||||
actions: read
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
firebase-config-dir: './adev'
|
||||
firebase-public-dir: './adev/build/browser'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -0,0 +1,42 @@
|
||||
# This workflow builds the AIO previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build AIO app for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
aio-build:
|
||||
runs-on: ubuntu-latest
|
||||
# We only want to build and deploy the AIO app if the `aio: preview` label has been
|
||||
# added, or if the label is already applied and new changes have been made in the PR.
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'aio: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'aio: preview'))
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/setup-bazel-remote-exec@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
# Build the web package. Note: We run Bazel from a low-resource Github action container,
|
||||
# so we manually need to instruct Bazel to run more actions concurrently as by default
|
||||
# the number of concurrent actions is determined based on the host resources.
|
||||
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
|
||||
|
||||
- uses: angular/dev-infra/github-actions/deploy-previews/pack-and-upload-artifact@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
workflow-artifact-name: 'aio'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/aio/build'
|
||||
@@ -0,0 +1,45 @@
|
||||
# This workflow runs whenever the AIO build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying AIO preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build AIO app for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
# TODO(pgschwendtner): use a different project for framework previews here.
|
||||
PREVIEW_PROJECT: ng-comp-dev
|
||||
|
||||
jobs:
|
||||
aio-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: aio/
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
# The default site is used as we don't use multi-sites in the project.
|
||||
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
|
||||
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_PROJECT}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/deploy-previews/upload-artifacts-to-firebase@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'aio'
|
||||
firebase-config-dir: './aio/'
|
||||
firebase-public-dir: './aio/dist'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -13,9 +13,9 @@ jobs:
|
||||
assistant_to_the_branch_manager:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
name: Run benchmark comparison
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
benchmark-compare:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/benchmark-compare ')}}
|
||||
steps:
|
||||
- uses: TheModdingInquisition/actions-team-membership@a69636a92bc927f32c3910baac06bacc949c984c # v1.0
|
||||
with:
|
||||
team: 'team'
|
||||
organization: angular
|
||||
token: ${{secrets.BENCHMARK_COMPARE_MEMBERSHIP_GITHUB_TOKEN}}
|
||||
exit: true
|
||||
|
||||
# Indicate that the benchmark command was received.
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
with:
|
||||
comment-id: ${{github.event.comment.id}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
reactions: 'rocket'
|
||||
|
||||
- uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1
|
||||
id: comment-branch
|
||||
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
# Specify repository as the PR branch might be from a fork.
|
||||
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
|
||||
# Checkout the pull request and assume it being trusted given we've checked
|
||||
# that the action was triggered by a team member.
|
||||
ref: ${{steps.comment-branch.outputs.head_ref}}
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
- name: Preparing benchmark for GitHub action
|
||||
id: info
|
||||
env:
|
||||
# Untrusted input used in an executable code, must be wrapped as an env var to prevent injections
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: yarn benchmarks prepare-for-github-action "$COMMENT_BODY"
|
||||
|
||||
- run: yarn benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}}
|
||||
id: benchmark
|
||||
name: Running benchmark
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
with:
|
||||
issue-number: ${{github.event.issue.number}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
body: |
|
||||
## Benchmark Test Results
|
||||
**Test**: `${{steps.info.outputs.benchmarkTarget}}`
|
||||
|
||||
### PR (${{steps.info.outputs.prHeadSha}})
|
||||
${{steps.benchmark.outputs.workingStageResultsText}}
|
||||
|
||||
### Compare Ref (${{steps.info.outputs.compareSha}})
|
||||
${{steps.benchmark.outputs.comparisonResultsText}}
|
||||
@@ -1,64 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
saucelabs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Starting Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh run &
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
run: |
|
||||
yarn bazel build //packages/core/test:downleveled_es5_fixture //packages/common/locales
|
||||
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
|
||||
mkdir -p dist/legacy-test-out/core/test/reflection/
|
||||
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
|
||||
dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js
|
||||
# Locale files are needed for i18n tests running within Saucelabs. These are added
|
||||
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
|
||||
# can succeed. Note that the base locale and currencies files are checked-in, so
|
||||
# we do not need to re-generate those through Bazel.
|
||||
mkdir -p packages/common/locales/extra
|
||||
cp dist/bin/packages/common/locales/*.ts packages/common/locales
|
||||
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
|
||||
- name: Build bundle of tests to run on Saucelabs
|
||||
run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
|
||||
- name: Wait and confirm Saucelabs tunnel has connected
|
||||
run: ./tools/saucelabs/sauce-service.sh ready-wait
|
||||
timeout-minutes: 3
|
||||
- name: Running tests on Saucelabs.
|
||||
run: KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run
|
||||
- name: Stop Saucelabs tunnel service
|
||||
run: ./tools/saucelabs/sauce-service.sh stop
|
||||
@@ -1,231 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./node_modules
|
||||
./aio/node_modules
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Check code lint
|
||||
run: yarn -s tslint
|
||||
- name: Check for circular dependencies
|
||||
run: yarn -s ts-circular-deps:check
|
||||
- name: Validate pull approve configuration
|
||||
run: yarn -s ng-dev pullapprove verify
|
||||
- name: Validate angular robot configuration
|
||||
run: yarn -s ng-dev ngbot verify
|
||||
- name: Confirm code builds with typescript as expected
|
||||
run: yarn -s check-tooling-setup
|
||||
- name: Check commit message
|
||||
# Commit message validation is only done on pull requests as its too late to validate once
|
||||
# it has been merged.
|
||||
if: github.event_name == 'pull_request'
|
||||
run: yarn ng-dev commit-message validate-range ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}
|
||||
- name: Check code format
|
||||
# Code formatting checks are only done on pull requests as its too late to validate once
|
||||
# it has been merged.
|
||||
if: github.event_name == 'pull_request'
|
||||
run: yarn ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
devtools:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
run: yarn devtools:test
|
||||
- name: Test build
|
||||
run: yarn devtools:build:chrome
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000
|
||||
- name: Run CI tests for framework
|
||||
run: yarn test:ci
|
||||
|
||||
adev:
|
||||
runs-on:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel test --config=aio_local_deps //adev:test
|
||||
|
||||
publish-snapshots:
|
||||
if: github.event_name == 'push'
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
- run: yarn build
|
||||
- run: ./scripts/ci/publish-build-artifacts.sh
|
||||
|
||||
zone-js:
|
||||
runs-on:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./node_modules
|
||||
./packages/zone.js/node_modules
|
||||
./packages/zone.js/test/typings/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
yarn bazel build \
|
||||
//packages/zone.js/bundles:zone.umd.js \
|
||||
//packages/zone.js:npm_package \
|
||||
//packages/zone.js/test/closure:closure_js \
|
||||
|
||||
- run: |
|
||||
rm -Rf packages/zone.js/build
|
||||
rm -Rf packages/zone.js/test/extra/*.umd.js
|
||||
|
||||
mkdir -p packages/zone.js/build/
|
||||
mkdir -p packages/zone.js/build/test/
|
||||
|
||||
cp dist/bin/packages/zone.js/bundles/zone.umd.js packages/zone.js/build/zone.umd.js
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/
|
||||
cp dist/bin/packages/zone.js/test/closure/zone.closure.mjs ./packages/zone.js/build/test/zone.closure.mjs
|
||||
|
||||
# Install
|
||||
- run: yarn --cwd packages/zone.js install --frozen-lockfile --non-interactive
|
||||
# Run zone.js tools tests
|
||||
- run: yarn --cwd packages/zone.js promisefinallytest
|
||||
- run: yarn --cwd packages/zone.js jest:test
|
||||
- run: yarn --cwd packages/zone.js jest:nodetest
|
||||
- run: yarn --cwd packages/zone.js electrontest
|
||||
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive
|
||||
- run: yarn --cwd packages/zone.js/test/typings test
|
||||
|
||||
bazel-saucelabs:
|
||||
# TODO: Replace legacy job with Bazel saucelabs job.
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
SAUCE_USERNAME: ${{ vars.SAUCE_USERNAME }}
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
- name: Run all saucelabs bazel tests
|
||||
run: |
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
yarn bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
|
||||
|
||||
adev-deploy:
|
||||
needs: [adev]
|
||||
if: needs.adev.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'
|
||||
env:
|
||||
DEPLOY_PROJECT: angular-dev-site
|
||||
DEPLOY_SITE: angular-dev-site
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
- name: Move generated project files into deployment directory
|
||||
run: cp -r dist/bin/adev/build adev/build
|
||||
- name: Setup firebase context
|
||||
run: |
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs ${{ env.DEPLOY_SITE }}
|
||||
- name: Deploy to firebase
|
||||
uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 # v0.7.1
|
||||
with:
|
||||
entryPoint: './adev'
|
||||
firebaseServiceAccount: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
|
||||
projectId: ${{ env.DEPLOY_PROJECT }}
|
||||
channelId: live
|
||||
@@ -12,14 +12,14 @@ jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
post_approval_changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Feature request triage bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 14:00 every day
|
||||
- cron: '0 14 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
feature_triage:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/feature-request@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
@@ -4,18 +4,17 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
permissions:
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
run-tests-guide-url: http://go/angular/g3sync
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sync-config: ./.ng-dev/google-sync-config.json
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Lock closed inactive issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 16:00 every day
|
||||
- cron: '0 16 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lock_closed:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/lock-closed@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }}
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Merge Ready
|
||||
|
||||
on: pull_request_target
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Monitoring
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 10:00AM every day.
|
||||
- cron: '0 10 * * *'
|
||||
|
||||
permissions: {}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
check_aio:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MINIMUM_PWA_SCORE: 95
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ['https://next.angular.io/', 'https://angular.io/']
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Run basic e2e and deployment config tests.
|
||||
run: yarn --cwd aio test-production-url --test_env=TARGET_URL="${{ matrix.version }}"
|
||||
- name: Run PWA-score tests.
|
||||
run: yarn --cwd aio test-pwa-score "${{ matrix.version }}" "${{ env.MINIMUM_PWA_SCORE }}"
|
||||
- name: Run a11y tests.
|
||||
run: yarn --cwd aio test-a11y-score "${{ matrix.version }}"
|
||||
|
||||
check_contributor_links:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Check website provided in contributors.json file
|
||||
run: node aio/scripts/test-external-urls.js
|
||||
@@ -25,12 +25,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Checkout code'
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # tag=v2.0.6
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3.1.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
|
||||
uses: github/codeql-action/upload-sarif@b2a92eb56d8cb930006a1c6ed86b0782dd8a4297 # tag=v2.1.35
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Update AIO Angular CLI help
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_cli_help:
|
||||
name: Update Angular CLI help (if necessary)
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
# an authenticated URL.
|
||||
persist-credentials: false
|
||||
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
|
||||
fetch-depth: 0
|
||||
- name: Generate CLI help
|
||||
run: node aio/scripts/update-cli-help/index.mjs
|
||||
env:
|
||||
ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN }}
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
branch-prefix: update-cli-help
|
||||
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
|
||||
pr-description: |
|
||||
Updated Angular CLI help contents.
|
||||
pr-labels: |
|
||||
action: review
|
||||
area: docs
|
||||
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Update the `events.json` file that powers the [AIO events page](https://angular.io/events) (if
|
||||
# necessary) and create a pull request.
|
||||
#
|
||||
# For more details on the overall process, see
|
||||
# [aio/scripts/generate-events/README.md](../../aio/scripts/generate-events/README.md).
|
||||
|
||||
name: Update AIO events
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
schedule:
|
||||
# Run every day at 15:00.
|
||||
- cron: '0 15 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_events:
|
||||
name: Update `events.json` (if necessary)
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
# an authenticated URL.
|
||||
persist-credentials: false
|
||||
- name: Install AIO dependencies
|
||||
run: yarn --cwd=aio install
|
||||
- name: Generate `events.json`
|
||||
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@96fdaaa056f1cfa7ffbc4c69b7e9007279f76c94
|
||||
with:
|
||||
branch-prefix: docs-update-events
|
||||
pr-title: 'docs: update events'
|
||||
pr-description: |
|
||||
Generated `events.json` with the latest events retrieved from the Firebase DB.
|
||||
pr-labels: |
|
||||
action: review
|
||||
area: docs
|
||||
target: patch
|
||||
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
|
||||
@@ -57,7 +57,3 @@ baseline.json
|
||||
# Husky
|
||||
.husky/_
|
||||
aio/content/examples/.DS_Store
|
||||
|
||||
|
||||
# Ignore cache created with the Angular CLI.
|
||||
.angular/
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
|
||||
yarn -s ng-dev commit-message pre-commit-validate --file $1 2>/dev/null
|
||||
|
||||
+4
-1
@@ -1,8 +1,11 @@
|
||||
set +e
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
yarn -s ng-dev format staged 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "WARNING: failed to run file formatting (ng-dev format staged)"
|
||||
fi
|
||||
|
||||
|
||||
exit 0;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname $0)/_/husky.sh"
|
||||
|
||||
set +e
|
||||
|
||||
yarn -s ng-dev commit-message restore-commit-message-draft $1 $2 2>/dev/null
|
||||
|
||||
@@ -14,7 +14,7 @@ export const caretaker: CaretakerConfig = {
|
||||
},
|
||||
{
|
||||
name: 'Initial Triage Queue',
|
||||
query: `is:open no:milestone -draft:true`,
|
||||
query: `is:open no:milestone`,
|
||||
},
|
||||
],
|
||||
caretakerGroup: 'angular-caretaker',
|
||||
|
||||
@@ -25,10 +25,13 @@ export const commitMessage: CommitMessageConfig = {
|
||||
'language-service',
|
||||
'localize',
|
||||
'migrations',
|
||||
'ngcc',
|
||||
'packaging',
|
||||
'platform-browser',
|
||||
'platform-browser-dynamic',
|
||||
'platform-server',
|
||||
'platform-webworker',
|
||||
'platform-webworker-dynamic',
|
||||
'router',
|
||||
'service-worker',
|
||||
'upgrade',
|
||||
|
||||
+1
-47
@@ -5,33 +5,7 @@ import {FormatConfig} from '@angular/ng-dev';
|
||||
*/
|
||||
export const format: FormatConfig = {
|
||||
'prettier': {
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
'adev/**/*.{js,ts}',
|
||||
'devtools/**/*.{js,ts}',
|
||||
'tools/**/*.{js,ts}',
|
||||
'modules/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}',
|
||||
'packages/animations/**/*.{js,ts}',
|
||||
'packages/bazel/**/*.{js,ts}',
|
||||
'packages/benchpress/**/*.{js,ts}',
|
||||
'packages/common/**/*.{js,ts}',
|
||||
'packages/docs/**/*.{js,ts}',
|
||||
'packages/elements/**/*.{js,ts}',
|
||||
'packages/examples/**/*.{js,ts}',
|
||||
'packages/misc/**/*.{js,ts}',
|
||||
'packages/private/**/*.{js,ts}',
|
||||
'packages/router/**/*.{js,ts}',
|
||||
'packages/service-worker/**/*.{js,ts}',
|
||||
'packages/upgrade/**/*.{js,ts}',
|
||||
|
||||
// Do not format d.ts files as they are generated
|
||||
'!**/*.d.ts',
|
||||
// Both third_party and .yarn are directories containing copied code which should
|
||||
// not be modified.
|
||||
'!third_party/**',
|
||||
'!.yarn/**',
|
||||
],
|
||||
'matchers': ['**/*.{yaml,yml}'],
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
@@ -55,26 +29,6 @@ export const format: FormatConfig = {
|
||||
'!packages/core/src/i18n/locale_en.ts',
|
||||
'!packages/common/locales/closure-locale.ts',
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
// Temporarily disable formatting for adev
|
||||
'!adev/**',
|
||||
|
||||
// Migrated to prettier
|
||||
'!devtools/**/*.{js,ts}',
|
||||
'!tools/**/*.{js,ts}',
|
||||
'!modules/**/*.{js,ts}',
|
||||
'!scripts/**/*.{js,ts}',
|
||||
'!packages/animations/**/*.{js,ts}',
|
||||
'!packages/bazel/**/*.{js,ts}',
|
||||
'!packages/benchpress/**/*.{js,ts}',
|
||||
'!packages/common/**/*.{js,ts}',
|
||||
'!packages/docs/**/*.{js,ts}',
|
||||
'!packages/elements/**/*.{js,ts}',
|
||||
'!packages/examples/**/*.{js,ts}',
|
||||
'!packages/misc/**/*.{js,ts}',
|
||||
'!packages/private/**/*.{js,ts}',
|
||||
'!packages/router/**/*.{js,ts}',
|
||||
'!packages/service-worker/**/*.{js,ts}',
|
||||
'!packages/upgrade/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"syncedFilePatterns": [
|
||||
"LICENSE",
|
||||
"modules/benchmarks/**",
|
||||
"packages/**"
|
||||
],
|
||||
"separateFilePatterns": [
|
||||
"packages/core/primitives/**"
|
||||
],
|
||||
"syncedFilePatterns": ["LICENSE", "modules/benchmarks/**", "modules/system.d.ts", "packages/**"],
|
||||
"alwaysExternalFilePatterns": [
|
||||
"packages/*",
|
||||
"packages/bazel/*",
|
||||
@@ -18,9 +11,10 @@
|
||||
"packages/bazel/src/protractor/**",
|
||||
"packages/bazel/src/schematics/**",
|
||||
"packages/bazel/src/types_bundle/**",
|
||||
"packages/compiler-cli/src/ngcc/**",
|
||||
"packages/compiler-cli/linker/**",
|
||||
"packages/compiler-cli/ngcc/**",
|
||||
"packages/compiler-cli/src/ngtsc/sourcemaps/**",
|
||||
"packages/compiler-cli/src/ngtsc/testing/**",
|
||||
"packages/compiler-cli/private/bazel.ts",
|
||||
"packages/compiler-cli/private/localize.ts",
|
||||
"packages/compiler-cli/private/tooling.ts",
|
||||
@@ -29,6 +23,7 @@
|
||||
"packages/docs/**",
|
||||
"packages/elements/schematics/**",
|
||||
"packages/examples/**",
|
||||
"packages/language-service/**",
|
||||
"packages/localize/**",
|
||||
"packages/private/**",
|
||||
"packages/service-worker/**",
|
||||
@@ -37,6 +32,8 @@
|
||||
"packages/http/**",
|
||||
"**/.gitignore",
|
||||
"**/.gitkeep",
|
||||
"**/yarn.lock",
|
||||
"**/package.json",
|
||||
"**/third_party/**",
|
||||
"**/tsconfig-build.json",
|
||||
"**/tsconfig-tsec.json",
|
||||
|
||||
@@ -17,9 +17,4 @@ export const pullRequest: PullRequestConfig = {
|
||||
// the `bazel` package is not considered part of the public API so that features
|
||||
// can land in patch branches.
|
||||
targetLabelExemptScopes: ['dev-infra', 'docs-infra', 'bazel'],
|
||||
// enables specific validations during the pull request merge process
|
||||
validators: {
|
||||
assertEnforceTested: true,
|
||||
assertIsolatedSeparateFiles: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export const release: ReleaseConfig = {
|
||||
representativeNpmPackage: '@angular/core',
|
||||
npmPackages: [
|
||||
{name: '@angular/animations'},
|
||||
{name: '@angular/bazel'},
|
||||
{name: '@angular/common'},
|
||||
{name: '@angular/compiler'},
|
||||
{name: '@angular/compiler-cli'},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"tabs": false,
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"singleQuote": true,
|
||||
"semicolon": true,
|
||||
"quoteProps": "preserve",
|
||||
|
||||
+86
-90
@@ -53,11 +53,16 @@
|
||||
# @angular/framework-global-approvers-for-docs-only-changes
|
||||
# =========================================================
|
||||
# Used for approving minor documentation-only changes that don't require engineering review.
|
||||
#
|
||||
# josmar-crwdstffng
|
||||
# ileil
|
||||
# TeriGlover
|
||||
# bob-watson
|
||||
|
||||
version: 3
|
||||
|
||||
availability:
|
||||
users_unavailable: []
|
||||
# availability:
|
||||
# users_unavailable:
|
||||
|
||||
# Meta field that goes unused by PullApprove to allow for defining aliases to be
|
||||
# used throughout the config.
|
||||
@@ -122,10 +127,8 @@ groups:
|
||||
'packages/animations/**/{*,.*}',
|
||||
'packages/platform-browser/animations/**/{*,.*}',
|
||||
'aio/content/guide/animations.md',
|
||||
'aio/content/guide/add-an-animation.md',
|
||||
'aio/content/guide/animate-state-style.md',
|
||||
'aio/content/guide/animation-api-summary.md',
|
||||
'aio/content/guide/understanding-angular-animation.md',
|
||||
'aio/content/examples/animations/**/{*,.*}',
|
||||
'aio/content/images/guide/animations/**/{*,.*}',
|
||||
'aio/content/guide/complex-animation-sequences.md',
|
||||
@@ -149,7 +152,7 @@ groups:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
contains_any_globs(files.exclude('packages/compiler-cli/ngcc/*'), [
|
||||
'packages/compiler/**/{*,.*}',
|
||||
'packages/examples/compiler/**/{*,.*}',
|
||||
'aio/content/examples/angular-compiler-options/**/{*,.*}',
|
||||
@@ -157,17 +160,25 @@ groups:
|
||||
'aio/content/guide/angular-compiler-options.md',
|
||||
'aio/content/guide/aot-compiler.md',
|
||||
'aio/content/guide/aot-metadata-errors.md',
|
||||
'aio/content/guide/template-typecheck.md',
|
||||
'aio/content/extended-diagnostics/*.md'
|
||||
'aio/content/guide/template-typecheck.md'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- JoostK
|
||||
|
||||
# =========================================================
|
||||
# Framework: Compiler / ngcc
|
||||
# =========================================================
|
||||
fw-ngcc:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- files.include('packages/compiler-cli/ngcc/*')
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- JoostK
|
||||
|
||||
# =========================================================
|
||||
@@ -183,8 +194,6 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- jessicajaniuk
|
||||
- AndrewKushnir
|
||||
|
||||
# =========================================================
|
||||
# Framework: Core
|
||||
@@ -208,7 +217,6 @@ groups:
|
||||
'aio/content/guide/architecture-services.md',
|
||||
'aio/content/guide/architecture.md',
|
||||
'aio/content/examples/architecture/**/{*,.*}',
|
||||
'aio/content/guide/rxjs-interop.md',
|
||||
'aio/content/images/guide/architecture/**/{*,.*}',
|
||||
'aio/content/guide/attribute-directives.md',
|
||||
'aio/content/examples/attribute-directives/**/{*,.*}',
|
||||
@@ -222,12 +230,31 @@ groups:
|
||||
'aio/content/images/guide/change-detection/**/{*,.*}',
|
||||
'aio/content/guide/class-binding.md',
|
||||
'aio/content/guide/cheatsheet.md',
|
||||
'aio/content/guide/component/component-content-projection-complex.md',
|
||||
'aio/content/guide/component/component-content-projection-conditional.md',
|
||||
'aio/content/guide/component/component-content-projection-multi-slot.md',
|
||||
'aio/content/guide/component/component-content-projection-overview.md',
|
||||
'aio/content/guide/component/component-content-projection-single-slot.md',
|
||||
'aio/content/guide/component/component-create-cli.md',
|
||||
'aio/content/guide/component/component-create-manual.md',
|
||||
'aio/content/guide/component/component-example-dynamic-load.md',
|
||||
'aio/content/guide/component/component-example-interaction.md',
|
||||
'aio/content/guide/component/component-example.md',
|
||||
'aio/content/guide/component/component-exchange-data.md',
|
||||
'aio/content/guide/component/component-lifecycle-overview.md',
|
||||
'aio/content/guide/component/component-overview.md',
|
||||
'aio/content/guide/component/component-share-data-overview.md',
|
||||
'aio/content/guide/component/component-share-data-to-child.md',
|
||||
'aio/content/guide/component/component-share-data-to-parent.md',
|
||||
'aio/content/guide/component/component-structure.md',
|
||||
'aio/content/guide/component/component-style-overview.md',
|
||||
'aio/content/guide/component/component-style-view-encapsulate.md',
|
||||
'aio/content/guide/component/component-style.md',
|
||||
'aio/content/guide/component/component-use-overview.md',
|
||||
'aio/content/guide/component-interaction.md',
|
||||
'aio/content/examples/component-interaction/**/{*,.*}',
|
||||
'aio/content/images/guide/component-interaction/**/{*,.*}',
|
||||
'aio/content/guide/component-overview.md',
|
||||
'aio/content/errors/*.md',
|
||||
'aio/content/examples/errors/**/{*,.*}',
|
||||
'aio/content/examples/component-overview/**/{*,.*}',
|
||||
'aio/content/guide/component-styles.md',
|
||||
'aio/content/guide/developer-guide-overview.md',
|
||||
@@ -245,7 +272,6 @@ groups:
|
||||
'aio/content/images/guide/dependency-injection-in-action/**/{*,.*}',
|
||||
'aio/content/guide/dependency-injection-navtree.md',
|
||||
'aio/content/guide/dependency-injection-providers.md',
|
||||
'aio/content/guide/dependency-injection-context.md',
|
||||
'aio/content/guide/lightweight-injection-tokens.md',
|
||||
'aio/content/guide/displaying-data.md',
|
||||
'aio/content/examples/displaying-data/**/{*,.*}',
|
||||
@@ -254,6 +280,7 @@ groups:
|
||||
'aio/content/examples/dynamic-component-loader/**/{*,.*}',
|
||||
'aio/content/images/guide/dynamic-component-loader/**/{*,.*}',
|
||||
'aio/content/guide/example-apps-list.md',
|
||||
'aio/content/guide/entry-components.md',
|
||||
'aio/content/guide/feature-modules.md',
|
||||
'aio/content/examples/feature-modules/**/{*,.*}',
|
||||
'aio/content/images/guide/feature-modules/**/{*,.*}',
|
||||
@@ -268,6 +295,8 @@ groups:
|
||||
'aio/content/guide/lazy-loading-ngmodules.md',
|
||||
'aio/content/examples/lazy-loading-ngmodules/**/{*,.*}',
|
||||
'aio/content/images/guide/lazy-loading-ngmodules/**/{*,.*}',
|
||||
'aio/content/guide/lifecycle-hook/lifecycle-hook-example.md',
|
||||
'aio/content/guide/lifecycle-hook/lifecycle-hook-overview.md',
|
||||
'aio/content/guide/lifecycle-hooks.md',
|
||||
'aio/content/examples/lifecycle-hooks/**/{*,.*}',
|
||||
'aio/content/images/guide/lifecycle-hooks/**/{*,.*}',
|
||||
@@ -314,7 +343,6 @@ groups:
|
||||
'aio/content/guide/pipes.md',
|
||||
'aio/content/guide/pipes-custom-data-trans.md',
|
||||
'aio/content/guide/pipes-overview.md',
|
||||
'aio/content/guide/pipe-precedence.md',
|
||||
'aio/content/guide/pipes-transform-data.md',
|
||||
'aio/content/guide/pipe-template.md',
|
||||
'aio/content/examples/pipes/**/{*,.*}',
|
||||
@@ -322,12 +350,9 @@ groups:
|
||||
'aio/content/guide/providers.md',
|
||||
'aio/content/examples/providers/**/{*,.*}',
|
||||
'aio/content/images/guide/providers/**/{*,.*}',
|
||||
'aio/content/guide/signal-queries.md',
|
||||
'aio/content/guide/singleton-services.md',
|
||||
'aio/content/guide/signal-inputs.md',
|
||||
'aio/content/guide/sharing-ngmodules.md',
|
||||
'aio/content/guide/standalone-components.md',
|
||||
'aio/content/guide/standalone-migration.md',
|
||||
'aio/content/guide/structural-directives.md',
|
||||
'aio/content/examples/structural-directives/**/{*,.*}',
|
||||
'aio/content/guide/svg-in-templates.md',
|
||||
@@ -341,22 +366,13 @@ groups:
|
||||
'aio/content/guide/view-encapsulation.md',
|
||||
'aio/content/examples/view-encapsulation/**/{*,.*}',
|
||||
'aio/content/images/guide/view-encapsulation/**/{*,.*}',
|
||||
'aio/content/special-elements/**/{*,.*}',
|
||||
'aio/content/blocks/**/{*,.*}',
|
||||
'aio/content/guide/hydration.md',
|
||||
'aio/content/guide/defer.md',
|
||||
'aio/content/guide/signals.md',
|
||||
'aio/content/guide/control_flow.md',
|
||||
'aio/content/guide/model-inputs.md',
|
||||
'aio/content/examples/injection-token/**/{*,.*}',
|
||||
'aio/content/special-elements/**/{*,.*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- dylhunn
|
||||
- jessicajaniuk
|
||||
- pkozlowski-opensource
|
||||
@@ -391,21 +407,7 @@ groups:
|
||||
contains_any_globs(files, [
|
||||
'packages/common/http/**/{*,.*}',
|
||||
'packages/examples/http/**/{*,.*}',
|
||||
'aio/content/guide/understanding-communicating-with-http.md',
|
||||
'aio/content/guide/http-configure-http-url-parameters.md',
|
||||
'aio/content/guide/http-handle-request-errors.md',
|
||||
'aio/content/guide/http-intercept-requests-and-responses.md',
|
||||
'aio/content/guide/http-interceptor-use-cases.md',
|
||||
'aio/content/guide/http-make-jsonp-request.md',
|
||||
'aio/content/guide/http-optimize-server-interaction.md',
|
||||
'aio/content/guide/http-pass-metadata-to-interceptors.md',
|
||||
'aio/content/guide/http-request-data-from-server.md',
|
||||
'aio/content/guide/http-security-xsrf-protection.md',
|
||||
'aio/content/guide/http-send-data-to-server.md',
|
||||
'aio/content/guide/http-server-communication.md',
|
||||
'aio/content/guide/http-setup-server-communication.md',
|
||||
'aio/content/guide/http-test-requests.md',
|
||||
'aio/content/guide/http-track-show-request-progress.md',
|
||||
'aio/content/guide/http.md',
|
||||
'aio/content/examples/http/**/{*,.*}',
|
||||
'aio/content/images/guide/http/**/{*,.*}'
|
||||
])
|
||||
@@ -526,8 +528,8 @@ groups:
|
||||
contains_any_globs(files, [
|
||||
'packages/platform-server/**/{*,.*}',
|
||||
'aio/content/guide/prerendering.md',
|
||||
'aio/content/guide/ssr.md',
|
||||
'aio/content/examples/ssr/**/{*,.*}',
|
||||
'aio/content/guide/universal.md',
|
||||
'aio/content/examples/universal/**/{*,.*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@@ -793,9 +795,8 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- devversion
|
||||
- alxhub
|
||||
- josephperrott
|
||||
- pkozlowski-opensource
|
||||
|
||||
# =========================================================
|
||||
# Integration Tests
|
||||
@@ -847,8 +848,6 @@ groups:
|
||||
'aio/content/tutorial/**/{*,.*}',
|
||||
'aio/content/images/guide/toh/**/{*,.*}',
|
||||
'aio/content/demos/first-app/**/{*,.*}',
|
||||
'aio/content/examples/first-app-*/**/{*,.*}',
|
||||
'aio/content/images/guide/faa/**/{*,.*}',
|
||||
'aio/content/examples/toh-pt0/**/{*,.*}',
|
||||
'aio/content/examples/toh-pt1/**/{*,.*}',
|
||||
'aio/content/examples/toh-pt2/**/{*,.*}',
|
||||
@@ -931,22 +930,24 @@ groups:
|
||||
'aio/content/guide/releases.md',
|
||||
'aio/content/guide/updating.md',
|
||||
'aio/content/guide/deprecations.md',
|
||||
'aio/content/guide/migration-legacy-message-id.md',
|
||||
'aio/content/examples/deprecation-guide/**/{*,.*}',
|
||||
'aio/content/guide/migration-renderer.md',
|
||||
'aio/content/guide/migration-undecorated-classes.md',
|
||||
'aio/content/guide/migration-dynamic-flag.md',
|
||||
'aio/content/guide/migration-injectable.md',
|
||||
'aio/content/guide/migration-localize.md',
|
||||
'aio/content/guide/migration-module-with-providers.md',
|
||||
'aio/content/guide/static-query-migration.md',
|
||||
'aio/content/guide/update-to-latest-version.md',
|
||||
'aio/content/guide/versions.md',
|
||||
'aio/content/guide/update-to-version-14.md',
|
||||
'aio/content/guide/update-to-version-15.md',
|
||||
'aio/content/guide/update-to-version-16.md',
|
||||
'aio/content/guide/update-to-version-17.md',
|
||||
'aio/content/guide/update-to-version-15.md'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alxhub
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- jessicajaniuk
|
||||
- jelbourn
|
||||
|
||||
# =========================================================
|
||||
@@ -1014,7 +1015,6 @@ groups:
|
||||
'aio/content/guide/workspace-config.md',
|
||||
'aio/content/guide/migration-solution-style-tsconfig.md',
|
||||
'aio/content/guide/migration-update-libraries-tslib.md',
|
||||
'aio/content/guide/esbuild.md',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
@@ -1063,30 +1063,6 @@ groups:
|
||||
- dylhunn
|
||||
- mgechev
|
||||
|
||||
# =========================================================
|
||||
# Docs: Angular Dev Site
|
||||
# =========================================================
|
||||
angular-dev:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'adev/**/{.*,*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- josephperrott
|
||||
- jelbourn
|
||||
- bencodezen
|
||||
- jessicajaniuk
|
||||
- dylhunn
|
||||
- AndrewKushnir
|
||||
- alxhub
|
||||
- crisbeto
|
||||
- atscott
|
||||
- pkozlowski-opensource
|
||||
- ~JeanMeche
|
||||
|
||||
# =========================================================
|
||||
# Docs-infra
|
||||
# =========================================================
|
||||
@@ -1160,11 +1136,9 @@ groups:
|
||||
users:
|
||||
- AleksanderBodurri
|
||||
- devversion
|
||||
- dgp1130
|
||||
- josephperrott
|
||||
- mgechev
|
||||
- twerske
|
||||
- ~JeanMeche
|
||||
|
||||
# =========================================================
|
||||
# Dev-infra
|
||||
@@ -1196,13 +1170,10 @@ groups:
|
||||
'tools/bazel-repo-patches/**/{*,.*}',
|
||||
'tools/circular_dependency_test/**/{*,.*}',
|
||||
'tools/contributing-stats/**/{*,.*}',
|
||||
'tools/esm-interop/**/{*,.*}',
|
||||
'tools/esm-loader/**/{*,.*}',
|
||||
'tools/gulp-tasks/**/{*,.*}',
|
||||
'tools/legacy-saucelabs/**/{*,.*}',
|
||||
'tools/manual_api_docs/**/{*,.*}',
|
||||
'tools/npm-patches/**/{*,.*}',
|
||||
'tools/rxjs/**/{*,.*}',
|
||||
'tools/saucelabs-daemon/**/{*,.*}',
|
||||
'tools/saucelabs/**/{*,.*}',
|
||||
'tools/symbol-extractor/**/{*,.*}',
|
||||
'tools/testing/**/{*,.*}',
|
||||
@@ -1230,8 +1201,11 @@ groups:
|
||||
'goldens/public-api/**/{*,.*}',
|
||||
'docs/NAMING.md',
|
||||
'aio/content/guide/angular-package-format.md',
|
||||
'aio/content/errors/*.md',
|
||||
'aio/content/extended-diagnostics/*.md',
|
||||
'aio/content/guide/glossary.md',
|
||||
'aio/content/guide/styleguide.md',
|
||||
'aio/content/examples/errors/**/{*,.*}',
|
||||
'aio/content/examples/styleguide/**/{*,.*}',
|
||||
'aio/content/images/guide/styleguide/{*,.*}'
|
||||
])
|
||||
@@ -1397,6 +1371,28 @@ groups:
|
||||
author_value: 0 # The author of the PR cannot provide an approval for themself
|
||||
reviewed_for: ignored # All reviews apply to this group whether noted via Reviewed-for or not
|
||||
reviewers:
|
||||
teams:
|
||||
# Any member of the team can provide a review to perform the minimum review required
|
||||
- team
|
||||
users:
|
||||
- alan-agius4 # Alan Agius
|
||||
- AleksanderBodurri # Aleksander Bodurri
|
||||
- alxhub # Alex Rickabaugh
|
||||
- AndrewKushnir # Andrew Kushnir
|
||||
- andrewseguin # Andrew Seguin
|
||||
- atscott # Andrew Scott
|
||||
- clydin # Charles Lyding
|
||||
- crisbeto # Kristiyan Kostadinov
|
||||
- devversion # Paul Gschwendtner
|
||||
- dgp1130 # Doug Parker
|
||||
- dylhunn # Dylan Hunn
|
||||
- filipesilva # Filipe Silva
|
||||
- jelbourn # Jeremy Elbourn
|
||||
- jessicajaniuk # Jessica Janiuk
|
||||
- JiaLiPassion # Jia Li
|
||||
- JoostK # Joost Koehoorn
|
||||
- josephperrott # Joey Perrott
|
||||
- MarkTechson # Mark Thompson (Techson)
|
||||
- mgechev # Minko Gechev
|
||||
- mmalerba # Miles Malerba
|
||||
- pkozlowski-opensource # Pawel Kozlowski
|
||||
- Splaktar # Michael Prentice
|
||||
- twerske # Emma Twersky
|
||||
- zarend # Zach Arend
|
||||
|
||||
Vendored
+6
-1
@@ -4,8 +4,13 @@
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"BazelBuild.vscode-bazel",
|
||||
"devondcarew.bazel-code",
|
||||
"gkalpak.aio-docs-utils",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"xaver.clang-format",
|
||||
// The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory).
|
||||
//"angular.ng-template",
|
||||
//"dbaeumer.vscode-eslint",
|
||||
//"errata-ai.vale-server",
|
||||
],
|
||||
}
|
||||
|
||||
+147520
File diff suppressed because one or more lines are too long
Vendored
-147529
File diff suppressed because one or more lines are too long
@@ -2,4 +2,4 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
yarn-path ".yarn/releases/yarn-1.22.19.cjs"
|
||||
yarn-path ".yarn/releases/yarn-1.22.17.cjs"
|
||||
|
||||
+12
-17
@@ -1,5 +1,4 @@
|
||||
load("//tools:defaults.bzl", "nodejs_binary")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
load("//:yarn.bzl", "YARN_PATH")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
@@ -13,7 +12,6 @@ exports_files([
|
||||
"scripts/ci/payload-size.sh",
|
||||
"scripts/ci/payload-size.js",
|
||||
"package.json",
|
||||
"angular.json",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
@@ -28,11 +26,17 @@ alias(
|
||||
actual = "//packages:tsconfig-build.json",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = "browser-providers",
|
||||
filegroup(
|
||||
name = "web_test_bootstrap_scripts",
|
||||
# do not sort
|
||||
srcs = [
|
||||
"browser-providers.conf.d.ts",
|
||||
"browser-providers.conf.js",
|
||||
"@npm//:node_modules/core-js-bundle/index.js",
|
||||
"//packages/zone.js/bundles:zone.umd.js",
|
||||
"//packages/zone.js/bundles:zone-testing.umd.js",
|
||||
"//packages/zone.js/bundles:task-tracking.umd.js",
|
||||
# Including systemjs because it defines `__eval`, which produces correct stack traces.
|
||||
"@npm//:node_modules/systemjs/dist/system.src.js",
|
||||
"@npm//:node_modules/reflect-metadata/Reflect.js",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -64,19 +68,10 @@ config_setting(
|
||||
|
||||
nodejs_binary(
|
||||
name = "yarn_vendored",
|
||||
data = [".yarn/releases/yarn-1.22.19.cjs"],
|
||||
entry_point = ".yarn/releases/yarn-1.22.19.cjs",
|
||||
data = [".yarn/releases/yarn-1.22.17.cjs"],
|
||||
entry_point = ".yarn/releases/yarn-1.22.17.cjs",
|
||||
visibility = [
|
||||
"//aio/tools/examples/shared:__pkg__",
|
||||
"//integration:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "sauce_connect",
|
||||
actual = select({
|
||||
"@npm//@angular/build-tooling/bazel/constraints:linux_x64": "@sauce_connect_linux_amd64//:bin/sc",
|
||||
"@npm//@angular/build-tooling/bazel/constraints:macos_x64": "@sauce_connect_mac//:bin/sc",
|
||||
"@npm//@angular/build-tooling/bazel/constraints:macos_arm64": "@sauce_connect_mac//:bin/sc",
|
||||
}),
|
||||
)
|
||||
|
||||
+2618
-2134
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -65,14 +65,6 @@ Angular maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
### Appeal
|
||||
|
||||
If you are banned you may contest the decision. To do so email conduct@angular.io with the subject line "Repeal Ban for {{your name here}}" and body with the responses to the following:
|
||||
|
||||
* Why do you believe you did not violate the Code of Conduct?
|
||||
* Were other factors involved in this situation the leadership team may have been unaware of?
|
||||
* Why do you wish to be a part of the Angular community?
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
|
||||
|
||||
+6
-2
@@ -109,7 +109,7 @@ Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
```shell
|
||||
git commit --all
|
||||
```
|
||||
Note: the optional commit `--all` command line option will automatically "add" and "rm" edited files.
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
10. Push your branch to GitHub:
|
||||
|
||||
@@ -246,7 +246,7 @@ The `footer` is optional. The [Commit Message Footer](#commit-footer) format des
|
||||
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
│ elements|forms|http|language-service|localize|platform-browser|
|
||||
│ platform-browser-dynamic|platform-server|router|service-worker|
|
||||
│ upgrade|zone.js|packaging|changelog|docs-infra|migrations|
|
||||
│ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve|
|
||||
│ devtools
|
||||
│
|
||||
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
@@ -306,6 +306,10 @@ There are currently a few exceptions to the "use package name" rule:
|
||||
|
||||
* `migrations`: used for changes to the `ng update` migrations.
|
||||
|
||||
* `ngcc`: used for changes to the [Angular Compatibility Compiler](./packages/compiler-cli/ngcc/README.md)
|
||||
|
||||
* `ve`: used for changes specific to ViewEngine (legacy compiler/renderer).
|
||||
|
||||
* `devtools`: used for changes in the [browser extension](./devtools/README.md).
|
||||
|
||||
* none/empty string: useful for `test` and `refactor` changes that are done across all packages (e.g. `test: add missing unit tests`) and for docs changes that are not related to a specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
|
||||
Copyright (c) 2010-2022 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<h1 align="center">Angular - The modern web developer's platform</h1>
|
||||
<h1 align="center">Angular - The modern web developer's platform.</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="aio/src/assets/images/logos/angular/angular_renaissance.png" alt="angular-logo" width="120px" height="120px"/>
|
||||
<img src="aio/src/assets/images/logos/angular/angular.png" alt="angular-logo" width="120px" height="120px"/>
|
||||
<br>
|
||||
<em>Angular is a development platform for building mobile and desktop web applications
|
||||
<br> using TypeScript/JavaScript and other languages.</em>
|
||||
<i>Angular is a development platform for building mobile and desktop web applications
|
||||
<br> using Typescript/JavaScript and other languages.</i>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://angular.dev/"><strong>angular.dev</strong></a>
|
||||
<a href="https://www.angular.io"><strong>www.angular.io</strong></a>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
@@ -61,25 +61,6 @@ Get started with Angular, learn the fundamentals and explore advanced topics on
|
||||
- [Lazy Loading][lazyloading]
|
||||
- [Animations][animations]
|
||||
|
||||
### Local Development
|
||||
|
||||
To contribute to Angular docs, you can setup a local environment with the following commands:
|
||||
|
||||
```bash
|
||||
# Clone Angular repo
|
||||
git clone https://github.com/angular/angular.git
|
||||
|
||||
# Navigate to project directory
|
||||
cd angular
|
||||
|
||||
# Install dependencies
|
||||
yarn
|
||||
|
||||
# Build and run local dev server
|
||||
# Note: Initial build will take some time
|
||||
yarn docs
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
@@ -148,7 +129,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
||||
|
||||
Join the conversation and help the community.
|
||||
|
||||
- [X (formerly Twitter)][X (formerly Twitter)]
|
||||
- [Twitter][twitter]
|
||||
- [Discord][discord]
|
||||
- [Gitter][gitter]
|
||||
- [YouTube][youtube]
|
||||
@@ -160,27 +141,27 @@ Join the conversation and help the community.
|
||||
**Love Angular? Give our repo a star :star: :arrow_up:.**
|
||||
|
||||
[contributing]: CONTRIBUTING.md
|
||||
[quickstart]: https://angular.dev/tutorials/learn-angular
|
||||
[quickstart]: https://angular.io/start
|
||||
[changelog]: CHANGELOG.md
|
||||
[ng]: https://angular.dev
|
||||
[documentation]: https://angular.dev/overview
|
||||
[ng]: https://angular.io
|
||||
[documentation]: https://angular.io/docs
|
||||
[angularmaterial]: https://material.angular.io/
|
||||
[cli]: https://angular.dev/tools/cli
|
||||
[architecture]: https://angular.dev/essentials
|
||||
[componentstemplates]: https://angular.dev/tutorials/learn-angular/components-in-angular
|
||||
[forms]: https://angular.dev/tutorials/learn-angular/forms
|
||||
[api]: https://angular.dev/api
|
||||
[angularelements]: https://angular.dev/guide/elements
|
||||
[ssr]: https://angular.dev/guide/ssr
|
||||
[schematics]: https://angular.dev/tools/cli/schematics
|
||||
[lazyloading]: https://angular.dev/guide/ngmodules/lazy-loading
|
||||
[cli]: https://cli.angular.io/
|
||||
[architecture]: https://angular.io/guide/architecture
|
||||
[componentstemplates]: https://angular.io/guide/displaying-data
|
||||
[forms]: https://angular.io/guide/forms-overview
|
||||
[api]: https://angular.io/api
|
||||
[angularelements]: https://angular.io/guide/elements
|
||||
[ssr]: https://angular.io/guide/universal
|
||||
[schematics]: https://angular.io/guide/schematics
|
||||
[lazyloading]: https://angular.io/guide/lazy-loading-ngmodules
|
||||
[node.js]: https://nodejs.org/
|
||||
[npm]: https://www.npmjs.com/get-npm
|
||||
[codeofconduct]: CODE_OF_CONDUCT.md
|
||||
[X (formerly Twitter)]: https://www.twitter.com/angular
|
||||
[twitter]: https://www.twitter.com/angular
|
||||
[discord]: https://discord.gg/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular
|
||||
[youtube]: https://youtube.com/angular
|
||||
[meetup]: https://www.meetup.com/find/?keywords=angular
|
||||
[animations]: https://angular.dev/guide/animations
|
||||
[animations]: https://angular.io/guide/animations
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules). For vulnerabilities in Angular, please submit your report [here](https://bughunters.google.com/report).
|
||||
Angular is part of Google [Open Source Software Vulnerability Reward Program](https://bughunters.google.com/about/rules/6521337925468160/google-open-source-software-vulnerability-reward-program-rules), for vulnerabilities in Angular please submit your report [here](https://bughunters.google.com/report).
|
||||
|
||||
For more information, check out [Angular's security policy](https://angular.io/guide/security).
|
||||
For more information on Angular's security policy visit: https://angular.io/guide/security
|
||||
|
||||
@@ -26,10 +26,11 @@ http_archive(
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
patches = [
|
||||
"//tools/esm-interop:patches/bazel/nodejs_binary_esm_support.patch",
|
||||
# TODO(devversion): remove when https://github.com/bazelbuild/rules_nodejs/pull/3605 is available.
|
||||
"//tools:bazel-repo-patches/rules_nodejs__#3605.patch",
|
||||
],
|
||||
sha256 = "5dd1e5dea1322174c57d3ca7b899da381d516220793d0adef3ba03b9d23baa8e",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.3/rules_nodejs-5.8.3.tar.gz"],
|
||||
sha256 = "c29944ba9b0b430aadcaf3bf2570fece6fc5ebfb76df145c6cdad40d65c20811",
|
||||
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.7.0/rules_nodejs-5.7.0.tar.gz"],
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
|
||||
@@ -38,8 +39,6 @@ build_bazel_rules_nodejs_dependencies()
|
||||
|
||||
# The PKG rules are needed to build tar packages for integration tests. The builtin
|
||||
# rule in `@bazel_tools` is not Windows compatible and outdated.
|
||||
# NOTE: We cannot move past version 0.6.0 as pkg_tar no longer works on directories, which rules_nodejs
|
||||
# relies on for node_modules setup.
|
||||
http_archive(
|
||||
name = "rules_pkg",
|
||||
sha256 = "62eeb544ff1ef41d786e329e1536c1d541bb9bcad27ae984d57f18f314018e66",
|
||||
@@ -50,13 +49,37 @@ http_archive(
|
||||
)
|
||||
|
||||
# Fetch Aspect lib for utilities like write_source_files
|
||||
# NOTE: We cannot move past version 1.23.2 of aspect_bazel_lib because it requires us to move to bazel 6.0.0 which
|
||||
# breaks our usage of managed_directories
|
||||
http_archive(
|
||||
name = "aspect_bazel_lib",
|
||||
sha256 = "4b2e774387bae6242879820086b7b738d49bf3d0659522ea5d9363be01a27582",
|
||||
strip_prefix = "bazel-lib-1.23.2",
|
||||
url = "https://github.com/aspect-build/bazel-lib/archive/refs/tags/v1.23.2.tar.gz",
|
||||
sha256 = "be236556c7b9c7b91cb370e837fdcec62b6e8893408cd4465ae883c9d7c67024",
|
||||
strip_prefix = "bazel-lib-1.18.0",
|
||||
url = "https://github.com/aspect-build/bazel-lib/archive/refs/tags/v1.18.0.tar.gz",
|
||||
)
|
||||
|
||||
# Download cli source from angular/cli-builds for doc generation at a specific tag or commit.
|
||||
# If the ref is a commit sha, use the full sha instead of the abbreviated form. Tags, which
|
||||
# hold an abbreviated sha by convention in cli-builds, can continue to use the short form.
|
||||
CLI_SRC_REF = "6ccbc6b4cba4fde7898b67d2e195a1ea3bf4553e"
|
||||
|
||||
http_archive(
|
||||
name = "angular_cli_src",
|
||||
build_file_content = """
|
||||
# Include files used in doc generation
|
||||
filegroup(
|
||||
name = "files_for_docgen",
|
||||
srcs = glob([
|
||||
"help/**/*.json",
|
||||
"package.json",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
""",
|
||||
# Run the following command to calculate the sha256, substituting the CLI_SRC_REF
|
||||
# wget -O- -q https://github.com/angular/cli-builds/archive/{CLI_SRC_REF}.tar.gz | sha256sum
|
||||
# Alternatively, just remove the parameter and bazel debug will output the sha as a suggestion.
|
||||
sha256 = "6558e1774904719d0615f01b88368d7fe3a927ea1e54cb0436e7957bc8f6d0a6",
|
||||
strip_prefix = "cli-builds-%s" % CLI_SRC_REF,
|
||||
url = "https://github.com/angular/cli-builds/archive/%s.tar.gz" % CLI_SRC_REF,
|
||||
)
|
||||
|
||||
# Setup the Node.js toolchain.
|
||||
@@ -64,17 +87,7 @@ load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
|
||||
|
||||
nodejs_register_toolchains(
|
||||
name = "nodejs",
|
||||
node_repositories = {
|
||||
"18.18.2-darwin_arm64": ("node-v18.18.2-darwin-arm64.tar.gz", "node-v18.18.2-darwin-arm64", "9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553"),
|
||||
"18.18.2-darwin_amd64": ("node-v18.18.2-darwin-x64.tar.gz", "node-v18.18.2-darwin-x64", "5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f"),
|
||||
"18.18.2-linux_arm64": ("node-v18.18.2-linux-arm64.tar.xz", "node-v18.18.2-linux-arm64", "2e630e18548627f61eaf573233da7949dc0a1df5eef3f486fa9820c5f6c121aa"),
|
||||
"18.18.2-linux_ppc64le": ("node-v18.18.2-linux-ppc64le.tar.xz", "node-v18.18.2-linux-ppc64le", "b0adff5cf5938266b711d6c724fb134d802e0dee40b3a3f73d162de1b3d11880"),
|
||||
"18.18.2-linux_s390x": ("node-v18.18.2-linux-s390x.tar.xz", "node-v18.18.2-linux-s390x", "c70ec2074b5e2b42c55bb4b8105418b67bf8a61c500d9376a07430dfcc341fdb"),
|
||||
"18.18.2-linux_amd64": ("node-v18.18.2-linux-x64.tar.xz", "node-v18.18.2-linux-x64", "75aba25ae76999309fc6c598efe56ce53fbfc221381a44a840864276264ab8ac"),
|
||||
"18.18.2-windows_amd64": ("node-v18.18.2-win-x64.zip", "node-v18.18.2-win-x64", "3bb0e51e579a41a22b3bf6cb2f3e79c03801aa17acbe0ca00fc555d1282e7acd"),
|
||||
},
|
||||
# We need at least Node 18.17 due to some transitive dependencies.
|
||||
node_version = "18.18.2",
|
||||
node_version = "16.13.0",
|
||||
)
|
||||
|
||||
# Download npm dependencies.
|
||||
@@ -88,13 +101,9 @@ yarn_install(
|
||||
data = [
|
||||
YARN_LABEL,
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//:scripts/puppeteer-chromedriver-versions.js",
|
||||
"//:scripts/webdriver-manager-update.js",
|
||||
"//tools:postinstall-patches.js",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-e0ec7b60641d7f6369be45d8d02663fd50f320be.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
|
||||
"//tools/esm-interop:patches/npm/rxjs+6.6.7.patch",
|
||||
],
|
||||
# Currently disabled due to:
|
||||
# 1. Missing Windows support currently.
|
||||
@@ -116,7 +125,6 @@ yarn_install(
|
||||
data = [
|
||||
YARN_LABEL,
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//aio:tools/cli-patches/bazel-architect-output.patch",
|
||||
"//aio:tools/cli-patches/patch.js",
|
||||
],
|
||||
@@ -206,10 +214,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "974f5e14c54d300ca63a67910ebfcea6d838d8811fefc61184f2872107152705",
|
||||
strip_prefix = "rules_sass-888dd9c02709e228b60a8b3e51e2b16b2ca5279d",
|
||||
sha256 = "618f0b7aae019c149ac2ff4142a66c110de116fb5c173dbeccedbaee06fc5f6d",
|
||||
strip_prefix = "rules_sass-a2fce75bcf103750f7accbf7f966ad48bcbca2d4",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/888dd9c02709e228b60a8b3e51e2b16b2ca5279d.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/a2fce75bcf103750f7accbf7f966ad48bcbca2d4.zip",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -227,20 +235,3 @@ register_toolchains(
|
||||
"@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_arm64_toolchain",
|
||||
"@npm//@angular/build-tooling/bazel/git-toolchain:git_windows_toolchain",
|
||||
)
|
||||
|
||||
# Fetch sauce connect (tool to open Saucelabs tunnel for Saucelabs browser tests)
|
||||
http_archive(
|
||||
name = "sauce_connect_linux_amd64",
|
||||
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
|
||||
sha256 = "26b9c3630f441b47854b6032f7eca6f1d88d3f62e50ee44c27015d71a5155c36",
|
||||
strip_prefix = "sc-4.8.2-linux",
|
||||
url = "https://saucelabs.com/downloads/sc-4.8.2-linux.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "sauce_connect_mac",
|
||||
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
|
||||
sha256 = "28277ce81ef9ab84f5b87b526258920a8ead44789a5034346e872629bbf38089",
|
||||
strip_prefix = "sc-4.8.2-osx",
|
||||
url = "https://saucelabs.com/downloads/sc-4.8.2-osx.zip",
|
||||
)
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
load("//:packages.bzl", "link_packages")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
# All source and configuration files required to build the docs app
|
||||
APPLICATION_FILES = [
|
||||
"angular.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.worker.json",
|
||||
] + glob(
|
||||
["src/**/*"],
|
||||
exclude = [
|
||||
"src/**/*.spec.ts",
|
||||
# Temporarily exclude generated sources produced by the non-bazel
|
||||
# build until the whole project is built by bazel and this directory
|
||||
# isn't needed.
|
||||
"src/generated/**/*",
|
||||
],
|
||||
)
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
)
|
||||
|
||||
APPLICATION_DEPS = link_packages([
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular/animations",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/compiler",
|
||||
"@npm//@angular/compiler-cli",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/docs",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@angular/platform-browser",
|
||||
"@npm//@angular/platform-server",
|
||||
"@npm//@angular/router",
|
||||
"@npm//gsap",
|
||||
"@npm//marked",
|
||||
"@npm//ngx-progressbar",
|
||||
"@npm//ogl",
|
||||
"@npm//rxjs",
|
||||
"@npm//typescript",
|
||||
"//adev/src/assets/images",
|
||||
"//adev/src/assets/textures",
|
||||
"//adev/src/assets/previews",
|
||||
"//adev/src/assets:tutorials",
|
||||
"//adev/src/assets/icons",
|
||||
"//adev/src/assets:api",
|
||||
"//adev/src/assets:content",
|
||||
"@npm//@typescript/vfs",
|
||||
"@npm//@codemirror/state",
|
||||
"@npm//@codemirror/view",
|
||||
"@npm//@codemirror/language",
|
||||
"@npm//@codemirror/commands",
|
||||
"@npm//@codemirror/search",
|
||||
"@npm//@codemirror/autocomplete",
|
||||
"@npm//@codemirror/lint",
|
||||
"@npm//@codemirror/lang-html",
|
||||
"@npm//@codemirror/lang-angular",
|
||||
"@npm//@codemirror/lang-css",
|
||||
"@npm//@codemirror/lang-sass",
|
||||
"@npm//@codemirror/lang-javascript",
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//xterm",
|
||||
"@npm//xterm-addon-fit",
|
||||
"@npm//angular-split",
|
||||
])
|
||||
|
||||
TEST_DEPS = APPLICATION_DEPS + link_packages([
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//assert",
|
||||
"@npm//jasmine",
|
||||
"@npm//jasmine-core",
|
||||
"@npm//karma-chrome-launcher",
|
||||
"@npm//karma-coverage",
|
||||
"@npm//karma-jasmine",
|
||||
"@npm//karma-jasmine-html-reporter",
|
||||
"//aio/tools:windows-chromium-path",
|
||||
])
|
||||
|
||||
copy_to_bin(
|
||||
name = "application_files_bin",
|
||||
srcs = APPLICATION_FILES,
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "fast_build_mode",
|
||||
build_setting_default = False,
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "fast",
|
||||
flag_values = {
|
||||
":fast_build_mode": "true",
|
||||
},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "full",
|
||||
flag_values = {
|
||||
":fast_build_mode": "false",
|
||||
},
|
||||
)
|
||||
|
||||
config_based_architect_flags = select({
|
||||
":fast": ["--no-prerender"],
|
||||
":full": ["--prerender"],
|
||||
})
|
||||
|
||||
architect(
|
||||
name = "build",
|
||||
args = [
|
||||
"angular-dev:build",
|
||||
"--output-path=build",
|
||||
] + config_based_architect_flags,
|
||||
chdir = "$(RULEDIR)",
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
# Network is required to inline fonts.
|
||||
exec_properties = ENABLE_NETWORK,
|
||||
output_dir = True,
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
],
|
||||
)
|
||||
|
||||
architect(
|
||||
name = "serve",
|
||||
args = [
|
||||
"angular-dev:serve",
|
||||
"--poll=1000",
|
||||
"--live-reload",
|
||||
"--watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
],
|
||||
)
|
||||
|
||||
architect_test(
|
||||
name = "test",
|
||||
args = [
|
||||
"angular-dev:test",
|
||||
"--no-watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = TEST_DEPS + TEST_FILES,
|
||||
env = {
|
||||
"CHROME_BIN": "../$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"$schema": "../node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": ".",
|
||||
"projects": {
|
||||
"angular-dev": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss",
|
||||
"standalone": true,
|
||||
"changeDetection": "OnPush"
|
||||
}
|
||||
},
|
||||
"root": ".",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "adev",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"externalDependencies": [
|
||||
"path"
|
||||
],
|
||||
"outputPath": "dist/angular-dev",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"server": "src/main.server.ts",
|
||||
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/robots.txt",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"optimization": {
|
||||
"fonts": {
|
||||
// TODO(josephperrott): enabled inline scripts
|
||||
"inline": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-dev:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-dev:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "web-ui:build",
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "angular-dev:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
"scripts": [],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "dca119a9-da31-47f7-a6cb-b60541037021"
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "angular-docs",
|
||||
"public": "./build/browser",
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
"value": "cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public,max-age=31536000,immutable"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/[0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].js",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/@(ngsw-worker.js|ngsw.json)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Opener-Policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const {getAdjustedChromeBinPathForWindows} = require('../aio/tools/windows-chromium-path');
|
||||
|
||||
process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows();
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
|
||||
],
|
||||
proxies: {
|
||||
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
|
||||
},
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
random: true,
|
||||
seed: '',
|
||||
},
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true, // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/site'),
|
||||
subdir: '.',
|
||||
reporters: [{type: 'html'}, {type: 'text-summary'}],
|
||||
},
|
||||
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
flags: [
|
||||
'--no-sandbox',
|
||||
'--headless',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--hide-scrollbars',
|
||||
'--mute-audio',
|
||||
],
|
||||
},
|
||||
},
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function JasmineSeedReporter(baseReporterDecorator) {
|
||||
baseReporterDecorator(this);
|
||||
|
||||
this.onBrowserComplete = (browser, result) => {
|
||||
const seed = result.order && result.order.random && result.order.seed;
|
||||
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
|
||||
};
|
||||
|
||||
this.onRunComplete = () => undefined;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
@defer (when isBrowser) {
|
||||
<adev-progress-bar />
|
||||
}
|
||||
<button (click)="focusFirstHeading()" class="adev-skip">Skip to main content</button>
|
||||
|
||||
<div class="adev-nav"></div>
|
||||
@if (displaySecondaryNav()) {
|
||||
<adev-secondary-navigation />
|
||||
}
|
||||
<div class="docs-app-main-content">
|
||||
<!--
|
||||
Avoid rendering cookies popup on the server,
|
||||
since there is no benefit of doing this and
|
||||
it requires additional state management.
|
||||
-->
|
||||
@defer (when isBrowser) {
|
||||
<docs-cookie-popup />
|
||||
|
||||
@defer (when displaySearchDialog(); prefetch on idle) {
|
||||
@if (displaySearchDialog()) {
|
||||
<docs-search-dialog (onClose)="displaySearchDialog.set(false)" />
|
||||
}
|
||||
}
|
||||
}
|
||||
<router-outlet />
|
||||
|
||||
@if (displayFooter()) {
|
||||
<footer adev-footer></footer>
|
||||
}
|
||||
</div>
|
||||
@@ -1,86 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
:host {
|
||||
max-width: 2560px;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
min-height: 100vh;
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
flex-direction: column;
|
||||
}
|
||||
// If navs are open, render a blurry background over content
|
||||
&:has(.docs-nav-secondary--open),
|
||||
&:has(.adev-nav-primary--open) {
|
||||
.docs-app-main-content {
|
||||
&::after {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.adev-home) {
|
||||
.adev-nav {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-up {
|
||||
footer {
|
||||
margin-left: var(--primary-nav-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-skip {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 1000;
|
||||
background: var(--primary-contrast);
|
||||
color: var(--page-background);
|
||||
border: 1px solid var(--vivid-pink);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transform: translateY(-150%);
|
||||
transition: transform 0.3s ease-out;
|
||||
&:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.docs-app-main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
// If navs are open, render a blurry background over content
|
||||
::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
backdrop-filter: blur(2px);
|
||||
background-color: color-mix(in srgb, var(--gray-1000) 5%, transparent);
|
||||
z-index: 50;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {AppComponent} from './app.component';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {routes} from './routes';
|
||||
import {Search, WINDOW} from '@angular/docs';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
const fakeSearch = {};
|
||||
const fakeWindow = {};
|
||||
|
||||
it('should create the app', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
{
|
||||
provide: Search,
|
||||
useValue: fakeSearch,
|
||||
},
|
||||
],
|
||||
});
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
NgZone,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
signal,
|
||||
WritableSignal,
|
||||
} from '@angular/core';
|
||||
import {NavigationEnd, NavigationSkipped, Router, RouterLink, RouterOutlet} from '@angular/router';
|
||||
import {filter, map, skip} from 'rxjs/operators';
|
||||
import {
|
||||
CookiePopup,
|
||||
getActivatedRouteSnapshotFromRouter,
|
||||
IS_SEARCH_DIALOG_OPEN,
|
||||
SearchDialog,
|
||||
WINDOW,
|
||||
} from '@angular/docs';
|
||||
import {Footer} from './core/layout/footer/footer.component';
|
||||
import {Navigation} from './core/layout/navigation/navigation.component';
|
||||
import {SecondaryNavigation} from './core/layout/secondary-navigation/secondary-navigation.component';
|
||||
import {ProgressBarComponent} from './core/layout/progress-bar/progress-bar.component';
|
||||
import {ESCAPE, SEARCH_TRIGGER_KEY} from './core/constants/keys';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'adev-root',
|
||||
imports: [
|
||||
CookiePopup,
|
||||
Navigation,
|
||||
Footer,
|
||||
SecondaryNavigation,
|
||||
RouterOutlet,
|
||||
RouterLink,
|
||||
SearchDialog,
|
||||
ProgressBarComponent,
|
||||
],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
private readonly router = inject(Router);
|
||||
private readonly window = inject(WINDOW);
|
||||
|
||||
currentUrl = signal('');
|
||||
displayFooter = signal(false);
|
||||
displaySecondaryNav = signal(false);
|
||||
displaySearchDialog: WritableSignal<boolean> = inject(IS_SEARCH_DIALOG_OPEN);
|
||||
|
||||
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setSearchDialogVisibilityOnKeyPress();
|
||||
this.closeSearchDialogOnNavigationSkipped();
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
map((event) => event.urlAfterRedirects),
|
||||
)
|
||||
.subscribe((url) => {
|
||||
this.currentUrl.set(url);
|
||||
this.setComponentsVisibility();
|
||||
this.displaySearchDialog.set(false);
|
||||
});
|
||||
|
||||
this.focusFirstHeadingOnRouteChange();
|
||||
}
|
||||
|
||||
focusFirstHeading(): void {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const h1 = this.document.querySelector<HTMLHeadingElement>('h1');
|
||||
h1?.focus();
|
||||
}
|
||||
|
||||
private setComponentsVisibility(): void {
|
||||
const activatedRoute = getActivatedRouteSnapshotFromRouter(this.router as any);
|
||||
|
||||
this.displaySecondaryNav.set(activatedRoute.data['displaySecondaryNav']);
|
||||
this.displayFooter.set(!activatedRoute.data['hideFooter']);
|
||||
}
|
||||
|
||||
private focusFirstHeadingOnRouteChange(): void {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
// Skip first emission, cause on the initial load we would like to `Skip to main content` popup when it's focused
|
||||
skip(1),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.focusFirstHeading();
|
||||
});
|
||||
}
|
||||
|
||||
private setSearchDialogVisibilityOnKeyPress(): void {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
this.window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === SEARCH_TRIGGER_KEY && (event.metaKey || event.ctrlKey)) {
|
||||
this.ngZone.run(() => {
|
||||
event.preventDefault();
|
||||
this.displaySearchDialog.update((display) => !display);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.key === ESCAPE && this.displaySearchDialog()) {
|
||||
this.ngZone.run(() => {
|
||||
event.preventDefault();
|
||||
this.displaySearchDialog.set(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private closeSearchDialogOnNavigationSkipped(): void {
|
||||
this.router.events.pipe(filter((event) => event instanceof NavigationSkipped)).subscribe(() => {
|
||||
this.displaySearchDialog.set(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {mergeApplicationConfig, ApplicationConfig} from '@angular/core';
|
||||
import {provideServerRendering} from '@angular/platform-server';
|
||||
import {appConfig} from './app.config';
|
||||
import {
|
||||
ReferenceScrollHandler,
|
||||
ReferenceScrollHandlerNoop,
|
||||
} from './features/references/services/reference-scroll-handler.service';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(),
|
||||
{provide: ReferenceScrollHandler, useClass: ReferenceScrollHandlerNoop},
|
||||
],
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||
@@ -1,97 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {provideHttpClient, withFetch} from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
ErrorHandler,
|
||||
inject,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
DOCS_CONTENT_LOADER,
|
||||
ENVIRONMENT,
|
||||
EXAMPLE_VIEWER_CONTENT_LOADER,
|
||||
PREVIEWS_COMPONENTS,
|
||||
WINDOW,
|
||||
windowProvider,
|
||||
} from '@angular/docs';
|
||||
import {provideClientHydration} from '@angular/platform-browser';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {
|
||||
RouteReuseStrategy,
|
||||
Router,
|
||||
TitleStrategy,
|
||||
createUrlTreeFromSnapshot,
|
||||
provideRouter,
|
||||
withInMemoryScrolling,
|
||||
withViewTransitions,
|
||||
} from '@angular/router';
|
||||
import environment from './environment';
|
||||
import {PREVIEWS_COMPONENTS_MAP} from './../assets/previews/previews';
|
||||
import {ADevTitleStrategy} from './core/services/a-dev-title-strategy';
|
||||
import {AnalyticsService} from './core/services/analytics/analytics.service';
|
||||
import {ContentLoader} from './core/services/content-loader.service';
|
||||
import {CustomErrorHandler} from './core/services/errors-handling/error-handler';
|
||||
import {ExampleContentLoader} from './core/services/example-content-loader.service';
|
||||
import {ReuseTutorialsRouteStrategy} from './features/tutorial/tutorials-route-reuse-strategy';
|
||||
import {routes} from './routes';
|
||||
import {ReferenceScrollHandler} from './features/references/services/reference-scroll-handler.service';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideRouter(
|
||||
routes,
|
||||
withInMemoryScrolling({anchorScrolling: 'enabled', scrollPositionRestoration: 'enabled'}),
|
||||
withViewTransitions({
|
||||
onViewTransitionCreated: ({transition, to}) => {
|
||||
const router = inject(Router);
|
||||
const toTree = createUrlTreeFromSnapshot(to, []);
|
||||
// Skip the transition if the only thing changing is the fragment and queryParams
|
||||
if (
|
||||
router.isActive(toTree, {
|
||||
paths: 'exact',
|
||||
matrixParams: 'exact',
|
||||
fragment: 'ignored',
|
||||
queryParams: 'ignored',
|
||||
})
|
||||
) {
|
||||
transition.skipTransition();
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
provideClientHydration(),
|
||||
provideHttpClient(withFetch()),
|
||||
provideAnimationsAsync(),
|
||||
{provide: ENVIRONMENT, useValue: environment},
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => inject(AnalyticsService),
|
||||
},
|
||||
{provide: ErrorHandler, useClass: CustomErrorHandler},
|
||||
{provide: PREVIEWS_COMPONENTS, useValue: PREVIEWS_COMPONENTS_MAP},
|
||||
{provide: DOCS_CONTENT_LOADER, useClass: ContentLoader},
|
||||
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useClass: ExampleContentLoader},
|
||||
{
|
||||
provide: RouteReuseStrategy,
|
||||
useClass: ReuseTutorialsRouteStrategy,
|
||||
},
|
||||
{
|
||||
provide: WINDOW,
|
||||
useFactory: (document: Document) => windowProvider(document),
|
||||
deps: [DOCUMENT],
|
||||
},
|
||||
{provide: TitleStrategy, useClass: ADevTitleStrategy},
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
ReferenceScrollHandler,
|
||||
],
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const PRIMARY_NAV_ID = 'primaryNav';
|
||||
export const SECONDARY_NAV_ID = 'secondaryNav';
|
||||
@@ -1,12 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const COMMAND = 'Command';
|
||||
export const CONTROL = 'Control';
|
||||
export const ESCAPE = 'Escape';
|
||||
export const SEARCH_TRIGGER_KEY = 'k';
|
||||
@@ -1,12 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const GITHUB = 'https://github.com/angular/angular';
|
||||
export const X = 'https://x.com/angular';
|
||||
export const MEDIUM = 'https://blog.angular.io';
|
||||
export const YOUTUBE = 'https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw';
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
// File contains the enums used across whole application.
|
||||
|
||||
// The enum with the prefixes of the main routes
|
||||
export enum PagePrefix {
|
||||
API = 'api',
|
||||
CLI = 'cli',
|
||||
DOCS = 'docs',
|
||||
HOME = '',
|
||||
PLAYGROUND = 'playground',
|
||||
REFERENCE = 'reference',
|
||||
TUTORIALS = 'tutorials',
|
||||
}
|
||||
|
||||
// The enum with the default pages for each main tab
|
||||
export enum DefaultPage {
|
||||
DOCS = 'overview',
|
||||
REFERENCE = 'api',
|
||||
TUTORIALS = 'tutorials',
|
||||
PLAYGROUND = 'playground',
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<div class="adev-footer-container">
|
||||
<div class="adev-footer-columns">
|
||||
<div>
|
||||
<h2>Social Media</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a [href]="MEDIUM" title="Angular blog">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="X" title="X (formerly Twitter)">X (formerly Twitter)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="YOUTUBE" title="YouTube">YouTube</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://discord.gg/angular"
|
||||
title="Join the discussions at Angular Community Discord server."
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="GITHUB" title="GitHub">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://stackoverflow.com/questions/tagged/angular"
|
||||
title="Stack Overflow: where the community answers your technical Angular questions."
|
||||
>
|
||||
Stack Overflow
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Community</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md"
|
||||
title="Contribute to Angular"
|
||||
>
|
||||
Contribute
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/code-of-conduct/blob/main/CODE_OF_CONDUCT.md"
|
||||
title="Treating each other with respect."
|
||||
>
|
||||
Code of Conduct
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/angular/angular/issues"
|
||||
title="Post issues and suggestions on github."
|
||||
>
|
||||
Report Issues
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://devlibrary.withgoogle.com/products/angular?sort=updated"
|
||||
title="Google's DevLibrary"
|
||||
>
|
||||
Google's DevLibrary
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://developers.google.com/community/experts/directory?specialization=angular"
|
||||
title="Angular Google Developer Experts"
|
||||
>
|
||||
Angular Google Developer Experts
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a routerLink="/press-kit" title="Press contacts, logos, and branding.">Press Kit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/roadmap" title="Roadmap">Roadmap</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Languages</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://angular.cn/" title="简体中文版">简体中文版</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular.tw/" title="正體中文版">正體中文版</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular.jp/" title="日本語版">日本語版</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://angular.kr/" title="한국어">한국어</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://angular-gr.web.app"
|
||||
title="Ελληνικά"
|
||||
>
|
||||
Ελληνικά
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="docs-license">
|
||||
Super-powered by Google ©2010-2024. Code licensed under an
|
||||
<a routerLink="/license" title="License text">MIT-style License</a>
|
||||
. Documentation licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,59 +0,0 @@
|
||||
.adev-footer-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2rem;
|
||||
|
||||
@container footer (max-width: 600px) {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-footer-container {
|
||||
container: footer / inline-size;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
padding: var(--layout-padding);
|
||||
padding-inline-end: 1rem;
|
||||
background-color: var(--page-background);
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
// this accounts for absolutely positioned TOC on the right
|
||||
@media only screen and (min-width: 1430px) {
|
||||
width: calc(100% - 195px - var(--layout-padding) * 3);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-block-end: 1.75rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.95rem;
|
||||
li {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--quaternary-contrast);
|
||||
font-weight: 300;
|
||||
transition: color 0.3s ease;
|
||||
&:hover {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
p.docs-license {
|
||||
transition: color 0.3s ease;
|
||||
color: var(--quaternary-contrast);
|
||||
font-weight: 300;
|
||||
grid-column: span 4;
|
||||
font-size: 0.75rem;
|
||||
margin-block-start: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {WINDOW} from '@angular/docs';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {Footer} from './footer.component';
|
||||
|
||||
describe('Footer', () => {
|
||||
let component: Footer;
|
||||
let fixture: ComponentFixture<Footer>;
|
||||
const fakeWindow = {
|
||||
location: {
|
||||
origin: 'example-origin',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Footer, RouterTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(Footer);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
import {ExternalLink} from '@angular/docs';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {GITHUB, X, MEDIUM, YOUTUBE} from './../../constants/links';
|
||||
|
||||
@Component({
|
||||
selector: 'footer[adev-footer]',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ExternalLink, RouterLink],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Footer {
|
||||
readonly GITHUB = GITHUB;
|
||||
readonly X = X;
|
||||
readonly YOUTUBE = YOUTUBE;
|
||||
readonly MEDIUM = MEDIUM;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
// Dark/Light mode switcher & social media icon buttons
|
||||
// ul
|
||||
.adev-mini-menu {
|
||||
padding: 0;
|
||||
color: var(--primary-contrast);
|
||||
background-color: var(--page-background);
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
z-index: var(--z-index-mini-menu);
|
||||
box-shadow: 10px 4px 40px 0 rgba(0, 0, 0, 0.075);
|
||||
|
||||
@include mq.for-tablet {
|
||||
top: 75px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
// themes and versions
|
||||
button {
|
||||
padding: 1rem;
|
||||
min-width: 75px;
|
||||
min-height: 75px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
docs-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--senary-contrast);
|
||||
span,
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// social links
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
min-width: 50px;
|
||||
svg {
|
||||
fill: var(--quaternary-contrast);
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--senary-contrast);
|
||||
svg {
|
||||
fill: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&-open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-version-picker {
|
||||
top: 30px;
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
bottom: auto;
|
||||
|
||||
li {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
li button {
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
top: 30px;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
.adev-nav-item {
|
||||
color: var(--quaternary-contrast);
|
||||
position: relative;
|
||||
width: 6.875rem;
|
||||
|
||||
@include mq.for-phone-only {
|
||||
width: 5.05rem;
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
@include mq.for-tablet {
|
||||
width: auto;
|
||||
top: auto;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
// TODO: Make this gradient if the page is currently active, and black if child page active
|
||||
background-color: var(--primary-contrast);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transform-origin: center;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:not(.adev-nav-item--logo) {
|
||||
@include mq.for-tablet {
|
||||
a,
|
||||
.adev-nav-button {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-nav-button {
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a,
|
||||
.adev-nav-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-block: 1.25rem;
|
||||
text-decoration: none;
|
||||
fill: var(--quaternary-contrast);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin: 0;
|
||||
font-size: 0.813; // 13px
|
||||
color: inherit;
|
||||
|
||||
abbr {
|
||||
font-size: 0.688; // 11px
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
color: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
span,
|
||||
abbr {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
a,
|
||||
.adev-nav-button {
|
||||
fill: var(--primary-contrast);
|
||||
}
|
||||
span,
|
||||
abbr {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
i {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
&:not(.adev-nav-item--logo) {
|
||||
path {
|
||||
fill: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
span,
|
||||
abbr,
|
||||
i {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
<!-- Primary Nav -->
|
||||
<div
|
||||
[attr.id]="PRIMARY_NAV_ID"
|
||||
(docsClickOutside)="closeMobileNav()"
|
||||
[docsClickOutsideIgnore]="[SECONDARY_NAV_ID]"
|
||||
>
|
||||
<!-- Mobile nav bar -->
|
||||
<div class="adev-mobile-nav-bar">
|
||||
<!-- Logo icon -->
|
||||
<button
|
||||
type="button"
|
||||
class="adev-mobile-nav-button"
|
||||
aria-label="Toggle mobile navigation"
|
||||
(click)="openMobileNav($event)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="32">
|
||||
<g clip-path="url(#2a)">
|
||||
<path
|
||||
fill="url(#2b)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#2c)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="2b"
|
||||
x1="49.009"
|
||||
x2="225.829"
|
||||
y1="213.75"
|
||||
y2="129.722"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#E40035" />
|
||||
<stop offset=".24" stop-color="#F60A48" />
|
||||
<stop offset=".352" stop-color="#F20755" />
|
||||
<stop offset=".494" stop-color="#DC087D" />
|
||||
<stop offset=".745" stop-color="#9717E7" />
|
||||
<stop offset="1" stop-color="#6C00F5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="2c"
|
||||
x1="41.025"
|
||||
x2="156.741"
|
||||
y1="28.344"
|
||||
y2="160.344"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF31D9" />
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="2a"><path fill="#fff" d="M0 0h223v236H0z" /></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<docs-icon role="presentation">menu</docs-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="adev-nav-primary docs-scroll-hide"
|
||||
[class.adev-nav-primary--open]="isMobileNavigationOpened()"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="adev-close-nav"
|
||||
(click)="closeMobileNav()"
|
||||
aria-label="Close navigation"
|
||||
>
|
||||
<docs-icon role="presentation">close</docs-icon>
|
||||
</button>
|
||||
<!-- Top section of primary nav -->
|
||||
<ul class="adev-nav__top">
|
||||
<!-- Home / Angular logo -->
|
||||
<li
|
||||
class="adev-nav-item adev-nav-item--logo"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === HOME_ROUTE"
|
||||
>
|
||||
<a aria-label="Angular homepage" routerLink="/">
|
||||
<!-- Logo Symbol -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="32">
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#c)"
|
||||
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="49.009"
|
||||
x2="225.829"
|
||||
y1="213.75"
|
||||
y2="129.722"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#E40035" />
|
||||
<stop offset=".24" stop-color="#F60A48" />
|
||||
<stop offset=".352" stop-color="#F20755" />
|
||||
<stop offset=".494" stop-color="#DC087D" />
|
||||
<stop offset=".745" stop-color="#9717E7" />
|
||||
<stop offset="1" stop-color="#6C00F5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="c"
|
||||
x1="41.025"
|
||||
x2="156.741"
|
||||
y1="28.344"
|
||||
y2="160.344"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF31D9" />
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h223v236H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="adev-beta-version">Beta Docs</div>
|
||||
</a>
|
||||
|
||||
<!-- Version picker for v18+ -->
|
||||
<div class="adev-nav-item">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Select Angular version"
|
||||
role="menu"
|
||||
class="adev-version-button"
|
||||
[class.adev-mini-menu-open]="openedMenu === 'version-picker'"
|
||||
[cdkMenuTriggerFor]="docsVersionMiniMenu"
|
||||
[cdkMenuPosition]="miniMenuPositions"
|
||||
(cdkMenuClosed)="closeMenu()"
|
||||
(click)="openVersionMenu($event)"
|
||||
>
|
||||
{{ currentDocsVersion()?.displayName }}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="15"
|
||||
viewBox="0 -960 960 960"
|
||||
width="15"
|
||||
fill="inherit"
|
||||
>
|
||||
<path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<ng-template #docsVersionMiniMenu>
|
||||
<ul class="adev-mini-menu adev-version-picker" cdkMenu>
|
||||
@for (item of versions(); track item) {
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="redirectToDocsVersion(item.url)"
|
||||
[attr.aria-label]="item.displayName"
|
||||
>
|
||||
<span>{{ item.displayName }}</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Search -->
|
||||
<li class="adev-nav-item">
|
||||
<button
|
||||
class="adev-nav-button"
|
||||
type="button"
|
||||
(click)="toggleSearchDialog($event)"
|
||||
title="Search docs"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="M14.583 15.48 9.104 10a4.591 4.591 0 0 1-1.458.844 5.156 5.156 0 0 1-1.771.302c-1.5 0-2.77-.52-3.813-1.563C1.022 8.542.5 7.285.5 5.813c0-1.473.52-2.73 1.563-3.771C3.103 1 4.367.479 5.854.479 7.326.48 8.58 1 9.614 2.042c1.035 1.041 1.553 2.298 1.553 3.77 0 .598-.098 1.174-.292 1.73A5.287 5.287 0 0 1 10 9.104l5.5 5.459-.917.916ZM5.854 9.895c1.125 0 2.083-.4 2.875-1.198a3.95 3.95 0 0 0 1.188-2.885 3.95 3.95 0 0 0-1.188-2.886C7.938 2.13 6.98 1.73 5.854 1.73c-1.139 0-2.107.4-2.906 1.198-.799.799-1.198 1.76-1.198 2.886 0 1.125.4 2.086 1.198 2.885.799.799 1.767 1.198 2.906 1.198Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="adev-nav-item__label adev-search-desktop"
|
||||
[attr.aria-label]="'Open search dialog with ' + searchTitle"
|
||||
>
|
||||
<kbd>{{ searchLabel }}</kbd>
|
||||
<kbd>K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<!-- Docs -->
|
||||
<li class="adev-nav-item" [class.adev-nav-item--active]="activeRouteItem() === DOCS_ROUTE">
|
||||
<a [routerLink]="DOCS_ROUTE">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="18"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="M3.645 13.792h6.708v-1.25H3.645v1.25Zm0-3.542h6.708V9H3.645v1.25Zm-2.063 7.083a1.2 1.2 0 0 1-.875-.375 1.2 1.2 0 0 1-.375-.875V1.917a1.2 1.2 0 0 1 .375-.875 1.2 1.2 0 0 1 .875-.375h7.52l4.563 4.562v10.854a1.2 1.2 0 0 1-.375.875 1.2 1.2 0 0 1-.875.375H1.582ZM8.478 5.792V1.917H1.582v14.166h10.833V5.792H8.478Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Docs</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Tutorials -->
|
||||
<li
|
||||
class="adev-nav-item"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === TUTORIALS_ROUTE"
|
||||
>
|
||||
<a [routerLink]="TUTORIALS_ROUTE">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="10"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="m5.668 10-5-5 5-5 1.187 1.188L3.022 5.02l3.813 3.812L5.668 10Zm6.667 0-1.188-1.188L14.98 4.98l-3.812-3.812L12.335 0l5 5-5 5Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Tutorials</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Playground -->
|
||||
<li
|
||||
class="adev-nav-item"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === PLAYGROUND_ROUTE"
|
||||
>
|
||||
<a [routerLink]="PLAYGROUND_ROUTE">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path
|
||||
d="M450.001-611.691v-32.386q-39.385-9.923-64.692-41.897-25.308-31.975-25.308-74.025 0-49.922 35.038-84.96 35.039-35.038 84.961-35.038t84.961 35.038q35.038 35.038 35.038 84.96 0 42.05-25.308 74.025-25.307 31.974-64.692 41.897v32.386l273.846 157.538q17.173 9.912 26.663 26.582 9.491 16.671 9.491 36.495v62.152q0 19.824-9.491 36.495-9.49 16.67-26.663 26.582L516.154-111.771q-17.203 9.846-36.217 9.846t-36.091-9.846L176.155-265.847q-17.173-9.912-26.663-26.582-9.491-16.671-9.491-36.495v-62.152q0-19.824 9.491-36.495 9.49-16.67 26.663-26.582l273.846-157.538Zm-6.155 364.537L200-387.461v58.537q0 3.078 1.539 5.962 1.538 2.885 4.615 4.808l267.692 154.692q3.077 1.923 6.154 1.923t6.154-1.923l267.692-154.692q3.077-1.923 4.615-4.808 1.539-2.884 1.539-5.962v-58.537L516.154-247.154q-17.203 9.847-36.217 9.847t-36.091-9.847Zm6.155-162.847V-542.77L250.46-427.691l223.386 128.846q3.077 1.924 6.154 1.924t6.154-1.924l223.001-128.846L509.999-542.77v132.769h-59.998ZM480-699.999q25 0 42.5-17.5t17.5-42.5q0-25-17.5-42.5t-42.5-17.5q-25 0-42.5 17.5t-17.5 42.5q0 25 17.5 42.5t42.5 17.5Zm-2.308 538.46Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Playground</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- API Ref -->
|
||||
<li
|
||||
class="adev-nav-item"
|
||||
[class.adev-nav-item--active]="activeRouteItem() === REFERENCE_ROUTE"
|
||||
>
|
||||
<a [routerLink]="REFERENCE_ROUTE">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="18"
|
||||
fill="inherit"
|
||||
>
|
||||
<path
|
||||
d="M3.645 13.792h6.708v-1.25H3.645v1.25Zm0-3.542h6.708V9H3.645v1.25Zm-2.063 7.083a1.2 1.2 0 0 1-.875-.375 1.2 1.2 0 0 1-.375-.875V1.917a1.2 1.2 0 0 1 .375-.875 1.2 1.2 0 0 1 .875-.375h7.52l4.563 4.562v10.854a1.2 1.2 0 0 1-.375.875 1.2 1.2 0 0 1-.875.375H1.582ZM8.478 5.792V1.917H1.582v14.166h10.833V5.792H8.478Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="adev-nav-item__label">Reference</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Bottom section of primary nav -->
|
||||
<div class="adev-nav__bottom">
|
||||
<!-- Social Icons Dots -->
|
||||
<div class="adev-nav-item" [class.adev-nav-item--active]="openedMenu === 'social'">
|
||||
<button
|
||||
type="button"
|
||||
[cdkMenuTriggerFor]="socialMiniMenu"
|
||||
[cdkMenuPosition]="miniMenuPositions"
|
||||
aria-label="Open social media links"
|
||||
(cdkMenuClosed)="closeMenu()"
|
||||
(cdkMenuOpened)="openMenu('social')"
|
||||
>
|
||||
<docs-icon role="presentation">more_horiz</docs-icon>
|
||||
</button>
|
||||
|
||||
<ng-template #socialMiniMenu>
|
||||
<ul class="adev-mini-menu" cdkMenu>
|
||||
<li>
|
||||
<a
|
||||
[href]="YOUTUBE"
|
||||
cdkMenuItem
|
||||
title="Angular YouTube channel"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- Youtube Icon -->
|
||||
<svg
|
||||
width="20"
|
||||
height="15"
|
||||
viewBox="0 0 20 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.7556 2.94783C18.5803 1.98018 17.745 1.27549 16.7756 1.05549C15.325 0.747832 12.6403 0.527832 9.73563 0.527832C6.83266 0.527832 4.105 0.747832 2.65266 1.05549C1.685 1.27549 0.847969 1.93549 0.672656 2.94783C0.495625 4.04783 0.320312 5.58783 0.320312 7.56783C0.320312 9.54783 0.495625 11.0878 0.715625 12.1878C0.892656 13.1555 1.72797 13.8602 2.69563 14.0802C4.23563 14.3878 6.87563 14.6078 9.78031 14.6078C12.685 14.6078 15.325 14.3878 16.865 14.0802C17.8327 13.8602 18.668 13.2002 18.845 12.1878C19.0203 11.0878 19.2403 9.50314 19.285 7.56783C19.1956 5.58783 18.9756 4.04783 18.7556 2.94783ZM7.36031 10.6478V4.48783L12.728 7.56783L7.36031 10.6478Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
[href]="X"
|
||||
cdkMenuItem
|
||||
title="Angular X (formerly Twitter) profile"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- X Icon -->
|
||||
<svg
|
||||
width="17"
|
||||
height="16"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.04145 0.04432l6.56351 8.77603L0 15.95564h1.48651l5.78263-6.24705 4.6722 6.24705h5.05865l-6.9328-9.26967L16.21504.04432h-1.48651l-5.32552 5.75341L5.1001.04432H.04145Zm2.18602 1.09497h2.32396l10.26221 13.72122h-2.32396L2.22747 1.13928Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
[href]="MEDIUM"
|
||||
cdkMenuItem
|
||||
title="Angular Medium blog"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<!-- Medium Icon -->
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 6A7 7 0 107 20 7 7 0 107 6zM18 6.5A3 6.5 0 1018 19.5 3 6.5 0 1018 6.5zM23 8A1 5 0 1023 18 1 5 0 1023 8z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a [href]="GITHUB" cdkMenuItem title="Angular Github" target="_blank" rel="noopener">
|
||||
<!-- Github Icon -->
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.59948 19.0428C7.59948 18.8069 7.59118 18.182 7.58656 17.3526C4.89071 17.9526 4.32164 16.0201 4.32164 16.0201C3.88087 14.8718 3.24533 14.5663 3.24533 14.5663C2.36518 13.9492 3.31179 13.9621 3.31179 13.9621C4.28471 14.0323 4.79656 14.9868 4.79656 14.9868C5.66102 16.5052 7.06456 16.0672 7.61748 15.8125C7.70564 15.17 7.95579 14.732 8.23271 14.4837C6.08056 14.2331 3.81764 13.3801 3.81764 9.57199C3.81764 8.48737 4.19564 7.6003 4.81548 6.90522C4.71625 6.65414 4.38302 5.64384 4.91056 4.27537C4.91056 4.27537 5.72471 4.00814 7.57594 5.29399C8.34856 5.07384 9.17795 4.96307 10.0027 4.95937C10.8256 4.96307 11.6546 5.07384 12.429 5.29399C14.2793 4.00814 15.0921 4.27537 15.0921 4.27537C15.621 5.64337 15.2883 6.65368 15.1881 6.90522C15.8093 7.6003 16.1841 8.48737 16.1841 9.57199C16.1841 13.3898 13.9179 14.2298 11.7589 14.4758C12.1073 14.7828 12.4166 15.3892 12.4166 16.3165C12.4166 17.6452 12.4041 18.7174 12.4041 19.0428C12.4041 19.3091 12.579 19.6178 13.071 19.5205C16.9193 18.2041 19.6936 14.4814 19.6936 10.0926C19.6936 4.60353 15.3538 0.154297 10.0009 0.154297C4.64887 0.154297 0.309021 4.60353 0.309021 10.0926C0.309483 14.4828 3.08656 18.2078 6.9381 19.5218C7.42225 19.6128 7.59948 19.3058 7.59948 19.0428Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
<!-- TODO: Refactor this and make it better. Accessible, animated, etc. -->
|
||||
<div class="adev-nav-item" [class.adev-nav-item--active]="openedMenu === 'theme-picker'">
|
||||
<button
|
||||
type="button"
|
||||
[cdkMenuTriggerFor]="themeMiniMenu"
|
||||
[cdkMenuPosition]="miniMenuPositions"
|
||||
aria-label="Open theme picker"
|
||||
(cdkMenuClosed)="closeMenu()"
|
||||
(cdkMenuOpened)="openMenu('theme-picker')"
|
||||
>
|
||||
<docs-icon role="presentation">
|
||||
@switch (theme()) { @case ('light') {
|
||||
{{ 'light_mode' }}
|
||||
} @case ('dark') {
|
||||
{{ 'dark_mode' }}
|
||||
} @case ('auto') {
|
||||
{{ 'routine' }}
|
||||
} }
|
||||
</docs-icon>
|
||||
</button>
|
||||
|
||||
<ng-template #themeMiniMenu>
|
||||
<ul class="adev-mini-menu" cdkMenu>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="setTheme('auto')"
|
||||
aria-label="Set default system theme"
|
||||
>
|
||||
<docs-icon class="docs-icon_high-contrast">routine</docs-icon>
|
||||
<span>System</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="setTheme('dark')"
|
||||
aria-label="Set dark theme"
|
||||
>
|
||||
<docs-icon class="docs-icon_high-contrast">dark_mode</docs-icon>
|
||||
<span>Dark</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
cdkMenuItem
|
||||
(click)="setTheme('light')"
|
||||
aria-label="Set light theme"
|
||||
>
|
||||
<docs-icon class="docs-icon_high-contrast">light_mode</docs-icon>
|
||||
<span>Light</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Tablet: Second horizontal nav layer which opens the secondary nav -->
|
||||
@if (activeRouteItem() === DOCS_ROUTE || activeRouteItem() === REFERENCE_ROUTE) {
|
||||
<div class="adev-secondary-tablet-bar">
|
||||
<button type="button" (click)="openMobileNav($event)">
|
||||
<docs-icon class="docs-icon_high-contrast">menu</docs-icon>
|
||||
@if (activeRouteItem() === DOCS_ROUTE) {
|
||||
<span>Docs</span>
|
||||
} @if (activeRouteItem() === REFERENCE_ROUTE) {
|
||||
<span>API</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,283 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
// primary nav
|
||||
:host {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-index-nav);
|
||||
|
||||
.adev-mobile-nav-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
// render tablet primary nav under blur when secondary is open
|
||||
@include mq.for-tablet {
|
||||
&:has(.adev-nav-primary--open) {
|
||||
z-index: 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-mobile-nav-bar {
|
||||
display: none;
|
||||
@include mq.for-phone-only {
|
||||
display: flex;
|
||||
}
|
||||
gap: 0.75rem;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
padding-block: 0.75rem;
|
||||
padding-inline: var(--layout-padding);
|
||||
border-block-end: 1px solid var(--septenary-contrast);
|
||||
box-sizing: border-box;
|
||||
|
||||
transform: translateY(0);
|
||||
// mobile bar: enter with delay
|
||||
transition: transform 0.3s ease-out 0.6s;
|
||||
|
||||
&:has(+ .adev-nav-primary--open) {
|
||||
transform: translateY(-100%);
|
||||
// mobile bar: exit without delay
|
||||
transition: transform 0.3s ease-in;
|
||||
}
|
||||
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
// First level navigation
|
||||
.adev-nav-primary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
// render primary nav / mini menu above tablet secondary bar
|
||||
z-index: 250;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
height: 100dvh;
|
||||
padding-block-start: 1rem;
|
||||
padding-block-end: 2rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
// div containing mobile close button and adev-nav_top
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background-color: var(--page-background);
|
||||
box-shadow: 10px 4px 3px 0 rgba(0, 0, 0, 0.001);
|
||||
|
||||
transform: translateX(-100%);
|
||||
// primary nav: exit delayed
|
||||
// TODO: should only be delayed if there is a secondary nav
|
||||
transition: transform 0.3s ease-in 0.38s;
|
||||
|
||||
&.adev-nav-primary--open {
|
||||
transform: translateX(0%);
|
||||
// primary nav: enter after mobile nav bar exits
|
||||
transition: transform 0.3s ease-out 0.1s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce-motion) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
width: 100vw;
|
||||
padding-inline: calc(var(--layout-padding) - 1.25rem);
|
||||
height: auto;
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
// Add a subtle border for the home page
|
||||
border-block-end: 1px solid var(--septenary-contrast);
|
||||
@include mq.for-tablet-up {
|
||||
border-inline-end: 1px solid var(--septenary-contrast);
|
||||
}
|
||||
@include mq.for-phone-only {
|
||||
border-inline-end: 1px solid var(--septenary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.adev-nav {
|
||||
&__top {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.adev-beta-version {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
// version dropdown button
|
||||
.adev-version-button {
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.25rem;
|
||||
color: var(--quaternary-contrast);
|
||||
fill: var(--quaternary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
|
||||
docs-icon {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
&.adev-mini-menu-open {
|
||||
&::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@include mq.for-tablet-landscape-up {
|
||||
&.adev-mini-menu-open {
|
||||
&::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-tablet {
|
||||
> li:first-of-type {
|
||||
padding-inline-start: 1.25rem;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
@include mq.for-tablet {
|
||||
flex-direction: row;
|
||||
margin-inline-end: 1.25rem;
|
||||
}
|
||||
|
||||
.adev-nav-item--active {
|
||||
button {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding-inline: 1rem;
|
||||
|
||||
@include mq.for-tablet {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
docs-icon {
|
||||
color: var(--quaternary-contrast);
|
||||
font-size: 1.5rem;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-close-nav {
|
||||
display: none;
|
||||
@include mq.for-phone-only {
|
||||
display: block;
|
||||
}
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
|
||||
.adev-search-desktop {
|
||||
height: 1.375rem;
|
||||
text-transform: capitalize;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-sub-navigation-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.adev-secondary-tablet-bar {
|
||||
font-size: 0.875rem;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
border-block-end: 1px solid var(--septenary-contrast);
|
||||
padding-block: 1rem;
|
||||
padding-inline: var(--layout-padding);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
button {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
color: var(--primary-contrast);
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
@include mq.for-tablet-landscape-up {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Navigation} from './navigation.component';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {Theme, ThemeManager} from '../../services/theme-manager.service';
|
||||
import {Version, signal} from '@angular/core';
|
||||
import {of} from 'rxjs';
|
||||
import {VersionManager} from '../../services/version-manager.service';
|
||||
import {Search, WINDOW} from '@angular/docs';
|
||||
|
||||
describe('Navigation', () => {
|
||||
let component: Navigation;
|
||||
let fixture: ComponentFixture<Navigation>;
|
||||
|
||||
const fakeThemeManager = {
|
||||
theme: signal<Theme>('dark'),
|
||||
setTheme: (theme: Theme) => {},
|
||||
themeChanged$: of(),
|
||||
};
|
||||
|
||||
const fakeVersionManager = {
|
||||
currentDocsVersion: signal('v17'),
|
||||
versions: signal<Version[]>([]),
|
||||
};
|
||||
|
||||
const fakeWindow = {};
|
||||
const fakeSearch = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Navigation, RouterTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
{
|
||||
provide: Search,
|
||||
useValue: fakeSearch,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
TestBed.overrideProvider(ThemeManager, {useValue: fakeThemeManager});
|
||||
TestBed.overrideProvider(VersionManager, {useValue: fakeVersionManager});
|
||||
|
||||
fixture = TestBed.createComponent(Navigation);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should append active class to DOCS_ROUTE when DOCS_ROUTE is active', () => {
|
||||
component.activeRouteItem.set(PagePrefix.DOCS);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).parent?.nativeElement;
|
||||
|
||||
expect(docsLink).toHaveClass('adev-nav-item--active');
|
||||
});
|
||||
|
||||
it('should not have active class when activeRouteItem is null', () => {
|
||||
component.activeRouteItem.set(null);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const docsLink = fixture.debugElement.query(By.css('a[href="/docs"]')).nativeElement;
|
||||
const referenceLink = fixture.debugElement.query(By.css('a[href="/reference"]')).nativeElement;
|
||||
|
||||
expect(docsLink).not.toHaveClass('adev-nav-item--active');
|
||||
expect(referenceLink).not.toHaveClass('adev-nav-item--active');
|
||||
});
|
||||
|
||||
it('should call themeManager.setTheme(dark) when user tries to set dark theme', () => {
|
||||
const openThemePickerButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label^="Open theme picker"]'),
|
||||
).nativeElement;
|
||||
const setThemeSpy = spyOn(fakeThemeManager, 'setTheme');
|
||||
|
||||
openThemePickerButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const setDarkModeButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label="Set dark theme"]'),
|
||||
).nativeElement;
|
||||
|
||||
setDarkModeButton.click();
|
||||
|
||||
expect(setThemeSpy).toHaveBeenCalledOnceWith('dark');
|
||||
});
|
||||
});
|
||||
@@ -1,228 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
|
||||
import {CommonModule, DOCUMENT, Location, isPlatformBrowser} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
ClickOutside,
|
||||
NavigationList,
|
||||
NavigationState,
|
||||
WINDOW,
|
||||
IconComponent,
|
||||
getBaseUrlAfterRedirects,
|
||||
isApple,
|
||||
IS_SEARCH_DIALOG_OPEN,
|
||||
} from '@angular/docs';
|
||||
import {NavigationEnd, Router, RouterLink} from '@angular/router';
|
||||
import {filter, map, startWith} from 'rxjs/operators';
|
||||
import {DOCS_ROUTES, REFERENCE_ROUTES, TUTORIALS_ROUTES} from '../../../routes';
|
||||
import {GITHUB, MEDIUM, X, YOUTUBE} from '../../constants/links';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {Theme, ThemeManager} from '../../services/theme-manager.service';
|
||||
import {VersionManager} from '../../services/version-manager.service';
|
||||
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';
|
||||
import {ConnectionPositionPair} from '@angular/cdk/overlay';
|
||||
import {COMMAND, CONTROL, SEARCH_TRIGGER_KEY} from '../../constants/keys';
|
||||
|
||||
type MenuType = 'social' | 'theme-picker' | 'version-picker';
|
||||
|
||||
@Component({
|
||||
selector: 'div.adev-nav',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
NavigationList,
|
||||
ClickOutside,
|
||||
CdkMenu,
|
||||
CdkMenuItem,
|
||||
CdkMenuTrigger,
|
||||
IconComponent,
|
||||
],
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss', './mini-menu.scss', './nav-item.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Navigation implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly router = inject(Router);
|
||||
private readonly location = inject(Location);
|
||||
private readonly themeManager = inject(ThemeManager);
|
||||
private readonly isSearchDialogOpen = inject(IS_SEARCH_DIALOG_OPEN);
|
||||
private readonly window = inject(WINDOW);
|
||||
private readonly versionManager = inject(VersionManager);
|
||||
|
||||
readonly DOCS_ROUTE = PagePrefix.DOCS;
|
||||
readonly HOME_ROUTE = PagePrefix.HOME;
|
||||
readonly PLAYGROUND_ROUTE = PagePrefix.PLAYGROUND;
|
||||
readonly REFERENCE_ROUTE = PagePrefix.REFERENCE;
|
||||
readonly TUTORIALS_ROUTE = PagePrefix.TUTORIALS;
|
||||
|
||||
readonly GITHUB = GITHUB;
|
||||
readonly X = X;
|
||||
readonly MEDIUM = MEDIUM;
|
||||
readonly YOUTUBE = YOUTUBE;
|
||||
|
||||
readonly PRIMARY_NAV_ID = PRIMARY_NAV_ID;
|
||||
readonly SECONDARY_NAV_ID = SECONDARY_NAV_ID;
|
||||
|
||||
miniMenuPositions = [
|
||||
new ConnectionPositionPair(
|
||||
{originX: 'end', originY: 'center'},
|
||||
{overlayX: 'start', overlayY: 'center'},
|
||||
),
|
||||
new ConnectionPositionPair(
|
||||
{originX: 'end', originY: 'top'},
|
||||
{overlayX: 'start', overlayY: 'top'},
|
||||
),
|
||||
];
|
||||
|
||||
readonly APPLE_SEARCH_LABEL = `⌘`;
|
||||
readonly DEFAULT_SEARCH_LABEL = `ctrl`;
|
||||
|
||||
activeRouteItem = this.navigationState.primaryActiveRouteItem;
|
||||
theme = this.themeManager.theme;
|
||||
openedMenu: MenuType | null = null;
|
||||
|
||||
currentDocsVersion = this.versionManager.currentDocsVersion;
|
||||
// Set the values of the search label and title only on the client, because the label is user-agent specific.
|
||||
searchLabel = this.isBrowser
|
||||
? isApple
|
||||
? this.APPLE_SEARCH_LABEL
|
||||
: this.DEFAULT_SEARCH_LABEL
|
||||
: '';
|
||||
searchTitle = this.isBrowser
|
||||
? isApple
|
||||
? `${COMMAND} ${SEARCH_TRIGGER_KEY.toUpperCase()}`
|
||||
: `${CONTROL} ${SEARCH_TRIGGER_KEY.toUpperCase()}`
|
||||
: '';
|
||||
versions = this.versionManager.versions;
|
||||
|
||||
isMobileNavigationOpened = this.navigationState.isMobileNavVisible;
|
||||
isMobileNavigationOpened$ = toObservable(this.isMobileNavigationOpened);
|
||||
primaryRouteChanged$ = toObservable(this.activeRouteItem);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.listenToRouteChange();
|
||||
this.preventToScrollContentWhenSecondaryNavIsOpened();
|
||||
this.closeMobileNavOnPrimaryRouteChange();
|
||||
}
|
||||
|
||||
redirectToDocsVersion(versionUrl: string): void {
|
||||
this.window.location.href = versionUrl;
|
||||
}
|
||||
|
||||
setTheme(theme: Theme): void {
|
||||
this.themeManager.setTheme(theme);
|
||||
}
|
||||
|
||||
openVersionMenu($event: MouseEvent): void {
|
||||
// It's required to avoid redirection to `home`
|
||||
$event.stopImmediatePropagation();
|
||||
$event.preventDefault();
|
||||
this.openMenu('version-picker');
|
||||
}
|
||||
|
||||
openMenu(menuType: MenuType): void {
|
||||
this.openedMenu = menuType;
|
||||
}
|
||||
|
||||
closeMenu(): void {
|
||||
this.openedMenu = null;
|
||||
}
|
||||
|
||||
openMobileNav($event: MouseEvent): void {
|
||||
$event.stopPropagation();
|
||||
this.navigationState.setMobileNavigationListVisibility(true);
|
||||
}
|
||||
|
||||
closeMobileNav(): void {
|
||||
this.navigationState.setMobileNavigationListVisibility(false);
|
||||
}
|
||||
|
||||
toggleSearchDialog(event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.isSearchDialogOpen.update((isOpen) => !isOpen);
|
||||
}
|
||||
|
||||
private closeMobileNavOnPrimaryRouteChange(): void {
|
||||
this.primaryRouteChanged$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.closeMobileNav();
|
||||
});
|
||||
}
|
||||
|
||||
private listenToRouteChange(): void {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).urlAfterRedirects),
|
||||
)
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
//using location because router.url will only return "/" here
|
||||
startWith(this.location.path()),
|
||||
)
|
||||
.subscribe((url) => {
|
||||
this.setActivePrimaryRoute(getBaseUrlAfterRedirects(url, this.router));
|
||||
});
|
||||
}
|
||||
|
||||
// Set active route item, based on urlAfterRedirects.
|
||||
// First check if url starts with the main prefixes (docs, reference, tutorials).
|
||||
// (*) Docs navigation tree contains items which will navigate to /tutorials.
|
||||
// In that case after click on such link we should mark as active item, and display tutorials navigation tree.
|
||||
// If it's not starting with prefix then check if specific path exist in the array of defined routes
|
||||
// (*) Reference navigation tree contains items which are not start with prefix like /migrations or /errors.
|
||||
private setActivePrimaryRoute(urlAfterRedirects: string): void {
|
||||
if (urlAfterRedirects === '') {
|
||||
this.activeRouteItem.set(PagePrefix.HOME);
|
||||
} else if (urlAfterRedirects.startsWith(PagePrefix.DOCS)) {
|
||||
this.activeRouteItem.set(PagePrefix.DOCS);
|
||||
} else if (
|
||||
urlAfterRedirects.startsWith(PagePrefix.REFERENCE) ||
|
||||
urlAfterRedirects.startsWith(PagePrefix.API)
|
||||
) {
|
||||
this.activeRouteItem.set(PagePrefix.REFERENCE);
|
||||
} else if (urlAfterRedirects === PagePrefix.PLAYGROUND) {
|
||||
this.activeRouteItem.set(PagePrefix.PLAYGROUND);
|
||||
} else if (urlAfterRedirects.startsWith(PagePrefix.TUTORIALS)) {
|
||||
this.activeRouteItem.set(PagePrefix.TUTORIALS);
|
||||
} else if (DOCS_ROUTES.some((route) => route.path === urlAfterRedirects)) {
|
||||
this.activeRouteItem.set(PagePrefix.DOCS);
|
||||
} else if (REFERENCE_ROUTES.some((route) => route.path === urlAfterRedirects)) {
|
||||
this.activeRouteItem.set(PagePrefix.REFERENCE);
|
||||
} else if (TUTORIALS_ROUTES.some((route) => route.path === urlAfterRedirects)) {
|
||||
this.activeRouteItem.set(PagePrefix.TUTORIALS);
|
||||
} else {
|
||||
// Reset if no active route item could be found
|
||||
this.activeRouteItem.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private preventToScrollContentWhenSecondaryNavIsOpened(): void {
|
||||
this.isMobileNavigationOpened$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((opened) => {
|
||||
if (opened) {
|
||||
this.document.body.style.overflowY = 'hidden';
|
||||
} else {
|
||||
this.document.body.style.removeProperty('overflow-y');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import {PROGRESS_BAR_DELAY, ProgressBarComponent} from './progress-bar.component';
|
||||
import {RouterTestingHarness, RouterTestingModule} from '@angular/router/testing';
|
||||
|
||||
describe('ProgressBarComponent', () => {
|
||||
let component: ProgressBarComponent;
|
||||
let fixture: ComponentFixture<ProgressBarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProgressBarComponent, RouterTestingModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProgressBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call progressBar.complete() on route change', fakeAsync(async () => {
|
||||
const progressBarCompleteSpy = spyOn(component.progressBar, 'complete');
|
||||
|
||||
const harness = await RouterTestingHarness.create();
|
||||
await harness.navigateByUrl('/');
|
||||
|
||||
tick(PROGRESS_BAR_DELAY);
|
||||
expect(progressBarCompleteSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
import {NgProgressComponent} from 'ngx-progressbar';
|
||||
import {
|
||||
NavigationCancel,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
NavigationSkipped,
|
||||
NavigationStart,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {filter, map, switchMap, take} from 'rxjs/operators';
|
||||
|
||||
/** Time to wait after navigation starts before showing the progress bar. This delay allows a small amount of time to skip showing the progress bar when a navigation is effectively immediate. 30ms is approximately the amount of time we can wait before a delay is perceptible.*/
|
||||
export const PROGRESS_BAR_DELAY = 30;
|
||||
|
||||
@Component({
|
||||
selector: 'adev-progress-bar',
|
||||
standalone: true,
|
||||
imports: [NgProgressComponent],
|
||||
template: `
|
||||
<ng-progress aria-label="Page load progress" />
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProgressBarComponent implements OnInit {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
@ViewChild(NgProgressComponent, {static: true}) progressBar!: NgProgressComponent;
|
||||
|
||||
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
ngOnInit() {
|
||||
this.setupPageNavigationDimming();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dims the main router-outlet content when navigating to a new page.
|
||||
*/
|
||||
private setupPageNavigationDimming() {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((e) => e instanceof NavigationStart),
|
||||
map(() => {
|
||||
// Only apply set the property if the navigation is not "immediate"
|
||||
return setTimeout(() => {
|
||||
this.progressBar.start();
|
||||
}, PROGRESS_BAR_DELAY);
|
||||
}),
|
||||
switchMap((timeoutId) => {
|
||||
return this.router.events.pipe(
|
||||
filter((e) => {
|
||||
return (
|
||||
e instanceof NavigationEnd ||
|
||||
e instanceof NavigationCancel ||
|
||||
e instanceof NavigationSkipped ||
|
||||
e instanceof NavigationError
|
||||
);
|
||||
}),
|
||||
take(1),
|
||||
map(() => timeoutId),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe((timeoutId) => {
|
||||
// When the navigation finishes, prevent the navigating class from being applied in the timeout.
|
||||
clearTimeout(timeoutId);
|
||||
this.progressBar.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<div [attr.id]="SECONDARY_NAV_ID" class="adev-secondary-nav-mask" [class.docs-nav-secondary--open]="isSecondaryNavVisible()" (docsClickOutside)="close()" [docsClickOutsideIgnore]="[PRIMARY_NAV_ID]">
|
||||
<div class="docs-nav-secondary docs-scroll-track-transparent" [style.transform]="translateX()" [style.transition]="transition">
|
||||
<!-- Secondary Nav -->
|
||||
@if (navigationItems && navigationItems.length > 0) {
|
||||
<docs-navigation-list
|
||||
[navigationItems]="navigationItems"
|
||||
[displayItemsToLevel]="maxVisibleLevelsOnSecondaryNav()"
|
||||
[expandableLevel]="maxVisibleLevelsOnSecondaryNav()"
|
||||
(linkClicked)="close()"
|
||||
/>
|
||||
}
|
||||
|
||||
<!-- Third, fourth and next levels of navigation -->
|
||||
@for (item of navigationItemsSlides(); track item; let level = $index) {
|
||||
<docs-navigation-list
|
||||
[@leaveAnimation]
|
||||
[collapsableLevel]="level + maxVisibleLevelsOnSecondaryNav()"
|
||||
[expandableLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
|
||||
[navigationItems]="[item]"
|
||||
[displayItemsToLevel]="level + maxVisibleLevelsOnSecondaryNav() + 1"
|
||||
(linkClicked)="close()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,72 +0,0 @@
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
:host {
|
||||
@include mq.for-tablet-landscape-up {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
// Render secondary nav below primary nav
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
position: fixed;
|
||||
// Render secondary nav above mobile menu
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
transform: translateX(0);
|
||||
z-index: 99;
|
||||
transition: transform 0.3s ease;
|
||||
&:has(.docs-nav-secondary--open) {
|
||||
transform: translateX(82px);
|
||||
transition: transform 0.3s ease-in 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .adev-secondary-nav-mask stays in the same place during nav animation, as a mask
|
||||
.adev-secondary-nav-mask {
|
||||
position: sticky;
|
||||
@include mq.for-tablet-landscape-down {
|
||||
position: absolute;
|
||||
}
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
min-width: var(--secondary-nav-width);
|
||||
border-inline-end: 1px solid var(--septenary-contrast);
|
||||
background-color: var(--page-background);
|
||||
z-index: var(--z-index-nav);
|
||||
transition: transform 0.45s ease;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform 0.45s ease, background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
transform: translateX(-100%);
|
||||
|
||||
&.docs-nav-secondary--open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
transform: translateX(-100%);
|
||||
// exit immediately
|
||||
transition: transform 0.45s ease-in;
|
||||
|
||||
&.docs-nav-secondary--open {
|
||||
transform: translateX(0);
|
||||
// enter delayed
|
||||
transition: transform 0.45s ease-out 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// secondary nav - translated on x to display levels
|
||||
.docs-nav-secondary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: var(--secondary-nav-width);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {SecondaryNavigation} from './secondary-navigation.component';
|
||||
import {WINDOW} from '@angular/docs';
|
||||
|
||||
describe('SecondaryNavigation', () => {
|
||||
let component: SecondaryNavigation;
|
||||
let fixture: ComponentFixture<SecondaryNavigation>;
|
||||
|
||||
const fakeWindow = {
|
||||
location: {
|
||||
origin: 'example-origin',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SecondaryNavigation],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(SecondaryNavigation);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
ClickOutside,
|
||||
NavigationItem,
|
||||
NavigationList,
|
||||
NavigationState,
|
||||
WINDOW,
|
||||
findNavigationItem,
|
||||
getBaseUrlAfterRedirects,
|
||||
getNavigationItemsTree,
|
||||
markExternalLinks,
|
||||
shouldReduceMotion,
|
||||
} from '@angular/docs';
|
||||
import {distinctUntilChanged, filter, map, skip, startWith} from 'rxjs/operators';
|
||||
import {SUB_NAVIGATION_DATA} from '../../../sub-navigation-data';
|
||||
import {PagePrefix} from '../../enums/pages';
|
||||
import {ActivatedRouteSnapshot, NavigationEnd, Router, RouterStateSnapshot} from '@angular/router';
|
||||
import {NgFor, NgIf, isPlatformBrowser} from '@angular/common';
|
||||
import {trigger, transition, style, animate} from '@angular/animations';
|
||||
import {PRIMARY_NAV_ID, SECONDARY_NAV_ID} from '../../constants/element-ids';
|
||||
|
||||
export const ANIMATION_DURATION = 500;
|
||||
|
||||
@Component({
|
||||
selector: 'adev-secondary-navigation',
|
||||
standalone: true,
|
||||
imports: [NavigationList, NgIf, NgFor, ClickOutside],
|
||||
templateUrl: './secondary-navigation.component.html',
|
||||
styleUrls: ['./secondary-navigation.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('leaveAnimation', [
|
||||
transition(':leave', [
|
||||
style({transform: 'translateX(0%)'}),
|
||||
animate(
|
||||
`${ANIMATION_DURATION}ms ${ANIMATION_DURATION}ms ease-out`,
|
||||
style({transform: 'translateX(100%)'}),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class SecondaryNavigation implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
private readonly router = inject(Router);
|
||||
private readonly window = inject(WINDOW);
|
||||
|
||||
isSecondaryNavVisible = this.navigationState.isMobileNavVisible;
|
||||
primaryActiveRouteItem = this.navigationState.primaryActiveRouteItem;
|
||||
maxVisibleLevelsOnSecondaryNav = computed(() =>
|
||||
this.primaryActiveRouteItem() === PagePrefix.REFERENCE ? 1 : 2,
|
||||
);
|
||||
navigationItemsSlides = this.navigationState.expandedItems;
|
||||
navigationItems: NavigationItem[] | undefined;
|
||||
|
||||
translateX = computed(() => {
|
||||
const level = this.navigationState.expandedItems()?.length ?? 0;
|
||||
return `translateX(${-level * 100}%)`;
|
||||
});
|
||||
transition: string = '0ms';
|
||||
|
||||
readonly PRIMARY_NAV_ID = PRIMARY_NAV_ID;
|
||||
readonly SECONDARY_NAV_ID = SECONDARY_NAV_ID;
|
||||
|
||||
private readonly routeMap: Record<string, NavigationItem[]> = {
|
||||
[PagePrefix.REFERENCE]: getNavigationItemsTree(SUB_NAVIGATION_DATA.reference, (tree) =>
|
||||
markExternalLinks(tree, this.window.origin),
|
||||
),
|
||||
[PagePrefix.DOCS]: getNavigationItemsTree(SUB_NAVIGATION_DATA.docs, (tree) =>
|
||||
markExternalLinks(tree, this.window.origin),
|
||||
),
|
||||
};
|
||||
|
||||
private readonly primaryActiveRouteChanged$ = toObservable(this.primaryActiveRouteItem).pipe(
|
||||
distinctUntilChanged(),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
private readonly urlAfterRedirects$ = this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).urlAfterRedirects),
|
||||
filter((url): url is string => url !== undefined),
|
||||
startWith(this.getInitialPath(this.router.routerState.snapshot)),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.navigationState.cleanExpandedState();
|
||||
this.listenToPrimaryRouteChange();
|
||||
this.setActiveRouteOnNavigationEnd();
|
||||
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.initSlideAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.navigationState.setMobileNavigationListVisibility(false);
|
||||
}
|
||||
|
||||
private setActiveRouteOnNavigationEnd(): void {
|
||||
this.urlAfterRedirects$.subscribe((url) => {
|
||||
const activeNavigationItem = this.getActiveNavigationItem(url);
|
||||
if (
|
||||
activeNavigationItem?.level &&
|
||||
activeNavigationItem.level <= this.maxVisibleLevelsOnSecondaryNav()
|
||||
) {
|
||||
this.navigationState.cleanExpandedState();
|
||||
} else if (activeNavigationItem) {
|
||||
/**
|
||||
* For the `Docs`, we don't expand the "level === 1" items because they are already displayed in the main navigation list.
|
||||
* Example:
|
||||
* In-depth Guides (level == 0)
|
||||
* Components (level == 1) -> Selectors, Styling, etc (level == 2)
|
||||
* Template Syntax (level == 1) -> Text interpolation, etc (level == 2)
|
||||
*
|
||||
* For the `Tutorials`, we display the navigation in the dropdown and it has flat structure (all items are displayed as items with level === 0).
|
||||
*
|
||||
* For the `Reference` we would like to give possibility to expand the "level === 1" items cause they are not visible in the first slide of navigation list.
|
||||
* Example:
|
||||
* API Reference (level == 0) -> Overview, Animations, common, etc (level == 1) -> API Package exports (level == 2)
|
||||
*/
|
||||
const shouldExpandItem = (node: NavigationItem): boolean =>
|
||||
!!node.level &&
|
||||
(this.primaryActiveRouteItem() === PagePrefix.REFERENCE
|
||||
? node.level > 0
|
||||
: node.level > 1);
|
||||
|
||||
// Skip expand when active item is API Reference homepage - `/api`.
|
||||
// It protect us from displaying second level of the navigation when user clicks on `Reference`,
|
||||
// Because in this situation we want to display the first level, which contains, in addition to the API Reference, also the CLI Reference, Error Encyclopedia etc.
|
||||
const skipExpandPredicateFn = (node: NavigationItem): boolean =>
|
||||
node.path === PagePrefix.API;
|
||||
|
||||
this.navigationState.expandItemHierarchy(
|
||||
activeNavigationItem,
|
||||
shouldExpandItem,
|
||||
skipExpandPredicateFn,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getActiveNavigationItem(url: string): NavigationItem | null {
|
||||
// set visible navigation items if not present
|
||||
this.setVisibleNavigationItems();
|
||||
|
||||
const activeNavigationItem = findNavigationItem(
|
||||
this.navigationItems!,
|
||||
(item) =>
|
||||
!!item.path &&
|
||||
getBaseUrlAfterRedirects(item.path, this.router) ===
|
||||
getBaseUrlAfterRedirects(url, this.router),
|
||||
);
|
||||
|
||||
this.navigationState.setActiveNavigationItem(activeNavigationItem);
|
||||
|
||||
return activeNavigationItem;
|
||||
}
|
||||
|
||||
private initSlideAnimation(): void {
|
||||
if (shouldReduceMotion()) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.transition = `${ANIMATION_DURATION}ms`;
|
||||
}, ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
private setVisibleNavigationItems(): void {
|
||||
const routeMap = this.routeMap[this.primaryActiveRouteItem()!];
|
||||
this.navigationItems = routeMap
|
||||
? getNavigationItemsTree(routeMap, (item) => {
|
||||
item.isExpanded = this.primaryActiveRouteItem() === PagePrefix.DOCS && item.level === 1;
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
private listenToPrimaryRouteChange(): void {
|
||||
// Fix: flicker of sub-navigation on init
|
||||
this.primaryActiveRouteChanged$.pipe(skip(1)).subscribe(() => {
|
||||
this.navigationState.cleanExpandedState();
|
||||
});
|
||||
}
|
||||
|
||||
private getInitialPath(routerState: RouterStateSnapshot): string {
|
||||
let route: ActivatedRouteSnapshot = routerState.root;
|
||||
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
return route.routeConfig?.path ?? '';
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {TITLE_SUFFIX, ADevTitleStrategy, DEFAULT_PAGE_TITLE} from './a-dev-title-strategy';
|
||||
import {Router, provideRouter} from '@angular/router';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
class FakeComponent {}
|
||||
|
||||
describe('ADevTitleStrategy', () => {
|
||||
let service: ADevTitleStrategy;
|
||||
let router: Router;
|
||||
let title: Title;
|
||||
|
||||
const routes = [
|
||||
{path: 'first', data: {label: 'First'}, component: FakeComponent},
|
||||
{path: 'second', component: FakeComponent},
|
||||
{
|
||||
path: 'third',
|
||||
data: {label: 'Third'},
|
||||
component: FakeComponent,
|
||||
children: [{path: 'child', data: {label: 'Child'}, component: FakeComponent}],
|
||||
},
|
||||
{
|
||||
path: 'fourth',
|
||||
data: {label: 'Fourth'},
|
||||
component: FakeComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'child',
|
||||
data: {label: 'Overview', parent: {label: 'Fourth'}},
|
||||
component: FakeComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter(routes)],
|
||||
});
|
||||
service = TestBed.inject(ADevTitleStrategy);
|
||||
router = TestBed.inject(Router);
|
||||
title = TestBed.inject(Title);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should set '${TITLE_SUFFIX}' when route doesn't have defined label`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/second');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith(TITLE_SUFFIX);
|
||||
});
|
||||
|
||||
it(`should set 'Third - ${TITLE_SUFFIX}' when route has defined label equal to 'Third'`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/third');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith('Third • Angular');
|
||||
});
|
||||
|
||||
it(`shouldn't take label from the parent route when current route label is not equal to ${DEFAULT_PAGE_TITLE}`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/third/child');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith('Child • Angular');
|
||||
});
|
||||
|
||||
it(`should take label from the parent route when current route label is equal to ${DEFAULT_PAGE_TITLE}`, async () => {
|
||||
spyOn(title, 'setTitle');
|
||||
|
||||
await router.navigateByUrl('/fourth/child');
|
||||
service.updateTitle(router.routerState.snapshot);
|
||||
|
||||
expect(title.setTitle).toHaveBeenCalledOnceWith('Fourth • Overview • Angular');
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NavigationItem} from '@angular/docs';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot, TitleStrategy} from '@angular/router';
|
||||
|
||||
export const ROUTE_TITLE_PROPERTY = 'label';
|
||||
export const ROUTE_PARENT_PROPERTY = 'parent';
|
||||
export const TITLE_SUFFIX = 'Angular';
|
||||
export const TITLE_SEPARATOR = ' • ';
|
||||
export const DEFAULT_PAGE_TITLE = 'Overview';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class ADevTitleStrategy extends TitleStrategy {
|
||||
constructor(private readonly title: Title) {
|
||||
super();
|
||||
}
|
||||
|
||||
override updateTitle(routerState: RouterStateSnapshot) {
|
||||
const title = this.buildTitle(routerState);
|
||||
|
||||
if (title !== undefined) {
|
||||
this.title.setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
override buildTitle(snapshot: RouterStateSnapshot): string {
|
||||
let route: ActivatedRouteSnapshot = snapshot.root;
|
||||
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
const data = route.data as NavigationItem;
|
||||
const routeTitle = data.label ?? '';
|
||||
|
||||
const prefix =
|
||||
routeTitle.startsWith(DEFAULT_PAGE_TITLE) && data.parent
|
||||
? `${data.parent.label}${TITLE_SEPARATOR}`
|
||||
: '';
|
||||
|
||||
return !!routeTitle ? `${prefix}${routeTitle}${TITLE_SEPARATOR}${TITLE_SUFFIX}` : TITLE_SUFFIX;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formats an `ErrorEvent` to a human-readable string that can
|
||||
* be sent to Google Analytics.
|
||||
*/
|
||||
export function formatErrorEventForAnalytics(event: ErrorEvent): string {
|
||||
const {message, filename, colno, lineno, error} = event;
|
||||
|
||||
if (error instanceof Error) {
|
||||
return formatErrorForAnalytics(error);
|
||||
}
|
||||
|
||||
return `${stripErrorMessagePrefix(message)}\n${filename}:` + `${lineno || '?'}:${colno || '?'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an `Error` to a human-readable string that can be sent
|
||||
* to Google Analytics.
|
||||
*/
|
||||
export function formatErrorForAnalytics(error: Error): string {
|
||||
let stack = '<no-stack>';
|
||||
|
||||
if (error.stack) {
|
||||
stack = stripErrorMessagePrefix(error.stack)
|
||||
// strip the message from the stack trace, if present
|
||||
.replace(error.message + '\n', '')
|
||||
// strip leading spaces
|
||||
.replace(/^ +/gm, '')
|
||||
// strip all leading "at " for each frame
|
||||
.replace(/^at /gm, '')
|
||||
// replace long urls with just the last segment: `filename:line:column`
|
||||
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
|
||||
// replace "eval code" in Edge
|
||||
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n');
|
||||
}
|
||||
|
||||
return `${error.message}\n${stack}`;
|
||||
}
|
||||
|
||||
/** Strips the error message prefix from a message or stack trace. */
|
||||
function stripErrorMessagePrefix(input: string): string {
|
||||
return input.replace(/^(Uncaught )?Error: /, '');
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injector} from '@angular/core';
|
||||
import {ENVIRONMENT, WINDOW} from '@angular/docs';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
let service: AnalyticsService;
|
||||
let injector: Injector;
|
||||
let gtagSpy: jasmine.Spy;
|
||||
let gtagAppendNodeSpy: jasmine.Spy;
|
||||
let windowOnErrorHandler: (event: ErrorEvent) => void;
|
||||
|
||||
let mockWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
gtagSpy = jasmine.createSpy('gtag');
|
||||
gtagAppendNodeSpy = jasmine.createSpy('gtag.js script head attach');
|
||||
|
||||
mockWindow = {
|
||||
name: 'Some name',
|
||||
document: {
|
||||
head: {appendChild: gtagAppendNodeSpy},
|
||||
createElement: (tag: string) => document.createElement(tag),
|
||||
querySelector: (_gtagIdSelector: string) => null,
|
||||
},
|
||||
addEventListener: (_name: string, handler: typeof windowOnErrorHandler) =>
|
||||
(windowOnErrorHandler = handler),
|
||||
};
|
||||
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ENVIRONMENT, useValue: {}},
|
||||
{provide: AnalyticsService, deps: [WINDOW]},
|
||||
{provide: WINDOW, useFactory: () => mockWindow, deps: []},
|
||||
],
|
||||
});
|
||||
|
||||
service = injector.get(AnalyticsService);
|
||||
|
||||
// The `gtag` function is attached to the `Window`, so we can spy on it
|
||||
// after the service has been initialized.
|
||||
gtagSpy = spyOn(mockWindow, 'gtag');
|
||||
});
|
||||
|
||||
describe('error reporting', () => {
|
||||
it('should subscribe to window uncaught errors and report them', () => {
|
||||
spyOn(service, 'reportError');
|
||||
|
||||
windowOnErrorHandler(
|
||||
new ErrorEvent('error', {
|
||||
error: new Error('Test Error'),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(service.reportError).toHaveBeenCalledTimes(1);
|
||||
expect(service.reportError).toHaveBeenCalledWith(
|
||||
jasmine.stringContaining('Test Error\n'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should report errors to analytics by dispatching `gtag` and `ga` events', () => {
|
||||
gtagSpy.calls.reset();
|
||||
|
||||
windowOnErrorHandler(
|
||||
new ErrorEvent('error', {
|
||||
error: new Error('Test Error'),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(gtagSpy).toHaveBeenCalledTimes(1);
|
||||
expect(gtagSpy).toHaveBeenCalledWith(
|
||||
'event',
|
||||
'exception',
|
||||
jasmine.objectContaining({
|
||||
description: jasmine.stringContaining('Test Error\n'),
|
||||
fatal: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
|
||||
import {WINDOW, ENVIRONMENT} from '@angular/docs';
|
||||
|
||||
import {formatErrorEventForAnalytics} from './analytics-format-error';
|
||||
|
||||
/** Extension of `Window` with potential Google Analytics fields. */
|
||||
interface WindowWithAnalytics extends Window {
|
||||
dataLayer?: any[];
|
||||
gtag?(...args: any[]): void;
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
/**
|
||||
* Google Analytics Service - captures app behaviors and sends them to Google Analytics.
|
||||
*
|
||||
* Associates data wi`th properties determined from the environment configurations:
|
||||
* - Data is uploaded to our main Google Analytics 4+ property.
|
||||
*/
|
||||
export class AnalyticsService {
|
||||
private environment = inject(ENVIRONMENT);
|
||||
private window: WindowWithAnalytics = inject(WINDOW);
|
||||
|
||||
constructor() {
|
||||
this._installGlobalSiteTag();
|
||||
this._installWindowErrorHandler();
|
||||
}
|
||||
|
||||
reportError(description: string, fatal = true) {
|
||||
// Limit descriptions to maximum of 150 characters.
|
||||
// See: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#exd.
|
||||
description = description.substring(0, 150);
|
||||
|
||||
this._gtag('event', 'exception', {description, fatal});
|
||||
}
|
||||
|
||||
sendEvent(name: string, parameters: Record<string, string | boolean | number>) {
|
||||
this._gtag('event', name, parameters);
|
||||
}
|
||||
|
||||
private _gtag(...args: any[]) {
|
||||
if (this.window.gtag) {
|
||||
this.window.gtag(...args);
|
||||
}
|
||||
}
|
||||
|
||||
private _installGlobalSiteTag() {
|
||||
const window = this.window;
|
||||
const url = `https://www.googletagmanager.com/gtag/js?id=${this.environment.googleAnalyticsId}`;
|
||||
|
||||
// Note: This cannot be an arrow function as `gtag.js` expects an actual `Arguments`
|
||||
// instance with e.g. `callee` to be set. Do not attempt to change this and keep this
|
||||
// as much as possible in sync with the tracking code snippet suggested by the Google
|
||||
// Analytics 4 web UI under `Data Streams`.
|
||||
window.dataLayer = this.window.dataLayer || [];
|
||||
window.gtag = function () {
|
||||
window.dataLayer?.push(arguments);
|
||||
};
|
||||
window.gtag('js', new Date());
|
||||
|
||||
// Configure properties before loading the script. This is necessary to avoid
|
||||
// loading multiple instances of the gtag JS scripts.
|
||||
window.gtag('config', this.environment.googleAnalyticsId);
|
||||
|
||||
// Only add the element if `gtag` is not loaded yet. It might already
|
||||
// be inlined into the `index.html` via SSR.
|
||||
if (window.document.querySelector('#gtag-script') === null) {
|
||||
const el = window.document.createElement('script');
|
||||
el.async = true;
|
||||
el.src = url as string;
|
||||
el.id = 'gtag-script';
|
||||
window.document.head.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
private _installWindowErrorHandler() {
|
||||
this.window.addEventListener('error', (event) =>
|
||||
this.reportError(formatErrorEventForAnalytics(event), true),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ErrorHandler, Injector, VERSION} from '@angular/core';
|
||||
|
||||
import {ReportingErrorHandler} from './reporting-error-handler';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
describe('ReportingErrorHandler service', () => {
|
||||
let handler: ReportingErrorHandler;
|
||||
let superHandler: jasmine.Spy;
|
||||
let reportErrorSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
|
||||
reportErrorSpy = jasmine.createSpy('analytics report error');
|
||||
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{provide: AnalyticsService, useValue: {reportError: reportErrorSpy}},
|
||||
{provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [AnalyticsService]},
|
||||
],
|
||||
});
|
||||
|
||||
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
|
||||
});
|
||||
|
||||
describe('handleError', () => {
|
||||
it('should call the super class handleError', () => {
|
||||
const error = new Error();
|
||||
handler.handleError(error);
|
||||
expect(superHandler).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should cope with the super handler throwing an error', () => {
|
||||
const error = new Error('initial error');
|
||||
superHandler.and.throwError('super handler error');
|
||||
handler.handleError(error);
|
||||
|
||||
expect(reportErrorSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Error from super handler is reported first
|
||||
expect(reportErrorSpy.calls.argsFor(0)[0]).toEqual(
|
||||
jasmine.stringContaining('super handler error\n'),
|
||||
);
|
||||
|
||||
// Then error from initial exception
|
||||
expect(reportErrorSpy.calls.argsFor(1)[0]).toEqual(
|
||||
jasmine.stringContaining(`[v${VERSION.full}] initial error\n`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should augment an error object with version info', () => {
|
||||
const originalMessage = 'this is an error message';
|
||||
const error = new Error(originalMessage);
|
||||
|
||||
expect(error.message).toBe(originalMessage);
|
||||
// NOTE:
|
||||
// Intentionally not checking `error.stack` here, because accessing it causes the property to
|
||||
// be computed and may affect the observed behavior of `handleError()`.
|
||||
|
||||
handler.handleError(error);
|
||||
|
||||
expect(error.message).toBe(`[v${VERSION.full}] ${originalMessage}`);
|
||||
if (error.stack) {
|
||||
const expected = `Error: [v${VERSION.full}] ${originalMessage}`;
|
||||
const actual = error.stack.split('\n', 1)[0];
|
||||
expect(actual.startsWith(expected))
|
||||
.withContext(`Expected '${actual}' to start with '${expected}'.`)
|
||||
.toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('should send an error object to analytics', () => {
|
||||
const error = new Error('this is an error message');
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringContaining('this is an error message\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send a non-error object to analytics', () => {
|
||||
const error = {reason: 'this is an error message'};
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(JSON.stringify(error));
|
||||
});
|
||||
|
||||
it('should send a non-error object with circular references to analytics', () => {
|
||||
const error = {
|
||||
reason: 'this is an error message',
|
||||
get self() {
|
||||
return this;
|
||||
},
|
||||
toString() {
|
||||
return `{reason: ${this.reason}}`;
|
||||
},
|
||||
};
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith('{reason: this is an error message}');
|
||||
});
|
||||
|
||||
it('should send an error string to analytics (prefixed with version info)', () => {
|
||||
const error = 'this is an error message';
|
||||
handler.handleError(error);
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(`[v${VERSION.full}] ${error}`);
|
||||
});
|
||||
|
||||
it('should send a non-object, non-string error stringified to analytics', () => {
|
||||
handler.handleError(404);
|
||||
handler.handleError(false);
|
||||
handler.handleError(null);
|
||||
handler.handleError(undefined);
|
||||
|
||||
expect(reportErrorSpy.calls.allArgs()).toEqual([['404'], ['false'], ['null'], ['undefined']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ErrorHandler, Injectable, VERSION} from '@angular/core';
|
||||
import {formatErrorForAnalytics} from './analytics-format-error';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
/**
|
||||
* Extend the default error handling to report errors to an external service - e.g Google Analytics.
|
||||
*
|
||||
* Errors outside the Angular application may also be handled by `window.onerror`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ReportingErrorHandler extends ErrorHandler {
|
||||
constructor(private _analytics: AnalyticsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error info to Google Analytics, in addition to the default handling.
|
||||
*
|
||||
* @param error Information about the error.
|
||||
*/
|
||||
override handleError(error: any) {
|
||||
const versionedError = this.prefixErrorWithVersion(error);
|
||||
|
||||
try {
|
||||
super.handleError(versionedError);
|
||||
} catch (e) {
|
||||
this.reportError(e);
|
||||
}
|
||||
this.reportError(versionedError);
|
||||
}
|
||||
|
||||
private prefixErrorWithVersion<T>(error: T): T {
|
||||
const prefix = `[v${VERSION.full}] `;
|
||||
|
||||
if (error instanceof Error) {
|
||||
const oldMessage = error.message;
|
||||
const oldStack = error.stack;
|
||||
|
||||
error.message = prefix + oldMessage;
|
||||
error.stack = oldStack?.replace(oldMessage, error.message);
|
||||
} else if (typeof error === 'string') {
|
||||
error = (prefix + error) as unknown as T;
|
||||
}
|
||||
// If it is a different type, omit the version to avoid altering the original `error` object.
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
private reportError(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
this._analytics.reportError(formatErrorForAnalytics(error));
|
||||
} else {
|
||||
if (typeof error === 'object') {
|
||||
try {
|
||||
error = JSON.stringify(error);
|
||||
} catch {
|
||||
// Ignore the error and just let it be stringified.
|
||||
}
|
||||
}
|
||||
this._analytics.reportError(`${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ContentLoader} from './content-loader.service';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
|
||||
describe('ContentLoader', () => {
|
||||
let service: ContentLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ContentLoader],
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
service = TestBed.inject(ContentLoader);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {DocContent, DocsContentLoader} from '@angular/docs';
|
||||
import {Router} from '@angular/router';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ContentLoader implements DocsContentLoader {
|
||||
private readonly cache = new Map<string, Promise<DocContent | undefined>>();
|
||||
private readonly httpClient = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
async getContent(path: string): Promise<DocContent | undefined> {
|
||||
// If the path does not end with a file extension, add `.md.html` as the default
|
||||
if (!path.match(/\.\w+$/)) {
|
||||
path += '.md.html';
|
||||
}
|
||||
if (!this.cache.has(path)) {
|
||||
try {
|
||||
this.cache.set(
|
||||
path,
|
||||
this.httpClient
|
||||
.get(`assets/content/${path}`, {
|
||||
responseType: 'text',
|
||||
})
|
||||
.pipe(map((contents) => ({contents, id: path})))
|
||||
.toPromise(),
|
||||
);
|
||||
} catch {
|
||||
this.router.navigateByUrl('/404');
|
||||
}
|
||||
}
|
||||
return this.cache.get(path)!;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformServer} from '@angular/common';
|
||||
import {ErrorHandler, PLATFORM_ID, inject} from '@angular/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {AnalyticsService} from '../analytics/analytics.service';
|
||||
import {ErrorSnackBar, ErrorSnackBarData} from './error-snack-bar';
|
||||
|
||||
export class CustomErrorHandler implements ErrorHandler {
|
||||
snackBar = inject(MatSnackBar);
|
||||
document = inject(DOCUMENT);
|
||||
isServer = isPlatformServer(inject(PLATFORM_ID));
|
||||
analyticsService = inject(AnalyticsService);
|
||||
|
||||
get isOnline(): boolean {
|
||||
if (this.isServer) return false;
|
||||
|
||||
const win = this.document.defaultView;
|
||||
return win?.navigator.onLine ?? true;
|
||||
}
|
||||
|
||||
handleError(error: any) {
|
||||
if (typeof error.message === 'string') {
|
||||
// Just looking at the first line of the error message (ignoring the call stack part),
|
||||
// which should contain a pattern that we are looking for.
|
||||
const firstLine = error.message.split('\n')[0];
|
||||
if (this.isOnline && firstLine?.match(/chunk-(.*?)\.js/)) {
|
||||
// Trying to load a chunk that doesn't exist anymore
|
||||
// Users should reload the app.
|
||||
this.openErrorSnackBar();
|
||||
this.analyticsService.reportError('Chunk loading error');
|
||||
}
|
||||
}
|
||||
|
||||
// We still want to log every error
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
openErrorSnackBar(): void {
|
||||
this.snackBar
|
||||
.openFromComponent(ErrorSnackBar, {
|
||||
panelClass: 'docs-invert-mode',
|
||||
data: {
|
||||
message: `Our docs have been updated, reload the page to see the latest.`,
|
||||
actionText: `Reload`,
|
||||
} satisfies ErrorSnackBarData,
|
||||
})
|
||||
.onAction()
|
||||
.subscribe(() => {
|
||||
this.document.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {MAT_SNACK_BAR_DATA, MatSnackBarAction, MatSnackBarRef} from '@angular/material/snack-bar';
|
||||
|
||||
export interface ErrorSnackBarData {
|
||||
message: string;
|
||||
actionText?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'error-snack-bar',
|
||||
template: `
|
||||
{{ message }}
|
||||
<button
|
||||
class="docs-primary-btn"
|
||||
type="button"
|
||||
matSnackBarAction
|
||||
[attr.text]="actionText"
|
||||
(click)="snackBarRef.dismissWithAction()"
|
||||
>
|
||||
{{ actionText }}
|
||||
</button>
|
||||
`,
|
||||
imports: [MatSnackBarAction],
|
||||
standalone: true,
|
||||
styles: `:host { display: flex; align-items: center; button { margin-left: 16px }}`,
|
||||
})
|
||||
export class ErrorSnackBar {
|
||||
protected message: string;
|
||||
protected actionText?: string;
|
||||
|
||||
constructor(
|
||||
protected snackBarRef: MatSnackBarRef<ErrorSnackBar>,
|
||||
@Inject(MAT_SNACK_BAR_DATA) public data: ErrorSnackBarData,
|
||||
) {
|
||||
this.message = data.message;
|
||||
this.actionText = data.actionText;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ExampleContentLoader} from './example-content-loader.service';
|
||||
import {PREVIEWS_COMPONENTS} from '@angular/docs';
|
||||
|
||||
describe('ExampleContentLoader', () => {
|
||||
let service: ExampleContentLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ExampleContentLoader, {provide: PREVIEWS_COMPONENTS, useValue: []}],
|
||||
});
|
||||
service = TestBed.inject(ExampleContentLoader);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable, Type, inject} from '@angular/core';
|
||||
import {PREVIEWS_COMPONENTS} from '@angular/docs';
|
||||
|
||||
@Injectable()
|
||||
export class ExampleContentLoader {
|
||||
private readonly previewsComponents = inject(PREVIEWS_COMPONENTS);
|
||||
|
||||
loadPreview(id: string): Promise<Type<unknown>> {
|
||||
return this.previewsComponents[id]();
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
DestroyRef,
|
||||
ENVIRONMENT_INITIALIZER,
|
||||
EnvironmentInjector,
|
||||
Injectable,
|
||||
Injector,
|
||||
Provider,
|
||||
ProviderToken,
|
||||
Type,
|
||||
createEnvironmentInjector,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
/**
|
||||
* inject a service asynchronously
|
||||
*
|
||||
* @param: injector. If the injector is a NodeInjector the loaded module will be destroyed alonside its injector
|
||||
*/
|
||||
export async function injectAsync<T>(
|
||||
injector: Injector,
|
||||
providerLoader: () => Promise<ProviderToken<T>>,
|
||||
): Promise<T> {
|
||||
const injectImpl = injector.get(InjectAsyncImpl);
|
||||
return injectImpl.get(injector, providerLoader);
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class InjectAsyncImpl<T> {
|
||||
private overrides = new WeakMap(); // no need to cleanup
|
||||
override<T>(type: Type<T>, mock: Type<unknown>) {
|
||||
this.overrides.set(type, mock);
|
||||
}
|
||||
|
||||
async get(injector: Injector, providerLoader: () => Promise<ProviderToken<T>>): Promise<T> {
|
||||
const type = await providerLoader();
|
||||
|
||||
// Check if we have overrides, O(1), low overhead
|
||||
if (this.overrides.has(type)) {
|
||||
const override = this.overrides.get(type);
|
||||
return new override();
|
||||
}
|
||||
|
||||
if (!(injector instanceof EnvironmentInjector)) {
|
||||
// this is the DestroyRef of the component
|
||||
const destroyRef = injector.get(DestroyRef);
|
||||
|
||||
// This is the parent injector of the injector we're creating
|
||||
const environmentInjector = injector.get(EnvironmentInjector);
|
||||
|
||||
// Creating an environment injector to destroy it afterwards
|
||||
const newInjector = createEnvironmentInjector([type as Provider], environmentInjector);
|
||||
|
||||
// Destroy the injector to trigger DestroyRef.onDestroy on our service
|
||||
destroyRef.onDestroy(() => {
|
||||
newInjector.destroy();
|
||||
});
|
||||
|
||||
// We want to create the new instance of our service with our new injector
|
||||
injector = newInjector;
|
||||
}
|
||||
|
||||
return injector.get(type)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to mock the lazy loaded module in `injectAsync`
|
||||
*
|
||||
* @usage
|
||||
* TestBed.configureTestingModule({
|
||||
* providers: [
|
||||
* mockAsyncProvider(SandboxService, fakeSandboxService)
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
export function mockAsyncProvider<T>(type: Type<T>, mock: Type<unknown>) {
|
||||
return [
|
||||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
inject(InjectAsyncImpl).override(type, mock);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ThemeManager} from './theme-manager.service';
|
||||
import {LOCAL_STORAGE} from '@angular/docs';
|
||||
|
||||
describe('ThemeManager', () => {
|
||||
let service: ThemeManager;
|
||||
let localStorageSpy: jasmine.SpyObj<Storage>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageSpy = jasmine.createSpyObj<Storage>('localStorage', ['getItem', 'setItem']);
|
||||
localStorageSpy.getItem.and.returnValue(null);
|
||||
localStorageSpy.setItem.and.returnValue();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: LOCAL_STORAGE,
|
||||
useValue: localStorageSpy,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should set theme based on device preferences (auto) when user did not set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue(null);
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('auto');
|
||||
});
|
||||
|
||||
it('should set theme based on stored user preferences (dark) when user already set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue('dark');
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('dark');
|
||||
});
|
||||
|
||||
it('should set theme based on stored user preferences (light) when user already set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue('light');
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('light');
|
||||
});
|
||||
|
||||
it('should set theme based on stored user preferences (auto) when user already set theme manually', () => {
|
||||
localStorageSpy.getItem.and.returnValue('auto');
|
||||
|
||||
service = TestBed.inject(ThemeManager);
|
||||
|
||||
expect(service.theme()).toBe('auto');
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {Injectable, PLATFORM_ID, inject, signal} from '@angular/core';
|
||||
import {LOCAL_STORAGE} from '@angular/docs';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
// Keep these constants in sync with the code in index.html
|
||||
|
||||
export const THEME_PREFERENCE_LOCAL_STORAGE_KEY = 'themePreference';
|
||||
export const DARK_MODE_CLASS_NAME = 'docs-dark-mode';
|
||||
export const LIGHT_MODE_CLASS_NAME = 'docs-light-mode';
|
||||
export const PREFERS_COLOR_SCHEME_DARK = '(prefers-color-scheme: dark)';
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'auto';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ThemeManager {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
|
||||
readonly theme = signal<Theme | null>(this.getThemeFromLocalStorageValue());
|
||||
// Zoneless - it's required to notify that theme was changed. It could be removed when signal-based components will be available.
|
||||
readonly themeChanged$ = new Subject<void>();
|
||||
|
||||
constructor() {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadThemePreference();
|
||||
this.watchPreferredColorScheme();
|
||||
}
|
||||
|
||||
setTheme(theme: Theme): void {
|
||||
this.theme.set(theme);
|
||||
this.setThemeInLocalStorage();
|
||||
this.setThemeBodyClasses(theme === 'auto' ? preferredScheme() : theme);
|
||||
}
|
||||
|
||||
// 1. Read theme preferences stored in localStorage
|
||||
// 2. In case when there are no stored user preferences, then read them from device preferences.
|
||||
private loadThemePreference(): void {
|
||||
const savedUserPreference = this.getThemeFromLocalStorageValue();
|
||||
const useTheme = savedUserPreference ?? 'auto';
|
||||
|
||||
this.theme.set(useTheme);
|
||||
this.setThemeBodyClasses(useTheme === 'auto' ? preferredScheme() : useTheme);
|
||||
}
|
||||
|
||||
// Set theme classes on the body element
|
||||
private setThemeBodyClasses(theme: 'dark' | 'light'): void {
|
||||
const documentClassList = this.document.documentElement.classList;
|
||||
if (theme === 'dark') {
|
||||
documentClassList.add(DARK_MODE_CLASS_NAME);
|
||||
documentClassList.remove(LIGHT_MODE_CLASS_NAME);
|
||||
} else {
|
||||
documentClassList.add(LIGHT_MODE_CLASS_NAME);
|
||||
documentClassList.remove(DARK_MODE_CLASS_NAME);
|
||||
}
|
||||
this.themeChanged$.next();
|
||||
}
|
||||
|
||||
private getThemeFromLocalStorageValue(): Theme | null {
|
||||
const theme = this.localStorage?.getItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY) as Theme | null;
|
||||
return theme ?? null;
|
||||
}
|
||||
|
||||
private setThemeInLocalStorage(): void {
|
||||
if (this.theme()) {
|
||||
this.localStorage?.setItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY, this.theme()!);
|
||||
}
|
||||
}
|
||||
|
||||
private watchPreferredColorScheme() {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
||||
const preferredScheme = event.matches ? 'dark' : 'light';
|
||||
this.setThemeBodyClasses(preferredScheme);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function preferredScheme(): 'dark' | 'light' {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {VersionManager} from './version-manager.service';
|
||||
|
||||
describe('VersionManager', () => {
|
||||
let service: VersionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(VersionManager);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Injectable, VERSION, computed, signal} from '@angular/core';
|
||||
|
||||
// TODO(josephperrott): extract this out of the file into a managed location.
|
||||
const VERSIONS_CONFIG = {
|
||||
currentVersion: 'stable',
|
||||
historicalVersionsLinkPattern: 'https://v{{version}}.angular.dev',
|
||||
mainVersions: [
|
||||
{
|
||||
version: 'stable',
|
||||
url: 'https://angular.dev',
|
||||
},
|
||||
{
|
||||
version: 'v16',
|
||||
url: 'https://v16.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v15',
|
||||
url: 'https://v15.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v14',
|
||||
url: 'https://v14.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v13',
|
||||
url: 'https://v13.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v12',
|
||||
url: 'https://v12.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v11',
|
||||
url: 'https://v11.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v10',
|
||||
url: 'https://v10.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v9',
|
||||
url: 'https://v9.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v8',
|
||||
url: 'https://v8.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v7',
|
||||
url: 'https://v7.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v6',
|
||||
url: 'https://v6.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v5',
|
||||
url: 'https://v5.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v4',
|
||||
url: 'https://v4.angular.io/docs',
|
||||
},
|
||||
{
|
||||
version: 'v2',
|
||||
url: 'https://v2.angular.io/docs',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export interface Version {
|
||||
displayName: string;
|
||||
version: VersionMode;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type VersionMode = 'stable' | 'rc' | 'next' | number;
|
||||
|
||||
export const INITIAL_DOCS_VERSION = 17;
|
||||
export const VERSION_PATTERN_PLACEHOLDER = '{{version}}';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class VersionManager {
|
||||
versions = signal<Version[]>([
|
||||
...VERSIONS_CONFIG.mainVersions.map((item) =>
|
||||
this.mapToVersion(item as Pick<Version, 'url' | 'version'>),
|
||||
),
|
||||
...this.getHistoricalVersions(),
|
||||
]);
|
||||
|
||||
currentDocsVersion = computed(() => {
|
||||
return this.versions().find(
|
||||
(version) => version.version.toString() === VERSIONS_CONFIG.currentVersion,
|
||||
);
|
||||
});
|
||||
|
||||
private getHistoricalVersions(): Version[] {
|
||||
const historicalVersions: Version[] = [];
|
||||
for (let version = Number(VERSION.major) - 1; version >= INITIAL_DOCS_VERSION; version--) {
|
||||
historicalVersions.push({
|
||||
url: VERSIONS_CONFIG.historicalVersionsLinkPattern.replace(
|
||||
VERSION_PATTERN_PLACEHOLDER,
|
||||
version.toString(),
|
||||
),
|
||||
displayName: this.getVersion(version),
|
||||
version,
|
||||
});
|
||||
}
|
||||
return historicalVersions;
|
||||
}
|
||||
|
||||
private mapToVersion(value: Pick<Version, 'url' | 'version'>): Version {
|
||||
return {
|
||||
...value,
|
||||
displayName: this.getVersion(value.version),
|
||||
};
|
||||
}
|
||||
|
||||
private getVersion(versionMode: VersionMode): string {
|
||||
if (versionMode === 'stable') {
|
||||
return `v${VERSION.major}`;
|
||||
}
|
||||
if (Number.isInteger(versionMode)) {
|
||||
return `v${versionMode}`;
|
||||
}
|
||||
return versionMode.toString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user