Compare commits

..

34 Commits

Author SHA1 Message Date
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
468 changed files with 21103 additions and 26119 deletions
+4 -4
View File
@@ -21,16 +21,16 @@ jobs:
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e52eb8237f2ed71195f87ce8046467a176568e58
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@e52eb8237f2ed71195f87ce8046467a176568e58
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
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@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- 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}}'
+2 -2
View File
@@ -40,11 +40,11 @@ jobs:
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- 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-public-dir: './dist/bin/adev/build/browser'
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
with:
bazelrc: ./.bazelrc.user
@@ -34,7 +34,7 @@ jobs:
# the number of concurrent actions is determined based on the host resources.
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@7bd2697b40203c84826129b611e539fa663881f8
with:
workflow-artifact-name: 'aio'
pull-number: '${{github.event.pull_request.number}}'
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@7bd2697b40203c84826129b611e539fa663881f8
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'aio'
@@ -16,6 +16,6 @@ jobs:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/branch-manager@7bd2697b40203c84826129b611e539fa663881f8
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
with:
bazelrc: ./.bazelrc.user
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
# Checking out the pull request commit is intended here as we need to run the changed code tests.
@@ -31,7 +31,7 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Starting Saucelabs tunnel service
run: ./tools/saucelabs/sauce-service.sh run &
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
+28 -28
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
@@ -60,13 +60,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Run unit tests
@@ -78,13 +78,13 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules
run: yarn install --frozen-lockfile --network-timeout 100000
- name: Run CI tests for framework
@@ -94,15 +94,15 @@ jobs:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with upstream packages
@@ -115,11 +115,11 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e52eb8237f2ed71195f87ce8046467a176568e58
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@e52eb8237f2ed71195f87ce8046467a176568e58
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
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
@@ -130,15 +130,15 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with local packages
@@ -153,15 +153,15 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Set the stable branch environment variable
@@ -197,13 +197,13 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules
run: yarn install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -215,7 +215,7 @@ jobs:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
@@ -223,9 +223,9 @@ jobs:
./packages/zone.js/node_modules
./packages/zone.js/test/typings/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules
run: yarn install --frozen-lockfile
- run: |
@@ -262,7 +262,7 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
# Checking out the pull request commit is intended here as we need to run the changed code tests.
@@ -270,9 +270,9 @@ jobs:
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Set up Sauce Tunnel Daemon
run: yarn bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
env:
+2 -2
View File
@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/commit-message-based-labels@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/commit-message-based-labels@7bd2697b40203c84826129b611e539fa663881f8
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
post_approval_changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/post-approval-changes@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/post-approval-changes@7bd2697b40203c84826129b611e539fa663881f8
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,6 +14,6 @@ jobs:
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/feature-request@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/feature-request@7bd2697b40203c84826129b611e539fa663881f8
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: angular/dev-infra/github-actions/google-internal-tests@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/google-internal-tests@7bd2697b40203c84826129b611e539fa663881f8
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
- uses: angular/dev-infra/github-actions/unified-status-check@7bd2697b40203c84826129b611e539fa663881f8
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+4 -4
View File
@@ -22,15 +22,15 @@ jobs:
version: ['https://next.angular.io/', 'https://angular.io/']
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/setup@7bd2697b40203c84826129b611e539fa663881f8
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/bazel/configure-remote@7bd2697b40203c84826129b611e539fa663881f8
- name: Install node modules in aio
run: yarn install --frozen-lockfile --cwd aio
- name: Run basic e2e and deployment config tests.
@@ -44,7 +44,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@7bd2697b40203c84826129b611e539fa663881f8
with:
cache-node-modules: true
node-module-directories: |
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
env:
ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN: ${{ secrets.ANGULAR_CLI_BUILDS_READONLY_GITHUB_TOKEN }}
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/create-pr-for-changes@7bd2697b40203c84826129b611e539fa663881f8
with:
branch-prefix: update-cli-help
pr-title: 'docs: update Angular CLI help [${{github.ref_name}}]'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- name: Generate `events.json`
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@ae9155d1083fd8390e98bfe09a5ccf89f0375b52
uses: angular/dev-infra/github-actions/create-pr-for-changes@7bd2697b40203c84826129b611e539fa663881f8
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
-14
View File
@@ -7,15 +7,8 @@ export const format: FormatConfig = {
'prettier': {
'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': {
@@ -44,15 +37,8 @@ export const format: FormatConfig = {
'!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,6 +31,8 @@
"packages/http/**",
"**/.gitignore",
"**/.gitkeep",
"**/yarn.lock",
"**/package.json",
"**/third_party/**",
"**/tsconfig-build.json",
"**/tsconfig-tsec.json",
-2
View File
@@ -1150,7 +1150,6 @@ groups:
- josephperrott
- mgechev
- twerske
- ~jeanmeche
# =========================================================
# Dev-infra
@@ -1395,7 +1394,6 @@ groups:
- devversion # Paul Gschwendtner
- dgp1130 # Doug Parker
- dylhunn # Dylan Hunn
- jeanmeche # Matthieu Riegler
- jelbourn # Jeremy Elbourn
- jessicajaniuk # Jessica Janiuk
- JiaLiPassion # Jia Li
-19
View File
@@ -61,25 +61,6 @@ Get started with Angular, learn the fundamentals and explore advanced topics on
- [Lazy Loading][lazyloading]
- [Animations][animations]
### Local Development
To contribute to Angular docs, you can setup a local environment with the following commands:
```bash
# Clone Angular repo
git clone https://github.com/angular/angular.git
# Navigate to project directory
cd angular
# Install dependencies
yarn
# Build and run local dev server
# Note: Initial build will take some time
yarn docs
```
## Development Setup
### Prerequisites
+1 -1
View File
@@ -1,7 +1,7 @@
{
"hosting": {
"target": "angular-docs",
"public": "./build/browser",
"public": "../dist/bin/adev/build/browser",
"ignore": ["**/.*"],
"headers": [
{
@@ -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`.
IMPORTANT: The hashes are different now, because the content of the files changed. The old version is no longer available on the server.
IMPORANT: 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.
+1 -4
View File
@@ -240,13 +240,10 @@ 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 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.
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.
```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 you are in an injection context, will allow you to use the [`inject`](api/core/inject) function to inject instances.
Knowing when your are in an injection context, will allow you to use the [`inject`](api/core/inject) function to inject instances.
## Class constructors
@@ -33,7 +33,6 @@ 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) => {
+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.request.method).toBe('GET');
expect(req.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`, `outputPath`, `styles`, and `scripts` options can have either simple path string values, or object values with specific fields.
The `assets`, `index`, `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,7 +418,6 @@ 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
@@ -509,16 +508,3 @@ 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,7 +9,6 @@ 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 |
+20 -16
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 adjustments will be needed to update the application server to support the new integrated SSR capabilities.
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.
The `application` builder now provides the integrated functionality for all of the following preexisting builders:
- `app-shell`
@@ -117,23 +117,25 @@ 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, run the experimental `use-application-builder` migration.
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.
<docs-code language="shell">
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.
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.
In the future, a schematic will make this migration process easier for existing applications.
## Executing a build
@@ -173,6 +175,8 @@ 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"/>
IMPORTANT: Do not remove the `@Component` decorator, you will update that code in an upcoming step.
IMPORANT: 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 referred 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 refered 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/templates) to get access to the `input` element as its value.
This example uses a [template reference variable](/guide/templatess) 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 language="javascript">
<docs-code anguage="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 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.
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.
<docs-code language="html" highlight="[5]">
@defer {
@@ -18,7 +18,7 @@ import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
template: `
templateUrl: `
<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 documentation](/guide/templates/property-binding).
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).
@@ -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 occurred. 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 occured. In this scenario components need to communicate with parent components.
Angular uses the `@Output` decorator to enable this type of behavior.
+1 -4
View File
@@ -240,13 +240,10 @@ 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 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.
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.
```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: `
+21 -23
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` | move option to `outputPath.media` | defaults to `media` |
| `resourcesOutputPath` | remove option | always `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 adjustments will be needed to update the application server to support the new integrated SSR capabilities.
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.
The `application` builder now provides the integrated functionality for all of the following preexisting builders:
* `app-shell`
@@ -124,27 +124,25 @@ 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, run the experimental `use-application-builder` migration.
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.
<code-example language="shell">
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.
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>
In the future, a schematic will make this migration process easier for existing applications.
## Executing a build
-1
View File
@@ -10,7 +10,6 @@ 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 |
+1 -1
View File
@@ -311,7 +311,6 @@ 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';
@@ -326,6 +325,7 @@ 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';
+4 -16
View File
@@ -246,11 +246,12 @@ 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`, `outputPath`, `styles`, and `scripts` options can have either simple path string values, or object values with specific fields.
The `assets`, `index`, `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.
@@ -426,7 +427,7 @@ See also [Using runtime-global libraries inside your application](guide/using-li
### Optimization configuration
The `optimization` option can be either a Boolean or an Object for more fine-tune configuration.
The `optimization` browser builder 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-->
@@ -453,9 +454,8 @@ 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,18 +539,6 @@ 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.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 26 KiB

+2 -8
View File
@@ -108,13 +108,6 @@
"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",
@@ -163,7 +156,8 @@
"name": "Aristeidis Bampakos",
"picture": "aristeidis.jpg",
"twitter": "abampakos",
"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.",
"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.",
"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 `OnInit`, `Observable` and `CartService`.
1. In `shipping.component.ts`, import `CartService`.
<code-example header="src/app/shipping/shipping.component.ts" path="getting-started/src/app/shipping/shipping.component.ts" region="imports"></code-example>
+19 -19
View File
@@ -61,33 +61,33 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0-rc.0",
"@angular/cdk": "17.1.0-rc.0",
"@angular/common": "17.1.0-rc.0",
"@angular/compiler": "17.1.0-rc.0",
"@angular/core": "17.1.0-rc.0",
"@angular/elements": "17.1.0-rc.0",
"@angular/forms": "17.1.0-rc.0",
"@angular/material": "17.1.0-rc.0",
"@angular/platform-browser": "17.1.0-rc.0",
"@angular/platform-browser-dynamic": "17.1.0-rc.0",
"@angular/router": "17.1.0-rc.0",
"@angular/service-worker": "17.1.0-rc.0",
"@angular/animations": "17.1.0-next.5",
"@angular/cdk": "17.1.0-next.3",
"@angular/common": "17.1.0-next.5",
"@angular/compiler": "17.1.0-next.5",
"@angular/core": "17.1.0-next.5",
"@angular/elements": "17.1.0-next.5",
"@angular/forms": "17.1.0-next.5",
"@angular/material": "17.1.0-next.3",
"@angular/platform-browser": "17.1.0-next.5",
"@angular/platform-browser-dynamic": "17.1.0-next.5",
"@angular/router": "17.1.0-next.5",
"@angular/service-worker": "17.1.0-next.5",
"rxjs": "~7.8.0",
"safevalues": "^0.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/architect-cli": "0.1701.0-rc.1",
"@angular-devkit/build-angular": "17.1.0-rc.1",
"@angular-devkit/architect-cli": "0.1701.0-rc.0",
"@angular-devkit/build-angular": "17.1.0-rc.0",
"@angular-eslint/builder": "16.0.3",
"@angular-eslint/eslint-plugin": "16.0.3",
"@angular-eslint/eslint-plugin-template": "16.0.3",
"@angular-eslint/template-parser": "^16.0.3",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#8a32f9fd9af99832f337db9fe29842797121e96d",
"@angular/cli": "17.1.0-rc.1",
"@angular/compiler-cli": "17.1.0-rc.0",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#6e00fd0afb199cc491e800986970ee6df67d8226",
"@angular/cli": "17.1.0-rc.0",
"@angular/compiler-cli": "17.1.0-next.5",
"@bazel/bazelisk": "^1.7.5",
"@bazel/buildozer": "^6.0.0",
"@bazel/ibazel": "^0.16.2",
@@ -110,8 +110,8 @@
"dgeni": "^0.4.14",
"dgeni-packages": "^0.30.0",
"entities": "^4.0.0",
"eslint": "8.56.0",
"eslint-plugin-import": "2.29.1",
"eslint": "8.51.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jasmine": "4.1.3",
"eslint-plugin-jsdoc": "46.8.2",
"eslint-plugin-prefer-arrow": "1.2.3",
@@ -13,25 +13,25 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/router": "17.1.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/router": "17.0.8",
"angular": "^1.8.3",
"angular-in-memory-web-api": "~0.17.0",
"angular-in-memory-web-api": "~0.16.0",
"angular-route": "1.8.3",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@types/angular": "^1.8.4",
"@types/angular-route": "^1.7.2",
"@types/jasmine": "~5.1.0",
@@ -13,23 +13,23 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/router": "17.1.0",
"angular-in-memory-web-api": "~0.17.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/router": "17.0.8",
"angular-in-memory-web-api": "~0.16.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"copyfiles": "^2.4.1",
@@ -13,24 +13,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/elements": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/router": "17.1.0",
"angular-in-memory-web-api": "~0.17.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/elements": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/router": "17.0.8",
"angular-in-memory-web-api": "~0.16.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"jasmine-core": "~5.1.0",
@@ -15,24 +15,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/localize": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/router": "17.1.0",
"angular-in-memory-web-api": "~0.17.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/localize": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/router": "17.0.8",
"angular-in-memory-web-api": "~0.16.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"jasmine-core": "~5.1.0",
@@ -13,24 +13,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/router": "17.1.0",
"@angular/service-worker": "17.1.0",
"angular-in-memory-web-api": "~0.17.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/router": "17.0.8",
"@angular/service-worker": "17.0.8",
"angular-in-memory-web-api": "~0.16.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
"jasmine-core": "~5.1.0",
@@ -25,15 +25,15 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/router": "17.1.0",
"@angular/upgrade": "17.1.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/router": "17.0.8",
"@angular/upgrade": "17.0.8",
"core-js": "^3.0.0",
"rxjs": "~7.8.0",
"systemjs": "~0.19.39",
@@ -42,7 +42,7 @@
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular/compiler-cli": "17.1.0",
"@angular/compiler-cli": "17.0.8",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^13.2.0",
"@types/angular": "^1.8.4",
@@ -13,26 +13,26 @@
},
"private": true,
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/platform-server": "17.1.0",
"@angular/router": "17.1.0",
"@angular/ssr": "17.1.0",
"angular-in-memory-web-api": "~0.17.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/platform-server": "17.0.8",
"@angular/router": "17.0.8",
"@angular/ssr": "17.0.9",
"angular-in-memory-web-api": "~0.16.0",
"express": "^4.18.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@types/express": "^4.17.13",
"@types/jasmine": "~5.1.0",
"@types/node": "^16.11.35",
+19 -19
View File
@@ -19,22 +19,22 @@
"license": "MIT",
"repository": {},
"dependencies": {
"@angular/animations": "17.1.0",
"@angular/common": "17.1.0",
"@angular/compiler": "17.1.0",
"@angular/core": "17.1.0",
"@angular/elements": "17.1.0",
"@angular/forms": "17.1.0",
"@angular/localize": "17.1.0",
"@angular/platform-browser": "17.1.0",
"@angular/platform-browser-dynamic": "17.1.0",
"@angular/platform-server": "17.1.0",
"@angular/router": "17.1.0",
"@angular/service-worker": "17.1.0",
"@angular/ssr": "17.1.0",
"@angular/upgrade": "17.1.0",
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/elements": "17.0.8",
"@angular/forms": "17.0.8",
"@angular/localize": "17.0.8",
"@angular/platform-browser": "17.0.8",
"@angular/platform-browser-dynamic": "17.0.8",
"@angular/platform-server": "17.0.8",
"@angular/router": "17.0.8",
"@angular/service-worker": "17.0.8",
"@angular/ssr": "17.0.9",
"@angular/upgrade": "17.0.8",
"angular": "^1.8.3",
"angular-in-memory-web-api": "~0.17.0",
"angular-in-memory-web-api": "~0.16.0",
"angular-route": "1.8.3",
"core-js": "^3.0.0",
"express": "^4.18.1",
@@ -45,9 +45,9 @@
"zone.js": "~0.14.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.1.0",
"@angular/cli": "17.1.0",
"@angular/compiler-cli": "17.1.0",
"@angular-devkit/build-angular": "17.0.9",
"@angular/cli": "17.0.9",
"@angular/compiler-cli": "17.0.8",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^13.2.0",
"@types/angular": "^1.8.4",
@@ -57,7 +57,7 @@
"@types/angular-route": "^1.7.2",
"@types/express": "^4.17.13",
"@types/jasmine": "~5.1.0",
"@types/jquery": "3.5.29",
"@types/jquery": "3.5.23",
"@types/node": "^16.11.35",
"canonical-path": "1.0.0",
"concurrently": "^8.0.0",
File diff suppressed because it is too large Load Diff
+1178 -806
View File
File diff suppressed because it is too large Load Diff
@@ -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,29 +19,27 @@ 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,100 +52,86 @@ 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,21 +21,20 @@ 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,4 +17,5 @@ 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,10 +2,3 @@
<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,21 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {JsonPipe} from '@angular/common';
import {
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
EventEmitter,
inject,
Injector,
Input,
Output,
signal,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, EventEmitter, inject, Injector, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core';
import {createCustomElement} from '@angular/elements';
import {RouterOutlet} from '@angular/router';
import {initializeMessageBus} from 'ng-devtools-backend';
@@ -36,8 +22,8 @@ import {ZippyComponent} from './zippy.component';
styleUrls: ['./demo-app.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [HeavyComponent, RouterOutlet, JsonPipe],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [HeavyComponent, RouterOutlet],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class DemoAppComponent {
@ViewChild(ZippyComponent) zippy!: ZippyComponent;
@@ -49,21 +35,12 @@ 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';
}
@@ -84,10 +61,5 @@ export const ROUTES = [
},
];
initializeMessageBus(
new ZoneUnawareIFrameMessageBus(
'angular-devtools-backend',
'angular-devtools',
() => window.parent,
),
);
initializeMessageBus(new ZoneUnawareIFrameMessageBus(
'angular-devtools-backend', 'angular-devtools', () => window.parent));
@@ -18,7 +18,8 @@ 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: {
@@ -26,12 +27,14 @@ 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,6 +18,7 @@ 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,10 +6,12 @@
* 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 {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatFormFieldModule} from '@angular/material/form-field';
export interface DialogData {
animal: string;
@@ -19,7 +21,7 @@ export interface DialogData {
@Component({
selector: 'app-dialog',
standalone: true,
imports: [MatDialogModule, MatFormField, MatLabel, FormsModule],
imports: [MatDialogModule, MatFormFieldModule, FormsModule],
template: `
<h1 mat-dialog-title>Hi {{ data.name }}</h1>
<div mat-dialog-content>
@@ -37,9 +39,8 @@ 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: unknown) {
transform(val: any): void {
return val;
}
@@ -22,21 +22,17 @@ 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
@@ -46,7 +42,7 @@ export interface Todo {
(keydown.enter)="completeEdit($any($event.target).value)"
/>
</li>
`,
`
})
export class TodoComponent {
@Input() todo!: Todo;
@@ -7,16 +7,7 @@
*/
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';
@@ -66,25 +57,19 @@ 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>
@@ -95,7 +80,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,6 +6,7 @@
* 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';
@@ -18,9 +19,9 @@ import {DialogComponent} from './dialog.component';
@Component({
selector: 'app-todo-demo',
standalone: true,
imports: [RouterLink, RouterOutlet, MatDialogModule, FormsModule],
styles: [
`
imports:
[RouterLink, RouterOutlet, MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule],
styles: [`
nav {
padding-top: 20px;
padding-bottom: 10px;
@@ -36,8 +37,7 @@ 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,29 +11,27 @@ 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>
@@ -41,7 +39,7 @@ import {Component, Input} from '@angular/core';
<ng-content></ng-content>
</div>
</section>
`,
`
})
export class ZippyComponent {
@Input() title!: string;
@@ -7,51 +7,45 @@
*/
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: [DevToolsComponent],
imports: [NgDevToolsModule],
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 DemoDevToolsComponent {
messageBus: IFrameMessageBus | null = null;
export class DevToolsComponent {
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,6 +16,7 @@ import {DemoApplicationOperations} from '../../../src/demo-application-operation
import {AppComponent} from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideAnimations(),
@@ -23,7 +24,7 @@ bootstrapApplication(AppComponent, {
{
path: '',
loadComponent: () =>
import('./app/devtools-app/devtools-app.component').then((m) => m.DemoDevToolsComponent),
import('./app/devtools-app/devtools-app.component').then((m) => m.DevToolsComponent),
pathMatch: 'full',
},
{
@@ -39,5 +40,5 @@ bootstrapApplication(AppComponent, {
provide: ApplicationEnvironment,
useClass: DemoApplicationEnvironment,
},
],
]
});
@@ -124,7 +124,6 @@ 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,43 +6,12 @@
* 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';
@@ -50,15 +19,13 @@ 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 {hasDiDebugAPIs, runOutsideAngular} from './utils';
import {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));
@@ -87,8 +54,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'));
});
}
};
@@ -101,58 +68,55 @@ 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);
}
idToInjector.delete(injectorId);
}
}
injectorsSeen.clear();
injectorsSeen.clear();
if (!query) {
messageBus.emit('latestComponentExplorerView', [{forest}]);
return;
}
if (!query) {
messageBus.emit('latestComponentExplorerView', [{forest}]);
return;
}
const state = getLatestComponentState(
query,
initializeOrGetDirectiveForestHooks().getDirectiveForest(),
);
const state = getLatestComponentState(
query, initializeOrGetDirectiveForestHooks().getDirectiveForest());
if (state) {
const {directiveProperties} = state;
messageBus.emit('latestComponentExplorerView', [{forest, properties: directiveProperties}]);
}
};
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()]);
@@ -160,41 +124,33 @@ 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 =
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();
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);
}
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;
};
}
messageBus.emit('nestedProperties', [position, {props: serializeDirectiveState(data)}, propPath]);
return;
};
//
// Subscribe Helpers
@@ -253,46 +209,44 @@ 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),
} :
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;
};
serializedNodes.push(serializedNode);
if (includeResolutionPath) {
serializedNode.resolutionPath = getNodeDIResolutionPath(node);
}
}
return serializedNodes;
};
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[] | undefined {
function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]|undefined {
const nodeInjector = getInjectorFromElementNode(node.nativeElement!);
if (!nodeInjector) {
return [];
@@ -320,95 +274,91 @@ function getNodeDIResolutionPath(node: ComponentTreeNode): SerializedInjector[]
return serializedPath;
}
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 getInjectorProvidersCallback = (messageBus: MessageBus<Events>) =>
(injector: SerializedInjector) => {
if (!idToInjector.has(injector.id)) {
return;
}
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(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);
}
}
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
};
const serializedProviderRecords: SerializedProviderRecord[] = [];
const logProvider = (
serializedInjector: SerializedInjector,
serializedProvider: SerializedProviderRecord,
): void => {
if (!idToInjector.has(serializedInjector.id)) {
return;
}
for (const [token, records] of tokenToRecords.entries()) {
const multiRecords = records.filter(record => record.multi);
const nonMultiRecords = records.filter(record => !record.multi);
const injector = idToInjector.get(serializedInjector.id)!;
for (const record of nonMultiRecords) {
serializedProviderRecords.push(record);
}
const providerRecords = getInjectorProviders(injector);
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),
});
}
}
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);
messageBus.emit('latestInjectorProviders', [injector, serializedProviderRecords]);
};
if (typeof serializedProvider.index === 'number') {
const provider = providerRecords[serializedProvider.index];
const logProvider =
(serializedInjector: SerializedInjector, serializedProvider: SerializedProviderRecord):
void => {
if (!idToInjector.has(serializedInjector.id)) {
return;
}
// 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 injector = idToInjector.get(serializedInjector.id)!;
// 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}));
}
const providerRecords = getInjectorProviders(injector);
console.groupEnd();
};
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();
};
@@ -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,13 +28,11 @@ 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;
@@ -59,8 +57,7 @@ export class ComponentInspector {
if (this._selectedComponent.component && this._selectedComponent.host) {
this._onComponentSelect(
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!,
);
initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!);
}
}
@@ -76,8 +73,7 @@ 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)!);
}
}
@@ -97,7 +93,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,28 +5,11 @@
* 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
@@ -39,21 +22,9 @@ 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 {isSignal} from './utils';
import type {ClassProvider, ExistingProvider, FactoryProvider, InjectOptions, InjectionToken, Injector, Type, ValueProvider, ɵComponentDebugMetadata as ComponentDebugMetadata, ɵProviderRecord as ProviderRecord} from '@angular/core';
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>();
@@ -75,14 +46,12 @@ 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);
@@ -95,9 +64,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();
@@ -111,19 +80,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) {
@@ -136,9 +105,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,
};
}
@@ -154,7 +123,7 @@ export const getLatestComponentState = (
};
};
function serializeElementInjectorWithId(injector: Injector): SerializedInjector | null {
function serializeElementInjectorWithId(injector: Injector): SerializedInjector|null {
let id: string;
const element = getElementInjectorElement(injector);
@@ -176,7 +145,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 {
@@ -184,7 +153,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)) {
@@ -254,16 +223,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;
@@ -286,9 +255,8 @@ 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
@@ -345,7 +313,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) {
@@ -385,11 +353,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') {
@@ -409,11 +377,7 @@ 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),
@@ -424,9 +388,10 @@ export function serializeProviderRecord(
};
if (hasImportPath) {
serializedProvider['importPath'] = (providerRecord.importPath ?? []).map((injector) =>
valueToLabel(injector),
);
serializedProvider['importPath'] = (providerRecord.importPath ?? [])
.map(
(injector) => valueToLabel(injector),
);
}
return serializedProvider;
@@ -435,8 +400,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 {
@@ -447,7 +412,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';
}
@@ -481,7 +446,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;
@@ -504,13 +469,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) {
@@ -522,16 +487,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());
};
@@ -541,9 +506,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;
}
@@ -572,27 +537,19 @@ 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 {
if (isSignal(parentObjectOfValueToUpdate[valueKey])) {
parentObjectOfValueToUpdate[valueKey].set(updatedStateData.newValue);
} else {
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
}
} catch {}
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,16 +9,12 @@
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,19 +28,20 @@ const TYPE = 1;
const ELEMENT = 0;
const LVIEW_TVIEW = 1;
// Big oversimplification of the LView structure.
type LView = Array<any>;
export const isLContainer = (value: unknown): boolean => {
export const isLContainer = (value: any): boolean => {
return Array.isArray(value) && value[TYPE] === true;
};
const isLView = (value: unknown): value is LView => {
const isLView = (value: any): boolean => {
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;
}
@@ -77,9 +78,9 @@ export class LTreeStrategy {
return typeof (element as any).__ngContext__ !== 'undefined';
}
private _getNode(lView: LView, data: any, idx: number): ComponentTreeNode {
private _getNode(lView: any, 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,60 +10,56 @@ 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,
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,
};
}),
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 (!(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;
};
}
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(): boolean {
return (['getDirectiveMetadata', 'getComponent', 'getDirectives'] as const).every(
(method) => typeof ngDebugClient()[method] === 'function',
);
supports(_: any): 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,27 +40,26 @@ 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';
}
@@ -98,12 +97,11 @@ 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;
}
@@ -139,7 +137,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,13 +6,7 @@
* 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';
@@ -65,20 +59,16 @@ 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) {
@@ -121,31 +111,22 @@ 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, {
@@ -158,12 +139,7 @@ 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) {
@@ -177,23 +153,19 @@ 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);
@@ -203,10 +175,8 @@ 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;
@@ -245,36 +215,33 @@ 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 = {
@@ -285,7 +252,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,8 +22,7 @@ interface TreeNode {
}
export type NodeArray = {
directive: any;
isComponent: boolean;
directive: any; isComponent: boolean;
}[];
export class IdentityTracker {
@@ -44,11 +43,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);
}
@@ -57,9 +56,7 @@ export class IdentityTracker {
}
index(): {
newNodes: NodeArray;
removedNodes: NodeArray;
indexedForest: IndexedNode[];
newNodes: NodeArray; removedNodes: NodeArray; indexedForest: IndexedNode[];
directiveForest: ComponentTreeNode[];
} {
const directiveForest = buildDirectiveForest();
@@ -81,11 +78,8 @@ 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);
@@ -119,10 +113,7 @@ 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,
@@ -135,5 +126,4 @@ 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,37 +15,32 @@ 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 {
@@ -111,11 +106,9 @@ 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 {
@@ -128,11 +121,9 @@ 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,11 +6,7 @@
* 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';
@@ -86,7 +82,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(() => {
@@ -126,7 +122,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,64 +11,32 @@ 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;
@@ -111,12 +79,8 @@ 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;
}
@@ -125,12 +89,8 @@ 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;
}
@@ -139,11 +99,7 @@ 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;
}
@@ -152,11 +108,7 @@ 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;
}
@@ -165,12 +117,8 @@ 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;
}
@@ -179,12 +127,8 @@ 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;
}
@@ -192,13 +136,8 @@ 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;
}
@@ -206,13 +145,7 @@ 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;
}
@@ -243,7 +176,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,9 +35,8 @@ 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) {
@@ -46,12 +45,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) {
@@ -67,4 +66,4 @@ const setDirectiveKey = (node: ComponentTreeNode | null, key: string) => {
};
const getConsoleReferenceWithIndexOf = (consoleReferenceIndex: number) =>
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
`${CONSOLE_REFERENCE_PREFIX}${consoleReferenceIndex}`;
@@ -6,8 +6,6 @@
* 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.
@@ -18,13 +16,12 @@ 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;
}
@@ -42,6 +39,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): PropertyDescriptor | undefined =>
Object.getOwnPropertyDescriptor(instance, propName) ||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
export const getDescriptor = (instance: any, propName: string) =>
Object.getOwnPropertyDescriptor(instance, propName) ||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName);
@@ -73,14 +73,11 @@ 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,8 +8,6 @@
import {PropType} from 'protocol';
import {isSignal} from '../utils';
const commonTypes = {
boolean: PropType.Boolean,
bigint: PropType.BigInt,
@@ -26,10 +24,6 @@ const commonTypes = {
* @see `devtools/projects/protocol`
*/
export const getPropType = (prop: unknown): PropType => {
if (isSignal(prop)) {
prop = prop();
}
if (prop === undefined) {
return PropType.Undefined;
}
@@ -6,30 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ContainerType, Descriptor, NestedProp, PropType} from 'protocol';
import {isSignal, unwrapSignal} from '../utils';
import {Descriptor, NestedProp, PropType} from 'protocol';
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;
@@ -40,15 +36,23 @@ interface LevelOptions {
level?: number;
}
const serializable: Set<PropType> = new Set([
PropType.Boolean,
PropType.String,
PropType.Null,
PropType.Number,
PropType.Object,
PropType.Undefined,
PropType.Unknown,
]);
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 typeToDescriptorPreview: Formatter<string> = {
[PropType.Array]: (prop: Array<unknown>) => `Array(${prop.length})`,
@@ -73,280 +77,211 @@ const typeToDescriptorPreview: Formatter<string> = {
[PropType.Unknown]: (_: any) => 'unknown',
};
type Key = string | number;
type NestedSerializerFn = (
instance: any,
propName: string | number,
nodes: NestedProp[],
isReadonly: boolean,
currentLevel: number,
level?: number,
) => Descriptor;
type Key = string|number;
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 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: PropertyDescriptor | undefined,
propName: string | number,
propData: TerminalType,
isGetterOrSetter: boolean,
) => {
if (propData.containerType === 'ReadonlySignal') {
return false;
}
const isEditable =
(descriptor: any, propName: string|number, propData: TerminalType,
isGetterOrSetter: boolean) => {
if (typeof propName === 'symbol') {
return false;
}
if (typeof propName === 'symbol') {
return false;
}
if (isGetterOrSetter) {
return false;
}
if (isGetterOrSetter) {
return false;
}
if (descriptor?.writable === false) {
return false;
}
if (descriptor?.writable === false) {
return false;
}
return shallowPropTypeToTreeMetaData[propData.type].editable;
};
return shallowPropTypeToTreeMetaData[propData.type].editable;
};
const isGetterOrSetter = (descriptor: any): boolean =>
(descriptor?.set || descriptor?.get) && !('value' in descriptor);
(descriptor?.set || descriptor?.get) && !('value' in 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 getPreview = (propData: TerminalType|CompositeType, isGetterOrSetter: boolean) => {
return !isGetterOrSetter ? typeToDescriptorPreview[propData.type](propData.prop) :
typeToDescriptorPreview[PropType.Function]({name: ''});
};
export function createShallowSerializedDescriptor(
instance: any,
propName: string | number,
propData: TerminalType,
isReadonly: boolean,
): Descriptor {
const {type, containerType} = propData;
export const createShallowSerializedDescriptor =
(instance: any, propName: string|number, propData: TerminalType): Descriptor => {
const {type} = propData;
const descriptor = getDescriptor(instance, propName as string);
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
const descriptor = getDescriptor(instance, propName as string);
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
const shallowSerializedDescriptor: Descriptor = {
type,
expandable: shallowPropTypeToTreeMetaData[type].expandable,
editable: isEditable(descriptor, propName, propData, getterOrSetter) && !isReadonly,
preview: getPreview(propData, getterOrSetter),
containerType,
};
const shallowSerializedDescriptor: Descriptor = {
type,
expandable: shallowPropTypeToTreeMetaData[type].expandable,
editable: isEditable(descriptor, propName, propData, getterOrSetter),
preview: getPreview(propData, getterOrSetter),
};
if (propData.prop !== undefined && serializable.has(type)) {
shallowSerializedDescriptor.value = unwrapSignal(propData.prop);
}
if (propData.prop !== undefined && serializable[type]) {
shallowSerializedDescriptor.value = propData.prop;
}
return shallowSerializedDescriptor;
}
return shallowSerializedDescriptor;
};
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;
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 descriptor = getDescriptor(instance, propName as string);
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
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,
};
const levelSerializedDescriptor: Descriptor = {
type,
editable: false,
expandable: !getterOrSetter && getKeys(prop).length > 0,
preview: getPreview(propData, getterOrSetter),
};
if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) {
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
if (value !== undefined) {
levelSerializedDescriptor.value = value;
}
}
if (levelOptions.level !== undefined && levelOptions.currentLevel < levelOptions.level) {
const value = getLevelDescriptorValue(propData, levelOptions, continuation);
if (value !== undefined) {
levelSerializedDescriptor.value = value;
}
}
return levelSerializedDescriptor;
}
return levelSerializedDescriptor;
};
export function createNestedSerializedDescriptor(
instance: {},
propName: string | number,
propData: CompositeType,
levelOptions: LevelOptions,
nodes: NestedProp[],
nestedSerializer: NestedSerializerFn,
): Descriptor {
const {type, prop, containerType} = propData;
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;
const descriptor = getDescriptor(instance, propName as string);
const getterOrSetter: boolean = isGetterOrSetter(descriptor);
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,
};
const nestedSerializedDescriptor: Descriptor = {
type,
editable: false,
expandable: !getterOrSetter && getKeys(prop).length > 0,
preview: getPreview(propData, getterOrSetter),
};
if (nodes?.length) {
const value = getNestedDescriptorValue(propData, levelOptions, nodes, nestedSerializer);
if (value !== undefined) {
nestedSerializedDescriptor.value = value;
}
}
return nestedSerializedDescriptor;
}
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: NestedSerializerFn,
) {
propData: CompositeType, levelOptions: LevelOptions, nodes: NestedProp[],
nestedSerializer: (
instance: any, propName: string|number, nodes: NestedProp[], currentLevel: number,
level?: number) => void) {
const {type, prop} = propData;
const {currentLevel} = levelOptions;
const value = unwrapSignal(prop);
switch (type) {
case PropType.Array:
return nodes.map((nestedProp) =>
nestedSerializer(value, nestedProp.name, nestedProp.children, false, currentLevel + 1),
);
return nodes.map(
(nestedProp) =>
nestedSerializer(prop, nestedProp.name, nestedProp.children, currentLevel + 1));
case PropType.Object:
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>,
);
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>);
}
}
function getLevelDescriptorValue(
propData: CompositeType,
levelOptions: LevelOptions,
continuation: (
instance: any,
propName: string | number,
isReadonly: boolean,
level?: number,
max?: number,
) => Descriptor,
) {
propData: CompositeType, levelOptions: LevelOptions,
continuation: (instance: any, propName: string|number, level?: number, max?: number) => void) {
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(value, idx, isReadonly, currentLevel + 1, level),
);
return prop.map((_: any, idx: number) => continuation(prop, idx, currentLevel + 1, level));
case PropType.Object:
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>,
);
return getKeys(prop).reduce((accumulator, propName) => {
if (!ignoreList.has(propName)) {
accumulator[propName] = continuation(prop, propName, currentLevel + 1, level);
}
return accumulator;
}, {} as Record<string, void>);
}
}
function truncate(str: string, max = 20): string {
return str.length > max ? str.substring(0, max) + '...' : str;
}
const truncate = (str: string, max = 20): string => {
if (str.length > max) {
return str.substring(0, max) + '...';
}
return str;
};

Some files were not shown because too many files have changed in this diff Show More