mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2e0f08d99 | |||
| e194fbfbaa | |||
| 8ad1ee764b | |||
| f222bee8fa | |||
| 9a9a00e0f9 | |||
| 5f9cac037b | |||
| 9778326f10 | |||
| fac5a7cf4e | |||
| ae145d19c6 | |||
| 30b6d34a49 | |||
| 3e0b7c1bdc | |||
| a58a35dfe4 | |||
| a7c9fbb087 | |||
| 20811aad27 | |||
| df453c3549 | |||
| 1f6c96a1dc | |||
| 656549d88a | |||
| c0f0baac58 | |||
| d38aa4f103 | |||
| 7397b6648a | |||
| 0fe9331958 | |||
| 7620f506c0 | |||
| 62ad2f0c71 | |||
| 424a26b3c7 | |||
| 18bd1a5ccb | |||
| 8922a11693 | |||
| 4e0341050b | |||
| 0616dc2839 | |||
| dc228c6fc1 | |||
| b1a7249c24 | |||
| 9ff4893bc0 | |||
| 025f7af694 | |||
| 1c4a99a6d1 | |||
| 51d53fc728 | |||
| be882c3efb | |||
| 18fa6d7556 | |||
| bb8a5a4c70 | |||
| 9c00506a26 | |||
| 79e8539ce2 | |||
| 6f79507ea7 | |||
| 6616019735 | |||
| 4d4858fa28 | |||
| 538fe47d9d | |||
| b07c549802 | |||
| 6f6ad0279b | |||
| d08f91f861 | |||
| 28aa7b3188 | |||
| 6ac0a68d73 | |||
| ea19bff302 | |||
| 1112e9196a | |||
| 0d8a5c4665 | |||
| 76ceebad04 | |||
| 99c2d76651 | |||
| 246cb9c381 | |||
| d7c75c6edb | |||
| 0daca457bb | |||
| 12f9ed01cc | |||
| 69713413f7 | |||
| 43d6816f9e | |||
| 4c4fea1304 | |||
| c6df89c827 | |||
| 9cae9b4a8c | |||
| 29b959ce31 | |||
| a24b24e8a4 | |||
| f51c008744 | |||
| 3dedd6a07a | |||
| 881376eb42 |
@@ -0,0 +1,38 @@
|
||||
# This workflow builds the previews for pull requests when a certain label is applied.
|
||||
# The actual deployment happens as part of a dedicated second workflow to avoid security
|
||||
# issues where the building would otherwise occur in an authorized context where secrets
|
||||
# could be leaked. More details can be found here:
|
||||
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Build adev for preview deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, labeled]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
adev-build:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'adev: preview') ||
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e52eb8237f2ed71195f87ce8046467a176568e58
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e52eb8237f2ed71195f87ce8046467a176568e58
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e52eb8237f2ed71195f87ce8046467a176568e58
|
||||
- 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@73777547f819464aa5f48071b966eed34fd2c402
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/build/browser'
|
||||
@@ -0,0 +1,50 @@
|
||||
# This workflow runs whenever the ADEV build workflow has completed. Deployment happens
|
||||
# as part of a dedicated second workflow to avoid security issues where the building would
|
||||
# otherwise occur in an authorized context where secrets could be leaked.
|
||||
#
|
||||
# More details can be found here:
|
||||
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
|
||||
|
||||
name: Deploying adev preview to Firebase
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build adev for preview deployment']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
# Needed in order to be able to comment on the pull request.
|
||||
pull-requests: write
|
||||
# Needed in order to checkout the repository
|
||||
contents: read
|
||||
# Needed in order to retrieve the artifacts from the previous job
|
||||
actions: read
|
||||
|
||||
env:
|
||||
PREVIEW_PROJECT: ng-dev-previews
|
||||
PREVIEW_SITE: ng-dev-previews-fw
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@73777547f819464aa5f48071b966eed34fd2c402
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
firebase-config-dir: './adev'
|
||||
firebase-public-dir: './adev/build/browser'
|
||||
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
|
||||
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build --config=aio_local_deps //adev:build
|
||||
run: yarn bazel test --config=aio_local_deps //adev:test
|
||||
|
||||
aio-local:
|
||||
runs-on:
|
||||
@@ -235,9 +235,11 @@ jobs:
|
||||
//packages/zone.js/test/closure:closure_js \
|
||||
|
||||
- run: |
|
||||
rm -Rf packages/zone.js/build
|
||||
rm -Rf packages/zone.js/test/extra/*.umd.js
|
||||
|
||||
mkdir -p packages/zone.js/build/
|
||||
mkdir -p packages/zone.js/build/test/
|
||||
mkdir -p packages/zone.js/test/
|
||||
|
||||
cp dist/bin/packages/zone.js/bundles/zone.umd.js packages/zone.js/build/zone.umd.js
|
||||
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/
|
||||
|
||||
+23
-1
@@ -5,7 +5,18 @@ import {FormatConfig} from '@angular/ng-dev';
|
||||
*/
|
||||
export const format: FormatConfig = {
|
||||
'prettier': {
|
||||
'matchers': ['**/*.{yaml,yml}'],
|
||||
'matchers': [
|
||||
'**/*.{yaml,yml}',
|
||||
'devtools/**/*.{js,ts}',
|
||||
'tools/**/*.{js,ts}',
|
||||
'modules/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}',
|
||||
'packages/animations/**/*.{js,ts}',
|
||||
'packages/bazel/**/*.{js,ts}',
|
||||
'packages/benchpress/**/*.{js,ts}',
|
||||
'packages/elements/**/*.{js,ts}',
|
||||
'packages/misc/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
'clang-format': {
|
||||
'matchers': [
|
||||
@@ -31,6 +42,17 @@ export const format: FormatConfig = {
|
||||
'!packages/common/src/i18n/currencies.ts',
|
||||
// Temporarily disable formatting for adev
|
||||
'!adev/**',
|
||||
|
||||
// Migrated to prettier
|
||||
'!devtools/**/*.{js,ts}',
|
||||
'!tools/**/*.{js,ts}',
|
||||
'!modules/**/*.{js,ts}',
|
||||
'!scripts/**/*.{js,ts}',
|
||||
'!packages/animations/**/*.{js,ts}',
|
||||
'!packages/bazel/**/*.{js,ts}',
|
||||
'!packages/benchpress/**/*.{js,ts}',
|
||||
'!packages/elements/**/*.{js,ts}',
|
||||
'!packages/misc/**/*.{js,ts}',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
"packages/http/**",
|
||||
"**/.gitignore",
|
||||
"**/.gitkeep",
|
||||
"**/yarn.lock",
|
||||
"**/package.json",
|
||||
"**/third_party/**",
|
||||
"**/tsconfig-build.json",
|
||||
"**/tsconfig-tsec.json",
|
||||
|
||||
@@ -1150,6 +1150,7 @@ groups:
|
||||
- josephperrott
|
||||
- mgechev
|
||||
- twerske
|
||||
- ~jeanmeche
|
||||
|
||||
# =========================================================
|
||||
# Dev-infra
|
||||
@@ -1394,6 +1395,7 @@ groups:
|
||||
- devversion # Paul Gschwendtner
|
||||
- dgp1130 # Doug Parker
|
||||
- dylhunn # Dylan Hunn
|
||||
- jeanmeche # Matthieu Riegler
|
||||
- jelbourn # Jeremy Elbourn
|
||||
- jessicajaniuk # Jessica Janiuk
|
||||
- JiaLiPassion # Jia Li
|
||||
|
||||
+47
-198
@@ -1,3 +1,50 @@
|
||||
<a name="17.1.1"></a>
|
||||
# 17.1.1 (2024-01-24)
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f222bee8fa](https://github.com/angular/angular/commit/f222bee8fa037f437761e5f7f127f22f280e9154) | fix | revert commit that replaced `last` helper with native `Array.at(-1)` ([#54021](https://github.com/angular/angular/pull/54021)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0"></a>
|
||||
# 17.1.0 (2024-01-17)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [79ff91a813](https://github.com/angular/angular/commit/79ff91a813e544929cb5eb5f9aab762a9f3d0435) | fix | allow TS jsDocParsingMode host option to be programmatically set ([#53126](https://github.com/angular/angular/pull/53126)) |
|
||||
| [5613051a8b](https://github.com/angular/angular/commit/5613051a8bd2626ae347292807b2bf21085c4c02) | fix | allow TS jsDocParsingMode host option to be programmatically set again ([#53292](https://github.com/angular/angular/pull/53292)) |
|
||||
| [df8a825910](https://github.com/angular/angular/commit/df8a825910951bebf34a4eede42f3ce5cd3e6fb7) | fix | project empty block root node ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
| [478d622265](https://github.com/angular/angular/commit/478d6222650884478314985e3d5132587c4f670c) | fix | project empty block root node in template pipeline ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [abdc7e4578](https://github.com/angular/angular/commit/abdc7e45786667e4283912024a641975f1917d97) | feat | support type-checking for generic signal inputs ([#53521](https://github.com/angular/angular/pull/53521)) |
|
||||
| [e620b3a724](https://github.com/angular/angular/commit/e620b3a724cb615af24b7779c0ab492d24efb8cc) | fix | add compiler option to disable control flow content projection diagnostic ([#53311](https://github.com/angular/angular/pull/53311)) |
|
||||
| [4c1d69e288](https://github.com/angular/angular/commit/4c1d69e2880f22745c820eee630d10071e4fa86b) | fix | add diagnostic for control flow that prevents content projection ([#53190](https://github.com/angular/angular/pull/53190)) |
|
||||
| [76ceebad04](https://github.com/angular/angular/commit/76ceebad047f62972654a8c934c77d8d02d9fa14) | fix | do not throw fatal error if extended type check fails ([#53896](https://github.com/angular/angular/pull/53896)) |
|
||||
| [1a6eaa0fea](https://github.com/angular/angular/commit/1a6eaa0fea1024b919e17ac9d2e8c07df7916de8) | fix | input transform in local compilation mode ([#53645](https://github.com/angular/angular/pull/53645)) |
|
||||
| [56a76d73e0](https://github.com/angular/angular/commit/56a76d73e037aeea1975808d5c51608fd23d4fa6) | fix | modify `getConstructorDependencies` helper to work with reflection host after the previous change ([#52215](https://github.com/angular/angular/pull/52215)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [863be4b698](https://github.com/angular/angular/commit/863be4b6981dc60ca0610b0e61d2ba1f5759e2a3) | feat | expose new `input` API for signal-based inputs ([#53872](https://github.com/angular/angular/pull/53872)) |
|
||||
| [94096c6ede](https://github.com/angular/angular/commit/94096c6ede67436a349ae07901f2bb418bf9f461) | feat | support TypeScript 5.3 ([#52572](https://github.com/angular/angular/pull/52572)) |
|
||||
| [69b384c0d1](https://github.com/angular/angular/commit/69b384c0d16f631741339d8757c32ef08260cfce) | fix | `SignalNode` reactive node incorrectly exposing unset field ([#53571](https://github.com/angular/angular/pull/53571)) |
|
||||
| [6f79507ea7](https://github.com/angular/angular/commit/6f79507ea7f272d8d09250e222ca831f407867d8) | fix | Change defer block fixture default behavior to playthrough ([#53956](https://github.com/angular/angular/pull/53956)) |
|
||||
| [32f908ab70](https://github.com/angular/angular/commit/32f908ab70f1b9ed3f92df1cae05ddde68932404) | fix | do not accidentally inherit input transforms when overridden ([#53571](https://github.com/angular/angular/pull/53571)) |
|
||||
| [bdd61c768a](https://github.com/angular/angular/commit/bdd61c768a28b56c68634b99c036986499829f45) | fix | replace assertion with more intentional error ([#52234](https://github.com/angular/angular/pull/52234)) |
|
||||
| [0daca457bb](https://github.com/angular/angular/commit/0daca457bb5bb6ffe14b7037264f8497eb5b3daf) | fix | TestBed should still use the microtask queue to schedule effects ([#53843](https://github.com/angular/angular/pull/53843)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5c1d441029](https://github.com/angular/angular/commit/5c1d4410298e20cb03d7a1ddf7931538b6a181b4) | feat | Add info property to `NavigationExtras` ([#53303](https://github.com/angular/angular/pull/53303)) |
|
||||
| [50d7916278](https://github.com/angular/angular/commit/50d79162785bb8d3e158a7a4a3733f4c75d3b127) | feat | Add router configuration to resolve navigation promise on error ([#48910](https://github.com/angular/angular/pull/48910)) |
|
||||
| [a5a9b408e2](https://github.com/angular/angular/commit/a5a9b408e2eb64dcf1d3ca16da4897649dd2fc34) | feat | Add transient info to RouterLink input ([#53784](https://github.com/angular/angular/pull/53784)) |
|
||||
| [726530a9af](https://github.com/angular/angular/commit/726530a9af9c8daf7295cc3548f24e70f380d70e) | feat | Allow `onSameUrlNavigation: 'ignore'` in `navigateByUrl` ([#52265](https://github.com/angular/angular/pull/52265)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.9"></a>
|
||||
# 17.0.9 (2024-01-10)
|
||||
### common
|
||||
@@ -30,36 +77,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0-next.5"></a>
|
||||
# 17.1.0-next.5 (2023-12-21)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3a689c2050](https://github.com/angular/angular/commit/3a689c20509ed5972b3831988fc0014d293d0cc3) | fix | correctly intercept index in loop tracking function ([#53604](https://github.com/angular/angular/pull/53604)) |
|
||||
| [df8a825910](https://github.com/angular/angular/commit/df8a825910951bebf34a4eede42f3ce5cd3e6fb7) | fix | project empty block root node ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
| [478d622265](https://github.com/angular/angular/commit/478d6222650884478314985e3d5132587c4f670c) | fix | project empty block root node in template pipeline ([#53620](https://github.com/angular/angular/pull/53620)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [aecb675fa5](https://github.com/angular/angular/commit/aecb675fa5059e510e0223e00fa114d92e799b04) | fix | avoid repeated work when parsing version ([#53598](https://github.com/angular/angular/pull/53598)) |
|
||||
| [872e7f25fe](https://github.com/angular/angular/commit/872e7f25fea8a01f980e4e406ad382a9e187ce5c) | fix | tree shake version class ([#53598](https://github.com/angular/angular/pull/53598)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d49333edc3](https://github.com/angular/angular/commit/d49333edc3fe170c57d953aa2d8df58d0db379e0) | fix | cf migration - detect and error when result is invalid i18n nesting ([#53638](https://github.com/angular/angular/pull/53638)) ([#53639](https://github.com/angular/angular/pull/53639)) |
|
||||
| [8e2178792d](https://github.com/angular/angular/commit/8e2178792d474e84817c27851cd6b9600deb15b5) | fix | cf migration - ensure full check runs for all imports ([#53637](https://github.com/angular/angular/pull/53637)) |
|
||||
| [22b95de9bc](https://github.com/angular/angular/commit/22b95de9bcb92267b8f848a1aafa71af70d5577b) | fix | cf migration - fix bug in attribute formatting ([#53636](https://github.com/angular/angular/pull/53636)) |
|
||||
| [b40bb22a66](https://github.com/angular/angular/commit/b40bb22a66a3c2ae478778f3ef0be0ce971a1b2c) | fix | cf migration - improve import declaration handling ([#53622](https://github.com/angular/angular/pull/53622)) |
|
||||
| [8bf752539f](https://github.com/angular/angular/commit/8bf752539f6b4b5955e3d61a76423910af55181b) | fix | cf migration - preserve indentation on attribute strings ([#53625](https://github.com/angular/angular/pull/53625)) |
|
||||
| [7bb312fcf6](https://github.com/angular/angular/commit/7bb312fcf612c57d6606ae14f21707f10d8ff9ce) | fix | cf migration - stop removing empty newlines from i18n blocks ([#53578](https://github.com/angular/angular/pull/53578)) |
|
||||
| [db6b4a6bc4](https://github.com/angular/angular/commit/db6b4a6bc479b0fac0a4a99e1a202c38d1ea8944) | fix | Fix cf migration bug with parsing for loop conditions properly ([#53558](https://github.com/angular/angular/pull/53558)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [502f300757](https://github.com/angular/angular/commit/502f3007575a5445c2876c92548415cb003582b1) | fix | Should not freeze original object used for route data ([#53635](https://github.com/angular/angular/pull/53635)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.8"></a>
|
||||
# 17.0.8 (2023-12-21)
|
||||
### compiler
|
||||
@@ -89,45 +106,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0-next.4"></a>
|
||||
# 17.1.0-next.4 (2023-12-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b98d8f79ed](https://github.com/angular/angular/commit/b98d8f79ed4979fb91661dbdfb6266a917b41474) | fix | handle ambient types in input transform function ([#51474](https://github.com/angular/angular/pull/51474)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9e5456912a](https://github.com/angular/angular/commit/9e5456912a699315f1f5e8458d35df946c01dc12) | fix | generate less type checking code in for loops ([#53515](https://github.com/angular/angular/pull/53515)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2565121851](https://github.com/angular/angular/commit/25651218512becb3a491b1c94d643a5066a6d3cb) | fix | Avoid refreshing a host view twice when using transplanted views ([#53021](https://github.com/angular/angular/pull/53021)) |
|
||||
| [629343f247](https://github.com/angular/angular/commit/629343f24741760e76d98ea1005debaf4775bfc7) | fix | Multiple subscribers to ApplicationRef.isStable should all see values ([#53541](https://github.com/angular/angular/pull/53541)) |
|
||||
| [42f4f70e97](https://github.com/angular/angular/commit/42f4f70e97b47f7ffcf15e8af2c1a3fa3f393903) | fix | remove signal equality check short-circuit ([#53446](https://github.com/angular/angular/pull/53446)) |
|
||||
| [f35adcb9b2](https://github.com/angular/angular/commit/f35adcb9b255741515d54bbce694f4cd34aac249) | fix | update feature usage marker ([#53542](https://github.com/angular/angular/pull/53542)) |
|
||||
| [1fc5442947](https://github.com/angular/angular/commit/1fc5442947b07be7c11fe7c79b9e69f9458c8108) | perf | avoid changes Observable creation on QueryList ([#53498](https://github.com/angular/angular/pull/53498)) |
|
||||
| [e3a6bf9b6c](https://github.com/angular/angular/commit/e3a6bf9b6c3bef03df9bfc8f05b817bc875cbad6) | perf | optimize memory allocation when reconcilling lists ([#52245](https://github.com/angular/angular/pull/52245)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a02767956a](https://github.com/angular/angular/commit/a02767956a00e0b44b690411f01584f6660c83ea) | fix | CF Migration - ensure bound ngIfElse cases ignore line breaks ([#53435](https://github.com/angular/angular/pull/53435)) |
|
||||
| [6aa1bb78e8](https://github.com/angular/angular/commit/6aa1bb78e8b9ca866b3c2d84f4005a0e67f2b6bc) | fix | cf migration - undo changes when html fails to parse post migration ([#53530](https://github.com/angular/angular/pull/53530)) |
|
||||
| [cc02852ac4](https://github.com/angular/angular/commit/cc02852ac44bfea13e8b9d5768694a094fc2c4b5) | fix | CF migration only remove newlines of changed template content ([#53508](https://github.com/angular/angular/pull/53508)) |
|
||||
| [ce1076785c](https://github.com/angular/angular/commit/ce1076785c98ecc9660637cafa4a62a63be8d315) | fix | cf migration validate structure of ngswitch before migrating ([#53530](https://github.com/angular/angular/pull/53530)) |
|
||||
| [1f5c8bf116](https://github.com/angular/angular/commit/1f5c8bf116783c25234ab7fa301a0be842e58007) | fix | ensure we do not overwrite prior template replacements in migration ([#53393](https://github.com/angular/angular/pull/53393)) |
|
||||
| [79f791543b](https://github.com/angular/angular/commit/79f791543b607eea1bb6a28b9c744760ba4fd028) | fix | fix cf migration import removal when errors occur ([#53502](https://github.com/angular/angular/pull/53502)) |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fdb9cb7a5b](https://github.com/angular/angular/commit/fdb9cb7a5be4695abaa60efc89ecd50ddb9e9e6e) | fix | Get correct base path when using "." as base href when serving from the file:// protocol. ([#53547](https://github.com/angular/angular/pull/53547)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [48c5041687](https://github.com/angular/angular/commit/48c50416872313f23d2759232c990cf8d2de6996) | fix | provide more actionable error message when route is not matched in production mode ([#53523](https://github.com/angular/angular/pull/53523)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.7"></a>
|
||||
# 17.0.7 (2023-12-13)
|
||||
### compiler
|
||||
@@ -167,47 +145,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0-next.3"></a>
|
||||
# 17.1.0-next.3 (2023-12-06)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5613051a8b](https://github.com/angular/angular/commit/5613051a8bd2626ae347292807b2bf21085c4c02) | fix | allow TS jsDocParsingMode host option to be programmatically set again ([#53292](https://github.com/angular/angular/pull/53292)) |
|
||||
| [77ac4cd324](https://github.com/angular/angular/commit/77ac4cd32491d0c994cb4ea50372601c955cec3d) | fix | generate proper code for nullish coalescing in styling host bindings ([#53305](https://github.com/angular/angular/pull/53305)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e620b3a724](https://github.com/angular/angular/commit/e620b3a724cb615af24b7779c0ab492d24efb8cc) | fix | add compiler option to disable control flow content projection diagnostic ([#53311](https://github.com/angular/angular/pull/53311)) |
|
||||
| [d7a83f9521](https://github.com/angular/angular/commit/d7a83f95213cdd5d3ceefbc95fa1190856b0198c) | fix | avoid conflicts with built-in global variables in for loop blocks ([#53319](https://github.com/angular/angular/pull/53319)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [77939a3bd3](https://github.com/angular/angular/commit/77939a3bd39073d07e24797a4632ec2d2a6b92e0) | fix | cleanup signal consumers for all views ([#53351](https://github.com/angular/angular/pull/53351)) |
|
||||
| [899f6c4a12](https://github.com/angular/angular/commit/899f6c4a12127f87aeedf47ee128ce949ebe717c) | fix | handle hydration of multiple nodes projected in a single slot ([#53270](https://github.com/angular/angular/pull/53270)) |
|
||||
| [4b23221b4e](https://github.com/angular/angular/commit/4b23221b4e5f8be7bcffc8ace255143653550d3d) | fix | support hydration for cases when content is re-projected using ng-template ([#53304](https://github.com/angular/angular/pull/53304)) |
|
||||
| [82609d471c](https://github.com/angular/angular/commit/82609d471c9802b847a5654918eca1ba3ebb29b3) | fix | support swapping hydrated views in `@for` loops ([#53274](https://github.com/angular/angular/pull/53274)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1c1e8c477b](https://github.com/angular/angular/commit/1c1e8c477b8d5c8deecf05744bd1d5f7c86d2e14) | fix | CF migration - ensure NgIfElse attributes are properly removed ([#53298](https://github.com/angular/angular/pull/53298)) |
|
||||
| [2998d482dd](https://github.com/angular/angular/commit/2998d482dd3f1f5ff7f08260c3947ded74dac126) | fix | CF Migration - Fix case of aliases on i18n ng-templates preventing removal ([#53299](https://github.com/angular/angular/pull/53299)) |
|
||||
| [aad5e5bd0e](https://github.com/angular/angular/commit/aad5e5bd0e7f6581f51fc3a23c98a7f11219a8f8) | fix | CF Migration add support for ngIf with just a then ([#53297](https://github.com/angular/angular/pull/53297)) |
|
||||
| [6f75471307](https://github.com/angular/angular/commit/6f75471307a9458d0cffd0ee2bbf4190640c3c3a) | fix | CF Migration fix missing alias for bound ngifs ([#53296](https://github.com/angular/angular/pull/53296)) |
|
||||
| [2a5a8f6f05](https://github.com/angular/angular/commit/2a5a8f6f052961c010a68a05d868f50220f2fcf2) | fix | Change CF Migration ng-template placeholder generation and handling ([#53394](https://github.com/angular/angular/pull/53394)) |
|
||||
| [03e2f1bb25](https://github.com/angular/angular/commit/03e2f1bb25693d1a5f4e53fc4f4cd1937cf6bda1) | fix | fix regexp for else and then in cf migration ([#53257](https://github.com/angular/angular/pull/53257)) |
|
||||
| [f4a96a9160](https://github.com/angular/angular/commit/f4a96a9160927903c38e172f6375c4bc5f8e0905) | fix | handle aliases on bound ngIf migrations ([#53261](https://github.com/angular/angular/pull/53261)) |
|
||||
| [5a0ed28c9d](https://github.com/angular/angular/commit/5a0ed28c9d3f1c7507feee482e37c3346d52b033) | fix | handle nested ng-template replacement safely in CF migration ([#53368](https://github.com/angular/angular/pull/53368)) |
|
||||
| [01b18a4248](https://github.com/angular/angular/commit/01b18a4248f068df33c0ca8a2d62ef2fc69f941c) | fix | handle templates outside of component in cf migration ([#53368](https://github.com/angular/angular/pull/53368)) |
|
||||
| [9834fd2738](https://github.com/angular/angular/commit/9834fd27387ed5edc65bed56aa003fc45d250420) | fix | remove setting that removes comments in CF migration ([#53350](https://github.com/angular/angular/pull/53350)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5c1d441029](https://github.com/angular/angular/commit/5c1d4410298e20cb03d7a1ddf7931538b6a181b4) | feat | Add info property to `NavigationExtras` ([#53303](https://github.com/angular/angular/pull/53303)) |
|
||||
| [50d7916278](https://github.com/angular/angular/commit/50d79162785bb8d3e158a7a4a3733f4c75d3b127) | feat | Add router configuration to resolve navigation promise on error ([#48910](https://github.com/angular/angular/pull/48910)) |
|
||||
| [1940280d27](https://github.com/angular/angular/commit/1940280d27bb3ece585d6345dbd742d208f46912) | fix | Ensure canMatch guards run on wildcard routes ([#53239](https://github.com/angular/angular/pull/53239)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.6"></a>
|
||||
# 17.0.6 (2023-12-06)
|
||||
### compiler
|
||||
@@ -247,43 +184,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0-next.2"></a>
|
||||
# 17.1.0-next.2 (2023-11-29)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [79ff91a813](https://github.com/angular/angular/commit/79ff91a813e544929cb5eb5f9aab762a9f3d0435) | fix | allow TS jsDocParsingMode host option to be programmatically set ([#53126](https://github.com/angular/angular/pull/53126)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4c1d69e288](https://github.com/angular/angular/commit/4c1d69e2880f22745c820eee630d10071e4fa86b) | fix | add diagnostic for control flow that prevents content projection ([#53190](https://github.com/angular/angular/pull/53190)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [58cf389d80](https://github.com/angular/angular/commit/58cf389d8095dd522a998b6b9a7d7b8da1656644) | fix | avoid stale provider info when TestBed.overrideProvider is used ([#52918](https://github.com/angular/angular/pull/52918)) |
|
||||
| [c7c7ea9813](https://github.com/angular/angular/commit/c7c7ea9813f6dcf91c096bb37d36bfe0c715a04f) | fix | inherit host directives ([#52992](https://github.com/angular/angular/pull/52992)) |
|
||||
| [b35c6731e5](https://github.com/angular/angular/commit/b35c6731e51de9c33707010fc780cbaa559be6c3) | fix | Reattached views that are dirty from a signal update should refresh ([#53001](https://github.com/angular/angular/pull/53001)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dbca1c9d61](https://github.com/angular/angular/commit/dbca1c9d618be4d195d779ef80606c6d22f7c977) | fix | Add ngForTemplate support to control flow migration ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [53912fdf74](https://github.com/angular/angular/commit/53912fdf74e1224f95edc3dd550c51451e613c44) | fix | allows colons in ngIf else cases to migrate ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [fadfee4324](https://github.com/angular/angular/commit/fadfee43247acec7e450d5cc929a0a6780a54bd1) | fix | cf migration fix migrating empty switch default ([#53237](https://github.com/angular/angular/pull/53237)) |
|
||||
| [b2aeaf5d97](https://github.com/angular/angular/commit/b2aeaf5d97327842a5bb657e9f02bb4f1358304b) | fix | CF migration log warning when collection aliasing detected in `@for` ([#53238](https://github.com/angular/angular/pull/53238)) |
|
||||
| [c6326289f8](https://github.com/angular/angular/commit/c6326289f87089b3415ba0f3e371206b8a396b66) | fix | cf migration removes unnecessary bound ngifelse attribute ([#53236](https://github.com/angular/angular/pull/53236)) |
|
||||
| [e6f10e81d2](https://github.com/angular/angular/commit/e6f10e81d2486b5cc1f468777b7e1057d3f09dc5) | fix | control flow migration formatting fixes ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [6291c8db09](https://github.com/angular/angular/commit/6291c8db092c0ab67f6789cc03c19dfa0bdc251d) | fix | fix off by one issue with template removal in CF migration ([#53255](https://github.com/angular/angular/pull/53255)) |
|
||||
| [f1b7d40057](https://github.com/angular/angular/commit/f1b7d400575a6a02a2fc2b642c2b8c15cca9b3c4) | fix | fixes CF migration i18n ng-template offsets ([#53212](https://github.com/angular/angular/pull/53212)) |
|
||||
| [a738b48717](https://github.com/angular/angular/commit/a738b48717e16613b34c3e50480cd49f2a31858f) | fix | fixes control flow migration common module removal ([#53076](https://github.com/angular/angular/pull/53076)) |
|
||||
| [a3599515bb](https://github.com/angular/angular/commit/a3599515bb2513fbcb2899284c7f6b4bc021ddaf) | fix | properly handle ngIfThen cases in CF migration ([#53256](https://github.com/angular/angular/pull/53256)) |
|
||||
| [8a52674faa](https://github.com/angular/angular/commit/8a52674faacefd4042726383fdf0aed59a04fb7e) | fix | Update CF migration to skip templates with duplicate ng-template names ([#53204](https://github.com/angular/angular/pull/53204)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [29e0834c4d](https://github.com/angular/angular/commit/29e0834c4deecfa8bf384b5e4359796c8123afcd) | fix | Resolvers in different parts of the route tree should be able to execute together ([#52934](https://github.com/angular/angular/pull/52934)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.5"></a>
|
||||
# 17.0.5 (2023-11-29)
|
||||
### core
|
||||
@@ -313,39 +213,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0-next.1"></a>
|
||||
# 17.1.0-next.1 (2023-11-20)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [29c5416d14](https://github.com/angular/angular/commit/29c5416d14638a05a894269aa5dbe67e98754418) | fix | remove `load` on image once it fails to load ([#52990](https://github.com/angular/angular/pull/52990)) |
|
||||
| [7affa57754](https://github.com/angular/angular/commit/7affa5775427e92ef6e949c879765b7c8aa172da) | fix | scan images once page is loaded ([#52991](https://github.com/angular/angular/pull/52991)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ec2d6e7b9c](https://github.com/angular/angular/commit/ec2d6e7b9c2b386247d1320ee89f8e3ac5e5a0dd) | fix | changed after checked error in for loops ([#52935](https://github.com/angular/angular/pull/52935)) |
|
||||
| [406049b95e](https://github.com/angular/angular/commit/406049b95e5234f17a7a18839ac848640f53fdde) | fix | generate i18n instructions for blocks ([#52958](https://github.com/angular/angular/pull/52958)) |
|
||||
| [d9d566d315](https://github.com/angular/angular/commit/d9d566d31540582d73201675d0b8ed901261669e) | fix | nested for loops incorrectly calculating computed variables ([#52931](https://github.com/angular/angular/pull/52931)) |
|
||||
| [5fb707f81a](https://github.com/angular/angular/commit/5fb707f81aee43751e61d2ed0861afc9b85bc85a) | fix | produce placeholder for blocks in i18n bundles ([#52958](https://github.com/angular/angular/pull/52958)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b4d022e230](https://github.com/angular/angular/commit/b4d022e230ca141b12437949d11dc384bfe5c082) | fix | add diagnostic for control flow that prevents content projection ([#52726](https://github.com/angular/angular/pull/52726)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ed0fbd4071](https://github.com/angular/angular/commit/ed0fbd4071339b1af22d82bac07d51c6c41790cd) | fix | cleanup loading promise when no dependencies are defined ([#53031](https://github.com/angular/angular/pull/53031)) |
|
||||
| [1ce31d819b](https://github.com/angular/angular/commit/1ce31d819b2e4f4425a41f07167a6edce98e77e1) | fix | handle local refs when `getDeferBlocks` is invoked in tests ([#52973](https://github.com/angular/angular/pull/52973)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e33f6e0f1a](https://github.com/angular/angular/commit/e33f6e0f1a483cad908fa6d7376d62332797499c) | fix | control flow migration fails for async pipe with unboxing of observable ([#52756](https://github.com/angular/angular/pull/52756)) ([#52972](https://github.com/angular/angular/pull/52972)) |
|
||||
| [5564d020cd](https://github.com/angular/angular/commit/5564d020cdcea8273b65cf69c45c3f935195af66) | fix | Fixes control flow migration if then else case ([#53006](https://github.com/angular/angular/pull/53006)) |
|
||||
| [28f6cbf9c9](https://github.com/angular/angular/commit/28f6cbf9c91f957b4926fe34610387e1f1919d4f) | fix | fixes migrations of nested switches in control flow ([#53010](https://github.com/angular/angular/pull/53010)) |
|
||||
| [e090b48bf8](https://github.com/angular/angular/commit/e090b48bf8534761d46523be57a7889a325bcdec) | fix | tweaks to formatting in control flow migration ([#53058](https://github.com/angular/angular/pull/53058)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.4"></a>
|
||||
# 17.0.4 (2023-11-20)
|
||||
### common
|
||||
@@ -379,24 +246,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.1.0-next.0"></a>
|
||||
# 17.1.0-next.0 (2023-11-15)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [56a76d73e0](https://github.com/angular/angular/commit/56a76d73e037aeea1975808d5c51608fd23d4fa6) | fix | modify `getConstructorDependencies` helper to work with reflection host after the previous change ([#52215](https://github.com/angular/angular/pull/52215)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [94096c6ede](https://github.com/angular/angular/commit/94096c6ede67436a349ae07901f2bb418bf9f461) | feat | support TypeScript 5.3 ([#52572](https://github.com/angular/angular/pull/52572)) |
|
||||
| [bdd61c768a](https://github.com/angular/angular/commit/bdd61c768a28b56c68634b99c036986499829f45) | fix | replace assertion with more intentional error ([#52234](https://github.com/angular/angular/pull/52234)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [726530a9af](https://github.com/angular/angular/commit/726530a9af9c8daf7295cc3548f24e70f380d70e) | feat | Allow `onSameUrlNavigation: 'ignore'` in `navigateByUrl` ([#52265](https://github.com/angular/angular/pull/52265)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="17.0.3"></a>
|
||||
# 17.0.3 (2023-11-15)
|
||||
### animations
|
||||
|
||||
+40
-2
@@ -1,6 +1,6 @@
|
||||
load("//:packages.bzl", "link_packages")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
@@ -21,6 +21,13 @@ APPLICATION_FILES = [
|
||||
],
|
||||
)
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
)
|
||||
|
||||
APPLICATION_DEPS = link_packages([
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular/animations",
|
||||
@@ -69,6 +76,21 @@ APPLICATION_DEPS = link_packages([
|
||||
"@npm//angular-split",
|
||||
])
|
||||
|
||||
TEST_DEPS = APPLICATION_DEPS + link_packages([
|
||||
"@npm//@angular/platform-browser-dynamic",
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium",
|
||||
"@npm//@types/jasmine",
|
||||
"@npm//@types/node",
|
||||
"@npm//assert",
|
||||
"@npm//jasmine",
|
||||
"@npm//jasmine-core",
|
||||
"@npm//karma-chrome-launcher",
|
||||
"@npm//karma-coverage",
|
||||
"@npm//karma-jasmine",
|
||||
"@npm//karma-jasmine-html-reporter",
|
||||
"//aio/tools:windows-chromium-path",
|
||||
])
|
||||
|
||||
copy_to_bin(
|
||||
name = "application_files_bin",
|
||||
srcs = APPLICATION_FILES,
|
||||
@@ -78,7 +100,7 @@ architect(
|
||||
name = "build",
|
||||
args = [
|
||||
"angular-dev:build",
|
||||
"--output-path=build-app",
|
||||
"--output-path=build",
|
||||
],
|
||||
chdir = "$(RULEDIR)",
|
||||
data = APPLICATION_DEPS + [
|
||||
@@ -100,3 +122,19 @@ architect(
|
||||
":application_files_bin",
|
||||
],
|
||||
)
|
||||
|
||||
architect_test(
|
||||
name = "test",
|
||||
args = [
|
||||
"angular-dev:test",
|
||||
"--no-watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = TEST_DEPS + TEST_FILES,
|
||||
env = {
|
||||
"CHROME_BIN": "../$(CHROMIUM)",
|
||||
},
|
||||
toolchains = [
|
||||
"@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias",
|
||||
],
|
||||
)
|
||||
|
||||
+2
-2
@@ -26,8 +26,7 @@
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"server": "src/main.server.ts",
|
||||
// TODO(josephperrott): enabled prerender
|
||||
"prerender": false,
|
||||
"prerender": true,
|
||||
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
@@ -89,6 +88,7 @@
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"hosting": {
|
||||
"target": "angular-docs",
|
||||
"public": "./build/browser",
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
"value": "cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public,max-age=31536000,immutable"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/[0-9a-f][0-9a-f][0-9a-f].[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].js",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/@(ngsw-worker.js|ngsw.json)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/**",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Opener-Policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const {getAdjustedChromeBinPathForWindows} = require('../aio/tools/windows-chromium-path');
|
||||
|
||||
process.env.CHROME_BIN = getAdjustedChromeBinPathForWindows();
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
{'reporter:jasmine-seed': ['type', JasmineSeedReporter]},
|
||||
],
|
||||
proxies: {
|
||||
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
|
||||
},
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
random: true,
|
||||
seed: '',
|
||||
},
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/site'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
],
|
||||
},
|
||||
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
// See /integration/README.md#browser-tests for more info on these args
|
||||
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
|
||||
},
|
||||
},
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
browserNoActivityTimeout: 60000,
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function JasmineSeedReporter(baseReporterDecorator) {
|
||||
baseReporterDecorator(this);
|
||||
|
||||
this.onBrowserComplete = (browser, result) => {
|
||||
const seed = result.order && result.order.random && result.order.seed;
|
||||
if (seed) this.write(`${browser}: Randomized with seed ${seed}.\n`);
|
||||
};
|
||||
|
||||
this.onRunComplete = () => undefined;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Injector} from '@angular/core';
|
||||
import {WINDOW} from '@angular/docs';
|
||||
import {ENVIRONMENT, WINDOW} from '@angular/docs';
|
||||
import {AnalyticsService} from './analytics.service';
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
@@ -36,6 +36,7 @@ describe('AnalyticsService', () => {
|
||||
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
{provide: ENVIRONMENT, useValue: {}},
|
||||
{provide: AnalyticsService, deps: [WINDOW]},
|
||||
{provide: WINDOW, useFactory: () => mockWindow, deps: []},
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
|
||||
|
||||
import {CodeEditor, REQUIRED_FILES} from './code-editor.component';
|
||||
import {CodeMirrorEditor} from './code-mirror-editor.service';
|
||||
import {FakeChangeDetectorRef} from '../../../../../shared/src/lib/utils/test-utils.spec';
|
||||
import {FakeChangeDetectorRef} from '@angular/docs/testing';
|
||||
import {TutorialType} from '@angular/docs';
|
||||
|
||||
const files = [
|
||||
|
||||
@@ -8,16 +8,9 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import type {FileSystemTree} from '@webcontainer/api';
|
||||
|
||||
import type {
|
||||
TutorialConfig,
|
||||
TutorialMetadata,
|
||||
} from '@angular/docs';
|
||||
import {
|
||||
TUTORIALS_COMMON_DIRECTORY,
|
||||
TUTORIALS_METADATA_WEB_PATH,
|
||||
TUTORIALS_SOURCE_CODE_WEB_PATH,
|
||||
TutorialType,
|
||||
} from '@angular/docs';
|
||||
import type {TutorialConfig, TutorialMetadata} from '@angular/docs';
|
||||
import {TUTORIALS_ASSETS_WEB_PATH} from '../editor/constants';
|
||||
import {TutorialType} from '@angular/docs';
|
||||
import {EmbeddedTutorialManager} from './embedded-tutorial-manager.service';
|
||||
|
||||
describe('EmbeddedTutorialManager', () => {
|
||||
@@ -252,13 +245,7 @@ describe('EmbeddedTutorialManager', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const commonSourceCode: FileSystemTree = {
|
||||
'common.ts': {
|
||||
file: {
|
||||
contents: 'code',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const metadata: TutorialMetadata = {
|
||||
tutorialFiles: {'app.js': ''},
|
||||
openFiles: ['app.js'],
|
||||
@@ -271,14 +258,13 @@ describe('EmbeddedTutorialManager', () => {
|
||||
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
fetchMock.and.returnValues(
|
||||
Promise.resolve(new Response(JSON.stringify(commonSourceCode))),
|
||||
Promise.resolve(new Response(JSON.stringify(tutorialSourceCode))),
|
||||
Promise.resolve(new Response(JSON.stringify(metadata))),
|
||||
);
|
||||
|
||||
await service.fetchAndSetTutorialFiles(tutorial);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(service.tutorialId()).toEqual(tutorial);
|
||||
expect(service.tutorialFilesystemTree()).toEqual(tutorialSourceCode);
|
||||
@@ -289,8 +275,6 @@ describe('EmbeddedTutorialManager', () => {
|
||||
|
||||
expect(service.type()).toEqual(metadata.type);
|
||||
|
||||
expect(service.commonFilesystemTree()).toEqual(commonSourceCode);
|
||||
|
||||
expect(service.hiddenFiles()).toEqual(metadata.hiddenFiles);
|
||||
expect(service.answerFiles()).toEqual(metadata.answerFiles!);
|
||||
});
|
||||
@@ -299,15 +283,11 @@ describe('EmbeddedTutorialManager', () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response(null, {status: 404})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`)
|
||||
.and.returnValues(Promise.resolve(new Response(null, {status: 404})));
|
||||
|
||||
await expectAsync(service.fetchAndSetTutorialFiles(tutorial)).toBeRejectedWithError(
|
||||
@@ -315,38 +295,15 @@ describe('EmbeddedTutorialManager', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the tutorial common project cannot be fetched', async () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response(null, {status: 404})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
await expectAsync(service.fetchAndSetTutorialFiles(tutorial)).toBeRejectedWithError(
|
||||
'Missing source code for tutorial common',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set shouldReinstallDependencies if project did not change', async () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@@ -368,15 +325,12 @@ describe('EmbeddedTutorialManager', () => {
|
||||
it('should trigger shouldReInstallDependencies if new metadata has different dependencies', async () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@@ -404,14 +358,10 @@ describe('EmbeddedTutorialManager', () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@@ -437,14 +387,10 @@ describe('EmbeddedTutorialManager', () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@@ -472,16 +418,12 @@ describe('EmbeddedTutorialManager', () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
const allFiles = ['file1.ts', 'file2.ts'];
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@@ -507,16 +449,12 @@ describe('EmbeddedTutorialManager', () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
const allFiles = ['file1.ts', 'file2.ts'];
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
@@ -539,16 +477,12 @@ describe('EmbeddedTutorialManager', () => {
|
||||
const fetchMock = spyOn(window, 'fetch');
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${TUTORIALS_COMMON_DIRECTORY}.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
fetchMock
|
||||
.withArgs(`${TUTORIALS_SOURCE_CODE_WEB_PATH}/${tutorial}.json`)
|
||||
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
|
||||
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
|
||||
|
||||
const allFiles = ['file1.ts', 'file2.ts'];
|
||||
|
||||
fetchMock.withArgs(`${TUTORIALS_METADATA_WEB_PATH}/${tutorial}.json`).and.returnValues(
|
||||
fetchMock.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`).and.returnValues(
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -15,7 +15,7 @@ import {TutorialType} from '@angular/docs';
|
||||
import {
|
||||
FakeWebContainer,
|
||||
FakeWebContainerProcess,
|
||||
} from '../../../../shared/src/lib/utils/test-utils.spec';
|
||||
} from '@angular/docs/testing';
|
||||
import {AlertManager} from './alert-manager.service';
|
||||
import {EmbeddedTutorialManager} from './embedded-tutorial-manager.service';
|
||||
import {LoadingStep} from './enums/loading-steps';
|
||||
|
||||
@@ -11,7 +11,8 @@ import {By} from '@angular/platform-browser';
|
||||
|
||||
import {Terminal} from './terminal.component';
|
||||
import {TerminalHandler, TerminalType} from './terminal-handler.service';
|
||||
import {FakeEventTarget, WINDOW} from '@angular/docs';
|
||||
import {WINDOW} from '@angular/docs';
|
||||
import {FakeEventTarget} from '@angular/docs/testing';
|
||||
|
||||
describe('Terminal', () => {
|
||||
let component: Terminal;
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
EmbeddedEditor,
|
||||
EmbeddedTutorialManager,
|
||||
NodeRuntimeSandbox
|
||||
} from '@angular/docs';
|
||||
} from '../../editor';
|
||||
|
||||
import {mockAsyncProvider} from '../../core/services/inject-async';
|
||||
import Tutorial from './tutorial.component';
|
||||
|
||||
@@ -78,7 +78,7 @@ For example, imagine the following scenario:
|
||||
1. After some time, a new version of the application is deployed to the server.
|
||||
This newer version includes the files `index.html`, `main.<main-hash-2>.js` and `lazy-chunk.<lazy-hash-2>.js`.
|
||||
|
||||
IMPORANT: The hashes are different now, because the content of the files changed. The old version is no longer available on the server.
|
||||
IMPORTANT: The hashes are different now, because the content of the files changed. The old version is no longer available on the server.
|
||||
|
||||
1. In the meantime, the user's browser decides to evict `lazy-chunk.<lazy-hash-1>.js` from its cache.
|
||||
Browsers might decide to evict specific (or all) resources from a cache in order to reclaim disk space.
|
||||
|
||||
@@ -110,7 +110,7 @@ Make a change to the application, and watch the service worker install the updat
|
||||
<docs-code language="shell">
|
||||
|
||||
ng build
|
||||
npx http-server -p 8080 -c-1 dist/
|
||||
npx http-server -p 8080 -c-1 dist/<project-name>/browser
|
||||
|
||||
</docs-code>
|
||||
|
||||
|
||||
@@ -240,10 +240,13 @@ In the example below, the prefetching starts when a browser becomes idle and the
|
||||
|
||||
## Testing
|
||||
|
||||
Angular provides TestBed APIs to simplify the process of testing `@defer` blocks and triggering different states during testing. By default, `@defer` blocks in tests are in "paused" (`@placeholder`) state, so that you can manually transition between states.
|
||||
Angular provides TestBed APIs to simplify the process of testing `@defer` blocks and triggering different states during testing. By default, `@defer` blocks in tests will play through like a defer block would behave in a real application. If you want to manually step through states, you can switch the defer block behavior to `Manual` in the TestBed configuration.
|
||||
|
||||
```typescript
|
||||
it('should render a defer block in different states', async () => {
|
||||
// configures the defer block behavior to start in "paused" state for manual control.
|
||||
TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual});
|
||||
|
||||
@Component({
|
||||
// ...
|
||||
template: `
|
||||
|
||||
@@ -11,7 +11,7 @@ The injection context is available in these situations:
|
||||
* In the `factory` function specified for an `InjectionToken`.
|
||||
* Within a stack frame that is run in a injection context.
|
||||
|
||||
Knowing when your are in an injection context, will allow you to use the [`inject`](api/core/inject) function to inject instances.
|
||||
Knowing when you are in an injection context, will allow you to use the [`inject`](api/core/inject) function to inject instances.
|
||||
|
||||
## Class constructors
|
||||
|
||||
@@ -33,6 +33,7 @@ class MyComponent {
|
||||
Some APIs are designed to be run in an injection context. This is the case, for example, of the router guards. It allows the use of [`inject`](api/core/inject) to access a service within the guard function.
|
||||
|
||||
Here is an example for `CanActivateFn`
|
||||
|
||||
<docs-code language="typescript" highlight="[3]">
|
||||
const canActivateTeam: CanActivateFn =
|
||||
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
|
||||
@@ -12,9 +12,6 @@ With hierarchical dependency injection, you can isolate sections of the applicat
|
||||
|
||||
## Types of injector hierarchies
|
||||
|
||||
Injectors in Angular have rules that you can leverage to achieve the desired visibility of injectables in your applications.
|
||||
By understanding these rules, you can determine whether to declare a provider at the application level, in a Component, or in a Directive.
|
||||
|
||||
Angular has two injector hierarchies:
|
||||
|
||||
| Injector hierarchies | Details |
|
||||
|
||||
@@ -50,7 +50,7 @@ const configPromise = firstValueFrom(config$);
|
||||
const req = httpTesting.expectOne('/api/config', 'Request to load the configuration');
|
||||
|
||||
// We can assert various properties of the request if desired.
|
||||
expect(req.method).toBe('GET');
|
||||
expect(req.request.method).toBe('GET');
|
||||
|
||||
// Flushing the request causes it to complete, delivering the result.
|
||||
req.flush(DEFAULT_CONFIG);
|
||||
|
||||
@@ -224,7 +224,7 @@ For details of those options and their possible values, see the [Angular CLI Ref
|
||||
|
||||
## Complex configuration values
|
||||
|
||||
The `assets`, `index`, `styles`, and `scripts` options can have either simple path string values, or object values with specific fields.
|
||||
The `assets`, `index`, `outputPath`, `styles`, and `scripts` options can have either simple path string values, or object values with specific fields.
|
||||
The `sourceMap` and `optimization` options can be set to a simple boolean value. They can also be given a complex value using the configuration file.
|
||||
|
||||
The following sections provide more details of how these complex values are used in each case.
|
||||
@@ -418,6 +418,7 @@ Several options can be used to fine-tune the optimization of an application.
|
||||
|:--- |:--- |:--- |:--- |
|
||||
| `minify` | Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers, and minimizing values. | `boolean` | `true` |
|
||||
| `inlineCritical` | Extract and inline critical CSS definitions to improve [First Contentful Paint](https://web.dev/first-contentful-paint). | `boolean` | `true` |
|
||||
| `removeSpecialComments` | Remove comments in global CSS that contains `@license` or `@preserve` or that starts with `//!` or `/*!`. | `boolean` | `true` |
|
||||
|
||||
#### Fonts optimization options
|
||||
|
||||
@@ -508,3 +509,16 @@ When supplying the value as a string the filename of the specified path will be
|
||||
|:--- |:--- |:--- |:--- |
|
||||
| `input` | The path of a file to use for the application's generated HTML index. | `string` | None (required) |
|
||||
| `output` | The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path. | `string` | `index.html` |
|
||||
|
||||
### Output path configuration
|
||||
|
||||
The `outputPath` option can be either a String which will be used as the `base` value or an Object for more fine-tune configuration.
|
||||
|
||||
Several options can be used to fine-tune the output structure of an application.
|
||||
|
||||
| Options | Details | Value type | Default value |
|
||||
|:--- |:--- |:--- |:--- |
|
||||
| `base` | Specify the output path relative to workspace root. | `string` | |
|
||||
| `browser` | The output directory name for your browser build is within the base output path. This can be safely served to users. | `string` | `browser` |
|
||||
| `server` | The output directory name of your server build within the output path base. | `string` | `server` |
|
||||
| `media` | The output directory name for your media files located within the output browser directory. These media files are commonly referred to as resources in CSS files. | `string` | `media` |
|
||||
|
||||
@@ -9,6 +9,7 @@ This table covers [Angular versions under active support](reference/releases#act
|
||||
|
||||
| Angular | Node.js | TypeScript | RxJS |
|
||||
| ------------------ | ------------------------------------ | -------------- | ------------------ |
|
||||
| 17.1.0 | ^18.13.0 \|\| ^20.9.0 | >=5.2.0 <5.4.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 17.0.x | ^18.13.0 \|\| ^20.9.0 | >=4.9.3 <5.3.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 16.1.x \|\| 16.2.x | ^16.14.0 \|\| ^18.10.0 | >=4.9.3 <5.2.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 16.0.x | ^16.14.0 \|\| ^18.10.0 | >=4.9.3 <5.1.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
|
||||
@@ -106,7 +106,7 @@ Also, the later sections of this guide provide additional information on several
|
||||
|
||||
For applications new to SSR, the [Angular SSR Guide](guide/ssr) provides additional information regarding the setup process for adding SSR to an application.
|
||||
|
||||
For applications that are already using SSR, additional manual adjustments will be needed to update the application server to support the new integrated SSR capabilities.
|
||||
For applications that are already using SSR, additional adjustments will be needed to update the application server to support the new integrated SSR capabilities.
|
||||
The `application` builder now provides the integrated functionality for all of the following preexisting builders:
|
||||
|
||||
- `app-shell`
|
||||
@@ -117,25 +117,23 @@ The `application` builder now provides the integrated functionality for all of t
|
||||
The `ng update` process will automatically remove usages of the `@nguniversal` scope packages where some of these builders were previously located.
|
||||
The new `@angular/ssr` package will also be automatically added and used with configuration and code being adjusted during the update.
|
||||
The `@angular/ssr` package supports the `browser` builder as well as the `application` builder.
|
||||
To convert from the separate SSR builders to the integrated capabilities of the `application` builder, there are several required manual steps.
|
||||
However, as each application is different, there may be more application specific changes needed beyond these to complete the process.
|
||||
To convert from the separate SSR builders to the integrated capabilities of the `application` builder, run the experimental `use-application-builder` migration.
|
||||
|
||||
1. Combine the options for the above mentioned SSR builders into the `application` builder options within the `angular.json` file.
|
||||
The previously used builders and their target configurations can then be fully removed from the file.
|
||||
2. Combine server TypeScript configuration from `tsconfig.server.json` into `tsconfig.app.json`.
|
||||
The `types` and `files` options are typically the only setting that needs to be combined but others may be needed based on application specific customizations.
|
||||
You should also add the TypeScript option `"esModuleInterop": true` to ensure `express` imports are [ESM compliant](#esm-default-imports-vs-namespace-imports).
|
||||
The `tsconfig.server.json` can then be removed as it will no longer be used during builds.
|
||||
3. Remove and/or update any `npm` scripts referencing the now removed builder targets.
|
||||
The `ng build` and `ng serve` commands provide equivalent functionality when using the `application` builder.
|
||||
4. Update application server code to remove Webpack specific elements.
|
||||
5. Update application server code to use new bootstrapping and output directory structure.
|
||||
An example of the changes for a v16 project that has been converted can be found [here](https://github.com/alan-agius4/angular-cli-use-application-builder/commit/1defdb93a7f508662bc427439e51505668bf84cd#diff-1ba718c1eb8aa39cd20c2562d92523068c734d75f54655e97d652b992d9b4259).
|
||||
6. Remove any CommonJS assumptions in the application server code such as `require`, `__filename`, `__dirname`, or other constructs from the [CommonJS module scope](https://nodejs.org/api/modules.html#the-module-scope).
|
||||
All application code should be ESM compatible.
|
||||
This does not apply to third-party dependencies.
|
||||
<docs-code language="shell">
|
||||
|
||||
In the future, a schematic will make this migration process easier for existing applications.
|
||||
ng update @angular/cli --name use-application-builder
|
||||
|
||||
</docs-code>
|
||||
|
||||
The migration does the following:
|
||||
|
||||
* Converts existing `browser` or `browser-esbuild` target to `application`
|
||||
* Removes any previous SSR builders (because `application` does that now).
|
||||
* Updates configuration accordingly.
|
||||
* Merges `tsconfig.server.json` with `tsconfig.app.json` and adds the TypeScript option `"esModuleInterop": true` to ensure `express` imports are [ESM compliant](#esm-default-imports-vs-namespace-imports).
|
||||
* Updates application server code to use new bootstrapping and output directory structure.
|
||||
|
||||
HELPFUL: Remember to remove any CommonJS assumptions in the application server code such as `require`, `__filename`, `__dirname`, or other constructs from the [CommonJS module scope](https://nodejs.org/api/modules.html#the-module-scope). All application code should be ESM compatible. This does not apply to third-party dependencies.
|
||||
|
||||
## Executing a build
|
||||
|
||||
@@ -175,8 +173,6 @@ Several build options are not yet implemented but will be added in the future as
|
||||
|
||||
- [WASM imports](https://github.com/angular/angular-cli/issues/25102) -- WASM can still be loaded manually via [standard web APIs](https://developer.mozilla.org/en-US/docs/WebAssembly/Loading_and_running).
|
||||
|
||||
Building libraries with the new build system via `ng-packagr` is also not yet possible but library build support will be available in a future release.
|
||||
|
||||
## ESM default imports vs. namespace imports
|
||||
|
||||
TypeScript by default allows default exports to be imported as namespace imports and then used in call expressions.
|
||||
|
||||
@@ -27,7 +27,7 @@ In the `HomeComponent` there is only a single housing location. In this step, yo
|
||||
1. Update the `HomeComponent` class to have a property called `housingLocationList`. Update your code to match the following code:
|
||||
<docs-code header="Add housingLocationList property" path="adev/src/content/tutorials/first-app/steps/09-services/src/app/home/home.component.ts" visibleLines="31-136"/>
|
||||
|
||||
IMPORANT: Do not remove the `@Component` decorator, you will update that code in an upcoming step.
|
||||
IMPORTANT: Do not remove the `@Component` decorator, you will update that code in an upcoming step.
|
||||
|
||||
</docs-step>
|
||||
|
||||
@@ -37,7 +37,7 @@ Now the app has a dataset that you can use to display the entries in the browser
|
||||
1. Update the `<app-housing-location>` tag in the template code to this:
|
||||
<docs-code header="Add ngFor to HomeComponent template" path="adev/src/content/tutorials/first-app/steps/09-services/src/app/home/home.component.ts" visibleLines="[21,24]"/>
|
||||
|
||||
Note, in the code `[housingLocation] = "housingLocation"` the `housingLocation` value now refers to the variable used in the `ngFor` directive. Before this change, it refered to the property on the `HomeComponent` class.
|
||||
Note, in the code `[housingLocation] = "housingLocation"` the `housingLocation` value now refers to the variable used in the `ngFor` directive. Before this change, it referred to the property on the `HomeComponent` class.
|
||||
|
||||
1. Save all changes.
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ The `HomeComponent` already contains an input field that you will use to capture
|
||||
<input type="text" placeholder="Filter by city" #filter>
|
||||
</docs-code>
|
||||
|
||||
This example uses a [template reference variable](/guide/templatess) to get access to the `input` element as its value.
|
||||
This example uses a [template reference variable](/guide/templates) to get access to the `input` element as its value.
|
||||
|
||||
1. Next, update the component template to attach an event handler to the "Search" button.
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ The data source has been configured, the next step is to update your web app to
|
||||
|
||||
1. Add a string property called `url` and set its value to `'http://localhost:3000/locations'`
|
||||
|
||||
<docs-code anguage="javascript">
|
||||
<docs-code language="javascript">
|
||||
url = 'http://localhost:3000/locations';
|
||||
</docs-code>
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Add a `@loading` block to the `@defer` block. The `@loading` block is where you
|
||||
|
||||
<docs-step title="Add a minimum duration">
|
||||
|
||||
Both `@placeholder` and `@loading` sections have optional parameters to prevent flickering from occuring when loading happens quickly. `@placeholder` has `minimum` and `@loading` has `minimum` and `after`. Add a `minimum` duration to the `@loading` block so it will be rendered for at least 2 seconds.
|
||||
Both `@placeholder` and `@loading` sections have optional parameters to prevent flickering from occurring when loading happens quickly. `@placeholder` has `minimum` and `@loading` has `minimum` and `after`. Add a `minimum` duration to the `@loading` block so it will be rendered for at least 2 seconds.
|
||||
|
||||
<docs-code language="html" highlight="[5]">
|
||||
@defer {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ReactiveFormsModule } from '@angular/forms';
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
templateUrl: `
|
||||
template: `
|
||||
<form>
|
||||
<label>Name
|
||||
<input type="text" />
|
||||
|
||||
@@ -43,4 +43,4 @@ Next, bind the `contentEditable` attribute of the `div` to the `isEditable` prop
|
||||
|
||||
The div is now editable. Nice work 👍
|
||||
|
||||
Property binding is one of Angular's many powerful features. If you'd like to learn more checkout [the Angular documention](/guide/templates/property-binding).
|
||||
Property binding is one of Angular's many powerful features. If you'd like to learn more checkout [the Angular documentation](/guide/templates/property-binding).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Component Communication with `@Output`
|
||||
|
||||
When working with components it may be required to notify other components that something has happened. Perhaps a button has been clicked, an item has been added/removed from a list or some other important update has occured. In this scenario components need to communicate with parent components.
|
||||
When working with components it may be required to notify other components that something has happened. Perhaps a button has been clicked, an item has been added/removed from a list or some other important update has occurred. In this scenario components need to communicate with parent components.
|
||||
|
||||
Angular uses the `@Output` decorator to enable this type of behavior.
|
||||
|
||||
|
||||
+4
-1
@@ -28,7 +28,10 @@
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
"paths": {
|
||||
"@angular/*": ["../packages/*"],
|
||||
},
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
|
||||
@@ -240,10 +240,13 @@ In the example below, the prefetching starts when a browser becomes idle and the
|
||||
|
||||
## Testing
|
||||
|
||||
Angular provides TestBed APIs to simplify the process of testing `@defer` blocks and triggering different states during testing. By default, `@defer` blocks in tests are in "paused" (`@placeholder`) state, so that you can manually transition between states.
|
||||
Angular provides TestBed APIs to simplify the process of testing `@defer` blocks and triggering different states during testing. By default, `@defer` blocks in tests will play through like a defer block would behave in a real application. If you want to manually step through states, you can switch the defer block behavior to `Manual` in the TestBed configuration.
|
||||
|
||||
```typescript
|
||||
it('should render a defer block in different states', async () => {
|
||||
// configures the defer block behavior to start in "paused" state for manual control.
|
||||
TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual});
|
||||
|
||||
@Component({
|
||||
// ...
|
||||
template: `
|
||||
|
||||
@@ -96,12 +96,12 @@ The following table lists all the `browser` builder options that will need to be
|
||||
| `browser` Option | Action | Notes |
|
||||
| :-------------- | :----- | :----- |
|
||||
| `main` | rename option to `browser` | |
|
||||
| `polyfills` | convert value to an array | may already have been migrated |
|
||||
| `polyfills` | convert value to an array | may already have been migrated |
|
||||
| `buildOptimizer` | remove option |
|
||||
| `resourcesOutputPath` | remove option | always `media` |
|
||||
| `resourcesOutputPath` | move option to `outputPath.media` | defaults to `media` |
|
||||
| `vendorChunk` | remove option |
|
||||
| `commonChunk` | remove option |
|
||||
| `deployUrl` | remove option |
|
||||
| `deployUrl` | remove option |
|
||||
| `ngswConfigPath` | move value to `serviceWorker` and remove option | `serviceWorker` is now either `false` or a configuration path
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ Also, the later sections of this guide provide additional information on several
|
||||
|
||||
For applications new to SSR, the [Angular SSR Guide](/guide/ssr) provides additional information regarding the setup process for adding SSR to an application.
|
||||
|
||||
For applications that are already using SSR, additional manual adjustments will be needed to update the application server to support the new integrated SSR capabilities.
|
||||
For applications that are already using SSR, additional adjustments will be needed to update the application server to support the new integrated SSR capabilities.
|
||||
The `application` builder now provides the integrated functionality for all of the following preexisting builders:
|
||||
|
||||
* `app-shell`
|
||||
@@ -124,25 +124,27 @@ The `application` builder now provides the integrated functionality for all of t
|
||||
The `ng update` process will automatically remove usages of the `@nguniversal` scope packages where some of these builders were previously located.
|
||||
The new `@angular/ssr` package will also be automatically added and used with configuration and code being adjusted during the update.
|
||||
The `@angular/ssr` package supports the `browser` builder as well as the `application` builder.
|
||||
To convert from the separate SSR builders to the integrated capabilities of the `application` builder, there are several required manual steps.
|
||||
However, as each application is different, there may be more application specific changes needed beyond these to complete the process.
|
||||
To convert from the separate SSR builders to the integrated capabilities of the `application` builder, run the experimental `use-application-builder` migration.
|
||||
|
||||
1. Combine the options for the above mentioned SSR builders into the `application` builder options within the `angular.json` file.
|
||||
The previously used builders and their target configurations can then be fully removed from the file.
|
||||
2. Combine server TypeScript configuration from `tsconfig.server.json` into `tsconfig.app.json`.
|
||||
The `types` and `files` options are typically the only setting that needs to be combined but others may be needed based on application specific customizations.
|
||||
You should also add the TypeScript option `"esModuleInterop": true` to ensure `express` imports are [ESM compliant](#esm-default-imports-vs-namespace-imports).
|
||||
The `tsconfig.server.json` can then be removed as it will no longer be used during builds.
|
||||
3. Remove and/or update any `npm` scripts referencing the now removed builder targets.
|
||||
The `ng build` and `ng serve` commands provide equivalent functionality when using the `application` builder.
|
||||
4. Update application server code to remove Webpack specific elements.
|
||||
5. Update application server code to use new bootstrapping and output directory structure.
|
||||
An example of the changes for a v16 project that has been converted can be found [here](https://github.com/alan-agius4/angular-cli-use-application-builder/commit/1defdb93a7f508662bc427439e51505668bf84cd#diff-1ba718c1eb8aa39cd20c2562d92523068c734d75f54655e97d652b992d9b4259).
|
||||
6. Remove any CommonJS assumptions in the application server code such as `require`, `__filename`, `__dirname`, or other constructs from the [CommonJS module scope](https://nodejs.org/api/modules.html#the-module-scope).
|
||||
All application code should be ESM compatible.
|
||||
This does not apply to third-party dependencies.
|
||||
<code-example language="shell">
|
||||
|
||||
In the future, a schematic will make this migration process easier for existing applications.
|
||||
ng update @angular/cli --name use-application-builder
|
||||
|
||||
</code-example>
|
||||
|
||||
The migration does the following:
|
||||
|
||||
* Converts existing `browser` or `browser-esbuild` target to `application`
|
||||
* Removes any previous SSR builders (because `application` does that now).
|
||||
* Updates configuration accordingly.
|
||||
* Merges `tsconfig.server.json` with `tsconfig.app.json` and adds the TypeScript option `"esModuleInterop": true` to ensure `express` imports are [ESM compliant](#esm-default-imports-vs-namespace-imports).
|
||||
* Updates application server code to use new bootstrapping and output directory structure.
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
Remember to remove any CommonJS assumptions in the application server code such as `require`, `__filename`, `__dirname`, or other constructs from the [CommonJS module scope](https://nodejs.org/api/modules.html#the-module-scope). All application code should be ESM compatible. This does not apply to third-party dependencies.
|
||||
|
||||
</div>
|
||||
|
||||
## Executing a build
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ You can run it without installing it like this:
|
||||
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
npx http-server -p 8080 -c-1 dist/
|
||||
npx http-server -p 8080 -c-1 dist/<project-name>/browser
|
||||
|
||||
</code-example>
|
||||
|
||||
@@ -174,7 +174,7 @@ Make a change to the application, and watch the service worker install the updat
|
||||
<code-example format="shell" language="shell">
|
||||
|
||||
ng build
|
||||
npx http-server -p 8080 -c-1 dist/
|
||||
npx http-server -p 8080 -c-1 dist/<project-name>/browser
|
||||
|
||||
</code-example>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ covers [Angular versions under active support](guide/releases#actively-supported
|
||||
|
||||
| Angular | Node.js | TypeScript | RxJS |
|
||||
| ------------------ | ------------------------------------ | -------------- | ------------------ |
|
||||
| 17.1.0 | ^18.13.0 \|\| ^20.9.0 | >=5.2.0 <5.4.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 17.0.x | ^18.13.0 \|\| ^20.9.0 | >=4.9.3 <5.3.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 16.1.x \|\| 16.2.x | ^16.14.0 \|\| ^18.10.0 | >=4.9.3 <5.2.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
| 16.0.x | ^16.14.0 \|\| ^18.10.0 | >=4.9.3 <5.1.0 | ^6.5.3 \|\| ^7.4.0 |
|
||||
|
||||
@@ -233,7 +233,7 @@ Similar to defining a component, directives are comprised of the following:
|
||||
- A selector that defines the tag name is when the component is called
|
||||
- A TypeScript class that defines the extended behavior the directive will add to the respective HTML element.
|
||||
|
||||
For example, here’s what a custom directive for highlighting an element:
|
||||
For example, here’s a custom directive for highlighting an element:
|
||||
|
||||
```ts
|
||||
@Directive({
|
||||
@@ -311,6 +311,7 @@ Standalone components are a new organizational pattern that were introduced in A
|
||||
For example, in the traditional NgModule pattern, you would need to create a TodoModule and manage all of its dependencies through this module.
|
||||
|
||||
```ts (Todo.module.ts)
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {TodoList} from '../todo/todo-list.component';
|
||||
|
||||
@@ -325,7 +326,6 @@ export class TodoModule {}
|
||||
However, you can now achieve something similar with a standalone component without the need for a module file:
|
||||
|
||||
```ts (Todo.component.ts)
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {TodoList} from '../todo/todo-list.component';
|
||||
|
||||
|
||||
@@ -246,12 +246,11 @@ Some extra options can only be set through the configuration file, either by dir
|
||||
| `fileReplacements` | An object containing files and their compile-time replacements. See more in [Configure target-specific file replacements](guide/build#configure-target-specific-file-replacements). |
|
||||
`index` | Configures the generation of the application's HTML index. See more in [Index configuration](#index-config). \(Only available in `browser` section.\) | |
|
||||
|
||||
|
||||
<a id="complex-config"></a>
|
||||
|
||||
## Complex configuration values
|
||||
|
||||
The `assets`, `index`, `styles`, and `scripts` options can have either simple path string values, or object values with specific fields.
|
||||
The `assets`, `index`, `outputPath`, `styles`, and `scripts` options can have either simple path string values, or object values with specific fields.
|
||||
The `sourceMap` and `optimization` options can be set to a simple Boolean value with a command flag. They can also be given a complex value using the configuration file.
|
||||
|
||||
The following sections provide more details of how these complex values are used in each case.
|
||||
@@ -427,7 +426,7 @@ See also [Using runtime-global libraries inside your application](guide/using-li
|
||||
|
||||
### Optimization configuration
|
||||
|
||||
The `optimization` browser builder option can be either a Boolean or an Object for more fine-tune configuration.
|
||||
The `optimization` option can be either a Boolean or an Object for more fine-tune configuration.
|
||||
This option enables various optimizations of the build output, including:
|
||||
|
||||
<!-- vale Angular.Angular_Spelling = NO-->
|
||||
@@ -454,8 +453,9 @@ Several options can be used to fine-tune the optimization of an application.
|
||||
|
||||
| Options | Details | Value type | Default value |
|
||||
|:--- |:--- |:--- |:--- |
|
||||
| `minify` | Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers, and minimizing values. | `boolean` | `true` |
|
||||
| `minify` | Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers, and minimizing values. | `boolean` | `true` |
|
||||
| `inlineCritical` | Extract and inline critical CSS definitions to improve [First Contentful Paint](https://web.dev/first-contentful-paint). | `boolean` | `true` |
|
||||
| `removeSpecialComments` | Remove comments in global CSS that contains `@license` or `@preserve` or that starts with `//!` or `/*!`. | `boolean` | `true` |
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
@@ -539,6 +539,18 @@ When supplying the value as a String the filename of the specified path will be
|
||||
| `input` | The path of a file to use for the application's generated HTML index. | `string` | |
|
||||
| `output` | The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path. | `string` | `index.html` |
|
||||
|
||||
### Output path configuration
|
||||
|
||||
The `outputPath` option can be either a String which will be used as the `base` value or an Object for more fine-tune configuration.
|
||||
|
||||
Several options can be used to fine-tune the output structure of an application.
|
||||
|
||||
| Options | Details | Value type | Default value |
|
||||
|:--- |:--- |:--- |:--- |
|
||||
| `base` | Specify the output path relative to workspace root. | `string` | |
|
||||
| `browser` | The output directory name for your browser build is within the base output path. This can be safely served to users. | `string` | `browser` |
|
||||
| `server` | The output directory name of your server build within the output path base. | `string` | `server` |
|
||||
| `media` | The output directory name for your media files located within the output browser directory. These media files are commonly referred to as resources in CSS files. | `string` | `media` |
|
||||
|
||||
<!-- links -->
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 338 KiB |
@@ -108,6 +108,13 @@
|
||||
"bio": "SDE III @cisco | GDE for Angular | Microsoft MVP | Author | Speaker | Passionate Programmer",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"ankitasood": {
|
||||
"name": "Ankita Sood",
|
||||
"picture": "ankita.jpg",
|
||||
"groups": ["GDE"],
|
||||
"twitter": "guacamoleankita",
|
||||
"bio": "Ankita is an Angular developer, mentor and speaker. She is passionate about all things Angular and Design."
|
||||
},
|
||||
"antoniocardenas": {
|
||||
"name": "Antonio Cardenas",
|
||||
"picture": "antonio-cardenas.jpg",
|
||||
@@ -156,8 +163,7 @@
|
||||
"name": "Aristeidis Bampakos",
|
||||
"picture": "aristeidis.jpg",
|
||||
"twitter": "abampakos",
|
||||
"website": "https://medium.com/@abampakos",
|
||||
"bio": "Aristeidis is a web development team lead at Plex-Earth working with Angular. He is the author of two books about Angular: Learning Angular - 3rd Edition and Angular Projects - 2nd Edition. He also works as an Angular senior tech instructor at Code.Hub where he teaches the Angular framework to other developers and professionals.",
|
||||
"bio": "Aristeidis works as a Web Development Team Leader @ Plex-Earth, specializing in Angular development. He is the author of Learning Angular and Angular Projects books and also works as an Angular Senior Tech Instructor @ Code.Hub where he nurtures aspiring Angular developers and professionals. Aristeidis is currently leading the effort of making Angular accessible to the Greek development community by maintaining the Greek translation of the Angular documentation.",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"bbrennan": {
|
||||
|
||||
@@ -255,7 +255,7 @@ Now that you've configured your application to retrieve shipping data, you can c
|
||||
|
||||
This section guides you through modifying the `ShippingComponent` to retrieve shipping data via HTTP from the `shipping.json` file.
|
||||
|
||||
1. In `shipping.component.ts`, import `CartService`.
|
||||
1. In `shipping.component.ts`, import `OnInit`, `Observable` and `CartService`.
|
||||
|
||||
<code-example header="src/app/shipping/shipping.component.ts" path="getting-started/src/app/shipping/shipping.component.ts" region="imports"></code-example>
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ module.exports =
|
||||
'localize/init/index.ts',
|
||||
'platform-browser/index.ts',
|
||||
'platform-browser/animations/index.ts',
|
||||
'platform-browser/animations/async/index.ts',
|
||||
'platform-browser/testing/index.ts',
|
||||
'platform-browser-dynamic/index.ts',
|
||||
'platform-browser-dynamic/testing/index.ts',
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
# Contributing to Angular DevTools
|
||||
|
||||
We would love for you to contribute to Angular DevTools and help make it even better than it is
|
||||
today! As a contributor, here are the guidelines we would like you to follow:
|
||||
|
||||
- [Question or Problem?](#question)
|
||||
- [Issues and Bugs](#issue)
|
||||
- [Feature Requests](#feature)
|
||||
- [Submission Guidelines](#submit)
|
||||
- [Coding Rules](#rules)
|
||||
- [Commit Message Guidelines](#commit)
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](stackoverflow.com/questions/tagged/angular-devtools) where the questions should be tagged with tag `angular-devtools`.
|
||||
|
||||
StackOverflow is a much better place to ask questions since:
|
||||
|
||||
- there are thousands of people willing to help on StackOverflow
|
||||
- questions and answers stay available for public viewing so your question / answer might help someone else
|
||||
- StackOverflow's voting system assures that the best answers are prominently visible.
|
||||
|
||||
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow.
|
||||
|
||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel](https://gitter.im/angular/angular).
|
||||
|
||||
## <a name="issue"></a> Found an Bug?
|
||||
|
||||
If you find a bug in the source code, you can help us by
|
||||
[submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can
|
||||
[submit a Pull Request](#submit-pr) with a fix.
|
||||
|
||||
## <a name="feature"></a> Missing a Feature?
|
||||
|
||||
You can _request_ a new feature by [submitting an issue](#submit-issue) to our GitHub
|
||||
Repository. If you would like to _implement_ a new feature, please submit an issue with
|
||||
a proposal for your work first, to be sure that we can use it.
|
||||
Please consider what kind of change it is:
|
||||
|
||||
- For a **Major Feature**, first open an issue and outline your proposal so that it can be
|
||||
discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
|
||||
and help you to craft the change so that it is successfully accepted into the project.
|
||||
- **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
|
||||
|
||||
## <a name="submit"></a> Submission Guidelines
|
||||
|
||||
### <a name="submit-issue"></a> Submitting an Issue
|
||||
|
||||
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario by providing a project which breaks Angular DevTools. Also make sure that you list:
|
||||
|
||||
- version of Angular DevTools used
|
||||
- version of Angular used
|
||||
- 3rd-party libraries and their versions
|
||||
- and most importantly - a use-case that fails
|
||||
|
||||
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new/choose).
|
||||
|
||||
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
|
||||
|
||||
#### General Guidelines
|
||||
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
- Search [GitHub](https://github.com/angular/angular) for an open or closed PR
|
||||
that relates to your submission. You don't want to duplicate effort.
|
||||
- [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the angular/angular repo.
|
||||
- In your forked repository, make your changes in a new git branch:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch main
|
||||
```
|
||||
|
||||
- Create your patch, **including appropriate test cases**.
|
||||
- Follow our [Coding Rules](#rules).
|
||||
- Commit your changes using a descriptive commit message that follows our
|
||||
[commit message conventions](#commit). Adherence to these conventions
|
||||
is necessary because release notes are automatically generated from these messages.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
- Push your branch to GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
- In GitHub, send a pull request to `angular:main`.
|
||||
- If we suggest changes then:
|
||||
|
||||
- Make the required updates.
|
||||
- Re-run the Angular DevTools test and lint suites to ensure tests are still passing and you're following the coding style.
|
||||
- Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
|
||||
|
||||
```shell
|
||||
git rebase upstream/main -i
|
||||
git push -f
|
||||
```
|
||||
|
||||
That's it! Thank you for your contribution!
|
||||
|
||||
#### After your pull request is merged
|
||||
|
||||
After your pull request is merged, you can safely delete your branch and pull the changes
|
||||
from the upstream repository:
|
||||
|
||||
- Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
|
||||
|
||||
```shell
|
||||
git push origin --delete my-fix-branch
|
||||
```
|
||||
|
||||
- Check out the main branch:
|
||||
|
||||
```shell
|
||||
git checkout main -f
|
||||
```
|
||||
|
||||
- Delete the local branch:
|
||||
|
||||
```shell
|
||||
git branch -D my-fix-branch
|
||||
```
|
||||
|
||||
- Update your local `main` with the latest upstream version:
|
||||
|
||||
```shell
|
||||
git pull --ff upstream main
|
||||
```
|
||||
|
||||
## <a name="rules"></a> Coding Rules
|
||||
|
||||
To ensure consistency throughout the source code, keep these rules in mind as you are working:
|
||||
|
||||
- All features or bug fixes **must be tested** by one or more specs (unit-tests).
|
||||
- We follow [Google's JavaScript Style Guide][js-style-guide].
|
||||
|
||||
## <a name="commit"></a> Commit Message Guidelines
|
||||
|
||||
We have very precise rules over how our git commit messages can be formatted. This leads to **more
|
||||
readable messages** that are easy to follow when looking through the **project history**. But also,
|
||||
we use the git commit messages to **generate the Angular change log**.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
|
||||
format that includes a **type**, a **scope** and a **subject**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The **header** is mandatory and the **scope** of the header is optional.
|
||||
|
||||
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
|
||||
to read on GitHub as well as in various git tools.
|
||||
|
||||
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
|
||||
Samples: (even more [samples](https://github.com/angular/angular/commits/main/devtools))
|
||||
|
||||
```
|
||||
docs(changelog): update change log to beta.5
|
||||
```
|
||||
|
||||
```
|
||||
fix(release): need to depend on latest rxjs and zone.js
|
||||
|
||||
The version in our package.json gets copied to the one we publish, and users need the latest of these.
|
||||
```
|
||||
|
||||
### Revert
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
||||
|
||||
### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- **feat**: A new feature
|
||||
- **fix**: A bug fix
|
||||
- **docs**: Documentation only changes
|
||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
- **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
- **perf**: A code change that improves performance
|
||||
- **test**: Adding missing tests or correcting existing tests
|
||||
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||
|
||||
### Scope
|
||||
|
||||
The scope could be anything specifying place of the commit change. For example
|
||||
`frontend`, `backend`, etc.
|
||||
|
||||
### Subject
|
||||
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
- use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
- don't capitalize first letter
|
||||
- no dot (.) at the end
|
||||
|
||||
### Body
|
||||
|
||||
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
|
||||
The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
### Footer
|
||||
|
||||
The footer should contain any information about **Breaking Changes** and is also the place to
|
||||
reference GitHub issues that this commit **Closes**.
|
||||
|
||||
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
A detailed explanation can be found in this [document][commit-message-format].
|
||||
|
||||
[github]: https://github.com/angular/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[stackoverflow]: https://stackoverflow.com/questions/tagged/angular-devtools
|
||||
+12
-6
@@ -4,14 +4,18 @@ To setup your development environment run:
|
||||
|
||||
```
|
||||
git clone git@github.com:angular/angular
|
||||
cd devtools
|
||||
yarn
|
||||
```
|
||||
|
||||
To run the extension in development mode run:
|
||||
|
||||
```
|
||||
ng serve
|
||||
yarn devtools:devserver
|
||||
```
|
||||
|
||||
You can also run a standalone version of the demo app with
|
||||
```
|
||||
yarn devtools:devserver:demo-standalone
|
||||
```
|
||||
|
||||
This would start a development server that you can access on <http://localhost:4200>. In development, Angular DevTools uses a "development shell." This is different from "chrome shell" in a way, that it runs the user's app in an iframe. DevTools then communicate with the user's app via message passing.
|
||||
@@ -21,13 +25,15 @@ This would start a development server that you can access on <http://localhost:4
|
||||
To build the Angular DevTools Chrome extension run:
|
||||
|
||||
```
|
||||
npm run release
|
||||
yarn devtools:build:chrome
|
||||
```
|
||||
|
||||
This would output the production assets of the app in `dist/shell-browser`. If you want to build the extension with development settings (i.e., not minified sources), just run:
|
||||
You can then install the extension from `dist/bin/devtools/projects/shell-browser/src/prodapp` following the guide from [here](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#load-unpacked).
|
||||
|
||||
|
||||
You can also build the extension for Firefox with:
|
||||
```
|
||||
ng build shell-browser
|
||||
yarn devtools:build:firefox
|
||||
```
|
||||
|
||||
Later on, you can install the extension from `dist/shell-browser` following the guide from [here](https://developer.chrome.com/apps/external_extensions).
|
||||
In Firefox, to load the extension, you can go to the about:debugging page, click the "This Firefox" option and then click the Load Temporary Add-on button. You'll have to select the manifest file in `dist/bin/devtools/projects/shell-browser/src/prodapp` directly.
|
||||
|
||||
+1
-8
@@ -4,14 +4,7 @@ Angular DevTools is a browser DevTools extension for debugging and profiling Ang
|
||||
|
||||
## Installing and Running locally
|
||||
|
||||
To setup your local development environment for Angular DevTools, perform the following steps:
|
||||
|
||||
```bash
|
||||
git clone git://github.com/angular/angular
|
||||
cd angular
|
||||
yarn
|
||||
yarn run devtools:devserver
|
||||
```
|
||||
To setup your local development environment for Angular DevTools, see [DEVELOPING.md](./DEVELOPING.md)
|
||||
|
||||
## Publish Angular DevTools
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ describe('Comment nodes', () => {
|
||||
it('should find comment nodes when the setting is enabled', () => {
|
||||
showComments();
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("#comment")')
|
||||
.its('length')
|
||||
.should('not.eq', 0);
|
||||
.find('.tree-node:contains("#comment")')
|
||||
.its('length')
|
||||
.should('not.eq', 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,27 +19,29 @@ describe('Tracking items from application to component tree', () => {
|
||||
});
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.its('length')
|
||||
.should('eq', 2);
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.its('length')
|
||||
.should('eq', 2);
|
||||
});
|
||||
|
||||
it('should be able to detect a new todo from user and add it to the tree', () => {
|
||||
cy.enter('#sample-app')
|
||||
.then((getBody) => {
|
||||
getBody().find('input.new-todo').type('Buy cookies{enter}');
|
||||
})
|
||||
.then(() => {
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo').contains('Buy milk');
|
||||
.then((getBody) => {
|
||||
getBody().find('input.new-todo').type('Buy cookies{enter}');
|
||||
})
|
||||
.then(() => {
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo').contains('Buy milk');
|
||||
|
||||
getBody().find('app-todo').contains('Build something fun!');
|
||||
getBody().find('app-todo').contains('Build something fun!');
|
||||
|
||||
getBody().find('app-todo').contains('Buy cookies');
|
||||
});
|
||||
getBody().find('app-todo').contains('Buy cookies');
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.should('have.length', 3);
|
||||
cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")').should(
|
||||
'have.length',
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,9 +83,9 @@ describe('Search items in component tree', () => {
|
||||
const amountOfBreadcrumbButtons = 4;
|
||||
const amountOfScrollButtons = 2;
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('button')
|
||||
.its('length')
|
||||
.should('eq', amountOfScrollButtons + amountOfBreadcrumbButtons);
|
||||
.find('button')
|
||||
.its('length')
|
||||
.should('eq', amountOfScrollButtons + amountOfBreadcrumbButtons);
|
||||
|
||||
// should display correct text in explorer panel
|
||||
checkComponentName('app-todos');
|
||||
|
||||
@@ -18,9 +18,9 @@ describe('node selection', () => {
|
||||
cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist');
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
|
||||
cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1);
|
||||
|
||||
@@ -39,9 +39,9 @@ describe('node selection', () => {
|
||||
});
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo:contains("Buy milk")').find('.destroy').click();
|
||||
@@ -52,86 +52,100 @@ describe('node selection', () => {
|
||||
|
||||
it('should select nodes with same name', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true});
|
||||
|
||||
cy.get('ng-property-view').last().find('mat-tree-node:contains("todo")').click();
|
||||
|
||||
cy.get('ng-property-view')
|
||||
.last()
|
||||
.find('mat-tree-node:contains("Build something fun!")')
|
||||
.its('length')
|
||||
.should('eq', 1);
|
||||
.last()
|
||||
.find('mat-tree-node:contains("Build something fun!")')
|
||||
.its('length')
|
||||
.should('eq', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('breadcrumb logic', () => {
|
||||
it('should overflow when breadcrumb list is long enough', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs').find('.breadcrumbs').then((breadcrumbsContainer) => {
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((breadcrumbsContainer) => {
|
||||
const hasOverflowX = () =>
|
||||
breadcrumbsContainer[0].scrollWidth > breadcrumbsContainer[0].clientWidth;
|
||||
breadcrumbsContainer[0].scrollWidth > breadcrumbsContainer[0].clientWidth;
|
||||
expect(hasOverflowX()).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should scroll right when right scroll button is clicked', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
|
||||
cy.get('ng-breadcrumbs').find('.scroll-button').last().click().then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
});
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.scroll-button')
|
||||
.last()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should scroll left when left scroll button is clicked', () => {
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
.find('.tree-node:contains("div[TooltipDirective]")')
|
||||
.last()
|
||||
.click({force: true})
|
||||
.then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.breadcrumbs')
|
||||
.then((el) => {
|
||||
el[0].style.scrollBehavior = 'auto';
|
||||
})
|
||||
.then((breadcrumbsContainer) => {
|
||||
const scrollLeft = () => breadcrumbsContainer[0].scrollLeft;
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
|
||||
cy.get('ng-breadcrumbs').find('.scroll-button').last().click().then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.scroll-button')
|
||||
.last()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(scrollLeft()).to.be.greaterThan(0);
|
||||
|
||||
cy.get('ng-breadcrumbs').find('.scroll-button').first().click().then(() => {
|
||||
cy.get('ng-breadcrumbs')
|
||||
.find('.scroll-button')
|
||||
.first()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(scrollLeft()).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,9 +17,9 @@ describe('edit properties of directive in the property view tab', () => {
|
||||
beforeEach(() => {
|
||||
// select todo node in component tree
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
});
|
||||
|
||||
it('should be able to enable editMode', () => {
|
||||
@@ -28,13 +28,13 @@ describe('edit properties of directive in the property view tab', () => {
|
||||
});
|
||||
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("editMode")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click({force: true})
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
.find('ng-property-view mat-tree-node:contains("editMode")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click({force: true})
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
getBody().find('app-todo input.edit').should('be.visible');
|
||||
@@ -45,8 +45,8 @@ describe('edit properties of directive in the property view tab', () => {
|
||||
beforeEach(() => {
|
||||
// expand todo state
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
});
|
||||
|
||||
it('should change todo label in app when edited', () => {
|
||||
@@ -57,13 +57,13 @@ describe('edit properties of directive in the property view tab', () => {
|
||||
|
||||
// find label variable and run through edit logic
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("label")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('Buy cookies')
|
||||
.type('{enter}');
|
||||
.find('ng-property-view mat-tree-node:contains("label")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('Buy cookies')
|
||||
.type('{enter}');
|
||||
|
||||
// assert that the page has been updated
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
@@ -79,13 +79,13 @@ describe('edit properties of directive in the property view tab', () => {
|
||||
|
||||
// find completed variable and run through edit logic
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("completed")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
.find('ng-property-view mat-tree-node:contains("completed")')
|
||||
.find('ng-property-editor .editor')
|
||||
.click()
|
||||
.find('.editor-input')
|
||||
.clear()
|
||||
.type('true')
|
||||
.type('{enter}');
|
||||
|
||||
// assert that the page has been updated
|
||||
cy.enter('#sample-app').then((getBody) => {
|
||||
|
||||
@@ -21,20 +21,21 @@ describe('change of the state should reflect in property update', () => {
|
||||
|
||||
// Select the todo item
|
||||
cy.get('.tree-wrapper')
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
.find('.tree-node:contains("app-todo[TooltipDirective]")')
|
||||
.first()
|
||||
.click({force: true});
|
||||
|
||||
// Expand the todo in the property explorer
|
||||
cy.get('.explorer-panel:contains("app-todo")')
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
.find('ng-property-view mat-tree-node:contains("todo")')
|
||||
.click();
|
||||
|
||||
// Verify its value is now completed
|
||||
cy.contains(
|
||||
'.explorer-panel:contains("app-todo") ' +
|
||||
'ng-property-view mat-tree-node:contains("completed") ' +
|
||||
'ng-property-editor .editor',
|
||||
'true');
|
||||
'.explorer-panel:contains("app-todo") ' +
|
||||
'ng-property-view mat-tree-node:contains("completed") ' +
|
||||
'ng-property-editor .editor',
|
||||
'true',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,9 +17,9 @@ describe('Viewing component metadata', () => {
|
||||
});
|
||||
|
||||
describe('viewing TodoComponent', () => {
|
||||
beforeEach(
|
||||
() => prepareHeaderExpansionPanelForAssertions(
|
||||
'.tree-node:contains("app-todo[TooltipDirective]")'));
|
||||
beforeEach(() =>
|
||||
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-todo[TooltipDirective]")'),
|
||||
);
|
||||
|
||||
it('should display view encapsulation', () => {
|
||||
cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: Emulated');
|
||||
@@ -31,9 +31,9 @@ describe('Viewing component metadata', () => {
|
||||
});
|
||||
|
||||
describe('viewing DemoAppComponent', () => {
|
||||
beforeEach(
|
||||
() =>
|
||||
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'));
|
||||
beforeEach(() =>
|
||||
prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'),
|
||||
);
|
||||
|
||||
it('should display view encapsulation', () => {
|
||||
cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: None');
|
||||
|
||||
@@ -17,5 +17,4 @@ import {AppComponent} from './app.component';
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
export class AppModule {}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import {AppModule} from './app/app.module';
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule, {
|
||||
ngZone: 'noop',
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
.bootstrapModule(AppModule, {
|
||||
ngZone: 'noop',
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
@@ -13,7 +13,7 @@ import {Router, RouterOutlet} from '@angular/router';
|
||||
selector: 'app-root',
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
standalone: true,
|
||||
imports: [RouterOutlet]
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(public router: Router) {}
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
<app-zippy [title]="getTitle()">This is my content</app-zippy>
|
||||
<app-heavy></app-heavy>
|
||||
<div #elementReference>HTMLElement</div>
|
||||
|
||||
<hr/>
|
||||
<h2>Signals</h2>
|
||||
<div>Object Signal: {{objectSignal()|json}}</div>
|
||||
<div>Object Computed: {{objectComputed()|json}}</div>
|
||||
<div>Editable Signal: {{primitiveSignal()}}</div>
|
||||
<div>Primitive computed: {{primitiveComputed()}}</div>
|
||||
|
||||
@@ -6,7 +6,21 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, EventEmitter, inject, Injector, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core';
|
||||
import {JsonPipe} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Injector,
|
||||
Input,
|
||||
Output,
|
||||
signal,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {createCustomElement} from '@angular/elements';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {initializeMessageBus} from 'ng-devtools-backend';
|
||||
@@ -22,8 +36,8 @@ import {ZippyComponent} from './zippy.component';
|
||||
styleUrls: ['./demo-app.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
imports: [HeavyComponent, RouterOutlet],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
imports: [HeavyComponent, RouterOutlet, JsonPipe],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class DemoAppComponent {
|
||||
@ViewChild(ZippyComponent) zippy!: ZippyComponent;
|
||||
@@ -35,12 +49,21 @@ export class DemoAppComponent {
|
||||
@Output() outputOne = new EventEmitter();
|
||||
@Output('output_two') outputTwo = new EventEmitter();
|
||||
|
||||
primitiveSignal = signal(123);
|
||||
primitiveComputed = computed(() => this.primitiveSignal() ** 2);
|
||||
objectSignal = signal({name: 'John', age: 40});
|
||||
objectComputed = computed(() => {
|
||||
const original = this.objectSignal();
|
||||
return {...original, age: original.age + 1};
|
||||
});
|
||||
test = [signal(3)];
|
||||
|
||||
constructor() {
|
||||
const el = createCustomElement(ZippyComponent, {injector: inject(Injector)});
|
||||
customElements.define('app-zippy', el as any);
|
||||
}
|
||||
|
||||
getTitle(): '► Click to expand'|'▼ Click to collapse' {
|
||||
getTitle(): '► Click to expand' | '▼ Click to collapse' {
|
||||
if (!this.zippy || !this.zippy.visible) {
|
||||
return '► Click to expand';
|
||||
}
|
||||
@@ -61,5 +84,10 @@ export const ROUTES = [
|
||||
},
|
||||
];
|
||||
|
||||
initializeMessageBus(new ZoneUnawareIFrameMessageBus(
|
||||
'angular-devtools-backend', 'angular-devtools', () => window.parent));
|
||||
initializeMessageBus(
|
||||
new ZoneUnawareIFrameMessageBus(
|
||||
'angular-devtools-backend',
|
||||
'angular-devtools',
|
||||
() => window.parent,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -18,8 +18,7 @@ const fib = (n: number): number => {
|
||||
@Component({selector: 'app-heavy', template: `<h1>{{ calculate() }}</h1>`, standalone: true})
|
||||
export class HeavyComponent {
|
||||
@Input()
|
||||
set foo(_: any) {
|
||||
}
|
||||
set foo(_: any) {}
|
||||
|
||||
state = {
|
||||
nested: {
|
||||
@@ -27,14 +26,12 @@ export class HeavyComponent {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
[Symbol(3)]():
|
||||
number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo():
|
||||
number {
|
||||
return 42;
|
||||
},
|
||||
[Symbol(3)](): number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
},
|
||||
};
|
||||
calculate(): number {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {RouterLink, RouterOutlet} from '@angular/router';
|
||||
<a [routerLink]="">Home</a>
|
||||
<a [routerLink]="">Home</a>
|
||||
<a [routerLink]="">Home</a>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class AboutComponent {
|
||||
}
|
||||
export class AboutComponent {}
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
|
||||
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
||||
|
||||
export interface DialogData {
|
||||
animal: string;
|
||||
@@ -21,7 +19,7 @@ export interface DialogData {
|
||||
@Component({
|
||||
selector: 'app-dialog',
|
||||
standalone: true,
|
||||
imports: [MatDialogModule, MatFormFieldModule, FormsModule],
|
||||
imports: [MatDialogModule, MatFormField, MatLabel, FormsModule],
|
||||
template: `
|
||||
<h1 mat-dialog-title>Hi {{ data.name }}</h1>
|
||||
<div mat-dialog-content>
|
||||
@@ -39,8 +37,9 @@ export interface DialogData {
|
||||
})
|
||||
export class DialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData) {}
|
||||
public dialogRef: MatDialogRef<DialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData,
|
||||
) {}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
|
||||
@@ -10,7 +10,7 @@ import {OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'sample', pure: false, standalone: true})
|
||||
export class SamplePipe implements PipeTransform, OnDestroy {
|
||||
transform(val: any): void {
|
||||
transform(val: unknown) {
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,17 +22,21 @@ export interface Todo {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TooltipDirective],
|
||||
styles: [`
|
||||
styles: [
|
||||
`
|
||||
.destroy {
|
||||
cursor: pointer;
|
||||
display: unset !important;
|
||||
}
|
||||
`],
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<li [class.completed]="todo.completed">
|
||||
<div class="view" appTooltip>
|
||||
<input class="toggle" type="checkbox" [checked]="todo.completed" (change)="toggle()" />
|
||||
<label (dblclick)="enableEditMode()" [style.display]="editMode ? 'none' : 'block'">{{ todo.label }}</label>
|
||||
<label (dblclick)="enableEditMode()" [style.display]="editMode ? 'none' : 'block'">{{
|
||||
todo.label
|
||||
}}</label>
|
||||
<button class="destroy" (click)="delete.emit(todo)"></button>
|
||||
</div>
|
||||
<input
|
||||
@@ -42,7 +46,7 @@ export interface Todo {
|
||||
(keydown.enter)="completeEdit($any($event.target).value)"
|
||||
/>
|
||||
</li>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class TodoComponent {
|
||||
@Input() todo!: Todo;
|
||||
|
||||
@@ -7,7 +7,16 @@
|
||||
*/
|
||||
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, Pipe, PipeTransform} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
Pipe,
|
||||
PipeTransform,
|
||||
} from '@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
import {SamplePipe} from './sample.pipe';
|
||||
@@ -57,19 +66,25 @@ const fib = (n: number): number => {
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input (keydown.enter)="addTodo(input)" #input class="new-todo" placeholder="What needs to be done?" autofocus />
|
||||
<input
|
||||
(keydown.enter)="addTodo(input)"
|
||||
#input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
<section class="main">
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox" />
|
||||
<label for="toggle-all">Mark all as complete</label>
|
||||
<ul class="todo-list">
|
||||
@for (todo of todos | todosFilter: filterValue; track todo) {
|
||||
<app-todo
|
||||
appTooltip
|
||||
[todo]="todo"
|
||||
(delete)="onDelete($event)"
|
||||
(update)="onChange($event)"
|
||||
/>
|
||||
<app-todo
|
||||
appTooltip
|
||||
[todo]="todo"
|
||||
(delete)="onDelete($event)"
|
||||
(update)="onChange($event)"
|
||||
/>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -80,7 +95,7 @@ const fib = (n: number): number => {
|
||||
<button class="clear-completed" (click)="clearCompleted()">Clear completed</button>
|
||||
</footer>
|
||||
</section>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class TodosComponent implements OnInit, OnDestroy {
|
||||
todos: Todo[] = [
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
|
||||
@@ -19,9 +18,9 @@ import {DialogComponent} from './dialog.component';
|
||||
@Component({
|
||||
selector: 'app-todo-demo',
|
||||
standalone: true,
|
||||
imports:
|
||||
[RouterLink, RouterOutlet, MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule],
|
||||
styles: [`
|
||||
imports: [RouterLink, RouterOutlet, MatDialogModule, FormsModule],
|
||||
styles: [
|
||||
`
|
||||
nav {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
@@ -37,7 +36,8 @@ import {DialogComponent} from './dialog.component';
|
||||
padding: 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
`],
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<nav>
|
||||
<a routerLink="/demo-app/todos/app">Todos</a>
|
||||
@@ -47,7 +47,7 @@ import {DialogComponent} from './dialog.component';
|
||||
<button class="dialog-open-button" (click)="openDialog()">Open dialog</button>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class TodoAppComponent {
|
||||
name!: string;
|
||||
|
||||
@@ -11,27 +11,29 @@ import {Component, Input} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-zippy',
|
||||
standalone: true,
|
||||
styles: [`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
header {
|
||||
max-width: 120px;
|
||||
border: 1px solid #ccc;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
header {
|
||||
max-width: 120px;
|
||||
border: 1px solid #ccc;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div {
|
||||
max-width: 120px;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
`],
|
||||
div {
|
||||
max-width: 120px;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<section>
|
||||
<header (click)="visible = !visible">{{ title }}</header>
|
||||
@@ -39,7 +41,7 @@ import {Component, Input} from '@angular/core';
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class ZippyComponent {
|
||||
@Input() title!: string;
|
||||
|
||||
@@ -7,45 +7,51 @@
|
||||
*/
|
||||
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {DevToolsModule as NgDevToolsModule} from 'ng-devtools';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
|
||||
import {IFrameMessageBus} from '../../../../../src/iframe-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [NgDevToolsModule],
|
||||
imports: [DevToolsComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
return new PriorityAwareMessageBus(new IFrameMessageBus(
|
||||
'angular-devtools', 'angular-devtools-backend',
|
||||
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!));
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: [`
|
||||
iframe {
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
styles: [
|
||||
`
|
||||
iframe {
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.devtools-wrapper {
|
||||
height: calc(100vh - 345px);
|
||||
}
|
||||
`],
|
||||
.devtools-wrapper {
|
||||
height: calc(100vh - 345px);
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `
|
||||
<iframe #ref src="demo-app/todos/app" id="sample-app"></iframe>
|
||||
<br />
|
||||
<div class="devtools-wrapper">
|
||||
<ng-devtools></ng-devtools>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
})
|
||||
export class DevToolsComponent {
|
||||
messageBus: IFrameMessageBus|null = null;
|
||||
export class DemoDevToolsComponent {
|
||||
messageBus: IFrameMessageBus | null = null;
|
||||
@ViewChild('ref') iframe!: ElementRef;
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
LATEST_SHA: 'BUILD_SCM_COMMIT_SHA', // Stamped at build time by bazel
|
||||
LATEST_SHA: 'BUILD_SCM_COMMIT_SHA', // Stamped at build time by bazel
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ import {DemoApplicationOperations} from '../../../src/demo-application-operation
|
||||
|
||||
import {AppComponent} from './app/app.component';
|
||||
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideAnimations(),
|
||||
@@ -24,7 +23,7 @@ bootstrapApplication(AppComponent, {
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./app/devtools-app/devtools-app.component').then((m) => m.DevToolsComponent),
|
||||
import('./app/devtools-app/devtools-app.component').then((m) => m.DemoDevToolsComponent),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
@@ -40,5 +39,5 @@ bootstrapApplication(AppComponent, {
|
||||
provide: ApplicationEnvironment,
|
||||
useClass: DemoApplicationEnvironment,
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
@@ -124,6 +124,7 @@ ts_library(
|
||||
srcs = ["component-tree.ts"],
|
||||
deps = [
|
||||
":interfaces",
|
||||
":utils",
|
||||
"//devtools/projects/ng-devtools-backend/src/lib/directive-forest",
|
||||
"//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api",
|
||||
"//devtools/projects/ng-devtools-backend/src/lib/state-serializer",
|
||||
|
||||
@@ -6,12 +6,43 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ComponentExplorerViewQuery, ComponentType, DevToolsNode, DirectivePosition, DirectiveType, ElementPosition, Events, MessageBus, ProfilerFrame, SerializedInjector, SerializedProviderRecord} from 'protocol';
|
||||
import {
|
||||
ComponentExplorerViewQuery,
|
||||
ComponentType,
|
||||
DevToolsNode,
|
||||
DirectivePosition,
|
||||
DirectiveType,
|
||||
ElementPosition,
|
||||
Events,
|
||||
MessageBus,
|
||||
ProfilerFrame,
|
||||
SerializedInjector,
|
||||
SerializedProviderRecord,
|
||||
} from 'protocol';
|
||||
import {debounceTime} from 'rxjs/operators';
|
||||
import {appIsAngularInDevMode, appIsAngularIvy, appIsSupportedAngularVersion, getAngularVersion,} from 'shared-utils';
|
||||
import {
|
||||
appIsAngularInDevMode,
|
||||
appIsAngularIvy,
|
||||
appIsSupportedAngularVersion,
|
||||
getAngularVersion,
|
||||
} from 'shared-utils';
|
||||
|
||||
import {ComponentInspector} from './component-inspector/component-inspector';
|
||||
import {getElementInjectorElement, getInjectorFromElementNode, getInjectorProviders, getInjectorResolutionPath, getLatestComponentState, idToInjector, injectorsSeen, isElementInjector, nodeInjectorToResolutionPath, queryDirectiveForest, serializeProviderRecord, serializeResolutionPath, updateState} from './component-tree';
|
||||
import {
|
||||
getElementInjectorElement,
|
||||
getInjectorFromElementNode,
|
||||
getInjectorProviders,
|
||||
getInjectorResolutionPath,
|
||||
getLatestComponentState,
|
||||
idToInjector,
|
||||
injectorsSeen,
|
||||
isElementInjector,
|
||||
nodeInjectorToResolutionPath,
|
||||
queryDirectiveForest,
|
||||
serializeProviderRecord,
|
||||
serializeResolutionPath,
|
||||
updateState,
|
||||
} from './component-tree';
|
||||
import {unHighlight} from './highlighter';
|
||||
import {disableTimingAPI, enableTimingAPI, initializeOrGetDirectiveForestHooks} from './hooks';
|
||||
import {start as startProfiling, stop as stopProfiling} from './hooks/capture';
|
||||
@@ -19,13 +50,15 @@ import {ComponentTreeNode} from './interfaces';
|
||||
import {ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
|
||||
import {setConsoleReference} from './set-console-reference';
|
||||
import {serializeDirectiveState} from './state-serializer/state-serializer';
|
||||
import {runOutsideAngular} from './utils';
|
||||
import {hasDiDebugAPIs, runOutsideAngular} from './utils';
|
||||
|
||||
export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void => {
|
||||
messageBus.on('shutdown', shutdownCallback(messageBus));
|
||||
|
||||
messageBus.on(
|
||||
'getLatestComponentExplorerView', getLatestComponentExplorerViewCallback(messageBus));
|
||||
'getLatestComponentExplorerView',
|
||||
getLatestComponentExplorerViewCallback(messageBus),
|
||||
);
|
||||
|
||||
messageBus.on('queryNgAvailability', checkForAngularCallback(messageBus));
|
||||
|
||||
@@ -54,8 +87,8 @@ export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void =>
|
||||
// once every 250ms
|
||||
runOutsideAngular(() => {
|
||||
initializeOrGetDirectiveForestHooks()
|
||||
.profiler.changeDetection$.pipe(debounceTime(250))
|
||||
.subscribe(() => messageBus.emit('componentTreeDirty'));
|
||||
.profiler.changeDetection$.pipe(debounceTime(250))
|
||||
.subscribe(() => messageBus.emit('componentTreeDirty'));
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -68,55 +101,58 @@ const shutdownCallback = (messageBus: MessageBus<Events>) => () => {
|
||||
messageBus.destroy();
|
||||
};
|
||||
|
||||
const getLatestComponentExplorerViewCallback = (messageBus: MessageBus<Events>) =>
|
||||
(query?: ComponentExplorerViewQuery) => {
|
||||
// We want to force re-indexing of the component tree.
|
||||
// Pressing the refresh button means the user saw stuck UI.
|
||||
const getLatestComponentExplorerViewCallback =
|
||||
(messageBus: MessageBus<Events>) => (query?: ComponentExplorerViewQuery) => {
|
||||
// We want to force re-indexing of the component tree.
|
||||
// Pressing the refresh button means the user saw stuck UI.
|
||||
|
||||
initializeOrGetDirectiveForestHooks().indexForest();
|
||||
initializeOrGetDirectiveForestHooks().indexForest();
|
||||
|
||||
const forest = prepareForestForSerialization(
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
ngDebugDependencyInjectionApiIsSupported());
|
||||
const forest = prepareForestForSerialization(
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
ngDebugDependencyInjectionApiIsSupported(),
|
||||
);
|
||||
|
||||
// cleanup injector id mappings
|
||||
for (const injectorId of idToInjector.keys()) {
|
||||
if (!injectorsSeen.has(injectorId)) {
|
||||
const injector = idToInjector.get(injectorId)!;
|
||||
if (isElementInjector(injector)) {
|
||||
const element = getElementInjectorElement(injector);
|
||||
if (element) {
|
||||
nodeInjectorToResolutionPath.delete(element);
|
||||
}
|
||||
// cleanup injector id mappings
|
||||
for (const injectorId of idToInjector.keys()) {
|
||||
if (!injectorsSeen.has(injectorId)) {
|
||||
const injector = idToInjector.get(injectorId)!;
|
||||
if (isElementInjector(injector)) {
|
||||
const element = getElementInjectorElement(injector);
|
||||
if (element) {
|
||||
nodeInjectorToResolutionPath.delete(element);
|
||||
}
|
||||
|
||||
idToInjector.delete(injectorId);
|
||||
}
|
||||
}
|
||||
injectorsSeen.clear();
|
||||
|
||||
if (!query) {
|
||||
messageBus.emit('latestComponentExplorerView', [{forest}]);
|
||||
return;
|
||||
idToInjector.delete(injectorId);
|
||||
}
|
||||
}
|
||||
injectorsSeen.clear();
|
||||
|
||||
const state = getLatestComponentState(
|
||||
query, initializeOrGetDirectiveForestHooks().getDirectiveForest());
|
||||
if (!query) {
|
||||
messageBus.emit('latestComponentExplorerView', [{forest}]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
const {directiveProperties} = state;
|
||||
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
|
||||
}
|
||||
};
|
||||
const state = getLatestComponentState(
|
||||
query,
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveForest(),
|
||||
);
|
||||
|
||||
if (state) {
|
||||
const {directiveProperties} = state;
|
||||
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForAngularCallback = (messageBus: MessageBus<Events>) => () =>
|
||||
checkForAngular(messageBus);
|
||||
checkForAngular(messageBus);
|
||||
const getRoutesCallback = (messageBus: MessageBus<Events>) => () => getRoutes(messageBus);
|
||||
|
||||
const startProfilingCallback = (messageBus: MessageBus<Events>) => () =>
|
||||
startProfiling((frame: ProfilerFrame) => {
|
||||
messageBus.emit('sendProfilerChunk', [frame]);
|
||||
});
|
||||
startProfiling((frame: ProfilerFrame) => {
|
||||
messageBus.emit('sendProfilerChunk', [frame]);
|
||||
});
|
||||
|
||||
const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
|
||||
messageBus.emit('profilerResults', [stopProfiling()]);
|
||||
@@ -124,33 +160,41 @@ const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
|
||||
|
||||
const selectedComponentCallback = (position: ElementPosition) => {
|
||||
const node = queryDirectiveForest(
|
||||
position, initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest());
|
||||
position,
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
);
|
||||
setConsoleReference({node, position});
|
||||
};
|
||||
|
||||
const getNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (
|
||||
position: DirectivePosition, propPath: string[]) => {
|
||||
const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]);
|
||||
const node = queryDirectiveForest(
|
||||
position.element, initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest());
|
||||
if (!node) {
|
||||
return emitEmpty();
|
||||
}
|
||||
const current =
|
||||
position.directive === undefined ? node.component : node.directives[position.directive];
|
||||
if (!current) {
|
||||
return emitEmpty();
|
||||
}
|
||||
let data = current.instance;
|
||||
for (const prop of propPath) {
|
||||
data = data[prop];
|
||||
if (!data) {
|
||||
console.error('Cannot access the properties', propPath, 'of', node);
|
||||
const getNestedPropertiesCallback =
|
||||
(messageBus: MessageBus<Events>) => (position: DirectivePosition, propPath: string[]) => {
|
||||
const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]);
|
||||
const node = queryDirectiveForest(
|
||||
position.element,
|
||||
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
|
||||
);
|
||||
if (!node) {
|
||||
return emitEmpty();
|
||||
}
|
||||
}
|
||||
messageBus.emit('nestedProperties', [position, {props: serializeDirectiveState(data)}, propPath]);
|
||||
return;
|
||||
};
|
||||
const current =
|
||||
position.directive === undefined ? node.component : node.directives[position.directive];
|
||||
if (!current) {
|
||||
return emitEmpty();
|
||||
}
|
||||
let data = current.instance;
|
||||
for (const prop of propPath) {
|
||||
data = data[prop];
|
||||
if (!data) {
|
||||
console.error('Cannot access the properties', propPath, 'of', node);
|
||||
}
|
||||
}
|
||||
messageBus.emit('nestedProperties', [
|
||||
position,
|
||||
{props: serializeDirectiveState(data)},
|
||||
propPath,
|
||||
]);
|
||||
return;
|
||||
};
|
||||
|
||||
//
|
||||
// Subscribe Helpers
|
||||
@@ -209,44 +253,46 @@ export interface SerializableComponentInstanceType extends ComponentType {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface SerializableComponentTreeNode extends
|
||||
DevToolsNode<SerializableDirectiveInstanceType, SerializableComponentInstanceType> {
|
||||
export interface SerializableComponentTreeNode
|
||||
extends DevToolsNode<SerializableDirectiveInstanceType, SerializableComponentInstanceType> {
|
||||
children: SerializableComponentTreeNode[];
|
||||
}
|
||||
|
||||
// Here we drop properties to prepare the tree for serialization.
|
||||
// We don't need the component instance, so we just traverse the tree
|
||||
// and leave the component name.
|
||||
const prepareForestForSerialization = (roots: ComponentTreeNode[], includeResolutionPath = false):
|
||||
SerializableComponentTreeNode[] => {
|
||||
const serializedNodes: SerializableComponentTreeNode[] = [];
|
||||
for (const node of roots) {
|
||||
const serializedNode: SerializableComponentTreeNode = {
|
||||
element: node.element,
|
||||
component: node.component ? {
|
||||
const prepareForestForSerialization = (
|
||||
roots: ComponentTreeNode[],
|
||||
includeResolutionPath = false,
|
||||
): SerializableComponentTreeNode[] => {
|
||||
const serializedNodes: SerializableComponentTreeNode[] = [];
|
||||
for (const node of roots) {
|
||||
const serializedNode: SerializableComponentTreeNode = {
|
||||
element: node.element,
|
||||
component: node.component
|
||||
? {
|
||||
name: node.component.name,
|
||||
isElement: node.component.isElement,
|
||||
id: initializeOrGetDirectiveForestHooks().getDirectiveId(node.component.instance)!,
|
||||
} :
|
||||
null,
|
||||
directives: node.directives.map(
|
||||
(d) => ({
|
||||
name: d.name,
|
||||
id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!,
|
||||
})),
|
||||
children: prepareForestForSerialization(node.children, includeResolutionPath),
|
||||
};
|
||||
serializedNodes.push(serializedNode);
|
||||
|
||||
if (includeResolutionPath) {
|
||||
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
|
||||
}
|
||||
}
|
||||
|
||||
return serializedNodes;
|
||||
}
|
||||
: null,
|
||||
directives: node.directives.map((d) => ({
|
||||
name: d.name,
|
||||
id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!,
|
||||
})),
|
||||
children: prepareForestForSerialization(node.children, includeResolutionPath),
|
||||
};
|
||||
serializedNodes.push(serializedNode);
|
||||
|
||||
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|undefined {
|
||||
if (includeResolutionPath) {
|
||||
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
|
||||
}
|
||||
}
|
||||
|
||||
return serializedNodes;
|
||||
};
|
||||
|
||||
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[] | undefined {
|
||||
const nodeInjector = getInjectorFromElementNode(node.nativeElement!);
|
||||
if (!nodeInjector) {
|
||||
return [];
|
||||
@@ -274,91 +320,95 @@ function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|
|
||||
return serializedPath;
|
||||
}
|
||||
|
||||
const getInjectorProvidersCallback = (messageBus: MessageBus<Events>) =>
|
||||
(injector: SerializedInjector) => {
|
||||
if (!idToInjector.has(injector.id)) {
|
||||
return;
|
||||
const getInjectorProvidersCallback =
|
||||
(messageBus: MessageBus<Events>) => (injector: SerializedInjector) => {
|
||||
if (!idToInjector.has(injector.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerRecords = getInjectorProviders(idToInjector.get(injector.id)!);
|
||||
const allProviderRecords: SerializedProviderRecord[] = [];
|
||||
|
||||
const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map();
|
||||
|
||||
for (const [index, providerRecord] of providerRecords.entries()) {
|
||||
const record = serializeProviderRecord(
|
||||
providerRecord,
|
||||
index,
|
||||
injector.type === 'environment',
|
||||
);
|
||||
|
||||
allProviderRecords.push(record);
|
||||
|
||||
const records = tokenToRecords.get(providerRecord.token) ?? [];
|
||||
records.push(record);
|
||||
tokenToRecords.set(providerRecord.token, records);
|
||||
}
|
||||
|
||||
const serializedProviderRecords: SerializedProviderRecord[] = [];
|
||||
|
||||
for (const [token, records] of tokenToRecords.entries()) {
|
||||
const multiRecords = records.filter((record) => record.multi);
|
||||
const nonMultiRecords = records.filter((record) => !record.multi);
|
||||
|
||||
for (const record of nonMultiRecords) {
|
||||
serializedProviderRecords.push(record);
|
||||
}
|
||||
|
||||
const providerRecords = getInjectorProviders(idToInjector.get(injector.id)!);
|
||||
const allProviderRecords: SerializedProviderRecord[] = [];
|
||||
|
||||
const tokenToRecords: Map<any, SerializedProviderRecord[]> = new Map();
|
||||
|
||||
for (const [index, providerRecord] of providerRecords.entries()) {
|
||||
const record =
|
||||
serializeProviderRecord(providerRecord, index, injector.type === 'environment');
|
||||
|
||||
allProviderRecords.push(record);
|
||||
|
||||
const records = tokenToRecords.get(providerRecord.token) ?? [];
|
||||
records.push(record);
|
||||
tokenToRecords.set(providerRecord.token, records);
|
||||
const [firstMultiRecord] = multiRecords;
|
||||
if (firstMultiRecord !== undefined) {
|
||||
// All multi providers will have the same token, so we can just use the first one.
|
||||
serializedProviderRecords.push({
|
||||
token: firstMultiRecord.token,
|
||||
type: 'multi',
|
||||
multi: true,
|
||||
// todo(aleksanderbodurri): implememnt way to differentiate multi providers that
|
||||
// provided as viewProviders
|
||||
isViewProvider: firstMultiRecord.isViewProvider,
|
||||
index: records.map((record) => record.index as number),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const serializedProviderRecords: SerializedProviderRecord[] = [];
|
||||
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
|
||||
};
|
||||
|
||||
for (const [token, records] of tokenToRecords.entries()) {
|
||||
const multiRecords = records.filter(record => record.multi);
|
||||
const nonMultiRecords = records.filter(record => !record.multi);
|
||||
const logProvider = (
|
||||
serializedInjector: SerializedInjector,
|
||||
serializedProvider: SerializedProviderRecord,
|
||||
): void => {
|
||||
if (!idToInjector.has(serializedInjector.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const record of nonMultiRecords) {
|
||||
serializedProviderRecords.push(record);
|
||||
}
|
||||
const injector = idToInjector.get(serializedInjector.id)!;
|
||||
|
||||
const [firstMultiRecord] = multiRecords;
|
||||
if (firstMultiRecord !== undefined) {
|
||||
// All multi providers will have the same token, so we can just use the first one.
|
||||
serializedProviderRecords.push({
|
||||
token: firstMultiRecord.token,
|
||||
type: 'multi',
|
||||
multi: true,
|
||||
// todo(aleksanderbodurri): implememnt way to differentiate multi providers that
|
||||
// provided as viewProviders
|
||||
isViewProvider: firstMultiRecord.isViewProvider,
|
||||
index: records.map(record => record.index as number),
|
||||
});
|
||||
}
|
||||
}
|
||||
const providerRecords = getInjectorProviders(injector);
|
||||
|
||||
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
|
||||
};
|
||||
console.group(
|
||||
`%c${serializedInjector.name}`,
|
||||
`color: ${
|
||||
serializedInjector.type === 'element' ? '#a7d5a9' : '#f05057'
|
||||
}; font-size: 1.25rem; font-weight: bold;`,
|
||||
);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('injector: ', injector);
|
||||
|
||||
const logProvider =
|
||||
(serializedInjector: SerializedInjector, serializedProvider: SerializedProviderRecord):
|
||||
void => {
|
||||
if (!idToInjector.has(serializedInjector.id)) {
|
||||
return;
|
||||
}
|
||||
if (typeof serializedProvider.index === 'number') {
|
||||
const provider = providerRecords[serializedProvider.index];
|
||||
|
||||
const injector = idToInjector.get(serializedInjector.id)!;
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('provider: ', provider);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(provider.token, null, {optional: true}));
|
||||
} else if (Array.isArray(serializedProvider.index)) {
|
||||
const providers = serializedProvider.index.map((index) => providerRecords[index]);
|
||||
|
||||
const providerRecords = getInjectorProviders(injector);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('providers: ', providers);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(providers[0].token, null, {optional: true}));
|
||||
}
|
||||
|
||||
console.group(
|
||||
`%c${serializedInjector.name}`,
|
||||
`color: ${
|
||||
serializedInjector.type === 'element' ?
|
||||
'#a7d5a9' :
|
||||
'#f05057'}; font-size: 1.25rem; font-weight: bold;`);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('injector: ', injector);
|
||||
|
||||
if (typeof serializedProvider.index === 'number') {
|
||||
const provider = providerRecords[serializedProvider.index];
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('provider: ', provider);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(provider.token, null, {optional: true}));
|
||||
} else if (Array.isArray(serializedProvider.index)) {
|
||||
const providers = serializedProvider.index.map(index => providerRecords[index]);
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('providers: ', providers);
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`value: `, injector.get(providers[0].token, null, {optional: true}));
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
};
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
+13
-9
@@ -14,7 +14,7 @@ import {initializeOrGetDirectiveForestHooks} from '../hooks';
|
||||
import {ComponentTreeNode} from '../interfaces';
|
||||
|
||||
interface Type<T> extends Function {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
export interface ComponentInspectorOptions {
|
||||
onComponentEnter: (id: number) => void;
|
||||
@@ -28,11 +28,13 @@ export class ComponentInspector {
|
||||
private readonly _onComponentSelect;
|
||||
private readonly _onComponentLeave;
|
||||
|
||||
constructor(componentOptions: ComponentInspectorOptions = {
|
||||
onComponentEnter: () => {},
|
||||
onComponentLeave: () => {},
|
||||
onComponentSelect: () => {},
|
||||
}) {
|
||||
constructor(
|
||||
componentOptions: ComponentInspectorOptions = {
|
||||
onComponentEnter: () => {},
|
||||
onComponentLeave: () => {},
|
||||
onComponentSelect: () => {},
|
||||
},
|
||||
) {
|
||||
this.bindMethods();
|
||||
this._onComponentEnter = componentOptions.onComponentEnter;
|
||||
this._onComponentSelect = componentOptions.onComponentSelect;
|
||||
@@ -57,7 +59,8 @@ export class ComponentInspector {
|
||||
|
||||
if (this._selectedComponent.component && this._selectedComponent.host) {
|
||||
this._onComponentSelect(
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +76,8 @@ export class ComponentInspector {
|
||||
if (this._selectedComponent.component && this._selectedComponent.host) {
|
||||
highlight(this._selectedComponent.host);
|
||||
this._onComponentEnter(
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
|
||||
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +97,7 @@ export class ComponentInspector {
|
||||
|
||||
highlightByPosition(position: ElementPosition): void {
|
||||
const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest();
|
||||
const elementToHighlight: HTMLElement|null = findNodeInForest(position, forest);
|
||||
const elementToHighlight: HTMLElement | null = findNodeInForest(position, forest);
|
||||
if (elementToHighlight) {
|
||||
highlight(elementToHighlight);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,28 @@
|
||||
* 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 {ComponentExplorerViewQuery, DirectiveMetadata, DirectivesProperties, ElementPosition, PropertyQueryTypes, SerializedInjectedService, SerializedInjector, SerializedProviderRecord, UpdatedStateData,} from 'protocol';
|
||||
import {
|
||||
ComponentExplorerViewQuery,
|
||||
DirectiveMetadata,
|
||||
DirectivesProperties,
|
||||
ElementPosition,
|
||||
PropertyQueryTypes,
|
||||
SerializedInjectedService,
|
||||
SerializedInjector,
|
||||
SerializedProviderRecord,
|
||||
UpdatedStateData,
|
||||
} from 'protocol';
|
||||
|
||||
import {buildDirectiveTree, getLViewFromDirectiveOrElementInstance} from './directive-forest/index';
|
||||
import {ngDebugApiIsSupported, ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api';
|
||||
import {deeplySerializeSelectedProperties, serializeDirectiveState,} from './state-serializer/state-serializer';
|
||||
import {
|
||||
ngDebugApiIsSupported,
|
||||
ngDebugClient,
|
||||
ngDebugDependencyInjectionApiIsSupported,
|
||||
} from './ng-debug-api/ng-debug-api';
|
||||
import {
|
||||
deeplySerializeSelectedProperties,
|
||||
serializeDirectiveState,
|
||||
} from './state-serializer/state-serializer';
|
||||
|
||||
// Need to be kept in sync with Angular framework
|
||||
// We can't directly import it from framework now
|
||||
@@ -22,9 +39,21 @@ enum ChangeDetectionStrategy {
|
||||
|
||||
import {ComponentTreeNode, DirectiveInstanceType, ComponentInstanceType} from './interfaces';
|
||||
|
||||
import type {ClassProvider, ExistingProvider, FactoryProvider, InjectOptions, InjectionToken, Injector, Type, ValueProvider, ɵComponentDebugMetadata as ComponentDebugMetadata, ɵProviderRecord as ProviderRecord} from '@angular/core';
|
||||
import type {
|
||||
ClassProvider,
|
||||
ExistingProvider,
|
||||
FactoryProvider,
|
||||
InjectOptions,
|
||||
InjectionToken,
|
||||
Injector,
|
||||
Type,
|
||||
ValueProvider,
|
||||
ɵComponentDebugMetadata as ComponentDebugMetadata,
|
||||
ɵProviderRecord as ProviderRecord,
|
||||
} from '@angular/core';
|
||||
import {isSignal} from './utils';
|
||||
|
||||
export const injectorToId = new WeakMap<Injector|HTMLElement, string>();
|
||||
export const injectorToId = new WeakMap<Injector | HTMLElement, string>();
|
||||
export const nodeInjectorToResolutionPath = new WeakMap<HTMLElement, SerializedInjector[]>();
|
||||
export const idToInjector = new Map<string, Injector>();
|
||||
export const injectorsSeen = new Set<string>();
|
||||
@@ -46,12 +75,14 @@ export function getInjectorResolutionPath(injector: Injector): Injector[] {
|
||||
return ngDebugClient().ɵgetInjectorResolutionPath(injector);
|
||||
}
|
||||
|
||||
export function getInjectorFromElementNode(element: Node): Injector|null {
|
||||
export function getInjectorFromElementNode(element: Node): Injector | null {
|
||||
return ngDebugClient().getInjector(element);
|
||||
}
|
||||
|
||||
function getDirectivesFromElement(element: HTMLElement):
|
||||
{component: unknown|null; directives: unknown[];} {
|
||||
function getDirectivesFromElement(element: HTMLElement): {
|
||||
component: unknown | null;
|
||||
directives: unknown[];
|
||||
} {
|
||||
let component = null;
|
||||
if (element instanceof Element) {
|
||||
component = ngDebugClient().getComponent(element);
|
||||
@@ -64,9 +95,9 @@ function getDirectivesFromElement(element: HTMLElement):
|
||||
}
|
||||
|
||||
export const getLatestComponentState = (
|
||||
query: ComponentExplorerViewQuery,
|
||||
directiveForest?: ComponentTreeNode[],
|
||||
): {directiveProperties: DirectivesProperties}|undefined => {
|
||||
query: ComponentExplorerViewQuery,
|
||||
directiveForest?: ComponentTreeNode[],
|
||||
): {directiveProperties: DirectivesProperties} | undefined => {
|
||||
// if a directive forest is passed in we don't have to build the forest again.
|
||||
directiveForest = directiveForest ?? buildDirectiveForest();
|
||||
|
||||
@@ -80,19 +111,19 @@ export const getLatestComponentState = (
|
||||
const injector = ngDebugClient().getInjector(node.nativeElement!);
|
||||
|
||||
const injectors = getInjectorResolutionPath(injector);
|
||||
const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported() ?
|
||||
[] :
|
||||
injectors.map((injector) => ({
|
||||
injector,
|
||||
providers: getInjectorProviders(injector),
|
||||
}));
|
||||
const populateResultSet = (dir: DirectiveInstanceType|ComponentInstanceType) => {
|
||||
const resolutionPathWithProviders = !ngDebugDependencyInjectionApiIsSupported()
|
||||
? []
|
||||
: injectors.map((injector) => ({
|
||||
injector,
|
||||
providers: getInjectorProviders(injector),
|
||||
}));
|
||||
const populateResultSet = (dir: DirectiveInstanceType | ComponentInstanceType) => {
|
||||
const {instance, name} = dir;
|
||||
const metadata = getDirectiveMetadata(instance);
|
||||
metadata.dependencies = getDependenciesForDirective(
|
||||
injector,
|
||||
resolutionPathWithProviders,
|
||||
instance.constructor,
|
||||
injector,
|
||||
resolutionPathWithProviders,
|
||||
instance.constructor,
|
||||
);
|
||||
|
||||
if (query.propertyQuery.type === PropertyQueryTypes.All) {
|
||||
@@ -105,9 +136,9 @@ export const getLatestComponentState = (
|
||||
if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
|
||||
directiveProperties[name] = {
|
||||
props: deeplySerializeSelectedProperties(
|
||||
instance,
|
||||
query.propertyQuery.properties[name] || [],
|
||||
),
|
||||
instance,
|
||||
query.propertyQuery.properties[name] || [],
|
||||
),
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
@@ -123,7 +154,7 @@ export const getLatestComponentState = (
|
||||
};
|
||||
};
|
||||
|
||||
function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
function serializeElementInjectorWithId(injector: Injector): SerializedInjector | null {
|
||||
let id: string;
|
||||
const element = getElementInjectorElement(injector);
|
||||
|
||||
@@ -145,7 +176,7 @@ function serializeElementInjectorWithId(injector: Injector): SerializedInjector|
|
||||
return {id, ...serializedInjector};
|
||||
}
|
||||
|
||||
function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
function serializeInjectorWithId(injector: Injector): SerializedInjector | null {
|
||||
if (isElementInjector(injector)) {
|
||||
return serializeElementInjectorWithId(injector);
|
||||
} else {
|
||||
@@ -153,7 +184,7 @@ function serializeInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
}
|
||||
}
|
||||
|
||||
function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector|null {
|
||||
function serializeEnvironmentInjectorWithId(injector: Injector): SerializedInjector | null {
|
||||
let id: string;
|
||||
|
||||
if (!injectorToId.has(injector)) {
|
||||
@@ -223,16 +254,16 @@ export function getInjectorProviders(injector: Injector) {
|
||||
}
|
||||
|
||||
const getDependenciesForDirective = (
|
||||
injector: Injector,
|
||||
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
|
||||
directive: any,
|
||||
): SerializedInjectedService[] => {
|
||||
injector: Injector,
|
||||
resolutionPath: {injector: Injector; providers: ProviderRecord[]}[],
|
||||
directive: any,
|
||||
): SerializedInjectedService[] => {
|
||||
if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let dependencies =
|
||||
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
|
||||
ngDebugClient().ɵgetDependenciesFromInjectable(injector, directive)?.dependencies ?? [];
|
||||
const serializedInjectedServices: SerializedInjectedService[] = [];
|
||||
|
||||
let position = 0;
|
||||
@@ -255,8 +286,9 @@ const getDependenciesForDirective = (
|
||||
// dependency (2)
|
||||
const dependencyResolutionPath: SerializedInjector[] = [
|
||||
// (1)
|
||||
...resolutionPath.slice(0, foundInjectorIndex + 1)
|
||||
.map((node) => serializeInjectorWithId(node.injector)!),
|
||||
...resolutionPath
|
||||
.slice(0, foundInjectorIndex + 1)
|
||||
.map((node) => serializeInjectorWithId(node.injector)!),
|
||||
|
||||
// (2)
|
||||
// We slice the import path to remove the first element because this is the same
|
||||
@@ -313,7 +345,7 @@ function stripUnderscore(str: string): string {
|
||||
return str;
|
||||
}
|
||||
|
||||
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'>|null {
|
||||
export function serializeInjector(injector: Injector): Omit<SerializedInjector, 'id'> | null {
|
||||
const metadata = getInjectorMetadata(injector);
|
||||
|
||||
if (metadata === null) {
|
||||
@@ -353,11 +385,11 @@ export function serializeInjector(injector: Injector): Omit<SerializedInjector,
|
||||
}
|
||||
|
||||
export function serializeProviderRecord(
|
||||
providerRecord: ProviderRecord,
|
||||
index: number,
|
||||
hasImportPath = false,
|
||||
): SerializedProviderRecord {
|
||||
let type: 'type'|'class'|'value'|'factory'|'existing' = 'type';
|
||||
providerRecord: ProviderRecord,
|
||||
index: number,
|
||||
hasImportPath = false,
|
||||
): SerializedProviderRecord {
|
||||
let type: 'type' | 'class' | 'value' | 'factory' | 'existing' = 'type';
|
||||
let multi = false;
|
||||
|
||||
if (typeof providerRecord.provider === 'object') {
|
||||
@@ -377,7 +409,11 @@ export function serializeProviderRecord(
|
||||
}
|
||||
|
||||
const serializedProvider: {
|
||||
token: string; type: typeof type; multi: boolean; isViewProvider: boolean; index: number;
|
||||
token: string;
|
||||
type: typeof type;
|
||||
multi: boolean;
|
||||
isViewProvider: boolean;
|
||||
index: number;
|
||||
importPath?: string[];
|
||||
} = {
|
||||
token: valueToLabel(providerRecord.token),
|
||||
@@ -388,10 +424,9 @@ export function serializeProviderRecord(
|
||||
};
|
||||
|
||||
if (hasImportPath) {
|
||||
serializedProvider['importPath'] = (providerRecord.importPath ?? [])
|
||||
.map(
|
||||
(injector) => valueToLabel(injector),
|
||||
);
|
||||
serializedProvider['importPath'] = (providerRecord.importPath ?? []).map((injector) =>
|
||||
valueToLabel(injector),
|
||||
);
|
||||
}
|
||||
|
||||
return serializedProvider;
|
||||
@@ -400,8 +435,8 @@ export function serializeProviderRecord(
|
||||
function elementToDirectiveNames(element: HTMLElement): string[] {
|
||||
const {component, directives} = getDirectivesFromElement(element);
|
||||
return [component, ...directives]
|
||||
.map((dir) => dir?.constructor?.name ?? '')
|
||||
.filter((dir) => !!dir);
|
||||
.map((dir) => dir?.constructor?.name ?? '')
|
||||
.filter((dir) => !!dir);
|
||||
}
|
||||
|
||||
export function getElementInjectorElement(elementInjector: Injector): HTMLElement {
|
||||
@@ -412,7 +447,7 @@ export function getElementInjectorElement(elementInjector: Injector): HTMLElemen
|
||||
return getInjectorMetadata(elementInjector)!.source as HTMLElement;
|
||||
}
|
||||
|
||||
function isInjectionToken(token: Type<unknown>|InjectionToken<unknown>): boolean {
|
||||
function isInjectionToken(token: Type<unknown> | InjectionToken<unknown>): boolean {
|
||||
return token.constructor.name === 'InjectionToken';
|
||||
}
|
||||
|
||||
@@ -446,7 +481,7 @@ const getRoots = () => {
|
||||
const roots = Array.from(document.documentElement.querySelectorAll('[ng-version]'));
|
||||
|
||||
const isTopLevel = (element: Element) => {
|
||||
let parent: Element|null = element;
|
||||
let parent: Element | null = element;
|
||||
|
||||
while (parent?.parentElement) {
|
||||
parent = parent.parentElement;
|
||||
@@ -469,13 +504,13 @@ export const buildDirectiveForest = (): ComponentTreeNode[] => {
|
||||
// Based on an ElementID we return a specific component node.
|
||||
// If we can't find any, we return null.
|
||||
export const queryDirectiveForest = (
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): ComponentTreeNode|null => {
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): ComponentTreeNode | null => {
|
||||
if (!position.length) {
|
||||
return null;
|
||||
}
|
||||
let node: null|ComponentTreeNode = null;
|
||||
let node: null | ComponentTreeNode = null;
|
||||
for (const i of position) {
|
||||
node = forest[i];
|
||||
if (!node) {
|
||||
@@ -487,16 +522,16 @@ export const queryDirectiveForest = (
|
||||
};
|
||||
|
||||
export const findNodeInForest = (
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): HTMLElement|null => {
|
||||
const foundComponent: ComponentTreeNode|null = queryDirectiveForest(position, forest);
|
||||
position: ElementPosition,
|
||||
forest: ComponentTreeNode[],
|
||||
): HTMLElement | null => {
|
||||
const foundComponent: ComponentTreeNode | null = queryDirectiveForest(position, forest);
|
||||
return foundComponent ? (foundComponent.nativeElement as HTMLElement) : null;
|
||||
};
|
||||
|
||||
export const findNodeFromSerializedPosition = (
|
||||
serializedPosition: string,
|
||||
): ComponentTreeNode|null => {
|
||||
serializedPosition: string,
|
||||
): ComponentTreeNode | null => {
|
||||
const position: number[] = serializedPosition.split(',').map((index) => parseInt(index, 10));
|
||||
return queryDirectiveForest(position, buildDirectiveForest());
|
||||
};
|
||||
@@ -506,9 +541,9 @@ export const updateState = (updatedStateData: UpdatedStateData): void => {
|
||||
const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest());
|
||||
if (!node) {
|
||||
console.warn(
|
||||
'Could not update the state of component',
|
||||
updatedStateData,
|
||||
'because the component was not found',
|
||||
'Could not update the state of component',
|
||||
updatedStateData,
|
||||
'because the component was not found',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -537,19 +572,27 @@ const mutateComponentOrDirective = (updatedStateData: UpdatedStateData, compOrDi
|
||||
parentObjectOfValueToUpdate = parentObjectOfValueToUpdate[key];
|
||||
});
|
||||
|
||||
if (isSignal(parentObjectOfValueToUpdate)) {
|
||||
// we don't support updating nested objects in signals yet
|
||||
return;
|
||||
}
|
||||
|
||||
// When we try to set a property which only has a getter
|
||||
// the line below could throw an error.
|
||||
try {
|
||||
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
||||
} catch {
|
||||
}
|
||||
if (isSignal(parentObjectOfValueToUpdate[valueKey])) {
|
||||
parentObjectOfValueToUpdate[valueKey].set(updatedStateData.newValue);
|
||||
} else {
|
||||
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export function serializeResolutionPath(resolutionPath: Injector[]): SerializedInjector[] {
|
||||
const serializedResolutionPath: SerializedInjector[] = [];
|
||||
|
||||
for (const injector of resolutionPath) {
|
||||
let serializedInjectorWithId: SerializedInjector|null = null;
|
||||
let serializedInjectorWithId: SerializedInjector | null = null;
|
||||
|
||||
if (isElementInjector(injector)) {
|
||||
serializedInjectorWithId = serializeElementInjectorWithId(injector);
|
||||
|
||||
@@ -9,12 +9,16 @@
|
||||
import {LTreeStrategy} from './ltree';
|
||||
import {RTreeStrategy} from './render-tree';
|
||||
|
||||
export {getDirectiveHostElement, getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME} from './ltree';
|
||||
export {
|
||||
getDirectiveHostElement,
|
||||
getLViewFromDirectiveOrElementInstance,
|
||||
METADATA_PROPERTY_NAME,
|
||||
} from './ltree';
|
||||
|
||||
// The order of the strategies matters. Lower indices have higher priority.
|
||||
const strategies = [new RTreeStrategy(), new LTreeStrategy()];
|
||||
|
||||
let strategy: null|RTreeStrategy|LTreeStrategy = null;
|
||||
let strategy: null | RTreeStrategy | LTreeStrategy = null;
|
||||
|
||||
const selectStrategy = (element: Element) => {
|
||||
for (const s of strategies) {
|
||||
|
||||
@@ -28,20 +28,19 @@ const TYPE = 1;
|
||||
const ELEMENT = 0;
|
||||
const LVIEW_TVIEW = 1;
|
||||
|
||||
|
||||
// Big oversimplification of the LView structure.
|
||||
type LView = Array<any>;
|
||||
|
||||
export const isLContainer = (value: any): boolean => {
|
||||
export const isLContainer = (value: unknown): boolean => {
|
||||
return Array.isArray(value) && value[TYPE] === true;
|
||||
};
|
||||
|
||||
const isLView = (value: any): boolean => {
|
||||
const isLView = (value: unknown): value is LView => {
|
||||
return Array.isArray(value) && typeof value[TYPE] === 'object';
|
||||
};
|
||||
|
||||
export const METADATA_PROPERTY_NAME = '__ngContext__';
|
||||
export function getLViewFromDirectiveOrElementInstance(dir: any): null|LView {
|
||||
export function getLViewFromDirectiveOrElementInstance(dir: any): null | LView {
|
||||
if (!dir) {
|
||||
return null;
|
||||
}
|
||||
@@ -78,9 +77,9 @@ export class LTreeStrategy {
|
||||
return typeof (element as any).__ngContext__ !== 'undefined';
|
||||
}
|
||||
|
||||
private _getNode(lView: any, data: any, idx: number): ComponentTreeNode {
|
||||
private _getNode(lView: LView, data: any, idx: number): ComponentTreeNode {
|
||||
const directives: DirectiveInstanceType[] = [];
|
||||
let component: ComponentInstanceType|null = null;
|
||||
let component: ComponentInstanceType | null = null;
|
||||
const tNode = data[idx];
|
||||
const node = lView[idx][ELEMENT];
|
||||
const element = (node.tagName || node.nodeName).toLowerCase();
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('render tree extraction', () => {
|
||||
componentMap = new Map();
|
||||
|
||||
(window as any).ng = {
|
||||
getDirectiveMetadata(): void{},
|
||||
getDirectiveMetadata(): void {},
|
||||
getComponent(element: Element): any {
|
||||
return componentMap.get(element);
|
||||
},
|
||||
@@ -32,12 +32,12 @@ describe('render tree extraction', () => {
|
||||
afterEach(() => delete (window as any).ng);
|
||||
|
||||
it('should detect Angular Ivy apps', () => {
|
||||
expect(treeStrategy.supports({})).toBeTrue();
|
||||
expect(treeStrategy.supports()).toBeTrue();
|
||||
});
|
||||
|
||||
it('should fail with detection of non-Ivy apps', () => {
|
||||
delete (window as any).ng.getDirectiveMetadata;
|
||||
expect(treeStrategy.supports({})).toBeFalse();
|
||||
expect(treeStrategy.supports()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should extract render tree from an empty element', () => {
|
||||
|
||||
@@ -10,56 +10,60 @@ import {ComponentTreeNode} from '../interfaces';
|
||||
import {ngDebugClient} from '../ng-debug-api/ng-debug-api';
|
||||
import {isCustomElement} from '../utils';
|
||||
|
||||
const extractViewTree =
|
||||
(domNode: Node|Element, result: ComponentTreeNode[],
|
||||
getComponent: (element: Element) => {} | null,
|
||||
getDirectives: (node: Node) => {}[]): ComponentTreeNode[] => {
|
||||
const directives = getDirectives(domNode);
|
||||
if (!directives.length && !(domNode instanceof Element)) {
|
||||
return result;
|
||||
}
|
||||
const componentTreeNode: ComponentTreeNode = {
|
||||
children: [],
|
||||
component: null,
|
||||
directives: directives.map((dir) => {
|
||||
return {
|
||||
instance: dir,
|
||||
name: dir.constructor.name,
|
||||
};
|
||||
}),
|
||||
element: domNode.nodeName.toLowerCase(),
|
||||
nativeElement: domNode,
|
||||
const extractViewTree = (
|
||||
domNode: Node | Element,
|
||||
result: ComponentTreeNode[],
|
||||
getComponent: (element: Element) => {} | null,
|
||||
getDirectives: (node: Node) => {}[],
|
||||
): ComponentTreeNode[] => {
|
||||
const directives = getDirectives(domNode);
|
||||
if (!directives.length && !(domNode instanceof Element)) {
|
||||
return result;
|
||||
}
|
||||
const componentTreeNode: ComponentTreeNode = {
|
||||
children: [],
|
||||
component: null,
|
||||
directives: directives.map((dir) => {
|
||||
return {
|
||||
instance: dir,
|
||||
name: dir.constructor.name,
|
||||
};
|
||||
if (!(domNode instanceof Element)) {
|
||||
result.push(componentTreeNode);
|
||||
return result;
|
||||
}
|
||||
const component = getComponent(domNode);
|
||||
if (component) {
|
||||
componentTreeNode.component = {
|
||||
instance: component,
|
||||
isElement: isCustomElement(domNode),
|
||||
name: domNode.nodeName.toLowerCase(),
|
||||
};
|
||||
}
|
||||
if (component || componentTreeNode.directives.length) {
|
||||
result.push(componentTreeNode);
|
||||
}
|
||||
if (componentTreeNode.component || componentTreeNode.directives.length) {
|
||||
domNode.childNodes.forEach(
|
||||
(node) =>
|
||||
extractViewTree(node, componentTreeNode.children, getComponent, getDirectives));
|
||||
} else {
|
||||
domNode.childNodes.forEach(
|
||||
(node) => extractViewTree(node, result, getComponent, getDirectives));
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
element: domNode.nodeName.toLowerCase(),
|
||||
nativeElement: domNode,
|
||||
};
|
||||
if (!(domNode instanceof Element)) {
|
||||
result.push(componentTreeNode);
|
||||
return result;
|
||||
}
|
||||
const component = getComponent(domNode);
|
||||
if (component) {
|
||||
componentTreeNode.component = {
|
||||
instance: component,
|
||||
isElement: isCustomElement(domNode),
|
||||
name: domNode.nodeName.toLowerCase(),
|
||||
};
|
||||
}
|
||||
if (component || componentTreeNode.directives.length) {
|
||||
result.push(componentTreeNode);
|
||||
}
|
||||
if (componentTreeNode.component || componentTreeNode.directives.length) {
|
||||
domNode.childNodes.forEach((node) =>
|
||||
extractViewTree(node, componentTreeNode.children, getComponent, getDirectives),
|
||||
);
|
||||
} else {
|
||||
domNode.childNodes.forEach((node) =>
|
||||
extractViewTree(node, result, getComponent, getDirectives),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export class RTreeStrategy {
|
||||
supports(_: any): boolean {
|
||||
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const)
|
||||
.every((method) => typeof ngDebugClient()[method] === 'function');
|
||||
supports(): boolean {
|
||||
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const).every(
|
||||
(method) => typeof ngDebugClient()[method] === 'function',
|
||||
);
|
||||
}
|
||||
|
||||
build(element: Element): ComponentTreeNode[] {
|
||||
|
||||
@@ -12,7 +12,7 @@ let overlayContent: HTMLElement;
|
||||
declare const ng: any;
|
||||
|
||||
interface Type<T> extends Function {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
const DEV_TOOLS_HIGHLIGHT_NODE_ID = '____ngDevToolsHighlight';
|
||||
@@ -40,26 +40,27 @@ function init(): void {
|
||||
overlay.appendChild(overlayContent);
|
||||
}
|
||||
|
||||
export const findComponentAndHost =
|
||||
(el: Node|undefined): {component: any; host: HTMLElement | null} => {
|
||||
if (!el) {
|
||||
return {component: null, host: null};
|
||||
}
|
||||
while (el) {
|
||||
const component = el instanceof HTMLElement && ng.getComponent(el);
|
||||
if (component) {
|
||||
return {component, host: el as HTMLElement};
|
||||
}
|
||||
if (!el.parentElement) {
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return {component: null, host: null};
|
||||
};
|
||||
export const findComponentAndHost = (
|
||||
el: Node | undefined,
|
||||
): {component: any; host: HTMLElement | null} => {
|
||||
if (!el) {
|
||||
return {component: null, host: null};
|
||||
}
|
||||
while (el) {
|
||||
const component = el instanceof HTMLElement && ng.getComponent(el);
|
||||
if (component) {
|
||||
return {component, host: el as HTMLElement};
|
||||
}
|
||||
if (!el.parentElement) {
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return {component: null, host: null};
|
||||
};
|
||||
|
||||
// Todo(aleksanderbodurri): this should not be part of the highlighter, move this somewhere else
|
||||
export function getDirectiveName(dir: Type<unknown>|undefined|null): string {
|
||||
export function getDirectiveName(dir: Type<unknown> | undefined | null): string {
|
||||
return dir ? dir.constructor.name : 'unknown';
|
||||
}
|
||||
|
||||
@@ -97,11 +98,12 @@ export function inDoc(node: any): boolean {
|
||||
}
|
||||
const doc = node.ownerDocument.documentElement;
|
||||
const parent = node.parentNode;
|
||||
return doc === node || doc === parent ||
|
||||
!!(parent && parent.nodeType === 1 && doc.contains(parent));
|
||||
return (
|
||||
doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent))
|
||||
);
|
||||
}
|
||||
|
||||
function getComponentRect(el: Node): DOMRect|undefined {
|
||||
function getComponentRect(el: Node): DOMRect | undefined {
|
||||
if (!(el instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
@@ -137,7 +139,7 @@ function positionOverlayContent(dimensions: DOMRect) {
|
||||
|
||||
// Attempt to position the content element so that it's always in the
|
||||
// viewport along the Y axis. Prefer to position on the bottom.
|
||||
if ((dimensions.bottom + yOffset) <= viewportHeight) {
|
||||
if (dimensions.bottom + yOffset <= viewportHeight) {
|
||||
style.bottom = yOffsetValue;
|
||||
// If it doesn't fit on the bottom, try to position on top.
|
||||
} else if (dimensions.top - yOffset >= 0) {
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DirectiveProfile, ElementPosition, ElementProfile, LifecycleProfile, ProfilerFrame} from 'protocol';
|
||||
import {
|
||||
DirectiveProfile,
|
||||
ElementPosition,
|
||||
ElementProfile,
|
||||
LifecycleProfile,
|
||||
ProfilerFrame,
|
||||
} from 'protocol';
|
||||
|
||||
import {getDirectiveName} from '../highlighter';
|
||||
import {ComponentTreeNode} from '../interfaces';
|
||||
@@ -59,16 +65,20 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
||||
// We flush here because it's possible the current node to overwrite
|
||||
// an existing removed node.
|
||||
onCreate(
|
||||
directive: any, node: Node, _: number, isComponent: boolean, position: ElementPosition):
|
||||
void {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
},
|
||||
directive: any,
|
||||
node: Node,
|
||||
_: number,
|
||||
isComponent: boolean,
|
||||
position: ElementPosition,
|
||||
): void {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
},
|
||||
onChangeDetectionStart(component: any, node: Node): void {
|
||||
startEvent(timeStartMap, component, 'changeDetection');
|
||||
if (!inChangeDetection) {
|
||||
@@ -111,22 +121,31 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
||||
console.warn('Could not find profile for', component);
|
||||
}
|
||||
},
|
||||
onDestroy(directive: any, node: Node, _: number, isComponent: boolean, __: ElementPosition):
|
||||
void {
|
||||
// Make sure we reflect such directives in the report.
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isComponent && isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onDestroy(
|
||||
directive: any,
|
||||
node: Node,
|
||||
_: number,
|
||||
isComponent: boolean,
|
||||
__: ElementPosition,
|
||||
): void {
|
||||
// Make sure we reflect such directives in the report.
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isComponent && isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onLifecycleHookStart(
|
||||
directive: any, hookName: keyof LifecycleProfile, node: Node, __: number,
|
||||
isComponent: boolean): void {
|
||||
directive: any,
|
||||
hookName: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
__: number,
|
||||
isComponent: boolean,
|
||||
): void {
|
||||
startEvent(timeStartMap, directive, hookName);
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
@@ -139,7 +158,12 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
||||
}
|
||||
},
|
||||
onLifecycleHookEnd(
|
||||
directive: any, hookName: keyof LifecycleProfile, _: Node, __: number, ___: boolean): void {
|
||||
directive: any,
|
||||
hookName: keyof LifecycleProfile,
|
||||
_: Node,
|
||||
__: number,
|
||||
___: boolean,
|
||||
): void {
|
||||
const dir = eventMap.get(directive);
|
||||
const startTimestamp = getEventStart(timeStartMap, directive, hookName);
|
||||
if (startTimestamp === undefined) {
|
||||
@@ -153,19 +177,23 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
||||
dir.lifecycle[hookName] = (dir.lifecycle[hookName] || 0) + duration;
|
||||
frameDuration += duration;
|
||||
},
|
||||
onOutputStart(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean):
|
||||
void {
|
||||
startEvent(timeStartMap, componentOrDirective, outputName);
|
||||
if (!eventMap.has(componentOrDirective)) {
|
||||
eventMap.set(componentOrDirective, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(componentOrDirective),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onOutputStart(
|
||||
componentOrDirective: any,
|
||||
outputName: string,
|
||||
node: Node,
|
||||
isComponent: boolean,
|
||||
): void {
|
||||
startEvent(timeStartMap, componentOrDirective, outputName);
|
||||
if (!eventMap.has(componentOrDirective)) {
|
||||
eventMap.set(componentOrDirective, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(componentOrDirective),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
outputs: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onOutputEnd(componentOrDirective: any, outputName: string): void {
|
||||
const name = outputName;
|
||||
const entry = eventMap.get(componentOrDirective);
|
||||
@@ -175,8 +203,10 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
||||
}
|
||||
if (!entry) {
|
||||
console.warn(
|
||||
'Could not find directive or component in onOutputEnd callback', componentOrDirective,
|
||||
outputName);
|
||||
'Could not find directive or component in onOutputEnd callback',
|
||||
componentOrDirective,
|
||||
outputName,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const duration = performance.now() - startTimestamp;
|
||||
@@ -215,33 +245,36 @@ const insertOrMerge = (lastFrame: ElementProfile, profile: DirectiveProfile) =>
|
||||
}
|
||||
};
|
||||
|
||||
const insertElementProfile =
|
||||
(frames: ElementProfile[], position: ElementPosition, profile?: DirectiveProfile) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const original = frames;
|
||||
for (let i = 0; i < position.length - 1; i++) {
|
||||
const pos = position[i];
|
||||
if (!frames[pos]) {
|
||||
// TODO(mgechev): consider how to ensure we don't hit this case
|
||||
console.warn('Unable to find parent node for', profile, original);
|
||||
return;
|
||||
}
|
||||
frames = frames[pos].children;
|
||||
}
|
||||
const lastIdx = position[position.length - 1];
|
||||
let lastFrame: ElementProfile = {
|
||||
children: [],
|
||||
directives: [],
|
||||
};
|
||||
if (frames[lastIdx]) {
|
||||
lastFrame = frames[lastIdx];
|
||||
} else {
|
||||
frames[lastIdx] = lastFrame;
|
||||
}
|
||||
insertOrMerge(lastFrame, profile);
|
||||
};
|
||||
const insertElementProfile = (
|
||||
frames: ElementProfile[],
|
||||
position: ElementPosition,
|
||||
profile?: DirectiveProfile,
|
||||
) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const original = frames;
|
||||
for (let i = 0; i < position.length - 1; i++) {
|
||||
const pos = position[i];
|
||||
if (!frames[pos]) {
|
||||
// TODO(mgechev): consider how to ensure we don't hit this case
|
||||
console.warn('Unable to find parent node for', profile, original);
|
||||
return;
|
||||
}
|
||||
frames = frames[pos].children;
|
||||
}
|
||||
const lastIdx = position[position.length - 1];
|
||||
let lastFrame: ElementProfile = {
|
||||
children: [],
|
||||
directives: [],
|
||||
};
|
||||
if (frames[lastIdx]) {
|
||||
lastFrame = frames[lastIdx];
|
||||
} else {
|
||||
frames[lastIdx] = lastFrame;
|
||||
}
|
||||
insertOrMerge(lastFrame, profile);
|
||||
};
|
||||
|
||||
const prepareInitialFrame = (source: string, duration: number) => {
|
||||
const frame: ProfilerFrame = {
|
||||
@@ -252,7 +285,7 @@ const prepareInitialFrame = (source: string, duration: number) => {
|
||||
const directiveForestHooks = initializeOrGetDirectiveForestHooks();
|
||||
const directiveForest = directiveForestHooks.getIndexedDirectiveForest();
|
||||
const traverse = (node: ComponentTreeNode, children = frame.directives) => {
|
||||
let position: ElementPosition|undefined;
|
||||
let position: ElementPosition | undefined;
|
||||
if (node.component) {
|
||||
position = directiveForestHooks.getDirectivePosition(node.component.instance);
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ export class DirectiveForestHooks {
|
||||
|
||||
profiler: Profiler = selectProfilerStrategy();
|
||||
|
||||
getDirectivePosition(dir: any): ElementPosition|undefined {
|
||||
getDirectivePosition(dir: any): ElementPosition | undefined {
|
||||
const result = this._tracker.getDirectivePosition(dir);
|
||||
if (result === undefined) {
|
||||
console.warn('Unable to find position of', dir);
|
||||
@@ -36,7 +36,7 @@ export class DirectiveForestHooks {
|
||||
return result;
|
||||
}
|
||||
|
||||
getDirectiveId(dir: any): number|undefined {
|
||||
getDirectiveId(dir: any): number | undefined {
|
||||
const result = this._tracker.getDirectiveId(dir);
|
||||
if (result === undefined) {
|
||||
console.warn('Unable to find ID of', result);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {buildDirectiveForest} from '../component-tree';
|
||||
import {ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType} from '../interfaces';
|
||||
|
||||
export declare interface Type<T> extends Function {
|
||||
new(...args: any[]): T;
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
@@ -22,7 +22,8 @@ interface TreeNode {
|
||||
}
|
||||
|
||||
export type NodeArray = {
|
||||
directive: any; isComponent: boolean;
|
||||
directive: any;
|
||||
isComponent: boolean;
|
||||
}[];
|
||||
|
||||
export class IdentityTracker {
|
||||
@@ -43,11 +44,11 @@ export class IdentityTracker {
|
||||
return IdentityTracker._instance;
|
||||
}
|
||||
|
||||
getDirectivePosition(dir: any): ElementPosition|undefined {
|
||||
getDirectivePosition(dir: any): ElementPosition | undefined {
|
||||
return this._currentDirectivePosition.get(dir);
|
||||
}
|
||||
|
||||
getDirectiveId(dir: any): number|undefined {
|
||||
getDirectiveId(dir: any): number | undefined {
|
||||
return this._currentDirectiveId.get(dir);
|
||||
}
|
||||
|
||||
@@ -56,7 +57,9 @@ export class IdentityTracker {
|
||||
}
|
||||
|
||||
index(): {
|
||||
newNodes: NodeArray; removedNodes: NodeArray; indexedForest: IndexedNode[];
|
||||
newNodes: NodeArray;
|
||||
removedNodes: NodeArray;
|
||||
indexedForest: IndexedNode[];
|
||||
directiveForest: ComponentTreeNode[];
|
||||
} {
|
||||
const directiveForest = buildDirectiveForest();
|
||||
@@ -78,8 +81,11 @@ export class IdentityTracker {
|
||||
}
|
||||
|
||||
private _index(
|
||||
node: IndexedNode, parent: TreeNode|null, newNodes: {directive: any; isComponent: boolean}[],
|
||||
allNodes: Set<any>): void {
|
||||
node: IndexedNode,
|
||||
parent: TreeNode | null,
|
||||
newNodes: {directive: any; isComponent: boolean}[],
|
||||
allNodes: Set<any>,
|
||||
): void {
|
||||
if (node.component) {
|
||||
allNodes.add(node.component.instance);
|
||||
this.isComponent.set(node.component.instance, true);
|
||||
@@ -113,7 +119,10 @@ export interface IndexedNode extends DevToolsNode<DirectiveInstanceType, Compone
|
||||
}
|
||||
|
||||
const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
|
||||
node: T, idx: number, parentPosition: number[] = []): IndexedNode => {
|
||||
node: T,
|
||||
idx: number,
|
||||
parentPosition: number[] = [],
|
||||
): IndexedNode => {
|
||||
const position = parentPosition.concat([idx]);
|
||||
return {
|
||||
position,
|
||||
@@ -126,4 +135,5 @@ const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstan
|
||||
};
|
||||
|
||||
export const indexForest = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
|
||||
forest: T[]): IndexedNode[] => forest.map((n, i) => indexTree(n, i));
|
||||
forest: T[],
|
||||
): IndexedNode[] => forest.map((n, i) => indexTree(n, i));
|
||||
|
||||
@@ -15,9 +15,9 @@ import {DirectiveForestHooks} from './hooks';
|
||||
const markName = (s: string, method: Method) => `🅰️ ${s}#${method}`;
|
||||
|
||||
const supportsPerformance =
|
||||
globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
|
||||
globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
|
||||
|
||||
type Method = keyof LifecycleProfile|'changeDetection'|string;
|
||||
type Method = keyof LifecycleProfile | 'changeDetection' | string;
|
||||
|
||||
const recordMark = (s: string, method: Method) => {
|
||||
if (supportsPerformance) {
|
||||
|
||||
@@ -15,32 +15,37 @@ import {IdentityTracker, NodeArray} from '../identity-tracker';
|
||||
|
||||
import {getLifeCycleName, Hooks, Profiler} from './shared';
|
||||
|
||||
type ProfilerCallback = (event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) =>
|
||||
void;
|
||||
type ProfilerCallback = (
|
||||
event: ɵProfilerEvent,
|
||||
instanceOrLView: {} | null,
|
||||
hookOrListener: any,
|
||||
) => void;
|
||||
|
||||
/** Implementation of Profiler that utilizes framework APIs fire profiler hooks. */
|
||||
export class NgProfiler extends Profiler {
|
||||
private _tracker = IdentityTracker.getInstance();
|
||||
private _callbacks: ProfilerCallback[] = [];
|
||||
private _lastDirectiveInstance: {}|null = null;
|
||||
private _lastDirectiveInstance: {} | null = null;
|
||||
|
||||
constructor(config: Partial<Hooks> = {}) {
|
||||
super(config);
|
||||
this._setProfilerCallback(
|
||||
(event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) => {
|
||||
if (this[event] === undefined) {
|
||||
return;
|
||||
}
|
||||
(event: ɵProfilerEvent, instanceOrLView: {} | null, hookOrListener: any) => {
|
||||
if (this[event] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this[event](instanceOrLView, hookOrListener);
|
||||
});
|
||||
this[event](instanceOrLView, hookOrListener);
|
||||
},
|
||||
);
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
ngDebugClient().ɵsetProfiler(
|
||||
(event: ɵProfilerEvent, instanceOrLView: {}|null, hookOrListener: any) =>
|
||||
this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener)));
|
||||
(event: ɵProfilerEvent, instanceOrLView: {} | null, hookOrListener: any) =>
|
||||
this._callbacks.forEach((cb) => cb(event, instanceOrLView, hookOrListener)),
|
||||
);
|
||||
}
|
||||
|
||||
private _setProfilerCallback(callback: ProfilerCallback): void {
|
||||
@@ -106,9 +111,11 @@ export class NgProfiler extends Profiler {
|
||||
}
|
||||
|
||||
this._onChangeDetectionStart(
|
||||
this._lastDirectiveInstance, getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance));
|
||||
this._lastDirectiveInstance,
|
||||
getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance),
|
||||
);
|
||||
}
|
||||
|
||||
[ɵProfilerEvent.TemplateUpdateEnd](context: any, _hookOrListener: any): void {
|
||||
@@ -121,9 +128,11 @@ export class NgProfiler extends Profiler {
|
||||
}
|
||||
|
||||
this._onChangeDetectionEnd(
|
||||
this._lastDirectiveInstance, getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance));
|
||||
this._lastDirectiveInstance,
|
||||
getDirectiveHostElement(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectiveId(this._lastDirectiveInstance),
|
||||
this._tracker.getDirectivePosition(this._lastDirectiveInstance),
|
||||
);
|
||||
}
|
||||
|
||||
[ɵProfilerEvent.LifecycleHookStart](directive: any, hook: any): void {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getDirectiveHostElement, getLViewFromDirectiveOrElementInstance, METADATA_PROPERTY_NAME,} from '../../directive-forest';
|
||||
import {
|
||||
getDirectiveHostElement,
|
||||
getLViewFromDirectiveOrElementInstance,
|
||||
METADATA_PROPERTY_NAME,
|
||||
} from '../../directive-forest';
|
||||
import {runOutsideAngular} from '../../utils';
|
||||
import {IdentityTracker, NodeArray} from '../identity-tracker';
|
||||
|
||||
@@ -82,7 +86,7 @@ export class PatchingProfiler extends Profiler {
|
||||
if (original.patched) {
|
||||
return;
|
||||
}
|
||||
declarations.tView.template = function(_: any, component: any): void {
|
||||
declarations.tView.template = function (_: any, component: any): void {
|
||||
if (!self._inChangeDetection) {
|
||||
self._inChangeDetection = true;
|
||||
runOutsideAngular(() => {
|
||||
@@ -122,7 +126,7 @@ export class PatchingProfiler extends Profiler {
|
||||
}
|
||||
if (typeof el === 'function') {
|
||||
const self = this;
|
||||
current[idx] = function(): any {
|
||||
current[idx] = function (): any {
|
||||
// We currently don't want to notify the consumer
|
||||
// for execution of lifecycle hooks of services and pipes.
|
||||
// These two abstractions don't have `__ngContext__`, and
|
||||
|
||||
@@ -11,32 +11,64 @@ import {Subject} from 'rxjs';
|
||||
|
||||
import {NodeArray} from '../identity-tracker';
|
||||
|
||||
type CreationHook =
|
||||
(componentOrDirective: any, node: Node, id: number, isComponent: boolean,
|
||||
position: ElementPosition) => void;
|
||||
type CreationHook = (
|
||||
componentOrDirective: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type LifecycleStartHook =
|
||||
(componentOrDirective: any, hook: keyof LifecycleProfile, node: Node, id: number,
|
||||
isComponent: boolean) => void;
|
||||
type LifecycleStartHook = (
|
||||
componentOrDirective: any,
|
||||
hook: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
|
||||
type LifecycleEndHook =
|
||||
(componentOrDirective: any, hook: keyof LifecycleProfile, node: Node, id: number,
|
||||
isComponent: boolean) => void;
|
||||
type LifecycleEndHook = (
|
||||
componentOrDirective: any,
|
||||
hook: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
|
||||
type ChangeDetectionStartHook =
|
||||
(component: any, node: Node, id: number, position: ElementPosition) => void;
|
||||
type ChangeDetectionStartHook = (
|
||||
component: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type ChangeDetectionEndHook = (component: any, node: Node, id: number, position: ElementPosition) =>
|
||||
void;
|
||||
type ChangeDetectionEndHook = (
|
||||
component: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type DestroyHook =
|
||||
(componentOrDirective: any, node: Node, id: number, isComponent: boolean,
|
||||
position: ElementPosition) => void;
|
||||
type DestroyHook = (
|
||||
componentOrDirective: any,
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
position: ElementPosition,
|
||||
) => void;
|
||||
|
||||
type OutputStartHook =
|
||||
(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean) => void;
|
||||
type OutputEndHook =
|
||||
(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean) => void;
|
||||
type OutputStartHook = (
|
||||
componentOrDirective: any,
|
||||
outputName: string,
|
||||
node: Node,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
type OutputEndHook = (
|
||||
componentOrDirective: any,
|
||||
outputName: string,
|
||||
node: Node,
|
||||
isComponent: boolean,
|
||||
) => void;
|
||||
|
||||
export interface Hooks {
|
||||
onCreate: CreationHook;
|
||||
@@ -79,8 +111,12 @@ export abstract class Profiler {
|
||||
|
||||
/** @internal */
|
||||
protected _onCreate(
|
||||
_: any, __: Node, id: number|undefined, ___: boolean,
|
||||
position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
___: boolean,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -89,8 +125,12 @@ export abstract class Profiler {
|
||||
|
||||
/** @internal */
|
||||
protected _onDestroy(
|
||||
_: any, __: Node, id: number|undefined, ___: boolean,
|
||||
position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
___: boolean,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -99,7 +139,11 @@ export abstract class Profiler {
|
||||
|
||||
/** @internal */
|
||||
protected _onChangeDetectionStart(
|
||||
_: any, __: Node, id: number|undefined, position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +152,11 @@ export abstract class Profiler {
|
||||
|
||||
/** @internal */
|
||||
protected _onChangeDetectionEnd(
|
||||
_: any, __: Node, id: number|undefined, position: ElementPosition|undefined): void {
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
position: ElementPosition | undefined,
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -117,8 +165,12 @@ export abstract class Profiler {
|
||||
|
||||
/** @internal */
|
||||
protected _onLifecycleHookStart(
|
||||
_: any, __: keyof LifecycleProfile|'unknown', ___: Node, id: number|undefined,
|
||||
____: boolean): void {
|
||||
_: any,
|
||||
__: keyof LifecycleProfile | 'unknown',
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -127,8 +179,12 @@ export abstract class Profiler {
|
||||
|
||||
/** @internal */
|
||||
protected _onLifecycleHookEnd(
|
||||
_: any, __: keyof LifecycleProfile|'unknown', ___: Node, id: number|undefined,
|
||||
____: boolean): void {
|
||||
_: any,
|
||||
__: keyof LifecycleProfile | 'unknown',
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -136,8 +192,13 @@ export abstract class Profiler {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
protected _onOutputStart(_: any, __: string, ___: Node, id: number|undefined, ____: boolean):
|
||||
void {
|
||||
protected _onOutputStart(
|
||||
_: any,
|
||||
__: string,
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -145,7 +206,13 @@ export abstract class Profiler {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
protected _onOutputEnd(_: any, __: string, ___: Node, id: number|undefined, ____: boolean): void {
|
||||
protected _onOutputEnd(
|
||||
_: any,
|
||||
__: string,
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean,
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -176,7 +243,7 @@ const hookNames = [
|
||||
|
||||
const hookMethodNames = new Set(hookNames.map((hook) => `ng${hook}`));
|
||||
|
||||
export const getLifeCycleName = (obj: {}, fn: any): keyof LifecycleProfile|'unknown' => {
|
||||
export const getLifeCycleName = (obj: {}, fn: any): keyof LifecycleProfile | 'unknown' => {
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
const keys = Object.getOwnPropertyNames(proto);
|
||||
for (const propName of keys) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface ComponentInstanceType {
|
||||
isElement: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentTreeNode extends
|
||||
DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
|
||||
export interface ComponentTreeNode
|
||||
extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
|
||||
children: ComponentTreeNode[];
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function parseRoutes(router: Router): Route {
|
||||
return root;
|
||||
}
|
||||
|
||||
function assignChildrenToParent(parentPath: string|null, children: Routes): Route[] {
|
||||
function assignChildrenToParent(parentPath: string | null, children: Routes): Route[] {
|
||||
return children.map((child: AngularRoute) => {
|
||||
const childName = childRouteName(child);
|
||||
const childDescendents: [any] = (child as any)._loadedConfig?.routes || child.children;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {arrayEquals} from 'shared-utils';
|
||||
import {ComponentTreeNode} from './interfaces';
|
||||
|
||||
interface ConsoleReferenceNode {
|
||||
node: ComponentTreeNode|null;
|
||||
node: ComponentTreeNode | null;
|
||||
position: ElementPosition;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ const _setConsoleReference = (referenceNode: ConsoleReferenceNode) => {
|
||||
};
|
||||
|
||||
const prepareCurrentReferencesForInsertion = (referenceNode: ConsoleReferenceNode) => {
|
||||
const foundIndex = nodesForConsoleReference.findIndex(
|
||||
(nodeToLookFor) => arrayEquals(nodeToLookFor.position, referenceNode.position));
|
||||
const foundIndex = nodesForConsoleReference.findIndex((nodeToLookFor) =>
|
||||
arrayEquals(nodeToLookFor.position, referenceNode.position),
|
||||
);
|
||||
if (foundIndex !== -1) {
|
||||
nodesForConsoleReference.splice(foundIndex, 1);
|
||||
} else if (nodesForConsoleReference.length === CAPACITY) {
|
||||
@@ -45,12 +46,12 @@ const prepareCurrentReferencesForInsertion = (referenceNode: ConsoleReferenceNod
|
||||
};
|
||||
|
||||
const assignConsoleReferencesFrom = (referenceNodes: ConsoleReferenceNode[]) => {
|
||||
referenceNodes.forEach(
|
||||
(referenceNode, index) =>
|
||||
setDirectiveKey(referenceNode.node, getConsoleReferenceWithIndexOf(index)));
|
||||
referenceNodes.forEach((referenceNode, index) =>
|
||||
setDirectiveKey(referenceNode.node, getConsoleReferenceWithIndexOf(index)),
|
||||
);
|
||||
};
|
||||
|
||||
const setDirectiveKey = (node: ComponentTreeNode|null, key: string) => {
|
||||
const setDirectiveKey = (node: ComponentTreeNode | null, key: string) => {
|
||||
Object.defineProperty(window, key, {
|
||||
get: () => {
|
||||
if (node?.component) {
|
||||
@@ -66,4 +67,4 @@ const setDirectiveKey = (node: ComponentTreeNode|null, key: string) => {
|
||||
};
|
||||
|
||||
const getConsoleReferenceWithIndexOf = (consoleReferenceIndex: number) =>
|
||||
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
|
||||
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {unwrapSignal} from '../utils';
|
||||
|
||||
// Intentionally do not include all the prototype (Except for getters and setters)
|
||||
// because it contains inherited methods (hasOwnProperty, etc.). Also ignore symbols
|
||||
// because it is tricky to pass a path to a symbol.
|
||||
@@ -16,12 +18,13 @@ export function getKeys(obj: {}): string[] {
|
||||
if (!obj) {
|
||||
return [];
|
||||
}
|
||||
obj = unwrapSignal(obj);
|
||||
const properties = Object.getOwnPropertyNames(obj);
|
||||
|
||||
const prototypeMembers = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(obj));
|
||||
|
||||
const ignoreList = ['__proto__'];
|
||||
const gettersAndSetters = Object.keys(prototypeMembers).filter(methodName => {
|
||||
const gettersAndSetters = Object.keys(prototypeMembers).filter((methodName) => {
|
||||
if (ignoreList.includes(methodName)) {
|
||||
return false;
|
||||
}
|
||||
@@ -39,6 +42,6 @@ export function getKeys(obj: {}): string[] {
|
||||
* @param propName The string representation of the target property name
|
||||
* @returns The Descriptor object of the property
|
||||
*/
|
||||
export const getDescriptor = (instance: any, propName: string) =>
|
||||
Object.getOwnPropertyDescriptor(instance, propName) ||
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
|
||||
export const getDescriptor = (instance: any, propName: string): PropertyDescriptor | undefined =>
|
||||
Object.getOwnPropertyDescriptor(instance, propName) ||
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
|
||||
|
||||
@@ -73,11 +73,14 @@ describe('getPropType', () => {
|
||||
propTypeName: 'Set',
|
||||
},
|
||||
{
|
||||
expression:
|
||||
new Map<unknown, unknown>([['name', 'John'], ['age', 40], [{id: 123}, undefined]]),
|
||||
expression: new Map<unknown, unknown>([
|
||||
['name', 'John'],
|
||||
['age', 40],
|
||||
[{id: 123}, undefined],
|
||||
]),
|
||||
propType: PropType.Map,
|
||||
propTypeName: 'Map',
|
||||
}
|
||||
},
|
||||
];
|
||||
for (const {expression, propType, propTypeName} of testCases) {
|
||||
it(`should determine ${String(expression)} as PropType:${propTypeName}(${propType})`, () => {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import {PropType} from 'protocol';
|
||||
|
||||
import {isSignal} from '../utils';
|
||||
|
||||
const commonTypes = {
|
||||
boolean: PropType.Boolean,
|
||||
bigint: PropType.BigInt,
|
||||
@@ -24,6 +26,10 @@ const commonTypes = {
|
||||
* @see `devtools/projects/protocol`
|
||||
*/
|
||||
export const getPropType = (prop: unknown): PropType => {
|
||||
if (isSignal(prop)) {
|
||||
prop = prop();
|
||||
}
|
||||
|
||||
if (prop === undefined) {
|
||||
return PropType.Undefined;
|
||||
}
|
||||
|
||||
+256
-191
@@ -6,26 +6,30 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Descriptor, NestedProp, PropType} from 'protocol';
|
||||
import {ContainerType, Descriptor, NestedProp, PropType} from 'protocol';
|
||||
|
||||
import {isSignal, unwrapSignal} from '../utils';
|
||||
|
||||
import {getDescriptor, getKeys} from './object-utils';
|
||||
|
||||
// todo(aleksanderbodurri) pull this out of this file
|
||||
const METADATA_PROPERTY_NAME = '__ngContext__';
|
||||
|
||||
type NestedType = PropType.Array|PropType.Object;
|
||||
type NestedType = PropType.Array | PropType.Object;
|
||||
|
||||
export interface CompositeType {
|
||||
type: Extract<PropType, NestedType>;
|
||||
prop: any;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export interface TerminalType {
|
||||
type: Exclude<PropType, NestedType>;
|
||||
prop: any;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export type PropertyData = TerminalType|CompositeType;
|
||||
export type PropertyData = TerminalType | CompositeType;
|
||||
|
||||
export type Formatter<Result> = {
|
||||
[key in PropType]: (data: any) => Result;
|
||||
@@ -36,23 +40,15 @@ interface LevelOptions {
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const serializable: {[key in PropType]: boolean} = {
|
||||
[PropType.Boolean]: true,
|
||||
[PropType.String]: true,
|
||||
[PropType.Null]: true,
|
||||
[PropType.Number]: true,
|
||||
[PropType.Object]: true,
|
||||
[PropType.Undefined]: true,
|
||||
[PropType.Unknown]: true,
|
||||
[PropType.Array]: false,
|
||||
[PropType.Set]: false,
|
||||
[PropType.Map]: false,
|
||||
[PropType.BigInt]: false,
|
||||
[PropType.Function]: false,
|
||||
[PropType.HTMLNode]: false,
|
||||
[PropType.Symbol]: false,
|
||||
[PropType.Date]: false,
|
||||
};
|
||||
const serializable: Set<PropType> = new Set([
|
||||
PropType.Boolean,
|
||||
PropType.String,
|
||||
PropType.Null,
|
||||
PropType.Number,
|
||||
PropType.Object,
|
||||
PropType.Undefined,
|
||||
PropType.Unknown,
|
||||
]);
|
||||
|
||||
const typeToDescriptorPreview: Formatter<string> = {
|
||||
[PropType.Array]: (prop: Array<unknown>) => `Array(${prop.length})`,
|
||||
@@ -77,211 +73,280 @@ const typeToDescriptorPreview: Formatter<string> = {
|
||||
[PropType.Unknown]: (_: any) => 'unknown',
|
||||
};
|
||||
|
||||
type Key = string|number;
|
||||
type Key = string | number;
|
||||
type NestedSerializerFn = (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
nodes: NestedProp[],
|
||||
isReadonly: boolean,
|
||||
currentLevel: number,
|
||||
level?: number,
|
||||
) => Descriptor;
|
||||
|
||||
const ignoreList: Set<Key> = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
|
||||
|
||||
const shallowPropTypeToTreeMetaData:
|
||||
Record<Exclude<PropType, NestedType>, {editable: boolean, expandable: boolean}> = {
|
||||
[PropType.String]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.BigInt]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Boolean]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Number]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Date]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Null]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Undefined]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Symbol]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Function]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.HTMLNode]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Unknown]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Set]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Map]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
};
|
||||
|
||||
const isEditable =
|
||||
(descriptor: any, propName: string|number, propData: TerminalType,
|
||||
isGetterOrSetter: boolean) => {
|
||||
if (typeof propName === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isGetterOrSetter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (descriptor?.writable === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shallowPropTypeToTreeMetaData[propData.type].editable;
|
||||
};
|
||||
|
||||
const isGetterOrSetter = (descriptor: any): boolean =>
|
||||
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
|
||||
|
||||
const getPreview = (propData: TerminalType|CompositeType, isGetterOrSetter: boolean) => {
|
||||
return !isGetterOrSetter ? typeToDescriptorPreview[propData.type](propData.prop) :
|
||||
typeToDescriptorPreview[PropType.Function]({name: ''});
|
||||
const shallowPropTypeToTreeMetaData: Record<
|
||||
Exclude<PropType, NestedType>,
|
||||
{editable: boolean; expandable: boolean}
|
||||
> = {
|
||||
[PropType.String]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.BigInt]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Boolean]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Number]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Date]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Null]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Undefined]: {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Symbol]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Function]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.HTMLNode]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Unknown]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Set]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
[PropType.Map]: {
|
||||
editable: false,
|
||||
expandable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const createShallowSerializedDescriptor =
|
||||
(instance: any, propName: string|number, propData: TerminalType): Descriptor => {
|
||||
const {type} = propData;
|
||||
const isEditable = (
|
||||
descriptor: PropertyDescriptor | undefined,
|
||||
propName: string | number,
|
||||
propData: TerminalType,
|
||||
isGetterOrSetter: boolean,
|
||||
) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
if (typeof propName === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const shallowSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
expandable: shallowPropTypeToTreeMetaData[type].expandable,
|
||||
editable: isEditable(descriptor, propName, propData, getterOrSetter),
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
};
|
||||
if (isGetterOrSetter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (propData.prop !== undefined && serializable[type]) {
|
||||
shallowSerializedDescriptor.value = propData.prop;
|
||||
}
|
||||
if (descriptor?.writable === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shallowSerializedDescriptor;
|
||||
};
|
||||
return shallowPropTypeToTreeMetaData[propData.type].editable;
|
||||
};
|
||||
|
||||
export const createLevelSerializedDescriptor =
|
||||
(instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (instance: any, propName: string|number, level?: number, max?: number) => void):
|
||||
Descriptor => {
|
||||
const {type, prop} = propData;
|
||||
const isGetterOrSetter = (descriptor: any): boolean =>
|
||||
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
const getPreview = (propData: TerminalType | CompositeType, isGetterOrSetter: boolean) => {
|
||||
if (propData.containerType === 'ReadonlySignal') {
|
||||
return `Readonly Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
|
||||
} else if (propData.containerType === 'WritableSignal') {
|
||||
return `Signal(${typeToDescriptorPreview[propData.type](propData.prop())})`;
|
||||
}
|
||||
return !isGetterOrSetter
|
||||
? typeToDescriptorPreview[propData.type](propData.prop)
|
||||
: typeToDescriptorPreview[PropType.Function]({name: ''});
|
||||
};
|
||||
|
||||
const levelSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
};
|
||||
export function createShallowSerializedDescriptor(
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
propData: TerminalType,
|
||||
isReadonly: boolean,
|
||||
): Descriptor {
|
||||
const {type, containerType} = propData;
|
||||
|
||||
if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) {
|
||||
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
|
||||
if (value !== undefined) {
|
||||
levelSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
|
||||
return levelSerializedDescriptor;
|
||||
};
|
||||
const shallowSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
expandable: shallowPropTypeToTreeMetaData[type].expandable,
|
||||
editable: isEditable(descriptor, propName, propData, getterOrSetter) && !isReadonly,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
containerType,
|
||||
};
|
||||
|
||||
export const createNestedSerializedDescriptor =
|
||||
(instance: {}, propName: string|number, propData: CompositeType, levelOptions: LevelOptions,
|
||||
nodes: NestedProp[],
|
||||
nestedSerializer: (
|
||||
instance: any, propName: string|number, nodes: NestedProp[], currentLevel: number,
|
||||
level?: number) => void): Descriptor => {
|
||||
const {type, prop} = propData;
|
||||
if (propData.prop !== undefined && serializable.has(type)) {
|
||||
shallowSerializedDescriptor.value = unwrapSignal(propData.prop);
|
||||
}
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
return shallowSerializedDescriptor;
|
||||
}
|
||||
|
||||
const nestedSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
};
|
||||
export function createLevelSerializedDescriptor(
|
||||
instance: {},
|
||||
propName: string | number,
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
level?: number,
|
||||
max?: number,
|
||||
) => Descriptor,
|
||||
): Descriptor {
|
||||
const {type, prop, containerType} = propData;
|
||||
|
||||
if (nodes?.length) {
|
||||
const value = getNestedDescriptorValue(propData, levelOptions, nodes, nestedSerializer);
|
||||
if (value !== undefined) {
|
||||
nestedSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
return nestedSerializedDescriptor;
|
||||
};
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
|
||||
const levelSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
containerType,
|
||||
};
|
||||
|
||||
if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) {
|
||||
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
|
||||
if (value !== undefined) {
|
||||
levelSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
return levelSerializedDescriptor;
|
||||
}
|
||||
|
||||
export function createNestedSerializedDescriptor(
|
||||
instance: {},
|
||||
propName: string | number,
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
nodes: NestedProp[],
|
||||
nestedSerializer: NestedSerializerFn,
|
||||
): Descriptor {
|
||||
const {type, prop, containerType} = propData;
|
||||
|
||||
const descriptor = getDescriptor(instance, propName as string);
|
||||
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
|
||||
|
||||
const nestedSerializedDescriptor: Descriptor = {
|
||||
type,
|
||||
editable: false,
|
||||
expandable: !getterOrSetter && getKeys(prop).length > 0,
|
||||
preview: getPreview(propData, getterOrSetter),
|
||||
containerType,
|
||||
};
|
||||
|
||||
if (nodes?.length) {
|
||||
const value = getNestedDescriptorValue(propData, levelOptions, nodes, nestedSerializer);
|
||||
if (value !== undefined) {
|
||||
nestedSerializedDescriptor.value = value;
|
||||
}
|
||||
}
|
||||
return nestedSerializedDescriptor;
|
||||
}
|
||||
|
||||
function getNestedDescriptorValue(
|
||||
propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[],
|
||||
nestedSerializer: (
|
||||
instance: any, propName: string|number, nodes: NestedProp[], currentLevel: number,
|
||||
level?: number) => void) {
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
nodes: NestedProp[],
|
||||
nestedSerializer: NestedSerializerFn,
|
||||
) {
|
||||
const {type, prop} = propData;
|
||||
const {currentLevel} = levelOptions;
|
||||
const value = unwrapSignal(prop);
|
||||
|
||||
switch (type) {
|
||||
case PropType.Array:
|
||||
return nodes.map(
|
||||
(nestedProp) =>
|
||||
nestedSerializer(prop, nestedProp.name, nestedProp.children, currentLevel + 1));
|
||||
return nodes.map((nestedProp) =>
|
||||
nestedSerializer(value, nestedProp.name, nestedProp.children, false, currentLevel + 1),
|
||||
);
|
||||
case PropType.Object:
|
||||
return nodes.reduce((accumulator, nestedProp) => {
|
||||
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
|
||||
accumulator[nestedProp.name] =
|
||||
nestedSerializer(prop, nestedProp.name, nestedProp.children, currentLevel + 1);
|
||||
}
|
||||
return accumulator;
|
||||
}, {} as Record<string, void>);
|
||||
return nodes.reduce(
|
||||
(accumulator, nestedProp) => {
|
||||
if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) {
|
||||
accumulator[nestedProp.name] = nestedSerializer(
|
||||
value,
|
||||
nestedProp.name,
|
||||
nestedProp.children,
|
||||
false,
|
||||
currentLevel + 1,
|
||||
);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
{} as Record<string, Descriptor>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getLevelDescriptorValue(
|
||||
propData: CompositeType, levelOptions: LevelOptions,
|
||||
continuation: (instance: any, propName: string|number, level?: number, max?: number) => void) {
|
||||
propData: CompositeType,
|
||||
levelOptions: LevelOptions,
|
||||
continuation: (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
level?: number,
|
||||
max?: number,
|
||||
) => Descriptor,
|
||||
) {
|
||||
const {type, prop} = propData;
|
||||
const {currentLevel, level} = levelOptions;
|
||||
|
||||
const value = unwrapSignal(prop);
|
||||
const isReadonly = isSignal(prop);
|
||||
switch (type) {
|
||||
case PropType.Array:
|
||||
return prop.map((_: any, idx: number) => continuation(prop, idx, currentLevel + 1, level));
|
||||
return prop.map((_: any, idx: number) =>
|
||||
continuation(value, idx, isReadonly, currentLevel + 1, level),
|
||||
);
|
||||
case PropType.Object:
|
||||
return getKeys(prop).reduce((accumulator, propName) => {
|
||||
if (!ignoreList.has(propName)) {
|
||||
accumulator[propName] = continuation(prop, propName, currentLevel + 1, level);
|
||||
}
|
||||
return accumulator;
|
||||
}, {} as Record<string, void>);
|
||||
return getKeys(prop).reduce(
|
||||
(accumulator, propName) => {
|
||||
if (!ignoreList.has(propName)) {
|
||||
accumulator[propName] = continuation(
|
||||
value,
|
||||
propName,
|
||||
isReadonly,
|
||||
currentLevel + 1,
|
||||
level,
|
||||
);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
{} as Record<string, Descriptor>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const truncate = (str: string, max = 20): string => {
|
||||
if (str.length > max) {
|
||||
return str.substring(0, max) + '...';
|
||||
}
|
||||
return str;
|
||||
};
|
||||
function truncate(str: string, max = 20): string {
|
||||
return str.length > max ? str.substring(0, max) + '...' : str;
|
||||
}
|
||||
|
||||
+159
-128
@@ -93,6 +93,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null,
|
||||
},
|
||||
nested: {
|
||||
type: PropType.Object,
|
||||
@@ -105,8 +106,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
expandable: true,
|
||||
editable: false,
|
||||
preview: 'Array(4)',
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -120,6 +123,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null,
|
||||
},
|
||||
nested: {
|
||||
type: PropType.Object,
|
||||
@@ -151,21 +155,26 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
],
|
||||
containerType: null,
|
||||
},
|
||||
{
|
||||
type: PropType.Set,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: 'Set(2)',
|
||||
|
||||
containerType: null,
|
||||
},
|
||||
],
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -193,34 +202,37 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: 'Array(3)',
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with getters with specified query', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: 3,
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: 3,
|
||||
},
|
||||
};
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'foo',
|
||||
children: [
|
||||
{
|
||||
name: 'baz',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'foo',
|
||||
children: [
|
||||
{
|
||||
name: 'baz',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
foo: {
|
||||
type: PropType.Object,
|
||||
@@ -233,26 +245,29 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: '{...}',
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with getters without specified query', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: {
|
||||
cos: 3,
|
||||
},
|
||||
{
|
||||
get foo(): any {
|
||||
return {
|
||||
baz: {
|
||||
qux: {
|
||||
cos: 3,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
[]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
foo: {
|
||||
type: PropType.Object,
|
||||
@@ -265,24 +280,27 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
expandable: true,
|
||||
preview: '{...}',
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('both getters and setters should be readonly', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
get bar(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(val: number) {},
|
||||
{
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
[]);
|
||||
get bar(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(val: number) {},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Neither getter and setter is editable
|
||||
expect(result).toEqual({
|
||||
@@ -292,6 +310,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Number,
|
||||
@@ -299,57 +318,59 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the precise path requested', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
state: {
|
||||
nested: {
|
||||
props: {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
[Symbol(3)](): number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
{
|
||||
state: {
|
||||
nested: {
|
||||
props: {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
[Symbol(3)](): number {
|
||||
return 1.618;
|
||||
},
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'state',
|
||||
children: [
|
||||
{
|
||||
name: 'nested',
|
||||
children: [
|
||||
{
|
||||
name: 'props',
|
||||
children: [
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
},
|
||||
[
|
||||
{
|
||||
name: 'state',
|
||||
children: [
|
||||
{
|
||||
name: 'nested',
|
||||
children: [
|
||||
{
|
||||
name: 'props',
|
||||
children: [
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
state: {
|
||||
type: PropType.Object,
|
||||
@@ -375,6 +396,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Number,
|
||||
@@ -382,8 +404,10 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '2',
|
||||
value: 2,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
foo: {
|
||||
type: PropType.Number,
|
||||
@@ -391,35 +415,41 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: false,
|
||||
preview: '(...)',
|
||||
value: 42,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('both setter and getter would get a (...) as preview', () => {
|
||||
const result = deeplySerializeSelectedProperties(
|
||||
{
|
||||
set foo(_: any) {},
|
||||
get bar(): Object {
|
||||
return {foo: 1};
|
||||
},
|
||||
{
|
||||
set foo(_: any) {},
|
||||
get bar(): Object {
|
||||
return {foo: 1};
|
||||
},
|
||||
[]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(result).toEqual({
|
||||
foo: {
|
||||
type: PropType.Undefined,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: '(...)',
|
||||
containerType: null,
|
||||
},
|
||||
bar: {
|
||||
type: PropType.Object,
|
||||
editable: false,
|
||||
expandable: false,
|
||||
preview: '(...)',
|
||||
containerType: null,
|
||||
value: {
|
||||
foo: {
|
||||
type: PropType.Number,
|
||||
@@ -427,6 +457,7 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
preview: '1',
|
||||
value: 1,
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -441,49 +472,49 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
editable: true,
|
||||
expandable: false,
|
||||
preview: 'undefined',
|
||||
}
|
||||
containerType: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype',
|
||||
() => {
|
||||
const instance = {
|
||||
__proto__: {
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(newNum: number) {},
|
||||
get baz(): number {
|
||||
return 42;
|
||||
},
|
||||
set baz(newNum: number) {},
|
||||
}
|
||||
};
|
||||
it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype', () => {
|
||||
const instance = {
|
||||
__proto__: {
|
||||
get foo(): number {
|
||||
return 42;
|
||||
},
|
||||
set bar(newNum: number) {},
|
||||
get baz(): number {
|
||||
return 42;
|
||||
},
|
||||
set baz(newNum: number) {},
|
||||
},
|
||||
};
|
||||
|
||||
const descriptorFoo = getDescriptor(instance, 'foo');
|
||||
expect(descriptorFoo).not.toBeNull();
|
||||
expect(descriptorFoo!.get).not.toBeNull();
|
||||
expect(descriptorFoo!.set).toBeUndefined();
|
||||
expect(descriptorFoo!.value).toBeUndefined();
|
||||
expect(descriptorFoo!.enumerable).toBe(true);
|
||||
expect(descriptorFoo!.configurable).toBe(true);
|
||||
const descriptorFoo = getDescriptor(instance, 'foo');
|
||||
expect(descriptorFoo).not.toBeNull();
|
||||
expect(descriptorFoo!.get).not.toBeNull();
|
||||
expect(descriptorFoo!.set).toBeUndefined();
|
||||
expect(descriptorFoo!.value).toBeUndefined();
|
||||
expect(descriptorFoo!.enumerable).toBe(true);
|
||||
expect(descriptorFoo!.configurable).toBe(true);
|
||||
|
||||
const descriptorBar = getDescriptor(instance, 'bar');
|
||||
expect(descriptorBar).not.toBeNull();
|
||||
expect(descriptorBar!.get).toBeUndefined();
|
||||
expect(descriptorBar!.set).not.toBeNull();
|
||||
expect(descriptorBar!.value).toBeUndefined();
|
||||
expect(descriptorBar!.enumerable).toBe(true);
|
||||
expect(descriptorBar!.configurable).toBe(true);
|
||||
const descriptorBar = getDescriptor(instance, 'bar');
|
||||
expect(descriptorBar).not.toBeNull();
|
||||
expect(descriptorBar!.get).toBeUndefined();
|
||||
expect(descriptorBar!.set).not.toBeNull();
|
||||
expect(descriptorBar!.value).toBeUndefined();
|
||||
expect(descriptorBar!.enumerable).toBe(true);
|
||||
expect(descriptorBar!.configurable).toBe(true);
|
||||
|
||||
const descriptorBaz = getDescriptor(instance, 'baz');
|
||||
expect(descriptorBaz).not.toBeNull();
|
||||
expect(descriptorBaz!.get).not.toBeNull();
|
||||
expect(descriptorBaz!.set).not.toBeNull();
|
||||
expect(descriptorBaz!.value).toBeUndefined();
|
||||
expect(descriptorBaz!.enumerable).toBe(true);
|
||||
expect(descriptorBaz!.configurable).toBe(true);
|
||||
});
|
||||
const descriptorBaz = getDescriptor(instance, 'baz');
|
||||
expect(descriptorBaz).not.toBeNull();
|
||||
expect(descriptorBaz!.get).not.toBeNull();
|
||||
expect(descriptorBaz!.set).not.toBeNull();
|
||||
expect(descriptorBaz!.value).toBeUndefined();
|
||||
expect(descriptorBaz!.enumerable).toBe(true);
|
||||
expect(descriptorBaz!.configurable).toBe(true);
|
||||
});
|
||||
|
||||
it('getKeys should all keys including getters and setters', () => {
|
||||
const instance = {
|
||||
@@ -493,8 +524,8 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
return 42;
|
||||
},
|
||||
set foo(newNum: number) {},
|
||||
set bar(newNum: number) {}
|
||||
}
|
||||
set bar(newNum: number) {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getKeys(instance)).toEqual(['baz', 'foo', 'bar']);
|
||||
@@ -507,8 +538,8 @@ describe('deeplySerializeSelectedProperties', () => {
|
||||
set __proto__(newObj: Object) {},
|
||||
get __proto__(): Object {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getKeys(instance)).toEqual(['baz']);
|
||||
|
||||
+108
-56
@@ -6,11 +6,19 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Descriptor, NestedProp, PropType} from 'protocol';
|
||||
import {ContainerType, Descriptor, NestedProp, PropType} from 'protocol';
|
||||
import type {Signal} from '@angular/core';
|
||||
|
||||
import {isSignal, unwrapSignal} from '../utils';
|
||||
|
||||
import {getKeys} from './object-utils';
|
||||
import {getPropType} from './prop-type';
|
||||
import {createLevelSerializedDescriptor, createNestedSerializedDescriptor, createShallowSerializedDescriptor, PropertyData,} from './serialized-descriptor-factory';
|
||||
import {
|
||||
createLevelSerializedDescriptor,
|
||||
createNestedSerializedDescriptor,
|
||||
createShallowSerializedDescriptor,
|
||||
PropertyData,
|
||||
} from './serialized-descriptor-factory';
|
||||
|
||||
// todo(aleksanderbodurri) pull this out of this file
|
||||
const METADATA_PROPERTY_NAME = '__ngContext__';
|
||||
@@ -19,80 +27,124 @@ const ignoreList = new Set([METADATA_PROPERTY_NAME, '__ngSimpleChanges__']);
|
||||
|
||||
const MAX_LEVEL = 1;
|
||||
|
||||
const nestedSerializer =
|
||||
(instance: any, propName: string|number, nodes: NestedProp[], currentLevel = 0,
|
||||
level = MAX_LEVEL): Descriptor => {
|
||||
const serializableInstance = instance[propName];
|
||||
const propData:
|
||||
PropertyData = {prop: serializableInstance, type: getPropType(serializableInstance)};
|
||||
function nestedSerializer(
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
nodes: NestedProp[],
|
||||
isReadonly: boolean,
|
||||
currentLevel = 0,
|
||||
level = MAX_LEVEL,
|
||||
): Descriptor {
|
||||
instance = unwrapSignal(instance);
|
||||
const serializableInstance = instance[propName];
|
||||
const propData: PropertyData = {
|
||||
prop: serializableInstance,
|
||||
type: getPropType(serializableInstance),
|
||||
containerType: getContainerType(serializableInstance),
|
||||
};
|
||||
|
||||
if (currentLevel < level) {
|
||||
return levelSerializer(
|
||||
instance, propName, currentLevel, level, nestedSerializerContinuation(nodes, level));
|
||||
}
|
||||
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createNestedSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, nodes, nestedSerializer);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData);
|
||||
}
|
||||
if (currentLevel < level) {
|
||||
const continuation = (
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
nestedLevel?: number,
|
||||
_?: number,
|
||||
) => {
|
||||
const nodeChildren = nodes.find((v) => v.name === propName)?.children ?? [];
|
||||
return nestedSerializer(instance, propName, nodeChildren, isReadonly, nestedLevel, level);
|
||||
};
|
||||
|
||||
const nestedSerializerContinuation = (nodes: NestedProp[], level: number) =>
|
||||
(instance: any, propName: string|number, nestedLevel?: number, _?: number) => {
|
||||
const idx = nodes.findIndex((v) => v.name === propName);
|
||||
if (idx < 0) {
|
||||
// The property is not specified in the query.
|
||||
return nestedSerializer(instance, propName, [], nestedLevel, level);
|
||||
}
|
||||
return nestedSerializer(instance, propName, nodes[idx].children, nestedLevel, level);
|
||||
};
|
||||
return levelSerializer(instance, propName, isReadonly, currentLevel, level, continuation);
|
||||
}
|
||||
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createNestedSerializedDescriptor(
|
||||
instance,
|
||||
propName,
|
||||
propData,
|
||||
{level, currentLevel},
|
||||
nodes,
|
||||
nestedSerializer,
|
||||
);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
|
||||
}
|
||||
}
|
||||
|
||||
function levelSerializer(
|
||||
instance: any, propName: string|number, currentLevel = 0, level = MAX_LEVEL,
|
||||
continuation = levelSerializer): Descriptor {
|
||||
instance: any,
|
||||
propName: string | number,
|
||||
isReadonly: boolean,
|
||||
currentLevel = 0,
|
||||
level = MAX_LEVEL,
|
||||
continuation = levelSerializer,
|
||||
): Descriptor {
|
||||
const serializableInstance = instance[propName];
|
||||
const propData:
|
||||
PropertyData = {prop: serializableInstance, type: getPropType(serializableInstance)};
|
||||
const propData: PropertyData = {
|
||||
prop: serializableInstance,
|
||||
type: getPropType(serializableInstance),
|
||||
containerType: getContainerType(serializableInstance),
|
||||
};
|
||||
|
||||
switch (propData.type) {
|
||||
case PropType.Array:
|
||||
case PropType.Object:
|
||||
return createLevelSerializedDescriptor(
|
||||
instance, propName, propData, {level, currentLevel}, continuation);
|
||||
instance,
|
||||
propName,
|
||||
propData,
|
||||
{level, currentLevel},
|
||||
continuation,
|
||||
);
|
||||
default:
|
||||
return createShallowSerializedDescriptor(instance, propName, propData);
|
||||
return createShallowSerializedDescriptor(instance, propName, propData, isReadonly);
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeDirectiveState(
|
||||
instance: object, levels = MAX_LEVEL): {[key: string]: Descriptor} {
|
||||
export function serializeDirectiveState(instance: object): Record<string, Descriptor> {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
getKeys(instance).forEach((prop) => {
|
||||
const value = unwrapSignal(instance);
|
||||
const isReadonly = isSignal(instance);
|
||||
getKeys(value).forEach((prop) => {
|
||||
if (typeof prop === 'string' && ignoreList.has(prop)) {
|
||||
return;
|
||||
}
|
||||
result[prop] = levelSerializer(instance, prop, 0, 0);
|
||||
result[prop] = levelSerializer(value, prop, isReadonly, 0, 0);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const deeplySerializeSelectedProperties =
|
||||
(instance: any, props: NestedProp[]): {[name: string]: Descriptor} => {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
getKeys(instance).forEach((prop) => {
|
||||
if (ignoreList.has(prop)) {
|
||||
return;
|
||||
}
|
||||
const idx = props.findIndex((v) => v.name === prop);
|
||||
if (idx < 0) {
|
||||
result[prop] = levelSerializer(instance, prop);
|
||||
} else {
|
||||
result[prop] = nestedSerializer(instance, prop, props[idx].children);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
export function deeplySerializeSelectedProperties(
|
||||
instance: object,
|
||||
props: NestedProp[],
|
||||
): Record<string, Descriptor> {
|
||||
const result: Record<string, Descriptor> = {};
|
||||
const isReadonly = isSignal(instance);
|
||||
getKeys(instance).forEach((prop) => {
|
||||
if (ignoreList.has(prop)) {
|
||||
return;
|
||||
}
|
||||
const childrenProps = props.find((v) => v.name === prop)?.children;
|
||||
if (!childrenProps) {
|
||||
result[prop] = levelSerializer(instance, prop, isReadonly);
|
||||
} else {
|
||||
result[prop] = nestedSerializer(instance, prop, childrenProps, isReadonly);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getContainerType(instance: unknown): ContainerType {
|
||||
if (isSignal(instance)) {
|
||||
return isWritableSignal(instance) ? 'WritableSignal' : 'ReadonlySignal';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isWritableSignal(s: any): boolean {
|
||||
return typeof s['set'] === 'function';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export const runOutsideAngular = (f: () => any): void => {
|
||||
export const ngDebug = () => (window as any).ng;
|
||||
|
||||
export const runOutsideAngular = (f: () => void): void => {
|
||||
const w = window as any;
|
||||
if (!w.Zone || !w.Zone.current) {
|
||||
f();
|
||||
@@ -34,3 +36,36 @@ export const isCustomElement = (node: Node) => {
|
||||
const tagName = node.tagName.toLowerCase();
|
||||
return !!customElements.get(tagName);
|
||||
};
|
||||
|
||||
export function hasDiDebugAPIs(): boolean {
|
||||
if (!ngDebugApiIsSupported('ɵgetInjectorResolutionPath')) {
|
||||
return false;
|
||||
}
|
||||
if (!ngDebugApiIsSupported('ɵgetDependenciesFromInjectable')) {
|
||||
return false;
|
||||
}
|
||||
if (!ngDebugApiIsSupported('ɵgetInjectorProviders')) {
|
||||
return false;
|
||||
}
|
||||
if (!ngDebugApiIsSupported('ɵgetInjectorMetadata')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function ngDebugApiIsSupported(api: string): boolean {
|
||||
const ng = ngDebug();
|
||||
return typeof ng[api] === 'function';
|
||||
}
|
||||
|
||||
export function isSignal(prop: unknown): prop is () => unknown {
|
||||
if (!ngDebugApiIsSupported('isSignal')) {
|
||||
return false;
|
||||
}
|
||||
return (window as any).ng.isSignal(prop);
|
||||
}
|
||||
|
||||
export function unwrapSignal(s: any): any {
|
||||
return isSignal(s) ? s() : s;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ ng_module(
|
||||
name = "devtools-tabs",
|
||||
srcs = [
|
||||
"devtools-tabs.component.ts",
|
||||
"devtools-tabs.module.ts",
|
||||
],
|
||||
angular_assets = [
|
||||
"devtools-tabs.component.html",
|
||||
|
||||
+122
-134
@@ -27,18 +27,17 @@ export type InjectorTreeD3Node = d3.HierarchyPointNode<InjectorTreeNode>;
|
||||
|
||||
export abstract class GraphRenderer<T, U> {
|
||||
abstract render(graph: T): void;
|
||||
abstract getNodeById(id: string): U|null;
|
||||
abstract getNodeById(id: string): U | null;
|
||||
abstract snapToNode(node: U): void;
|
||||
abstract snapToRoot(): void;
|
||||
abstract zoomScale(scale: number): void;
|
||||
abstract root: U|null;
|
||||
abstract root: U | null;
|
||||
abstract get graphElement(): HTMLElement;
|
||||
|
||||
protected nodeClickListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
|
||||
protected nodeMouseoverListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
|
||||
protected nodeMouseoutListeners: ((pointerEvent: PointerEvent, node: U) => void)[] = [];
|
||||
|
||||
|
||||
cleanup(): void {
|
||||
this.nodeClickListeners = [];
|
||||
this.nodeMouseoverListeners = [];
|
||||
@@ -59,7 +58,7 @@ export abstract class GraphRenderer<T, U> {
|
||||
}
|
||||
|
||||
interface InjectorTreeVisualizerConfig {
|
||||
orientation: 'horizontal'|'vertical';
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
nodeSize: [width: number, height: number];
|
||||
nodeSeparation: (nodeA: InjectorTreeD3Node, nodeB: InjectorTreeD3Node) => number;
|
||||
nodeLabelSize: [width: number, height: number];
|
||||
@@ -69,14 +68,14 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
||||
public config: InjectorTreeVisualizerConfig;
|
||||
|
||||
constructor(
|
||||
private _containerElement: HTMLElement,
|
||||
private _graphElement: HTMLElement,
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
nodeSize = [200, 500],
|
||||
nodeSeparation = () => 2,
|
||||
nodeLabelSize = [250, 60],
|
||||
}: Partial<InjectorTreeVisualizerConfig> = {},
|
||||
private _containerElement: HTMLElement,
|
||||
private _graphElement: HTMLElement,
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
nodeSize = [200, 500],
|
||||
nodeSeparation = () => 2,
|
||||
nodeLabelSize = [250, 60],
|
||||
}: Partial<InjectorTreeVisualizerConfig> = {},
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -90,13 +89,15 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
||||
|
||||
private d3 = d3;
|
||||
|
||||
override root: InjectorTreeD3Node|null = null;
|
||||
zoomController: d3.ZoomBehavior<HTMLElement, unknown>|null = null;
|
||||
override root: InjectorTreeD3Node | null = null;
|
||||
zoomController: d3.ZoomBehavior<HTMLElement, unknown> | null = null;
|
||||
|
||||
override zoomScale(scale: number) {
|
||||
if (this.zoomController) {
|
||||
this.zoomController.scaleTo(
|
||||
this.d3.select<HTMLElement, unknown>(this._containerElement), scale);
|
||||
this.d3.select<HTMLElement, unknown>(this._containerElement),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +109,8 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
||||
|
||||
override snapToNode(node: InjectorTreeD3Node, scale = 1): void {
|
||||
const svg = this.d3.select(this._containerElement);
|
||||
const halfWidth = (this._containerElement.clientWidth / 2);
|
||||
const halfHeight = (this._containerElement.clientHeight / 2);
|
||||
const halfWidth = this._containerElement.clientWidth / 2;
|
||||
const halfHeight = this._containerElement.clientHeight / 2;
|
||||
const t = d3.zoomIdentity.translate(halfWidth - node.y, halfHeight - node.x).scale(scale);
|
||||
svg.transition().duration(500).call(this.zoomController!.transform, t);
|
||||
}
|
||||
@@ -118,9 +119,10 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
||||
return this._graphElement;
|
||||
}
|
||||
|
||||
override getNodeById(id: string): InjectorTreeD3Node|null {
|
||||
const selection = this.d3.select<HTMLElement, InjectorTreeD3Node>(this._containerElement)
|
||||
.select(`.node[data-id="${id}"]`);
|
||||
override getNodeById(id: string): InjectorTreeD3Node | null {
|
||||
const selection = this.d3
|
||||
.select<HTMLElement, InjectorTreeD3Node>(this._containerElement)
|
||||
.select(`.node[data-id="${id}"]`);
|
||||
if (selection.empty()) {
|
||||
return null;
|
||||
}
|
||||
@@ -157,142 +159,128 @@ export class InjectorTreeVisualizer extends GraphRenderer<InjectorTreeNode, Inje
|
||||
this.root = nodes;
|
||||
|
||||
arrowDefId++;
|
||||
svg.append('svg:defs')
|
||||
.selectAll('marker')
|
||||
.data([`end${arrowDefId}`]) // Different link/path types can be defined here
|
||||
.enter()
|
||||
.append('svg:marker') // This section adds in the arrows
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 15)
|
||||
.attr('refY', 0)
|
||||
.attr('class', 'arrow')
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('svg:path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
svg
|
||||
.append('svg:defs')
|
||||
.selectAll('marker')
|
||||
.data([`end${arrowDefId}`]) // Different link/path types can be defined here
|
||||
.enter()
|
||||
.append('svg:marker') // This section adds in the arrows
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 15)
|
||||
.attr('refY', 0)
|
||||
.attr('class', 'arrow')
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('svg:path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
|
||||
g.selectAll('.link')
|
||||
.data(nodes.descendants().slice(1))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr(
|
||||
'class',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
const parentId = node.parent?.data?.injector?.id;
|
||||
if (parentId === 'N/A') {
|
||||
return 'link-hidden';
|
||||
}
|
||||
.data(nodes.descendants().slice(1))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', (node: InjectorTreeD3Node) => {
|
||||
const parentId = node.parent?.data?.injector?.id;
|
||||
if (parentId === 'N/A') {
|
||||
return 'link-hidden';
|
||||
}
|
||||
|
||||
return `link`;
|
||||
})
|
||||
.attr(
|
||||
'data-id',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
const from = node.data.injector.id;
|
||||
const to = node.parent?.data?.injector?.id;
|
||||
return `link`;
|
||||
})
|
||||
.attr('data-id', (node: InjectorTreeD3Node) => {
|
||||
const from = node.data.injector.id;
|
||||
const to = node.parent?.data?.injector?.id;
|
||||
|
||||
if (from && to) {
|
||||
return `${from}-to-${to}`;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.attr('marker-end', `url(#end${arrowDefId})`)
|
||||
.attr('d', (node: InjectorTreeD3Node) => {
|
||||
const parent = node.parent!;
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `
|
||||
if (from && to) {
|
||||
return `${from}-to-${to}`;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.attr('marker-end', `url(#end${arrowDefId})`)
|
||||
.attr('d', (node: InjectorTreeD3Node) => {
|
||||
const parent = node.parent!;
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `
|
||||
M${node.y},${node.x}
|
||||
C${(node.y + parent.y) / 2},
|
||||
${node.x} ${(node.y + parent.y) / 2},
|
||||
${parent.x} ${parent.y},
|
||||
${parent.x}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
return `
|
||||
M${node.x},${node.y}
|
||||
C${(node.x + parent.x) / 2},
|
||||
${node.y} ${(node.x + parent.x) / 2},
|
||||
${parent.y} ${parent.x},
|
||||
${parent.y}`;
|
||||
});
|
||||
});
|
||||
|
||||
// Declare the nodes
|
||||
const node =
|
||||
g.selectAll('g.node')
|
||||
.data(nodes.descendants())
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr(
|
||||
'class',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
if (node.data.injector.id === 'N/A') {
|
||||
return 'node-hidden';
|
||||
}
|
||||
return `node`;
|
||||
})
|
||||
.attr(
|
||||
'data-component-id',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
const injector = node.data.injector;
|
||||
if (injector.type === 'element') {
|
||||
return injector.node?.component?.id ?? -1;
|
||||
}
|
||||
return -1;
|
||||
})
|
||||
.attr(
|
||||
'data-id',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.id;
|
||||
})
|
||||
.on('click',
|
||||
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeClickListeners.forEach(listener => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseover',
|
||||
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoverListeners.forEach(listener => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseout',
|
||||
(pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoutListeners.forEach(listener => listener(pointerEvent, node));
|
||||
})
|
||||
const node = g
|
||||
.selectAll('g.node')
|
||||
.data(nodes.descendants())
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', (node: InjectorTreeD3Node) => {
|
||||
if (node.data.injector.id === 'N/A') {
|
||||
return 'node-hidden';
|
||||
}
|
||||
return `node`;
|
||||
})
|
||||
.attr('data-component-id', (node: InjectorTreeD3Node) => {
|
||||
const injector = node.data.injector;
|
||||
if (injector.type === 'element') {
|
||||
return injector.node?.component?.id ?? -1;
|
||||
}
|
||||
return -1;
|
||||
})
|
||||
.attr('data-id', (node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.id;
|
||||
})
|
||||
.on('click', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeClickListeners.forEach((listener) => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseover', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoverListeners.forEach((listener) => listener(pointerEvent, node));
|
||||
})
|
||||
.on('mouseout', (pointerEvent: PointerEvent, node: InjectorTreeD3Node) => {
|
||||
this.nodeMouseoutListeners.forEach((listener) => listener(pointerEvent, node));
|
||||
})
|
||||
|
||||
.attr('transform', (node: InjectorTreeD3Node) => {
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `translate(${node.y},${node.x})`;
|
||||
}
|
||||
.attr('transform', (node: InjectorTreeD3Node) => {
|
||||
if (this.config.orientation === 'horizontal') {
|
||||
return `translate(${node.y},${node.x})`;
|
||||
}
|
||||
|
||||
return `translate(${node.x},${node.y})`;
|
||||
});
|
||||
return `translate(${node.x},${node.y})`;
|
||||
});
|
||||
|
||||
const [width, height] = this.config.nodeLabelSize!;
|
||||
|
||||
node.append('foreignObject')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('x', -1 * (width - 10))
|
||||
.attr('y', -1 * (height / 2))
|
||||
.append('xhtml:div')
|
||||
.attr(
|
||||
'title',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.name;
|
||||
})
|
||||
.attr(
|
||||
'class',
|
||||
(node: InjectorTreeD3Node) => {
|
||||
return [
|
||||
injectorTypeToClassMap.get(node.data?.injector?.type) ?? '', 'node-container'
|
||||
].join(' ');
|
||||
})
|
||||
.html((node: InjectorTreeD3Node) => {
|
||||
const label = node.data.injector.name;
|
||||
const lengthLimit = 25;
|
||||
return label.length > lengthLimit ? label.slice(0, lengthLimit - '...'.length) + '...' :
|
||||
label;
|
||||
});
|
||||
node
|
||||
.append('foreignObject')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('x', -1 * (width - 10))
|
||||
.attr('y', -1 * (height / 2))
|
||||
.append('xhtml:div')
|
||||
.attr('title', (node: InjectorTreeD3Node) => {
|
||||
return node.data.injector.name;
|
||||
})
|
||||
.attr('class', (node: InjectorTreeD3Node) => {
|
||||
return [injectorTypeToClassMap.get(node.data?.injector?.type) ?? '', 'node-container'].join(
|
||||
' ',
|
||||
);
|
||||
})
|
||||
.html((node: InjectorTreeD3Node) => {
|
||||
const label = node.data.injector.name;
|
||||
const lengthLimit = 25;
|
||||
return label.length > lengthLimit
|
||||
? label.slice(0, lengthLimit - '...'.length) + '...'
|
||||
: label;
|
||||
});
|
||||
|
||||
svg.attr('height', '100%').attr('width', '100%');
|
||||
}
|
||||
|
||||
+21
-12
@@ -14,20 +14,26 @@ import {InjectorTreeNode, InjectorTreeVisualizer} from './injector-tree-visualiz
|
||||
@Component({
|
||||
selector: 'ng-resolution-path',
|
||||
template: `
|
||||
<section class="injector-graph">
|
||||
<svg #svgContainer>
|
||||
<g #mainGroup></g>
|
||||
</svg>
|
||||
</section>
|
||||
<section class="injector-graph">
|
||||
<svg #svgContainer>
|
||||
<g #mainGroup></g>
|
||||
</svg>
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
styles: [`:host { display: block; }`],
|
||||
standalone: true
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class ResolutionPathComponent implements OnDestroy, AfterViewInit {
|
||||
@ViewChild('svgContainer', {static: true}) private svgContainer!: ElementRef;
|
||||
@ViewChild('mainGroup', {static: true}) private g!: ElementRef;
|
||||
|
||||
@Input() orientation: 'horizontal'|'vertical' = 'horizontal';
|
||||
@Input() orientation: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
private injectorTree!: InjectorTreeVisualizer;
|
||||
private pathNode!: InjectorTreeNode;
|
||||
@@ -56,10 +62,13 @@ export class ResolutionPathComponent implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.injectorTree =
|
||||
new InjectorTreeVisualizer(this.svgContainer.nativeElement, this.g.nativeElement, {
|
||||
orientation: this.orientation,
|
||||
});
|
||||
this.injectorTree = new InjectorTreeVisualizer(
|
||||
this.svgContainer.nativeElement,
|
||||
this.g.nativeElement,
|
||||
{
|
||||
orientation: this.orientation,
|
||||
},
|
||||
);
|
||||
|
||||
if (this.pathNode) {
|
||||
this.injectorTree.render(this.pathNode);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user