mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 600a8cd59a | |||
| bb57d34110 | |||
| ef94bb6bf6 | |||
| e627c6eaef | |||
| 65c9148c71 | |||
| 0a3edfb543 | |||
| f00336c31f | |||
| 8997260e29 | |||
| 033608e17b | |||
| 926063eb0a | |||
| 187b46a3a0 | |||
| 6447c0eecc | |||
| ed8857e46f | |||
| 3e31f1a34e | |||
| d411ee735a | |||
| d34e3298db | |||
| db35266021 | |||
| 790f4f7c26 | |||
| 5e80e6db1c | |||
| 5597e5d172 | |||
| f8a5b40489 | |||
| 3bd5860c74 | |||
| b975ff1bc2 | |||
| ba686cc43f | |||
| a7e4b99515 | |||
| 02fcd928b2 | |||
| fba7fe175c | |||
| e5e8be4d72 | |||
| 8521188cd1 | |||
| 44a0ea4e2d | |||
| 3eda005680 | |||
| 7234824228 | |||
| 0b95d141ab | |||
| 78b48cc272 | |||
| 1c536250b6 | |||
| 674b6c36b0 | |||
| 35925263bd | |||
| 501c159e05 | |||
| 277540c4fc | |||
| 6d00115bf4 | |||
| ef965ac87c | |||
| 369e96c837 | |||
| 2363e77002 | |||
| 1581988d2e | |||
| 0ecc2d12f1 | |||
| 453e00c9b0 | |||
| 4d722a276f | |||
| 744cb1e561 | |||
| deb9ea07c5 | |||
| 62b87b4551 | |||
| 6c278a9069 | |||
| 5de985a565 | |||
| 85c05514be | |||
| 7aa92fa8fe | |||
| 4b96f370ee | |||
| 9e2c1405d1 | |||
| 9f05732656 | |||
| ed41bfcad7 |
@@ -177,6 +177,9 @@ test:saucelabs --flaky_test_attempts=1
|
||||
|
||||
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
|
||||
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
|
||||
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
|
||||
# for local development.
|
||||
build --flag_alias=fast_adev=//adev:fast_build_mode
|
||||
|
||||
####################################################
|
||||
# User bazel configuration
|
||||
|
||||
@@ -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@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -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@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
# the number of concurrent actions is determined based on the host resources.
|
||||
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
workflow-artifact-name: 'aio'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
|
||||
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'aio'
|
||||
|
||||
@@ -16,6 +16,6 @@ jobs:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
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.
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
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
|
||||
|
||||
+28
-28
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -60,13 +60,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -78,13 +78,13 @@ jobs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000
|
||||
- name: Run CI tests for framework
|
||||
@@ -94,15 +94,15 @@ jobs:
|
||||
runs-on: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules for aio
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with upstream packages
|
||||
@@ -115,11 +115,11 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
@@ -130,15 +130,15 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Run AIO tests with local packages
|
||||
@@ -153,15 +153,15 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules for aio
|
||||
run: yarn install --cwd aio --frozen-lockfile
|
||||
- name: Set the stable branch environment variable
|
||||
@@ -197,13 +197,13 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
labels: ubuntu-latest-4core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
@@ -223,9 +223,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@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
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.
|
||||
@@ -272,9 +272,9 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
|
||||
@@ -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@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/commit-message-based-labels@c83e99a12397014162531ca125c94549db55dd84
|
||||
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@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Feature request triage bot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 14:00 every day
|
||||
- cron: '0 14 * * *'
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
feature_triage:
|
||||
if: github.repository == 'angular/angular'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/feature-request@f89362d698858f9674463e48fde514b097a15838
|
||||
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@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@c83e99a12397014162531ca125c94549db55dd84
|
||||
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@f89362d698858f9674463e48fde514b097a15838
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@c83e99a12397014162531ca125c94549db55dd84
|
||||
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@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
cache-node-modules: true
|
||||
node-module-directories: |
|
||||
./aio/node_modules
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
|
||||
- name: Install node modules in aio
|
||||
run: yarn install --frozen-lockfile --cwd aio
|
||||
- name: Run basic e2e and deployment config tests.
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
|
||||
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@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
branch-prefix: update-cli-help
|
||||
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Generate `events.json`
|
||||
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
|
||||
- name: Create a PR (if necessary)
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@f89362d698858f9674463e48fde514b097a15838
|
||||
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
|
||||
with:
|
||||
branch-prefix: docs-update-events
|
||||
pr-title: 'docs: update events'
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ export const format: FormatConfig = {
|
||||
'packages/examples/**/*.{js,ts}',
|
||||
'packages/misc/**/*.{js,ts}',
|
||||
'packages/private/**/*.{js,ts}',
|
||||
'packages/router/**/*.{js,ts}',
|
||||
'packages/service-worker/**/*.{js,ts}',
|
||||
'packages/upgrade/**/*.{js,ts}',
|
||||
|
||||
@@ -33,7 +34,7 @@ export const format: FormatConfig = {
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
//'**/*.{js,ts}',
|
||||
'**/*.{js,ts}',
|
||||
// TODO: burn down format failures and remove aio and integration exceptions.
|
||||
'!aio/**',
|
||||
'!integration/**',
|
||||
@@ -70,6 +71,7 @@ export const format: FormatConfig = {
|
||||
'!packages/examples/**/*.{js,ts}',
|
||||
'!packages/misc/**/*.{js,ts}',
|
||||
'!packages/private/**/*.{js,ts}',
|
||||
'!packages/router/**/*.{js,ts}',
|
||||
'!packages/service-worker/**/*.{js,ts}',
|
||||
'!packages/upgrade/**/*.{js,ts}',
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"tabs": false,
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"singleQuote": true,
|
||||
"semicolon": true,
|
||||
"quoteProps": "preserve",
|
||||
|
||||
@@ -322,7 +322,9 @@ groups:
|
||||
'aio/content/guide/providers.md',
|
||||
'aio/content/examples/providers/**/{*,.*}',
|
||||
'aio/content/images/guide/providers/**/{*,.*}',
|
||||
'aio/content/guide/signal-queries.md',
|
||||
'aio/content/guide/singleton-services.md',
|
||||
'aio/content/guide/signal-inputs.md',
|
||||
'aio/content/guide/sharing-ngmodules.md',
|
||||
'aio/content/guide/standalone-components.md',
|
||||
'aio/content/guide/standalone-migration.md',
|
||||
|
||||
+149
-112
@@ -1,3 +1,90 @@
|
||||
<a name="17.2.2"></a>
|
||||
# 17.2.2 (2024-02-21)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d34e3298db](https://github.com/angular/angular/commit/d34e3298db2da9d3aa8f8e2c49e1d1aed849e7d2) | fix | image placeholder not removed in OnPush component ([#54515](https://github.com/angular/angular/pull/54515)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6447c0eecc](https://github.com/angular/angular/commit/6447c0eecc15800417bb5bde2d273865c559b7d4) | fix | adding the inert property to the "SCHEMA" array ([#53148](https://github.com/angular/angular/pull/53148)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0a3edfb543](https://github.com/angular/angular/commit/0a3edfb5433e1b67f37d7b3f69a5c77b48eedce6) | fix | correctly detect deferred dependencies across scoped nodes ([#54499](https://github.com/angular/angular/pull/54499)) |
|
||||
| [790f4f7c26](https://github.com/angular/angular/commit/790f4f7c26474af2b1d81a4f3c761047bf920edf) | fix | use correct symbol name for default imported symbols in defer blocks ([#54495](https://github.com/angular/angular/pull/54495)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3bd5860c74](https://github.com/angular/angular/commit/3bd5860c749517c4d35850703c53a768138b7bde) | fix | properly execute content queries for root components ([#54457](https://github.com/angular/angular/pull/54457)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bb57d34110](https://github.com/angular/angular/commit/bb57d34110ea8796f5c5f522b98dadf078e8ad8a) | fix | Fix cf migration regular expression to include underscores ([#54533](https://github.com/angular/angular/pull/54533)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3e31f1a34e](https://github.com/angular/angular/commit/3e31f1a34ee97a802a4032cd9e7816d683a51d5e) | fix | Clear internal transition when navigation finalizes ([#54261](https://github.com/angular/angular/pull/54261)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.1"></a>
|
||||
# 17.2.1 (2024-02-14)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7234824228](https://github.com/angular/angular/commit/7234824228df11249a5ebe01a6dee381be74e02e) | fix | fix broken version detection condition ([#54443](https://github.com/angular/angular/pull/54443)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.0"></a>
|
||||
# 17.2.0 (2024-02-14)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [03c3b3eb79](https://github.com/angular/angular/commit/03c3b3eb79ec061b0031d6ad7ba386d185c87d8d) | feat | add Netlify image loader ([#54311](https://github.com/angular/angular/pull/54311)) |
|
||||
| [f5c520b836](https://github.com/angular/angular/commit/f5c520b836c4545c7043649f28b3a0369c168747) | feat | add placeholder to NgOptimizedImage ([#53783](https://github.com/angular/angular/pull/53783)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [47e6e84101](https://github.com/angular/angular/commit/47e6e841016abfca0c1aa84051d82a04b3027617) | feat | Add a TSConfig option `useTemplatePipeline` ([#54057](https://github.com/angular/angular/pull/54057)) |
|
||||
| [66e940aebf](https://github.com/angular/angular/commit/66e940aebfd5a93944860a4e0dbd14e1072f80f2) | feat | scope selectors in @starting-style ([#53943](https://github.com/angular/angular/pull/53943)) |
|
||||
| [7b4d275f49](https://github.com/angular/angular/commit/7b4d275f494a64c38b61cea7045ba8b6e8447b78) | fix | Fix the template pipeline option ([#54148](https://github.com/angular/angular/pull/54148)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7e861c640e](https://github.com/angular/angular/commit/7e861c640edf90c5f8d4f7e091861d3d98cd49c0) | feat | generate extra imports for component local dependencies in local mode ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [3263df23f2](https://github.com/angular/angular/commit/3263df23f2f4da722ef2c1a1dacfb0866498dd60) | feat | generate global imports in local compilation mode ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [b774e22d8e](https://github.com/angular/angular/commit/b774e22d8e384f43e9cd8f5c55475d06e7f66988) | feat | make it configurable to generate alias reexports ([#53937](https://github.com/angular/angular/pull/53937)) |
|
||||
| [3e1384048e](https://github.com/angular/angular/commit/3e1384048eb76c92532ae19aa2883318121c00e8) | feat | support host directives for local compilation mode ([#53877](https://github.com/angular/angular/pull/53877)) |
|
||||
| [a592904c69](https://github.com/angular/angular/commit/a592904c691844d2c1aed00bd914edabef49f9b1) | fix | allow custom/duplicate decorators for `@Injectable` classes in local compilation mode ([#54139](https://github.com/angular/angular/pull/54139)) |
|
||||
| [4b1d948b36](https://github.com/angular/angular/commit/4b1d948b36285ec6d80dbe93e0b92133f9d4be94) | fix | consider the case of duplicate Angular decorators in local compilation diagnostics ([#54139](https://github.com/angular/angular/pull/54139)) |
|
||||
| [96bcf4fb12](https://github.com/angular/angular/commit/96bcf4fb1208d1f073784a2cde4a03553e905807) | fix | forbid custom/duplicate decorator when option `forbidOrphanComponents` is set ([#54139](https://github.com/angular/angular/pull/54139)) |
|
||||
| [64fa5715c6](https://github.com/angular/angular/commit/64fa5715c696101fba0b4f8623eaec0eadc5b159) | fix | generating extra imports in local compilation mode when cycle is introduced ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [6c8b09468a](https://github.com/angular/angular/commit/6c8b09468a05a80cba3960861f0ab8d3bae80415) | fix | highlight the unresolved element in the @Component.styles array for the error LOCAL_COMPILATION_UNRESOLVED_CONST ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [0970129e20](https://github.com/angular/angular/commit/0970129e20f77dc309f2b4f76f961b310124778c) | fix | show proper error for custom decorators in local compilation mode ([#53983](https://github.com/angular/angular/pull/53983)) |
|
||||
| [f39cb06418](https://github.com/angular/angular/commit/f39cb064183d984254bdf4e41b61d3dc9379738a) | fix | show specific error for unresolved @Directive.exportAs in local compilation mode ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [f3851b5945](https://github.com/angular/angular/commit/f3851b59459a1d9c214ace3db5a716d51c1f93c7) | fix | show specific error for unresolved @HostBinding's argument in local compilation mode ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [39ddd884e8](https://github.com/angular/angular/commit/39ddd884e826cc0be63fd0f7d7de20d642877ef9) | fix | show specific error for unresolved @HostListener's event name in local compilation mode ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [5d633240fd](https://github.com/angular/angular/commit/5d633240fd5927c4318a9240e60c3a30b2333cee) | fix | show the correct message for the error LOCAL_COMPILATION_UNRESOLVED_CONST when an unresolved symbol used for @Component.styles ([#54230](https://github.com/angular/angular/pull/54230)) |
|
||||
| [58b8a232d6](https://github.com/angular/angular/commit/58b8a232d64f5fe3207c90c8145cab36e7e192c2) | fix | support jumping to definitions of signal-based inputs ([#54053](https://github.com/angular/angular/pull/54053)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [702ab28b4c](https://github.com/angular/angular/commit/702ab28b4c07a903c403a20af2ca287348b6afd0) | feat | add support for model inputs ([#54252](https://github.com/angular/angular/pull/54252)) |
|
||||
| [e95ef2cbc6](https://github.com/angular/angular/commit/e95ef2cbc6f850d8fe96218b74cff76cea947674) | feat | expose queries as signals ([#54283](https://github.com/angular/angular/pull/54283)) |
|
||||
| [656bc282e3](https://github.com/angular/angular/commit/656bc282e345c5e37a9189a0a4daa631e02c31bf) | fix | add toString implementation to signals ([#54002](https://github.com/angular/angular/pull/54002)) |
|
||||
| [62b87b4551](https://github.com/angular/angular/commit/62b87b4551d77815f58af152d1921de3733621ba) | fix | do not crash for signal query that does not have any matches ([#54353](https://github.com/angular/angular/pull/54353)) |
|
||||
| [4b96f370ee](https://github.com/angular/angular/commit/4b96f370eea08d2531cc54f65a651f94b504692d) | fix | expose model signal subcribe for type checking purposes ([#54357](https://github.com/angular/angular/pull/54357)) |
|
||||
| [744cb1e561](https://github.com/angular/angular/commit/744cb1e561d9b0cbffd20ad612d5ea9e2cf6b2e7) | fix | return the same children query results if there are no changes ([#54392](https://github.com/angular/angular/pull/54392)) |
|
||||
| [6d00115bf4](https://github.com/angular/angular/commit/6d00115bf45731b80178e11921df6ec1dc89efaf) | fix | show placeholder block on the server with immediate trigger ([#54394](https://github.com/angular/angular/pull/54394)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1c536250b6](https://github.com/angular/angular/commit/1c536250b6112c67a68159de50c4afe0dbc440f8) | fix | Use string body to generate transfer cache key. ([#54379](https://github.com/angular/angular/pull/54379)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.3"></a>
|
||||
# 17.1.3 (2024-02-08)
|
||||
### compiler-cli
|
||||
@@ -21,50 +108,9 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.0-next.1"></a>
|
||||
# 17.2.0-next.1 (2024-01-31)
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dfc6c8d0c7](https://github.com/angular/angular/commit/dfc6c8d0c76380ee7beb6d904261e40857b375ed) | fix | cta clickability issue in adev homepage. ([#52905](https://github.com/angular/angular/pull/52905)) |
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [75aeae42b7](https://github.com/angular/angular/commit/75aeae42b7f512553262d515966f43d11e34d228) | fix | cleanup DOM elements when root view is removed with async animations ([#53033](https://github.com/angular/angular/pull/53033)) |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f5c520b836](https://github.com/angular/angular/commit/f5c520b836c4545c7043649f28b3a0369c168747) | feat | add placeholder to NgOptimizedImage ([#53783](https://github.com/angular/angular/pull/53783)) |
|
||||
| [122213d37d](https://github.com/angular/angular/commit/122213d37d3e73fc0dcfd5a10a2c388dc573b6cf) | fix | The date pipe should return ISO format for week and week-year as intended in the unit test. ([#53879](https://github.com/angular/angular/pull/53879)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [47e6e84101](https://github.com/angular/angular/commit/47e6e841016abfca0c1aa84051d82a04b3027617) | feat | Add a TSConfig option `useTemplatePipeline` ([#54057](https://github.com/angular/angular/pull/54057)) |
|
||||
| [7b4d275f49](https://github.com/angular/angular/commit/7b4d275f494a64c38b61cea7045ba8b6e8447b78) | fix | Fix the template pipeline option ([#54148](https://github.com/angular/angular/pull/54148)) |
|
||||
| [eddf5dae5e](https://github.com/angular/angular/commit/eddf5dae5eb9e1aa3ca4c276c2cb2b897b73a9e0) | fix | Update type check block to fix control flow source mappings ([#53980](https://github.com/angular/angular/pull/53980)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7e861c640e](https://github.com/angular/angular/commit/7e861c640edf90c5f8d4f7e091861d3d98cd49c0) | feat | generate extra imports for component local dependencies in local mode ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [3263df23f2](https://github.com/angular/angular/commit/3263df23f2f4da722ef2c1a1dacfb0866498dd60) | feat | generate global imports in local compilation mode ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [64fa5715c6](https://github.com/angular/angular/commit/64fa5715c696101fba0b4f8623eaec0eadc5b159) | fix | generating extra imports in local compilation mode when cycle is introduced ([#53543](https://github.com/angular/angular/pull/53543)) |
|
||||
| [0970129e20](https://github.com/angular/angular/commit/0970129e20f77dc309f2b4f76f961b310124778c) | fix | show proper error for custom decorators in local compilation mode ([#53983](https://github.com/angular/angular/pull/53983)) |
|
||||
| [58b8a232d6](https://github.com/angular/angular/commit/58b8a232d64f5fe3207c90c8145cab36e7e192c2) | fix | support jumping to definitions of signal-based inputs ([#54053](https://github.com/angular/angular/pull/54053)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [656bc282e3](https://github.com/angular/angular/commit/656bc282e345c5e37a9189a0a4daa631e02c31bf) | fix | add toString implementation to signals ([#54002](https://github.com/angular/angular/pull/54002)) |
|
||||
| [037b79b72e](https://github.com/angular/angular/commit/037b79b72ea18f08b3a74f9ad541bbdca183b1aa) | fix | change defer block fixture default behavior to playthrough ([#54088](https://github.com/angular/angular/pull/54088)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [28ad6fc4ad](https://github.com/angular/angular/commit/28ad6fc4ad518884bb5777fd20e9075d8969b27a) | fix | error in standalone migration when non-array value is used as declarations in TestBed ([#54122](https://github.com/angular/angular/pull/54122)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.2"></a>
|
||||
# 17.1.2 (2024-01-31)
|
||||
###
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ccddacf11d](https://github.com/angular/angular/commit/ccddacf11deaebeda12e1bdb6e93ec401397d352) | fix | cta clickability issue in adev homepage. ([#52905](https://github.com/angular/angular/pull/52905)) |
|
||||
@@ -91,15 +137,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.2.0-next.0"></a>
|
||||
# 17.2.0-next.0 (2024-01-24)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [66e940aebf](https://github.com/angular/angular/commit/66e940aebfd5a93944860a4e0dbd14e1072f80f2) | feat | scope selectors in @starting-style ([#53943](https://github.com/angular/angular/pull/53943)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.1"></a>
|
||||
# 17.1.1 (2024-01-24)
|
||||
### router
|
||||
@@ -435,9 +472,9 @@ Live long and prosper 🖖🏻
|
||||
[Blog post "Angular v17 is now available"](http://goo.gle/angular-v17).
|
||||
|
||||
## Breaking Changes
|
||||
###
|
||||
###
|
||||
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
|
||||
|
||||
|
||||
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
|
||||
### common
|
||||
- the NgSwitch directive now defaults to the === equality operator,
|
||||
@@ -451,30 +488,30 @@ Live long and prosper 🖖🏻
|
||||
- The `mutate` method was removed from the `WritableSignal` interface and completely
|
||||
dropped from the public API surface. As an alternative, please use the `update` method and
|
||||
make immutable changes to the object.
|
||||
|
||||
|
||||
Example before:
|
||||
|
||||
|
||||
```typescript
|
||||
items.mutate(itemsArray => itemsArray.push(newItem));
|
||||
```
|
||||
|
||||
|
||||
Example after:
|
||||
|
||||
|
||||
```typescript
|
||||
items.update(itemsArray => [itemsArray, …newItem]);
|
||||
```
|
||||
- The `mutate` method was removed from the `WritableSignal` interface and completely
|
||||
dropped from the public API surface. As an alternative please use the update method and
|
||||
make immutable changes to the object.
|
||||
|
||||
|
||||
Example before:
|
||||
|
||||
|
||||
```typescript
|
||||
items.mutate(itemsArray => itemsArray.push(newItem));
|
||||
```
|
||||
|
||||
|
||||
Example after:
|
||||
|
||||
|
||||
```typescript
|
||||
items.update(itemsArray => [itemsArray, …newItem]);
|
||||
```
|
||||
@@ -490,7 +527,7 @@ Live long and prosper 🖖🏻
|
||||
`detectChanges` on that component's `ChangeDetectorRef`.
|
||||
### platform-browser
|
||||
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
|
||||
|
||||
|
||||
```ts
|
||||
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
|
||||
...
|
||||
@@ -513,13 +550,13 @@ Live long and prosper 🖖🏻
|
||||
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
|
||||
- The following Router properties have been removed from
|
||||
the public API:
|
||||
|
||||
|
||||
- canceledNavigationResolution
|
||||
- paramsInheritanceStrategy
|
||||
- titleStrategy
|
||||
- urlUpdateStrategy
|
||||
- malformedUriErrorHandler
|
||||
|
||||
|
||||
These should instead be configured through the `provideRouter` or
|
||||
`RouterModule.forRoot` APIs.
|
||||
- The `setupTestingRouter` function has been removed. Use
|
||||
@@ -530,7 +567,7 @@ Live long and prosper 🖖🏻
|
||||
handled in the `UrlSerializer.parse` method.
|
||||
### zone.js
|
||||
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
|
||||
|
||||
|
||||
The proper way to import `zone.js` and `zone.js/testing` is:
|
||||
```js
|
||||
import 'zone.js';
|
||||
@@ -541,7 +578,7 @@ Live long and prosper 🖖🏻
|
||||
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
|
||||
### core
|
||||
- `ChangeDetectorRef.checkNoChanges` is deprecated.
|
||||
|
||||
|
||||
Test code should use `ComponentFixture` instead of `ChangeDetectorRef`.
|
||||
Application code should not call `ChangeDetectorRef.checkNoChanges` directly.
|
||||
- Swapping out the context object for `EmbeddedViewRef`
|
||||
@@ -550,12 +587,12 @@ Live long and prosper 🖖🏻
|
||||
simple assignments in most cases, or `Object.assign` , or alternatively
|
||||
still replace the full object by using a `Proxy` (see `NgTemplateOutlet`
|
||||
as an example).
|
||||
|
||||
|
||||
Also adds a warning if the deprecated
|
||||
- NgProbeToken
|
||||
|
||||
|
||||
The `NgProbeToken` is not used internally since the transition from View Engine to Ivy. The token has no utility and can be removed from applications and libraries.
|
||||
###
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
|
||||
@@ -1169,7 +1206,7 @@ Live long and prosper 🖖🏻
|
||||
[Blog post "Angular v16 is now available"](https://goo.gle/angular-v16).
|
||||
|
||||
## Breaking Changes
|
||||
###
|
||||
###
|
||||
- Angular Compatibility Compiler (ngcc) has been removed and as a result Angular View Engine libraries will no longer work
|
||||
- Deprecated `EventManager` method `addGlobalEventListener` has been removed as it is not used by Ivy.
|
||||
### bazel
|
||||
@@ -1192,14 +1229,14 @@ Live long and prosper 🖖🏻
|
||||
The ideal fix would be to update the code to instead be compatible with
|
||||
`MockPlatformLocation` instead.
|
||||
- If the 'ngTemplateOutletContext' is different from the context, it will result in a compile-time error.
|
||||
|
||||
|
||||
Before the change, the following template was compiling:
|
||||
|
||||
|
||||
```typescript
|
||||
interface MyContext {
|
||||
$implicit: string;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [NgTemplateOutlet],
|
||||
@@ -1217,7 +1254,7 @@ Live long and prosper 🖖🏻
|
||||
}
|
||||
```
|
||||
However, it does not compile now because the 'xxx' property does not exist in 'MyContext', resulting in the error: 'Type '{ $implicit: string; xxx: string; }' is not assignable to type 'MyContext'.'
|
||||
|
||||
|
||||
The solution is either:
|
||||
- add the 'xxx' property to 'MyContext' with the correct type or
|
||||
- add '$any(...)' inside the template to make the error disappear. However, adding '$any(...)' does not correct the error but only preserves the previous behavior of the code.
|
||||
@@ -1236,7 +1273,7 @@ Live long and prosper 🖖🏻
|
||||
value differs from the previous call to `setInput`.
|
||||
- `RendererType2.styles` no longer accepts a nested arrays.
|
||||
- The `APP_ID` token value is no longer randomly generated. If you are bootstrapping multiple application on the same page you will need to set to provide the `APP_ID` yourself.
|
||||
|
||||
|
||||
```ts
|
||||
bootstrapApplication(ComponentA, {
|
||||
providers: [
|
||||
@@ -1247,21 +1284,21 @@ Live long and prosper 🖖🏻
|
||||
```
|
||||
- The `ReflectiveInjector` and related symbols were removed. Please update the code to avoid references to the `ReflectiveInjector` symbol. Use `Injector.create` as a replacement to create an injector instead.
|
||||
- Node.js v14 support has been removed
|
||||
|
||||
|
||||
Node.js v14 is planned to be End-of-Life on 2023-04-30. Angular will stop supporting Node.js v14 in Angular v16. Angular v16 will continue to officially support Node.js versions v16 and v18.
|
||||
### platform-browser
|
||||
- The deprecated `BrowserTransferStateModule` was removed, since it's no longer needed. The `TransferState` class can be injected without providing the module. The `BrowserTransferStateModule` was empty starting from v14 and you can just remove the reference to that module from your applications.
|
||||
### platform-server
|
||||
- Users that are using SSR with JIT mode will now need to add `import to @angular/compiler` before bootstrapping the application.
|
||||
|
||||
|
||||
**NOTE:** this does not effect users using the Angular CLI.
|
||||
- `renderApplication` method no longer accepts a root component as first argument. Instead, provide a bootstrapping function that returns a `Promise<ApplicationRef>`.
|
||||
|
||||
|
||||
Before
|
||||
```ts
|
||||
const output: string = await renderApplication(RootComponent, options);
|
||||
```
|
||||
|
||||
|
||||
Now
|
||||
```ts
|
||||
const bootstrap = () => bootstrapApplication(RootComponent, appConfig);
|
||||
@@ -1296,14 +1333,14 @@ Live long and prosper 🖖🏻
|
||||
## Deprecations
|
||||
### core
|
||||
- `makeStateKey`, `StateKey` and `TransferState` exports have been moved from `@angular/platform-browser` to `@angular/core`. Please update the imports.
|
||||
|
||||
|
||||
```diff
|
||||
- import {makeStateKey, StateKey, TransferState} from '@angular/platform-browser';
|
||||
+ import {makeStateKey, StateKey, TransferState} from '@angular/core';
|
||||
```
|
||||
- `EnvironmentInjector.runInContext` is now deprecated, with
|
||||
`runInInjectionContext` functioning as a direct replacement:
|
||||
|
||||
|
||||
```typescript
|
||||
// Previous method version (deprecated):
|
||||
envInjector.runInContext(fn);
|
||||
@@ -1316,7 +1353,7 @@ Live long and prosper 🖖🏻
|
||||
### platform-browser
|
||||
- `BrowserModule.withServerTransition` has been deprecated. `APP_ID` should be used instead to set the application ID.
|
||||
NB: Unless, you render multiple Angular applications on the same page, setting an application ID is not necessary.
|
||||
|
||||
|
||||
Before:
|
||||
```ts
|
||||
imports: [
|
||||
@@ -1324,7 +1361,7 @@ Live long and prosper 🖖🏻
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
After:
|
||||
```ts
|
||||
imports: [
|
||||
@@ -1336,7 +1373,7 @@ Live long and prosper 🖖🏻
|
||||
- `ApplicationConfig` has moved, please import `ApplicationConfig` from `@angular/core` instead.
|
||||
### platform-server
|
||||
- `PlatformConfig.baseUrl` and `PlatformConfig.useAbsoluteUrl` platform-server config options are deprecated as these were not used.
|
||||
###
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [48aa96ea13](https://github.com/angular/angular/commit/48aa96ea13ebfadf2f6b13516c7702dae740a7be) | refactor | remove Angular Compatibility Compiler (ngcc) ([#49101](https://github.com/angular/angular/pull/49101)) |
|
||||
@@ -1645,11 +1682,11 @@ Alan Agius, Andrew Kushnir, Aristeidis Bampakos, Craig Spence, Doug Parker, Ivá
|
||||
<a name="15.2.0"></a>
|
||||
# 15.2.0 (2023-02-22)
|
||||
## Deprecations
|
||||
###
|
||||
###
|
||||
- Class and `InjectionToken` guards and resolvers are
|
||||
deprecated. Instead, write guards as plain JavaScript functions and
|
||||
inject dependencies with `inject` from `@angular/core`.
|
||||
###
|
||||
###
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [926c35f4ac](https://github.com/angular/angular/commit/926c35f4ac70f5e4d142e545d6d056dd67aac97b) | docs | Deprecate class and InjectionToken and resolvers ([#47924](https://github.com/angular/angular/pull/47924)) |
|
||||
@@ -1814,21 +1851,21 @@ Alan Agius, Alex Rickabaugh, Andrew Scott, Aristeidis Bampakos, Bob Watson, Jens
|
||||
- CanLoad guards in the Router are deprecated. Use CanMatch
|
||||
instead.
|
||||
- router writable properties
|
||||
|
||||
|
||||
The following strategies are meant to be configured by registering the
|
||||
application strategy in DI via the `providers` in the root `NgModule` or
|
||||
`bootstrapApplication`:
|
||||
* `routeReuseStrategy`
|
||||
* `titleStrategy`
|
||||
* `urlHandlingStrategy`
|
||||
|
||||
|
||||
The following options are meant to be configured using the options
|
||||
available in `RouterModule.forRoot` or `provideRouter`.
|
||||
* `onSameUrlNavigation`
|
||||
* `paramsInheritanceStrategy`
|
||||
* `urlUpdateStrategy`
|
||||
* `canceledNavigationResolution`
|
||||
|
||||
|
||||
The following options are available in `RouterModule.forRoot` but not
|
||||
available in `provideRouter`:
|
||||
* `malformedUriErrorHandler` - This was found to not be used anywhere
|
||||
@@ -2023,23 +2060,23 @@ Andrew Kushnir
|
||||
- Keyframes names are now prefixed with the component's "scope name".
|
||||
For example, the following keyframes rule in a component definition,
|
||||
whose "scope name" is host-my-cmp:
|
||||
|
||||
|
||||
@keyframes foo { ... }
|
||||
|
||||
|
||||
will become:
|
||||
|
||||
|
||||
@keyframes host-my-cmp_foo { ... }
|
||||
|
||||
|
||||
Any TypeScript/JavaScript code which relied on the names of keyframes rules
|
||||
will no longer match.
|
||||
|
||||
|
||||
The recommended solutions in this case are to either:
|
||||
- change the component's view encapsulation to the `None` or `ShadowDom`
|
||||
- define keyframes rules in global stylesheets (e.g styles.css)
|
||||
- define keyframes rules programmatically in code.
|
||||
### compiler-cli
|
||||
- Invalid constructors for DI may now report compilation errors
|
||||
|
||||
|
||||
When a class inherits its constructor from a base class, the compiler may now
|
||||
report an error when that constructor cannot be used for DI purposes. This may
|
||||
either be because the base class is missing an Angular decorator such as
|
||||
@@ -2048,7 +2085,7 @@ Andrew Kushnir
|
||||
These situations used to behave unexpectedly at runtime, where the class may be
|
||||
constructed without any of its constructor parameters, so this is now reported
|
||||
as an error during compilation.
|
||||
|
||||
|
||||
Any new errors that may be reported because of this change can be resolved either
|
||||
by decorating the base class from which the constructor is inherited, or by adding
|
||||
an explicit constructor to the class for which the error is reported.
|
||||
@@ -2082,16 +2119,16 @@ Andrew Kushnir
|
||||
### core
|
||||
- - The ability to pass an `NgModule` to the `providedIn` option for
|
||||
`@Injectable` and `InjectionToken` is now deprecated.
|
||||
|
||||
|
||||
`providedIn: NgModule` was intended to be a tree-shakable alternative to
|
||||
NgModule providers. It does not have wide usage, and in most cases is used
|
||||
incorrectly, in circumstances where `providedIn: 'root'` should be
|
||||
preferred. If providers should truly be scoped to a specific NgModule, use
|
||||
`NgModule.providers` instead.
|
||||
|
||||
|
||||
- The ability to set `providedIn: 'any'` for an `@Injectable` or
|
||||
`InjectionToken` is now deprecated.
|
||||
|
||||
|
||||
`providedIn: 'any'` is an option with confusing semantics and is almost
|
||||
never used apart from a handful of esoteric cases internal to the framework.
|
||||
- The bit field signature of `Injector.get()` has been deprecated, in favor of the new options object.
|
||||
@@ -2694,16 +2731,16 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
|
||||
- Keyframes names are now prefixed with the component's "scope name".
|
||||
For example, the following keyframes rule in a component definition,
|
||||
whose "scope name" is host-my-cmp:
|
||||
|
||||
|
||||
@keyframes foo { ... }
|
||||
|
||||
|
||||
will become:
|
||||
|
||||
|
||||
@keyframes host-my-cmp_foo { ... }
|
||||
|
||||
|
||||
Any TypeScript/JavaScript code which relied on the names of keyframes rules
|
||||
will no longer match.
|
||||
|
||||
|
||||
The recommended solutions in this case are to either:
|
||||
- change the component's view encapsulation to the `None` or `ShadowDom`
|
||||
- define keyframes rules in global stylesheets (e.g styles.css)
|
||||
@@ -2713,23 +2750,23 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
|
||||
- Support for Node.js v12 has been removed as it will become EOL on 2022-04-30. Please use Node.js v14.15 or later.
|
||||
- TypeScript versions older than 4.6 are no longer supported.
|
||||
- Forms [email] input coercion
|
||||
|
||||
|
||||
Forms [email] input value will be considered as true if it is defined with any value rather
|
||||
than false and 'false'.
|
||||
- Since Ivy, TestBed doesn't use AOT summaries. The `aotSummaries` fields in TestBed APIs were present, but unused. The fields were deprecated in previous major version and in v14 those fields are removed. The `aotSummaries` fields were completely unused, so you can just drop them from the TestBed APIs usage.
|
||||
### forms
|
||||
- Forms classes accept a generic.
|
||||
|
||||
|
||||
Forms model classes now accept a generic type parameter. Untyped versions of these classes are available to opt-out of the new, stricter behavior.
|
||||
- objects with a length key set to zero will no longer validate as empty.
|
||||
|
||||
|
||||
This is technically a breaking change, since objects with a key `length` and value `0` will no longer validate as empty. This is a very minor change, and any reliance on this behavior is probably a bug anyway.
|
||||
### http
|
||||
- Queries including + will now actually query for + instead of space.
|
||||
Most workarounds involving custom codecs will be unaffected.
|
||||
Possible server-side workarounds will need to be undone.
|
||||
- JSONP will throw an error when headers are set on a reques
|
||||
|
||||
|
||||
JSONP does not support headers being set on requests. Before when
|
||||
a request was sent to a JSONP backend that had headers set the headers
|
||||
were ignored. The JSONP backend will now throw an error if it
|
||||
@@ -2740,7 +2777,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
|
||||
- This change may cause a breaking change in unit tests that are implicitly depending on a specific
|
||||
number and sequence of change detections in order for their assertions to pass.
|
||||
- This may break invalid calls to `TransferState` methods.
|
||||
|
||||
|
||||
This tightens parameter types of `TransferState` usage, and is a minor breaking change which may reveal existing problematic calls.
|
||||
### router
|
||||
- The type of `Route.pathMatch` is now stricter. Places that use
|
||||
@@ -2762,7 +2799,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
|
||||
actually gets set to something completely different. It's set to the
|
||||
current internal `UrlTree` of the Router at the time navigation
|
||||
occurs.
|
||||
|
||||
|
||||
With this change, there is no exact replacement for the old value of
|
||||
`initialUrl` because it was never intended to be exposed.
|
||||
`Router.url` is likely the best replacement for this.
|
||||
@@ -2786,7 +2823,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
|
||||
returned by other guards: only the first value is used.
|
||||
### zone.js
|
||||
- in TaskTrackingZoneSpec track a periodic task until it is cancelled
|
||||
|
||||
|
||||
The breaking change is scoped only to the plugin
|
||||
`zone.js/plugins/task-tracking`. If you used `TaskTrackingZoneSpec` and
|
||||
checked the pending macroTasks e.g. using `(this.ngZone as any)._inner
|
||||
@@ -2795,7 +2832,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
|
||||
previously the `setInterval` macrotask was no longer tracked after its
|
||||
callback was executed for the first time. Now it's tracked until
|
||||
the task is explicitly cancelled, e.g with `clearInterval(id)`.
|
||||
|
||||
|
||||
## Deprecations
|
||||
### common
|
||||
- The `ngModuleFactory` input of the `NgComponentOutlet` directive is deprecated in favor of a newly added `ngModule` input. The `ngModule` input accepts references to the NgModule class directly, without the need to resolve module factory first.
|
||||
|
||||
@@ -64,7 +64,17 @@ load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains")
|
||||
|
||||
nodejs_register_toolchains(
|
||||
name = "nodejs",
|
||||
node_version = "18.13.0",
|
||||
node_repositories = {
|
||||
"18.18.2-darwin_arm64": ("node-v18.18.2-darwin-arm64.tar.gz", "node-v18.18.2-darwin-arm64", "9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553"),
|
||||
"18.18.2-darwin_amd64": ("node-v18.18.2-darwin-x64.tar.gz", "node-v18.18.2-darwin-x64", "5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f"),
|
||||
"18.18.2-linux_arm64": ("node-v18.18.2-linux-arm64.tar.xz", "node-v18.18.2-linux-arm64", "2e630e18548627f61eaf573233da7949dc0a1df5eef3f486fa9820c5f6c121aa"),
|
||||
"18.18.2-linux_ppc64le": ("node-v18.18.2-linux-ppc64le.tar.xz", "node-v18.18.2-linux-ppc64le", "b0adff5cf5938266b711d6c724fb134d802e0dee40b3a3f73d162de1b3d11880"),
|
||||
"18.18.2-linux_s390x": ("node-v18.18.2-linux-s390x.tar.xz", "node-v18.18.2-linux-s390x", "c70ec2074b5e2b42c55bb4b8105418b67bf8a61c500d9376a07430dfcc341fdb"),
|
||||
"18.18.2-linux_amd64": ("node-v18.18.2-linux-x64.tar.xz", "node-v18.18.2-linux-x64", "75aba25ae76999309fc6c598efe56ce53fbfc221381a44a840864276264ab8ac"),
|
||||
"18.18.2-windows_amd64": ("node-v18.18.2-win-x64.zip", "node-v18.18.2-win-x64", "3bb0e51e579a41a22b3bf6cb2f3e79c03801aa17acbe0ca00fc555d1282e7acd"),
|
||||
},
|
||||
# We need at least Node 18.17 due to some transitive dependencies.
|
||||
node_version = "18.18.2",
|
||||
)
|
||||
|
||||
# Download npm dependencies.
|
||||
|
||||
+26
-1
@@ -1,4 +1,5 @@
|
||||
load("//:packages.bzl", "link_packages")
|
||||
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")
|
||||
|
||||
@@ -96,12 +97,36 @@ copy_to_bin(
|
||||
srcs = APPLICATION_FILES,
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "fast_build_mode",
|
||||
build_setting_default = False,
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "fast",
|
||||
flag_values = {
|
||||
":fast_build_mode": "true",
|
||||
},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "full",
|
||||
flag_values = {
|
||||
":fast_build_mode": "false",
|
||||
},
|
||||
)
|
||||
|
||||
config_based_architect_flags = select({
|
||||
":fast": ["--no-prerender"],
|
||||
":full": ["--prerender"],
|
||||
})
|
||||
|
||||
architect(
|
||||
name = "build",
|
||||
args = [
|
||||
"angular-dev:build",
|
||||
"--output-path=build",
|
||||
],
|
||||
] + config_based_architect_flags,
|
||||
chdir = "$(RULEDIR)",
|
||||
data = APPLICATION_DEPS + [
|
||||
":application_files_bin",
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": true,
|
||||
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@
|
||||
&:has(.docs-reference-api-tab) {
|
||||
width: 60%;
|
||||
}
|
||||
&:not(.docs-reference-api-tab) {
|
||||
&:not(:has(.docs-reference-api-tab)) {
|
||||
width: 100%;
|
||||
max-width: var(--page-width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,13 @@ copy_to_directory(
|
||||
"//packages/upgrade:upgrade_docs",
|
||||
"//packages/upgrade/static:upgrade_static_docs",
|
||||
"//packages/upgrade/static/testing:upgrade_static_testing_docs",
|
||||
"//tools/manual_api_docs/blocks:blocks_docs",
|
||||
"//tools/manual_api_docs/elements:elements_docs",
|
||||
],
|
||||
replace_prefixes = {
|
||||
"adev/src/content": "",
|
||||
"packages/**/": "api/",
|
||||
"tools/**/": "api/",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ It also includes troubleshooting information and any recommended manual changes
|
||||
For simple updates, the CLI command [`ng update`](cli/update) is all you need.
|
||||
Without additional arguments, [`ng update`](cli/update) lists the updates that are available to you and provides recommended steps to update your application to the most current version.
|
||||
|
||||
[Angular Versioning and Releases](reference/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based a release's version number.
|
||||
[Angular Versioning and Releases](reference/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based on a release's version number.
|
||||
It also describes supported update paths.
|
||||
|
||||
## Resource summary
|
||||
|
||||
@@ -79,7 +79,7 @@ If you look at the template for the `name` input again, it is fairly similar to
|
||||
|
||||
This form differs from the template-driven version in that it no longer exports any directives. Instead, it uses the `name` getter defined in the component class.
|
||||
|
||||
Notice that the `required` attribute is still present in the template. Although it's not necessary for validation, it should be retained to for accessibility purposes.
|
||||
Notice that the `required` attribute is still present in the template. Although it's not necessary for validation, it should be retained for accessibility purposes.
|
||||
|
||||
## Defining custom validators
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ Just as a form group contains a group of controls, the *profileForm* `FormGroup`
|
||||
</docs-step>
|
||||
|
||||
<docs-step title="Save form data">
|
||||
The `ProfileEditor` component accepts input from the user, but in a real scenario you want to capture the form value and make available for further processing outside the component. The `FormGroup` directive listens for the `submit` event emitted by the `form` element and emits an `ngSubmit` event that you can bind to a callback function. Add an `ngSubmit` event listener to the `form` tag with the `onSubmit()` callback method.
|
||||
The `ProfileEditor` component accepts input from the user, but in a real scenario you want to capture the form value and make it available for further processing outside the component. The `FormGroup` directive listens for the `submit` event emitted by the `form` element and emits an `ngSubmit` event that you can bind to a callback function. Add an `ngSubmit` event listener to the `form` tag with the `onSubmit()` callback method.
|
||||
|
||||
<docs-code header="src/app/profile-editor/profile-editor.component.html (submit event)" path="adev/src/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.html" visibleRegion="ng-submit"/>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ This tutorial shows you how to create a template-driven form. The control elemen
|
||||
Template-driven forms use [two-way data binding](guide/templates/two-way-binding) to update the data model in the component as changes are made in the template and vice versa.
|
||||
|
||||
<docs-callout helpful title="Template vs Reactive forms">
|
||||
Angular supports two design approaches for interactive forms. Template-driven forms allow you to use form-specific directives in your Angular template.Reactive forms provide a model-driven approach to building forms.
|
||||
Angular supports two design approaches for interactive forms. Template-driven forms allow you to use form-specific directives in your Angular template. Reactive forms provide a model-driven approach to building forms.
|
||||
|
||||
Template-driven forms are a great choice for small or simple forms, while reactive forms are more scalable and suitable for complex forms. For a comparison of the two approaches, see [Choosing an approach](guide/forms#choosing-an-approach)
|
||||
</docs-callout>
|
||||
@@ -294,7 +294,7 @@ To let form users add a new actor, you will add a **New Actor** button that resp
|
||||
|
||||
<docs-code header="src/app/actor-form/actor-form.component.ts (New Actor method)" path="adev/src/content/examples/forms/src/app/actor-form/actor-form.component.ts" visibleRegion="new-actor"/>
|
||||
|
||||
1. Bind the button's click event to a actor-creation method, `newActor()`.
|
||||
1. Bind the button's click event to an actor-creation method, `newActor()`.
|
||||
|
||||
<docs-code header="src/app/actor-form/actor-form.component.html (New Actor button)" path="adev/src/content/examples/forms/src/app/actor-form/actor-form.component.html" visibleRegion="new-actor-button-no-reset"/>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The localization process includes the following actions.
|
||||
* Format data for a specific locale
|
||||
|
||||
A *locale* identifies a region in which people speak a particular language or language variant.
|
||||
Possible regions includes countries and geographical regions.
|
||||
Possible regions include countries and geographical regions.
|
||||
A locale determines the formatting and parsing of the following details.
|
||||
|
||||
* Measurement units including date and time, numbers, and currencies
|
||||
|
||||
@@ -345,7 +345,7 @@ Note: even though the `width` property may not always be present, a custom loade
|
||||
|
||||
### The `loaderParams` Property
|
||||
|
||||
There is an additional attribute supported by the `NgOptimizedImage` directive, called `loaderParams`, which is specifically designed to support the use of custom loaders. The `loaderParams` attribute take an object with any properties as a value, and does not do anything on its own. The data in `loaderParams` is added to the `ImageLoaderConfig` object passed to your custom loader, and can be used to control the behavior of the loader.
|
||||
There is an additional attribute supported by the `NgOptimizedImage` directive, called `loaderParams`, which is specifically designed to support the use of custom loaders. The `loaderParams` attribute takes an object with any properties as a value, and does not do anything on its own. The data in `loaderParams` is added to the `ImageLoaderConfig` object passed to your custom loader, and can be used to control the behavior of the loader.
|
||||
|
||||
A common use for `loaderParams` is controlling advanced image CDN features.
|
||||
|
||||
@@ -400,3 +400,7 @@ The `ngSrc` attribute was chosen as the trigger for NgOptimizedImage due to tech
|
||||
The [image loaders](#configuring-an-image-loader-for-ngoptimizedimage) provider pattern is designed to be as simple as possible for the common use case of having only a single image CDN used within a component. However, it's still very possible to manage multiple image CDNs using a single provider.
|
||||
|
||||
To do this, we recommend writing a [custom image loader](#custom-loaders) which uses the [`loaderParams` property](#the-loaderparams-property) to pass a flag that specifies which image CDN should be used, and then invokes the appropriate loader based on that flag.
|
||||
|
||||
### Can you add a new built-in loader for my preferred CDN?
|
||||
|
||||
For maintenance reasons, we don't currently plan to support additional built-in loaders in the Angular repository. Instead, we encourage developers to publish any additional image loaders as third-party packages.
|
||||
|
||||
@@ -13,7 +13,7 @@ Angular CLI includes four builders typically used as `build` targets:
|
||||
| `@angular-devkit/build-angular:application` | Builds an application with a client-side bundle, a Node server, and build-time prerendered routes with [esbuild](https://esbuild.github.io/). |
|
||||
| `@angular-devkit/build-angular:ng-packagr` | Builds an Angular library adhering to [Angular Package Format](tools/libraries/angular-package-format). |
|
||||
|
||||
Applications generated by `ng new` use `@angular-devkit/build-angular:browser` by default.
|
||||
Applications generated by `ng new` use `@angular-devkit/build-angular:application` by default.
|
||||
Libraries generated by `ng generate library` use `@angular-devkit/build-angular:ng-packagr` by default.
|
||||
|
||||
You can determine which builder is being used for a particular project by looking up the `build` target for that project.
|
||||
@@ -26,7 +26,7 @@ You can determine which builder is being used for a particular project by lookin
|
||||
"architect": {
|
||||
// `ng build` invokes the Architect target named `build`.
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
…
|
||||
},
|
||||
"serve": { … }
|
||||
@@ -39,7 +39,7 @@ You can determine which builder is being used for a particular project by lookin
|
||||
|
||||
</docs-code>
|
||||
|
||||
This page discusses usage and options of `@angular-devkit/build-angular:browser`.
|
||||
This page discusses usage and options of `@angular-devkit/build-angular:application`.
|
||||
|
||||
## Output directory
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ This form differs from the template-driven version in that it no longer exports
|
||||
Instead, it uses the `name` getter defined in the component class.
|
||||
|
||||
Notice that the `required` attribute is still present in the template.
|
||||
Although it's not necessary for validation, it should be retained to for accessibility purposes.
|
||||
Although it's not necessary for validation, it should be retained for accessibility purposes.
|
||||
|
||||
<a id="custom-validators"></a>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ The localization process includes the following actions.
|
||||
* Format data for a specific locale
|
||||
|
||||
A *locale* identifies a region in which people speak a particular language or language variant.
|
||||
Possible regions includes countries and geographical regions.
|
||||
Possible regions include countries and geographical regions.
|
||||
A locale determines the formatting and parsing of the following details.
|
||||
|
||||
* Measurement units including date and time, numbers, and currencies
|
||||
|
||||
@@ -338,7 +338,7 @@ Note: even though the `width` property may not always be present, a custom loade
|
||||
|
||||
### The `loaderParams` Property
|
||||
|
||||
There is an additional attribute supported by the `NgOptimizedImage` directive, called `loaderParams`, which is specifically designed to support the use of custom loaders. The `loaderParams` attribute take an object with any properties as a value, and does not do anything on its own. The data in `loaderParams` is added to the `ImageLoaderConfig` object passed to your custom loader, and can be used to control the behavior of the loader.
|
||||
There is an additional attribute supported by the `NgOptimizedImage` directive, called `loaderParams`, which is specifically designed to support the use of custom loaders. The `loaderParams` attribute takes an object with any properties as a value, and does not do anything on its own. The data in `loaderParams` is added to the `ImageLoaderConfig` object passed to your custom loader, and can be used to control the behavior of the loader.
|
||||
|
||||
A common use for `loaderParams` is controlling advanced image CDN features.
|
||||
|
||||
@@ -402,6 +402,10 @@ providers: [
|
||||
|
||||
If you use a variable to pass the domain string to the loader, or you're not using a loader, the static analysis will not be able to identify the domain, and no preconnect link will be generated. In this case you should manually add a preconnect link to the document head, as [described above.](#resource-hints).
|
||||
|
||||
### Can you add a new built-in loader for my preferred CDN?
|
||||
|
||||
For maintenance reasons, we don't currently plan to support additional built-in loaders in the Angular repository. Instead, we encourage developers to publish any additional image loaders as third-party packages.
|
||||
|
||||
<!-- links -->
|
||||
|
||||
<!-- external links -->
|
||||
|
||||
@@ -25,6 +25,13 @@ The `<parent-component>` serves as the context for the `<child-component>`.
|
||||
`@Input()` lets a parent component update data in the child component.
|
||||
Conversely, `@Output()` lets the child send data to a parent component.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Signal-based inputs are available in developer preview and may be a better fit for some use cases as an alternative to `@Input`.
|
||||
Learn more in the [signal inputs guide](/guide/signal-inputs).
|
||||
|
||||
</div>
|
||||
|
||||
<a id="input"></a>
|
||||
|
||||
## Sending data to a child component
|
||||
@@ -214,11 +221,11 @@ To make `Input` property as required for a child component while passing values
|
||||
|
||||
<code-example header="src/app/item-details-metadata.component.ts" path="inputs-outputs/src/app/item-details-metadata.component.ts" region="use-input-metadata-required"></code-example>
|
||||
|
||||
Next, in the parent template add the following:
|
||||
Next, in the parent template add the following:
|
||||
|
||||
<code-example header="src/app/app.component.html" path="inputs-outputs/src/app/app.component.html" region="input-parent-metadata"></code-example>
|
||||
|
||||
If required inputs in a child component are not specified in the parent component template will result a compile time error:
|
||||
If required inputs in a child component are not specified in the parent component template will result a compile time error:
|
||||
|
||||
<div class="alert is-helpful">
|
||||
NG8008: Required input item from component ItemDetailMetadataComponent must be specified.
|
||||
|
||||
@@ -137,7 +137,7 @@ To add a form group to this component, take the following steps.
|
||||
|:--- |:--- |
|
||||
| Create a `FormGroup` instance | Create a property in the component class named `profileForm` and set the property to a new form group instance. To initialize the form group, provide the constructor with an object of named keys mapped to their control. <br /> For the profile form, add two form control instances with the names `firstName` and `lastName`. <code-example header="src/app/profile-editor/profile-editor.component.ts (form group)" path="reactive-forms/src/app/profile-editor/profile-editor.component.1.ts" region="formgroup"></code-example> The individual form controls are now collected within a group. A `FormGroup` instance provides its model value as an object reduced from the values of each control in the group. A form group instance has the same properties \(such as `value` and `untouched`\) and methods \(such as `setValue()`\) as a form control instance. |
|
||||
| Associate the `FormGroup` model and view | A form group tracks the status and changes for each of its controls, so if one of the controls changes, the parent control also emits a new status or value change. The model for the group is maintained from its members. After you define the model, you must update the template to reflect the model in the view. <code-example header="src/app/profile-editor/profile-editor.component.html (template form group)" path="reactive-forms/src/app/profile-editor/profile-editor.component.1.html" region="formgroup"></code-example> <div class="alert is-helpful"> **NOTE**: <br /> Just as a form group contains a group of controls, the *profileForm* `FormGroup` is bound to the `form` element with the `FormGroup` directive, creating a communication layer between the model and the form containing the inputs. </div> The `formControlName` input provided by the `FormControlName` directive binds each individual input to the form control defined in `FormGroup`. The form controls communicate with their respective elements. They also communicate changes to the form group instance, which provides the source of truth for the model value. |
|
||||
| Save form data | The `ProfileEditor` component accepts input from the user, but in a real scenario you want to capture the form value and make available for further processing outside the component. The `FormGroup` directive listens for the `submit` event emitted by the `form` element and emits an `ngSubmit` event that you can bind to a callback function. Add an `ngSubmit` event listener to the `form` tag with the `onSubmit()` callback method. <code-example header="src/app/profile-editor/profile-editor.component.html (submit event)" path="reactive-forms/src/app/profile-editor/profile-editor.component.html" region="ng-submit"></code-example> The `onSubmit()` method in the `ProfileEditor` component captures the current value of `profileForm`. Use `EventEmitter` to keep the form encapsulated and to provide the form value outside the component. The following example uses `console.warn` to log a message to the browser console. <code-example header="src/app/profile-editor/profile-editor.component.ts (submit method)" path="reactive-forms/src/app/profile-editor/profile-editor.component.ts" region="on-submit"></code-example> The `submit` event is emitted by the `form` tag using the built-in DOM event. You trigger the event by clicking a button with `submit` type. This lets the user press the **Enter** key to submit the completed form. <br /> Use a `button` element to add a button to the bottom of the form to trigger the form submission. <code-example header="src/app/profile-editor/profile-editor.component.html (submit button)" path="reactive-forms/src/app/profile-editor/profile-editor.component.html" region="submit-button"></code-example> <div class="alert is-helpful"> **NOTE**: <br /> The button in the preceding snippet also has a `disabled` binding attached to it to disable the button when `profileForm` is invalid. You aren't performing any validation yet, so the button is always enabled. Basic form validation is covered in the [Validating form input](#basic-form-validation "Basic form validation.") section. </div> |
|
||||
| Save form data | The `ProfileEditor` component accepts input from the user, but in a real scenario you want to capture the form value and make it available for further processing outside the component. The `FormGroup` directive listens for the `submit` event emitted by the `form` element and emits an `ngSubmit` event that you can bind to a callback function. Add an `ngSubmit` event listener to the `form` tag with the `onSubmit()` callback method. <code-example header="src/app/profile-editor/profile-editor.component.html (submit event)" path="reactive-forms/src/app/profile-editor/profile-editor.component.html" region="ng-submit"></code-example> The `onSubmit()` method in the `ProfileEditor` component captures the current value of `profileForm`. Use `EventEmitter` to keep the form encapsulated and to provide the form value outside the component. The following example uses `console.warn` to log a message to the browser console. <code-example header="src/app/profile-editor/profile-editor.component.ts (submit method)" path="reactive-forms/src/app/profile-editor/profile-editor.component.ts" region="on-submit"></code-example> The `submit` event is emitted by the `form` tag using the built-in DOM event. You trigger the event by clicking a button with `submit` type. This lets the user press the **Enter** key to submit the completed form. <br /> Use a `button` element to add a button to the bottom of the form to trigger the form submission. <code-example header="src/app/profile-editor/profile-editor.component.html (submit button)" path="reactive-forms/src/app/profile-editor/profile-editor.component.html" region="submit-button"></code-example> <div class="alert is-helpful"> **NOTE**: <br /> The button in the preceding snippet also has a `disabled` binding attached to it to disable the button when `profileForm` is invalid. You aren't performing any validation yet, so the button is always enabled. Basic form validation is covered in the [Validating form input](#basic-form-validation "Basic form validation.") section. </div> |
|
||||
| Display the component | To display the `ProfileEditor` component that contains the form, add it to a component template. <code-example header="src/app/app.component.html (profile editor)" path="reactive-forms/src/app/app.component.1.html" region="app-profile-editor"></code-example> `ProfileEditor` lets you manage the form control instances for the `firstName` and `lastName` controls within the form group instance. <div class="lightbox"> <img alt="Profile Editor with labels and inputs for first and last name as well as a submit button" src="generated/images/guide/reactive-forms/profile-editor-1.gif"> </div> |
|
||||
|
||||
<a id="nested-groups"></a>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Signal inputs
|
||||
|
||||
Signal inputs allow values to be bound from parent components.
|
||||
Those values are exposed using a `Signal` and can change during the lifecycle of your component.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Signal inputs are currently in [developer preview](/guide/releases#developer-preview).
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
Angular supports two variants of inputs:
|
||||
|
||||
**Optional inputs**
|
||||
Inputs are optional by default, unless you use `input.required`.
|
||||
You can specify an explicit initial value, or Angular will use `undefined` implicitly.
|
||||
|
||||
**Required inputs**
|
||||
Required inputs always have a value of the given input type.
|
||||
They are declared using the `input.required` function.
|
||||
|
||||
```typescript
|
||||
import {Component, input} from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class MyComp {
|
||||
// optional
|
||||
firstName = input<string>(); // InputSignal<string|undefined>
|
||||
age = input(0); // InputSignal<number>
|
||||
|
||||
// required
|
||||
lastName = input.required<string>(); // InputSignal<string>
|
||||
}
|
||||
```
|
||||
|
||||
An input is automatically recognized by Angular whenever you use the `input` or `input.required` functions as initializer of class members.
|
||||
|
||||
## Aliasing an input
|
||||
|
||||
Angular uses the class member name as the name of the input.
|
||||
You can alias inputs to change their public name to be different.
|
||||
|
||||
```typescript
|
||||
class StudentDirective {
|
||||
age = input(0, {alias: 'studentAge'});
|
||||
}
|
||||
```
|
||||
|
||||
This allows users to bind to your input using `[studentAge]`, while inside your component you can access the input values using `this.age`.
|
||||
|
||||
## Using in templates
|
||||
|
||||
Signal inputs are read-only signals.
|
||||
As with signals declared via `signal()`, you access the current value of the input by calling the input signal.
|
||||
|
||||
```html
|
||||
<p>First name: {{firstName()}}</p>
|
||||
<p>Last name: {{lastName()}}</p>
|
||||
```
|
||||
|
||||
This access to the value is captured in reactive contexts and can notify active consumers, like Angular itself, whenever the input value changes.
|
||||
|
||||
An input signal in practice is a trivial extension of signals that you know from [the signals guide](/guide/signals).
|
||||
|
||||
```typescript
|
||||
export class InputSignal<T> extends Signal<T> { ... }`.
|
||||
```
|
||||
|
||||
## Deriving values
|
||||
|
||||
As with signals, you can derive values from inputs using `computed`.
|
||||
|
||||
```typescript
|
||||
import {Component, input, computed} from '@angular/core';
|
||||
|
||||
@Component({...})
|
||||
export class MyComp {
|
||||
age = input(0);
|
||||
|
||||
// age multiplied by two.
|
||||
ageMultiplied = computed(() => this.age() * 2);
|
||||
}
|
||||
```
|
||||
|
||||
Computed signals memoize values.
|
||||
See more details in the [dedicated section for computed](/guide/signals#computed-signals).
|
||||
|
||||
## Monitoring changes
|
||||
|
||||
With signal inputs, users can leverage the `effect` function.
|
||||
The function will execute whenever the input changes.
|
||||
|
||||
Consider the following example.
|
||||
The new value is printed to the console whenever the `firstName` input changes.
|
||||
|
||||
```typescript
|
||||
import {input, effect} from '@angular/core';
|
||||
|
||||
class MyComp {
|
||||
firstName = input.required<string>();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
console.log(this.firstName());
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `console.log` function is invoked every time the `firstName` input changes.
|
||||
This will happen as soon as `firstName` is available, and for subsequent changes during the lifetime of `MyComp`.
|
||||
|
||||
## Value transforms
|
||||
|
||||
You may want to coerce or parse input values without changing the meaning of the input.
|
||||
Transforms convert the raw value from parent templates to the expected input type.
|
||||
Transforms should be [pure functions](https://en.wikipedia.org/wiki/Pure_function).
|
||||
|
||||
```typescript
|
||||
class MyComp {
|
||||
disabled = input(false, {
|
||||
transform: (value: boolean|string) => typeof v === 'string' ? v === '' : v,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, you are declaring an input named `disabled` that is accepting values of type `boolean` and `string`.
|
||||
This is captured by the explicit parameter type of `value` in the `transform` option.
|
||||
These values are then parsed to a `boolean` with the transform, resulting in booleans.
|
||||
|
||||
That way, you are only dealing with `boolean` inside your component when calling `this.disabled()`, while users of your component can pass an empty string as a shorthand to mark your component as disabled.
|
||||
|
||||
```html
|
||||
<my-custom-comp disabled>
|
||||
```
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
Do not use transforms if they change the meaning of the input, or if they are [impure](https://en.wikipedia.org/wiki/Pure_function#Impure_functions).
|
||||
|
||||
Instead, use `computed` for transformations with different meaning, or an `effect` for impure code that should run whenever the input changes.
|
||||
|
||||
</div>
|
||||
|
||||
## Why should we use signal inputs and not `@Input()`?
|
||||
|
||||
Signal inputs are a reactive alternative to decorator-based `@Input()`.
|
||||
|
||||
In comparison to decorator-based `@Input`, signal inputs provide numerous benefits:
|
||||
|
||||
1. Signal inputs are more **type safe**:
|
||||
<br/>• Required inputs do not require initial values, or tricks to tell TypeScript that an input _always_ has a value.
|
||||
<br/>• Transforms are automatically checked to match the accepted input values.
|
||||
2. Signal inputs, when used in templates, will **automatically** mark `OnPush` components as dirty.
|
||||
3. Values can be easily **derived** whenever an input changes using `computed`.
|
||||
4. Easier and more local monitoring of inputs using `effect` instead of `ngOnChanges` or setters.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Signal queries
|
||||
|
||||
A component or directive can define queries that find child elements and read values from their injectors.
|
||||
|
||||
Developers most commonly use queries to retrieve references to components, directives, DOM elements, and more.
|
||||
|
||||
There are two categories of query: view queries and content queries.
|
||||
|
||||
Signal queries supply query results as a reactive signal primitive. You can use query results in `computed` and `effect`, composing these results with other signals.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Signal queries are in [developer preview](/guide/releases#developer-preview).
|
||||
APIs may change based on feedback without going through Angular's deprecation cycle.
|
||||
|
||||
</div>
|
||||
|
||||
If you're already familiar with Angular queries, you can jump straight to [Comparing signal-based queries to decorator-based queries](#comparing-signal-based-queries-to-decorator-based-queries)
|
||||
|
||||
## View queries
|
||||
|
||||
View queries retrieve results from the elements in the component's own template (view).
|
||||
|
||||
### `viewChild`
|
||||
|
||||
You can declare a query targeting a single result with the `viewChild` function.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
template: `
|
||||
<div #el></div>
|
||||
<my-component />
|
||||
`
|
||||
})
|
||||
export class TestComponent {
|
||||
// query for a single result by a string predicate
|
||||
divEl = viewChild<ElementRef>('el'); // Signal<ElementRef|undefined>
|
||||
// query for a single result by a type predicate
|
||||
cmp = viewChild(MyComponent); // Signal<MyComponent|undefined>
|
||||
}
|
||||
```
|
||||
|
||||
### `viewChildren`
|
||||
|
||||
You can also query for multiple results with the `viewChildren` function.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
template: `
|
||||
<div #el></div>
|
||||
@if (show) {
|
||||
<div #el></div>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class TestComponent {
|
||||
show = true;
|
||||
|
||||
// query for multiple results
|
||||
divEls = viewChildren<ElementRef>('el'); // Signal<ReadonlyArray<ElementRef>>
|
||||
}
|
||||
```
|
||||
|
||||
### View query options
|
||||
|
||||
The `viewChild` and the `viewChildren` query declaration functions have a similar signature accepting two arguments:
|
||||
|
||||
* a **locator** to specify the query target - it can be either a `string` or any injectable token
|
||||
* a set of **options** to adjust behavior of a given query.
|
||||
|
||||
Signal-based view queries accept only one option: `read`. The `read` option indicates the type of result to inject from the matched nodes and return in the final results.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
template: `<my-component/>`
|
||||
})
|
||||
export class TestComponent {
|
||||
// query for a single result with options
|
||||
cmp = viewChild(MyComponent, {read: ElementRef}); // Signal<ElementRef|undefined>
|
||||
}
|
||||
```
|
||||
|
||||
## Content queries
|
||||
|
||||
Content queries retrieve results from the elements in the component's content — the elements nested inside the component tag in the template where it's used.
|
||||
|
||||
### `contentChild`
|
||||
|
||||
You can query for a single result with the `contentChild` function.
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class TestComponent {
|
||||
// query by a string predicate
|
||||
headerEl = contentChild<ElementRef>('h'); // Signal<ElementRef|undefined>
|
||||
|
||||
// query by a type predicate
|
||||
header = contentChild(MyHeader); // Signal<MyHeader|undefined>
|
||||
}
|
||||
```
|
||||
|
||||
### `contentChildren`
|
||||
|
||||
You can also query for multiple results with the `contentChildren` function.
|
||||
|
||||
```ts
|
||||
@Component({...})
|
||||
export class TestComponent {
|
||||
// query for multiple results
|
||||
divEls = contentChildren<ElementRef>('h'); // Signal<ReadonlyArray<ElementRef>>
|
||||
}
|
||||
```
|
||||
|
||||
### Content query options
|
||||
|
||||
The `contentChild` and the `contentChildren` query declaration functions have a similar signature accepting two arguments:
|
||||
|
||||
* a **locator** to specify the query target - it can be either a `string` or any injectable token
|
||||
* a set of **options** to adjust behavior of a given query.
|
||||
|
||||
Content queries accept the following options:
|
||||
|
||||
* `descendants` By default, content queries find only direct children of the component and do not traverse into descendants. If this option is changed to `true`, query results will include all descendants of the element. Even when `true`, however, queries _never_ descend into components.
|
||||
* `read` indicates the type of result to retrieve from the matched nodes and return in the final results.
|
||||
|
||||
### Required child queries
|
||||
|
||||
If a child query (`viewChild` or `contentChild`) does not find a result, its value is `undefined`. This may occur if the target element is hidden by a control flow statement like `@if` or `@for`.
|
||||
|
||||
Because of this, the child queries return a signal that potentially have the `undefined` value. Most of the time, and especially for the view child queries, developers author their code such that:
|
||||
* there is at least one matching result;
|
||||
* results are accessed when the template was processed and query results are available.
|
||||
|
||||
For such cases, you can mark child queries as `required` to enforce presence of at least one matching result. This eliminates `undefined` from the result type signature. If a `required` query does not find any results, Angular throws an error.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div #requiredEl></div>
|
||||
`,
|
||||
})
|
||||
export class App {
|
||||
existingEl = viewChild.required('requiredEl'); // required and existing result
|
||||
missingEl = viewChild.required('notInATemplate'); // required but NOT existing result
|
||||
|
||||
ngAfterViewInit() {
|
||||
console.log(this.existingEl()); // OK :-)
|
||||
console.log(this.missingEl()); // Runtime error: result marked as required but not available!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Results availability timing
|
||||
|
||||
A signal query authoring functions will be executed as part of the directive instance construction. This happens before we could create a query instance and execute the template’s creation mode to collect any matches. As a consequence, there is a period of time where the signal instance was created (and can be read) but no query results could have been collected. By default Angular will return `undefined` (for child queries) or an empty array (for children queries) before results are available. Required queries will throw if accessed at this point.
|
||||
|
||||
Angular computes signal-based query results lazily, on demand. This means that query results are not collected unless there is a code path that reads the signal.
|
||||
|
||||
Query results can change over time due to the view manipulation - either through the Angular's control flow (`@if`, `@for` etc.) or by the direct calls to the `ViewContainerRef` API. When you read the value from the query result signal, you can receive different values over time.
|
||||
|
||||
Note: to avoid returning incomplete query results while a template is rendered, Angular delays query resolution until it finishes rendering a given template.
|
||||
|
||||
## Query declarations functions and the associated rules
|
||||
|
||||
The `viewChild`, `contentChild`, `viewChildren` and `contentChildren` functions are special function recognized by the Angular compiler. You can use those functions to declare queries by initializing a component or a directive property. You can never call these functions outside of component and directive property initializers.
|
||||
|
||||
```ts
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div #el></div>
|
||||
`,
|
||||
})
|
||||
export class App {
|
||||
el = viewChild('el'); // all good!
|
||||
|
||||
constructor() {
|
||||
const myConst = viewChild('el'); // NOT SUPPORTED
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparing signal-based queries to decorator-based queries
|
||||
|
||||
Signal queries are an alternative approach to the queries declared using the `@ContentChild`, `@ContentChildren`, `@ViewChild` or `@ViewChildren` decorators. The new approach exposes query results as signals which means that query results can be composed with other signals (using `computed` or `effect`) and drive change detection. Additionally, the signal-based query system offers other benefits:
|
||||
|
||||
* **More predictable timing.** You can access query results as soon as they're available.
|
||||
* **Simpler API surface.** All queries return a signal, and queries with more than one result let you work with a standard array.
|
||||
* **Improved type safety.** Fewer query use cases include `undefined` in the possible results.
|
||||
* **More accurate type inference.** TypeScript can infer more accurate types when you use a type predicate or when you specify an explicit `read` option.
|
||||
* **Lazier updates.** - Angular updates signal-based query results lazily; the framework does no work unless your code explicitly reads the query results.
|
||||
|
||||
The underlying query mechanism doesn't change much - conceptually Angular still creates singular "child" or plural "children" queries that target elements in a template (view) or content. The difference is in type of results and the exact timing of the results availability. The authoring format for declaring signal-based queries changed as well: the `viewChild`, `viewChildren`, `contentChild` and `contentChildren` functions used as initializer of class members are automatically recognized by Angular.
|
||||
@@ -104,7 +104,7 @@ Template input variables can be seen in action in the long-form usage of `NgFor`
|
||||
```html
|
||||
<ul>
|
||||
<ng-template ngFor let-hero [ngForOf]="heroes">
|
||||
<li>{{hero.name}}
|
||||
<li>{{hero.name}}</li>
|
||||
</ng-template>
|
||||
</ul>
|
||||
```
|
||||
@@ -118,7 +118,7 @@ When an `<ng-template>` is instantiated, multiple named values can be passed whi
|
||||
```html
|
||||
<ul>
|
||||
<ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
|
||||
<li>Hero number {{i}}: {{hero.name}}
|
||||
<li>Hero number {{i}}: {{hero.name}}</li>
|
||||
</ng-template>
|
||||
</ul>
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ To get the most out of these developer guides, you should review the following t
|
||||
</a>
|
||||
<a href="guide/dependency-injection" class="docs-card" title="Dependency injection">
|
||||
<section>Dependency injection</section>
|
||||
<p>Learn about dependency injection. Dependency injection refers to services or objects that a class needs to perform a specific function.</p>
|
||||
<p>Learn about dependency injection. Dependency injection refers to the pattern Angular uses to create and deliver services or objects that a class needs to perform a specific function.</p>
|
||||
<p class="card-footer">Dependency injection</p>
|
||||
</a>
|
||||
<!-- <a href="guide/rendering-overview" class="docs-card" title="Angular service worker developer guide">
|
||||
|
||||
@@ -58,7 +58,7 @@ It also includes troubleshooting information and any recommended manual changes
|
||||
For simple updates, the CLI command [`ng update`](cli/update) is all you need.
|
||||
Without additional arguments, [`ng update`](cli/update) lists the updates that are available to you and provides recommended steps to update your application to the most current version.
|
||||
|
||||
[Angular Versioning and Releases](guide/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based a release's version number.
|
||||
[Angular Versioning and Releases](guide/releases#versioning "Angular Release Practices, Versioning") describes the level of change that you can expect based on a release's version number.
|
||||
It also describes supported update paths.
|
||||
|
||||
<a id="resources"></a>
|
||||
|
||||
@@ -787,6 +787,16 @@
|
||||
"title": "Signals",
|
||||
"tooltip": "Angular signals for optimized change detection"
|
||||
},
|
||||
{
|
||||
"url": "guide/signal-inputs",
|
||||
"title": "Signal inputs",
|
||||
"tooltip": "Signal inputs to optimize change detection and developer experience."
|
||||
},
|
||||
{
|
||||
"url": "guide/signal-queries",
|
||||
"title": "Signal queries",
|
||||
"tooltip": "Signal queries exposing results as a reactive signal primitive."
|
||||
},
|
||||
{
|
||||
"url": "guide/rxjs-interop",
|
||||
"title": "RxJS Interop for Signals",
|
||||
|
||||
+18
-18
@@ -61,33 +61,33 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.2.0-next.0",
|
||||
"@angular/cdk": "17.2.0-next.0",
|
||||
"@angular/common": "17.2.0-next.0",
|
||||
"@angular/compiler": "17.2.0-next.0",
|
||||
"@angular/core": "17.2.0-next.0",
|
||||
"@angular/elements": "17.2.0-next.0",
|
||||
"@angular/forms": "17.2.0-next.0",
|
||||
"@angular/material": "17.2.0-next.0",
|
||||
"@angular/platform-browser": "17.2.0-next.0",
|
||||
"@angular/platform-browser-dynamic": "17.2.0-next.0",
|
||||
"@angular/router": "17.2.0-next.0",
|
||||
"@angular/service-worker": "17.2.0-next.0",
|
||||
"@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-next.0",
|
||||
"@angular-devkit/build-angular": "17.2.0-next.0",
|
||||
"@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#07da06414a4ce04a8f9710f2bc9346c2c753a0d2",
|
||||
"@angular/cli": "17.2.0-next.0",
|
||||
"@angular/compiler-cli": "17.2.0-next.0",
|
||||
"@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",
|
||||
@@ -113,7 +113,7 @@
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jasmine": "4.1.3",
|
||||
"eslint-plugin-jsdoc": "48.0.5",
|
||||
"eslint-plugin-jsdoc": "48.0.6",
|
||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||
"find-free-port": "^2.0.0",
|
||||
"firebase-tools": "^13.0.0",
|
||||
|
||||
@@ -94,6 +94,10 @@ aio-api-list {
|
||||
h2 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
& .deprecated-api-item {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
/* API CLASS LIST */
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"angular": "^1.8.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"angular-route": "1.8.3",
|
||||
@@ -29,9 +29,9 @@
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@types/angular": "^1.8.4",
|
||||
"@types/angular-route": "^1.7.2",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
|
||||
@@ -13,23 +13,23 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^16.11.35",
|
||||
"copyfiles": "^2.4.1",
|
||||
|
||||
@@ -13,24 +13,24 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/elements": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/elements": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^16.11.35",
|
||||
"jasmine-core": "~5.1.0",
|
||||
|
||||
@@ -15,24 +15,24 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/localize": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/localize": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^16.11.35",
|
||||
"jasmine-core": "~5.1.0",
|
||||
|
||||
@@ -13,24 +13,24 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/service-worker": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"@angular/service-worker": "17.1.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^16.11.35",
|
||||
"jasmine-core": "~5.1.0",
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/upgrade": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"@angular/upgrade": "17.1.3",
|
||||
"core-js": "^3.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"systemjs": "~0.19.39",
|
||||
@@ -42,7 +42,7 @@
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@rollup/plugin-commonjs": "^25.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.2.0",
|
||||
"@types/angular": "^1.8.4",
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/platform-server": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/ssr": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/platform-server": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"@angular/ssr": "17.1.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"express": "^4.18.1",
|
||||
"rxjs": "~7.8.0",
|
||||
@@ -30,9 +30,9 @@
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^16.11.35",
|
||||
|
||||
@@ -19,20 +19,20 @@
|
||||
"license": "MIT",
|
||||
"repository": {},
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.1.1",
|
||||
"@angular/common": "17.1.1",
|
||||
"@angular/compiler": "17.1.1",
|
||||
"@angular/core": "17.1.1",
|
||||
"@angular/elements": "17.1.1",
|
||||
"@angular/forms": "17.1.1",
|
||||
"@angular/localize": "17.1.1",
|
||||
"@angular/platform-browser": "17.1.1",
|
||||
"@angular/platform-browser-dynamic": "17.1.1",
|
||||
"@angular/platform-server": "17.1.1",
|
||||
"@angular/router": "17.1.1",
|
||||
"@angular/service-worker": "17.1.1",
|
||||
"@angular/ssr": "17.1.1",
|
||||
"@angular/upgrade": "17.1.1",
|
||||
"@angular/animations": "17.1.3",
|
||||
"@angular/common": "17.1.3",
|
||||
"@angular/compiler": "17.1.3",
|
||||
"@angular/core": "17.1.3",
|
||||
"@angular/elements": "17.1.3",
|
||||
"@angular/forms": "17.1.3",
|
||||
"@angular/localize": "17.1.3",
|
||||
"@angular/platform-browser": "17.1.3",
|
||||
"@angular/platform-browser-dynamic": "17.1.3",
|
||||
"@angular/platform-server": "17.1.3",
|
||||
"@angular/router": "17.1.3",
|
||||
"@angular/service-worker": "17.1.3",
|
||||
"@angular/ssr": "17.1.3",
|
||||
"@angular/upgrade": "17.1.3",
|
||||
"angular": "^1.8.3",
|
||||
"angular-in-memory-web-api": "~0.17.0",
|
||||
"angular-route": "1.8.3",
|
||||
@@ -45,9 +45,9 @@
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.1.1",
|
||||
"@angular/cli": "17.1.1",
|
||||
"@angular/compiler-cli": "17.1.1",
|
||||
"@angular-devkit/build-angular": "17.1.3",
|
||||
"@angular/cli": "17.1.3",
|
||||
"@angular/compiler-cli": "17.1.3",
|
||||
"@rollup/plugin-commonjs": "^25.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.2.0",
|
||||
"@types/angular": "^1.8.4",
|
||||
|
||||
+105
-105
@@ -10,23 +10,23 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@angular-devkit/architect@0.1701.1":
|
||||
version "0.1701.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1701.1.tgz#80e2bc94763e21c273ef6d9f905390f1d935df60"
|
||||
integrity sha512-vT3ZRAIfNyIg0vJWT6umPbCKiKFCukNkxLe9kgOU0tinZKNr/LgWYaBZ92Rxxi6C3NrAnmAYjsih8x4zdyoRXw==
|
||||
"@angular-devkit/architect@0.1701.3":
|
||||
version "0.1701.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1701.3.tgz#974cc8680baa5b8440cfc8039bbc4c46008e80bf"
|
||||
integrity sha512-K5rvhslbXNwx04cCLviEJCA27MwoJRMMzALFXySi9BqjZnZUOtZnOBuuCdrTPaRmFaYqGO4Im5GNzpbb/NB8zg==
|
||||
dependencies:
|
||||
"@angular-devkit/core" "17.1.1"
|
||||
"@angular-devkit/core" "17.1.3"
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@angular-devkit/build-angular@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-17.1.1.tgz#26a65e3daf8d306e39634b3322ee68ff19d0118c"
|
||||
integrity sha512-GchDM8H+RQNts731c+jnhDgOm0PnCS3YB12uVwRiGsaNsUMrqKnu3P0poh6AImDMPyXKnIvTWLDCMD8TDziR0A==
|
||||
"@angular-devkit/build-angular@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-17.1.3.tgz#05634e6a81ade1eb4e12844e37cbf636bf949bc4"
|
||||
integrity sha512-pusFVSWMnrm2GrF3+Fw19OhA2rNw4WkfTMUruhaKAjW5QIvZ3wHYf+pH//1Ud+tuhFBi9BH7UALP2vnJMu1ehw==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "2.2.1"
|
||||
"@angular-devkit/architect" "0.1701.1"
|
||||
"@angular-devkit/build-webpack" "0.1701.1"
|
||||
"@angular-devkit/core" "17.1.1"
|
||||
"@angular-devkit/architect" "0.1701.3"
|
||||
"@angular-devkit/build-webpack" "0.1701.3"
|
||||
"@angular-devkit/core" "17.1.3"
|
||||
"@babel/core" "7.23.7"
|
||||
"@babel/generator" "7.23.6"
|
||||
"@babel/helper-annotate-as-pure" "7.22.5"
|
||||
@@ -37,7 +37,7 @@
|
||||
"@babel/preset-env" "7.23.7"
|
||||
"@babel/runtime" "7.23.7"
|
||||
"@discoveryjs/json-ext" "0.5.7"
|
||||
"@ngtools/webpack" "17.1.1"
|
||||
"@ngtools/webpack" "17.1.3"
|
||||
"@vitejs/plugin-basic-ssl" "1.0.2"
|
||||
ansi-colors "4.1.3"
|
||||
autoprefixer "10.4.16"
|
||||
@@ -90,18 +90,18 @@
|
||||
optionalDependencies:
|
||||
esbuild "0.19.11"
|
||||
|
||||
"@angular-devkit/build-webpack@0.1701.1":
|
||||
version "0.1701.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1701.1.tgz#ab510fef446d2a9a2e71d4b1a688000494b28e09"
|
||||
integrity sha512-YgNl/6xLmI0XdUCu/H4Jyi34BhrANCDP4N2Pz+tGwnz2+Vl8oZGLPGtKVbh/LKSAmEHk/B6GQUekSBpKWrPJoA==
|
||||
"@angular-devkit/build-webpack@0.1701.3":
|
||||
version "0.1701.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1701.3.tgz#584275d98be9e76d5db3d870ff37c57175d04a45"
|
||||
integrity sha512-fpZtJf6yvXM7mX1T83caeYpa0e3zPv7sgKmx0ZIJKGL8+DETgNcCCeCTgui7HMBcHGCD8yj72DZ8xMMBWwVBIA==
|
||||
dependencies:
|
||||
"@angular-devkit/architect" "0.1701.1"
|
||||
"@angular-devkit/architect" "0.1701.3"
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@angular-devkit/core@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.1.1.tgz#c140e6fcd158cea87e15d0d09f3879fc23a52637"
|
||||
integrity sha512-b1wd1caegc1p18nTrfPhfHQAZW1GnWWKGldq5MZ8C/nkgJbjjN8SKb1Vw7GONkOnH6KxWDAXS4i93/wdQcz4Bg==
|
||||
"@angular-devkit/core@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.1.3.tgz#d3b2212f9fa753506b1ccab24ee34edcdcd02e63"
|
||||
integrity sha512-iuVK4hyW3YhusxIi8zGBvvVA9pWtDT3H6LQbWdVk9D3jXCZBIrEMklvAiJErqficKnUurf6gtFOeA8Fop6GotA==
|
||||
dependencies:
|
||||
ajv "8.12.0"
|
||||
ajv-formats "2.1.1"
|
||||
@@ -110,33 +110,33 @@
|
||||
rxjs "7.8.1"
|
||||
source-map "0.7.4"
|
||||
|
||||
"@angular-devkit/schematics@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.1.1.tgz#caaaeff64519d691121a92c92dbfd53581ff0bc4"
|
||||
integrity sha512-3AtEO7k0Znzg11o51ZqebkW8063QkZ7Y7BC96Oye+wSdpT3ow57P0w0UtOpUNesNKzj1iMuPWqqm4i+YqitjCw==
|
||||
"@angular-devkit/schematics@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.1.3.tgz#9fc4c82f64af9375249910d26093e449a73f0166"
|
||||
integrity sha512-zKoWG1hDfvi1vR9Hqoca9hWo9vDg8evmQvGcBW5jXR5ndZi5Oit/uDcGdA8WUKvBd1EG7WMqp0FgcDR9EA9WCw==
|
||||
dependencies:
|
||||
"@angular-devkit/core" "17.1.1"
|
||||
"@angular-devkit/core" "17.1.3"
|
||||
jsonc-parser "3.2.0"
|
||||
magic-string "0.30.5"
|
||||
ora "5.4.1"
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@angular/animations@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-17.1.1.tgz#45b311fd9bd1d4aee1a4bedb49a4164cc622cd51"
|
||||
integrity sha512-85qm8b4fNKa5zx4YlvCvAb3lWGlRsEcClnpCuwSVP8pXG7n/cW8MsJOh8i/wOkQTr9Ac7vvAE+yFWMi2A/qlTg==
|
||||
"@angular/animations@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-17.1.3.tgz#be67fc479cfc700d7c1270222003c6d2bc622194"
|
||||
integrity sha512-AS9CHOjjKqkuAzlKEMJfAkZfkIdSoagB3D8HwvH+ZHo6GVJc9KbtLQn/okNijFK+Fg7QK/hYbQ3lJhjgk0GQDA==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/cli@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-17.1.1.tgz#3aadcee37b8fd5b4ecb6c7ae8057dfb26973cc73"
|
||||
integrity sha512-JG/Vf+PScR3PC6u7B+jFF4s5eBByzCpOfCfRFw98nlCqDAOxYOig7wi2Sbp5fnvILQH8vbc/NG8MzdgONrG6Hg==
|
||||
"@angular/cli@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-17.1.3.tgz#45af3e5041f3df14ff10dc48ee64960f575d5aac"
|
||||
integrity sha512-ysPWDdqo2cwfeskKVAg8p4C8xuezWcIWyW/ACSjWw6yp4OZvyVd6cGZrc0POVZzAPtTOYJSgWOpF/DCHQFluSg==
|
||||
dependencies:
|
||||
"@angular-devkit/architect" "0.1701.1"
|
||||
"@angular-devkit/core" "17.1.1"
|
||||
"@angular-devkit/schematics" "17.1.1"
|
||||
"@schematics/angular" "17.1.1"
|
||||
"@angular-devkit/architect" "0.1701.3"
|
||||
"@angular-devkit/core" "17.1.3"
|
||||
"@angular-devkit/schematics" "17.1.3"
|
||||
"@schematics/angular" "17.1.3"
|
||||
"@yarnpkg/lockfile" "1.1.0"
|
||||
ansi-colors "4.1.3"
|
||||
ini "4.1.1"
|
||||
@@ -152,17 +152,17 @@
|
||||
symbol-observable "4.0.0"
|
||||
yargs "17.7.2"
|
||||
|
||||
"@angular/common@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-17.1.1.tgz#67f6469329130d7c43d36e1cb3e6220157391ea0"
|
||||
integrity sha512-YMM2vImWJg7H3Yaej7ncGpFKT28V2Y6X9/rLpRdSKAiUbcbj7GeWtX/upfZGR9KmD08baYZw0YTNMR03Ubv/mg==
|
||||
"@angular/common@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-17.1.3.tgz#d8411db80510a99a23ad01a56264c3594625936a"
|
||||
integrity sha512-AzLzoNSeRSNGBQk0K+iG0XdYG36SDeJqYqE8rfoiWuv1NDFLL05UJM2/fQfaMNg0oX5bHOlHUqHFj3sFR/NVpw==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/compiler-cli@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-17.1.1.tgz#554e4cd090f4d64294bedd03868d3853a6d1af45"
|
||||
integrity sha512-d6Aev1P92q7wd5u3UcJifzNlU9svxaYI2Ts6MKoD4jY4/GPN/gPDqi20weDMujEgirrkcwGbsCXBRoEGkA5c9A==
|
||||
"@angular/compiler-cli@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-17.1.3.tgz#f1cbb68eb089c7aa161bab315cdc1a0004fac768"
|
||||
integrity sha512-bNDHXo3Twub0BZK9OmXly+0REs0RuR1SUXlTAeq+0XubCvnBDvpg9peL7UTTGS5YRo9sUTBnR6faSUA1F5objQ==
|
||||
dependencies:
|
||||
"@babel/core" "7.23.2"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
@@ -173,92 +173,92 @@
|
||||
tslib "^2.3.0"
|
||||
yargs "^17.2.1"
|
||||
|
||||
"@angular/compiler@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-17.1.1.tgz#b3e182390eb4f54690c0b6dd60d02ac389a1c0b4"
|
||||
integrity sha512-lEQ5YNMJQm2iO2EZbGkwL3SqnxtE2ENfymgbS023F6ACsnP3kKB2DMwOnIbGgQY4+8r4sJFiMAIjEkj5c9kttg==
|
||||
"@angular/compiler@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-17.1.3.tgz#157e834eb4b0cb4f339bc8a0da8dcb438e92034a"
|
||||
integrity sha512-k/s21gPPKStxVOLr6l4Y145OIxyBY7BhTPVOl/qEAgE+IcZ9vkiA8dYl8yjL884Kl1ZKPmFA3AofMJjWjZGNag==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/core@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-17.1.1.tgz#9da19f0c832d69994c79255c56e81f40ae7ae68a"
|
||||
integrity sha512-JtNYM9eHr8eUSrGPq/kn0+/F+TSZ7EBWxZhM1ZndOlGu1gA4fGhrDid4ZXIHIs07DbM4NZjMn+LhRyx02YDsSA==
|
||||
"@angular/core@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-17.1.3.tgz#12f4adb8d1037ba1572c7531997d82a82fbe8fc1"
|
||||
integrity sha512-2lZ4DRHN8KJ/aQads+YXIcx5Ri9yyeFIlw69m5Pn7wAi/+Rakg7IsclgLaWs7aBtWwMHG7LnqFKxAVq7CjXKtA==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/elements@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-17.1.1.tgz#1fbbe9291a5029949a6bc5862b3ca11b945d94a9"
|
||||
integrity sha512-GrPUCSw3RpjX1XmUpXrRdf19YE27hFDqXNpc/t36gWrQ3ndkYfU+I+fhNXigSoBvDW9/Pk+EJXTKX1LJ2CeOdw==
|
||||
"@angular/elements@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-17.1.3.tgz#944cb425e8c724417e499ab730009ce32d5ed2f7"
|
||||
integrity sha512-q2JCogiHFB7Ac3IpiQGIUNF7ZDM5t/fF7xlDFvTLto7HHCb0g8y4erzmUmV+QRKX0YKedo5QAkd8w9TFlJ4ecA==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/forms@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-17.1.1.tgz#1e07572b207f4e78dc8abe9435d00c8fc0f37343"
|
||||
integrity sha512-rqHVzaJDV8+VbnfC6mDgzX6ooa0X0hmnd+XfuOZaEJ7MtyOmqQ8qas2PAKXU7nMIImYXfYc4O4XWbSc1pRy1Hw==
|
||||
"@angular/forms@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-17.1.3.tgz#1e87b39e237a1d6f7cc2f78ad8d6b597b5ef08aa"
|
||||
integrity sha512-aNa0jGLT5d+hnKVrSo8tk3TRo/NLNu1RxLNx8RhIczKAeCK3eD8SvTMy27iJtyXmNG2GWN7QPiDeGepd75nbxQ==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/localize@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-17.1.1.tgz#f64b1724bdf4f0ab33551ca7bddd5e92d8cba6b8"
|
||||
integrity sha512-vZc+YhpE7eRWFzdZFOidSjZlTl6+8VH0x2m7WesaSgw2fu1IgOQffZFnaGveAQ8DXbMfgBb4exPNJFPYmNgoSQ==
|
||||
"@angular/localize@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-17.1.3.tgz#626564dcd89a3f5d26cd31eb2fcdc848e01898ad"
|
||||
integrity sha512-Jdz5FWcaQLRhEhUHJk0iyMOT1ts4HHcY9B20mBmAd7u3kqEoBc57N8hOX0vHjU4/rk1tGqIvzfhJJf6zqIWcZQ==
|
||||
dependencies:
|
||||
"@babel/core" "7.23.2"
|
||||
"@types/babel__core" "7.20.2"
|
||||
fast-glob "3.3.1"
|
||||
yargs "^17.2.1"
|
||||
|
||||
"@angular/platform-browser-dynamic@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.1.tgz#8d389d9739319a1260df27aceb7f4fffdecd28b7"
|
||||
integrity sha512-UKI8966nwo+p01EjmQdkepLIeVLpPZTSDZAM4va7CfMO6lbCN5xFecDd/sVbut8J6ySIsbJxyDkP+SHMQjE+xg==
|
||||
"@angular/platform-browser-dynamic@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.3.tgz#7391d547a22b85e95de8ee1f16faf6b876f26ae1"
|
||||
integrity sha512-0lFhcFJfDzCSSVe8l8OY+UgUiwUwcbxwpvLod3XWBpf1iEUlr5720FIMA3VJYwpW3Oj4Uey3nVm13EMtRqpqdA==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/platform-browser@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-17.1.1.tgz#758b289f9d4920217c3bf754135e1558cd3d1cc1"
|
||||
integrity sha512-/80znuEkdDvsF6EX/fe03isQlLCUS9+ldCgB4n0ZL+qAkf2/lJlU3n97SyEN7rzb189U+K1fDe0fb1nDwbbcWQ==
|
||||
"@angular/platform-browser@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-17.1.3.tgz#3883cf089f2e5bd24b99df1e7c2cdd3df3d97514"
|
||||
integrity sha512-onPCvdk9f/6OhOo2zP6nfGKpzLma1QIxpFqD3jymbmIJTcVMOOQDMYW3eLtY+uSX8ribcJ7GQcbDGIM4rliTFg==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/platform-server@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-17.1.1.tgz#9e0feec18c10974e3eed80e9a4e8cf28be6490a7"
|
||||
integrity sha512-zZbJL56m1n/btwVV0TNc/YPqXYZp9rcnDEvl9HHFNfoinkQrcxHy6MHQUjEyQ1IAIv9Y0f73pmSbJ86BcloIAw==
|
||||
"@angular/platform-server@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-17.1.3.tgz#b30236072a8a6f97198eaf41a121759e62922477"
|
||||
integrity sha512-EB+sk4oZuUPEbkGSM0jpHAKiZVH98IBlXaRYDcmRCRw1hjZwsxl0kAQNuInSGfwJlyjVF7WCrZ6eNCuxlxXiAQ==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
xhr2 "^0.2.0"
|
||||
|
||||
"@angular/router@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/router/-/router-17.1.1.tgz#752e5856360844512f78267bea5364c7b7dac0d7"
|
||||
integrity sha512-pPIRX0v8agij2dRSU25iwj9qFy0S25cztsy7bGfZ+M510jwRCqu1JsitqXtQ85XSv/bdFqiNiFU0UbwVFl+QiQ==
|
||||
"@angular/router@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/router/-/router-17.1.3.tgz#30d12b43079f44fda2ab968cde41da3548450d9e"
|
||||
integrity sha512-6HigdtFjm+76UU2hiLGLE2SpOecQhD6TnAVTocDuRitpN5m0dyiffBrqxarfNwoZuMdIiXyqClJR4JRo1rJjoQ==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/service-worker@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-17.1.1.tgz#1cc2823d9a5fce14197f008b3b226dcab4d24a58"
|
||||
integrity sha512-/ggG10tY5LWsGvJYLGhE0gdL8r9fEmRrZ6i+UCdwG/+Ydyb5GBxLS7OGH6yy0R8WP19FDRmVoDVMFJMtc4JMAw==
|
||||
"@angular/service-worker@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-17.1.3.tgz#1222de7a4752b6f8dc7bbb5dcc6015db13346b45"
|
||||
integrity sha512-DSndSwNvNnETiqtFAt/jPUMBUUfAxot7IhEUp19KFbS4HhasSZ+QxkNECN9bT6SUWjeaN7wvPcvR4gdSkQs3Pg==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/ssr@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/ssr/-/ssr-17.1.1.tgz#8199fc8ce5d321c45592f471b523813ea8d39885"
|
||||
integrity sha512-HHWk827BBLkVNpwsp31GYO484vXdFGMUoegoHCVd3qZL7gXcaAdN+zreE9E1OnaIImZWpj99le3MAsH6ee69BQ==
|
||||
"@angular/ssr@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/ssr/-/ssr-17.1.3.tgz#4bab9172d31043baeef4fdafb8a7789567f1704e"
|
||||
integrity sha512-zgghp3gdh+pv//lYWWynfZOE9jkndSmflojgRWBR4FdAtapdyrm1MaE6TIwC7qT+r/sW0bKypK2fKxwN8sXpHw==
|
||||
dependencies:
|
||||
critters "0.0.20"
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/upgrade@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-17.1.1.tgz#2c6b8b4f690782423a499b0ca1c4f9cbd319bf17"
|
||||
integrity sha512-DfIzp/t3cZZGcCyNghpyzBQFDMKo8qmRADtmR3ToBujERX4cwDcEySa/XBnFDjedcDusQB8pABnLOiatNk3HzA==
|
||||
"@angular/upgrade@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-17.1.3.tgz#4cf0a05866dced04114cb40de9f9e75d6897c0e4"
|
||||
integrity sha512-Iw6J9jOg4P+CohWCsbchPnmOiLzuHhs5y0RqdA4+JL0dtTK8FkGkgaI39za74MYAbqNm6Z+q2yzlwyoe3gNEuw==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
@@ -1737,10 +1737,10 @@
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
|
||||
"@ngtools/webpack@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-17.1.1.tgz#271177de0a382fccb5ba887a110ada9da79dc653"
|
||||
integrity sha512-uPWEpRuAUmMBZhYMpkEHNbeI8QAgeVM5lnWM+02lK75u1+sgYy32ed+RcRvEI+8hRQcsCQ8HtR4QubgJb4TzCQ==
|
||||
"@ngtools/webpack@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-17.1.3.tgz#510c5f6f6bf9ae470b0e43d4560cfc4ffc3a5ed5"
|
||||
integrity sha512-mszRSb7aMNKHnkh3Jrfo83KVOguX/cUamJJcGIYe9o7tnLGRIoMp4vP0fx6Og4J0/CGDRhSDG4IiJ29aOU7K8A==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -1937,13 +1937,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602"
|
||||
integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==
|
||||
|
||||
"@schematics/angular@17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.1.1.tgz#5ac8f09a66e7dff86cbf664b9388a0dde4a97de3"
|
||||
integrity sha512-1Wqefy1W9Y63g48Fp7BscL95V4U1seDGgZawH6DcJnytJVW89hazao7YREzLYfdoediuw7lU+OHJksWYe4VQww==
|
||||
"@schematics/angular@17.1.3":
|
||||
version "17.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.1.3.tgz#3c3e0f67985ed500d9d3b66fdb63387cc775e9a9"
|
||||
integrity sha512-hmeasOvzmniy6urtzUKhEqGO67iPuLX/dVtkF4nWp2NTtcEKlvcJobNDMc+CTlX4+ZMPVOvmhDMQqrlfekZ+NQ==
|
||||
dependencies:
|
||||
"@angular-devkit/core" "17.1.1"
|
||||
"@angular-devkit/schematics" "17.1.1"
|
||||
"@angular-devkit/core" "17.1.3"
|
||||
"@angular-devkit/schematics" "17.1.3"
|
||||
jsonc-parser "3.2.0"
|
||||
|
||||
"@sigstore/bundle@^2.1.0":
|
||||
|
||||
+789
-118
File diff suppressed because it is too large
Load Diff
+92
-16
@@ -8,29 +8,105 @@ To setup your local development environment for Angular DevTools, see [DEVELOPIN
|
||||
|
||||
## Publish Angular DevTools
|
||||
|
||||
To publish Angular DevTools to Firefox or Chrome stores:
|
||||
Publishing Angular DevTools is a five step process:
|
||||
1. Sync and update workspace.
|
||||
1. Update extension version numbers.
|
||||
1. Publish to Chrome.
|
||||
1. Publish to Firefox.
|
||||
1. Commit and merge the updated version numbers.
|
||||
|
||||
1. Update the version of the corresponding manifest file. Currently, Angular DevTools uses different versioning compared to the rest of the packages within the monorepo.
|
||||
* `manifest.chrome.json` for Chrome
|
||||
* `manifest.firefox.json` for Firefox
|
||||
1. Build the extension:
|
||||
* `yarn devtools:build:chrome` for Chrome
|
||||
* `yarn devtools:build:firefox` for Firefox
|
||||
1. Find the extension production assets in `dist/bin/devtools/projects/shell-browser/src/prodapp` relative to the root of the Angular monorepo
|
||||
1. Zip the files in the directory
|
||||
1. Upload them to the corresponding store
|
||||
### 1. Sync workspace
|
||||
|
||||
### Firefox
|
||||
Before starting anything, make sure your workspace is up to date with latest changes and dependencies.
|
||||
|
||||
1. Go to the Firefox Addons [page](https://addons.mozilla.org/developers/addons)
|
||||
1. For email enter `devrel@angular.io`
|
||||
1. Find the password on Valentine
|
||||
1. Setup Google Authenticator with the 2FA QR code. You can find the QR code on Valentine
|
||||
```shell
|
||||
git checkout main
|
||||
git pull upstream main
|
||||
nvm install
|
||||
yarn --frozen-lockfile
|
||||
```
|
||||
|
||||
### Chrome Web Store
|
||||
### 2. Update extension version numbers
|
||||
|
||||
Bump the version numbers listed in
|
||||
[`manifest.chrome.json`](/devtools/projects/shell-browser/src/manifest/manifest.chrome.json)
|
||||
and [`manifest.firefox.json`](/devtools/projects/shell-browser/src/manifest/manifest.firefox.json).
|
||||
|
||||
### 3. Publish to Chrome Chrome
|
||||
|
||||
To publish Angular DevTools to the Chrome Web Store, first build and package the extension.
|
||||
|
||||
```shell
|
||||
# Build the Chrome version.
|
||||
yarn devtools:build:chrome
|
||||
|
||||
# Package the extension.
|
||||
(cd dist/bin/devtools/projects/shell-browser/src/prodapp && zip -r ~/devtools-chrome.zip *)
|
||||
```
|
||||
|
||||
Then upload it to the Chrome Web Store.
|
||||
|
||||
1. Go to the extension [page](https://chrome.google.com/webstore/category/extensions)
|
||||
1. Make sure your email is part of the Google Group we use for publishing the extension
|
||||
1. Navigate to "Developer Dashboard"
|
||||
1. Enter your account credentials
|
||||
1. You should be able to change the publisher to "Angular"
|
||||
|
||||
You can choose to either publish immediately or only get approval but hold to publish at a later time.
|
||||
Note that even publishing immediately still requires approval from Chrome Web Store before it is
|
||||
available to users. Historically this has been pretty quick (< 30 minutes), but there is no hard upper
|
||||
limit on how long a review might take: https://developer.chrome.com/docs/webstore/review-process#review-time.
|
||||
|
||||
### 4. Firefox
|
||||
|
||||
To publish Angular DevTools as a Firefox Add-on, first build and package the extension.
|
||||
|
||||
```shell
|
||||
# Build the Firefox version.
|
||||
yarn devtools:build:firefox
|
||||
|
||||
# Package the extension.
|
||||
(cd dist/bin/devtools/projects/shell-browser/src/prodapp ~/devtools-firefox && zip -r ~/devtools-firefox.zip *)
|
||||
```
|
||||
|
||||
Then upload it:
|
||||
|
||||
1. Go to the Firefox Addons [page](https://addons.mozilla.org/developers/addons)
|
||||
1. For email enter `devrel@angular.io`
|
||||
1. Find the password [on Valentine](http://valentine/#/show/1651707871496288)
|
||||
1. Setup Google Authenticator with the 2FA QR code. You can find the QR code on Valentine
|
||||
|
||||
The Firefox publishing process is slightly more involved than Chrome. In particular, they
|
||||
require extension source code with instructions to build and run it. Since DevTools exists in
|
||||
a monorepo with critical build tooling existing outside the `devtools/` directory, we need to
|
||||
upload the entire monorepo. Package it without dependencies and generated files with the
|
||||
following command and upload it.
|
||||
|
||||
```shell
|
||||
zip -r ~/angular-source.zip * -x ".git/*" -x "node_modules/*" -x "**/node_modules/*" -x "dist/"
|
||||
```
|
||||
|
||||
Suggested note to reviewer:
|
||||
|
||||
> This is a monorepo and includes much more code than just the DevTools extension. The relevant
|
||||
> code is under `devtools/...` and `devtools/README.md` contains instructions for building and
|
||||
> testing locally.
|
||||
>
|
||||
> The uploaded source is equivalent to
|
||||
> https://github.com/angular/angular/tree/${permalink to current main}/ with the single change
|
||||
> of a bumped version number in the `manifest.json` file.
|
||||
|
||||
### 5. Commit and merge
|
||||
|
||||
Commit the version bump:
|
||||
|
||||
```shell
|
||||
git checkout -b devtools-release
|
||||
git add . && git commit -m "release: bump Angular DevTools version to 1.0.10"
|
||||
git push -u origin devtools-release
|
||||
```
|
||||
|
||||
Then create and merge a PR targeting `patch` with this change.
|
||||
|
||||
Once the PR merges and both Chrome and Firefox are showing the new version to end users, then
|
||||
the release is complete!
|
||||
|
||||
@@ -7,6 +7,7 @@ ng_module(
|
||||
srcs = ["devtools-app.component.ts"],
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/src:iframe_message_bus",
|
||||
"//packages/common",
|
||||
|
||||
@@ -11,11 +11,13 @@ import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
|
||||
import {IFrameMessageBus} from '../../../../../src/iframe-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
import {FrameManager} from '../../../../../projects/ng-devtools/src/lib/frame_manager';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [DevToolsComponent],
|
||||
providers: [
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(null)},
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
|
||||
@@ -86,6 +86,10 @@ export const subscribeToClientEvents = (
|
||||
|
||||
messageBus.on('logProvider', logProvider);
|
||||
|
||||
messageBus.on('log', ({message, level}) => {
|
||||
console[level](`[Angular DevTools]: ${message}`);
|
||||
});
|
||||
|
||||
if (appIsAngularInDevMode() && appIsSupportedAngularVersion() && appIsAngularIvy()) {
|
||||
setupInspector(messageBus);
|
||||
// Often websites have `scroll` event listener which triggers
|
||||
@@ -216,8 +220,8 @@ const getRoutes = (messageBus: MessageBus<Events>) => {
|
||||
const checkForAngular = (messageBus: MessageBus<Events>): void => {
|
||||
const ngVersion = getAngularVersion();
|
||||
const appIsIvy = appIsAngularIvy();
|
||||
|
||||
if (!ngVersion) {
|
||||
setTimeout(() => checkForAngular(messageBus), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
load("//devtools/tools:ng_module.bzl", "ng_module")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
load("//devtools/tools:typescript.bzl", "ts_test_library")
|
||||
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
@@ -12,13 +14,18 @@ ng_module(
|
||||
name = "lib",
|
||||
srcs = glob(
|
||||
include = ["*.ts"],
|
||||
exclude = ["theme-service.ts"],
|
||||
exclude = [
|
||||
"theme-service.ts",
|
||||
"frame_manager.ts",
|
||||
"*_spec.ts",
|
||||
],
|
||||
),
|
||||
angular_assets = [
|
||||
"devtools.component.html",
|
||||
":devtools_component_styles",
|
||||
],
|
||||
deps = [
|
||||
":frame_manager",
|
||||
":theme",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs",
|
||||
"//devtools/projects/protocol",
|
||||
@@ -34,6 +41,58 @@ ng_module(
|
||||
],
|
||||
)
|
||||
|
||||
ts_test_library(
|
||||
name = "devtools_test",
|
||||
srcs = ["devtools_spec.ts"],
|
||||
deps = [
|
||||
":frame_manager",
|
||||
":lib",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [
|
||||
":devtools_test",
|
||||
],
|
||||
)
|
||||
|
||||
ng_module(
|
||||
name = "frame_manager",
|
||||
srcs = glob(
|
||||
include = ["frame_manager.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
ts_test_library(
|
||||
name = "test_frame_manager_lib",
|
||||
srcs = [
|
||||
"frame_manager_spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_frame_manager",
|
||||
deps = [
|
||||
":test_frame_manager_lib",
|
||||
],
|
||||
)
|
||||
|
||||
ng_module(
|
||||
name = "theme",
|
||||
srcs = glob(
|
||||
|
||||
@@ -15,6 +15,15 @@ export interface Environment {
|
||||
LATEST_SHA: string;
|
||||
}
|
||||
|
||||
export const TOP_LEVEL_FRAME_ID = 0;
|
||||
|
||||
export interface Frame {
|
||||
id: number;
|
||||
name: string;
|
||||
url: URL;
|
||||
}
|
||||
|
||||
export abstract class ApplicationEnvironment {
|
||||
abstract get environment(): Environment;
|
||||
abstract frameSelectorEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import {DirectivePosition, ElementPosition} from 'protocol';
|
||||
|
||||
export abstract class ApplicationOperations {
|
||||
abstract viewSource(position: ElementPosition, directiveIndex?: number): void;
|
||||
abstract selectDomElement(position: ElementPosition): void;
|
||||
abstract inspect(directivePosition: DirectivePosition, objectPath: string[]): void;
|
||||
abstract viewSource(position: ElementPosition, directiveIndex?: number, target?: URL): void;
|
||||
abstract selectDomElement(position: ElementPosition, target?: URL): void;
|
||||
abstract inspect(directivePosition: DirectivePosition, objectPath: string[], target?: URL): void;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ ng_module(
|
||||
":devtools_tabs_component_styles",
|
||||
],
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib:theme",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer",
|
||||
@@ -42,6 +43,7 @@ ts_test_library(
|
||||
srcs = ["devtools-tabs.spec.ts"],
|
||||
deps = [
|
||||
":devtools-tabs",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib:theme",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer",
|
||||
@@ -56,7 +58,6 @@ ts_test_library(
|
||||
],
|
||||
)
|
||||
|
||||
# todo(aleksanderbodurri): fix this test suite
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [
|
||||
@@ -65,16 +66,3 @@ karma_web_test_suite(
|
||||
"//packages/platform-browser/animations",
|
||||
],
|
||||
)
|
||||
|
||||
# spec_bundle(
|
||||
# name = "test_bundle",
|
||||
# deps = [
|
||||
# ":devtools_tabs_test",
|
||||
# "//packages/platform-browser/animations",
|
||||
# "//packages/animations",
|
||||
# "//packages/common/http",
|
||||
# "//packages/core",
|
||||
# "//packages/core/src/util",
|
||||
# ],
|
||||
# platform = "browser",
|
||||
# )
|
||||
|
||||
@@ -10,6 +10,21 @@
|
||||
<mat-icon> info </mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select matTooltip="Select a frame to inspect with Angular Devtools" class="frame-selector" (change)="emitSelectedFrame($event.target.value)">
|
||||
@for (frame of frameManager.frames; track frame.id) {
|
||||
<option [value]="frame.id" [selected]="frameManager.isSelectedFrame(frame)">
|
||||
@if (frame.id === TOP_LEVEL_FRAME_ID) {
|
||||
top
|
||||
} @else {
|
||||
{{ frame.name }} ({{ frame.id }})
|
||||
}
|
||||
</option>
|
||||
} @empty {
|
||||
<option value="0" selected>top</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
@for (tab of tabs; track $index) {
|
||||
<a class="mat-tab-link" mat-tab-link (click)="changeTab(tab)" [active]="activeTab === tab">
|
||||
{{ tab }}
|
||||
@@ -27,19 +42,23 @@
|
||||
</section>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<div class="tab-content">
|
||||
<ng-directive-explorer
|
||||
[showCommentNodes]="showCommentNodes"
|
||||
[isHydrationEnabled]="isHydrationEnabled"
|
||||
[class.hidden]="activeTab !== 'Components'"
|
||||
(toggleInspector)="toggleInspector()"
|
||||
/>
|
||||
<ng-profiler [class.hidden]="activeTab !== 'Profiler'"/>
|
||||
<ng-router-tree [routes]="routes" [class.hidden]="activeTab !== 'Router Tree'"/>
|
||||
<ng-injector-tree [class.hidden]="activeTab !== 'Injector Tree'"/>
|
||||
</div>
|
||||
@if (!applicationEnvironment.frameSelectorEnabled || frameManager.selectedFrame !== null) {
|
||||
<div class="tab-content">
|
||||
<ng-directive-explorer
|
||||
[showCommentNodes]="showCommentNodes"
|
||||
[isHydrationEnabled]="isHydrationEnabled"
|
||||
[class.hidden]="activeTab !== 'Components'"
|
||||
(toggleInspector)="toggleInspector()"
|
||||
/>
|
||||
<ng-profiler [class.hidden]="activeTab !== 'Profiler'"/>
|
||||
<ng-router-tree [routes]="routes" [class.hidden]="activeTab !== 'Router Tree'"/>
|
||||
<ng-injector-tree [class.hidden]="activeTab !== 'Injector Tree'"/>
|
||||
</div>
|
||||
}
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
<div mat-menu-item disableRipple (click)="$event.stopPropagation(); toggleTimingAPI()">
|
||||
<mat-slide-toggle [checked]="timingAPIEnabled">
|
||||
@@ -57,6 +76,7 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #info="matMenu">
|
||||
<a mat-menu-item href="https://angular.io/devtools" target="_blank">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
|
||||
@@ -119,3 +119,22 @@ mat-icon {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frame-selector {
|
||||
background-color: #e2e2e2;
|
||||
border-radius: 2px;
|
||||
color: #474747;
|
||||
border: none;
|
||||
margin: 4px 4px 2px 4px;
|
||||
padding: 2px;
|
||||
outline-offset: -2px;
|
||||
width: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:host-context(.dark-theme) {
|
||||
.frame-selector {
|
||||
background-color: #464646;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AfterViewInit, Component, Input, OnInit, ViewChild} from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
|
||||
@@ -15,7 +24,8 @@ import {MatTabLink, MatTabNav, MatTabNavPanel} from '@angular/material/tabs';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {Events, MessageBus, Route} from 'protocol';
|
||||
|
||||
import {ApplicationEnvironment} from '../application-environment/index';
|
||||
import {ApplicationEnvironment, Frame, TOP_LEVEL_FRAME_ID} from '../application-environment/index';
|
||||
import {FrameManager} from '../frame_manager';
|
||||
import {Theme, ThemeService} from '../theme-service';
|
||||
|
||||
import {DirectiveExplorerComponent} from './directive-explorer/directive-explorer.component';
|
||||
@@ -52,25 +62,28 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
||||
@Input() angularVersion: string | undefined = undefined;
|
||||
@Input() isHydrationEnabled = false;
|
||||
|
||||
@Output() frameSelected = new EventEmitter<Frame>();
|
||||
@ViewChild(DirectiveExplorerComponent) directiveExplorer!: DirectiveExplorerComponent;
|
||||
@ViewChild('navBar', {static: true}) navbar!: MatTabNav;
|
||||
|
||||
applicationEnvironment = inject(ApplicationEnvironment);
|
||||
activeTab: Tabs = 'Components';
|
||||
|
||||
inspectorRunning = false;
|
||||
routerTreeEnabled = false;
|
||||
showCommentNodes = false;
|
||||
timingAPIEnabled = false;
|
||||
|
||||
currentTheme!: Theme;
|
||||
|
||||
routes: Route[] = [];
|
||||
|
||||
frameManager = inject(FrameManager);
|
||||
|
||||
TOP_LEVEL_FRAME_ID = TOP_LEVEL_FRAME_ID;
|
||||
|
||||
constructor(
|
||||
public tabUpdate: TabUpdate,
|
||||
public themeService: ThemeService,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _applicationEnvironment: ApplicationEnvironment,
|
||||
) {
|
||||
this.themeService.currentTheme
|
||||
.pipe(takeUntilDestroyed())
|
||||
@@ -81,6 +94,11 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
emitSelectedFrame(frameId: string): void {
|
||||
const frame = this.frameManager.frames.find((frame) => frame.id === parseInt(frameId, 10));
|
||||
this.frameSelected.emit(frame);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.navbar.stretchTabs = false;
|
||||
}
|
||||
@@ -95,7 +113,7 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
get latestSHA(): string {
|
||||
return this._applicationEnvironment.environment.LATEST_SHA.slice(0, 8);
|
||||
return this.applicationEnvironment.environment.LATEST_SHA.slice(0, 8);
|
||||
}
|
||||
|
||||
changeTab(tab: Tabs): void {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {Theme, ThemeService} from '../theme-service';
|
||||
import {DevToolsTabsComponent} from './devtools-tabs.component';
|
||||
import {TabUpdate} from './tab-update/index';
|
||||
import {DirectiveExplorerComponent} from './directive-explorer/directive-explorer.component';
|
||||
import {FrameManager} from '../frame_manager';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-directive-explorer',
|
||||
@@ -36,6 +37,7 @@ describe('DevtoolsTabsComponent', () => {
|
||||
beforeEach(() => {
|
||||
messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']);
|
||||
applicationEnvironmentMock = jasmine.createSpyObj('applicationEnvironment', ['environment']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatTooltip, MatMenuModule, DevToolsTabsComponent],
|
||||
providers: [
|
||||
@@ -43,6 +45,7 @@ describe('DevtoolsTabsComponent', () => {
|
||||
{provide: ThemeService, useFactory: () => ({currentTheme: new Subject<Theme>()})},
|
||||
{provide: MessageBus, useValue: messageBusMock},
|
||||
{provide: ApplicationEnvironment, useValue: applicationEnvironmentMock},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
|
||||
],
|
||||
}).overrideComponent(DevToolsTabsComponent, {
|
||||
remove: {imports: [DirectiveExplorerComponent]},
|
||||
@@ -74,4 +77,24 @@ describe('DevtoolsTabsComponent', () => {
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('inspectorEnd');
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('removeHighlightOverlay');
|
||||
});
|
||||
|
||||
it('should emit a selectedFrame when emitSelectedFrame is called', () => {
|
||||
let contentScriptConnected: Function = () => {};
|
||||
|
||||
// mock message bus on method with jasmine fake call in order to pick out callback
|
||||
// and call it with frame
|
||||
(messageBusMock.on as any).and.callFake((topic: string, cb: Function) => {
|
||||
if (topic === 'contentScriptConnected') {
|
||||
contentScriptConnected = cb;
|
||||
}
|
||||
});
|
||||
|
||||
const frameId = 1;
|
||||
expect(contentScriptConnected).toEqual(jasmine.any(Function));
|
||||
contentScriptConnected(frameId, 'name', 'http://localhost:4200/url');
|
||||
spyOn(comp.frameSelected, 'emit');
|
||||
comp.emitSelectedFrame('1');
|
||||
|
||||
expect(comp.frameSelected.emit).toHaveBeenCalledWith(comp.frameManager.frames[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,10 +43,14 @@ ts_test_library(
|
||||
srcs = ["directive-explorer.spec.ts"],
|
||||
deps = [
|
||||
":directive-explorer",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-operations",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/breadcrumbs",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/tab-update",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
@@ -59,8 +63,5 @@ karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [
|
||||
":directive_explorer_test",
|
||||
"//packages/common/http",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser/animations",
|
||||
],
|
||||
)
|
||||
|
||||
+46
-12
@@ -11,6 +11,7 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
|
||||
import {SplitComponent} from '../../../lib/vendor/angular-split/public_api';
|
||||
import {ApplicationOperations} from '../../application-operations/index';
|
||||
import {FrameManager} from '../../frame_manager';
|
||||
|
||||
import {BreadcrumbsComponent} from './directive-forest/breadcrumbs/breadcrumbs.component';
|
||||
import {FlatNode} from './directive-forest/component-data-source';
|
||||
@@ -122,11 +124,12 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
||||
private _refreshRetryTimeout: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
constructor(
|
||||
private _appOperations: ApplicationOperations,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _propResolver: ElementPropertyResolver,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
private _ngZone: NgZone,
|
||||
private readonly _appOperations: ApplicationOperations,
|
||||
private readonly _messageBus: MessageBus<Events>,
|
||||
private readonly _propResolver: ElementPropertyResolver,
|
||||
private readonly _cdr: ChangeDetectorRef,
|
||||
private readonly _ngZone: NgZone,
|
||||
private readonly _frameManager: FrameManager,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -198,18 +201,37 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
||||
(directive) => directive.name === directiveName,
|
||||
);
|
||||
|
||||
if (directiveIndex === -1) {
|
||||
// view the component definition
|
||||
this._appOperations.viewSource(this.currentSelectedElement.position);
|
||||
const selectedFrame = this._frameManager.selectedFrame;
|
||||
if (!this._frameManager.frameHasUniqueUrl(selectedFrame)) {
|
||||
this._messageBus.emit('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot view source.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// view the directive definition
|
||||
this._appOperations.viewSource(this.currentSelectedElement.position, directiveIndex);
|
||||
this._appOperations.viewSource(
|
||||
this.currentSelectedElement.position,
|
||||
directiveIndex !== -1 ? directiveIndex : undefined,
|
||||
new URL(selectedFrame!.url),
|
||||
);
|
||||
}
|
||||
|
||||
handleSelectDomElement(node: IndexedNode): void {
|
||||
this._appOperations.selectDomElement(node.position);
|
||||
const selectedFrame = this._frameManager.selectedFrame;
|
||||
if (!this._frameManager.frameHasUniqueUrl(selectedFrame)) {
|
||||
this._messageBus.emit('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot select DOM element.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this._appOperations.selectDomElement(node.position, new URL(selectedFrame!.url));
|
||||
}
|
||||
|
||||
highlight(node: FlatNode): void {
|
||||
@@ -278,7 +300,19 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
||||
directivePosition: DirectivePosition;
|
||||
}): void {
|
||||
const objectPath = constructPathOfKeysToPropertyValue(node.prop);
|
||||
this._appOperations.inspect(directivePosition, objectPath);
|
||||
|
||||
const selectedFrame = this._frameManager.selectedFrame;
|
||||
if (!this._frameManager.frameHasUniqueUrl(selectedFrame)) {
|
||||
this._messageBus.emit('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot inspect object.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this._appOperations.inspect(directivePosition, objectPath, new URL(selectedFrame!.url));
|
||||
}
|
||||
|
||||
hightlightHydrationNodes() {
|
||||
|
||||
+238
-17
@@ -6,37 +6,94 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Events, MessageBus, PropertyQueryTypes} from 'protocol';
|
||||
|
||||
import {ApplicationOperations} from '../../application-operations';
|
||||
import {DirectivePosition, MessageBus, PropType, PropertyQueryTypes} from 'protocol';
|
||||
|
||||
import {DirectiveExplorerComponent} from './directive-explorer.component';
|
||||
import {DirectiveForestComponent} from './directive-forest/directive-forest.component';
|
||||
import {IndexedNode} from './directive-forest/index-forest';
|
||||
import {ElementPropertyResolver} from './property-resolver/element-property-resolver';
|
||||
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {FrameManager} from '../../frame_manager';
|
||||
import {TabUpdate} from '../tab-update';
|
||||
import {Component, EventEmitter, Input, Output, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {ElementPropertyResolver, FlatNode} from './property-resolver/element-property-resolver';
|
||||
import {BreadcrumbsComponent} from './directive-forest/breadcrumbs/breadcrumbs.component';
|
||||
import {PropertyTabComponent} from './property-tab/property-tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-directive-forest',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class MockDirectiveForestComponent {
|
||||
@Input() forest: IndexedNode[] = [];
|
||||
@Input() currentSelectedElement: IndexedNode | null = null;
|
||||
@Input() showCommentNodes = false;
|
||||
@Output() selectNode = new EventEmitter<IndexedNode>();
|
||||
@Output() selectDomElement = new EventEmitter<IndexedNode>();
|
||||
@Output() setParents = new EventEmitter<IndexedNode>();
|
||||
@Output() highlightComponent = new EventEmitter<IndexedNode>();
|
||||
@Output() removeComponentHighlight = new EventEmitter<void>();
|
||||
@Output() toggleInspector = new EventEmitter<void>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-breadcrumbs',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class MockBreadcrumbsComponent {
|
||||
@Input() parents: IndexedNode[] = [];
|
||||
@Output() handleSelect = new EventEmitter<any>();
|
||||
@Output() mouseLeaveNode = new EventEmitter<any>();
|
||||
@Output() mouseOverNode = new EventEmitter<any>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-property-tab',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class MockPropertyTabComponent {
|
||||
@Input() currentSelectedElement: IndexedNode | null = null;
|
||||
@Output() inspect = new EventEmitter<{node: FlatNode; directivePosition: DirectivePosition}>();
|
||||
@Output() viewSource = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
describe('DirectiveExplorerComponent', () => {
|
||||
let messageBusMock: SpyObj<MessageBus<Events>>;
|
||||
let messageBusMock: SpyObj<any>;
|
||||
let fixture: ComponentFixture<DirectiveExplorerComponent>;
|
||||
let comp: DirectiveExplorerComponent;
|
||||
let applicationOperationsSpy: SpyObj<ApplicationOperations>;
|
||||
let contentScriptConnected = (frameId: number, name: string, url: string) => {};
|
||||
let frameConnected = (frameId: number) => {};
|
||||
|
||||
beforeEach(() => {
|
||||
applicationOperationsSpy = jasmine.createSpyObj<ApplicationOperations>('_appOperations', [
|
||||
'viewSource',
|
||||
'selectDomElement',
|
||||
'inspect',
|
||||
]);
|
||||
messageBusMock = jasmine.createSpyObj<MessageBus<Events>>('messageBus', [
|
||||
'on',
|
||||
'once',
|
||||
'emit',
|
||||
'destroy',
|
||||
]);
|
||||
|
||||
messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']);
|
||||
|
||||
messageBusMock.on.and.callFake((topic: string, cb: Function) => {
|
||||
if (topic === 'contentScriptConnected') {
|
||||
contentScriptConnected = cb as (frameId: number, name: string, url: string) => void;
|
||||
}
|
||||
if (topic === 'frameConnected') {
|
||||
frameConnected = cb as (frameId: number) => void;
|
||||
}
|
||||
});
|
||||
messageBusMock.emit.and.callFake((topic: string, args: any[]) => {
|
||||
if (topic === 'enableFrameConnection') {
|
||||
frameConnected(args[0]);
|
||||
}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -46,14 +103,24 @@ describe('DirectiveExplorerComponent', () => {
|
||||
provide: ElementPropertyResolver,
|
||||
useValue: new ElementPropertyResolver(messageBusMock),
|
||||
},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
|
||||
],
|
||||
}).overrideComponent(DirectiveExplorerComponent, {
|
||||
add: {schemas: [CUSTOM_ELEMENTS_SCHEMA]},
|
||||
remove: {imports: [DirectiveForestComponent]},
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DirectiveExplorerComponent);
|
||||
fixture = TestBed.overrideComponent(DirectiveExplorerComponent, {
|
||||
remove: {imports: [DirectiveForestComponent, BreadcrumbsComponent, PropertyTabComponent]},
|
||||
add: {
|
||||
imports: [MockDirectiveForestComponent, MockBreadcrumbsComponent, MockPropertyTabComponent],
|
||||
},
|
||||
}).createComponent(DirectiveExplorerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
TestBed.inject(FrameManager);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create instance from class', () => {
|
||||
@@ -61,8 +128,6 @@ describe('DirectiveExplorerComponent', () => {
|
||||
});
|
||||
|
||||
it('subscribe to backend events', () => {
|
||||
comp.subscribeToBackendEvents();
|
||||
expect(messageBusMock.on).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.on).toHaveBeenCalledWith(
|
||||
'latestComponentExplorerView',
|
||||
jasmine.any(Function),
|
||||
@@ -73,7 +138,9 @@ describe('DirectiveExplorerComponent', () => {
|
||||
describe('refresh', () => {
|
||||
it('should emit getLatestComponentExplorerView event on refresh', () => {
|
||||
comp.refresh();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should emit getLatestComponentExplorerView event with null view query', () => {
|
||||
@@ -110,7 +177,6 @@ describe('DirectiveExplorerComponent', () => {
|
||||
const position = [0];
|
||||
nodeMock.position = position;
|
||||
comp.handleNodeSelection(nodeMock);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('setSelectedComponent', [nodeMock.position]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
{
|
||||
@@ -126,11 +192,9 @@ describe('DirectiveExplorerComponent', () => {
|
||||
describe('hydration', () => {
|
||||
it('should highlight hydration nodes', () => {
|
||||
comp.hightlightHydrationNodes();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('createHydrationOverlay');
|
||||
|
||||
comp.removeHydrationNodesHightlights();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('removeHydrationOverlay');
|
||||
});
|
||||
|
||||
@@ -146,4 +210,161 @@ describe('DirectiveExplorerComponent', () => {
|
||||
expect(toggle2).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applicaton operations', () => {
|
||||
describe('view source', () => {
|
||||
it('should not call application operations view source if no frames are detected', () => {
|
||||
const directiveName = 'test';
|
||||
comp.currentSelectedElement = {
|
||||
directives: [{name: directiveName}],
|
||||
position: [0],
|
||||
children: [] as IndexedNode[],
|
||||
} as IndexedNode;
|
||||
comp.viewSource(directiveName);
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not call application operations view source if a frame is selected that does not have a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url');
|
||||
|
||||
const directiveName = 'test';
|
||||
comp.currentSelectedElement = {
|
||||
directives: [{name: directiveName}],
|
||||
position: [0],
|
||||
children: [] as IndexedNode[],
|
||||
} as IndexedNode;
|
||||
|
||||
comp.viewSource(directiveName);
|
||||
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(0);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot view source.`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call application operations view source if a frame is selected that has a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url2');
|
||||
|
||||
const directiveName = 'test';
|
||||
comp.currentSelectedElement = {
|
||||
directives: [{name: directiveName}],
|
||||
position: [0],
|
||||
children: [] as IndexedNode[],
|
||||
} as IndexedNode;
|
||||
|
||||
comp.viewSource(directiveName);
|
||||
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledWith(
|
||||
[0], // current selected element position
|
||||
0, // directive index
|
||||
new URL('http://localhost:4200/url'), // selected frame url
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('select dom element', () => {
|
||||
it('should not call application operations select dom element if no frames are detected', () => {
|
||||
comp.handleSelectDomElement({position: [0], children: [] as IndexedNode[]} as IndexedNode);
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not call application operations select dom element if a frame is selected that does not have a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url');
|
||||
|
||||
comp.handleSelectDomElement({position: [0], children: [] as IndexedNode[]} as IndexedNode);
|
||||
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(0);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot select DOM element.`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call application operations select dom element if a frame is selected that has a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url2');
|
||||
|
||||
comp.handleSelectDomElement({position: [0], children: [] as IndexedNode[]} as IndexedNode);
|
||||
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledWith(
|
||||
[0], // current selected element position
|
||||
new URL('http://localhost:4200/url'), // selected frame url
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspect', () => {
|
||||
let node: FlatNode;
|
||||
let directivePosition: DirectivePosition;
|
||||
|
||||
beforeEach(() => {
|
||||
node = {
|
||||
expandable: true,
|
||||
prop: {
|
||||
name: 'foo',
|
||||
parent: null,
|
||||
descriptor: {
|
||||
expandable: true,
|
||||
editable: false,
|
||||
type: PropType.String,
|
||||
preview: 'preview',
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
level: 1,
|
||||
};
|
||||
directivePosition = {element: [0], directive: 0};
|
||||
});
|
||||
|
||||
it('should not call application operations inspect if no frames are detected', () => {
|
||||
comp.inspect({node, directivePosition});
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not call application operations inspect if a frame is selected that does not have a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url');
|
||||
|
||||
comp.inspect({node, directivePosition});
|
||||
|
||||
expect(applicationOperationsSpy.inspect).toHaveBeenCalledTimes(0);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot inspect object.`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call application operations inspect if a frame is selected that has a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url2');
|
||||
|
||||
comp.inspect({node, directivePosition});
|
||||
|
||||
expect(applicationOperationsSpy.inspect).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(applicationOperationsSpy.inspect).toHaveBeenCalledWith(
|
||||
directivePosition,
|
||||
['foo'],
|
||||
new URL('http://localhost:4200/url'), // selected frame url
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ import {isChildOf, parentCollapsed} from './directive-forest-utils';
|
||||
import {IndexedNode} from './index-forest';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {FilterComponent} from './filter/filter.component';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-directive-forest',
|
||||
@@ -45,6 +46,7 @@ import {FilterComponent} from './filter/filter.component';
|
||||
CdkFixedSizeVirtualScroll,
|
||||
CdkVirtualForOf,
|
||||
MatIcon,
|
||||
MatTooltip,
|
||||
],
|
||||
})
|
||||
export class DirectiveForestComponent {
|
||||
|
||||
+2
@@ -47,6 +47,8 @@ ng_module(
|
||||
"property-tab-body.component.html",
|
||||
] + _STYLE_LABELS,
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:injector_tree_visualizer",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:resolution_path",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
<mat-toolbar>
|
||||
{{ directive }}
|
||||
<button matTooltip="Open source" (click)="handleViewSource($event)">
|
||||
<mat-icon> code </mat-icon>
|
||||
</button>
|
||||
{{ directive }}
|
||||
<button matTooltip="Open source" (click)="handleViewSource($event)">
|
||||
<mat-icon> code </mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
@@ -1,58 +1,57 @@
|
||||
<div class="mat-typography mat-app-background" style="height: 100%;">
|
||||
@if (angularExists === true) {
|
||||
@if (angularIsInDevMode) {
|
||||
@if (supportedVersion) {
|
||||
<div class="devtools-wrapper noselect" [@enterAnimation]>
|
||||
<ng-devtools-tabs [angularVersion]="angularVersion" [isHydrationEnabled]="hydration"/>
|
||||
</div>
|
||||
<div class="devtools mat-typography mat-app-background" style="height: 100%;">
|
||||
@switch (angularStatus) {
|
||||
@case (AngularStatus.EXISTS) {
|
||||
@if (angularIsInDevMode) {
|
||||
@if (supportedVersion) {
|
||||
<div class="devtools-wrapper noselect" [@enterAnimation]>
|
||||
<ng-devtools-tabs (frameSelected)="inspectFrame($event)" [isHydrationEnabled]="hydration" [angularVersion]="angularVersion"></ng-devtools-tabs>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="text-message">
|
||||
Angular Devtools only supports Angular versions 12 and above
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
<p class="text-message">
|
||||
Angular Devtools only supports Angular versions 12 and above
|
||||
<p class="text-message" matTooltip="A dev build is when the `optimization` flag is set to `false` in the angular.json config file.">
|
||||
We detected an application built with production configuration. Angular DevTools only supports development build.
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
<p class="text-message" title="A dev build is when the `optimization` flag is set to `true` in the angular.json config file.">
|
||||
We detected an application built with production configuration. Angular DevTools only supports development build.
|
||||
}
|
||||
@case (AngularStatus.DOES_NOT_EXIST) {
|
||||
<p class="text-message not-detected">
|
||||
<span class="info-icon" matTooltip="You see this message because the app is still loading, or this is not an Angular application">i</span>
|
||||
Angular application not detected.
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
@if (angularExists === false) {
|
||||
<p class="text-message">
|
||||
<span class="info-icon" matTooltip="You see this message because the app is still loading, or this is not an Angular application">i</span
|
||||
>
|
||||
Angular application not detected.
|
||||
</p>
|
||||
} @else {
|
||||
@if (angularExists === null) {
|
||||
<div class="initializing">
|
||||
<div class="loading">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="120">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="url(#b)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
<path fill="url(#c)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="49.009" x2="225.829" y1="213.75" y2="129.722" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E40035"></stop>
|
||||
<stop offset=".24" stop-color="#F60A48"></stop>
|
||||
<stop offset=".352" stop-color="#F20755"></stop>
|
||||
<stop offset=".494" stop-color="#DC087D"></stop>
|
||||
<stop offset=".745" stop-color="#9717E7"></stop>
|
||||
<stop offset="1" stop-color="#6C00F5"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="41.025" x2="156.741" y1="28.344" y2="160.344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF31D9"></stop>
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h223v236H0z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@case (AngularStatus.UNKNOWN) {
|
||||
<div class="initializing">
|
||||
<div class="loading">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="120">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="url(#b)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
<path fill="url(#c)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="49.009" x2="225.829" y1="213.75" y2="129.722" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E40035"></stop>
|
||||
<stop offset=".24" stop-color="#F60A48"></stop>
|
||||
<stop offset=".352" stop-color="#F20755"></stop>
|
||||
<stop offset=".494" stop-color="#DC087D"></stop>
|
||||
<stop offset=".745" stop-color="#9717E7"></stop>
|
||||
<stop offset="1" stop-color="#6C00F5"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="41.025" x2="156.741" y1="28.344" y2="160.344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF31D9"></stop>
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h223v236H0z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,36 @@
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {Platform} from '@angular/cdk/platform';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {Component, Inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Events, MessageBus} from 'protocol';
|
||||
import {interval} from 'rxjs';
|
||||
|
||||
import {FrameManager} from './frame_manager';
|
||||
import {ThemeService} from './theme-service';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {MatTooltip, MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
import {Frame} from './application-environment';
|
||||
|
||||
const DETECT_ANGULAR_ATTEMPTS = 10;
|
||||
|
||||
enum AngularStatus {
|
||||
/**
|
||||
* This page may have Angular but we don't know yet. We're still trying to detect it.
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* We've given up on trying to detect Angular. We tried ${DETECT_ANGULAR_ATTEMPTS} times and
|
||||
* failed.
|
||||
*/
|
||||
DOES_NOT_EXIST,
|
||||
|
||||
/**
|
||||
* Angular was detected somewhere on the page.
|
||||
*/
|
||||
EXISTS,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools',
|
||||
@@ -28,10 +51,11 @@ import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component';
|
||||
]),
|
||||
],
|
||||
standalone: true,
|
||||
imports: [DevToolsTabsComponent, MatTooltip],
|
||||
imports: [DevToolsTabsComponent, MatTooltip, MatProgressSpinnerModule, MatTooltipModule],
|
||||
})
|
||||
export class DevToolsComponent implements OnInit, OnDestroy {
|
||||
angularExists: boolean | null = null;
|
||||
AngularStatus = AngularStatus;
|
||||
angularStatus: AngularStatus = AngularStatus.UNKNOWN;
|
||||
angularVersion: string | boolean | undefined = undefined;
|
||||
angularIsInDevMode = true;
|
||||
hydration: boolean = false;
|
||||
@@ -39,26 +63,28 @@ export class DevToolsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly _firefoxStyleName = 'firefox_styles.css';
|
||||
private readonly _chromeStyleName = 'chrome_styles.css';
|
||||
|
||||
constructor(
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _themeService: ThemeService,
|
||||
private _platform: Platform,
|
||||
@Inject(DOCUMENT) private _document: Document,
|
||||
) {}
|
||||
private readonly _messageBus = inject<MessageBus<Events>>(MessageBus);
|
||||
private readonly _themeService = inject(ThemeService);
|
||||
private readonly _platform = inject(Platform);
|
||||
private readonly _document = inject(DOCUMENT);
|
||||
private readonly _frameManager = inject(FrameManager);
|
||||
|
||||
private _interval$ = interval(500).subscribe((attempt) => {
|
||||
if (attempt === 10) {
|
||||
this.angularExists = false;
|
||||
if (attempt === DETECT_ANGULAR_ATTEMPTS) {
|
||||
this.angularStatus = AngularStatus.DOES_NOT_EXIST;
|
||||
}
|
||||
this._messageBus.emit('queryNgAvailability');
|
||||
});
|
||||
|
||||
inspectFrame(frame: Frame) {
|
||||
this._frameManager.inspectFrame(frame);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._themeService.initializeThemeWatcher();
|
||||
|
||||
this._messageBus.once('ngAvailability', ({version, devMode, ivy, hydration}) => {
|
||||
this.angularExists = !!version;
|
||||
this.angularStatus = version ? AngularStatus.EXISTS : AngularStatus.DOES_NOT_EXIST;
|
||||
this.angularVersion = version;
|
||||
this.angularIsInDevMode = devMode;
|
||||
this.ivy = ivy;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {FrameManager} from './frame_manager';
|
||||
import {DevToolsComponent} from './devtools.component';
|
||||
import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component';
|
||||
import {MessageBus} from 'protocol';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools-tabs',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
export class MockNgDevToolsTabs {}
|
||||
|
||||
describe('DevtoolsComponent', () => {
|
||||
let fixture: ComponentFixture<DevToolsComponent>;
|
||||
let component: DevToolsComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockMessageBus = jasmine.createSpyObj('MessageBus', ['on', 'emit', 'once']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: MessageBus, useValue: mockMessageBus}],
|
||||
}).overrideComponent(DevToolsComponent, {
|
||||
remove: {imports: [DevToolsTabsComponent], providers: [FrameManager]},
|
||||
add: {
|
||||
imports: [MockNgDevToolsTabs],
|
||||
providers: [{provide: FrameManager, useFactory: () => FrameManager.initialize(123)}],
|
||||
},
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DevToolsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should render ng devtools tabs when Angular Status is EXISTS and is in dev mode and is supported version', () => {
|
||||
component.angularStatus = component.AngularStatus.EXISTS;
|
||||
component.angularIsInDevMode = true;
|
||||
component.angularVersion = '0.0.0';
|
||||
component.ivy = true;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('ng-devtools-tabs')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render Angular Devtools dev mode only support text when Angular Status is EXISTS and is angular is not in dev mode', () => {
|
||||
component.angularStatus = component.AngularStatus.EXISTS;
|
||||
component.angularIsInDevMode = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.devtools').textContent).toContain(
|
||||
'We detected an application built with production configuration. Angular DevTools only supports development build.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render version support message when Angular Status is EXISTS and angular version is not supported', () => {
|
||||
component.angularStatus = component.AngularStatus.EXISTS;
|
||||
component.angularIsInDevMode = true;
|
||||
component.angularVersion = '1.0.0';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.devtools').textContent).toContain(
|
||||
'Angular Devtools only supports Angular versions 12 and above',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Angular application not detected when Angular Status is DOES_NOT_EXIST', () => {
|
||||
component.angularStatus = component.AngularStatus.DOES_NOT_EXIST;
|
||||
fixture.detectChanges();
|
||||
// expect the text to be "Angular application not detected"
|
||||
expect(fixture.nativeElement.querySelector('.not-detected').textContent).toContain(
|
||||
'Angular application not detected',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render loading svg when Angular Status is UNKNOWN', () => {
|
||||
component.angularStatus = component.AngularStatus.UNKNOWN;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.loading svg')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {Events, MessageBus} from 'protocol';
|
||||
|
||||
import {Frame, TOP_LEVEL_FRAME_ID} from './application-environment';
|
||||
|
||||
@Injectable()
|
||||
export class FrameManager {
|
||||
private _selectedFrameId: number | null = null;
|
||||
private _frames = new Map<number, Frame>();
|
||||
private _inspectedWindowTabId: number | null = null;
|
||||
private _frameUrlToFrameIds = new Map<string, Set<number>>();
|
||||
private _messageBus = inject<MessageBus<Events>>(MessageBus);
|
||||
|
||||
get frames(): Frame[] {
|
||||
return Array.from(this._frames.values());
|
||||
}
|
||||
|
||||
get selectedFrame(): Frame | null {
|
||||
if (this._selectedFrameId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._frames.get(this._selectedFrameId) ?? null;
|
||||
}
|
||||
|
||||
static initialize(inspectedWindowTabIdTestOnly?: number | null) {
|
||||
const manager = new FrameManager();
|
||||
manager.initialize(inspectedWindowTabIdTestOnly);
|
||||
return manager;
|
||||
}
|
||||
|
||||
private initialize(inspectedWindowTabIdTestOnly?: number | null): void {
|
||||
if (inspectedWindowTabIdTestOnly === undefined) {
|
||||
this._inspectedWindowTabId = globalThis.chrome.devtools.inspectedWindow.tabId;
|
||||
} else {
|
||||
this._inspectedWindowTabId = inspectedWindowTabIdTestOnly;
|
||||
}
|
||||
|
||||
this._messageBus.on('frameConnected', (frameId: number) => {
|
||||
if (this._frames.has(frameId)) {
|
||||
this._selectedFrameId = frameId;
|
||||
}
|
||||
});
|
||||
|
||||
this._messageBus.on('contentScriptConnected', (frameId: number, name: string, url: string) => {
|
||||
// fragments are not considered when doing URL matching on a page
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=841429
|
||||
const urlWithoutHash = new URL(url);
|
||||
urlWithoutHash.hash = '';
|
||||
|
||||
this.addFrame({name, id: frameId, url: urlWithoutHash});
|
||||
|
||||
if (this.frames.length === 1) {
|
||||
this.inspectFrame(this._frames.get(frameId)!);
|
||||
}
|
||||
});
|
||||
|
||||
this._messageBus.on('contentScriptDisconnected', (frameId: number) => {
|
||||
if (!this._frames.has(frameId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeFrame(this._frames.get(frameId)!);
|
||||
|
||||
// Defensive check. This case should never happen, since we're always connected to at least
|
||||
// the top level frame.
|
||||
if (this.frames.length === 0) {
|
||||
this._selectedFrameId = null;
|
||||
console.error('Angular DevTools is not connected to any frames.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameId === this._selectedFrameId) {
|
||||
this._selectedFrameId = TOP_LEVEL_FRAME_ID;
|
||||
this.inspectFrame(this._frames.get(this._selectedFrameId!)!);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isSelectedFrame(frame: Frame): boolean {
|
||||
return this._selectedFrameId === frame.id;
|
||||
}
|
||||
|
||||
inspectFrame(frame: Frame): void {
|
||||
if (this._inspectedWindowTabId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._frames.has(frame.id)) {
|
||||
throw new Error('Attempted to inspect a frame that is not connected to Angular DevTools.');
|
||||
}
|
||||
|
||||
this._selectedFrameId = null;
|
||||
this._messageBus.emit('enableFrameConnection', [frame.id, this._inspectedWindowTabId]);
|
||||
}
|
||||
|
||||
frameHasUniqueUrl(frame: Frame | null): boolean {
|
||||
if (frame === null) {
|
||||
return false;
|
||||
}
|
||||
const frameUrl = frame.url.toString();
|
||||
const frameIds = this._frameUrlToFrameIds.get(frameUrl) ?? new Set<number>();
|
||||
return frameIds.size === 1;
|
||||
}
|
||||
|
||||
private addFrame(frame: Frame): void {
|
||||
this._frames.set(frame.id, frame);
|
||||
const frameUrl = frame.url.toString();
|
||||
const frameIdSet = this._frameUrlToFrameIds.get(frameUrl) ?? new Set<number>();
|
||||
frameIdSet.add(frame.id);
|
||||
this._frameUrlToFrameIds.set(frameUrl, frameIdSet);
|
||||
}
|
||||
|
||||
private removeFrame(frame: Frame): void {
|
||||
const frameId = frame.id;
|
||||
const frameUrl = frame.url.toString();
|
||||
const urlFrameIds = this._frameUrlToFrameIds.get(frameUrl) ?? new Set<number>();
|
||||
urlFrameIds.delete(frameId);
|
||||
if (urlFrameIds.size === 0) {
|
||||
this._frameUrlToFrameIds.delete(frameUrl);
|
||||
}
|
||||
this._frames.delete(frameId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Events, MessageBus} from 'protocol';
|
||||
import {FrameManager} from './frame_manager';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Frame} from './application-environment';
|
||||
|
||||
describe('FrameManager', () => {
|
||||
let frameManager: FrameManager;
|
||||
let messageBus: MessageBus<Events>;
|
||||
let topicToCallback: {[topic: string]: Function | null};
|
||||
|
||||
function getFrameFromFrameManager(frameId: number): Frame | undefined {
|
||||
return frameManager.frames.find((f: Frame) => f.id === frameId);
|
||||
}
|
||||
|
||||
function frameConnected(frameId: number): void {
|
||||
topicToCallback['frameConnected']!(frameId);
|
||||
}
|
||||
|
||||
function contentScriptConnected(frameId: number, name: string, url: string): void {
|
||||
topicToCallback['contentScriptConnected']!(frameId, name, url);
|
||||
}
|
||||
|
||||
function contentScriptDisconnected(frameId: number): void {
|
||||
topicToCallback['contentScriptDisconnected']!(frameId);
|
||||
}
|
||||
|
||||
const topLevelFrameId = 0;
|
||||
const otherFrameId = 1;
|
||||
const tabId = 123;
|
||||
|
||||
beforeEach(() => {
|
||||
topicToCallback = {
|
||||
frameConnected: null,
|
||||
contentScriptConnected: null,
|
||||
contentScriptDisconnected: null,
|
||||
};
|
||||
messageBus = jasmine.createSpyObj('MessageBus', ['on', 'emit']);
|
||||
|
||||
(messageBus.on as any).and.callFake((topic: string, cb: Function) => {
|
||||
topicToCallback[topic] = cb;
|
||||
});
|
||||
|
||||
(messageBus.emit as any).and.callFake((topic: string, args: any[]) => {
|
||||
if (topic === 'enableFrameConnection') {
|
||||
frameConnected(args[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const testModule = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: MessageBus, useValue: messageBus},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
|
||||
],
|
||||
});
|
||||
|
||||
frameManager = testModule.inject(FrameManager);
|
||||
});
|
||||
|
||||
it('should add frame when contentScriptConnected event is emitted', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
expect(frameManager.frames.length).toBe(1);
|
||||
expect(frameManager.frames[0].id).toBe(topLevelFrameId);
|
||||
expect(frameManager.frames[0].name).toBe('name');
|
||||
expect(frameManager.frames[0].url.toString()).toBe('http://localhost:4200/url');
|
||||
});
|
||||
|
||||
it('should set the selected frame to the first frame when there is only one frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('should set selected frame when frameConnected event is emitted', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
});
|
||||
|
||||
it('should remove frame when contentScriptDisconnected event is emitted', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
expect(frameManager.frames.length).toBe(2);
|
||||
contentScriptDisconnected(otherFrameId);
|
||||
expect(frameManager.frames.length).toBe(1);
|
||||
expect(frameManager.frames[0].id).toBe(topLevelFrameId);
|
||||
|
||||
const errorSpy = spyOn(console, 'error');
|
||||
contentScriptDisconnected(topLevelFrameId);
|
||||
expect(frameManager.frames.length).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith('Angular DevTools is not connected to any frames.');
|
||||
});
|
||||
|
||||
it('should set selected frame to top level frame when contentScriptDisconnected event is emitted for selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
contentScriptDisconnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('should not set selected frame to top level frame when contentScriptDisconnected event is emitted for non selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(topLevelFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
contentScriptDisconnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('should not set selected frame to top level frame when contentScriptDisconnected event is emitted for non existing frame', () => {
|
||||
const nonExistingFrameId = 3;
|
||||
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
contentScriptDisconnected(nonExistingFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
});
|
||||
|
||||
it('isSelectedFrame should return true when frame matches selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
const otherFrame = getFrameFromFrameManager(otherFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
expect(otherFrame).toBeDefined();
|
||||
expect(frameManager.isSelectedFrame(topLevelFrame!)).toBe(true);
|
||||
});
|
||||
|
||||
it('isSelectedFrame should return false when frame does not match selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
const otherFrame = getFrameFromFrameManager(otherFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
expect(otherFrame).toBeDefined();
|
||||
expect(frameManager.isSelectedFrame(otherFrame!)).toBe(false);
|
||||
});
|
||||
|
||||
it('inspectFrame should emit enableFrameConnection message', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
frameManager.inspectFrame(topLevelFrame!);
|
||||
expect(messageBus.emit).toHaveBeenCalledWith('enableFrameConnection', [topLevelFrameId, tabId]);
|
||||
});
|
||||
|
||||
it('inspectFrame should set selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'https://angular.dev/');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
frameManager.inspectFrame(topLevelFrame!);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should return false when a two frames have the same url', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'https://angular.dev/');
|
||||
expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/');
|
||||
expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(false);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should return true when only one frame has a given url', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/');
|
||||
contentScriptConnected(otherFrameId, 'name', 'https://angular.dev/overview');
|
||||
expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/');
|
||||
expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(true);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should not consider url fragments as part of the url comparison', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/guide/components');
|
||||
contentScriptConnected(
|
||||
otherFrameId,
|
||||
'name',
|
||||
'https://angular.dev/guide/components#using-components',
|
||||
);
|
||||
expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/guide/components');
|
||||
expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(false);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should return false when frame is null', () => {
|
||||
expect(frameManager.frameHasUniqueUrl(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -213,6 +213,19 @@ export interface Route {
|
||||
isAux: boolean;
|
||||
}
|
||||
|
||||
export interface AngularDetection {
|
||||
// This is necessary because the runtime
|
||||
// message listener handles messages globally
|
||||
// including from other extensions. We don't
|
||||
// want to set icon and/or popup based on
|
||||
// a message coming from an unrelated extension.
|
||||
isAngularDevTools: true;
|
||||
isIvy: boolean;
|
||||
isAngular: boolean;
|
||||
isDebugMode: boolean;
|
||||
isSupportedAngularVersion: boolean;
|
||||
}
|
||||
|
||||
export type Topic = keyof Events;
|
||||
|
||||
export interface InjectorGraphViewQuery {
|
||||
@@ -273,4 +286,13 @@ export interface Events {
|
||||
) => void;
|
||||
|
||||
logProvider: (injector: SerializedInjector, providers: SerializedProviderRecord) => void;
|
||||
|
||||
contentScriptConnected: (frameId: number, name: string, url: string) => void;
|
||||
contentScriptDisconnected: (frameId: number, name: string, url: string) => void;
|
||||
enableFrameConnection: (frameId: number, tabId: number) => void;
|
||||
frameConnected: (frameId: number) => void;
|
||||
detectAngular: (detectionResult: AngularDetection) => void;
|
||||
backendReady: () => void;
|
||||
|
||||
log: (logEvent: {message: string; level: 'log' | 'warn' | 'debug' | 'error'}) => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
load("//devtools/tools:ng_module.bzl", "ng_module")
|
||||
load("//devtools/tools:typescript.bzl", "ts_library")
|
||||
load("//devtools/tools:typescript.bzl", "ts_library", "ts_test_library")
|
||||
load("//tools:defaults.bzl", "esbuild")
|
||||
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
@@ -15,7 +16,6 @@ ng_module(
|
||||
srcs = [
|
||||
"app.component.ts",
|
||||
"app.module.ts",
|
||||
"inject.ts",
|
||||
],
|
||||
angular_assets = [
|
||||
"app.component.html",
|
||||
@@ -35,6 +35,8 @@ ng_module(
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser/animations",
|
||||
"@npm//@angular/material",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -78,8 +80,10 @@ ts_library(
|
||||
"chrome-application-operations.ts",
|
||||
],
|
||||
deps = [
|
||||
":chrome_application_environment",
|
||||
"//devtools/projects/ng-devtools",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
"@npm//@types",
|
||||
],
|
||||
)
|
||||
@@ -126,7 +130,38 @@ ts_library(
|
||||
srcs = [
|
||||
"background.ts",
|
||||
],
|
||||
deps = ["//devtools/projects/shell-browser/src/app:detect_angular_for_extension_icon"],
|
||||
deps = [
|
||||
":tab_manager",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/projects/shell-browser/src/app:detect_angular_for_extension_icon",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "tab_manager",
|
||||
srcs = [
|
||||
"tab_manager.ts",
|
||||
],
|
||||
deps = [
|
||||
"//devtools/projects/protocol",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "tab_manager_test",
|
||||
deps = [
|
||||
":tab_manager_test_lib",
|
||||
],
|
||||
)
|
||||
|
||||
ts_test_library(
|
||||
name = "tab_manager_test_lib",
|
||||
srcs = [
|
||||
"tab_manager_spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":tab_manager",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
@@ -162,6 +197,8 @@ ts_library(
|
||||
"detect-angular-for-extension-icon.ts",
|
||||
],
|
||||
deps = [
|
||||
":same_page_message_bus",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/projects/shared-utils",
|
||||
"@npm//@types",
|
||||
],
|
||||
|
||||
@@ -6,39 +6,21 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, Component, NgZone, OnInit} from '@angular/core';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
|
||||
import {injectScripts} from './inject';
|
||||
import {ZoneAwareChromeMessageBus} from './zone-aware-chrome-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
import {ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(ngZone: NgZone): MessageBus<Events> {
|
||||
const port = chrome.runtime.connect({
|
||||
name: '' + chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
return new PriorityAwareMessageBus(new ZoneAwareChromeMessageBus(port, ngZone));
|
||||
},
|
||||
deps: [NgZone],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private _cd: ChangeDetectorRef) {}
|
||||
private _cd = inject(ChangeDetectorRef);
|
||||
|
||||
ngOnInit(): void {
|
||||
chrome.devtools.network.onNavigated.addListener(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
injectScripts(['app/backend_bundle.js']);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,24 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {NgModule, NgZone} from '@angular/core';
|
||||
import {MatSelect} from '@angular/material/select';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ApplicationEnvironment, ApplicationOperations, DevToolsComponent} from 'ng-devtools';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
import {ChromeApplicationEnvironment} from './chrome-application-environment';
|
||||
import {ChromeApplicationOperations} from './chrome-application-operations';
|
||||
import {ZoneAwareChromeMessageBus} from './zone-aware-chrome-message-bus';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
import {FrameManager} from '../../../../projects/ng-devtools/src/lib/frame_manager';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserAnimationsModule, DevToolsComponent],
|
||||
imports: [BrowserAnimationsModule, DevToolsComponent, MatSelect],
|
||||
bootstrap: [AppComponent],
|
||||
providers: [
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize()},
|
||||
{
|
||||
provide: ApplicationOperations,
|
||||
useClass: ChromeApplicationOperations,
|
||||
@@ -27,6 +32,17 @@ import {ChromeApplicationOperations} from './chrome-application-operations';
|
||||
provide: ApplicationEnvironment,
|
||||
useClass: ChromeApplicationEnvironment,
|
||||
},
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(ngZone: NgZone): MessageBus<Events> {
|
||||
const port = chrome.runtime.connect({
|
||||
name: '' + chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
|
||||
return new PriorityAwareMessageBus(new ZoneAwareChromeMessageBus(port, ngZone));
|
||||
},
|
||||
deps: [NgZone],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {initializeExtendedWindowOperations} from './chrome-window-extensions';
|
||||
import {SamePageMessageBus} from './same-page-message-bus';
|
||||
|
||||
const messageBus = new SamePageMessageBus(
|
||||
'angular-devtools-backend',
|
||||
'angular-devtools-content-script',
|
||||
`angular-devtools-backend-${location.href}`,
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
);
|
||||
|
||||
let initialized = false;
|
||||
@@ -47,4 +47,6 @@ messageBus.on('handshake', () => {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
messageBus.emit('backendReady');
|
||||
});
|
||||
|
||||
@@ -8,164 +8,10 @@
|
||||
|
||||
/// <reference types="chrome"/>
|
||||
|
||||
import {AngularDetection} from './detect-angular-for-extension-icon';
|
||||
import {AngularDetection} from 'protocol';
|
||||
import {TabManager, Tabs} from './tab_manager';
|
||||
|
||||
const isManifestV3 = chrome.runtime.getManifest().manifest_version === 3;
|
||||
|
||||
const browserAction = (() => {
|
||||
// Electron does not expose browserAction object,
|
||||
// Use empty calls as fallback if they are not defined.
|
||||
const noopAction = {setIcon: () => {}, setPopup: () => {}};
|
||||
|
||||
if (isManifestV3) {
|
||||
return chrome.action || noopAction;
|
||||
}
|
||||
|
||||
return chrome.browserAction || noopAction;
|
||||
})();
|
||||
|
||||
// By default use the black and white icon.
|
||||
// Replace it only when we detect an Angular app.
|
||||
browserAction.setIcon(
|
||||
{
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon-bw16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon-bw48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon-bw128.png`),
|
||||
},
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
const ports: {
|
||||
[tab: string]:
|
||||
| {
|
||||
'content-script': chrome.runtime.Port | null;
|
||||
devtools: chrome.runtime.Port | null;
|
||||
}
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
let tab: string | null = null;
|
||||
let name: 'devtools' | 'content-script' | null = null;
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Connection event in the background script');
|
||||
|
||||
if (isNumeric(port.name)) {
|
||||
tab = port.name;
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Angular devtools connected, injecting the content script', port.name, ports[tab]);
|
||||
|
||||
name = 'devtools';
|
||||
installContentScript(parseInt(port.name, 10));
|
||||
} else {
|
||||
if (!port.sender || !port.sender.tab) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error('Unable to access the port sender and sender tab');
|
||||
|
||||
return;
|
||||
}
|
||||
if (port.sender.tab.id === undefined) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error('Sender tab id is undefined');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Content script connected', port.sender.tab.id);
|
||||
tab = port.sender.tab.id.toString();
|
||||
name = 'content-script';
|
||||
}
|
||||
|
||||
let portsTab = ports[tab];
|
||||
if (!portsTab) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Creating a tab port');
|
||||
|
||||
portsTab = ports[tab] = {
|
||||
devtools: null,
|
||||
'content-script': null,
|
||||
};
|
||||
}
|
||||
|
||||
portsTab[name] = port;
|
||||
|
||||
if (portsTab.devtools && portsTab['content-script']) {
|
||||
doublePipe(portsTab.devtools, portsTab['content-script'], tab);
|
||||
}
|
||||
});
|
||||
|
||||
const isNumeric = (str: string): boolean => {
|
||||
return +str + '' === str;
|
||||
};
|
||||
|
||||
const installContentScript = (tabId: number) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Installing the content-script');
|
||||
|
||||
// We first inject the content-script and after that
|
||||
// invoke the global that it exposes.
|
||||
|
||||
if (isManifestV3) {
|
||||
chrome.scripting.executeScript(
|
||||
{files: ['app/content_script_bundle.js'], target: {tabId}},
|
||||
() => {
|
||||
chrome.scripting.executeScript({func: () => (globalThis as any).main(), target: {tabId}});
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// manifest V2 APIs
|
||||
chrome.tabs.executeScript(tabId, {file: 'app/content_script_bundle.js'}, (result) => {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
code: 'globalThis.main()',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const doublePipe = (
|
||||
devtoolsPort: chrome.runtime.Port | null,
|
||||
contentScriptPort: chrome.runtime.Port,
|
||||
tab: string,
|
||||
) => {
|
||||
if (devtoolsPort === null) {
|
||||
console.warn('DevTools port is equal to null');
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Creating two-way communication channel', Date.now(), ports);
|
||||
|
||||
const onDevToolsMessage = (message: chrome.runtime.Port) => {
|
||||
contentScriptPort.postMessage(message);
|
||||
};
|
||||
devtoolsPort.onMessage.addListener(onDevToolsMessage);
|
||||
|
||||
const onContentScriptMessage = (message: chrome.runtime.Port) => {
|
||||
devtoolsPort.postMessage(message);
|
||||
};
|
||||
contentScriptPort.onMessage.addListener(onContentScriptMessage);
|
||||
|
||||
const shutdown = (source: string) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Disconnecting', source);
|
||||
|
||||
devtoolsPort.onMessage.removeListener(onDevToolsMessage);
|
||||
contentScriptPort.onMessage.removeListener(onContentScriptMessage);
|
||||
devtoolsPort.disconnect();
|
||||
contentScriptPort.disconnect();
|
||||
ports[tab] = undefined;
|
||||
};
|
||||
devtoolsPort.onDisconnect.addListener(shutdown.bind(null, 'devtools'));
|
||||
contentScriptPort.onDisconnect.addListener(shutdown.bind(null, 'content-script'));
|
||||
};
|
||||
|
||||
const getPopUpName = (ng: AngularDetection) => {
|
||||
function getPopUpName(ng: AngularDetection): string {
|
||||
if (!ng.isAngular) {
|
||||
return 'not-angular.html';
|
||||
}
|
||||
@@ -176,29 +22,63 @@ const getPopUpName = (ng: AngularDetection) => {
|
||||
return 'production.html';
|
||||
}
|
||||
return 'supported.html';
|
||||
};
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((req: AngularDetection, sender) => {
|
||||
if (!req.isAngularDevTools) {
|
||||
return;
|
||||
}
|
||||
if (sender && sender.tab) {
|
||||
browserAction.setPopup({
|
||||
tabId: sender.tab.id,
|
||||
popup: `popups/${getPopUpName(req)}`,
|
||||
});
|
||||
}
|
||||
if (sender && sender.tab && req.isAngular) {
|
||||
browserAction.setIcon(
|
||||
{
|
||||
tabId: sender.tab.id,
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon128.png`),
|
||||
},
|
||||
if (chrome !== undefined && chrome.runtime !== undefined) {
|
||||
const isManifestV3 = chrome.runtime.getManifest().manifest_version === 3;
|
||||
|
||||
const browserAction = (() => {
|
||||
// Electron does not expose browserAction object,
|
||||
// Use empty calls as fallback if they are not defined.
|
||||
const noopAction = {setIcon: () => {}, setPopup: () => {}};
|
||||
|
||||
if (isManifestV3) {
|
||||
return chrome.action || noopAction;
|
||||
}
|
||||
|
||||
return chrome.browserAction || noopAction;
|
||||
})();
|
||||
|
||||
// By default use the black and white icon.
|
||||
// Replace it only when we detect an Angular app.
|
||||
browserAction.setIcon(
|
||||
{
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon-bw16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon-bw48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon-bw128.png`),
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
chrome.runtime.onMessage.addListener((req: AngularDetection, sender) => {
|
||||
if (!req.isAngularDevTools) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender && sender.tab) {
|
||||
browserAction.setPopup({
|
||||
tabId: sender.tab.id,
|
||||
popup: `popups/${getPopUpName(req)}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (sender && sender.tab && req.isAngular) {
|
||||
browserAction.setIcon(
|
||||
{
|
||||
tabId: sender.tab.id,
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon128.png`),
|
||||
},
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = {};
|
||||
TabManager.initialize(tabs);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {ApplicationEnvironment, Environment} from 'ng-devtools';
|
||||
import {environment} from '../environments/environment';
|
||||
|
||||
export class ChromeApplicationEnvironment extends ApplicationEnvironment {
|
||||
frameSelectorEnabled = true;
|
||||
|
||||
override get environment(): Environment {
|
||||
return environment;
|
||||
}
|
||||
|
||||
@@ -11,32 +11,29 @@
|
||||
import {ApplicationOperations} from 'ng-devtools';
|
||||
import {DirectivePosition, ElementPosition} from 'protocol';
|
||||
|
||||
function runInInspectedWindow(script: string, frameURL?: URL): void {
|
||||
chrome.devtools.inspectedWindow.eval(script, {frameURL: frameURL?.toString?.()});
|
||||
}
|
||||
|
||||
export class ChromeApplicationOperations extends ApplicationOperations {
|
||||
override viewSource(position: ElementPosition, directiveIndex: number): void {
|
||||
if (chrome.devtools) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`inspect(inspectedApplication.findConstructorByPosition('${position}', ${directiveIndex}))`,
|
||||
);
|
||||
}
|
||||
override viewSource(position: ElementPosition, directiveIndex?: number, target?: URL): void {
|
||||
const viewSource = `inspect(inspectedApplication.findConstructorByPosition('${position}', ${directiveIndex}))`;
|
||||
runInInspectedWindow(viewSource, target);
|
||||
}
|
||||
|
||||
override selectDomElement(position: ElementPosition): void {
|
||||
if (chrome.devtools) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`inspect(inspectedApplication.findDomElementByPosition('${position}'))`,
|
||||
);
|
||||
}
|
||||
override selectDomElement(position: ElementPosition, target?: URL): void {
|
||||
const selectDomElement = `inspect(inspectedApplication.findDomElementByPosition('${position}'))`;
|
||||
runInInspectedWindow(selectDomElement, target);
|
||||
}
|
||||
|
||||
override inspect(directivePosition: DirectivePosition, objectPath: string[]): void {
|
||||
if (chrome.devtools) {
|
||||
const args = {
|
||||
directivePosition,
|
||||
objectPath,
|
||||
};
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`inspect(inspectedApplication.findPropertyByPosition('${JSON.stringify(args)}'))`,
|
||||
);
|
||||
}
|
||||
override inspect(directivePosition: DirectivePosition, objectPath: string[], target?: URL): void {
|
||||
const args = {
|
||||
directivePosition,
|
||||
objectPath,
|
||||
};
|
||||
const inspect = `inspect(inspectedApplication.findPropertyByPosition('${JSON.stringify(
|
||||
args,
|
||||
)}'))`;
|
||||
runInInspectedWindow(inspect, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,59 +9,92 @@
|
||||
import {ChromeMessageBus} from './chrome-message-bus';
|
||||
import {SamePageMessageBus} from './same-page-message-bus';
|
||||
|
||||
export const main = () => {
|
||||
let backgroundDisconnected = false;
|
||||
let backendInitialized = false;
|
||||
let backgroundDisconnected = false;
|
||||
let backendInstalled = false;
|
||||
let backendInitialized = false;
|
||||
|
||||
// console.log('Content script executing', (window as any));
|
||||
const port = chrome.runtime.connect({
|
||||
name: `${document.title || location.href}`,
|
||||
});
|
||||
|
||||
const port = chrome.runtime.connect({
|
||||
name: 'content-script',
|
||||
});
|
||||
|
||||
const handleDisconnect = (): void => {
|
||||
// console.log('Background disconnected', new Date());
|
||||
localMessageBus.emit('shutdown');
|
||||
localMessageBus.destroy();
|
||||
chromeMessageBus.destroy();
|
||||
backgroundDisconnected = true;
|
||||
};
|
||||
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
|
||||
const localMessageBus = new SamePageMessageBus(
|
||||
'angular-devtools-content-script',
|
||||
'angular-devtools-backend',
|
||||
);
|
||||
const chromeMessageBus = new ChromeMessageBus(port);
|
||||
|
||||
const handshakeWithBackend = (): void => {
|
||||
localMessageBus.emit('handshake');
|
||||
};
|
||||
|
||||
chromeMessageBus.onAny((topic, args) => {
|
||||
localMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
localMessageBus.onAny((topic, args) => {
|
||||
backendInitialized = true;
|
||||
chromeMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
if (!backendInitialized) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Attempting initialization', new Date());
|
||||
|
||||
const retry = () => {
|
||||
if (backendInitialized || backgroundDisconnected) {
|
||||
return;
|
||||
}
|
||||
handshakeWithBackend();
|
||||
setTimeout(retry, 500);
|
||||
};
|
||||
retry();
|
||||
}
|
||||
const handleDisconnect = (): void => {
|
||||
// console.log('Background disconnected', new Date());
|
||||
localMessageBus.emit('shutdown');
|
||||
localMessageBus.destroy();
|
||||
chromeMessageBus.destroy();
|
||||
backgroundDisconnected = true;
|
||||
};
|
||||
|
||||
// expose to use as callback for chrome.tabs.executeScript in background.ts
|
||||
(globalThis as any).main = main;
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
|
||||
const detectAngularMessageBus = new SamePageMessageBus(
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
`angular-devtools-detect-angular-${location.href}`,
|
||||
);
|
||||
|
||||
detectAngularMessageBus.on('detectAngular', (detectionResult) => {
|
||||
// only install backend once
|
||||
if (backendInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isAngularDevTools !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isAngular !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isDebugMode !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isSupportedAngularVersion !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defensive check against non html page. Realistically this should never happen.
|
||||
if (document.contentType !== 'text/html') {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = chrome.runtime.getURL('app/backend_bundle.js');
|
||||
document.documentElement.appendChild(script);
|
||||
document.documentElement.removeChild(script);
|
||||
backendInstalled = true;
|
||||
});
|
||||
|
||||
const localMessageBus = new SamePageMessageBus(
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
`angular-devtools-backend-${location.href}`,
|
||||
);
|
||||
const chromeMessageBus = new ChromeMessageBus(port);
|
||||
|
||||
const handshakeWithBackend = (): void => {
|
||||
localMessageBus.emit('handshake');
|
||||
};
|
||||
|
||||
chromeMessageBus.onAny((topic, args) => {
|
||||
localMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
localMessageBus.onAny((topic, args) => {
|
||||
backendInitialized = true;
|
||||
chromeMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
if (!backendInitialized) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Attempting initialization', new Date());
|
||||
|
||||
const retry = () => {
|
||||
if (backendInitialized || backgroundDisconnected) {
|
||||
return;
|
||||
}
|
||||
handshakeWithBackend();
|
||||
setTimeout(retry, 500);
|
||||
};
|
||||
retry();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AngularDetection} from 'protocol';
|
||||
import {
|
||||
appIsAngular,
|
||||
appIsAngularInDevMode,
|
||||
@@ -13,18 +14,12 @@ import {
|
||||
appIsSupportedAngularVersion,
|
||||
} from 'shared-utils';
|
||||
|
||||
export interface AngularDetection {
|
||||
// This is necessary because the runtime
|
||||
// message listener handles messages globally
|
||||
// including from other extensions. We don't
|
||||
// want to set icon and/or popup based on
|
||||
// a message coming from an unrelated extension.
|
||||
isAngularDevTools: true;
|
||||
isIvy: boolean;
|
||||
isAngular: boolean;
|
||||
isDebugMode: boolean;
|
||||
isSupportedAngularVersion: boolean;
|
||||
}
|
||||
import {SamePageMessageBus} from './same-page-message-bus';
|
||||
|
||||
const detectAngularMessageBus = new SamePageMessageBus(
|
||||
`angular-devtools-detect-angular-${location.href}`,
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
);
|
||||
|
||||
function detectAngular(win: Window): void {
|
||||
const isAngular = appIsAngular();
|
||||
@@ -32,20 +27,29 @@ function detectAngular(win: Window): void {
|
||||
const isDebugMode = appIsAngularInDevMode();
|
||||
const isIvy = appIsAngularIvy();
|
||||
|
||||
win.postMessage(
|
||||
const detection: AngularDetection = {
|
||||
isIvy,
|
||||
isAngular,
|
||||
isDebugMode,
|
||||
isSupportedAngularVersion,
|
||||
isAngularDevTools: true,
|
||||
};
|
||||
|
||||
// For the background script to toggle the icon.
|
||||
win.postMessage(detection, '*');
|
||||
|
||||
// For the content script to inject the backend.
|
||||
detectAngularMessageBus.emit('detectAngular', [
|
||||
{
|
||||
isIvy,
|
||||
isAngular,
|
||||
isDebugMode,
|
||||
isSupportedAngularVersion,
|
||||
isAngularDevTools: true,
|
||||
} as AngularDetection,
|
||||
'*',
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
||||
if (!isAngular) {
|
||||
setTimeout(() => detectAngular(win), 1000);
|
||||
}
|
||||
setTimeout(() => detectAngular(win), 1000);
|
||||
}
|
||||
|
||||
detectAngular(window);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const loadScripts = (urls: string[]) => {
|
||||
return urls
|
||||
.map((url, idx) => {
|
||||
return `
|
||||
const script${idx} = document.constructor.prototype.createElement.call(document, 'script');
|
||||
script${idx}.src = getScriptName("${url}");
|
||||
document.documentElement.appendChild(script${idx});
|
||||
script${idx}.parentNode.removeChild(script${idx});
|
||||
`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
let loaded = false;
|
||||
export const injectScripts = (urls: string[], cb?: () => void) => {
|
||||
if (loaded) {
|
||||
// Not throwing a hard error here, because we don't want to stop the
|
||||
// execution when folks are not using Trusted Types or when they have
|
||||
// allowed redeclaration of a security policy.
|
||||
console.error('Trying to reinject scripts');
|
||||
}
|
||||
loaded = true;
|
||||
urls = urls.map((s) => chrome.runtime.getURL(s));
|
||||
const script = `
|
||||
(function () {
|
||||
let policy = null;
|
||||
if (window.trustedTypes && window.trustedTypes.createPolicy) {
|
||||
policy = window.trustedTypes.createPolicy('angular#devtools', {
|
||||
createScriptURL: url => ${JSON.stringify(urls)}.indexOf(url) >= 0 ? url : null
|
||||
});
|
||||
}
|
||||
const getScriptName = name => policy ? policy.createScriptURL(name) : name;
|
||||
${loadScripts(urls)}
|
||||
})();
|
||||
`;
|
||||
chrome.devtools.inspectedWindow.eval(script, (_, err) => {
|
||||
if (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(err);
|
||||
}
|
||||
cb?.();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/// <reference types="chrome"/>
|
||||
|
||||
import {Events, Topic} from 'protocol';
|
||||
|
||||
export interface ContentScriptConnection {
|
||||
port: chrome.runtime.Port | null;
|
||||
enabled: boolean;
|
||||
frameId: 'devtools' | number;
|
||||
backendReady?: boolean;
|
||||
}
|
||||
|
||||
export interface DevToolsConnection {
|
||||
devtools: chrome.runtime.Port | null;
|
||||
contentScripts: {[name: string]: ContentScriptConnection};
|
||||
}
|
||||
|
||||
function isNumeric(str: string): boolean {
|
||||
return +str + '' === str;
|
||||
}
|
||||
|
||||
export interface Tabs {
|
||||
[tabId: string]: DevToolsConnection | undefined;
|
||||
}
|
||||
|
||||
export class TabManager {
|
||||
constructor(
|
||||
private tabs: Tabs,
|
||||
private runtime: typeof chrome.runtime,
|
||||
) {}
|
||||
|
||||
static initialize(tabs: Tabs, runtime: typeof chrome.runtime = chrome.runtime): TabManager {
|
||||
const manager = new TabManager(tabs, runtime);
|
||||
manager.initialize();
|
||||
return manager;
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.runtime.onConnect.addListener((port) => {
|
||||
if (isNumeric(port.name)) {
|
||||
this.registerDevToolsForTab(port);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!port.sender ||
|
||||
!port.sender.tab ||
|
||||
port.sender.tab.id === undefined ||
|
||||
port.sender.frameId === undefined
|
||||
) {
|
||||
console.warn('Received a connection from an unknown sender', port);
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerContentScriptForTab(port);
|
||||
});
|
||||
}
|
||||
|
||||
private ensureTabExists(tabId: number): void {
|
||||
this.tabs[tabId] ??= {
|
||||
devtools: null,
|
||||
contentScripts: {},
|
||||
};
|
||||
}
|
||||
|
||||
private registerDevToolsForTab(port: chrome.runtime.Port): void {
|
||||
// For the devtools page, our port name is the tab id.
|
||||
const tabId = parseInt(port.name, 10);
|
||||
|
||||
this.ensureTabExists(tabId);
|
||||
const tab = this.tabs[tabId]!;
|
||||
|
||||
tab.devtools = port;
|
||||
tab.devtools.onDisconnect.addListener(() => {
|
||||
tab.devtools = null;
|
||||
|
||||
for (const connection of Object.values(tab.contentScripts)) {
|
||||
connection.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// DevTools may register after the content script has already registered. If that's the case,
|
||||
// we need to set up the double pipe between the devtools and each content script, and send
|
||||
// the contentScriptConnected message to the devtools page to inform it of all frames on the page.
|
||||
for (const [frameId, connection] of Object.entries(tab.contentScripts)) {
|
||||
if (connection.port === null || connection.backendReady !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tab.devtools!.postMessage({
|
||||
topic: 'contentScriptConnected',
|
||||
args: [parseInt(frameId, 10), connection.port.name, connection.port.sender!.url],
|
||||
});
|
||||
this.doublePipe(tab.devtools, connection);
|
||||
}
|
||||
}
|
||||
|
||||
private registerContentScriptForTab(port: chrome.runtime.Port): void {
|
||||
// A content script connection will have a sender and a tab id.
|
||||
const sender = port.sender!;
|
||||
const frameId = sender.frameId!;
|
||||
const tabId = sender.tab!.id!;
|
||||
|
||||
this.ensureTabExists(tabId);
|
||||
const tab = this.tabs[tabId]!;
|
||||
|
||||
if (tab.contentScripts[frameId] === undefined) {
|
||||
tab.contentScripts[frameId] = {
|
||||
port: null,
|
||||
enabled: false,
|
||||
frameId: -1,
|
||||
};
|
||||
}
|
||||
|
||||
const contentScript = tab.contentScripts[frameId]!;
|
||||
contentScript.port = port;
|
||||
contentScript.frameId = frameId;
|
||||
contentScript.enabled = contentScript.enabled ?? false;
|
||||
|
||||
// When the content script disconnects, clean up the connection state we're storing in the
|
||||
// background page.
|
||||
contentScript.port.onDisconnect.addListener(() => {
|
||||
delete tab.contentScripts[frameId];
|
||||
|
||||
if (Object.keys(tab.contentScripts).length === 0) {
|
||||
delete this.tabs[tabId];
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for the backendReady message from the content script. This message is sent when the
|
||||
// content script has loaded the backend bundle and is ready to receive messages from the
|
||||
// backend.
|
||||
contentScript.port!.onMessage.addListener((message) => {
|
||||
if (message.topic === 'backendReady') {
|
||||
contentScript.backendReady = true;
|
||||
}
|
||||
});
|
||||
|
||||
// If the devtools connection is already established, set up the double pipe between the
|
||||
// devtools and the content script.
|
||||
if (tab.devtools) {
|
||||
this.doublePipe(tab.devtools, tab.contentScripts[frameId]);
|
||||
}
|
||||
}
|
||||
|
||||
private doublePipe(
|
||||
devtoolsPort: chrome.runtime.Port | null,
|
||||
contentScriptConnection: ContentScriptConnection,
|
||||
): void {
|
||||
if (devtoolsPort === null) {
|
||||
throw new Error('DevTools port is equal to null');
|
||||
}
|
||||
|
||||
const contentScriptPort = contentScriptConnection.port;
|
||||
|
||||
if (contentScriptPort === null) {
|
||||
throw new Error('Content script port is equal to null');
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Creating two-way communication channel', Date.now(), this.tabs);
|
||||
|
||||
const onDevToolsMessage = (message: {topic: Topic; args: Parameters<Events[Topic]>}) => {
|
||||
if (message.topic === 'enableFrameConnection') {
|
||||
if (message.args.length !== 2) {
|
||||
throw new Error('Expected two arguments for enableFrameConnection');
|
||||
}
|
||||
|
||||
const [frameId, tabId] = message.args as [frameId: number, tabId: number];
|
||||
|
||||
if (frameId === contentScriptConnection.frameId) {
|
||||
const tab = this.tabs[tabId];
|
||||
|
||||
if (tab === undefined) {
|
||||
throw new Error(`Expected tab to be registered with tabId ${tabId}`);
|
||||
}
|
||||
|
||||
for (const frameId of Object.keys(tab.contentScripts)) {
|
||||
tab.contentScripts[frameId].enabled = false;
|
||||
}
|
||||
|
||||
contentScriptConnection.enabled = true;
|
||||
devtoolsPort.postMessage({
|
||||
topic: 'frameConnected',
|
||||
args: [contentScriptConnection.frameId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Do not allow any message to be sent if a content script is not enabled. This is the
|
||||
// mechanism that lets us select which content script connection Angular Devtools is connected
|
||||
// to.
|
||||
if (!contentScriptConnection.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
contentScriptPort.postMessage(message);
|
||||
};
|
||||
devtoolsPort.onMessage.addListener(onDevToolsMessage);
|
||||
|
||||
const onContentScriptMessage = (message: {topic: Topic; args: Parameters<Events[Topic]>}) => {
|
||||
if (message.topic === 'backendReady') {
|
||||
devtoolsPort.postMessage({
|
||||
topic: 'contentScriptConnected',
|
||||
args: [
|
||||
contentScriptConnection.frameId,
|
||||
contentScriptConnection.port!.name,
|
||||
contentScriptConnection.port!.sender!.url,
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not allow any message to be sent if a content script is not enabled. This is the
|
||||
// mechanism that lets us select which content script connection Angular Devtools is connected
|
||||
// to.
|
||||
if (!contentScriptConnection.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
devtoolsPort.postMessage(message);
|
||||
};
|
||||
contentScriptPort.onMessage.addListener(onContentScriptMessage);
|
||||
|
||||
const shutdownContentScript = () => {
|
||||
devtoolsPort.onMessage.removeListener(onDevToolsMessage);
|
||||
devtoolsPort.postMessage({
|
||||
topic: 'contentScriptDisconnected',
|
||||
args: [contentScriptConnection.frameId, contentScriptConnection.port!.name],
|
||||
});
|
||||
|
||||
contentScriptPort.onMessage.removeListener(onContentScriptMessage);
|
||||
contentScriptPort.disconnect();
|
||||
};
|
||||
|
||||
contentScriptPort.onDisconnect.addListener(() => shutdownContentScript());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DevToolsConnection, TabManager, Tabs} from './tab_manager';
|
||||
|
||||
interface MockSender {
|
||||
url: string;
|
||||
tab: {
|
||||
id: number;
|
||||
};
|
||||
frameId: number;
|
||||
}
|
||||
|
||||
const TEST_MESSAGE_ONE = {topic: 'test', args: ['test1']};
|
||||
const TEST_MESSAGE_TWO = {topic: 'test', args: ['test2']};
|
||||
|
||||
class MockPort {
|
||||
onMessageListeners: Function[] = [];
|
||||
onDisconnectListeners: Function[] = [];
|
||||
messagesPosted: any[] = [];
|
||||
name: string;
|
||||
sender?: MockSender;
|
||||
|
||||
constructor(
|
||||
public properties: {
|
||||
name: string;
|
||||
sender?: MockSender;
|
||||
},
|
||||
) {
|
||||
this.name = properties.name;
|
||||
this.sender = properties.sender;
|
||||
}
|
||||
|
||||
postMessage(message: any): void {
|
||||
this.messagesPosted.push(message);
|
||||
}
|
||||
|
||||
onMessage = {
|
||||
addListener: (listener: Function): void => {
|
||||
this.onMessageListeners.push(listener);
|
||||
},
|
||||
};
|
||||
|
||||
onDisconnect = {
|
||||
addListener: (listener: Function): void => {
|
||||
this.onDisconnectListeners.push(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function assertArrayHasObj<T>(array: T[], obj: T) {
|
||||
expect(array).toContain(jasmine.objectContaining(obj as object));
|
||||
}
|
||||
|
||||
function assertArrayDoesNotHaveObj<T extends object>(array: T[], obj: T) {
|
||||
expect(array).not.toContain(jasmine.objectContaining(obj));
|
||||
}
|
||||
|
||||
function mockSpyFunction(obj: any, property: string, returnValue: any) {
|
||||
(obj[property] as any).and.returnValue(() => returnValue);
|
||||
}
|
||||
|
||||
function mockSpyProperty(obj: any, property: string, value: any) {
|
||||
(Object.getOwnPropertyDescriptor(obj, property)!.get as any).and.returnValue(value);
|
||||
}
|
||||
|
||||
describe('Tab Manager - ', () => {
|
||||
let tabs: Tabs;
|
||||
const tabId = 12345;
|
||||
let chromeRuntime: jasmine.SpyObj<typeof chrome.runtime>;
|
||||
let tabManager: TabManager;
|
||||
let tab: DevToolsConnection;
|
||||
let chromeRuntimeOnConnectListeners: ((port: MockPort) => void)[] = [];
|
||||
|
||||
function connectToChromeRuntime(port: MockPort): void {
|
||||
for (const listener of chromeRuntimeOnConnectListeners) {
|
||||
listener(port);
|
||||
}
|
||||
}
|
||||
|
||||
function emitMessageToPort(port: MockPort, message: any): void {
|
||||
for (const listener of port.onMessageListeners) {
|
||||
listener(message);
|
||||
}
|
||||
}
|
||||
|
||||
function emitDisconnectToPort(port: MockPort) {
|
||||
for (const listener of port.onDisconnectListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function createDevToolsPort() {
|
||||
const port = new MockPort({
|
||||
name: tabId.toString(),
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
chromeRuntimeOnConnectListeners = [];
|
||||
chromeRuntime = jasmine.createSpyObj(
|
||||
'chrome.runtime',
|
||||
['getManifest', 'getURL'],
|
||||
['onConnect', 'onDisconnect'],
|
||||
);
|
||||
mockSpyFunction(chromeRuntime, 'getManifest', {manifest_version: 3});
|
||||
mockSpyFunction(chromeRuntime, 'getURL', (path: string) => path);
|
||||
mockSpyProperty(chromeRuntime, 'onConnect', {
|
||||
addListener: (listener: (port: MockPort) => void) => {
|
||||
chromeRuntimeOnConnectListeners.push(listener);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single Frame', () => {
|
||||
const testURL = 'http://example.com';
|
||||
const contentScriptFrameId = 0;
|
||||
// let contentPort: MockPort;
|
||||
|
||||
function createContentScriptPort() {
|
||||
const port = new MockPort({
|
||||
name: 'Content Script',
|
||||
sender: {
|
||||
url: testURL,
|
||||
tab: {
|
||||
id: tabId,
|
||||
},
|
||||
frameId: contentScriptFrameId,
|
||||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
emitMessageToPort(port, {topic: 'backendReady'});
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tabs = {};
|
||||
tabManager = TabManager.initialize(tabs, chromeRuntime);
|
||||
});
|
||||
|
||||
it('should setup tab object in the tab manager', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab).toBeDefined();
|
||||
expect(tab!.devtools).toBe(devtoolsPort as unknown as chrome.runtime.Port);
|
||||
expect(tab!.contentScripts[contentScriptFrameId].port).toBe(
|
||||
contentScriptPort as unknown as chrome.runtime.Port,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set frame connection as enabled when an enableFrameConnection message is recieved', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
// Test backendReady and contentScriptConnected messages.
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(true);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'frameConnected',
|
||||
args: [contentScriptFrameId],
|
||||
});
|
||||
});
|
||||
|
||||
it('should pipe messages from the content script and devtools script to each other when the content script frame is enabled', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
|
||||
// Verify that the double pipe is set up between the content script and the devtools page.
|
||||
emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
assertArrayDoesNotHaveObj(contentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
assertArrayHasObj(contentScriptPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
});
|
||||
|
||||
it('should not pipe messages from the content script and devtools script to each other when the content script frame is disabled', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
|
||||
emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE);
|
||||
assertArrayDoesNotHaveObj(contentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
});
|
||||
|
||||
it('should set backendReady when the contentPort recieves the backendReady message', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.backendReady).toBe(true);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'contentScriptConnected',
|
||||
args: [contentScriptFrameId, contentScriptPort.name, contentScriptPort.sender!.url],
|
||||
});
|
||||
});
|
||||
|
||||
it('should set tab.devtools to null when the devtoolsPort disconnects', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(true);
|
||||
|
||||
emitDisconnectToPort(devtoolsPort);
|
||||
expect(tab.devtools).toBeNull();
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Frames', () => {
|
||||
const topLevelFrameId = 0;
|
||||
const childFrameId = 1;
|
||||
|
||||
function createTopLevelContentScriptPort() {
|
||||
const port = new MockPort({
|
||||
name: 'Top level content script',
|
||||
sender: {
|
||||
url: 'TEST_URL',
|
||||
tab: {
|
||||
id: tabId,
|
||||
},
|
||||
frameId: topLevelFrameId,
|
||||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
function createChildContentScriptPort() {
|
||||
const port = new MockPort({
|
||||
name: 'Child content script',
|
||||
sender: {
|
||||
url: 'TEST_URL_2',
|
||||
tab: {
|
||||
id: tabId,
|
||||
},
|
||||
frameId: childFrameId,
|
||||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tabs = {};
|
||||
tabManager = TabManager.initialize(tabs, chromeRuntime);
|
||||
});
|
||||
|
||||
it('should setup tab object in the tab manager', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
const topLevelContentScriptPort = createTopLevelContentScriptPort();
|
||||
const childContentScriptPort = createChildContentScriptPort();
|
||||
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab).toBeDefined();
|
||||
expect(tab!.devtools).toBe(devtoolsPort as unknown as chrome.runtime.Port);
|
||||
expect(tab!.contentScripts[topLevelFrameId].port).toBe(
|
||||
topLevelContentScriptPort as unknown as chrome.runtime.Port,
|
||||
);
|
||||
expect(tab!.contentScripts[childFrameId].port).toBe(
|
||||
childContentScriptPort as unknown as chrome.runtime.Port,
|
||||
);
|
||||
});
|
||||
|
||||
it('should setup message and disconnect listeners on devtools and content script ports', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
const topLevelContentScriptPort = createTopLevelContentScriptPort();
|
||||
const childContentScriptPort = createChildContentScriptPort();
|
||||
|
||||
// 1 listener to clean up tab object if this was the last content script connection.
|
||||
// 1 listener to cleanup the douple pipe between the content script and the devtools page.
|
||||
expect(topLevelContentScriptPort.onDisconnectListeners.length).toBe(2);
|
||||
expect(childContentScriptPort.onDisconnectListeners.length).toBe(2);
|
||||
|
||||
// 1 listener to clean up devtools connection
|
||||
expect(devtoolsPort.onDisconnectListeners.length).toBe(1);
|
||||
|
||||
// 1 listener set when the content script is registered to check for backendReady.
|
||||
// 1 listener set when the double pipe is set up between the content script and the devtools page.
|
||||
expect(topLevelContentScriptPort.onMessageListeners.length).toBe(2);
|
||||
expect(childContentScriptPort.onMessageListeners.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should set the correct frame connection as enabled when an enableFrameConnection message is recieved', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
createTopLevelContentScriptPort();
|
||||
createChildContentScriptPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab?.contentScripts[topLevelFrameId]?.enabled).toBe(false);
|
||||
expect(tab?.contentScripts[childFrameId]?.enabled).toBe(false);
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [topLevelFrameId, tabId],
|
||||
});
|
||||
expect(tab?.contentScripts[topLevelFrameId]?.enabled).toBe(true);
|
||||
expect(tab?.contentScripts[childFrameId]?.enabled).toBe(false);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'frameConnected',
|
||||
args: [topLevelFrameId],
|
||||
});
|
||||
assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'frameConnected',
|
||||
args: [childFrameId],
|
||||
});
|
||||
});
|
||||
|
||||
it('should pipe messages from the correct content script and devtools script when that content script frame is enabled', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
const topLevelContentScriptPort = createTopLevelContentScriptPort();
|
||||
const childContentScriptPort = createChildContentScriptPort();
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [topLevelFrameId, tabId],
|
||||
});
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_ONE);
|
||||
assertArrayHasObj(topLevelContentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
assertArrayDoesNotHaveObj(childContentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [childFrameId, tabId],
|
||||
});
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
assertArrayHasObj(childContentScriptPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
assertArrayDoesNotHaveObj(topLevelContentScriptPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@
|
||||
"short_name": "Angular DevTools",
|
||||
"name": "Angular DevTools",
|
||||
"description": "Angular DevTools extends Chrome DevTools adding Angular specific debugging and profiling capabilities.",
|
||||
"version": "1.0.9",
|
||||
"version_name": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"version_name": "1.0.10",
|
||||
"minimum_chrome_version": "102",
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||
@@ -27,8 +27,7 @@
|
||||
{
|
||||
"resources": [
|
||||
"app/backend_bundle.js",
|
||||
"app/detect_angular_for_extension_icon_bundle.js",
|
||||
"devtools.html"
|
||||
"app/detect_angular_for_extension_icon_bundle.js"
|
||||
],
|
||||
"matches": ["<all_urls>"],
|
||||
"extension_ids": []
|
||||
@@ -52,7 +51,18 @@
|
||||
"js": [
|
||||
"app/ng_validate_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"app/content_script_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "Angular DevTools",
|
||||
"name": "Angular DevTools",
|
||||
"description": "Angular DevTools extends Firefox DevTools adding Angular specific debugging and profiling capabilities.",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"icons": {
|
||||
"16": "assets/icon16.png",
|
||||
@@ -29,13 +29,26 @@
|
||||
"https://*/*",
|
||||
"file:///*"
|
||||
],
|
||||
"content_scripts": [{
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"app/ng_validate_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}]
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"app/content_script_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,6 +20,7 @@ ng_module(
|
||||
],
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/src:iframe_message_bus",
|
||||
"//packages/common",
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
|
||||
import {IFrameMessageBus} from '../../iframe-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
@@ -15,21 +14,6 @@ import {DevToolsComponent} from 'ng-devtools';
|
||||
@Component({
|
||||
templateUrl: './devtools-app.component.html',
|
||||
styleUrls: ['./devtools-app.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
return new PriorityAwareMessageBus(
|
||||
new IFrameMessageBus(
|
||||
'angular-devtools',
|
||||
'angular-devtools-backend',
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
imports: [DevToolsComponent],
|
||||
})
|
||||
|
||||
@@ -11,6 +11,9 @@ import {NgModule} from '@angular/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
|
||||
import {AppDevToolsComponent} from './devtools-app.component';
|
||||
import {FrameManager} from '../../../projects/ng-devtools/src/lib/frame_manager';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
import {IFrameMessageBus} from '../../../src/iframe-message-bus';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -24,5 +27,21 @@ import {AppDevToolsComponent} from './devtools-app.component';
|
||||
]),
|
||||
AppDevToolsComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
return new PriorityAwareMessageBus(
|
||||
new IFrameMessageBus(
|
||||
'angular-devtools',
|
||||
'angular-devtools-backend',
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(null)},
|
||||
],
|
||||
})
|
||||
export class DevToolsModule {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {ApplicationEnvironment, Environment} from 'ng-devtools';
|
||||
import {environment} from './environments/environment';
|
||||
|
||||
export class DemoApplicationEnvironment extends ApplicationEnvironment {
|
||||
frameSelectorEnabled = false;
|
||||
override get environment(): Environment {
|
||||
return environment;
|
||||
}
|
||||
|
||||
@@ -1098,7 +1098,9 @@ export interface ModelSignal<T> extends WritableSignal<T> {
|
||||
// (undocumented)
|
||||
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: T;
|
||||
// (undocumented)
|
||||
[SIGNAL]: ModelSignalNode<T>;
|
||||
[SIGNAL]: InputSignalNode<T, T>;
|
||||
// @deprecated (undocumented)
|
||||
subscribe(callback: (value: T) => void): () => void;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "17.2.0-next.1",
|
||||
"version": "17.2.2",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@@ -43,18 +43,18 @@
|
||||
"devtools:build:chrome": "bazelisk build --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- devtools/projects/shell-browser/src:prodapp",
|
||||
"devtools:build:firefox": "bazelisk build --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=firefox -- devtools/projects/shell-browser/src:prodapp",
|
||||
"devtools:test": "bazelisk test --config snapshot-build --//devtools/projects/shell-browser/src:flag_browser=chrome -- //devtools/...",
|
||||
"docs": "yarn bazel run --config=aio_local_deps //adev:serve",
|
||||
"docs:build": "yarn bazel build --config=aio_local_deps //adev:build",
|
||||
"docs": "[[ -n $CI ]] && echo 'Cannot run this yarn script on CI' && exit 1 || yarn bazel run --config=aio_local_deps //adev:serve --fast_adev",
|
||||
"docs:build": "[[ -n $CI ]] && echo 'Cannot run this yarn script on CI' && exit 1 || yarn bazel build --config=aio_local_deps //adev:build --fast_adev",
|
||||
"benchmarks": "ts-node --esm scripts/benchmarks/index.mts"
|
||||
},
|
||||
"// 1": "dependencies are used locally and by bazel",
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "17.2.0-next.0",
|
||||
"@angular-devkit/core": "17.2.0-next.0",
|
||||
"@angular-devkit/schematics": "17.2.0-next.0",
|
||||
"@angular/cdk": "17.2.0-next.0",
|
||||
"@angular/cli": "17.2.0-next.0",
|
||||
"@angular/material": "17.2.0-next.0",
|
||||
"@angular-devkit/build-angular": "17.2.0-rc.0",
|
||||
"@angular-devkit/core": "17.2.0-rc.0",
|
||||
"@angular-devkit/schematics": "17.2.0-rc.0",
|
||||
"@angular/cdk": "17.2.0-rc.0",
|
||||
"@angular/cli": "17.2.0-rc.0",
|
||||
"@angular/material": "17.2.0-rc.0",
|
||||
"@babel/cli": "7.23.9",
|
||||
"@babel/core": "7.23.9",
|
||||
"@babel/generator": "7.23.6",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@rollup/plugin-babel": "^6.0.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||
"@schematics/angular": "17.2.0-next.0",
|
||||
"@schematics/angular": "17.2.0-rc.0",
|
||||
"@types/angular": "^1.6.47",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"@types/babel__generator": "7.6.8",
|
||||
@@ -152,9 +152,9 @@
|
||||
"@actions/core": "^1.10.0",
|
||||
"@angular-devkit/architect-cli": "^0.1702.0-next",
|
||||
"@angular/animations": "^17.2.0-next",
|
||||
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#07da06414a4ce04a8f9710f2bc9346c2c753a0d2",
|
||||
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c4cf003cb4ac849986beaa243d7e85a893612f2",
|
||||
"@angular/docs": "https://github.com/angular/dev-infra-private-docs-builds.git#87eb92c0cc022678fa3fedbd5ac25b84f285a7fa",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#eacabd9f66aad05822ccb5b0dc893ebe7eb62321",
|
||||
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#c21f93acb618bcaeda52a8065e7b6c9242def182",
|
||||
"@babel/helper-remap-async-to-generator": "^7.18.9",
|
||||
"@babel/plugin-proposal-async-generator-functions": "^7.20.7",
|
||||
"@bazel/bazelisk": "^1.7.5",
|
||||
@@ -222,7 +222,7 @@
|
||||
"// 3": "Ensure that transitive dependencies on `https-proxy-agent` are at minimum v5 as older versions patch NodeJS directly, breaking tools like webdriver which is used by the karma-sauce-launcher as an example.",
|
||||
"// 4": "Ensure that a single instance of the `saucelabs` package is used. Protractor and the Karma sauce launcher pull this package as dependency. A single instance allows for e.g. easier patching in the Karma config.",
|
||||
"resolutions": {
|
||||
"**/https-proxy-agent": "7.0.2",
|
||||
"**/https-proxy-agent": "7.0.3",
|
||||
"**/saucelabs": "7.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user