Compare commits

...

67 Commits

Author SHA1 Message Date
Pawel Kozlowski e2e0f08d99 release: cut the v17.1.1 release 2024-01-24 19:31:20 +01:00
Max e194fbfbaa docs: fix typo in Custom directives section (#54044)
There was a grammatically incorrect sentence in the What is Angular page, Custom directives section. This change makes it more correct.
PR Close #54044
2024-01-24 15:32:22 +01:00
Paul Gschwendtner 8ad1ee764b build: ensure zone.js typing test is hermetic (#54048)
Currently the ZoneJS typing tests executes outside of Bazel, as a legacy
artifact of the monorepo merging (as it seems - not ideal at all).

Looks like this test relies on its own node modules, that were NOT
locked using a yarn lock file. This commit adds one, and specifically
locks it to a `@types/node` version that does not include the most
recent patch release (which seemingly introduced a breaking change)
that causes issues with TypeScript's lib checking.

Whenever we perform lock file maintenance in the future, we have the
following options:

- Consider disabling lib checking via `skipLibCheck` for this test. This
  may be acceptable.
- Continue locking the node version,
- Waiting for chokidar to comply with the new signature
- Waiting for the breaking change to be rolled back.

Culprit change:
https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68300

PR Close #54048
2024-01-24 14:57:05 +01:00
Andrew Scott f222bee8fa fix(router): revert commit that replaced last helper with native Array.at(-1) (#54021)
While `Array.at` is technically supported in all browsers we officially
support, the change was needlessly breaking without any real benefit.

PR Close #54021
2024-01-24 10:54:55 +01:00
Alan Agius 9a9a00e0f9 docs: update docs to reflect changes in Angular CLI version 17.1 (#53949)
This changes include mentioning the experimental application builder migration and changes to the `outputPath` and `optimization` option.

PR Close #53949
2024-01-23 18:56:19 +01:00
Younes Jaaidi 5f9cac037b test(core): fix test with false-negative risk (#54025)
change assertion to handle the case where the function doesn't throw

PR Close #54025
2024-01-23 16:58:57 +01:00
Dylan Hunn 9778326f10 refactor(compiler): ICUs should roll up to the root i18n block for application (#54026)
Previously, if an ICU was inside a nested i18n root, it would use the nested root to calculate whether it should be applied. Now, we use the root i18n block.

PR Close #54026
2024-01-23 15:09:37 +01:00
Aristeidis Bampakos fac5a7cf4e docs: update Aristeidis Bampakos bio (#51779)
PR Close #51779
2024-01-23 10:22:23 +01:00
Matthieu Riegler ae145d19c6 refactor(devtools): migrate to standalone (#53998)
Migrated with the schematics and cleanup by hand.

PR Close #53998
2024-01-23 09:53:24 +01:00
Matthieu Riegler 30b6d34a49 refactor(devtools): remove date utility. (#53997)
We can use the `toIsoString()` function instead !

PR Close #53997
2024-01-23 09:52:14 +01:00
Srinath Vayalpati 3e0b7c1bdc docs: fixed req.method in http client testing (#53395)
PR Close #53395
2024-01-23 09:51:00 +01:00
ilir.beqiri a58a35dfe4 docs: fix typo at component template metadata property (#54015)
Rename the component metadata property, templateUrl to template in the reactive forms tutorial step

PR Close #54015
2024-01-23 09:47:43 +01:00
Matthieu Riegler a7c9fbb087 docs: update version compatibility table for aio & adev (#54008)
PR Close #54008
2024-01-23 09:46:57 +01:00
divyasudagoni 20811aad27 docs: move import statement of ngmodule from standalone component to module code #53952 (#54016)
PR Close #54016
2024-01-22 19:43:37 +01:00
Joey Perrott df453c3549 refactor: migrate bazel, benchpress, elements and misc to prettier formatting (#53995)
Migrate formatting to prettier for bazel, benchpress, elements and misc from clang-format

PR Close #53995
2024-01-22 09:07:16 +01:00
Joey Perrott 1f6c96a1dc refactor: migrate animations to prettier formatting (#53977)
Migrate formatting to prettier for animations from clang-format

PR Close #53977
2024-01-19 20:08:58 +01:00
Joey Perrott 656549d88a refactor(devtools): migrate devtools to prettier formatting (#53945)
Migrate formatting to prettier for devtools from clang-format

PR Close #53945
2024-01-19 19:09:54 +01:00
Tomasz Ducin c0f0baac58 refactor(devtools): improving type safety (#53436)
This PR reduces the number of unnecessary `any` occurrences in devtools packages.

PR Close #53436
2024-01-19 17:35:26 +01:00
Paul d38aa4f103 docs: update start-data.md (#52577)
PR Close #52577
2024-01-19 17:34:45 +01:00
Matthieu Riegler 7397b6648a build: Adding myself for devtools approval (#53989)
This to help Alex with the reviewing of devtools PRs.

PR Close #53989
2024-01-19 17:33:21 +01:00
Paul Gschwendtner 0fe9331958 build: update g3 sync config to match internal one (#53987)
There were some changes to the Copybara config that should
be reflected here as well.

PR Close #53987
2024-01-19 17:18:11 +01:00
Andrew Scott 7620f506c0 refactor(core): node removal notifies scheduler only when animations are enabled (#53857)
Node removal is immediate and does not require change detection to run
when animations are not provided. This refactor makes the animation
engine notify the scheduler rather than doing it on all node removals.

PR Close #53857
2024-01-19 10:28:25 +01:00
Joey Perrott 62ad2f0c71 refactor: migrate scripts to prettier formatting (#53976)
Migrate formatting to prettier for scripts from clang-format

PR Close #53976
2024-01-19 10:22:07 +01:00
Jessica Janiuk 424a26b3c7 docs: updates defer guide to reflect updated default defer block test behavior (#53975)
This updates the deferrable view guide to specify playthrough as the default with examples of configuring to manual.

PR Close #53975
2024-01-19 10:16:36 +01:00
Joey Perrott 18bd1a5ccb build: update version of @angular/docs (#53974)
Update docs to later version

PR Close #53974
2024-01-18 18:49:58 +01:00
Joey Perrott 8922a11693 ci: provide proper location for artifacts to allow for firebase to find the artifacts for deployment (#53960)
Move the artifacts during deployment into adev/ so that its at the same level as the firebase.json file.

PR Close #53960
2024-01-18 18:08:30 +01:00
Ankita Sood 4e0341050b docs: add ankita (#52319)
add Ankita as GDE in contributors.json

PR Close #52319
2024-01-18 14:28:35 +01:00
lilbeqiri 0616dc2839 build(devtools): prevent underscores from being added at build time (#53921)
Add the keepNames config property to esbuild in order to prevent prepending of underscores to the class names.

PR Close #53921
2024-01-18 10:22:13 +01:00
Sasidharan SD dc228c6fc1 docs: fix broken code example (#53531)
PR Close #53531
2024-01-18 10:16:20 +01:00
Sasidharan SD b1a7249c24 docs: fix spelling errors in tutorials steps (#53840)
PR Close #53840
2024-01-18 10:14:48 +01:00
Matthieu Riegler 9ff4893bc0 refactor(devtools): Add support for signals. (#53269)
The devtools now support signals.
Writable signals of primitives are editable.
Object Signal and other non-writable signals (like computed) are not editable.

Co-authored-by: Tomasz Ducin <tomasz.ducin@gmail.com>

PR Close #53269
2024-01-17 16:47:17 -08:00
Matthieu Riegler 025f7af694 refactor(compiler): remove mentions of unused compiler options. (#53746)
Those options where removed in #49672.

PR Close #53746
2024-01-17 16:41:35 -08:00
macvag 1c4a99a6d1 docs: fix broken hyperlink in tutorials first app search (#53816)
PR Close #53816
2024-01-17 16:40:06 -08:00
Dylan Hunn 51d53fc728 release: cut the v17.1.0 release 2024-01-17 14:45:22 -08:00
Dylan Hunn be882c3efb Revert "refactor(core): improve forwardRef typings (#53880)"
This reverts commit af6f6e6448.
2024-01-17 13:57:11 -08:00
cexbrayat 18fa6d7556 refactor(compiler-cli): interpolatedSignalNotInvoked diagnostic for input signals (#53883)
This updates the extended diagnotic to handle input signals as well.

PR Close #53883
2024-01-17 11:36:43 -08:00
Matthieu Riegler bb8a5a4c70 refactor(router): Use Nullish coalescing assignment when possible. (#53930)
Inspired by #53923, lets simplify code when possible.

PR Close #53930
2024-01-17 11:36:13 -08:00
Joey Perrott 9c00506a26 ci: use ng-dev-previews project for previews (#53958)
Rather than use the actual adev site, we should be using the ng-dev-preview site

PR Close #53958
2024-01-17 11:19:29 -08:00
Andrew Kushnir 79e8539ce2 refactor(compiler): extra diagnostics for @defer in local compilation mode (#53899)
This commit adds extra logic to produce a diagnostic in case `@Component.deferredImports` contain types from imports that also bring eager symbols. This would result in retaining a regular import and generating a dynamic import, which would not allow to defer-load dependencies.

PR Close #53899
2024-01-17 10:46:15 -08:00
Jessica Janiuk 6f79507ea7 fix(core): Change defer block fixture default behavior to playthrough (#53956)
This inverts the default behavior of test bed to use playthrough for defer blocks instead of manual.

fixes: #53686

PR Close #53956
2024-01-17 10:45:42 -08:00
Joey Perrott 6616019735 refactor: migrate modules to prettier formatting (#53954)
Migrate formatting to prettier for modules directory from clang-format

PR Close #53954
2024-01-17 09:42:01 -08:00
Matthieu Riegler 4d4858fa28 docs: add platform-browser/animations/async entry to AIO (#53914)
fixes #53912

PR Close #53914
2024-01-17 09:14:44 -08:00
Joey Perrott 538fe47d9d ci: use the config from adev for adev previews (#53955)
Properly use the adev firebase config.

PR Close #53955
2024-01-17 09:13:44 -08:00
Andrew Kushnir b07c549802 refactor(compiler): enable register and resolve phases for local compilation (#53901)
This commit update the logic to enable `register` and `resolve` phases for local compilation. Those phases will be useful for local compilation in certain cases (will be used in followup PRs).

PR Close #53901
2024-01-17 09:03:57 -08:00
Joey Perrott 6f6ad0279b ci: set up previews of adev (#53941)
Set up preview actions to allow for a preview of the adev site to be deployed for a PR with the `adev: preview` label.

PR Close #53941
2024-01-16 19:18:33 -08:00
Joey Perrott d08f91f861 ci: run adev tests on CI (#53941)
Run tests in adev for CI

PR Close #53941
2024-01-16 19:18:32 -08:00
Joey Perrott 28aa7b3188 refactor: migrate tools to prettier formatting (#53947)
Migrate formatting to prettier for tools directory from clang-format

PR Close #53947
2024-01-16 19:17:51 -08:00
AleksanderBodurri 6ac0a68d73 docs(devtools): update development docs (#53948)
These docs are out of date with how we currently build devtools in dev mode and as a browser extension. This commit brings these docs up to date.

PR Close #53948
2024-01-16 19:17:20 -08:00
AleksanderBodurri ea19bff302 docs(devtools): remove devtools/CONTRIBUTING.md (#53948)
This is an artifact left over form the repo merge. There is already an existing CONTRIBUTING.md for this repo

PR Close #53948
2024-01-16 19:17:20 -08:00
Pawel Kozlowski 1112e9196a refactor(core): re-organize queries code (#53922)
This commit splits the query implementation and instructions
into a separate files. This is a pattern frequently used by
other functional areas of the framework and is a preparation for
introducing queries-as-signals where we are going to see more
instructions delegating to the same core functionality.

PR Close #53922
2024-01-16 16:00:36 -08:00
Ilija Leko 0d8a5c4665 docs: fix-build-path-in-http-server-command (#53226)
PR Close #53226
2024-01-16 09:37:12 -08:00
Paul Gschwendtner 76ceebad04 fix(compiler-cli): do not throw fatal error if extended type check fails (#53896)
Currently when the extended type check fails due to an import reference
that cannot be generated, the fatal diagnostic is not caught and
not properly exposed as a `ts.Diagnostic` that can be gracefully
handled. This is inconsistent to non-extended type checking diagnostics.

This is problematic because Angular CLI applications currently fail in
obscure ways because:

- the CLI does not expect `getDiagnosticsForFile` to actually throw
  runtime errors.
- the CLI does not seem to properly print these errors given the
  parallel workers and build excection, and those errors are
  especially hard to debug because there is no `stack` for
  `FatalDiagnosticError`'s.

Example: `MyDir` is not exported and the type check block cannot reference it.

PR Close #53896
2024-01-16 09:36:36 -08:00
Joey Perrott 99c2d76651 build(docs-infra): enable prerendering for adev (#53871)
Enable the prerender option for adev's angular.json file.

PR Close #53871
2024-01-16 09:35:46 -08:00
Sasidharan SD 246cb9c381 docs: remove repeated sentence (#53853)
PR Close #53853
2024-01-12 10:26:33 -08:00
Pawel Kozlowski d7c75c6edb refactor(core): improve forwardRef typings (#53880)
This commit improves the forwardRef typings for better
type safety and inference.

PR Close #53880
2024-01-12 10:26:02 -08:00
Andrew Scott 0daca457bb fix(core): TestBed should still use the microtask queue to schedule effects (#53843)
Prior to this commit, `TestBed` would require tests call `flushEffects`
or `fixture.detectChanges` in order to execute effects. In general, we
want to discourage authoring tests like this because it makes the timing
of change detection and effects differ from what happens in the
application. Instead, developers should perform actions and `await` (or
`flush`/`tick` when using `fakeAsync`) some `Promise` so that Angular
can react to the changes in the same way that it does in the
application.

Note that this still _allows_ developers to flush effects synchronously
with `flushEffects` and `detectChanges` but also enables the <action>,
`await` pattern described above.

PR Close #53843
2024-01-11 12:05:58 -08:00
Andrew Kushnir 12f9ed01cc refactor(compiler-cli): add tests for defer blocks and local compilation (#53591)
This commit adds tests to cover local compilation support for `@defer` blocks.

PR Close #53591
2024-01-10 15:29:00 -08:00
Andrew Kushnir 69713413f7 refactor(compiler): update TemplateDefinitionBuilder to support local compilation (#53591)
This commit updates the logic of the `TemplateDefinitionBuilder` to support local compilation and generate a single dependency function for all explicitly deferred deps within a component.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir 43d6816f9e refactor(compiler-cli): add checks to prevent same deps in imports and deferredImports (#53591)
This commit updates the typechecker logic to prevent same deps in `imports` and `deferredImports`.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir 4c4fea1304 refactor(compiler): add support for internal deferredImports field (#53591)
This commit updates the logic to add support for internal `deferredImports` field in compiler.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir c6df89c827 refactor(compiler-cli): update DeferredSymbolTracker to use explicit deps config (#53591)
This commit updates the `DeferredSymbolTracker` class to take info account the `onlyExplicitDeferDependencyImports` flag. The `DeferredSymbolTracker` class also exposes a new API to register import declarations as explicitly deferred, which will be used in followup commits.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir 9cae9b4a8c refactor(compiler-cli): add an internal config to enforce explicit deps in @defer for local compilation mode (#53591)
This commit adds an internal config option to enforce explicit deps in `@defer` for local compilation mode.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir 29b959ce31 refactor(compiler): add an internal API to check is an element is inside a defer block (#53591)
This commit updates the logic to expose an internal API on the `R3BoundTarget` to check if an element belongs to any defer block.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir a24b24e8a4 refactor(compiler-cli): store full Pipe metadata in typechecking code (#53591)
This commit updates typechecker to store full Pipe metadata in internal data strctures, so that this information is available to more places in the code, which will be updated in a followup commit.

PR Close #53591
2024-01-10 15:28:59 -08:00
Andrew Kushnir f51c008744 refactor(compiler-cli): extract various logic into separate functions (#53591)
This commit updates a few places to extract the logic into a separate functions which will be reused in a few places in followup commits.

PR Close #53591
2024-01-10 15:28:59 -08:00
Joey Perrott 3dedd6a07a build: set up adev testing (#53854)
Set up testing for the adev directory

PR Close #53854
2024-01-10 15:22:37 -08:00
Andrew Scott 881376eb42 release: cut the v17.1.0-rc.0 release 2024-01-10 14:25:36 -08:00
606 changed files with 25034 additions and 19436 deletions
+38
View File
@@ -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'
+50
View File
@@ -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}}'
+4 -2
View File
@@ -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
View File
@@ -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,
-2
View File
@@ -31,8 +31,6 @@
"packages/http/**",
"**/.gitignore",
"**/.gitkeep",
"**/yarn.lock",
"**/package.json",
"**/third_party/**",
"**/tsconfig-build.json",
"**/tsconfig-tsec.json",
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"],
+73
View File
@@ -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"
}
]
}
}
+74
View File
@@ -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/&lt;project-name&gt;/browser
</docs-code>
+4 -1
View File
@@ -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 |
+1 -1
View File
@@ -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` |
+1
View File
@@ -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 |
+16 -20
View File
@@ -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
&lt;input type="text" placeholder="Filter by city" #filter&gt;
</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
View File
@@ -28,7 +28,10 @@
"resolveJsonModule": true,
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
"jsxFragmentFactory": "Fragment",
"paths": {
"@angular/*": ["../packages/*"],
},
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+4 -1
View File
@@ -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: `
+23 -21
View File
@@ -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/&lt;project-name&gt;/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/&lt;project-name&gt;/browser
</code-example>
+1
View File
@@ -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 |
+2 -2
View File
@@ -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';
+16 -4
View File
@@ -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

+8 -2
View File
@@ -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": {
+1 -1
View File
@@ -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>
+1
View File
@@ -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',
-229
View File
@@ -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
View File
@@ -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
View File
@@ -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 {}
+4 -4
View File
@@ -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();
};
@@ -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;
}
@@ -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;
}
@@ -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']);
@@ -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",
@@ -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%');
}
@@ -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