mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60dbc2b39d | |||
| 293298a235 | |||
| 0be64c1628 | |||
| f50afb10c3 | |||
| 5328be6660 | |||
| cc1b0f4e8e | |||
| 635227b881 | |||
| 1081c51d1a | |||
| 79308d1668 | |||
| cb0379c8df | |||
| e8e1178e42 | |||
| 54a1fe05df | |||
| b334e29d59 | |||
| 6ca1911967 | |||
| 8af7d3a50b | |||
| 33c08e5aba | |||
| 181121dd9e | |||
| 5e45bb771b | |||
| 417cd71830 | |||
| 73969ae9d4 | |||
| cf61126ef3 | |||
| d2e5f23c38 | |||
| 1b9c807efa | |||
| 829a10937a | |||
| a75d9de874 | |||
| 7c1991048b | |||
| 52d672dd13 | |||
| d8db402833 | |||
| c5cdd8954c | |||
| 4876ed9510 | |||
| d90b6844a1 | |||
| 1a4d783bf8 | |||
| 6c7e1e61c6 | |||
| 17c59eb962 | |||
| 482335657b | |||
| 4feec728c0 | |||
| 76d8c7344e | |||
| a760470b20 | |||
| 5a63a475fb | |||
| c3da79710d | |||
| 879bd80b57 | |||
| b961075f92 | |||
| d01576b104 | |||
| de75fe0358 | |||
| 0e9c075cac | |||
| 922f950e45 | |||
| bec0179afe | |||
| 94bc3afc23 | |||
| dc80046132 | |||
| 6288f1c679 | |||
| 0482b6b4c7 | |||
| 9a56bd50f5 | |||
| 5901b0ca80 | |||
| 369157063c | |||
| 406448a09a | |||
| 9574edaa44 | |||
| e56c86f93c | |||
| d3309d7d9c | |||
| dbe2e3898b | |||
| 8cee78d25b | |||
| 97da5f7804 | |||
| 0472a92bc6 | |||
| e473b709bc | |||
| 70e2431b75 | |||
| 17199b9214 | |||
| 1f8a30dca9 | |||
| da906fdafc | |||
| 2b802587f2 | |||
| b71a32324e | |||
| 77a397b382 | |||
| 700c0520bb | |||
| 1b19d2d430 | |||
| 2d47329444 | |||
| 0dd441d08f | |||
| 10c5cdb49c | |||
| 8cad4e8cbe | |||
| edc1740d7e | |||
| e6ee6d25f9 | |||
| 0f7bbd0fc5 |
@@ -56,11 +56,6 @@ build --enable_runfiles
|
||||
build:release --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=release"
|
||||
build:release --stamp
|
||||
|
||||
# Building AIO against local Angular deps requires stamping
|
||||
# versions in Angular packages due to CLI version checks.
|
||||
build:aio_local_deps --stamp
|
||||
build:aio_local_deps --workspace_status_command="yarn -s --cwd aio local-workspace-status"
|
||||
|
||||
# Snapshots should also be stamped with version control information.
|
||||
build:snapshot-build --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=snapshot"
|
||||
build:snapshot-build --stamp
|
||||
@@ -74,14 +69,6 @@ build:snapshot-build --stamp
|
||||
##########################################################
|
||||
build --flag_alias=aio_build_config=//aio:flag_aio_build_config
|
||||
|
||||
####################################
|
||||
# AIO first party dep substitution #
|
||||
# Turn on with #
|
||||
# --config=aio_local_deps #
|
||||
####################################
|
||||
|
||||
build:aio_local_deps --//aio:flag_aio_local_deps
|
||||
|
||||
###############################
|
||||
# Output #
|
||||
###############################
|
||||
@@ -118,10 +105,6 @@ common:remote --jobs=200
|
||||
|
||||
build:remote --google_default_credentials
|
||||
|
||||
# Limit the number of test jobs for on an AIO local deps build. The example tests running
|
||||
# concurrently pushes the circleci executor RAM usage to its limits.
|
||||
test:aio_local_deps --jobs=24
|
||||
|
||||
# Force remote exeuctions to consider the entire run as linux
|
||||
build:remote --cpu=k8
|
||||
build:remote --host_cpu=k8
|
||||
|
||||
@@ -4,7 +4,7 @@ description: 'Installs the dependencies using Yarn'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
|
||||
- uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4
|
||||
with:
|
||||
path: |
|
||||
./node_modules/
|
||||
|
||||
@@ -21,16 +21,16 @@ jobs:
|
||||
(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
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- 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
|
||||
run: yarn bazel build //adev:build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
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
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
exit: true
|
||||
|
||||
# Indicate that the benchmark command was received.
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{github.event.comment.id}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
id: benchmark
|
||||
name: Running benchmark
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3
|
||||
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
issue-number: ${{github.event.issue.number}}
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
|
||||
@@ -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
|
||||
+73
-26
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
- 'remove-aio-stuff'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
@@ -23,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -58,13 +59,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -76,13 +77,13 @@ jobs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000
|
||||
- name: Run CI tests for framework
|
||||
@@ -93,15 +94,15 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- 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
|
||||
run: yarn bazel test //adev:test
|
||||
|
||||
publish-snapshots:
|
||||
if: github.event_name == 'push'
|
||||
@@ -109,13 +110,13 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
@@ -127,7 +128,7 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -135,9 +136,9 @@ jobs:
|
||||
./packages/zone.js/node_modules
|
||||
./packages/zone.js/test/typings/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -176,7 +177,7 @@ jobs:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
# Checking out the pull request commit is intended here as we need to run the changed code tests.
|
||||
@@ -184,20 +185,66 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@50bb71e51d3372ca2e359adb9069f89beeac4ae7
|
||||
- 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}
|
||||
|
||||
saucelabs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@50bb71e51d3372ca2e359adb9069f89beeac4ae7
|
||||
- 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
|
||||
|
||||
adev-deploy:
|
||||
needs: [adev]
|
||||
if: needs.adev.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'
|
||||
@@ -207,15 +254,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- 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
|
||||
run: yarn bazel build //adev:build
|
||||
- name: Move generated project files into deployment directory
|
||||
run: cp -r dist/bin/adev/build adev/build
|
||||
- name: Setup firebase context
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
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: angular/dev-infra/github-actions/post-approval-changes@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@c83e99a12397014162531ca125c94549db55dd84
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -22,15 +22,15 @@ jobs:
|
||||
version: ['https://next.angular.io/', 'https://angular.io/']
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Run basic e2e and deployment config tests.
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@5774b71c01a55c4c998f858ee37d3b77ae704c31
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
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
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@1421ff04811ecb45812930c5d1f61d912880e75f
|
||||
with:
|
||||
branch-prefix: update-cli-help
|
||||
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
|
||||
|
||||
+96
-47
@@ -1,25 +1,114 @@
|
||||
<a name="17.3.0-rc.0"></a>
|
||||
# 17.3.0-rc.0 (2024-03-06)
|
||||
<a name="18.0.0-next.0"></a>
|
||||
# 18.0.0-next.0 (2024-03-14)
|
||||
## Breaking Changes
|
||||
### core
|
||||
- `OnPush` views at the root of the application need to
|
||||
be marked dirty for their host bindings to refresh. Previously, the host
|
||||
bindings were refreshed for all root views without respecting the
|
||||
`OnPush` change detection strategy.
|
||||
- The `ComponentFixture` `autoDetect` feature will no
|
||||
longer refresh the component's host view when the component is `OnPush`
|
||||
and not marked dirty. This exposes existing issues in components which
|
||||
claim to be `OnPush` but do not correctly call `markForCheck` when they
|
||||
need to be refreshed. If this change causes test failures, the easiest
|
||||
fix is to change the component to `ChangeDetectionStrategy.Default`.
|
||||
- When Angular runs change detection, it will continue to
|
||||
refresh any views attached to `ApplicationRef` that are still marked for
|
||||
check after one round completes. In rare cases, this can result in infinite
|
||||
loops when certain patterns continue to mark views for check using
|
||||
`ChangeDetectorRef.detectChanges`. This will be surfaced as a runtime
|
||||
error with the `NG0103` code.
|
||||
- The `ComponentFixture.autoDetect` feature now executes
|
||||
change detection for the fixture within `ApplicationRef.tick`. This more
|
||||
closely matches the behavior of how a component would refresh in
|
||||
production. The order of component refresh in tests may be slightly
|
||||
affected as a result, especially when dealing with additional components
|
||||
attached to the application, such as dialogs. Tests sensitive to this
|
||||
type of change (such as screenshot tests) may need to be updated.
|
||||
Concretely, this change means that the component will refresh _before_
|
||||
additional views attached to `ApplicationRef` (i.e. dialog components).
|
||||
Prior to this change, the fixture component would refresh _after_ other
|
||||
views attached to the application.
|
||||
- The exact timing of change detection execution when
|
||||
using event or run coalescing with `NgZone` is now the first of either
|
||||
`setTimeout` or `requestAnimationFrame`. Code which relies on this
|
||||
timing (usually by accident) will need to be adjusted. If a callback
|
||||
needs to execute after change detection, we recommend `afterNextRender`
|
||||
instead of something like `setTimeout`.
|
||||
- Newly created and views marked for check and reattached
|
||||
during change detection are now guaranteed to be refreshed in that same
|
||||
change detection cycle. Previously, if they were attached at a location
|
||||
in the view tree that was already checked, they would either throw
|
||||
`ExpressionChangedAfterItHasBeenCheckedError` or not be refreshed until
|
||||
some future round of change detection. In rare circumstances, this
|
||||
correction can cause issues. We identified one instance that relied on
|
||||
the previous behavior by reading a value on initialization which was
|
||||
queued to be updated in a microtask instead of being available in the
|
||||
current change detection round. The component only read this value during
|
||||
initialization and did not read it again after the microtask updated it.
|
||||
### router
|
||||
- Guards can now return `RedirectCommand` for redirects
|
||||
in addition to `UrlTree`. Code which expects only `boolean` or `UrlTree`
|
||||
values in `Route` types will need to be adjusted.
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [ae7dbe42de](https://github.com/angular/angular/commit/ae7dbe42de5779e578fb636938699b8ba85b33f6) | fix | unwrap expressions with type parameters in query read property ([#54647](https://github.com/angular/angular/pull/54647)) |
|
||||
| [9b424d7224](https://github.com/angular/angular/commit/9b424d7224db46edb16c81979c7e231d5e3db5e9) | fix | preserve original reference to non-deferrable dependency ([#54759](https://github.com/angular/angular/pull/54759)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [64f870c12b](https://github.com/angular/angular/commit/64f870c12bae1ad66509f0d65f8d3e051aae6eaa) | fix | `ApplicationRef.tick` should respect OnPush for host bindings ([#53718](https://github.com/angular/angular/pull/53718)) ([#53718](https://github.com/angular/angular/pull/53718)) |
|
||||
| [8cad4e8cbe](https://github.com/angular/angular/commit/8cad4e8cbe2baf20dae7b7ef1f4253a4940cbba0) | fix | `ComponentFixture` `autoDetect` respects `OnPush` flag of host view ([#54824](https://github.com/angular/angular/pull/54824)) |
|
||||
| [ba8e465974](https://github.com/angular/angular/commit/ba8e46597435a827670f10b971b2c58f7033b180) | fix | Change Detection will continue to refresh views while marked for check ([#54734](https://github.com/angular/angular/pull/54734)) |
|
||||
| [24bc0ed4f2](https://github.com/angular/angular/commit/24bc0ed4f2de47bd998338d73cba394fb45dd497) | fix | ComponentFixture autodetect should detect changes within ApplicationRef.tick ([#54733](https://github.com/angular/angular/pull/54733)) |
|
||||
| [10c5cdb49c](https://github.com/angular/angular/commit/10c5cdb49c51c95086febd37f4d88a9b944d7e1c) | fix | ensure change detection runs in a reasonable timeframe with zone coalescing ([#54578](https://github.com/angular/angular/pull/54578)) |
|
||||
| [ad045efd4b](https://github.com/angular/angular/commit/ad045efd4b1565e01c14399998143538ebfbfd99) | fix | Ensure views marked for check are refreshed during change detection ([#54735](https://github.com/angular/angular/pull/54735)) |
|
||||
| [700c0520bb](https://github.com/angular/angular/commit/700c0520bb638952ba41a8d8260cf12afb078c0e) | fix | Update ApplicationRef.tick loop to only throw in dev mode ([#54848](https://github.com/angular/angular/pull/54848)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8735af08b9](https://github.com/angular/angular/commit/8735af08b976b30cf236a83f9e8b64b5ff62e9f3) | feat | Add ability to return `UrlTree` with `NavigationBehaviorOptions` from guards ([#45023](https://github.com/angular/angular/pull/45023)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0"></a>
|
||||
# 17.3.0 (2024-03-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
|
||||
| [f386a04c9d](https://github.com/angular/angular/commit/f386a04c9ddc2951c8105d5cafcce7b4bedea569) | fix | handle two-way bindings to signal-based template variables in instruction generation ([#54714](https://github.com/angular/angular/pull/54714)) |
|
||||
| [1f129f114e](https://github.com/angular/angular/commit/1f129f114edb21137d74471dd6b652848d2ffb68) | fix | not catching for loop empty tracking expressions ([#54772](https://github.com/angular/angular/pull/54772)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
|
||||
| [492e03f699](https://github.com/angular/angular/commit/492e03f699ca95df4cf854885253f63a5a1fb5c1) | fix | flag two-way bindings to non-signal values in templates ([#54714](https://github.com/angular/angular/pull/54714)) |
|
||||
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
|
||||
| [fb540e169a](https://github.com/angular/angular/commit/fb540e169a78a61f38d611f538eea8fdb0971f1d) | feat | add migration for invalid two-way bindings ([#54630](https://github.com/angular/angular/pull/54630)) |
|
||||
| [c687b8f453](https://github.com/angular/angular/commit/c687b8f4531252cd1c3dfbb9a7bd42bdbe666a36) | feat | expose new `output()` API ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [c809069f21](https://github.com/angular/angular/commit/c809069f213244afd0e2d803a6a43510b218e6f5) | feat | introduce `outputFromObservable()` interop function ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [aff65fd1f4](https://github.com/angular/angular/commit/aff65fd1f4a61ed76a6f9b623852f197eb3500e4) | feat | introduce `outputToObservable` interop helper ([#54650](https://github.com/angular/angular/pull/54650)) |
|
||||
| [33a6fab094](https://github.com/angular/angular/commit/33a6fab094205cc74aceb916e45c8afa22293cf4) | fix | apply TestBed provider overrides to `@defer` dependencies ([#54667](https://github.com/angular/angular/pull/54667)) |
|
||||
| [78e69117f0](https://github.com/angular/angular/commit/78e69117f07e8ae11aab3abe0343df815b3649be) | fix | generic inference for signal inputs may break with `--strictFunctionTypes` ([#54652](https://github.com/angular/angular/pull/54652)) |
|
||||
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [39a50f9a8d](https://github.com/angular/angular/commit/39a50f9a8df5afc4968d18924f3d9d7d6b649d3a) | fix | ensure all initializer functions run in an injection context ([#54761](https://github.com/angular/angular/pull/54761)) |
|
||||
| [243ccce624](https://github.com/angular/angular/commit/243ccce62475ae03a2e727d2b3cb2d51a595c4a7) | fix | exclude class attribute intended for projection matching from directive matching ([#54800](https://github.com/angular/angular/pull/54800)) |
|
||||
| [2909e9817d](https://github.com/angular/angular/commit/2909e9817daf69b6478d7d09229491a9a48cff16) | fix | prevent infinite loops in clobbered elements check ([#54425](https://github.com/angular/angular/pull/54425)) |
|
||||
| [7243c704cf](https://github.com/angular/angular/commit/7243c704cf8a4986fae419793027458e142658f0) | fix | return a readonly signal on `asReadonly`. ([#54706](https://github.com/angular/angular/pull/54706)) |
|
||||
| [ffad7b8ea9](https://github.com/angular/angular/commit/ffad7b8ea9d1286ddb3ee7a2dac7dd33fd76b3aa) | fix | untrack various core operations ([#54614](https://github.com/angular/angular/pull/54614)) |
|
||||
| [bb35414a38](https://github.com/angular/angular/commit/bb35414a38f8a8447d03c66dc5bf60b84409f9e3) | perf | speed up retrieval of `DestroyRef` in `EventEmitter` ([#54748](https://github.com/angular/angular/pull/54748)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8d37ed035c](https://github.com/angular/angular/commit/8d37ed035c9e9796ba9a7b1f055404ea220dbb3b) | fix | exclude caching for authenticated HTTP requests ([#54746](https://github.com/angular/angular/pull/54746)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c1c7384e02](https://github.com/angular/angular/commit/c1c7384e02becc623c6a42985f7178ca98137264) | feat | Add reusable types for router guards ([#54580](https://github.com/angular/angular/pull/54580)) |
|
||||
| [7225485311](https://github.com/angular/angular/commit/722548531108e247660ebe6966b99b57d510b615) | fix | Navigations triggered by cancellation events should cancel previous navigation ([#54710](https://github.com/angular/angular/pull/54710)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
@@ -63,46 +152,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0-next.1"></a>
|
||||
# 17.3.0-next.1 (2024-02-27)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [707bfc9b32](https://github.com/angular/angular/commit/707bfc9b326f321f302dccfdfebef7380c914eb5) | perf | `AsyncPipe` should not call `markForCheck` on subscription ([#54554](https://github.com/angular/angular/pull/54554)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [f578889ca2](https://github.com/angular/angular/commit/f578889ca2dbb22f2e050d4c4af6aa2d597bf70d) | fix | catch function instance properties in interpolated signal diagnostic ([#54325](https://github.com/angular/angular/pull/54325)) |
|
||||
| [f04ecc0cda](https://github.com/angular/angular/commit/f04ecc0cdaeb3a292a748b1ccc94ce70a573fc79) | fix | identify aliased initializer functions ([#54480](https://github.com/angular/angular/pull/54480)) |
|
||||
| [f5c566c079](https://github.com/angular/angular/commit/f5c566c0793eacf9ca146c8a6b8da15b0e8f4c4d) | fix | identify aliased initializer functions ([#54609](https://github.com/angular/angular/pull/54609)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
|
||||
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
|
||||
| [dcb9deb363](https://github.com/angular/angular/commit/dcb9deb3631ff4e839f738f2fb97ca6b894256ef) | fix | collect providers from NgModules while rendering `@defer` block ([#52881](https://github.com/angular/angular/pull/52881)) |
|
||||
| [ff40c9f762](https://github.com/angular/angular/commit/ff40c9f762b2c14870ac2859201ece9660087e79) | fix | fix typo in injectors.svg file ([#54596](https://github.com/angular/angular/pull/54596)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [71e0c7df69](https://github.com/angular/angular/commit/71e0c7df695f274a54d5ffb9dfc0587d06d3a953) | fix | resolve infinite loop for a single line element with a long tag name and angle bracket on a new line ([#54588](https://github.com/angular/angular/pull/54588)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.3.0-next.0"></a>
|
||||
# 17.3.0-next.0 (2024-02-21)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.2"></a>
|
||||
# 17.2.2 (2024-02-21)
|
||||
### common
|
||||
|
||||
@@ -2,7 +2,6 @@ workspace(
|
||||
name = "angular",
|
||||
managed_directories = {
|
||||
"@npm": ["node_modules"],
|
||||
"@aio_npm": ["aio/node_modules"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -109,30 +108,6 @@ yarn_install(
|
||||
yarn_lock = "//:yarn.lock",
|
||||
)
|
||||
|
||||
yarn_install(
|
||||
name = "aio_npm",
|
||||
# Note that we add the postinstall scripts here so that the dependencies are re-installed
|
||||
# when the postinstall patches are modified.
|
||||
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",
|
||||
],
|
||||
# Currently disabled due to:
|
||||
# 1. Missing Windows support currently.
|
||||
# 2. Incompatibilites with the `ts_library` rule.
|
||||
exports_directories_only = False,
|
||||
manual_build_file_contents = npm_package_archives(),
|
||||
package_json = "//aio:package.json",
|
||||
# We prefer to symlink the `node_modules` to only maintain a single install.
|
||||
# See https://github.com/angular/dev-infra/pull/446#issuecomment-1059820287 for details.
|
||||
symlink_node_modules = True,
|
||||
yarn = YARN_LABEL,
|
||||
yarn_lock = "//aio:yarn.lock",
|
||||
)
|
||||
|
||||
yarn_install(
|
||||
name = "aio_example_deps",
|
||||
# Rename the default js_library target from "node_modules" as this obscures the
|
||||
@@ -206,10 +181,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "974f5e14c54d300ca63a67910ebfcea6d838d8811fefc61184f2872107152705",
|
||||
strip_prefix = "rules_sass-888dd9c02709e228b60a8b3e51e2b16b2ca5279d",
|
||||
sha256 = "5633816e996a79c77f44306f6228b9ef18fc2ecb412d63b4e314a132225facba",
|
||||
strip_prefix = "rules_sass-79bd239ce77c101ea1aed575678020bd2999f17d",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/888dd9c02709e228b60a8b3e51e2b16b2ca5279d.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/79bd239ce77c101ea1aed575678020bd2999f17d.zip",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
load("//:packages.bzl", "link_packages")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@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")
|
||||
@@ -73,7 +73,7 @@ APPLICATION_DEPS = link_packages([
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//xterm",
|
||||
"@npm//@xterm/xterm",
|
||||
"@npm//xterm-addon-fit",
|
||||
"@npm//angular-split",
|
||||
])
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {inject} from '@angular/core';
|
||||
import {Subject} from 'rxjs';
|
||||
import {Terminal} from 'xterm';
|
||||
import {Terminal} from '@xterm/xterm';
|
||||
|
||||
import {WINDOW} from '@angular/docs';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Terminal} from 'xterm';
|
||||
import {Terminal} from '@xterm/xterm';
|
||||
import {FitAddon} from 'xterm-addon-fit';
|
||||
import {InteractiveTerminal} from './interactive-terminal';
|
||||
|
||||
|
||||
@@ -766,8 +766,8 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
path: 'guide/security', // Have not refactored due to build issues
|
||||
contentPath: 'best-practices/security',
|
||||
path: 'best-practices/security',
|
||||
contentPath: 'guide/security', // Have not refactored due to build issues
|
||||
},
|
||||
{
|
||||
label: 'Accessibility',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
+795
-816
File diff suppressed because it is too large
Load Diff
@@ -9,21 +9,21 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.2.0",
|
||||
"@angular/common": "^17.2.0",
|
||||
"@angular/compiler": "^17.2.0",
|
||||
"@angular/core": "^17.2.0",
|
||||
"@angular/forms": "^17.2.0",
|
||||
"@angular/platform-browser": "^17.2.0",
|
||||
"@angular/router": "^17.2.0",
|
||||
"@angular/animations": "^17.3.0-rc",
|
||||
"@angular/common": "^17.3.0-rc",
|
||||
"@angular/compiler": "^17.3.0-rc",
|
||||
"@angular/core": "^17.3.0-rc",
|
||||
"@angular/forms": "^17.3.0-rc",
|
||||
"@angular/platform-browser": "^17.3.0-rc",
|
||||
"@angular/router": "^17.3.0-rc",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.2.0",
|
||||
"@angular/cli": "^17.2.0",
|
||||
"@angular/compiler-cli": "^17.2.0",
|
||||
"@angular-devkit/build-angular": "^17.3.0-rc",
|
||||
"@angular/cli": "^17.3.0-rc",
|
||||
"@angular/compiler-cli": "^17.3.0-rc",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^16.11.35",
|
||||
"copyfiles": "^2.4.1",
|
||||
|
||||
+345
-366
File diff suppressed because it is too large
Load Diff
@@ -9,19 +9,19 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^17.2.0",
|
||||
"@angular/compiler": "^17.2.0",
|
||||
"@angular/core": "^17.2.0",
|
||||
"@angular/forms": "^17.2.0",
|
||||
"@angular/platform-browser": "^17.2.0",
|
||||
"@angular/common": "^17.3.0-rc",
|
||||
"@angular/compiler": "^17.3.0-rc",
|
||||
"@angular/core": "^17.3.0-rc",
|
||||
"@angular/forms": "^17.3.0-rc",
|
||||
"@angular/platform-browser": "^17.3.0-rc",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.2.0",
|
||||
"@angular/cli": "^17.2.0",
|
||||
"@angular/compiler-cli": "^17.2.0",
|
||||
"@angular-devkit/build-angular": "^17.3.0-rc",
|
||||
"@angular/cli": "^17.3.0-rc",
|
||||
"@angular/compiler-cli": "^17.3.0-rc",
|
||||
"typescript": "~5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
+352
-373
File diff suppressed because it is too large
Load Diff
@@ -9,20 +9,20 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^17.2.0",
|
||||
"@angular/compiler": "^17.2.0",
|
||||
"@angular/core": "^17.2.0",
|
||||
"@angular/forms": "^17.2.0",
|
||||
"@angular/platform-browser": "^17.2.0",
|
||||
"@angular/router": "^17.2.0",
|
||||
"@angular/common": "^17.3.0-rc",
|
||||
"@angular/compiler": "^17.3.0-rc",
|
||||
"@angular/core": "^17.3.0-rc",
|
||||
"@angular/forms": "^17.3.0-rc",
|
||||
"@angular/platform-browser": "^17.3.0-rc",
|
||||
"@angular/router": "^17.3.0-rc",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.2.0",
|
||||
"@angular/cli": "^17.2.0",
|
||||
"@angular/compiler-cli": "^17.2.0",
|
||||
"@angular-devkit/build-angular": "^17.3.0-rc",
|
||||
"@angular/cli": "^17.3.0-rc",
|
||||
"@angular/compiler-cli": "^17.3.0-rc",
|
||||
"typescript": "~5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1118
-1139
File diff suppressed because it is too large
Load Diff
@@ -9,22 +9,22 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.2.0",
|
||||
"@angular/cdk": "^17.2.0",
|
||||
"@angular/common": "^17.2.0",
|
||||
"@angular/compiler": "^17.2.0",
|
||||
"@angular/core": "^17.2.0",
|
||||
"@angular/forms": "^17.2.0",
|
||||
"@angular/material": "^17.2.0",
|
||||
"@angular/platform-browser": "^17.2.0",
|
||||
"@angular/animations": "^17.3.0-rc",
|
||||
"@angular/cdk": "^17.3.0-rc",
|
||||
"@angular/common": "^17.3.0-rc",
|
||||
"@angular/compiler": "^17.3.0-rc",
|
||||
"@angular/core": "^17.3.0-rc",
|
||||
"@angular/forms": "^17.3.0-rc",
|
||||
"@angular/material": "^17.3.0-rc",
|
||||
"@angular/platform-browser": "^17.3.0-rc",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.2.0",
|
||||
"@angular/cli": "^17.2.0",
|
||||
"@angular/compiler-cli": "^17.2.0",
|
||||
"@angular-devkit/build-angular": "^17.3.0-rc",
|
||||
"@angular/cli": "^17.3.0-rc",
|
||||
"@angular/compiler-cli": "^17.3.0-rc",
|
||||
"typescript": "~5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"tests/e2e/tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"@angular-eslint",
|
||||
"eslint-plugin-jsdoc",
|
||||
"eslint-plugin-import",
|
||||
"eslint-plugin-jsdoc",
|
||||
"eslint-plugin-prefer-arrow"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/component-class-suffix": "error",
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"prefix": "aio",
|
||||
"style": "kebab-case",
|
||||
"type": "element"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/contextual-lifecycle": "error",
|
||||
"@angular-eslint/directive-class-suffix": "error",
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"prefix": "aio",
|
||||
"style": "camelCase",
|
||||
"type": "attribute"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/no-conflicting-lifecycle": "error",
|
||||
"@angular-eslint/no-host-metadata-property": "off",
|
||||
"@angular-eslint/no-input-rename": "error",
|
||||
"@angular-eslint/no-inputs-metadata-property": "error",
|
||||
"@angular-eslint/no-output-native": "error",
|
||||
"@angular-eslint/no-output-on-prefix": "error",
|
||||
"@angular-eslint/no-output-rename": "error",
|
||||
"@angular-eslint/no-outputs-metadata-property": "error",
|
||||
"@angular-eslint/use-lifecycle-interface": "error",
|
||||
"@angular-eslint/use-pipe-transform-interface": "error",
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
"@typescript-eslint/array-type": "off",
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/dot-notation": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-ordering": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{
|
||||
"ignoreParameters": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/no-shadow": [
|
||||
"error"
|
||||
],
|
||||
"@typescript-eslint/no-unused-expressions": "error",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/prefer-for-of": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"@typescript-eslint/triple-slash-reference": [
|
||||
"error",
|
||||
{
|
||||
"lib": "always",
|
||||
"path": "always",
|
||||
"types": "prefer-import"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unified-signatures": "error",
|
||||
"arrow-body-style": "error",
|
||||
"arrow-parens": "off",
|
||||
"comma-dangle": "off",
|
||||
"complexity": "off",
|
||||
"constructor-super": "error",
|
||||
"curly": "error",
|
||||
"eol-last": "error",
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"guard-for-in": "error",
|
||||
"id-denylist": [
|
||||
"error",
|
||||
"any",
|
||||
"Number",
|
||||
"number",
|
||||
"String",
|
||||
"string",
|
||||
"Boolean",
|
||||
"boolean",
|
||||
"Undefined",
|
||||
"undefined"
|
||||
],
|
||||
"id-match": "error",
|
||||
"import/no-deprecated": "warn",
|
||||
"indent": "off",
|
||||
"jsdoc/check-alignment": "error",
|
||||
"jsdoc/no-types": "error",
|
||||
"max-classes-per-file": "off",
|
||||
"max-len": [
|
||||
"error",
|
||||
120
|
||||
],
|
||||
"new-parens": "error",
|
||||
"no-bitwise": "error",
|
||||
"no-caller": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"log",
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-debugger": "error",
|
||||
"no-empty": "off",
|
||||
"no-empty-function": "off",
|
||||
"no-eval": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-invalid-this": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"no-new-wrappers": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"message": "Please import directly from 'rxjs' instead",
|
||||
"name": "rxjs/Rx"
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"message": "Don't keep jasmine focus methods.",
|
||||
"selector": "CallExpression[callee.name=/^(fdescribe|fit)$/]"
|
||||
}
|
||||
],
|
||||
"no-shadow": "off",
|
||||
"no-tabs": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-labels": "error",
|
||||
"no-use-before-define": "off",
|
||||
"no-var": "error",
|
||||
"object-shorthand": "error",
|
||||
"one-var": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"prefer-arrow/prefer-arrow-functions": "off",
|
||||
"prefer-const": "error",
|
||||
"quote-props": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"quotes": "off",
|
||||
"radix": "error",
|
||||
"sort-keys": "off",
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always",
|
||||
"named": "never"
|
||||
}
|
||||
],
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/template/alt-text": "error",
|
||||
"@angular-eslint/template/elements-content": "error",
|
||||
"@angular-eslint/template/label-has-associated-control": "error",
|
||||
"@angular-eslint/template/table-scope": "error",
|
||||
"@angular-eslint/template/valid-aria": "error",
|
||||
"@angular-eslint/template/click-events-have-key-events": "error",
|
||||
"@angular-eslint/template/eqeqeq": "off",
|
||||
"@angular-eslint/template/mouse-events-have-key-events": "error",
|
||||
"@angular-eslint/template/no-autofocus": "error",
|
||||
"@angular-eslint/template/no-distracting-elements": "error",
|
||||
"@angular-eslint/template/no-positive-tabindex": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
engine-strict = true
|
||||
+43
-43
@@ -1,11 +1,11 @@
|
||||
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "npm_package_bin")
|
||||
load("//tools:defaults.bzl", "nodejs_binary")
|
||||
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
|
||||
load(":local_packages_util.bzl", "link_local_packages", "substitute_local_package_deps")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
|
||||
load("//aio/scripts:local_server_test.bzl", "local_server_test")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load(":aio_targets.bzl", "aio_test")
|
||||
load("@bazel_skylib//lib:collections.bzl", "collections")
|
||||
|
||||
@@ -91,7 +91,7 @@ npm_package_bin(
|
||||
tags = [
|
||||
"requires-network",
|
||||
],
|
||||
tool = "@aio_npm//dgeni/bin:dgeni",
|
||||
tool = "@npm//dgeni/bin:dgeni",
|
||||
toolchains = [
|
||||
"@npm//@angular/build-tooling/bazel/git-toolchain:current_git_toolchain",
|
||||
],
|
||||
@@ -148,29 +148,29 @@ APPLICATION_DEPS = [
|
||||
":stackblitz",
|
||||
":example-zips",
|
||||
"//aio/src/generated:ngsw-config",
|
||||
"@aio_npm//@angular-devkit/build-angular",
|
||||
"@aio_npm//@angular-eslint/builder",
|
||||
"@aio_npm//@angular/animations",
|
||||
"@aio_npm//@angular/cdk",
|
||||
"@aio_npm//@angular/cli",
|
||||
"@aio_npm//@angular/common",
|
||||
"@aio_npm//@angular/compiler",
|
||||
"@aio_npm//@angular/compiler-cli",
|
||||
"@aio_npm//@angular/core",
|
||||
"@aio_npm//@angular/elements",
|
||||
"@aio_npm//@angular/forms",
|
||||
"@aio_npm//@angular/material",
|
||||
"@aio_npm//@angular/platform-browser",
|
||||
"@aio_npm//@angular/platform-browser-dynamic",
|
||||
"@aio_npm//@angular/router",
|
||||
"@aio_npm//@angular/service-worker",
|
||||
"@aio_npm//@types/lunr",
|
||||
"@aio_npm//@types/trusted-types",
|
||||
"@aio_npm//lunr",
|
||||
"@aio_npm//rxjs",
|
||||
"@aio_npm//safevalues",
|
||||
"@aio_npm//tslib",
|
||||
"@aio_npm//zone.js",
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular-eslint/builder",
|
||||
"@npm//@angular/animations",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/cli",
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/compiler",
|
||||
"@npm//@angular/compiler-cli",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/elements",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@angular/platform-browser",
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@npm//@angular/router",
|
||||
"@npm//@angular/service-worker",
|
||||
"@npm//@types/lunr",
|
||||
"@npm//@types/trusted-types",
|
||||
"@npm//lunr",
|
||||
"@npm//rxjs",
|
||||
"@npm//safevalues",
|
||||
"@npm//tslib",
|
||||
"@npm//zone.js",
|
||||
]
|
||||
|
||||
# All sources, specs, and config files required to test the docs app
|
||||
@@ -183,16 +183,16 @@ TEST_FILES = APPLICATION_FILES + [
|
||||
|
||||
# Dependencies required to test the docs app
|
||||
TEST_DEPS = APPLICATION_DEPS + [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@aio_npm//@types/jasmine",
|
||||
"@aio_npm//@types/node",
|
||||
"@aio_npm//assert",
|
||||
"@aio_npm//jasmine",
|
||||
"@aio_npm//jasmine-core",
|
||||
"@aio_npm//karma-chrome-launcher",
|
||||
"@aio_npm//karma-coverage",
|
||||
"@aio_npm//karma-jasmine",
|
||||
"@aio_npm//karma-jasmine-html-reporter",
|
||||
"@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",
|
||||
]
|
||||
|
||||
@@ -202,12 +202,12 @@ E2E_FILES = APPLICATION_FILES + glob(["tests/e2e/**"])
|
||||
|
||||
# Dependencies required to run the e2e tests
|
||||
E2E_DEPS = APPLICATION_DEPS + [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@aio_npm//@types/jasmine",
|
||||
"@aio_npm//@types/node",
|
||||
"@aio_npm//jasmine-spec-reporter",
|
||||
"@aio_npm//protractor",
|
||||
"@aio_npm//ts-node",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//jasmine-spec-reporter",
|
||||
"@npm//protractor",
|
||||
"@npm//ts-node",
|
||||
"//aio/tools:windows-chromium-path",
|
||||
]
|
||||
|
||||
@@ -322,7 +322,7 @@ architect_test(
|
||||
# (presumably after AIO migration to Bazel)
|
||||
flaky = True,
|
||||
toolchains = [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
# Angular documentation project (https://angular.io)
|
||||
|
||||
Everything in this folder is part of the documentation project. This includes:
|
||||
|
||||
* the web site for displaying the documentation.
|
||||
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
|
||||
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
|
||||
|
||||
<a name="developer-tasks"></a>
|
||||
## Developer tasks
|
||||
|
||||
We use [Yarn](https://yarnpkg.com) to manage the dependencies and development tasks. Behind the scenes, [Bazel](https://bazel.build/) is used to build targets and run tests.
|
||||
You should run all these tasks from the `angular/aio` folder.
|
||||
Here are the most important tasks you might need to use:
|
||||
|
||||
* `yarn` - install all the dependencies.
|
||||
* `yarn build` - create a development build of the application.
|
||||
* `yarn build-prod` - create a production build of the application.
|
||||
* `yarn build-local` - same as `build`, but uses locally built Angular packages from source code rather than from npm.
|
||||
* `yarn start` - run a development web server that watches, rebuilds, and reloads the page when there are changes to the source code or docs.
|
||||
* `yarn start-local` - same as `start`, but uses local Angular packages.
|
||||
* `yarn test` - run all the unit tests for the doc-viewer once.
|
||||
* `yarn test-local` - similar to `test`, but tests against locally built Angular packages.
|
||||
* `yarn test-and-watch` - watch all the source files for the doc-viewer, and run all the unit tests when any change.
|
||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||
* `yarn e2e-local` - similar to `e2e`, but tests against locally built Angular packages.
|
||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||
|
||||
* `yarn docs-watch` - similar to `start`, but only watches for docs changes and uses a faster, low-fidelity build ideal for quick editing.
|
||||
* `yarn docs-test` - run the unit tests for the doc generation code.
|
||||
* `yarn docs-lint` - check that the doc gen code follows our style rules.
|
||||
|
||||
* `yarn create-example` - create a new example directory containing initial source files.
|
||||
* `yarn example-playground <exampleName>` - set up a playground to manually test an example combined with its boilerplate files
|
||||
- `--local` - link locally build Angular packages as deps
|
||||
- `--watch` - update the playground when sources change
|
||||
|
||||
* `yarn example-e2e` - run all e2e tests for examples. Available options:
|
||||
- `--local`: run e2e tests against locally built Angular packages.
|
||||
- `--filter=foo`: limit e2e tests to those containing the word "foo".
|
||||
- `--exclude=bar`: exclude e2e tests containing the word "bar".
|
||||
|
||||
> **Note for Windows users**
|
||||
>
|
||||
> The underlying Bazel build requires creating [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) (see [here](./tools/examples/README.md#symlinked-node_modules) for details). On Windows, this requires to either have [Developer Mode enabled](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10) (supported on Windows 10 or newer) or run the setup commands as administrator.
|
||||
|
||||
## Using ServiceWorker locally
|
||||
|
||||
Running `yarn start` (even when explicitly targeting production mode) does not set up the
|
||||
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
|
||||
serve the files with `yarn http-server ../dist/bin/aio/build -p 4200`.
|
||||
|
||||
|
||||
## Guide to authoring
|
||||
|
||||
There are two types of content in the documentation:
|
||||
|
||||
* **API docs**: descriptions of all that make up the Angular platform, such as the modules, classes, interfaces or decorators.
|
||||
API docs are generated directly from the source code.
|
||||
The source code is contained in TypeScript files, located in the `angular/packages` folder.
|
||||
Each API item may have a preceding comment, which contains JSDoc style tags and content.
|
||||
The content is written in markdown. To generate docs, each package's files need to be explicitly included in the [packages/BUILD.bazel](../packages/BUILD.bazel) file under the `files_for_docgen` target.
|
||||
|
||||
* **Other content**: guides, tutorials, and other marketing material.
|
||||
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
|
||||
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
|
||||
|
||||
* **Code examples**: code examples need to be testable to ensure their accuracy.
|
||||
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
|
||||
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
|
||||
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||
|
||||
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
||||
|
||||
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
|
||||
writing guide pages, explains how to use the documentation classes and components, and how to markup sample source code to produce code snippets.
|
||||
|
||||
### Generating the complete docs
|
||||
|
||||
Running the `yarn build` or `yarn start` tasks will automatically generate the docs. This will process all the source files (API and other),
|
||||
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
|
||||
|
||||
### Partial doc generation for editors
|
||||
|
||||
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
|
||||
|
||||
You can make small changes in a smart editor that displays formatted markdown:
|
||||
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
|
||||
|
||||
You also want to see those changes displayed properly in the doc viewer
|
||||
with a quick, edit/view cycle time.
|
||||
|
||||
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
|
||||
re-processes the files necessary to generate the docs that are related to the file that has changed.
|
||||
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
|
||||
fidelity content. For example, links to other docs and code examples may not render correctly. This is
|
||||
most particularly noticed in links to other docs and in the embedded examples, which may not always render
|
||||
correctly.
|
||||
|
||||
The general setup is as follows:
|
||||
|
||||
* Open a terminal, ensure the dependencies are installed, then start the doc-viewer:
|
||||
|
||||
```bash
|
||||
yarn docs-watch
|
||||
```
|
||||
|
||||
* A browser will open automatically at https://localhost:4200/. Navigate to the document on which you want to work.
|
||||
|
||||
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
|
||||
be regenerated, the app will rebuild and the page will reload.
|
||||
|
||||
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
|
||||
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||
@@ -1,42 +0,0 @@
|
||||
# Update the angular.io app
|
||||
|
||||
The dependencies of the angular.io app (including Angular, Angular Material and Angular CLI) are automatically updated using [Renovate](https://renovatebot.com/).
|
||||
|
||||
However, it is useful to periodically also manually update the app to more closely match (in file layout, configs, etc.) what a new Angular CLI app would look like.
|
||||
This is typically only needed once for each new major Angular version.
|
||||
|
||||
Since angular.io is an Angular CLI app, we can take advantage of `ng update` to apply migrations.
|
||||
|
||||
Follow these steps to align the angular.io app with new CLI apps.
|
||||
|
||||
> **Note:**
|
||||
> The following steps assume that the related Angular dependencies have already been updated in [aio/package.json](./package.json) (for example, automatically by Renovate).
|
||||
|
||||
> **Note:**
|
||||
> All commands shown below are expected to be executed from inside the [aio/](./) directory (unless specified otherwise).
|
||||
|
||||
- Determine (for example, by examining git history) what is the last versions for which this process was performed.
|
||||
These will be referred to as `<FROM_VERSION_*>`.
|
||||
If you can't determine these, use arbitrary versions, such as the previous major version.
|
||||
|
||||
- Run the following commands to automatically apply any available migrations to the project:
|
||||
```sh
|
||||
# Ensure dependencies are installed.
|
||||
yarn install
|
||||
|
||||
# Migrate project to new versions.
|
||||
yarn ng update @angular/cli --allow-dirty --migrate-only --from=<FROM_VERSION_CLI>
|
||||
yarn ng update @angular/core --allow-dirty --migrate-only --from=<FROM_VERSION_ANGULAR>
|
||||
yarn ng update @angular/material --allow-dirty --migrate-only --from=<FROM_VERSION_MATERIAL>
|
||||
```
|
||||
|
||||
> **Note:**
|
||||
> Depending on the number of changes generated from each `ng update` command, it might make sense to create a separate commit for each update.
|
||||
|
||||
- Inspect [package.json](./package.json) to determine what is the current version of Angular CLI (i.e. `@angular/cli`) used in the app.
|
||||
This will be referred to as `<TO_VERSION_CLI>`.
|
||||
|
||||
- Use the [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repository to discover more changes (which are not automatically applied via `ng update` migrations) between Angular CLI apps of different versions.
|
||||
Visit https://github.com/cexbrayat/angular-cli-diff/compare/<FROM_VERSION_CLI>...<TO_VERSION_CLI>, inspect the changes between the two versions and apply the ones that make sense to the angular.io source code.
|
||||
|
||||
- Commit all changes and [submit a pull request](../CONTRIBUTING.md#submit-pr).
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect_test")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect_test")
|
||||
|
||||
def aio_test(name, data, args, **kwargs):
|
||||
architect_test(
|
||||
@@ -10,7 +10,7 @@ def aio_test(name, data, args, **kwargs):
|
||||
"CHROME_BIN": "../$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "yarn",
|
||||
"warnings": {
|
||||
"typescriptMismatch": false
|
||||
},
|
||||
"analytics": false,
|
||||
"cache": {
|
||||
// Disable build caching as the cache folder will just be dropped
|
||||
// when run under Bazel sandboxed execution.
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"site": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
},
|
||||
"@schematics/angular:component": {
|
||||
"inlineStyle": true,
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "aio",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"ngswConfigPath": "src/generated/ngsw-config.json",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"optimization": {
|
||||
"fonts": {
|
||||
"inline": false
|
||||
},
|
||||
"scripts": true,
|
||||
"styles": {
|
||||
"inlineCritical": false,
|
||||
"minify": true
|
||||
}
|
||||
},
|
||||
"outputHashing": "all",
|
||||
"sourceMap": true,
|
||||
"namedChunks": true,
|
||||
"assets": [
|
||||
// Architect on Windows has difficulty service assets files within a
|
||||
// Bazel runfiles symlink forest. Using "followSymlinks" seems to
|
||||
// fix this. Note that the assets below don't need this workaround as
|
||||
// symlinked tree artifacts pointing to the output tree don't appear
|
||||
// to have the same issue.
|
||||
{
|
||||
"followSymlinks": true,
|
||||
"input": "src/assets",
|
||||
"glob": "**/*",
|
||||
"output": "/assets/"
|
||||
},
|
||||
{
|
||||
"followSymlinks": true,
|
||||
"input": "src/",
|
||||
"glob": "**/pwa-manifest.json",
|
||||
"output": "/"
|
||||
},
|
||||
{
|
||||
"followSymlinks": true,
|
||||
"input": "src/",
|
||||
"glob": "**/google385281288605d160.html",
|
||||
"output": "/"
|
||||
},
|
||||
{
|
||||
"input": "stackblitz/generated",
|
||||
"output": "generated",
|
||||
"glob": "**"
|
||||
},
|
||||
{
|
||||
"input": "example-zips/generated",
|
||||
"output": "generated",
|
||||
"glob": "**"
|
||||
},
|
||||
{
|
||||
"input": "dgeni/generated",
|
||||
"output": "generated",
|
||||
"glob": "**"
|
||||
},
|
||||
{
|
||||
"input": "dgeni-fast/generated",
|
||||
"output": "generated",
|
||||
"glob": "**"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles/main.scss",
|
||||
{
|
||||
"inject": false,
|
||||
"input": "src/styles/custom-themes/dark-theme.scss",
|
||||
"bundleName": "dark-theme"
|
||||
},
|
||||
{
|
||||
"inject": false,
|
||||
"input": "src/styles/custom-themes/light-theme.scss",
|
||||
"bundleName": "light-theme"
|
||||
}
|
||||
],
|
||||
"scripts": [],
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "850kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"next": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.next.ts"
|
||||
}
|
||||
],
|
||||
"serviceWorker": true
|
||||
},
|
||||
"rc": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.rc.ts"
|
||||
}
|
||||
],
|
||||
"serviceWorker": true
|
||||
},
|
||||
"stable": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.stable.ts"
|
||||
}
|
||||
],
|
||||
"serviceWorker": true
|
||||
},
|
||||
"archive": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.archive.ts"
|
||||
}
|
||||
],
|
||||
"serviceWorker": true
|
||||
},
|
||||
"ci": {
|
||||
"progress": false
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"outputHashing": "none",
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "stable"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"headers": {
|
||||
// Keep in sync with the `firebase.json` file.
|
||||
// Note: Unlike in `firebase.json` we omit the `report-uri` for development.
|
||||
"Content-Security-Policy": "require-trusted-types-for 'script'; trusted-types angular angular#bundler angular#unsafe-bypass aio#analytics google#safe goog#html"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"next": {
|
||||
"browserTarget": "site:build:next"
|
||||
},
|
||||
"rc": {
|
||||
"browserTarget": "site:build:rc"
|
||||
},
|
||||
"stable": {
|
||||
"browserTarget": "site:build:stable"
|
||||
},
|
||||
"archive": {
|
||||
"browserTarget": "site:build:archive"
|
||||
},
|
||||
"ci": {
|
||||
"browserTarget": "site:build:ci"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "site:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "site:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": ["src/assets", "src/pwa-manifest.json", "src/google385281288605d160.html"],
|
||||
"styles": [
|
||||
"src/styles/main.scss",
|
||||
{
|
||||
"inject": false,
|
||||
"input": "src/styles/custom-themes/dark-theme.scss",
|
||||
"bundleName": "dark-theme"
|
||||
},
|
||||
{
|
||||
"inject": false,
|
||||
"input": "src/styles/custom-themes/light-theme.scss",
|
||||
"bundleName": "light-theme"
|
||||
}
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/!(generated)/**/*.ts",
|
||||
"src/!(generated)/**/*.html",
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "tests/e2e/protractor.conf.js",
|
||||
"devServerTarget": "site:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"devServerTarget": "site:serve:ci"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/main",
|
||||
"sha": "6452ed70f1464c97e1ed4551dcf9f1346c5452f1"
|
||||
"sha": "76a340c895b3fb124b07aa8a12f78202f5b8ea98"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"command": "ng serve [project]",
|
||||
"shortDescription": "Builds and serves your application, rebuilding on file changes.",
|
||||
"aliases": [
|
||||
"dev",
|
||||
"s"
|
||||
],
|
||||
"deprecated": false,
|
||||
|
||||
@@ -206,7 +206,7 @@ def docs_example(name, test = True, test_tags = [], test_exec_properties = {}, f
|
||||
data = [
|
||||
":%s" % name,
|
||||
YARN_LABEL,
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"//aio/tools/examples:run-example-e2e",
|
||||
"//aio/tools:windows-chromium-path",
|
||||
# We install the whole node modules for runtime deps of e2e tests
|
||||
@@ -229,7 +229,7 @@ def docs_example(name, test = True, test_tags = [], test_exec_properties = {}, f
|
||||
"CHROMEDRIVER_BIN": "$(CHROMEDRIVER)",
|
||||
},
|
||||
toolchains = [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
exec_properties = test_exec_properties,
|
||||
flaky = flaky,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//aio/content/examples:examples.bzl", "docs_example")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
||||
@@ -1313,6 +1313,12 @@
|
||||
},
|
||||
"name": "JSDayIE"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-01-30"
|
||||
},
|
||||
"name": "THAT Conf TX"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-02-29"
|
||||
@@ -1325,12 +1331,30 @@
|
||||
},
|
||||
"name": "ng-conf"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-04-10"
|
||||
},
|
||||
"name": "Cloud Next"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-05-24"
|
||||
},
|
||||
"name": "NG Belgrade Conf"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-05-30"
|
||||
},
|
||||
"name": "Craft"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-06-12"
|
||||
},
|
||||
"name": "RenderATL"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-06-13"
|
||||
@@ -1342,5 +1366,17 @@
|
||||
"start": "2024-06-27"
|
||||
},
|
||||
"name": "dotJS"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-07-29"
|
||||
},
|
||||
"name": "THAT Conf WI"
|
||||
},
|
||||
{
|
||||
"date": {
|
||||
"start": "2024-10-10"
|
||||
},
|
||||
"name": "NG-DE"
|
||||
}
|
||||
]
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
".read": false,
|
||||
".write": false,
|
||||
|
||||
"events": {
|
||||
".read": true,
|
||||
".write": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
{
|
||||
// Docs on Firebase hosting configuration: https://firebase.google.com/docs/hosting/full-config
|
||||
"hosting": {
|
||||
"target": "aio",
|
||||
"public": "dist",
|
||||
"cleanUrls": true,
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ADDING REDIRECTS
|
||||
//
|
||||
// In order to permanently redirect a URL to a new address, add an new entry in the `redirects`
|
||||
// list:
|
||||
// ```json
|
||||
// {"type": 301, "source": "/old/url", "destination": "/new/url"}
|
||||
// ```
|
||||
//
|
||||
// This will cause the server to redirect any clients that visit `/old/url` to `/new/url`. At
|
||||
// the same time, the ServiceWorker will be automatically configured to let requests for
|
||||
// `/old/url` pass through to the server (instead of serving them locally with the cached
|
||||
// `index.html`).
|
||||
//
|
||||
// In addition, you should also add a corresponding testcase in `URLS_TO_REDIRECT.txt` (inside
|
||||
// `tests/deployment/shared/`) to ensure that the new redirect behaves as expected.
|
||||
//
|
||||
// NOTE 1:
|
||||
// On clients that still have an old ServiceWorker version installed the next time they visit a
|
||||
// recently redirected URL, the ServiceWorker will not yet know to let the request pass through
|
||||
// to the server (and get redirected). In such cases, those clients will get a "Page not found"
|
||||
// error, as the app tries to load `generated/docs/old/url.json` (which no longer exists). Such
|
||||
// errors would typically go away after reloading the page (since the ServiceWorker will have
|
||||
// updated to the latest version in the background).
|
||||
//
|
||||
// This temporary error is usually not a concern, because it only affects clients with an old
|
||||
// ServiceWorker version at the time of the first visit and it goes away on reload. If, however,
|
||||
// it is important to avoid this error for a specific redirect, you can add an additional
|
||||
// redirect entry for the requested JSON file:
|
||||
// ```json
|
||||
// {"type": 302, "source": "/generated/docs/old/url.json", "destination": "/generated/docs/new/url.json"}
|
||||
// ```
|
||||
//
|
||||
// In order to avoid bloating the redirects config, it is recommended to remove this additional
|
||||
// entry after some time has passed, at which point the large majority of existing clients will
|
||||
// have switched to an updated ServiceWorker version.
|
||||
//
|
||||
// NOTE 2:
|
||||
// If you decide to add the additional entry described above, keep in mind that the JSON file
|
||||
// URL is derived from the page URL by prepending `/generated/docs`, replacing any upper-case
|
||||
// letters with the corresponding lower-case ones followed by an underscore (`_`) and appending
|
||||
// `.json`.
|
||||
//
|
||||
// For example, the page URL `/api/forms/FormBuilder` would result in requesting the JSON file
|
||||
// `/generated/docs/api/forms/f_ormb_uilder.json`.
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
"redirects": [
|
||||
// A random bad indexed page that used `api/api`
|
||||
{"type": 301, "source": "/api/api/:rest*", "destination": "/api/:rest*"},
|
||||
|
||||
// Guide renames/removals
|
||||
{"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/start"},
|
||||
{"type": 301, "source": "/docs/*/latest/glossary.html", "destination": "/guide/glossary"},
|
||||
{"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/start"},
|
||||
{"type": 301, "source": "/docs/*/latest/guide/server-communication.html", "destination": "/guide/understanding-communicating-with-http"},
|
||||
{"type": 301, "source": "/docs/*/latest/guide/style-guide.html", "destination": "/guide/styleguide"},
|
||||
{"type": 301, "source": "/guide/bazel", "destination": "https://github.com/angular/angular/blob/main/packages/bazel/docs/BAZEL_SCHEMATICS.md"},
|
||||
{"type": 301, "source": "/guide", "destination": "/docs"},
|
||||
{"type": 301, "source": "/guide/cli-quickstart", "destination": "/start"},
|
||||
{"type": 301, "source": "/guide/i18n", "destination": "/guide/i18n-overview"},
|
||||
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"},
|
||||
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"},
|
||||
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"},
|
||||
{"type": 301, "source": "/guide/webpack", "destination": "https://v5.angular.io/guide/webpack"},
|
||||
{"type": 301, "source": "/guide/http", "destination": "/guide/understanding-communicating-with-http"},
|
||||
{"type": 301, "source": "/guide/setup", "destination": "/guide/setup-local"},
|
||||
{"type": 301, "source": "/guide/setup-systemjs-anatomy", "destination": "/guide/file-structure"},
|
||||
{"type": 301, "source": "/guide/change-log", "destination": "https://github.com/angular/angular/blob/main/CHANGELOG.md"},
|
||||
{"type": 301, "source": "/guide/quickstart", "destination": "/start"},
|
||||
{"type": 301, "source": "/getting-started", "destination": "/start"},
|
||||
{"type": 301, "source": "/getting-started/:rest*", "destination": "/start/:rest*"},
|
||||
{"type": 301, "source": "/guide/displaying-data", "destination": "/start#template-syntax"},
|
||||
{"type": 301, "source": "/guide/ivy", "destination": "https://v12.angular.io/guide/ivy"},
|
||||
{"type": 301, "source": "/guide/updating-to-version-10", "destination": "https://v10.angular.io/guide/updating-to-version-10"},
|
||||
{"type": 301, "source": "/guide/updating-to-version-11", "destination": "https://v11.angular.io/guide/updating-to-version-11"},
|
||||
{"type": 301, "source": "/guide/updating-to-version-12", "destination": "https://v12.angular.io/guide/updating-to-version-12"},
|
||||
{"type": 301, "source": "/guide/updating-to-version-13", "destination": "https://v13.angular.io/guide/update-to-latest-version"},
|
||||
{"type": 301, "source": "/guide/set-document-title", "destination": "/guide/router#setting-the-page-title"},
|
||||
|
||||
// Update Angular guide refactor to name individual update topics.
|
||||
// This redirects the generic link to the current version's topic.
|
||||
// The destination value must be updated with each new major version.
|
||||
// When changing this value, be sure to update or add the corresponding
|
||||
// test in angular/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt
|
||||
{"type": 301, "source": "/guide/update-to-latest-version", "destination": "/guide/update-to-version-17"},
|
||||
|
||||
// Renaming of Getting Started topics
|
||||
{"type": 301, "source": "/start/data", "destination": "/start/start-data"},
|
||||
{"type": 301, "source": "/start/deployment", "destination": "/start/start-deployment"},
|
||||
{"type": 301, "source": "/start/forms", "destination": "/start/start-forms"},
|
||||
{"type": 301, "source": "/start/routing", "destination": "/start/start-routing"},
|
||||
|
||||
// Tutorials moved to subdirectories
|
||||
{"type": 301, "source": "/tutorial/index", "destination": "/tutorial/tour-of-heroes/index"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt0", "destination": "/tutorial/tour-of-heroes/toh-pt0"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt1", "destination": "/tutorial/tour-of-heroes/toh-pt1"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt2", "destination": "/tutorial/tour-of-heroes/toh-pt2"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt3", "destination": "/tutorial/tour-of-heroes/toh-pt3"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt4", "destination": "/tutorial/tour-of-heroes/toh-pt4"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt5", "destination": "/tutorial/tour-of-heroes/toh-pt5"},
|
||||
{"type": 301, "source": "/tutorial/toh-pt6", "destination": "/tutorial/tour-of-heroes/toh-pt6"},
|
||||
|
||||
// some top level guide pages on old site were moved below the guide folder
|
||||
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"},
|
||||
{"type": 301, "source": "/docs/styleguide", "destination": "/guide/styleguide"},
|
||||
|
||||
// news is now blog
|
||||
{"type": 301, "source": "/news*", "destination": "https://blog.angular.io/"},
|
||||
|
||||
// cookbook guides were moved (and sometime renamed or removed)
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook", "destination": "/docs"},
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook/", "destination": "/docs"},
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook/index.html", "destination": "/docs"},
|
||||
{"type": 301, "source": "/**/cookbook/ts-to-js*", "destination": "https://v2.angular.io/docs/ts/latest/cookbook/ts-to-js.html"},
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"},
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"},
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"},
|
||||
{"type": 301, "source": "/docs/*/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"},
|
||||
|
||||
// Forms related code was moved from the `common` to `forms` package (and NgFor was renamed to NgForOf)
|
||||
{"type": 301, "source": "/**/NgFor-*", "destination": "/api/common/NgForOf"},
|
||||
{"type": 301, "source": "/**/api/common/index/MaxLengthValidator-*", "destination": "/api/forms/MaxLengthValidator"},
|
||||
{"type": 301, "source": "/**/api/common/ControlGroup*", "destination": "/api/forms/FormGroup"},
|
||||
{"type": 301, "source": "/**/api/common/Control*", "destination": "/api/forms/FormControl"},
|
||||
{"type": 301, "source": "/**/api/common/SelectControlValueAccessor-*", "destination": "/api/forms/SelectControlValueAccessor"},
|
||||
{"type": 301, "source": "/**/api/common/NgModel", "destination": "/api/forms/NgModel"},
|
||||
|
||||
// `@angular/http` package was removed, and new `HttpClient` APIs are available under `@angular/common/http` package
|
||||
{"type": 301, "source": "/api/http", "destination": "/guide/deprecations#http"},
|
||||
{"type": 301, "source": "/api/http/**", "destination": "/guide/deprecations#http"},
|
||||
|
||||
// Animations moves, renames and removals
|
||||
{"type": 301, "source": "/api/animate/:rest*", "destination": "/api/animations/:rest*"},
|
||||
// AnimationStateDeclarationMetadata was removed
|
||||
{"type": 301, "source": "/**/AnimationStateDeclarationMetadata*", "destination": "/api/animations"},
|
||||
// `AnimationDriver` was moved to the `animations/browser` package
|
||||
{"type": 301, "source": "/api/platform-browser/AnimationDriver", "destination": "/api/animations/browser/AnimationDriver"},
|
||||
|
||||
// The `testing` package was renamed to `core/testing`
|
||||
{"type": 301, "source": "/api/testing/:api-*", "destination": "/api/core/testing/:api"},
|
||||
|
||||
// CORE_DIRECTIVES & PLATFORM_PIPES were removed and are now in the CommonModule
|
||||
{"type": 301, "source": "/**/CORE_DIRECTIVES*", "destination": "/api/common/CommonModule"},
|
||||
{"type": 301, "source": "/**/PLATFORM_PIPES*", "destination": "/api/common/CommonModule"},
|
||||
|
||||
// DirectiveMetadata is now covered by the Directive decorator
|
||||
{"type": 301, "source": "/**/DirectiveMetadata-*", "destination": "/api/core/Directive"},
|
||||
|
||||
// OptionalMetadata is now covered by the Optional decorator
|
||||
{"type": 301, "source": "/**/OptionalMetadata-*", "destination": "/api/core/Optional"},
|
||||
|
||||
// HTTP_PROVIDERS was removed and is now provided in HttpModule
|
||||
{"type": 301, "source": "/**/HTTP_PROVIDERS*", "destination": "/api/http/HttpModule"},
|
||||
|
||||
// URLs that use the old scheme of adding the type to the end (e.g. `SomeClass-class`)
|
||||
// (Exclude disambiguated URLs that might be suffixed with `-\d+` (e.g. `SomeClass-1`))
|
||||
{"type": 301, "source": "/api/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/:api"},
|
||||
{"type": 301, "source": "/api/:package/testing/index/:api", "destination": "/api/:package/testing/:api"},
|
||||
{"type": 301, "source": "/api/:package/testing/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/:package/testing/:api"},
|
||||
{"type": 301, "source": "/api/upgrade/:package/index/:api", "destination": "/api/upgrade/:package/:api"},
|
||||
{"type": 301, "source": "/api/upgrade/:package/:api-@(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "destination": "/api/upgrade/:package/:api"},
|
||||
|
||||
// URLs that use the old scheme before we moved the docs to the angular/angular repo
|
||||
{"type": 301, "source": "/docs/*/latest", "destination": "/docs"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/", "destination": "/api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/testing/:api-*", "destination": "/api/core/testing/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/:package/:api-*", "destination": "/api/:package/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/:package/index/:api-*", "destination": "/api/:package/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/:package/testing", "destination": "/api/:package/testing"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/platform-browser/animations/index/:api-*", "destination": "/api/platform-browser/animations/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"},
|
||||
{"type": 301, "source": "/docs/*/latest/glossary", "destination": "/guide/glossary"},
|
||||
{"type": 301, "source": "/docs/*/latest/guide/", "destination": "/docs"},
|
||||
{"type": 301, "source": "/docs/*/latest/guide/lifecycle-hooks", "destination": "/guide/lifecycle-hooks"},
|
||||
{"type": 301, "source": "/docs/*/latest/:rest*", "destination": "/:rest*"},
|
||||
{"type": 301, "source": "/docs/latest/:rest*", "destination": "/:rest*"},
|
||||
{"type": 301, "source": "/docs/styleguide*", "destination": "/guide/styleguide"},
|
||||
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"},
|
||||
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"},
|
||||
{"type": 301, "source": "/guide/learning-angular*", "destination": "/start"},
|
||||
{"type": 301, "source": "/testing", "destination": "/guide/testing"},
|
||||
{"type": 301, "source": "/testing/**", "destination": "/guide/testing"},
|
||||
|
||||
// Work around Firebase hosting bug with `/index.html?<query>` leading to infinite redirects.
|
||||
// See https://github.com/angular/angular/issues/42518#issuecomment-858545483 for details.
|
||||
{"type": 301, "source": "/index.html:query*", "destination": "/:query*"},
|
||||
|
||||
// Strip off the `.html` extension, because Firebase will not do this automatically any more
|
||||
// (unless the new URL points to an existing file, which is not necessarily the case here).
|
||||
{"type": 301, "source": "/:topLevelFile.html", "destination": "/:topLevelFile"},
|
||||
{"type": 301, "source": "/:somePath*/:file.html", "destination": "/:somePath*/:file"},
|
||||
|
||||
// The below paths are referenced in users projects generated by the CLI
|
||||
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
|
||||
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
|
||||
{"type": 301, "source": "/strict", "destination": "/guide/strict-mode"},
|
||||
|
||||
// Use discoverable link for Angular DevTools and redirect to the guide page
|
||||
{"type": 301, "source": "/devtools", "destination": "/guide/devtools"},
|
||||
|
||||
// Use discoverable link for Angular Package Format and redirect to the guide page
|
||||
{"type": 301, "source": "/apf", "destination": "/guide/angular-package-format"},
|
||||
|
||||
// Temporarily serve the guide at `/devtools` as well as `/guide/devtools` until users have
|
||||
// their SW updated to a version that knows about the `/devtools` redirect.
|
||||
{"type": 302, "source": "/generated/docs/devtools.json", "destination": "/generated/docs/guide/devtools.json"},
|
||||
|
||||
// Analytics pages that have been deleted in v15
|
||||
{"type": 301, "source": "/cli/usage-analytics-gathering", "destination": "/cli/analytics"},
|
||||
{"type": 301, "source": "/analytics", "destination": "/cli/analytics"},
|
||||
|
||||
// Deprecated error messages
|
||||
{"type": 301, "source": "/errors/NG6999", "destination": "https://v13.angular.io/errors/NG6999"},
|
||||
|
||||
// Universal has been renamed to ssr
|
||||
{"type": 301, "source": "/guide/universal", "destination": "/guide/ssr"}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**/!(*.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
// All URLs
|
||||
"source": "**",
|
||||
"headers": [
|
||||
// Report Trusted Types violations
|
||||
{
|
||||
"key": "Content-Security-Policy-Report-Only",
|
||||
|
||||
// The following Trusted Types policies are allowed:
|
||||
// - angular: Angular's main internal policy. Defined in the `@angular/core` package.
|
||||
// - angular#bundler: Used by Angular's bundler. Defined in the `webpack` package, enabled by `@angular-devkit/build-angular`.
|
||||
// - angular#unsafe-bypass: For bypassSecurityTrust* usage. Defined in the `@angular/core` package.
|
||||
// - aio#analytics: For the Legacy Google Analytics snippet. Defined in `index.html`.
|
||||
// - google#safe: Used by the safevalues library. Defined in the `safevalues` package.
|
||||
// - goog#html: Used by the Global Site Tag (`gtag.js`).
|
||||
|
||||
// csp.withgoogle.com is Google's CSP report collecting
|
||||
// infrastructure.
|
||||
// NOTE: Keep in sync with the header in `angular.json`.
|
||||
"value": "require-trusted-types-for 'script'; trusted-types angular angular#bundler angular#unsafe-bypass aio#analytics google#safe goog#html; report-uri https://csp.withgoogle.com/csp/angular.io"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "DENY"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// All paths (URLs without a file extension).
|
||||
"source": "**/!(*.*)",
|
||||
"headers": [
|
||||
{"key": "Cache-Control", "value": "no-cache"},
|
||||
{"key": "Link", "value": "</generated/navigation.json>;rel=preload;as=fetch,</generated/docs/index.json>;rel=preload;as=fetch"}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Images, fonts, (non-hashed) CSS/JS files.
|
||||
"source": "**/*.@(gif|jpg|jpeg|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|woff2)",
|
||||
"headers": [
|
||||
{"key": "Cache-Control", "value": "max-age=86400"} // 1 day
|
||||
]
|
||||
},
|
||||
{
|
||||
// Hashed CSS/JS files...
|
||||
"source": "**/*.+([0-9a-f]).@(css|js)",
|
||||
"headers": [
|
||||
{"key": "Cache-Control", "value": "max-age=2592000"} // 30 days
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"database": {
|
||||
"rules": "database.rules.json"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const {getAdjustedChromeBinPathForWindows} = require('./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;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ def link_local_packages(all_aio_deps):
|
||||
|
||||
# Special case deps that must be testonly
|
||||
testonly_deps = [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
]
|
||||
|
||||
# Stamp a corresponding target for each AIO dep that filters out transitive
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app-shell",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/index.html",
|
||||
"/pwa-manifest.json",
|
||||
"/assets/images/favicons/favicon.ico",
|
||||
"/assets/js/*.js",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
],
|
||||
"urls": [
|
||||
"https://fonts.googleapis.com/**",
|
||||
"https://fonts.gstatic.com/s/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets-eager",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/images/**",
|
||||
"/generated/images/marketing/**",
|
||||
"!/assets/images/favicons/**",
|
||||
"!/**/_unused/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets-lazy",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/images/favicons/**",
|
||||
"!/**/_unused/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "docs-index",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/generated/*.json",
|
||||
"/generated/docs/*.json",
|
||||
"/generated/docs/api/api-list.json",
|
||||
"/generated/docs/app/search-data.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "docs-lazy",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "lazy",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/generated/docs/**/*.json",
|
||||
"/generated/images/**",
|
||||
"!/**/_unused/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"navigationUrls": [
|
||||
"/**",
|
||||
"!/**/*.*",
|
||||
"!/**/*__*",
|
||||
"!/**/*__*/**",
|
||||
"!/**/stackblitz/{0,1}",
|
||||
"!/**/AnimationStateDeclarationMetadata*",
|
||||
"!/**/CORE_DIRECTIVES*",
|
||||
"!/**/DirectiveMetadata-*",
|
||||
"!/**/HTTP_PROVIDERS*",
|
||||
"!/**/NgFor-*",
|
||||
"!/**/OptionalMetadata-*",
|
||||
"!/**/PLATFORM_PIPES*",
|
||||
"!/**/api/common/Control*",
|
||||
"!/**/api/common/ControlGroup*",
|
||||
"!/**/api/common/NgModel/{0,1}",
|
||||
"!/**/api/common/SelectControlValueAccessor-*",
|
||||
"!/**/api/common/index/MaxLengthValidator-*",
|
||||
"!/**/cookbook/ts-to-js*",
|
||||
"!/analytics/{0,1}",
|
||||
"!/apf/{0,1}",
|
||||
"!/api/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
|
||||
"!/api/*/testing/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
|
||||
"!/api/*/testing/index/*",
|
||||
"!/api/animate/**",
|
||||
"!/api/api/**",
|
||||
"!/api/http/**",
|
||||
"!/api/http/{0,1}",
|
||||
"!/api/platform-browser/AnimationDriver/{0,1}",
|
||||
"!/api/testing/*-*",
|
||||
"!/api/upgrade/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
|
||||
"!/api/upgrade/*/index/*",
|
||||
"!/cli/usage-analytics-gathering/{0,1}",
|
||||
"!/config/solution-tsconfig/{0,1}",
|
||||
"!/config/tsconfig/{0,1}",
|
||||
"!/devtools/{0,1}",
|
||||
"!/docs/*/latest/**",
|
||||
"!/docs/*/latest/{0,1}",
|
||||
"!/docs/latest/**",
|
||||
"!/docs/styleguide*",
|
||||
"!/docs/styleguide/{0,1}",
|
||||
"!/getting-started/**",
|
||||
"!/getting-started/{0,1}",
|
||||
"!/guide/bazel/{0,1}",
|
||||
"!/guide/change-log/{0,1}",
|
||||
"!/guide/cli-quickstart/{0,1}",
|
||||
"!/guide/displaying-data/{0,1}",
|
||||
"!/guide/i18n/{0,1}",
|
||||
"!/guide/ivy/{0,1}",
|
||||
"!/guide/learning-angular*",
|
||||
"!/guide/metadata/{0,1}",
|
||||
"!/guide/ngmodule/{0,1}",
|
||||
"!/guide/quickstart/{0,1}",
|
||||
"!/guide/service-worker-comm/{0,1}",
|
||||
"!/guide/service-worker-configref/{0,1}",
|
||||
"!/guide/service-worker-getstart/{0,1}",
|
||||
"!/guide/set-document-title/{0,1}",
|
||||
"!/guide/setup-systemjs-anatomy/{0,1}",
|
||||
"!/guide/setup/{0,1}",
|
||||
"!/guide/update-to-latest-version/{0,1}",
|
||||
"!/guide/updating-to-version-10/{0,1}",
|
||||
"!/guide/updating-to-version-11/{0,1}",
|
||||
"!/guide/updating-to-version-12/{0,1}",
|
||||
"!/guide/updating-to-version-13/{0,1}",
|
||||
"!/guide/webpack/{0,1}",
|
||||
"!/guide/{0,1}",
|
||||
"!/news*",
|
||||
"!/start/data/{0,1}",
|
||||
"!/start/deployment/{0,1}",
|
||||
"!/start/forms/{0,1}",
|
||||
"!/start/routing/{0,1}",
|
||||
"!/strict/{0,1}",
|
||||
"!/styleguide/{0,1}",
|
||||
"!/testing/**",
|
||||
"!/testing/{0,1}"
|
||||
]
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app-shell",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/index.html",
|
||||
"/pwa-manifest.json",
|
||||
"/assets/images/favicons/favicon.ico",
|
||||
"/assets/js/*.js",
|
||||
"/*.css",
|
||||
"/*.js"
|
||||
],
|
||||
"urls": [
|
||||
"https://fonts.googleapis.com/**",
|
||||
"https://fonts.gstatic.com/s/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets-eager",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/images/**",
|
||||
"/generated/images/marketing/**",
|
||||
"!/assets/images/favicons/**",
|
||||
"!/**/_unused/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets-lazy",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/images/favicons/**",
|
||||
"!/**/_unused/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "docs-index",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/generated/*.json",
|
||||
"/generated/docs/*.json",
|
||||
"/generated/docs/api/api-list.json",
|
||||
"/generated/docs/app/search-data.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "docs-lazy",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "lazy",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/generated/docs/**/*.json",
|
||||
"/generated/images/**",
|
||||
"!/**/_unused/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"navigationUrls": [
|
||||
"/**",
|
||||
"!/**/*.*",
|
||||
"!/**/*__*",
|
||||
"!/**/*__*/**",
|
||||
"!/**/stackblitz/{0,1}"
|
||||
]
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
{
|
||||
"name": "angular.io",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:angular/angular.git",
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"postinstall": "node tools/cli-patches/patch.js && patch-package --patch-dir ../tools/npm-patches/",
|
||||
"prebuild": "yarn setup",
|
||||
"build": "bazel build //aio:build",
|
||||
"build-prod": "yarn build --config=release",
|
||||
"build-local": "yarn build --config=aio_local_deps",
|
||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint && yarn security-lint",
|
||||
"test": "bazel test //aio:test --test_output=streamed --test_tag_filters=-broken",
|
||||
"test-and-watch": "ibazel test //aio:test-and-watch --test_output=streamed --test_tag_filters=-broken",
|
||||
"test-local": "yarn test --config=aio_local_deps",
|
||||
"test:ci": "bazel test //aio/... --test_tag_filters=-broken",
|
||||
"test-local:ci": "yarn test:ci --config=aio_local_deps",
|
||||
"e2e": "bazel test //aio:e2e",
|
||||
"e2e-local": "yarn e2e --config=aio_local_deps",
|
||||
"setup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env",
|
||||
"set-opensearch-url": "node --eval \"const sh = require('shelljs'); sh.set('-e'); sh.sed('-i', /PLACEHOLDER_URL/g, process.argv[1], '../dist/bin/aio/build/assets/opensearch.xml');\"",
|
||||
"smoke-tests": "protractor tests/deployment/e2e/protractor.conf.js --suite smoke --baseUrl",
|
||||
"test-a11y-score": "bazel run //aio/scripts:test-aio-a11y",
|
||||
"test-a11y-score-localhost": "bazel test //aio:test-a11y-score-localhost",
|
||||
"test-pwa-score": "sh -c 'bazel run //aio/scripts:audit-web-app ${0} all:0,pwa:${1}'",
|
||||
"test-pwa-score-localhost": "bazel test //aio:test-pwa-score-localhost",
|
||||
"test-production-url": "bazel test //aio/tests/deployment/e2e",
|
||||
"example-e2e": "node --experimental-import-meta-resolve tools/examples/run-filtered-example-e2es.mjs",
|
||||
"example-list-overrides": "bazel run //aio/tools/examples:example-boilerplate list-overrides",
|
||||
"example-lint": "eslint content/examples",
|
||||
"example-playground": "node ./tools/examples/create-example-playground-wrapper.mjs",
|
||||
"deploy-production": "node ./scripts/deploy-to-firebase/index.mjs",
|
||||
"check-env": "yarn ~~check-env",
|
||||
"payload-size": "scripts/payload.sh",
|
||||
"predocs": "node scripts/contributors/validate-data && bazel build -- //aio:stackblitz //aio:example-zips",
|
||||
"docs-watch": "bazel run //aio:docs-watch --config=release",
|
||||
"docs": "bazel build //aio:dgeni --config=release",
|
||||
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
|
||||
"docs-test": "bazel test //aio/tools/transforms/...",
|
||||
"redirects-test": "bazel test //aio/tests/deployment/unit:test",
|
||||
"firebase-utils-test": "bazel test //aio/tools/firebase-test-utils:test",
|
||||
"tools-lint": "eslint --ext=.mjs scripts/deploy-to-firebase && eslint tools/firebase-test-utils",
|
||||
"tools-test": "bazel test -- //aio/tools/... //aio/scripts/deploy-to-firebase:test",
|
||||
"start": "ibazel run //aio:serve --config=release",
|
||||
"start-local": "yarn start --config=aio_local_deps",
|
||||
"boilerplate:test": "bazel test //aio/tools/examples:example-boilerplate-test",
|
||||
"create-example": "bazel run //aio/tools/examples:create-example",
|
||||
"security-lint": "tsec -p tsconfig.app.json",
|
||||
"~~audit-web-app": "node scripts/audit-web-app.mjs",
|
||||
"~~check-env": "node scripts/check-environment",
|
||||
"~~light-server": "light-server --bind=localhost --historyindex=/index.html --no-reload",
|
||||
"local-workspace-status": "node scripts/local-workspace-status.mjs"
|
||||
},
|
||||
"//engines-comment": "If applicable, also update /package.json and /aio/tools/examples/shared/package.json",
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"yarn": ">=1.22.4 <2",
|
||||
"npm": "Please use yarn instead of NPM to install dependencies"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.2.0-rc.1",
|
||||
"@angular/cdk": "17.2.0-rc.0",
|
||||
"@angular/common": "17.2.0-rc.1",
|
||||
"@angular/compiler": "17.2.0-rc.1",
|
||||
"@angular/core": "17.2.0-rc.1",
|
||||
"@angular/elements": "17.2.0-rc.1",
|
||||
"@angular/forms": "17.2.0-rc.1",
|
||||
"@angular/material": "17.2.0-rc.0",
|
||||
"@angular/platform-browser": "17.2.0-rc.1",
|
||||
"@angular/platform-browser-dynamic": "17.2.0-rc.1",
|
||||
"@angular/router": "17.2.0-rc.1",
|
||||
"@angular/service-worker": "17.2.0-rc.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"safevalues": "^0.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/architect-cli": "0.1702.0-rc.0",
|
||||
"@angular-devkit/build-angular": "17.2.0-rc.0",
|
||||
"@angular-eslint/builder": "16.0.3",
|
||||
"@angular-eslint/eslint-plugin": "16.0.3",
|
||||
"@angular-eslint/eslint-plugin-template": "16.0.3",
|
||||
"@angular-eslint/template-parser": "^16.0.3",
|
||||
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c4cf003cb4ac849986beaa243d7e85a893612f2",
|
||||
"@angular/cli": "17.2.0-rc.0",
|
||||
"@angular/compiler-cli": "17.2.0-rc.1",
|
||||
"@bazel/bazelisk": "^1.7.5",
|
||||
"@bazel/buildozer": "^6.0.0",
|
||||
"@bazel/ibazel": "^0.16.2",
|
||||
"@bazel/jasmine": "^5.4.1",
|
||||
"@bazel/runfiles": "5.8.1",
|
||||
"@bazel/typescript": "5.8.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/lunr": "^2.3.3",
|
||||
"@types/node": "^12.7.9",
|
||||
"@types/trusted-types": "^2.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
"archiver": "^6.0.0",
|
||||
"assert": "^2.0.0",
|
||||
"canonical-path": "1.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cjson": "^0.5.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"css-selector-parser": "^3.0.0",
|
||||
"dgeni": "^0.4.14",
|
||||
"dgeni-packages": "^0.30.0",
|
||||
"entities": "^4.0.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jasmine": "4.1.3",
|
||||
"eslint-plugin-jsdoc": "48.2.0",
|
||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||
"find-free-port": "^2.0.0",
|
||||
"firebase-tools": "^13.0.0",
|
||||
"fs-extra": "^11.0.0",
|
||||
"get-port": "^7.0.0",
|
||||
"globby": "^14.0.0",
|
||||
"hast-util-has-property": "^1.0.4",
|
||||
"hast-util-is-element": "^1.1.0",
|
||||
"hast-util-to-string": "^1.0.4",
|
||||
"html": "^1.0.0",
|
||||
"ignore": "^5.1.8",
|
||||
"image-size": "^1.0.0",
|
||||
"jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"json5": "^2.2.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"light-server": "^2.9.1",
|
||||
"lighthouse": "^9.0.0",
|
||||
"lighthouse-logger": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lunr": "^2.3.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"patch-package": "^7.0.0",
|
||||
"protractor": "~7.0.0",
|
||||
"puppeteer-core": "21.3.8",
|
||||
"rehype-slug": "^4.0.1",
|
||||
"remark": "^12.0.0",
|
||||
"remark-html": "^13.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"semver": "^7.3.5",
|
||||
"shelljs": "^0.8.5",
|
||||
"source-map-support": "0.5.21",
|
||||
"stemmer": "^2.0.0",
|
||||
"tree-kill": "^1.1.0",
|
||||
"ts-node": "^10.8.1",
|
||||
"tsec": "^0.2.2",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "~5.0.2",
|
||||
"uglify-js": "^3.13.3",
|
||||
"unist-util-filter": "^2.0.3",
|
||||
"unist-util-source": "^3.0.0",
|
||||
"unist-util-visit": "^2.0.3",
|
||||
"unist-util-visit-parents": "^3.1.1",
|
||||
"watchr": "^3.0.1",
|
||||
"xregexp": "^5.0.2",
|
||||
"yargs": "^17.0.0"
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
load("@aio_npm//@angular/build-tooling/bazel:filter_outputs.bzl", "filter_first_output")
|
||||
load("@npm//@angular/build-tooling/bazel:filter_outputs.bzl", "filter_first_output")
|
||||
load("//tools:defaults.bzl", "nodejs_binary", "nodejs_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
@@ -13,8 +13,8 @@ nodejs_binary(
|
||||
data = [
|
||||
"//aio:firebase.json",
|
||||
"//aio:ngsw-config.template.json",
|
||||
"@aio_npm//canonical-path",
|
||||
"@aio_npm//json5",
|
||||
"@npm//canonical-path",
|
||||
"@npm//json5",
|
||||
],
|
||||
entry_point = "build-ngsw-config.js",
|
||||
)
|
||||
@@ -26,7 +26,7 @@ js_library(
|
||||
],
|
||||
deps = [
|
||||
"//aio/tools/transforms/authors-package:watchdocs",
|
||||
"@aio_npm//@angular-devkit/architect-cli",
|
||||
"@npm//@angular-devkit/architect-cli",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -40,17 +40,17 @@ nodejs_binary(
|
||||
testonly = True,
|
||||
data = [
|
||||
"//aio/tools:windows-chromium-path",
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@aio_npm//lighthouse",
|
||||
"@aio_npm//lighthouse-logger",
|
||||
"@aio_npm//puppeteer-core",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//lighthouse",
|
||||
"@npm//lighthouse-logger",
|
||||
"@npm//puppeteer-core",
|
||||
],
|
||||
entry_point = "audit-web-app.mjs",
|
||||
env = {
|
||||
"CHROME_BIN": "$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@aio_npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ nodejs_test(
|
||||
data = [
|
||||
":audit-web-app",
|
||||
":audit-web-app-script",
|
||||
"@aio_npm//shelljs",
|
||||
"@npm//shelljs",
|
||||
],
|
||||
entry_point = "test-aio-a11y.mjs",
|
||||
env = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("@aio_npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
|
||||
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
|
||||
DEPLOY_TO_FIREBASE_SOURCES = glob(
|
||||
["**/*.mjs"],
|
||||
@@ -7,7 +7,7 @@ DEPLOY_TO_FIREBASE_SOURCES = glob(
|
||||
)
|
||||
|
||||
DEPLOY_TO_FIREBASE_DEPS = [
|
||||
"@aio_npm//shelljs",
|
||||
"@npm//shelljs",
|
||||
"//aio:build",
|
||||
"//:package.json",
|
||||
]
|
||||
|
||||
@@ -16,16 +16,16 @@ def local_server_test(name, entry_point, serve_target, data = [], args = [], **k
|
||||
name = name,
|
||||
testonly = True,
|
||||
args = [
|
||||
"$(rootpath @aio_npm//light-server/bin:light-server)",
|
||||
"$(rootpath @npm//light-server/bin:light-server)",
|
||||
"$(rootpath %s)" % serve_target,
|
||||
"$(rootpath %s)" % entry_point,
|
||||
] + args,
|
||||
data = [
|
||||
"//aio/scripts:run-with-local-server.mjs",
|
||||
"@aio_npm//get-port",
|
||||
"@aio_npm//shelljs",
|
||||
"@aio_npm//tree-kill",
|
||||
"@aio_npm//light-server/bin:light-server",
|
||||
"@npm//get-port",
|
||||
"@npm//shelljs",
|
||||
"@npm//tree-kill",
|
||||
"@npm//light-server/bin:light-server",
|
||||
serve_target,
|
||||
entry_point,
|
||||
] + data,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"ban-reviewed-conversions": [
|
||||
"src/app/custom-elements/code/code.component.ts",
|
||||
"src/app/custom-elements/code/pretty-printer.service.ts",
|
||||
"src/app/documents/document.service.ts",
|
||||
"src/app/shared/security.ts"
|
||||
],
|
||||
"ban-worker-calls": [
|
||||
"src/app/search/search.service.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<!--
|
||||
This content replaces the `<body>` content of `index.html` to generate our custom `404.html` page.
|
||||
The content must visually and structurally resemble the resulting HTML of the main app for not
|
||||
found pages (e.g. https://angular.io/not/exist.ing).
|
||||
-->
|
||||
|
||||
<style>
|
||||
.mat-toolbar-row{display:flex;box-sizing:border-box;padding:0 16px;width:100%;flex-direction:row;align-items:center;white-space:nowrap}
|
||||
.mat-toolbar-row{height:64px}
|
||||
@media (max-width:600px){.mat-toolbar-row{height:56px}}
|
||||
.nav-link.home{margin-left:24px}
|
||||
</style>
|
||||
|
||||
<aio-shell class="mode-stable page-file-not-found folder-file-not-found view- aio-notification-hide">
|
||||
|
||||
<mat-toolbar class="app-toolbar no-print mat-toolbar mat-primary">
|
||||
<mat-toolbar-row class="mat-toolbar-row">
|
||||
<a class="nav-link home" href="/">
|
||||
<img alt="Home" height="40" src="assets/images/logos/angular/logo-nav@2x.png" title="Home" width="150">
|
||||
</a>
|
||||
</mat-toolbar-row>
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-sidenav-container class="sidenav-container mat-drawer-container mat-sidenav-container" role="main">
|
||||
<mat-sidenav-content class="mat-drawer-content mat-sidenav-content">
|
||||
<section class="sidenav-content" role="article">
|
||||
<aio-doc-viewer>
|
||||
<div class="content">
|
||||
<div class="nf-container l-flex-wrap flex-center">
|
||||
<img src="assets/images/support/angular-404.svg" width="300" height="300" alt="not found"/>
|
||||
<div class="nf-response l-flex-wrap center">
|
||||
<h1 class="no-toc" id="page-not-found">Resource Not Found</h1>
|
||||
<p>We're sorry. The resource you are looking for cannot be found.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aio-doc-viewer>
|
||||
</section>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
<footer class="no-print">
|
||||
<aio-footer>
|
||||
<p>
|
||||
Powered by Google ©2010-2022.
|
||||
Code licensed under an <a href="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>
|
||||
</aio-footer>
|
||||
</footer>
|
||||
|
||||
</aio-shell>
|
||||
@@ -1,87 +0,0 @@
|
||||
<div id="top-of-page"></div>
|
||||
|
||||
<aio-cookies-popup></aio-cookies-popup>
|
||||
|
||||
<a class="skip-to-content-link" href="#main-content">Skip to main content</a>
|
||||
|
||||
<div *ngIf="isFetching" class="progress-bar-container">
|
||||
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<mat-toolbar #appToolbar color="primary" class="app-toolbar no-print" [class.transitioning]="isTransitioning">
|
||||
<mat-toolbar-row class="notification-container">
|
||||
<aio-notification notificationId="state-of-js-2023" expirationDate="2023-12-12" [dismissOnContentClick]="true" (dismissed)="notificationDismissed()">
|
||||
<a href="https://survey.devographics.com/survey/state-of-js/2023?source=angular_homepage">
|
||||
<mat-icon class="icon" svgIcon="insert_comment" aria-label="Announcement"></mat-icon>
|
||||
<span class="message">Share your experience with Angular in <b>The State of JavaScript</b></span>
|
||||
<span class="action-button">Go to survey</span>
|
||||
</a>
|
||||
</aio-notification>
|
||||
</mat-toolbar-row>
|
||||
<mat-toolbar-row>
|
||||
<button mat-button class="hamburger" [class.no-animations]="disableAnimations" (click)="sidenav.toggle()" title="Docs menu">
|
||||
<mat-icon svgIcon="menu"></mat-icon>
|
||||
</button>
|
||||
<a class="nav-link home" href="/" [ngSwitch]="showTopMenu">
|
||||
<img *ngSwitchCase="true" src="assets/images/logos/angular/logo-nav@2x.png" width="150" height="40" title="Home" alt="Home">
|
||||
<img *ngSwitchDefault src="assets/images/logos/angular/shield-large.svg" width="37" height="40" title="Home" alt="Home">
|
||||
</a>
|
||||
<aio-top-menu *ngIf="showTopMenu" [nodes]="topMenuNodes" [currentNode]="currentNodes.TopBar"></aio-top-menu>
|
||||
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event, true)"></aio-search-box>
|
||||
<aio-theme-toggle #themeToggle></aio-theme-toggle>
|
||||
<div #externalIcons class="toolbar-external-icons-container">
|
||||
<a mat-icon-button href="https://twitter.com/angular" title="Twitter" aria-label="Angular on Twitter">
|
||||
<mat-icon svgIcon="logos:twitter"></mat-icon>
|
||||
</a>
|
||||
<a mat-icon-button href="https://github.com/angular/angular" title="GitHub" aria-label="Angular on GitHub">
|
||||
<mat-icon svgIcon="logos:github"></mat-icon>
|
||||
</a>
|
||||
<a mat-icon-button href="https://youtube.com/angular" title="YouTube" aria-label="Angular on YouTube">
|
||||
<mat-icon svgIcon="logos:youtube"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</mat-toolbar-row>
|
||||
</mat-toolbar>
|
||||
</header>
|
||||
|
||||
<aio-search-results *ngIf="showSearchResults"
|
||||
#searchResultsView
|
||||
[searchResults]="searchResults | async"
|
||||
(resultSelected)="hideSearchResults()"
|
||||
(closeButtonClick)="hideSearchResults()">
|
||||
</aio-search-results>
|
||||
|
||||
<mat-sidenav-container class="sidenav-container" [class.no-animations]="disableAnimations" [class.has-floating-toc]="hasFloatingToc">
|
||||
|
||||
<mat-sidenav [class.collapsed]="!dockSideNav" #sidenav class="sidenav" [mode]="mode" [opened]="isOpened" (openedChange)="updateHostClasses()">
|
||||
<aio-nav-menu *ngIf="!showTopMenu" [nodes]="topMenuNarrowNodes" [currentNode]="currentNodes.TopBarNarrow" [isWide]="dockSideNav" navLabel="primary"></aio-nav-menu>
|
||||
<aio-nav-menu [nodes]="sideNavNodes" [currentNode]="currentNodes.SideNav" [isWide]="dockSideNav" navLabel="guides and docs"></aio-nav-menu>
|
||||
|
||||
<div class="doc-version">
|
||||
<aio-nav-menu [nodes]="docVersions" [isWide]="true" [currentNode]="currentDocsVersionNode" navLabel="docs versions"></aio-nav-menu>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
|
||||
<section class="sidenav-content-container">
|
||||
|
||||
<main class="sidenav-content" [id]="pageId">
|
||||
<div id="main-content" tabindex="-1"></div>
|
||||
<aio-mode-banner *ngIf="versionInfo" [mode]="deployment.mode" [version]="versionInfo"></aio-mode-banner>
|
||||
<aio-doc-viewer [class.no-animations]="disableAnimations" [doc]="currentDocument" (docReady)="onDocReady()" (docRemoved)="onDocRemoved()" (docInserted)="onDocInserted()" (docRendered)="onDocRendered()">
|
||||
</aio-doc-viewer>
|
||||
<aio-dt *ngIf="dtOn" [(doc)]="currentDocument"></aio-dt>
|
||||
</main>
|
||||
|
||||
<div *ngIf="hasFloatingToc" class="toc-container no-print" [style.max-height.px]="tocMaxHeight" (wheel)="restrainScrolling($event)">
|
||||
<aio-lazy-ce selector="aio-toc"></aio-lazy-ce>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<footer class="no-print">
|
||||
<aio-footer [nodes]="footerNodes" [versionInfo]="versionInfo"></aio-footer>
|
||||
</footer>
|
||||
|
||||
</mat-sidenav-container>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,480 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { DocumentContents, DocumentService } from 'app/documents/document.service';
|
||||
import { NotificationComponent } from 'app/layout/notification/notification.component';
|
||||
import { CurrentNodes, NavigationNode, NavigationService, VersionInfo } from 'app/navigation/navigation.service';
|
||||
import { SearchResults } from 'app/search/interfaces';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
import { SwUpdatesService } from 'app/sw-updates/sw-updates.service';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
|
||||
const sideNavView = 'SideNav';
|
||||
export const showTopMenuWidth = 1150;
|
||||
export const dockSideNavWidth = 992;
|
||||
export const showFloatingTocWidth = 800;
|
||||
|
||||
@Component({
|
||||
selector: 'aio-shell',
|
||||
templateUrl: './app.component.html',
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
static reducedMotion = window.matchMedia('(prefers-reduced-motion)').matches;
|
||||
|
||||
// Disable all Angular animations if the user prefers reduced motion or for the initial render.
|
||||
@HostBinding('@.disabled')
|
||||
get disableAnimations(): boolean { return AppComponent.reducedMotion || this.isStarting; }
|
||||
|
||||
currentDocument: DocumentContents;
|
||||
currentDocVersion: NavigationNode;
|
||||
currentNodes: CurrentNodes = {};
|
||||
currentPath: string;
|
||||
docVersions: NavigationNode[];
|
||||
dtOn = false;
|
||||
footerNodes: NavigationNode[];
|
||||
|
||||
/**
|
||||
* An HTML friendly identifier for the currently displayed page.
|
||||
* This is computed from the `currentDocument.id` by replacing `/` with `-`
|
||||
*/
|
||||
pageId: string;
|
||||
/**
|
||||
* An HTML friendly identifier for the "folder" of the currently displayed page.
|
||||
* This is computed by taking everything up to the first `/` in the `currentDocument.id`
|
||||
*/
|
||||
folderId: string;
|
||||
/**
|
||||
* These CSS classes are computed from the current state of the application
|
||||
* (e.g. what document is being viewed) to allow for fine grain control over
|
||||
* the styling of individual pages.
|
||||
* You will get three classes:
|
||||
*
|
||||
* * `page-...`: computed from the current document id (e.g. events, guide-security, tutorial-toh-pt2)
|
||||
* * `folder-...`: computed from the top level folder for an id (e.g. guide, tutorial, etc)
|
||||
* * `view-...`: computed from the navigation view (e.g. SideNav, TopBar, etc)
|
||||
*/
|
||||
@HostBinding('class')
|
||||
hostClasses = '';
|
||||
|
||||
private isStarting = true;
|
||||
isTransitioning = true;
|
||||
isFetching = false;
|
||||
showTopMenu = false;
|
||||
dockSideNav = false;
|
||||
private isFetchingTimeout: any;
|
||||
private isSideNavDoc = false;
|
||||
|
||||
sideNavNodes: NavigationNode[];
|
||||
topMenuNodes: NavigationNode[];
|
||||
topMenuNarrowNodes: NavigationNode[];
|
||||
|
||||
hasFloatingToc = false;
|
||||
private showFloatingToc = new BehaviorSubject(false);
|
||||
tocMaxHeight: string;
|
||||
private tocMaxHeightOffset = 0;
|
||||
|
||||
currentDocsVersionNode?: NavigationNode;
|
||||
|
||||
versionInfo: VersionInfo | undefined;
|
||||
|
||||
get isOpened() { return this.dockSideNav && this.isSideNavDoc; }
|
||||
get mode() { return this.isOpened ? 'side' : 'over'; }
|
||||
|
||||
// Search related properties
|
||||
showSearchResults = false;
|
||||
searchResults: Observable<SearchResults>;
|
||||
@ViewChildren('searchBox, searchResultsView', { read: ElementRef })
|
||||
searchElements: QueryList<ElementRef>;
|
||||
@ViewChild(SearchBoxComponent, { static: true })
|
||||
searchBox: SearchBoxComponent;
|
||||
@ViewChild('searchResultsView', { read: ElementRef })
|
||||
searchResultsView: ElementRef;
|
||||
|
||||
@ViewChild(MatSidenav, { static: true })
|
||||
sidenav: MatSidenav;
|
||||
|
||||
@ViewChild(NotificationComponent, { static: true })
|
||||
notification: NotificationComponent;
|
||||
notificationAnimating = false;
|
||||
|
||||
@ViewChild('appToolbar', { read: ElementRef }) toolbar: ElementRef;
|
||||
|
||||
@ViewChildren('themeToggle, externalIcons', { read: ElementRef }) toolbarIcons: QueryList<ElementRef>;
|
||||
|
||||
constructor(
|
||||
public deployment: Deployment,
|
||||
private documentService: DocumentService,
|
||||
private hostElement: ElementRef,
|
||||
private locationService: LocationService,
|
||||
private navigationService: NavigationService,
|
||||
private scrollService: ScrollService,
|
||||
private searchService: SearchService,
|
||||
private swUpdatesService: SwUpdatesService,
|
||||
private tocService: TocService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
// Do not initialize the search on browsers that lack web worker support
|
||||
if ('Worker' in window) {
|
||||
// Delay initialization by up to 2 seconds
|
||||
this.searchService.initWorker(2000);
|
||||
}
|
||||
|
||||
this.onResize(window.innerWidth);
|
||||
|
||||
/* No need to unsubscribe because this root component never dies */
|
||||
|
||||
this.documentService.currentDocument.subscribe(doc => this.currentDocument = doc);
|
||||
|
||||
this.locationService.currentPath.subscribe(path => {
|
||||
if (path === this.currentPath) {
|
||||
// scroll only if on same page (most likely a change to the hash)
|
||||
this.scrollService.scroll();
|
||||
} else {
|
||||
// don't scroll; leave that to `onDocRendered`
|
||||
this.currentPath = path;
|
||||
|
||||
// Start progress bar if doc not rendered within brief time
|
||||
clearTimeout(this.isFetchingTimeout);
|
||||
this.isFetchingTimeout = setTimeout(() => this.isFetching = true, 200);
|
||||
}
|
||||
});
|
||||
|
||||
this.navigationService.currentNodes.subscribe(currentNodes => {
|
||||
this.currentNodes = currentNodes;
|
||||
|
||||
// Redirect to docs if we are in archive mode and are not hitting a docs page
|
||||
// (i.e. we have arrived at a marketing page)
|
||||
if (this.deployment.mode === 'archive' && !currentNodes[sideNavView]) {
|
||||
this.locationService.replace('docs');
|
||||
}
|
||||
});
|
||||
|
||||
// Compute the version picker list from the current version and the versions in the navigation map
|
||||
combineLatest([
|
||||
this.navigationService.versionInfo,
|
||||
this.navigationService.navigationViews.pipe(map(views => views.docVersions)),
|
||||
this.locationService.currentUrl,
|
||||
]).subscribe(([versionInfo, versions, currentUrl]) => {
|
||||
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet
|
||||
const computedVersions: NavigationNode[] = [
|
||||
{ title: 'next', url: 'https://next.angular.io/' },
|
||||
{ title: 'rc', url: 'https://rc.angular.io/' },
|
||||
{ title: 'stable', url: 'https://angular.io/' },
|
||||
];
|
||||
if (this.deployment.mode === 'archive') {
|
||||
computedVersions.push({ title: `v${versionInfo.major}` });
|
||||
}
|
||||
const allDocsVersionNodes = [...computedVersions, ...versions].map(version => ({
|
||||
...version,
|
||||
// Update the urls so that they point to the same page the user is currently at
|
||||
url: `${version.url}${(version.url?.endsWith('/') ? '' : '/' )}${currentUrl}`,
|
||||
}));
|
||||
// Find the current version - either title matches the current deployment mode
|
||||
// or its title matches the major version of the current version info
|
||||
this.currentDocsVersionNode = allDocsVersionNodes.find(
|
||||
version => version.title === this.deployment.mode || version.title === `v${versionInfo.major}`
|
||||
);
|
||||
this.docVersions = [
|
||||
{
|
||||
title: 'Docs Versions',
|
||||
children : allDocsVersionNodes
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
this.navigationService.navigationViews.subscribe(views => {
|
||||
this.footerNodes = views.Footer || [];
|
||||
this.sideNavNodes = views.SideNav || [];
|
||||
this.topMenuNodes = views.TopBar || [];
|
||||
this.topMenuNarrowNodes = views.TopBarNarrow || this.topMenuNodes;
|
||||
});
|
||||
|
||||
this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi);
|
||||
|
||||
const hasNonEmptyToc = this.tocService.tocList.pipe(map(tocList => tocList.length > 0));
|
||||
combineLatest([hasNonEmptyToc, this.showFloatingToc])
|
||||
.subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc);
|
||||
|
||||
// Generally, we want to delay updating the shell (e.g. host classes, sidenav state) for the new
|
||||
// document, until after the leaving document has been removed (to avoid having the styles for
|
||||
// the new document applied prematurely).
|
||||
// For the first document, though, (when we know there is no previous document), we want to
|
||||
// ensure the styles are applied as soon as possible to avoid flicker.
|
||||
combineLatest([
|
||||
this.documentService.currentDocument, // ...needed to determine host classes
|
||||
this.navigationService.currentNodes, // ...needed to determine `sidenav` state
|
||||
]).pipe(first())
|
||||
.subscribe(() => this.updateShell());
|
||||
|
||||
// Start listening for SW version update events.
|
||||
this.swUpdatesService.enable();
|
||||
}
|
||||
|
||||
onDocReady() {
|
||||
// About to transition to new view.
|
||||
this.isTransitioning = true;
|
||||
|
||||
// Stop fetching timeout (which, when render is fast, means progress bar never shown)
|
||||
clearTimeout(this.isFetchingTimeout);
|
||||
|
||||
// If progress bar has been shown, keep it for at least 500ms (to avoid flashing).
|
||||
setTimeout(() => this.isFetching = false, 500);
|
||||
}
|
||||
|
||||
onDocRemoved() {
|
||||
this.scrollService.removeStoredScrollInfo();
|
||||
}
|
||||
|
||||
onDocInserted() {
|
||||
// Update the shell (host classes, sidenav state) to match the new document.
|
||||
// This may be called as a result of actions initiated by view updates.
|
||||
// In order to avoid errors (e.g. `ExpressionChangedAfterItHasBeenChecked`), updating the view
|
||||
// (e.g. sidenav, host classes) needs to happen asynchronously.
|
||||
setTimeout(() => this.updateShell());
|
||||
|
||||
// Scroll the good position depending on the context
|
||||
this.scrollService.scrollAfterRender(500);
|
||||
}
|
||||
|
||||
onDocRendered() {
|
||||
if (this.isStarting) {
|
||||
// In order to ensure that the initial sidenav-content left margin
|
||||
// adjustment happens without animation, we need to ensure that
|
||||
// `isStarting` remains `true` until the margin change is triggered.
|
||||
// (Apparently, this happens with a slight delay.)
|
||||
setTimeout(() => this.isStarting = false, 100);
|
||||
}
|
||||
|
||||
this.isTransitioning = false;
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event.target.innerWidth'])
|
||||
onResize(width: number) {
|
||||
this.showTopMenu = width >= showTopMenuWidth;
|
||||
this.dockSideNav = width >= dockSideNavWidth;
|
||||
this.showFloatingToc.next(width > showFloatingTocWidth);
|
||||
|
||||
if (this.showTopMenu && !this.isSideNavDoc) {
|
||||
// If this is a non-sidenav doc and the screen is wide enough so that we can display menu
|
||||
// items in the top-bar, ensure the sidenav is closed.
|
||||
// (This condition can only be met when the resize event changes the value of `showTopMenu`
|
||||
// from `false` to `true` while on a non-sidenav doc.)
|
||||
this.sidenav.toggle(false);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('focusin', ['$event.target'])
|
||||
onFocus(eventTarget: HTMLElement) {
|
||||
// Implement a focus trap starting at the input search and ending after the search results
|
||||
if (this.showSearchResults) {
|
||||
const insideFocusLoop = [
|
||||
...this.toolbarIcons,
|
||||
...this.searchElements
|
||||
].some(element => element.nativeElement.contains(eventTarget));
|
||||
const insideToolbar = this.toolbar.nativeElement.contains(eventTarget);
|
||||
if (!insideFocusLoop) {
|
||||
if (!insideToolbar) {
|
||||
// the user is focusing forward at the last search result element,
|
||||
// loop it back to the search input
|
||||
this.focusSearchBox();
|
||||
} else {
|
||||
// the user is focusing backward from the search input,
|
||||
// loop it back to the results' close button
|
||||
const closeBtn: HTMLButtonElement =
|
||||
this.searchResultsView.nativeElement.querySelector('button.close-button');
|
||||
closeBtn.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event.target', '$event.button', '$event.ctrlKey', '$event.metaKey', '$event.altKey'])
|
||||
onClick(eventTarget: HTMLElement, button: number, ctrlKey: boolean, metaKey: boolean, altKey: boolean): boolean {
|
||||
// Hide the search results if we clicked outside both the "search box" and the "search results"
|
||||
if (
|
||||
this.showSearchResults &&
|
||||
!this.searchElements.some(element => element.nativeElement.contains(eventTarget))
|
||||
) {
|
||||
this.hideSearchResults();
|
||||
}
|
||||
|
||||
// Show developer source view if the footer is clicked while holding the meta and alt keys
|
||||
if (eventTarget.tagName === 'FOOTER' && metaKey && altKey) {
|
||||
this.dtOn = !this.dtOn;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deal with anchor clicks; climb DOM tree until anchor found (or null)
|
||||
let target: HTMLElement | null = eventTarget;
|
||||
while (target && !(target instanceof HTMLAnchorElement)) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
return this.locationService.handleAnchorClick(target, button, ctrlKey, metaKey);
|
||||
}
|
||||
|
||||
// Allow the click to pass through
|
||||
return true;
|
||||
}
|
||||
|
||||
setPageId(id: string) {
|
||||
// Special case the home page
|
||||
this.pageId = (id === 'index') ? 'home' : id.replace('/', '-');
|
||||
}
|
||||
|
||||
setFolderId(id: string) {
|
||||
// Special case the home page
|
||||
this.folderId = (id === 'index') ? 'home' : id.split('/', 1)[0];
|
||||
}
|
||||
|
||||
notificationDismissed() {
|
||||
this.notificationAnimating = true;
|
||||
// this should be kept in sync with the animation durations in:
|
||||
// - aio/src/styles/2-modules/_notification.scss
|
||||
// - aio/src/app/layout/notification/notification.component.ts
|
||||
setTimeout(() => this.notificationAnimating = false, 250);
|
||||
this.updateHostClasses();
|
||||
}
|
||||
|
||||
updateHostClasses() {
|
||||
const mode = `mode-${this.deployment.mode}`;
|
||||
const sideNavOpen = `sidenav-${this.sidenav.opened ? 'open' : 'closed'}`;
|
||||
const pageClass = `page-${this.pageId}`;
|
||||
const folderClass = `folder-${this.folderId}`;
|
||||
const viewClasses = Object.keys(this.currentNodes).map(view => `view-${view}`).join(' ');
|
||||
const notificationClass = `aio-notification-${this.notification.showNotification}`;
|
||||
const notificationAnimatingClass = this.notificationAnimating ? 'aio-notification-animating' : '';
|
||||
|
||||
this.hostClasses = [
|
||||
mode,
|
||||
sideNavOpen,
|
||||
pageClass,
|
||||
folderClass,
|
||||
viewClasses,
|
||||
notificationClass,
|
||||
notificationAnimatingClass
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
updateShell() {
|
||||
// Update the SideNav state (if necessary).
|
||||
this.updateSideNav();
|
||||
|
||||
// Update the host classes.
|
||||
this.setPageId(this.currentDocument.id);
|
||||
this.setFolderId(this.currentDocument.id);
|
||||
this.updateHostClasses();
|
||||
}
|
||||
|
||||
updateSideNav() {
|
||||
// Preserve current sidenav open state by default.
|
||||
let openSideNav = this.sidenav.opened;
|
||||
const isSideNavDoc = !!this.currentNodes[sideNavView];
|
||||
|
||||
if (this.isSideNavDoc !== isSideNavDoc) {
|
||||
// View type changed. Is it now a sidenav view (e.g, guide or tutorial)?
|
||||
// Open if changed to a sidenav doc; close if changed to a marketing doc.
|
||||
openSideNav = this.isSideNavDoc = isSideNavDoc;
|
||||
}
|
||||
|
||||
// May be open or closed when wide; always closed when narrow.
|
||||
this.sidenav.toggle(this.dockSideNav && openSideNav);
|
||||
}
|
||||
|
||||
// Dynamically change height of table of contents container
|
||||
@HostListener('window:scroll')
|
||||
onScroll() {
|
||||
if (!this.tocMaxHeightOffset) {
|
||||
// Must wait until `mat-toolbar` is measurable.
|
||||
const el = this.hostElement.nativeElement as Element;
|
||||
const headerEl = el.querySelector('.app-toolbar');
|
||||
const footerEl = el.querySelector('footer');
|
||||
|
||||
if (headerEl && footerEl) {
|
||||
this.tocMaxHeightOffset =
|
||||
headerEl.clientHeight +
|
||||
footerEl.clientHeight +
|
||||
24; // fudge margin
|
||||
}
|
||||
}
|
||||
|
||||
this.tocMaxHeight = (document.body.scrollHeight - window.scrollY - this.tocMaxHeightOffset).toFixed(2);
|
||||
}
|
||||
|
||||
// Restrain scrolling inside an element, when the cursor is over it
|
||||
restrainScrolling(evt: WheelEvent) {
|
||||
const elem = evt.currentTarget as Element;
|
||||
const scrollTop = elem.scrollTop;
|
||||
|
||||
if (evt.deltaY < 0) {
|
||||
// Trying to scroll up: Prevent scrolling if already at the top.
|
||||
if (scrollTop < 1) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
} else {
|
||||
// Trying to scroll down: Prevent scrolling if already at the bottom.
|
||||
const maxScrollTop = elem.scrollHeight - elem.clientHeight;
|
||||
if (maxScrollTop - scrollTop < 1) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Search related methods and handlers
|
||||
|
||||
hideSearchResults() {
|
||||
this.showSearchResults = false;
|
||||
const oldSearch = this.locationService.search();
|
||||
if (oldSearch.search !== undefined) {
|
||||
this.locationService.setSearch('', { ...oldSearch, search: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
focusSearchBox() {
|
||||
if (this.searchBox) {
|
||||
this.searchBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
doSearch(query: string, fromFocus = false) {
|
||||
if (this.showSearchResults && fromFocus) {
|
||||
// the results where already being displayed so there is no
|
||||
// need to perform the search until the input actually changes
|
||||
return;
|
||||
}
|
||||
this.searchResults = this.searchService.search(query);
|
||||
this.showSearchResults = !!query;
|
||||
}
|
||||
|
||||
@HostListener('document:keyup', ['$event.key', '$event.which'])
|
||||
onKeyUp(key: string, keyCode: number) {
|
||||
// forward slash "/"
|
||||
if (key === '/' || keyCode === 191) {
|
||||
this.focusSearchBox();
|
||||
}
|
||||
if (key === 'Escape' || keyCode === 27) {
|
||||
// escape key
|
||||
if (this.showSearchResults) {
|
||||
this.hideSearchResults();
|
||||
this.focusSearchBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ErrorHandler, NgModule } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
|
||||
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
|
||||
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
||||
import { svg } from 'app/shared/security';
|
||||
import { trustedResourceUrl, unwrapResourceUrl } from 'safevalues';
|
||||
|
||||
import { AppComponent } from 'app/app.component';
|
||||
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
|
||||
import { Deployment } from 'app/shared/deployment.service';
|
||||
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
|
||||
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
|
||||
import { AnalyticsService } from 'app/shared/analytics.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { STORAGE_PROVIDERS } from 'app/shared/storage.service';
|
||||
import { NavigationService } from 'app/navigation/navigation.service';
|
||||
import { DocumentService } from 'app/documents/document.service';
|
||||
import { SearchService } from 'app/search/search.service';
|
||||
import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component';
|
||||
import { FooterComponent } from 'app/layout/footer/footer.component';
|
||||
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
|
||||
import { ReportingErrorHandler } from 'app/shared/reporting-error-handler';
|
||||
import { ScrollService } from 'app/shared/scroll.service';
|
||||
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
|
||||
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
|
||||
import { NotificationComponent } from 'app/layout/notification/notification.component';
|
||||
import { TocService } from 'app/shared/toc.service';
|
||||
import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
|
||||
import { WindowToken, windowProvider } from 'app/shared/window';
|
||||
|
||||
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { ThemeToggleComponent } from 'app/shared/theme-picker/theme-toggle.component';
|
||||
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component.
|
||||
/* eslint-disable max-len */
|
||||
export const svgIconProviders = [
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
name: 'close',
|
||||
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
name: 'insert_comment',
|
||||
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
name: 'keyboard_arrow_right',
|
||||
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
name: 'menu',
|
||||
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
|
||||
// Namespace: logos
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
namespace: 'logos',
|
||||
name: 'github',
|
||||
svgSource:
|
||||
svg`<svg focusable="false" viewBox="0 0 51.8 50.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.9,0.2C11.8,0.2,0.3,11.7,0.3,25.8c0,11.3,7.3,20.9,17.5,24.3c1.3,0.2,1.7-0.6,1.7-1.2c0-0.6,0-2.6,0-4.8
|
||||
c-7.1,1.5-8.6-3-8.6-3c-1.2-3-2.8-3.7-2.8-3.7c-2.3-1.6,0.2-1.6,0.2-1.6c2.6,0.2,3.9,2.6,3.9,2.6c2.3,3.9,6,2.8,7.5,2.1
|
||||
c0.2-1.7,0.9-2.8,1.6-3.4c-5.7-0.6-11.7-2.8-11.7-12.7c0-2.8,1-5.1,2.6-6.9c-0.3-0.7-1.1-3.3,0.3-6.8c0,0,2.1-0.7,7,2.6
|
||||
c2-0.6,4.2-0.9,6.4-0.9c2.2,0,4.4,0.3,6.4,0.9c4.9-3.3,7-2.6,7-2.6c1.4,3.5,0.5,6.1,0.3,6.8c1.6,1.8,2.6,4.1,2.6,6.9
|
||||
c0,9.8-6,12-11.7,12.6c0.9,0.8,1.7,2.4,1.7,4.7c0,3.4,0,6.2,0,7c0,0.7,0.5,1.5,1.8,1.2c10.2-3.4,17.5-13,17.5-24.3
|
||||
C51.5,11.7,40.1,0.2,25.9,0.2z" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
namespace: 'logos',
|
||||
name: 'twitter',
|
||||
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: SVG_ICONS,
|
||||
useValue: {
|
||||
namespace: 'logos',
|
||||
name: 'youtube',
|
||||
svgSource: svg`<svg focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.58 7.19c-.23-.86-.91-1.54-1.77-1.77C18.25 5 12 5 12 5s-6.25 0-7.81.42c-.86.23-1.54.91-1.77 1.77
|
||||
C2 8.75 2 12 2 12s0 3.25.42 4.81c.23.86.91 1.54 1.77 1.77C5.75 19 12 19 12 19s6.25 0 7.81-.42
|
||||
c.86-.23 1.54-.91 1.77-1.77C22 15.25 22 12 22 12s0-3.25-.42-4.81zM10 15V9l5.2 3-5.2 3z" />
|
||||
</svg>`,
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
];
|
||||
/* eslint-enable max-len */
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule.withConfig({disableAnimations: AppComponent.reducedMotion}),
|
||||
CustomElementsModule,
|
||||
HttpClientModule,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
MatSidenavModule,
|
||||
MatToolbarModule,
|
||||
SharedModule,
|
||||
ServiceWorkerModule.register(
|
||||
// Make sure service worker is loaded with a TrustedScriptURL
|
||||
unwrapResourceUrl(trustedResourceUrl`/ngsw-worker.js`) as string,
|
||||
{enabled: environment.production}),
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
CookiesPopupComponent,
|
||||
DocViewerComponent,
|
||||
DtComponent,
|
||||
FooterComponent,
|
||||
ModeBannerComponent,
|
||||
NavMenuComponent,
|
||||
NavItemComponent,
|
||||
SearchBoxComponent,
|
||||
NotificationComponent,
|
||||
TopMenuComponent,
|
||||
ThemeToggleComponent,
|
||||
],
|
||||
providers: [
|
||||
AnalyticsService,
|
||||
Deployment,
|
||||
DocumentService,
|
||||
{ provide: ErrorHandler, useClass: ReportingErrorHandler },
|
||||
Logger,
|
||||
Location,
|
||||
{ provide: LocationStrategy, useClass: PathLocationStrategy },
|
||||
LocationService,
|
||||
{ provide: MatIconRegistry, useClass: CustomIconRegistry },
|
||||
NavigationService,
|
||||
ScrollService,
|
||||
ScrollSpyService,
|
||||
SearchService,
|
||||
STORAGE_PROVIDERS,
|
||||
svgIconProviders,
|
||||
TocService,
|
||||
{ provide: CurrentDateToken, useFactory: currentDateProvider },
|
||||
{ provide: WindowToken, useFactory: windowProvider },
|
||||
],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -1,116 +0,0 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { AnnouncementBarComponent } from './announcement-bar.component';
|
||||
|
||||
const today = new Date();
|
||||
const lastWeek = changeDays(today, -7);
|
||||
const yesterday = changeDays(today, -1);
|
||||
const tomorrow = changeDays(today, 1);
|
||||
const nextWeek = changeDays(today, 7);
|
||||
|
||||
describe('AnnouncementBarComponent', () => {
|
||||
|
||||
let element: HTMLElement;
|
||||
let fixture: ComponentFixture<AnnouncementBarComponent>;
|
||||
let component: AnnouncementBarComponent;
|
||||
let httpMock: HttpTestingController;
|
||||
let mockLogger: MockLogger;
|
||||
|
||||
beforeEach(() => {
|
||||
const injector = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
declarations: [AnnouncementBarComponent],
|
||||
providers: [{ provide: Logger, useClass: MockLogger }]
|
||||
});
|
||||
|
||||
httpMock = injector.inject(HttpTestingController);
|
||||
mockLogger = injector.inject(Logger) as any;
|
||||
fixture = TestBed.createComponent(AnnouncementBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
});
|
||||
|
||||
it('should have no announcement when first created', () => {
|
||||
expect(component.announcement).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should make a single request to the server', () => {
|
||||
component.ngOnInit();
|
||||
httpMock.expectOne('generated/announcements.json');
|
||||
expect().nothing(); // Prevent jasmine from complaining about no expectations.
|
||||
});
|
||||
|
||||
it('should set the announcement to the first "live" one in the list loaded from `announcements.json`', () => {
|
||||
component.ngOnInit();
|
||||
const request = httpMock.expectOne('generated/announcements.json');
|
||||
request.flush([
|
||||
{ startDate: lastWeek, endDate: yesterday, message: 'Test Announcement 0' },
|
||||
{ startDate: tomorrow, endDate: nextWeek, message: 'Test Announcement 1' },
|
||||
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 2' },
|
||||
{ startDate: yesterday, endDate: tomorrow, message: 'Test Announcement 3' }
|
||||
]);
|
||||
expect(component.announcement.message).toEqual('Test Announcement 2');
|
||||
});
|
||||
|
||||
it('should set the announcement to `undefined` if there are no announcements in `announcements.json`', () => {
|
||||
component.ngOnInit();
|
||||
const request = httpMock.expectOne('generated/announcements.json');
|
||||
request.flush([]);
|
||||
expect(component.announcement).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle invalid data in `announcements.json`', () => {
|
||||
component.ngOnInit();
|
||||
const request = httpMock.expectOne('generated/announcements.json');
|
||||
request.flush('some random response');
|
||||
expect(component.announcement).toBeUndefined();
|
||||
expect(mockLogger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json contains invalid data:/);
|
||||
});
|
||||
|
||||
it('should handle a failed request for `announcements.json`', () => {
|
||||
component.ngOnInit();
|
||||
const request = httpMock.expectOne('generated/announcements.json');
|
||||
request.error(new ProgressEvent('404'));
|
||||
expect(component.announcement).toBeUndefined();
|
||||
expect(mockLogger.output.error).toEqual([
|
||||
[jasmine.any(Error)]
|
||||
]);
|
||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json request failed:/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
component.announcement = {
|
||||
imageUrl: 'dummy/image',
|
||||
linkUrl: 'link/to/website',
|
||||
message: 'this is an <b>important</b> message',
|
||||
endDate: '2018-03-01',
|
||||
startDate: '2018-02-01'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display the message as HTML', () => {
|
||||
expect(element.innerHTML).toContain('this is an <b>important</b> message');
|
||||
});
|
||||
|
||||
it('should display an image', () => {
|
||||
expect(element.querySelector('img')?.src).toContain('dummy/image');
|
||||
});
|
||||
|
||||
it('should display a link', () => {
|
||||
expect(element.querySelector('a')?.href).toContain('link/to/website');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function changeDays(initial: Date, days: number) {
|
||||
return (new Date(initial.valueOf()).setDate(initial.getDate() + days));
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
|
||||
const announcementsPath = CONTENT_URL_PREFIX + 'announcements.json';
|
||||
|
||||
export interface Announcement {
|
||||
imageUrl: string;
|
||||
message: string;
|
||||
linkUrl: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the latest live announcement. This is used on the homepage.
|
||||
*
|
||||
* The data for the announcements is kept in `aio/content/marketing/announcements.json`.
|
||||
*
|
||||
* The format for that data file looks like:
|
||||
*
|
||||
* ```
|
||||
* [
|
||||
* {
|
||||
* "startDate": "2018-02-01",
|
||||
* "endDate": "2018-03-01",
|
||||
* "message": "This is an <b>important</b> announcement",
|
||||
* "imageUrl": "url/to/image",
|
||||
* "linkUrl": "url/to/website"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Only one announcement will be shown at any time. This is determined as the first "live"
|
||||
* announcement in the file, where "live" means that its start date is before today, and its
|
||||
* end date is after today.
|
||||
*
|
||||
* **Security Note:**
|
||||
* The `message` field can contain unsanitized HTML but this field should only updated by
|
||||
* verified members of the Angular team.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'aio-announcement-bar',
|
||||
template: `
|
||||
<div class="homepage-container" *ngIf="announcement">
|
||||
<div class="announcement-bar">
|
||||
<img [src]="announcement.imageUrl" alt="">
|
||||
<p [innerHTML]="announcement.message"></p>
|
||||
<a class="button" [href]="announcement.linkUrl">Learn More</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
export class AnnouncementBarComponent implements OnInit {
|
||||
announcement: Announcement;
|
||||
|
||||
constructor(private http: HttpClient, private logger: Logger) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.http.get<Announcement[]>(announcementsPath)
|
||||
.pipe(
|
||||
catchError(error => {
|
||||
this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
|
||||
return [];
|
||||
}),
|
||||
map(announcements => this.findCurrentAnnouncement(announcements)),
|
||||
catchError(error => {
|
||||
this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
|
||||
return [];
|
||||
}),
|
||||
)
|
||||
.subscribe(announcement => this.announcement = announcement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first date in the list that is "live" now
|
||||
*/
|
||||
private findCurrentAnnouncement(announcements: Announcement[]) {
|
||||
return announcements
|
||||
.filter(announcement => new Date(announcement.startDate).valueOf() < Date.now())
|
||||
.filter(announcement => new Date(announcement.endDate).valueOf() > Date.now())
|
||||
[0];
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { AnnouncementBarComponent } from './announcement-bar.component';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, SharedModule, HttpClientModule ],
|
||||
declarations: [ AnnouncementBarComponent ]
|
||||
})
|
||||
export class AnnouncementBarModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = AnnouncementBarComponent;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<div class="l-flex-wrap api-filter">
|
||||
|
||||
<aio-select (change)="setType($event.option)"
|
||||
[options]="types"
|
||||
[selected]="type"
|
||||
[showSymbol]="true"
|
||||
label="Type:">
|
||||
</aio-select>
|
||||
|
||||
<aio-select (change)="setStatus($event.option)"
|
||||
[options]="statuses"
|
||||
[selected]="status"
|
||||
[disabled]="type.value === 'package'"
|
||||
label="Status:">
|
||||
</aio-select>
|
||||
|
||||
<div class="form-search">
|
||||
<input #filter placeholder="Filter" (input)="setQuery($any($event.target).value)" aria-label="Filter Search">
|
||||
<em class="material-icons">search</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="api-list-container">
|
||||
<div *ngFor="let section of filteredSections | async" >
|
||||
<h2 *ngIf="section.items"><a [href]="section.path" [class.deprecated-api-item]="section.deprecated">{{section.title}}</a></h2>
|
||||
<ul class="api-list" *ngIf="section.items && section.items.length">
|
||||
<ng-container *ngFor="let item of section.items">
|
||||
<li class="api-item">
|
||||
<a [href]="item.path" [class.deprecated-api-item]="item.stability === 'deprecated'">
|
||||
<span class="symbol {{item.docType}}"></span>
|
||||
{{item.title}}
|
||||
</a>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1,317 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ApiListComponent } from './api-list.component';
|
||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { ApiListModule } from './api-list.module';
|
||||
|
||||
describe('ApiListComponent', () => {
|
||||
let component: ApiListComponent;
|
||||
let fixture: ComponentFixture<ApiListComponent>;
|
||||
let sections: ApiSection[];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ ApiListModule ],
|
||||
providers: [
|
||||
{ provide: ApiService, useClass: TestApiService },
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: LocationService, useClass: TestLocationService }
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(ApiListComponent);
|
||||
component = fixture.componentInstance;
|
||||
sections = getApiSections();
|
||||
});
|
||||
|
||||
/**
|
||||
* Expectation Utility: Assert that filteredSections has the expected result for this test.
|
||||
*
|
||||
* @param itemTest - return true if the item passes the match test
|
||||
*
|
||||
* Subscribes to `filteredSections` and performs expectation within subscription callback.
|
||||
*/
|
||||
function expectFilteredResult(label: string, itemTest: (item: ApiItem) => boolean) {
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
filtered = filtered.filter(section => section.items);
|
||||
expect(filtered.length).withContext('expected something').toBeGreaterThan(0);
|
||||
expect(filtered.every(section => section.items?.every(itemTest))).withContext(label).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
describe('#filteredSections', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return all complete sections when no criteria', () => {
|
||||
let filtered: ApiSection[]|undefined;
|
||||
component.filteredSections.subscribe(f => filtered = f);
|
||||
expect(filtered).toEqual(sections);
|
||||
});
|
||||
|
||||
it('item.show should be true for all queried items', () => {
|
||||
component.setQuery('class');
|
||||
expectFilteredResult('query: class', item => /class/.test(item.name));
|
||||
});
|
||||
|
||||
it('items should be an array for every item in section when query matches section name', () => {
|
||||
component.setQuery('core');
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
filtered = filtered.filter(section => Array.isArray(section.items));
|
||||
expect(filtered.length).withContext('only one section').toBe(1);
|
||||
expect(filtered[0].name).toBe('core');
|
||||
expect(filtered[0].items).toEqual(sections.find(section => section.name === 'core')?.items as ApiItem[]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('section.items', () => {
|
||||
it('should null if there are no matching items and the section itself does not match', () => {
|
||||
component.setQuery('core');
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
const commonSection = filtered.find(section => section.name === 'common');
|
||||
expect(commonSection?.items).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be visible if they have the selected stability status', () => {
|
||||
component.setStatus({value: 'stable', title: 'Stable'});
|
||||
expectFilteredResult('status: stable', item => item.stability === 'stable');
|
||||
});
|
||||
|
||||
it('should be visible if they have the selected security status', () => {
|
||||
component.setStatus({value: 'security-risk', title: 'Security Risk'});
|
||||
expectFilteredResult('status: security-risk', item => item.securityRisk);
|
||||
});
|
||||
|
||||
it('should be visible if they have the selected developer preview status', () => {
|
||||
component.setStatus({value: 'developer-preview', title: 'Developer Preview'});
|
||||
expectFilteredResult('status: developer-preview', item => item.developerPreview);
|
||||
});
|
||||
|
||||
it('should be visible if they match the selected API type', () => {
|
||||
component.setType({value: 'class', title: 'Class'});
|
||||
expectFilteredResult('type: class', item => item.docType === 'class');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have no sections and no items visible when there is no match', () => {
|
||||
component.setQuery('fizbuzz');
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
expect(filtered.some(section => !!section.items)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial criteria from location', () => {
|
||||
let locationService: TestLocationService;
|
||||
|
||||
beforeEach(() => {
|
||||
locationService = fixture.componentRef.injector.get<any>(LocationService);
|
||||
});
|
||||
|
||||
function expectOneItem(name: string, section: string, type: string, stability: string) {
|
||||
fixture.detectChanges();
|
||||
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
filtered = filtered.filter(s => s.items);
|
||||
expect(filtered.length).withContext('sections').toBe(1);
|
||||
expect(filtered[0].name).withContext('section name').toBe(section);
|
||||
const items = filtered[0].items as ApiItem[];
|
||||
expect(items.length).withContext('items').toBe(1);
|
||||
|
||||
const item = items[0];
|
||||
const badItem = 'Wrong item: ' + JSON.stringify(item, null, 2);
|
||||
|
||||
expect(item.docType).withContext(badItem).toBe(type);
|
||||
expect(item.stability).withContext(badItem).toBe(stability);
|
||||
expect(item.name).withContext(badItem).toBe(name);
|
||||
});
|
||||
}
|
||||
|
||||
it('should filter as expected for ?query', () => {
|
||||
locationService.query = {query: '_3'};
|
||||
expectOneItem('class_3', 'core', 'class', 'experimental');
|
||||
});
|
||||
|
||||
it('should filter as expected for ?status', () => {
|
||||
locationService.query = {status: 'deprecated'};
|
||||
expectOneItem('function_1', 'core', 'function', 'deprecated');
|
||||
});
|
||||
|
||||
it('should filter as expected when status is security-risk', () => {
|
||||
locationService.query = {status: 'security-risk'};
|
||||
fixture.detectChanges();
|
||||
expectFilteredResult('security-risk', item => item.securityRisk);
|
||||
});
|
||||
|
||||
it('should filter as expected when status is developer-preview', () => {
|
||||
locationService.query = {status: 'developer-preview'};
|
||||
fixture.detectChanges();
|
||||
expectFilteredResult('developer-preview', item => item.developerPreview);
|
||||
});
|
||||
|
||||
it('should filter as expected for ?type', () => {
|
||||
locationService.query = {type: 'pipe'};
|
||||
expectOneItem('pipe_1', 'common', 'pipe', 'stable');
|
||||
});
|
||||
|
||||
it('should filter as expected for ?query&status&type', () => {
|
||||
locationService.query = {
|
||||
query: 's_1',
|
||||
status: 'experimental',
|
||||
type: 'class'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
expectOneItem('class_1', 'common', 'class', 'experimental');
|
||||
});
|
||||
|
||||
it('should ignore case for ?query&status&type', () => {
|
||||
locationService.query = {
|
||||
query: 'S_1',
|
||||
status: 'ExperiMental',
|
||||
type: 'CLASS'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
expectOneItem('class_1', 'common', 'class', 'experimental');
|
||||
});
|
||||
});
|
||||
|
||||
describe('location path after criteria change', () => {
|
||||
let locationService: TestLocationService;
|
||||
|
||||
beforeEach(() => {
|
||||
locationService = fixture.componentRef.injector.get<any>(LocationService);
|
||||
});
|
||||
|
||||
it('should have query', () => {
|
||||
component.setQuery('foo');
|
||||
|
||||
// `setSearch` 2nd param is a query/search params object
|
||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||
expect(search.query).toBe('foo');
|
||||
});
|
||||
|
||||
it('should keep last of multiple query settings (in lowercase)', () => {
|
||||
component.setQuery('foo');
|
||||
component.setQuery('fooBar');
|
||||
|
||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||
expect(search.query).toBe('foobar');
|
||||
});
|
||||
|
||||
it('should have query, status, and type', () => {
|
||||
component.setQuery('foo');
|
||||
component.setStatus({value: 'stable', title: 'Stable'});
|
||||
component.setType({value: 'class', title: 'Class'});
|
||||
|
||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||
expect(search.query).toBe('foo');
|
||||
expect(search.status).toBe('stable');
|
||||
expect(search.type).toBe('class');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
////// Helpers ////////
|
||||
|
||||
class TestLocationService {
|
||||
query: {[index: string]: string } = {};
|
||||
setSearch = jasmine.createSpy('setSearch');
|
||||
search() { return this.query; }
|
||||
}
|
||||
|
||||
class TestApiService {
|
||||
sectionsSubject = new BehaviorSubject(getApiSections());
|
||||
sections = this.sectionsSubject.asObservable();
|
||||
}
|
||||
|
||||
const apiSections: ApiSection[] = [
|
||||
{
|
||||
name: 'common',
|
||||
title: 'common',
|
||||
path: 'api/common',
|
||||
deprecated: false,
|
||||
items: [
|
||||
{
|
||||
name: 'class_1',
|
||||
title: 'Class 1',
|
||||
path: 'api/common/class_1',
|
||||
docType: 'class',
|
||||
stability: 'experimental',
|
||||
securityRisk: false,
|
||||
developerPreview: false
|
||||
},
|
||||
{
|
||||
name: 'class_2',
|
||||
title: 'Class 2',
|
||||
path: 'api/common/class_2',
|
||||
docType: 'class',
|
||||
stability: 'stable',
|
||||
securityRisk: false,
|
||||
developerPreview: false
|
||||
},
|
||||
{
|
||||
name: 'directive_1',
|
||||
title: 'Directive 1',
|
||||
path: 'api/common/directive_1',
|
||||
docType: 'directive',
|
||||
stability: 'stable',
|
||||
securityRisk: true,
|
||||
developerPreview: false
|
||||
},
|
||||
{
|
||||
name: 'pipe_1',
|
||||
title: 'Pipe 1',
|
||||
path: 'api/common/pipe_1',
|
||||
docType: 'pipe',
|
||||
stability: 'stable',
|
||||
securityRisk: true,
|
||||
developerPreview: false
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'core',
|
||||
title: 'core',
|
||||
path: 'api/core',
|
||||
deprecated: false,
|
||||
items: [
|
||||
{
|
||||
name: 'class_3',
|
||||
title: 'Class 3',
|
||||
path: 'api/core/class_3',
|
||||
docType: 'class',
|
||||
stability: 'experimental',
|
||||
securityRisk: false,
|
||||
developerPreview: false
|
||||
},
|
||||
{
|
||||
name: 'function_1',
|
||||
title: 'Function 1',
|
||||
path: 'api/core/function 1',
|
||||
docType: 'function',
|
||||
stability: 'deprecated',
|
||||
securityRisk: true,
|
||||
developerPreview: false
|
||||
},
|
||||
{
|
||||
name: 'const_1',
|
||||
title: 'Const 1',
|
||||
path: 'api/core/const_1',
|
||||
docType: 'const',
|
||||
stability: 'stable',
|
||||
securityRisk: false,
|
||||
developerPreview: true
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function getApiSections() { return apiSections; }
|
||||
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
* API List & Filter Component
|
||||
*
|
||||
* A page that displays a formatted list of the public Angular API entities.
|
||||
* Clicking on a list item triggers navigation to the corresponding API entity document.
|
||||
* Can add/remove API entity links based on filter settings.
|
||||
*/
|
||||
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { combineLatest, Observable, ReplaySubject } from 'rxjs';
|
||||
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||
|
||||
import { Option } from 'app/shared/select/select.component';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
class SearchCriteria {
|
||||
query = '';
|
||||
status = 'all';
|
||||
type = 'all';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-api-list',
|
||||
templateUrl: './api-list.component.html',
|
||||
})
|
||||
export class ApiListComponent implements OnInit {
|
||||
|
||||
filteredSections: Observable<ApiSection[]>;
|
||||
|
||||
showStatusMenu = false;
|
||||
showTypeMenu = false;
|
||||
|
||||
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
|
||||
private searchCriteria = new SearchCriteria();
|
||||
|
||||
status: Option;
|
||||
type: Option;
|
||||
|
||||
// API types
|
||||
types: Option[] = [
|
||||
{ value: 'all', title: 'All' },
|
||||
{ value: 'class', title: 'Class' },
|
||||
{ value: 'const', title: 'Const'},
|
||||
{ value: 'decorator', title: 'Decorator' },
|
||||
{ value: 'directive', title: 'Directive' },
|
||||
{ value: 'element', title: 'Element'},
|
||||
{ value: 'block', title: 'Block'},
|
||||
{ value: 'enum', title: 'Enum' },
|
||||
{ value: 'function', title: 'Function' },
|
||||
{ value: 'interface', title: 'Interface' },
|
||||
{ value: 'package', title: 'Package'},
|
||||
{ value: 'pipe', title: 'Pipe'},
|
||||
{ value: 'ngmodule', title: 'NgModule'},
|
||||
{ value: 'type-alias', title: 'Type alias' },
|
||||
];
|
||||
|
||||
statuses: Option[] = [
|
||||
{ value: 'all', title: 'All' },
|
||||
{ value: 'stable', title: 'Stable'},
|
||||
{ value: 'developer-preview', title: 'Developer Preview'},
|
||||
{ value: 'deprecated', title: 'Deprecated' },
|
||||
{ value: 'security-risk', title: 'Security Risk' }
|
||||
];
|
||||
|
||||
@ViewChild('filter', { static: true }) queryEl: ElementRef;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private locationService: LocationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.filteredSections =
|
||||
combineLatest([
|
||||
this.apiService.sections,
|
||||
this.criteriaSubject,
|
||||
]).pipe(
|
||||
map( results => ({ sections: results[0], criteria: results[1]})),
|
||||
map( results => (
|
||||
results.sections
|
||||
.map(section => ({ ...section, items: this.filterSection(section, results.criteria) }))
|
||||
))
|
||||
);
|
||||
|
||||
this.initializeSearchCriteria();
|
||||
}
|
||||
|
||||
// TODO: may need to debounce as the original did
|
||||
// although there shouldn't be any perf consequences if we don't
|
||||
setQuery(query: string) {
|
||||
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
|
||||
}
|
||||
|
||||
setStatus(status: Option) {
|
||||
this.toggleStatusMenu();
|
||||
this.status = status;
|
||||
this.setSearchCriteria({status: status.value});
|
||||
}
|
||||
|
||||
setType(type: Option) {
|
||||
this.toggleTypeMenu();
|
||||
this.type = type;
|
||||
this.setSearchCriteria({type: type.value});
|
||||
}
|
||||
|
||||
toggleStatusMenu() {
|
||||
this.showStatusMenu = !this.showStatusMenu;
|
||||
}
|
||||
|
||||
toggleTypeMenu() {
|
||||
this.showTypeMenu = !this.showTypeMenu;
|
||||
}
|
||||
|
||||
//////// Private //////////
|
||||
|
||||
private filterSection(section: ApiSection, { query, status, type }: SearchCriteria) {
|
||||
const sectionNameMatches = !query || section.name.indexOf(query) !== -1;
|
||||
|
||||
const matchesQuery = (item: ApiItem) =>
|
||||
sectionNameMatches || item.name.indexOf(query) !== -1;
|
||||
const matchesStatus = (item: ApiItem) =>
|
||||
status === 'all' ||
|
||||
status === item.stability ||
|
||||
(status === 'security-risk' && item.securityRisk) ||
|
||||
(status === 'developer-preview' && item.developerPreview);
|
||||
const matchesType = (item: ApiItem) =>
|
||||
type === 'all' || type === item.docType;
|
||||
|
||||
const items: ApiItem[] = (section.items || []).filter(item =>
|
||||
matchesType(item) && matchesStatus(item) && matchesQuery(item));
|
||||
|
||||
// If there are no items we still return an empty array if the section name matches and the type is 'package'
|
||||
return items.length ? items : (sectionNameMatches && type === 'package') ? [] : null;
|
||||
}
|
||||
|
||||
// Get initial search criteria from URL search params
|
||||
private initializeSearchCriteria() {
|
||||
const {query, status, type} = this.locationService.search();
|
||||
|
||||
const q = (query || '').toLowerCase();
|
||||
// Hack: can't bind to query because input cursor always forced to end-of-line.
|
||||
this.queryEl.nativeElement.value = q;
|
||||
|
||||
this.status = this.statuses.find(x => x.value === status) || this.statuses[0];
|
||||
this.type = this.types.find(x => x.value === type) || this.types[0];
|
||||
|
||||
this.searchCriteria = {
|
||||
query: q,
|
||||
status: this.status.value,
|
||||
type: this.type.value
|
||||
};
|
||||
|
||||
this.criteriaSubject.next(this.searchCriteria);
|
||||
}
|
||||
|
||||
private setLocationSearch() {
|
||||
const {query, status, type} = this.searchCriteria;
|
||||
const params = {
|
||||
query: query ? query : undefined,
|
||||
status: status !== 'all' ? status : undefined,
|
||||
type: type !== 'all' ? type : undefined
|
||||
};
|
||||
|
||||
this.locationService.setSearch('API Search', params);
|
||||
}
|
||||
|
||||
private setSearchCriteria(criteria: Partial<SearchCriteria>) {
|
||||
this.criteriaSubject.next(Object.assign(this.searchCriteria, criteria));
|
||||
this.setLocationSearch();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { ApiListComponent } from './api-list.component';
|
||||
import { ApiService } from './api.service';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, SharedModule, HttpClientModule ],
|
||||
declarations: [ ApiListComponent ],
|
||||
providers: [ ApiService ]
|
||||
})
|
||||
export class ApiListModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = ApiListComponent;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
describe('ApiService', () => {
|
||||
|
||||
let injector: Injector;
|
||||
let service: ApiService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
ApiService,
|
||||
{ provide: Logger, useClass: TestLogger }
|
||||
]
|
||||
});
|
||||
|
||||
service = injector.get<ApiService>(ApiService);
|
||||
httpMock = injector.get(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it('should not immediately connect to the server', () => {
|
||||
httpMock.expectNone({});
|
||||
expect().nothing(); // Prevent jasmine from complaining about no expectations.
|
||||
});
|
||||
|
||||
it('subscribers should be completed/unsubscribed when service destroyed', () => {
|
||||
let completed = false;
|
||||
|
||||
service.sections.subscribe({complete: () => completed = true});
|
||||
|
||||
service.ngOnDestroy();
|
||||
expect(completed).toBe(true);
|
||||
|
||||
// Stop `httpMock.verify()` from complaining.
|
||||
httpMock.expectOne({});
|
||||
});
|
||||
|
||||
describe('#sections', () => {
|
||||
|
||||
it('first subscriber should fetch sections', done => {
|
||||
const data = [
|
||||
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
|
||||
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
|
||||
];
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
expect(sections).toEqual(data);
|
||||
done();
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(data);
|
||||
});
|
||||
|
||||
it('second subscriber should get previous sections and NOT trigger refetch', done => {
|
||||
const data = [
|
||||
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
|
||||
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
|
||||
];
|
||||
let subscriptions = 0;
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
subscriptions++;
|
||||
expect(sections).toEqual(data);
|
||||
});
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
subscriptions++;
|
||||
expect(sections).toEqual(data);
|
||||
expect(subscriptions).toBe(2);
|
||||
done();
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchSections', () => {
|
||||
|
||||
it('should connect to the server w/ expected URL', () => {
|
||||
service.fetchSections();
|
||||
httpMock.expectOne('generated/docs/api/api-list.json');
|
||||
expect().nothing(); // Prevent jasmine from complaining about no expectations.
|
||||
});
|
||||
|
||||
it('should refresh the #sections observable w/ new content on second call', () => {
|
||||
|
||||
let call = 0;
|
||||
|
||||
let data = [
|
||||
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
|
||||
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
|
||||
];
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
// called twice during this test
|
||||
// (1) during subscribe
|
||||
// (2) after refresh
|
||||
expect(sections).withContext('call ' + call++).toEqual(data);
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(data);
|
||||
|
||||
// refresh/refetch
|
||||
data = [{name: 'c', title: 'C', path: '', items: [], deprecated: false}];
|
||||
service.fetchSections();
|
||||
httpMock.expectOne({}).flush(data);
|
||||
|
||||
expect(call).withContext('should be called twice').toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestLogger {
|
||||
log = jasmine.createSpy('log');
|
||||
error = jasmine.createSpy('error');
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { ReplaySubject, Subject } from 'rxjs';
|
||||
import { takeUntil, tap } from 'rxjs/operators';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { DOC_CONTENT_URL_PREFIX } from 'app/documents/document.service';
|
||||
|
||||
export interface ApiItem {
|
||||
name: string;
|
||||
title: string;
|
||||
path: string;
|
||||
docType: string;
|
||||
stability: string;
|
||||
securityRisk: boolean;
|
||||
developerPreview: boolean;
|
||||
}
|
||||
|
||||
export interface ApiSection {
|
||||
path: string;
|
||||
name: string;
|
||||
title: string;
|
||||
deprecated: boolean;
|
||||
items: ApiItem[]|null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiService implements OnDestroy {
|
||||
|
||||
private apiBase = DOC_CONTENT_URL_PREFIX + 'api/';
|
||||
private apiListJsonDefault = 'api-list.json';
|
||||
private firstTime = true;
|
||||
private onDestroy = new Subject<void>();
|
||||
private sectionsSubject = new ReplaySubject<ApiSection[]>(1);
|
||||
private _sections = this.sectionsSubject.pipe(takeUntil(this.onDestroy));
|
||||
|
||||
/**
|
||||
* Return a cached observable of API sections from a JSON file.
|
||||
* API sections is an array of Angular top modules and metadata about their API documents (items).
|
||||
*/
|
||||
get sections() {
|
||||
|
||||
if (this.firstTime) {
|
||||
this.firstTime = false;
|
||||
this.fetchSections(); // TODO: get URL for fetchSections by configuration?
|
||||
|
||||
// makes sectionsSubject hot; subscribe ensures stays alive (always refCount > 0);
|
||||
this._sections.subscribe(sections => this.logger.log(`ApiService got API ${sections.length} section(s)`));
|
||||
}
|
||||
|
||||
return this._sections.pipe(tap(sections => {
|
||||
sections.forEach(section => {
|
||||
section.deprecated = !!section.items &&
|
||||
section.items.every(item => item.stability === 'deprecated');
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
constructor(private http: HttpClient, private logger: Logger) { }
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch API sections from a JSON file.
|
||||
* API sections is an array of Angular top modules and metadata about their API documents (items).
|
||||
* Updates `sections` observable
|
||||
*
|
||||
* @param [src] - Name of the api list JSON file
|
||||
*/
|
||||
fetchSections(src?: string) {
|
||||
// TODO: get URL by configuration?
|
||||
const url = this.apiBase + (src || this.apiListJsonDefault);
|
||||
this.http.get<ApiSection[]>(url)
|
||||
.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
tap(() => this.logger.log(`Got API sections from ${url}`)),
|
||||
)
|
||||
.subscribe({
|
||||
next: sections => this.sectionsSubject.next(sections),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
// TODO: handle error
|
||||
this.logger.error(err);
|
||||
throw err; // rethrow for now.
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CodeExampleComponent } from './code-example.component';
|
||||
import { CodeExampleModule } from './code-example.module';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
|
||||
describe('CodeExampleComponent', () => {
|
||||
let hostComponent: HostComponent;
|
||||
let codeExampleComponent: CodeExampleComponent;
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ CodeExampleModule ],
|
||||
declarations: [
|
||||
HostComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
hostComponent = fixture.componentInstance;
|
||||
codeExampleComponent = hostComponent.codeExampleComponent;
|
||||
});
|
||||
|
||||
it('should be able to capture the code snippet provided in content', () => {
|
||||
expect(codeExampleComponent.aioCode.code.toString().trim()).toBe('const foo = "bar";');
|
||||
});
|
||||
|
||||
it('should clean-up the projected code snippet once captured', () => {
|
||||
expect(codeExampleComponent.content.nativeElement.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('should change aio-code classes based on header presence', () => {
|
||||
expect(codeExampleComponent.header).toBe('Great Example');
|
||||
expect(fixture.nativeElement.querySelector('header')).toBeTruthy();
|
||||
|
||||
const aioCodeEl = fixture.nativeElement.querySelector('aio-code');
|
||||
|
||||
expect(aioCodeEl).toHaveClass('headed-code');
|
||||
expect(aioCodeEl).not.toHaveClass('simple-code');
|
||||
|
||||
codeExampleComponent.header = '';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(codeExampleComponent.header).toBe('');
|
||||
expect(fixture.nativeElement.querySelector('header')).toBeFalsy();
|
||||
expect(aioCodeEl).not.toHaveClass('headed-code');
|
||||
expect(aioCodeEl).toHaveClass('simple-code');
|
||||
});
|
||||
|
||||
it('should set avoidFile class if path has .avoid.', () => {
|
||||
const codeExampleComponentElement: HTMLElement =
|
||||
fixture.nativeElement.querySelector('code-example');
|
||||
|
||||
expect(codeExampleComponent.path).toBe('code-path');
|
||||
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(true);
|
||||
|
||||
codeExampleComponent.path = 'code-path.avoid.';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce hidecopy', () => {
|
||||
expect(codeExampleComponent.hidecopy).toBe(false);
|
||||
|
||||
hostComponent.hidecopy = true;
|
||||
fixture.detectChanges();
|
||||
expect(codeExampleComponent.hidecopy).toBe(true);
|
||||
|
||||
hostComponent.hidecopy = 'false';
|
||||
fixture.detectChanges();
|
||||
expect(codeExampleComponent.hidecopy).toBe(false);
|
||||
|
||||
hostComponent.hidecopy = 'true';
|
||||
fixture.detectChanges();
|
||||
expect(codeExampleComponent.hidecopy).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: 'aio-host-comp',
|
||||
template: `
|
||||
<code-example [header]="header" [path]="path" [hidecopy]="hidecopy">
|
||||
{{code}}
|
||||
</code-example>
|
||||
`
|
||||
})
|
||||
class HostComponent {
|
||||
code = 'const foo = "bar";';
|
||||
header = 'Great Example';
|
||||
path = 'code-path';
|
||||
hidecopy: boolean | string = false;
|
||||
|
||||
@ViewChild(CodeExampleComponent, {static: true}) codeExampleComponent: CodeExampleComponent;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
import { Component, HostBinding, ElementRef, ViewChild, Input, AfterViewInit } from '@angular/core';
|
||||
import { fromInnerHTML } from 'app/shared/security';
|
||||
import { CodeComponent } from './code.component';
|
||||
|
||||
/**
|
||||
* An embeddable code block that displays nicely formatted code.
|
||||
* Example usage:
|
||||
*
|
||||
* ```
|
||||
* <code-example language="ts" linenums="2" class="special" header="Do Stuff">
|
||||
* // a code block
|
||||
* console.log('do stuff');
|
||||
* </code-example>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'code-example',
|
||||
template: `
|
||||
<!-- Content projection is used to get the content HTML provided to this component -->
|
||||
<div #content style="display: none"><ng-content></ng-content></div>
|
||||
|
||||
<header *ngIf="header">{{header}}</header>
|
||||
|
||||
<aio-code [class.headed-code]="!!this.header"
|
||||
[class.simple-code]="!this.header"
|
||||
[language]="language"
|
||||
[linenums]="linenums"
|
||||
[path]="path"
|
||||
[region]="region"
|
||||
[hideCopy]="hidecopy"
|
||||
[header]="header">
|
||||
</aio-code>
|
||||
`,
|
||||
})
|
||||
export class CodeExampleComponent implements AfterViewInit {
|
||||
classes: { 'headed-code': boolean, 'simple-code': boolean };
|
||||
|
||||
@Input() language: string;
|
||||
|
||||
@Input() linenums: string;
|
||||
|
||||
@Input() region: string;
|
||||
|
||||
@Input() header: string;
|
||||
|
||||
@Input()
|
||||
set path(path: string) {
|
||||
this._path = path;
|
||||
this.isAvoid = this.path.indexOf('.avoid.') !== -1;
|
||||
}
|
||||
get path(): string { return this._path; }
|
||||
private _path = '';
|
||||
|
||||
@Input()
|
||||
set hidecopy(hidecopy: boolean) {
|
||||
// Coerce the boolean value.
|
||||
this._hidecopy = hidecopy != null && `${hidecopy}` !== 'false';
|
||||
}
|
||||
get hidecopy(): boolean { return this._hidecopy; }
|
||||
private _hidecopy: boolean;
|
||||
|
||||
/* eslint-disable-next-line @angular-eslint/no-input-rename */
|
||||
@Input('hide-copy')
|
||||
set hyphenatedHideCopy(hidecopy: boolean) {
|
||||
this.hidecopy = hidecopy;
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line @angular-eslint/no-input-rename */
|
||||
@Input('hideCopy')
|
||||
set capitalizedHideCopy(hidecopy: boolean) {
|
||||
this.hidecopy = hidecopy;
|
||||
}
|
||||
|
||||
@HostBinding('class.avoidFile') isAvoid = false;
|
||||
|
||||
@ViewChild('content', { static: true }) content: ElementRef<HTMLDivElement>;
|
||||
|
||||
@ViewChild(CodeComponent, { static: true }) aioCode: CodeComponent;
|
||||
|
||||
ngAfterViewInit() {
|
||||
const contentElem = this.content.nativeElement;
|
||||
this.aioCode.code = fromInnerHTML(contentElem);
|
||||
contentElem.textContent = ''; // Remove DOM nodes that are no longer needed.
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CodeExampleComponent } from './code-example.component';
|
||||
import { CodeModule } from './code.module';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, CodeModule ],
|
||||
declarations: [ CodeExampleComponent ],
|
||||
exports: [ CodeExampleComponent ]
|
||||
})
|
||||
export class CodeExampleModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = CodeExampleComponent;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { CodeTabsComponent } from './code-tabs.component';
|
||||
import { CodeTabsModule } from './code-tabs.module';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
|
||||
describe('CodeTabsComponent', () => {
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
let hostComponent: HostComponent;
|
||||
let codeTabsComponent: CodeTabsComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ HostComponent ],
|
||||
imports: [ CodeTabsModule, NoopAnimationsModule ],
|
||||
schemas: [ NO_ERRORS_SCHEMA ],
|
||||
providers: [
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
hostComponent = fixture.componentInstance;
|
||||
codeTabsComponent = hostComponent.codeTabsComponent;
|
||||
});
|
||||
|
||||
it('should get correct tab info', () => {
|
||||
const tabs = codeTabsComponent.tabs;
|
||||
expect(tabs.length).toBe(2);
|
||||
|
||||
// First code pane expectations
|
||||
expect(tabs[0].class).toBe('class-A');
|
||||
expect(tabs[0].language).toBe('language-A');
|
||||
expect(tabs[0].linenums).toBe('linenums-A');
|
||||
expect(tabs[0].path).toBe('path-A');
|
||||
expect(tabs[0].region).toBe('region-A');
|
||||
expect(tabs[0].header).toBe('header-A');
|
||||
expect(tabs[0].code.toString().trim()).toBe('Code example 1');
|
||||
|
||||
// Second code pane expectations
|
||||
expect(tabs[1].class).toBe('class-B');
|
||||
expect(tabs[1].language).toBe('language-B');
|
||||
expect(tabs[1].linenums).withContext('Default linenums should have been used')
|
||||
.toBe('default-linenums');
|
||||
expect(tabs[1].path).toBe('path-B');
|
||||
expect(tabs[1].region).toBe('region-B');
|
||||
expect(tabs[1].header).toBe('header-B');
|
||||
expect(tabs[1].code.toString().trim()).toBe('Code example 2');
|
||||
});
|
||||
|
||||
it('should create the right number of tabs with the right labels and classes', () => {
|
||||
const matTabs = fixture.nativeElement.querySelectorAll('.mdc-tab__text-label');
|
||||
expect(matTabs.length).toBe(2);
|
||||
|
||||
expect(matTabs[0].textContent.trim()).toBe('header-A');
|
||||
expect(matTabs[0].querySelector('.class-A')).toBeTruthy();
|
||||
|
||||
expect(matTabs[1].textContent.trim()).toBe('header-B');
|
||||
expect(matTabs[1].querySelector('.class-B')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show the first tab with the right code', () => {
|
||||
const codeContent = fixture.nativeElement.querySelector('aio-code').textContent;
|
||||
expect(codeContent.indexOf('Code example 1') !== -1).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clean-up the projected tabs content once captured', () => {
|
||||
expect(codeTabsComponent.content.nativeElement.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: 'aio-host-comp',
|
||||
template: `
|
||||
<code-tabs linenums="default-linenums">
|
||||
<code-pane class="class-A"
|
||||
language="language-A"
|
||||
linenums="linenums-A"
|
||||
path="path-A"
|
||||
region="region-A"
|
||||
header="header-A">
|
||||
Code example 1
|
||||
</code-pane>
|
||||
<code-pane class="class-B"
|
||||
language="language-B"
|
||||
path="path-B"
|
||||
region="region-B"
|
||||
header="header-B">
|
||||
Code example 2
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
`
|
||||
})
|
||||
class HostComponent {
|
||||
@ViewChild(CodeTabsComponent, {static: true}) codeTabsComponent: CodeTabsComponent;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
import { AfterViewInit, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { fromInnerHTML } from 'app/shared/security';
|
||||
import { CodeComponent } from './code.component';
|
||||
|
||||
export interface TabInfo {
|
||||
class: string;
|
||||
code: TrustedHTML;
|
||||
path: string;
|
||||
region: string;
|
||||
|
||||
header?: string;
|
||||
language?: string;
|
||||
linenums?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a set of tab group of code snippets.
|
||||
*
|
||||
* The innerHTML of the `<code-tabs>` component should contain `<code-pane>` elements.
|
||||
* Each `<code-pane>` has the same interface as the embedded `<code-example>` component.
|
||||
* The optional `linenums` attribute is the default `linenums` for each code pane.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'code-tabs',
|
||||
template: `
|
||||
<!-- Use content projection so that the provided HTML's code-panes can be split into tabs -->
|
||||
<div #content style="display: none"><ng-content></ng-content></div>
|
||||
|
||||
<mat-card>
|
||||
<mat-tab-group class="code-tab-group" [disableRipple]="true">
|
||||
<mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
||||
<ng-template mat-tab-label>
|
||||
<span class="{{ tab.class }}">{{ tab.header }}</span>
|
||||
</ng-template>
|
||||
<aio-code class="{{ tab.class }}"
|
||||
[language]="tab.language"
|
||||
[linenums]="tab.linenums"
|
||||
[path]="tab.path"
|
||||
[region]="tab.region"
|
||||
[header]="tab.header">
|
||||
</aio-code>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-card>
|
||||
`,
|
||||
})
|
||||
export class CodeTabsComponent implements OnInit, AfterViewInit {
|
||||
tabs: TabInfo[];
|
||||
|
||||
@Input() linenums: string | undefined;
|
||||
|
||||
@ViewChild('content', { static: true }) content: ElementRef<HTMLDivElement>;
|
||||
|
||||
@ViewChildren(CodeComponent) codeComponents: QueryList<CodeComponent>;
|
||||
|
||||
ngOnInit() {
|
||||
this.tabs = [];
|
||||
const contentElem = this.content.nativeElement;
|
||||
const codeExamples = Array.from(contentElem.querySelectorAll('code-pane'));
|
||||
|
||||
// Remove DOM nodes that are no longer needed.
|
||||
contentElem.textContent = '';
|
||||
|
||||
for (const tabContent of codeExamples) {
|
||||
this.tabs.push(this.getTabInfo(tabContent));
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.codeComponents.toArray().forEach((codeComponent, i) => {
|
||||
codeComponent.code = this.tabs[i].code;
|
||||
});
|
||||
}
|
||||
|
||||
/** Gets the extracted TabInfo data from the provided code-pane element. */
|
||||
private getTabInfo(tabContent: Element): TabInfo {
|
||||
return {
|
||||
class: tabContent.getAttribute('class') || '',
|
||||
code: fromInnerHTML(tabContent),
|
||||
path: tabContent.getAttribute('path') || '',
|
||||
region: tabContent.getAttribute('region') || '',
|
||||
|
||||
header: tabContent.getAttribute('header') || undefined,
|
||||
language: tabContent.getAttribute('language') || undefined,
|
||||
linenums: tabContent.getAttribute('linenums') || this.linenums,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CodeTabsComponent } from './code-tabs.component';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { CodeModule } from './code.module';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, MatCardModule, MatTabsModule, CodeModule ],
|
||||
declarations: [ CodeTabsComponent ],
|
||||
exports: [ CodeTabsComponent ]
|
||||
})
|
||||
export class CodeTabsModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = CodeTabsComponent;
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
import { Component, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
|
||||
import { CodeComponent } from './code.component';
|
||||
import { CodeModule } from './code.module';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { htmlEscape } from 'safevalues';
|
||||
import { MockPrettyPrinter } from 'testing/pretty-printer.service';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
|
||||
const oneLineCode = 'const foo = "bar";';
|
||||
|
||||
const smallMultiLineCode = `<hero-details>
|
||||
<h2>Bah Dah Bing</h2>
|
||||
<hero-team>
|
||||
<h3>NYC Team</h3>
|
||||
</hero-team>
|
||||
</hero-details>`;
|
||||
|
||||
const bigMultiLineCode = `${smallMultiLineCode}\n${smallMultiLineCode}\n${smallMultiLineCode}`;
|
||||
|
||||
describe('CodeComponent', () => {
|
||||
let hostComponent: HostComponent;
|
||||
let fixture: ComponentFixture<HostComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ NoopAnimationsModule, CodeModule ],
|
||||
declarations: [ HostComponent ],
|
||||
providers: [
|
||||
{ provide: Logger, useClass: TestLogger },
|
||||
{ provide: PrettyPrinter, useClass: MockPrettyPrinter },
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(HostComponent);
|
||||
hostComponent = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('pretty printing', () => {
|
||||
const getFormattedCode = () => fixture.nativeElement.querySelector('code').innerHTML;
|
||||
|
||||
it('should format a one-line code sample without linenums by default', () => {
|
||||
hostComponent.setCode(oneLineCode);
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: false): ${oneLineCode}`);
|
||||
});
|
||||
|
||||
it('should add line numbers to one-line code sample when linenums is `true`', () => {
|
||||
hostComponent.setCode(oneLineCode);
|
||||
hostComponent.linenums = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
|
||||
});
|
||||
|
||||
it('should add line numbers to one-line code sample when linenums is `\'true\'`', () => {
|
||||
hostComponent.setCode(oneLineCode);
|
||||
hostComponent.linenums = 'true';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: true): ${oneLineCode}`);
|
||||
});
|
||||
|
||||
it('should format a small multi-line code sample without linenums by default', () => {
|
||||
hostComponent.setCode(smallMultiLineCode);
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: false): ${htmlEscape(smallMultiLineCode)}`);
|
||||
});
|
||||
|
||||
it('should add line numbers to a small multi-line code sample when linenums is `true`', () => {
|
||||
hostComponent.setCode(smallMultiLineCode);
|
||||
hostComponent.linenums = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: true): ${htmlEscape(smallMultiLineCode)}`);
|
||||
});
|
||||
|
||||
it('should add line numbers to a small multi-line code sample when linenums is `\'true\'`', () => {
|
||||
hostComponent.setCode(smallMultiLineCode);
|
||||
hostComponent.linenums = 'true';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: true): ${htmlEscape(smallMultiLineCode)}`);
|
||||
});
|
||||
|
||||
it('should format a big multi-line code without linenums by default', () => {
|
||||
hostComponent.setCode(bigMultiLineCode);
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: false): ${htmlEscape(bigMultiLineCode)}`);
|
||||
});
|
||||
|
||||
it('should add line numbers to a big multi-line code sample when linenums is `true`', () => {
|
||||
hostComponent.setCode(bigMultiLineCode);
|
||||
hostComponent.linenums = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: true): ${htmlEscape(bigMultiLineCode)}`);
|
||||
});
|
||||
|
||||
it('should add line numbers to a big multi-line code sample when linenums is `\'true\'`', () => {
|
||||
hostComponent.setCode(bigMultiLineCode);
|
||||
hostComponent.linenums = 'true';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(
|
||||
`Formatted code (language: auto, linenums: true): ${htmlEscape(bigMultiLineCode)}`);
|
||||
});
|
||||
|
||||
it('should skip prettify if language is `\'none\'`', () => {
|
||||
hostComponent.setCode(bigMultiLineCode);
|
||||
hostComponent.language = 'none';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getFormattedCode()).toBe(htmlEscape(bigMultiLineCode).toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespace handling', () => {
|
||||
it('should remove common indentation from the code before rendering', () => {
|
||||
hostComponent.linenums = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
hostComponent.setCode(`
|
||||
abc
|
||||
let x = text.split('\\n');
|
||||
ghi
|
||||
|
||||
jkl
|
||||
`);
|
||||
const codeContent = fixture.nativeElement.querySelector('code').textContent;
|
||||
expect(codeContent).toEqual(
|
||||
'Formatted code (language: auto, linenums: false): abc\n let x = text.split(\'\\n\');\nghi\n\njkl');
|
||||
});
|
||||
|
||||
it('should trim whitespace from the code before rendering', () => {
|
||||
hostComponent.linenums = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
hostComponent.setCode('\n\n\n' + smallMultiLineCode + '\n\n\n');
|
||||
const codeContent = fixture.nativeElement.querySelector('code').textContent;
|
||||
expect(codeContent).toEqual(codeContent.trim());
|
||||
});
|
||||
|
||||
it('should trim whitespace from code before computing whether to format linenums', () => {
|
||||
hostComponent.setCode('\n\n\n' + oneLineCode + '\n\n\n');
|
||||
|
||||
// `<li>`s are a tell-tale for line numbers
|
||||
const lis = fixture.nativeElement.querySelectorAll('li');
|
||||
expect(lis.length).withContext('should be no linenums').toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error message', () => {
|
||||
|
||||
function getErrorMessage() {
|
||||
const missing: HTMLElement = fixture.nativeElement.querySelector('.code-missing');
|
||||
return missing ? missing.textContent : null;
|
||||
}
|
||||
|
||||
it('should not display "code-missing" class when there is some code', () => {
|
||||
expect(getErrorMessage()).withContext('should not have element with "code-missing" class').toBeNull();
|
||||
});
|
||||
|
||||
it('should display error message when there is no code (after trimming)', () => {
|
||||
hostComponent.setCode(' \n ');
|
||||
expect(getErrorMessage()).toContain('missing');
|
||||
});
|
||||
|
||||
it('should show path and region in missing-code error message', () => {
|
||||
hostComponent.path = 'fizz/buzz/foo.html';
|
||||
hostComponent.region = 'something';
|
||||
fixture.detectChanges();
|
||||
|
||||
hostComponent.setCode(' \n ');
|
||||
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html#something$/);
|
||||
});
|
||||
|
||||
it('should show path only in missing-code error message when no region', () => {
|
||||
hostComponent.path = 'fizz/buzz/foo.html';
|
||||
fixture.detectChanges();
|
||||
|
||||
hostComponent.setCode(' \n ');
|
||||
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html$/);
|
||||
});
|
||||
|
||||
it('should show simple missing-code error message when no path/region', () => {
|
||||
hostComponent.setCode(' \n ');
|
||||
expect(getErrorMessage()).toMatch(/missing.$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copy button', () => {
|
||||
|
||||
function getButton() {
|
||||
const btnDe = fixture.debugElement.query(By.css('button'));
|
||||
return btnDe ? btnDe.nativeElement : null;
|
||||
}
|
||||
|
||||
it('should be hidden if the `hideCopy` input is true', () => {
|
||||
hostComponent.hideCopy = true;
|
||||
fixture.detectChanges();
|
||||
expect(getButton()).toBe(null);
|
||||
});
|
||||
|
||||
it('should have title', () => {
|
||||
expect(getButton().title).toBe('Copy code snippet');
|
||||
});
|
||||
|
||||
it('should have no aria-label by default', () => {
|
||||
expect(getButton().getAttribute('aria-label')).toBe('');
|
||||
});
|
||||
|
||||
it('should have aria-label explaining what is being copied when header passed in', () => {
|
||||
hostComponent.header = 'a/b/c/foo.ts';
|
||||
fixture.detectChanges();
|
||||
expect(getButton().getAttribute('aria-label')).toContain(hostComponent.header);
|
||||
});
|
||||
|
||||
it('should call copier service when clicked', () => {
|
||||
const clipboard = TestBed.inject(Clipboard);
|
||||
const spy = spyOn(clipboard, 'copy');
|
||||
expect(spy.calls.count()).withContext('before click').toBe(0);
|
||||
getButton().click();
|
||||
expect(spy.calls.count()).withContext('after click').toBe(1);
|
||||
});
|
||||
|
||||
it('should copy code text when clicked', () => {
|
||||
const clipboard = TestBed.inject(Clipboard);
|
||||
const spy = spyOn(clipboard, 'copy');
|
||||
getButton().click();
|
||||
expect(spy.calls.argsFor(0)[0]).withContext('after click').toBe(oneLineCode);
|
||||
});
|
||||
|
||||
it('should preserve newlines in the copied code', () => {
|
||||
const clipboard = TestBed.inject(Clipboard);
|
||||
const spy = spyOn(clipboard, 'copy');
|
||||
const expectedCode = smallMultiLineCode.trim();
|
||||
let actualCode;
|
||||
|
||||
hostComponent.setCode(smallMultiLineCode);
|
||||
|
||||
[false, true, 42].forEach(linenums => {
|
||||
hostComponent.linenums = linenums;
|
||||
fixture.detectChanges();
|
||||
getButton().click();
|
||||
actualCode = spy.calls.mostRecent().args[0];
|
||||
|
||||
expect(actualCode).withContext(`when linenums=${linenums}`).toBe(expectedCode);
|
||||
expect(actualCode.match(/\r?\n/g)?.length).toBe(5);
|
||||
|
||||
spy.calls.reset();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a message when copy succeeds', () => {
|
||||
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
|
||||
const clipboard = TestBed.inject(Clipboard);
|
||||
spyOn(snackBar, 'open');
|
||||
spyOn(clipboard, 'copy').and.returnValue(true);
|
||||
getButton().click();
|
||||
expect(snackBar.open).toHaveBeenCalledWith('Code Copied', '', { duration: 800 });
|
||||
});
|
||||
|
||||
it('should display an error when copy fails', () => {
|
||||
const snackBar: MatSnackBar = TestBed.inject(MatSnackBar);
|
||||
const clipboard = TestBed.inject(Clipboard);
|
||||
const logger = TestBed.inject(Logger) as unknown as TestLogger;
|
||||
spyOn(snackBar, 'open');
|
||||
spyOn(clipboard, 'copy').and.returnValue(false);
|
||||
getButton().click();
|
||||
expect(snackBar.open).toHaveBeenCalledWith('Copy failed. Please try again!', '', { duration: 800 });
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(jasmine.any(Error));
|
||||
expect(logger.error.calls.mostRecent().args[0].message).toMatch(/^ERROR copying code to clipboard:/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//// Test helpers ////
|
||||
@Component({
|
||||
selector: 'aio-host-comp',
|
||||
template: `
|
||||
<aio-code [language]="language"
|
||||
[linenums]="linenums" [path]="path" [region]="region"
|
||||
[hideCopy]="hideCopy" [header]="header"></aio-code>
|
||||
`
|
||||
})
|
||||
class HostComponent implements AfterViewInit {
|
||||
hideCopy: boolean;
|
||||
language: string;
|
||||
linenums: boolean | number | string;
|
||||
path: string;
|
||||
region: string;
|
||||
header: string;
|
||||
|
||||
@ViewChild(CodeComponent, {static: false}) codeComponent: CodeComponent;
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.setCode(oneLineCode);
|
||||
}
|
||||
|
||||
/** Changes the displayed code on the code component. */
|
||||
setCode(code: string) {
|
||||
this.codeComponent.code = htmlEscape(code);
|
||||
}
|
||||
}
|
||||
|
||||
class TestLogger {
|
||||
log = jasmine.createSpy('log');
|
||||
error = jasmine.createSpy('error');
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { unwrapHtml } from 'safevalues';
|
||||
import { htmlSafeByReview } from 'safevalues/restricted/reviewed';
|
||||
import { fromOuterHTML } from 'app/shared/security';
|
||||
|
||||
/**
|
||||
* Formatted Code Block
|
||||
*
|
||||
* Pretty renders a code block, used in the docs and API reference by the code-example and
|
||||
* code-tabs embedded components.
|
||||
* It includes a "copy" button that will send the content to the clipboard when clicked
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```
|
||||
* <aio-code
|
||||
* [language]="ts"
|
||||
* [linenums]="true"
|
||||
* [path]="router/src/app/app.module.ts"
|
||||
* [region]="animations-module">
|
||||
* </aio-code>
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Renders code provided through the `updateCode` method.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'aio-code',
|
||||
template: `
|
||||
<pre class="prettyprint lang-{{language}}">
|
||||
<button *ngIf="!hideCopy" class="material-icons copy-button no-print"
|
||||
title="Copy code snippet"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
(click)="doCopy()">
|
||||
<span aria-hidden="true">content_copy</span>
|
||||
</button>
|
||||
<code class="animated fadeIn" #codeContainer></code>
|
||||
</pre>
|
||||
`
|
||||
})
|
||||
export class CodeComponent implements OnChanges {
|
||||
ariaLabel = '';
|
||||
|
||||
/** The code to be copied when clicking the copy button, this should not be HTML encoded */
|
||||
private codeText: string;
|
||||
|
||||
/** Code that should be formatted with current inputs and displayed in the view. */
|
||||
set code(code: TrustedHTML) {
|
||||
this._code = code;
|
||||
|
||||
if (!this._code.toString().trim()) {
|
||||
this.showMissingCodeMessage();
|
||||
} else {
|
||||
this.formatDisplayedCode();
|
||||
}
|
||||
}
|
||||
get code(): TrustedHTML {
|
||||
return this._code;
|
||||
}
|
||||
_code: TrustedHTML;
|
||||
|
||||
/** Whether the copy button should be shown. */
|
||||
@Input() hideCopy: boolean;
|
||||
|
||||
/** Language to render the code (e.g. javascript, typescript). */
|
||||
@Input() language: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether to display line numbers:
|
||||
* - If false: hide
|
||||
* - If true: show
|
||||
* - If number: show but start at that number
|
||||
*/
|
||||
@Input() linenums: boolean | number | string | undefined;
|
||||
|
||||
/** Path to the source of the code. */
|
||||
@Input() path: string;
|
||||
|
||||
/** Region of the source of the code being displayed. */
|
||||
@Input() region: string;
|
||||
|
||||
/** Optional header to be displayed above the code. */
|
||||
@Input()
|
||||
set header(header: string | undefined) {
|
||||
this._header = header;
|
||||
this.ariaLabel = this.header ? `Copy code snippet from ${this.header}` : '';
|
||||
}
|
||||
get header(): string|undefined { return this._header; }
|
||||
private _header: string | undefined;
|
||||
|
||||
@Output() codeFormatted = new EventEmitter<void>();
|
||||
|
||||
/** The element in the template that will display the formatted code. */
|
||||
@ViewChild('codeContainer', { static: true }) codeContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
private snackbar: MatSnackBar,
|
||||
private pretty: PrettyPrinter,
|
||||
private clipboard: Clipboard,
|
||||
private logger: Logger) {}
|
||||
|
||||
ngOnChanges() {
|
||||
// If some inputs have changed and there is code displayed, update the view with the latest
|
||||
// formatted code.
|
||||
if (this.code) {
|
||||
this.formatDisplayedCode();
|
||||
}
|
||||
}
|
||||
|
||||
private formatDisplayedCode() {
|
||||
const linenums = this.getLinenums();
|
||||
const leftAlignedCode = leftAlign(this.code);
|
||||
this.setCodeHtml(leftAlignedCode); // start with unformatted code
|
||||
this.codeText = this.getCodeText(); // store the unformatted code as text (for copying)
|
||||
|
||||
const skipPrettify = of(undefined);
|
||||
const prettifyCode = this.pretty
|
||||
.formatCode(leftAlignedCode, this.language, linenums)
|
||||
.pipe(tap(formattedCode => this.setCodeHtml(formattedCode)));
|
||||
|
||||
if (linenums !== false && this.language === 'none') {
|
||||
this.logger.warn("Using 'linenums' with 'language: none' is currently not supported.");
|
||||
}
|
||||
|
||||
((this.language === 'none' ? skipPrettify : prettifyCode) as Observable<unknown>)
|
||||
.subscribe({
|
||||
next: () => this.codeFormatted.emit(),
|
||||
error: () => { /* ignore failure to format */ },
|
||||
});
|
||||
}
|
||||
|
||||
/** Sets the message showing that the code could not be found. */
|
||||
private showMissingCodeMessage() {
|
||||
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
|
||||
const msg = `The code sample is missing${src ? ` for\n${src}` : '.'}`;
|
||||
const el = document.createElement('p');
|
||||
el.className = 'code-missing';
|
||||
el.textContent = msg;
|
||||
this.setCodeHtml(fromOuterHTML(el));
|
||||
}
|
||||
|
||||
/** Sets the innerHTML of the code container to the provided code string. */
|
||||
private setCodeHtml(formattedCode: TrustedHTML) {
|
||||
// **Security:** Code example content is provided by docs authors and as such its considered to
|
||||
// be safe for innerHTML purposes.
|
||||
this.codeContainer.nativeElement.innerHTML = unwrapHtml(formattedCode);
|
||||
}
|
||||
|
||||
/** Gets the textContent of the displayed code element. */
|
||||
private getCodeText() {
|
||||
// `prettify` may remove newlines, e.g. when `linenums` are on. Retrieve the content of the
|
||||
// container as text, before prettifying it.
|
||||
// We take the textContent because we don't want it to be HTML encoded.
|
||||
return this.codeContainer.nativeElement.textContent;
|
||||
}
|
||||
|
||||
/** Copies the code snippet to the user's clipboard. */
|
||||
doCopy() {
|
||||
const code = this.codeText;
|
||||
const successfullyCopied = this.clipboard.copy(code);
|
||||
|
||||
if (successfullyCopied) {
|
||||
this.logger.log('Copied code to clipboard:', code);
|
||||
this.snackbar.open('Code Copied', '', { duration: 800 });
|
||||
} else {
|
||||
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
|
||||
this.snackbar.open('Copy failed. Please try again!', '', { duration: 800 });
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the calculated value of linenums (boolean/number). */
|
||||
getLinenums() {
|
||||
const linenums =
|
||||
typeof this.linenums === 'boolean' ? this.linenums :
|
||||
this.linenums === 'true' ? true :
|
||||
this.linenums === 'false' ? false :
|
||||
typeof this.linenums === 'string' ? parseInt(this.linenums, 10) :
|
||||
this.linenums;
|
||||
|
||||
return (linenums != null) && !isNaN(linenums as number) && linenums;
|
||||
}
|
||||
}
|
||||
|
||||
function leftAlign(text: TrustedHTML): TrustedHTML {
|
||||
let indent = Number.MAX_VALUE;
|
||||
|
||||
const lines = text.toString().split('\n');
|
||||
lines.forEach(line => {
|
||||
const lineIndent = line.search(/\S/);
|
||||
if (lineIndent !== -1) {
|
||||
indent = Math.min(lineIndent, indent);
|
||||
}
|
||||
});
|
||||
|
||||
return htmlSafeByReview(
|
||||
lines.map(line => line.slice(indent)).join('\n').trim(),
|
||||
'safe manipulation of existing trusted HTML');
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CodeComponent } from './code.component';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { PrettyPrinter } from './pretty-printer.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, MatSnackBarModule ],
|
||||
declarations: [ CodeComponent ],
|
||||
exports: [ CodeComponent ],
|
||||
providers: [ PrettyPrinter ]
|
||||
})
|
||||
export class CodeModule { }
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { htmlSafeByReview } from 'safevalues/restricted/reviewed';
|
||||
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { first, map, share } from 'rxjs/operators';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
||||
type PrettyPrintOne = (code: TrustedHTML, language?: string, linenums?: number|boolean) => string;
|
||||
|
||||
/**
|
||||
* Wrapper around the prettify.js library
|
||||
*/
|
||||
@Injectable()
|
||||
export class PrettyPrinter {
|
||||
|
||||
private prettyPrintOne: Observable<PrettyPrintOne>;
|
||||
|
||||
constructor(private logger: Logger) {
|
||||
this.prettyPrintOne = from(this.getPrettyPrintOne()).pipe(share());
|
||||
}
|
||||
|
||||
private getPrettyPrintOne(): Promise<PrettyPrintOne> {
|
||||
const ppo = (window as any).prettyPrintOne;
|
||||
return ppo ? Promise.resolve(ppo) :
|
||||
// `prettyPrintOne` is not on `window`, which means `prettify.js` has not been loaded yet.
|
||||
// Import it; ad a side-effect it will add `prettyPrintOne` on `window`.
|
||||
import('assets/js/prettify.js' as any)
|
||||
.then(
|
||||
() => (window as any).prettyPrintOne,
|
||||
err => {
|
||||
const msg = `Cannot get prettify.js from server: ${err.message}`;
|
||||
this.logger.error(new Error(msg));
|
||||
// return a pretty print fn that always fails.
|
||||
return () => { throw new Error(msg); };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format code snippet as HTML.
|
||||
*
|
||||
* @param code - the code snippet to format; should already be HTML encoded
|
||||
* @param [language] - The language of the code to render (could be javascript, html, typescript, etc)
|
||||
* @param [linenums] - Whether to display line numbers:
|
||||
* - false: don't display
|
||||
* - true: do display
|
||||
* - number: do display but start at the given number
|
||||
* @returns Observable<string> - Observable of formatted code
|
||||
*/
|
||||
formatCode(code: TrustedHTML, language?: string, linenums?: number|boolean) {
|
||||
return this.prettyPrintOne.pipe(
|
||||
map(ppo => {
|
||||
try {
|
||||
return htmlSafeByReview(
|
||||
ppo(code, language, linenums), 'prettify.js modifies already trusted HTML inline');
|
||||
} catch (err) {
|
||||
const msg = `Could not format code that begins '${code.toString().slice(0, 50)}...'.`;
|
||||
console.error(msg, err);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}),
|
||||
first(), // complete immediately
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Injector } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { ContributorGroup } from './contributors.model';
|
||||
import { ContributorListComponent } from './contributor-list.component';
|
||||
import { ContributorService } from './contributor.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
|
||||
// Testing the component class behaviors, independent of its template
|
||||
// Let e2e tests verify how it displays.
|
||||
describe('ContributorListComponent', () => {
|
||||
|
||||
let component: ContributorListComponent;
|
||||
let injector: Injector;
|
||||
let contributorService: TestContributorService;
|
||||
let locationService: TestLocationService;
|
||||
let contributorGroups: ContributorGroup[];
|
||||
|
||||
beforeEach(() => {
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ContributorListComponent, deps: [ContributorService, LocationService] },
|
||||
{provide: ContributorService, useClass: TestContributorService, deps: [] },
|
||||
{provide: LocationService, useClass: TestLocationService, deps: [] }
|
||||
]
|
||||
});
|
||||
|
||||
locationService = injector.get(LocationService) as unknown as TestLocationService;
|
||||
contributorService = injector.get(ContributorService) as unknown as TestContributorService;
|
||||
contributorGroups = contributorService.testContributors;
|
||||
});
|
||||
|
||||
it('should select the first group when no query string', () => {
|
||||
component = getComponent();
|
||||
expect(component.selectedGroup).toBe(contributorGroups[0]);
|
||||
});
|
||||
|
||||
it('should select the first group when query string w/o "group" property', () => {
|
||||
locationService.searchResult = { foo: 'GDE' };
|
||||
component = getComponent();
|
||||
expect(component.selectedGroup).toBe(contributorGroups[0]);
|
||||
});
|
||||
|
||||
it('should select the first group when query group not found', () => {
|
||||
locationService.searchResult = { group: 'foo' };
|
||||
component = getComponent();
|
||||
expect(component.selectedGroup).toBe(contributorGroups[0]);
|
||||
});
|
||||
|
||||
it('should select the GDE group when query group is "GDE"', () => {
|
||||
locationService.searchResult = { group: 'GDE' };
|
||||
component = getComponent();
|
||||
expect(component.selectedGroup).toBe(contributorGroups[1]);
|
||||
});
|
||||
|
||||
it('should select the GDE group when query group is "gde" (case insensitive)', () => {
|
||||
locationService.searchResult = { group: 'gde' };
|
||||
component = getComponent();
|
||||
expect(component.selectedGroup).toBe(contributorGroups[1]);
|
||||
});
|
||||
|
||||
it('should set the query to the "GDE" group when user selects "GDE"', () => {
|
||||
component = getComponent();
|
||||
component.selectGroup('GDE');
|
||||
expect(locationService.searchResult.group).toBe('GDE');
|
||||
});
|
||||
|
||||
it('should set the query to the first group when user selects unknown name', () => {
|
||||
component = getComponent();
|
||||
component.selectGroup('GDE'); // a legit group that isn't the first
|
||||
|
||||
component.selectGroup('foo'); // not a legit group name
|
||||
expect(locationService.searchResult.group).toBe('Angular');
|
||||
});
|
||||
|
||||
//// Test Helpers ////
|
||||
function getComponent(): ContributorListComponent {
|
||||
const comp = injector.get(ContributorListComponent);
|
||||
comp.ngOnInit();
|
||||
return comp;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
class TestLocationService {
|
||||
searchResult: SearchResult = {};
|
||||
search = jasmine.createSpy('search').and.callFake(() => this.searchResult);
|
||||
setSearch = jasmine.createSpy('setSearch')
|
||||
.and.callFake((_label: string, result: SearchResult) => {
|
||||
this.searchResult = result;
|
||||
});
|
||||
}
|
||||
|
||||
class TestContributorService {
|
||||
testContributors = getTestData();
|
||||
contributors = of(this.testContributors);
|
||||
}
|
||||
|
||||
function getTestData(): ContributorGroup[] {
|
||||
return [
|
||||
// Not interested in the contributors data in these tests
|
||||
{ name: 'Angular', order: 0, contributors: [] },
|
||||
{ name: 'GDE', order: 1, contributors: [] },
|
||||
];
|
||||
}
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ContributorGroup } from './contributors.model';
|
||||
import { ContributorService } from './contributor.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-contributor-list',
|
||||
template: `
|
||||
<div class="flex-center group-buttons">
|
||||
<button *ngFor="let name of groupNames"
|
||||
class="button mat-button filter-button"
|
||||
[class.selected]="name === selectedGroup.name"
|
||||
(click)="selectGroup(name)">{{name}}</button>
|
||||
</div>
|
||||
<section *ngIf="selectedGroup" class="grid-fluid">
|
||||
<div class="contributor-group">
|
||||
<aio-contributor *ngFor="let person of selectedGroup.contributors"
|
||||
[person]="person"></aio-contributor>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class ContributorListComponent implements OnInit {
|
||||
private groups: ContributorGroup[];
|
||||
groupNames: string[];
|
||||
selectedGroup: ContributorGroup;
|
||||
|
||||
constructor(
|
||||
private contributorService: ContributorService,
|
||||
private locationService: LocationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
const groupName = this.locationService.search().group || '';
|
||||
// no need to unsubscribe because `contributors` completes
|
||||
this.contributorService.contributors
|
||||
.subscribe(grps => {
|
||||
this.groups = grps;
|
||||
this.groupNames = grps.map(g => g.name);
|
||||
this.selectGroup(groupName);
|
||||
});
|
||||
}
|
||||
|
||||
selectGroup(name: string) {
|
||||
name = name.toLowerCase();
|
||||
this.selectedGroup = this.groups.find(g => g.name.toLowerCase() === name) || this.groups[0];
|
||||
this.locationService.setSearch('', {group: this.selectedGroup.name});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ContributorListComponent } from './contributor-list.component';
|
||||
import { ContributorService } from './contributor.service';
|
||||
import { ContributorComponent } from './contributor.component';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, MatIconModule ],
|
||||
declarations: [ ContributorListComponent, ContributorComponent ],
|
||||
providers: [ ContributorService ]
|
||||
})
|
||||
export class ContributorListModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = ContributorListComponent;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user